Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta http-equiv="content-type" content="text/html; charset=UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | |
| <title>Vehicle Routing - SolverForge for Python</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.2/styles/vis-timeline-graph2d.min.css" | |
| integrity="sha256-svzNasPg1yR5gvEaRei2jg+n4Pc3sVyMUWeS6xRAh6U=" crossorigin="anonymous"> | |
| <link rel="stylesheet" href="/webjars/solverforge/css/solverforge-webui.css"/> | |
| <link rel="icon" href="/webjars/solverforge/img/solverforge-favicon.svg" type="image/svg+xml"> | |
| <style> | |
| /* Customer marker icons */ | |
| .customer-marker, .vehicle-home-marker, .temp-vehicle-marker { | |
| background: transparent ; | |
| border: none ; | |
| } | |
| /* Pulse animation for new vehicle placement */ | |
| @keyframes pulse { | |
| 0% { transform: scale(1); box-shadow: 0 2px 4px rgba(0,0,0,0.4); } | |
| 50% { transform: scale(1.1); box-shadow: 0 4px 8px rgba(99, 102, 241, 0.6); } | |
| 100% { transform: scale(1); box-shadow: 0 2px 4px rgba(0,0,0,0.4); } | |
| } | |
| /* Customer type buttons in modal */ | |
| .customer-type-btn { | |
| transition: all 0.2s ease; | |
| padding: 0.75rem 0.5rem; | |
| } | |
| .customer-type-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.15); | |
| } | |
| /* Vehicle table styling */ | |
| #vehicles tr.vehicle-row { | |
| transition: background-color 0.2s ease; | |
| } | |
| #vehicles tr.vehicle-row:hover { | |
| background-color: rgba(99, 102, 241, 0.1); | |
| } | |
| #vehicles tr.vehicle-row.table-active { | |
| background-color: rgba(99, 102, 241, 0.15) ; | |
| } | |
| /* Route number markers */ | |
| .route-number-marker { | |
| background: transparent ; | |
| border: none ; | |
| } | |
| /* Notification panel */ | |
| #notificationPanel { | |
| z-index: 1050; | |
| } | |
| /* Click hint for vehicle rows */ | |
| #vehicles tr.vehicle-row td:not(:last-child) { | |
| cursor: pointer; | |
| } | |
| /* Solving spinner */ | |
| #solvingSpinner { | |
| display: none; | |
| width: 1.25rem; | |
| height: 1.25rem; | |
| border: 2px solid #10b981; | |
| border-top-color: transparent; | |
| border-radius: 50%; | |
| animation: spin 0.75s linear infinite; | |
| vertical-align: middle; | |
| } | |
| #solvingSpinner.active { | |
| display: inline-block; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* Progress bar text should stay horizontal and inside */ | |
| .progress-bar { | |
| overflow: visible; | |
| white-space: nowrap; | |
| } | |
| /* Map hint overlay */ | |
| .map-hint { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background-color: rgba(255, 255, 255, 0.95); | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.2); | |
| font-size: 0.9rem; | |
| color: #374151; | |
| z-index: 1000; | |
| pointer-events: none; | |
| transition: opacity 0.3s ease; | |
| } | |
| .map-hint i { | |
| color: #10b981; | |
| margin-right: 6px; | |
| } | |
| .map-hint.hidden { | |
| opacity: 0; | |
| } | |
| /* Timeline stop badges */ | |
| .timeline-stop-badge { | |
| background-color: #6366f1; | |
| color: white; | |
| padding: 1px 6px; | |
| border-radius: 10px; | |
| font-size: 0.7rem; | |
| font-weight: bold; | |
| margin-right: 4px; | |
| } | |
| .timeline-status-icon { | |
| margin-left: 4px; | |
| font-size: 0.85rem; | |
| } | |
| .timeline-status-ontime { color: #10b981; } | |
| .timeline-status-late { color: #ef4444; } | |
| .timeline-status-early { color: #3b82f6; } | |
| .vis-item .vis-item-content { | |
| font-size: 0.85rem; | |
| padding: 2px 4px; | |
| } | |
| .vis-labelset .vis-label { | |
| padding: 4px 8px; | |
| } | |
| /* Loading overlay */ | |
| .loading-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(255, 255, 255, 0.95); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 2000; | |
| transition: opacity 0.3s ease; | |
| } | |
| .loading-overlay.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .loading-content { | |
| text-align: center; | |
| padding: 2rem; | |
| } | |
| .loading-spinner { | |
| width: 60px; | |
| height: 60px; | |
| border: 4px solid #e5e7eb; | |
| border-top-color: #10b981; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 1.5rem; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* Real Roads toggle styling */ | |
| #realRoadRouting:checked { | |
| background-color: #10b981; | |
| border-color: #10b981; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header id="solverforge-auto-header"> | |
| <!-- Filled in by app.js --> | |
| </header> | |
| <div class="tab-content"> | |
| <div id="demo" class="tab-pane fade show active container-fluid"> | |
| <div class="sticky-top d-flex justify-content-center align-items-center"> | |
| <div id="notificationPanel" style="position: absolute; top: .5rem;"></div> | |
| </div> | |
| <h1>Vehicle routing with capacity and time windows</h1> | |
| <p>Generate optimal route plan of a vehicle fleet with limited vehicle capacity and time windows.</p> | |
| <div class="container-fluid mb-2"> | |
| <div class="row justify-content-start"> | |
| <div class="col-9"> | |
| <ul class="nav nav-pills col" role="tablist"> | |
| <li class="nav-item" role="presentation"> | |
| <button class="nav-link active" id="mapTab" data-bs-toggle="tab" data-bs-target="#mapPanel" | |
| type="button" | |
| role="tab" aria-controls="mapPanel" aria-selected="false">Map | |
| </button> | |
| </li> | |
| <li class="nav-item" role="presentation"> | |
| <button class="nav-link" id="byVehicleTab" data-bs-toggle="tab" data-bs-target="#byVehiclePanel" | |
| type="button" role="tab" aria-controls="byVehiclePanel" aria-selected="false">By vehicle | |
| </button> | |
| </li> | |
| <li class="nav-item" role="presentation"> | |
| <button class="nav-link" id="byVisitTab" data-bs-toggle="tab" data-bs-target="#byVisitPanel" | |
| type="button" role="tab" aria-controls="byVisitPanel" aria-selected="false">By visit | |
| </button> | |
| </li> | |
| </ul> | |
| </div> | |
| <div class="col-3"> | |
| <button id="solveButton" type="button" class="btn btn-success"> | |
| <i class="fas fa-play"></i> Solve | |
| </button> | |
| <button id="stopSolvingButton" type="button" class="btn btn-danger p-2"> | |
| <i class="fas fa-stop"></i> Stop solving | |
| </button> | |
| <span id="solvingSpinner" class="ms-2"></span> | |
| <span id="score" class="score ms-2 align-middle fw-bold">Score: ?</span> | |
| <button id="analyzeButton" type="button" class="ms-2 btn btn-secondary"> | |
| <span class="fas fa-question"></span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="tab-content"> | |
| <div class="tab-pane fade show active" id="mapPanel" role="tabpanel" aria-labelledby="mapTab"> | |
| <div class="row"> | |
| <div class="col-7 col-lg-8 col-xl-9 position-relative"> | |
| <div id="map" style="width: 100%; height: 100vh;"></div> | |
| <div id="mapHint" class="map-hint"> | |
| <i class="fas fa-mouse-pointer"></i> Click on the map to add a new visit | |
| </div> | |
| </div> | |
| <div class="col-5 col-lg-4 col-xl-3" style="height: 100vh; overflow-y: scroll;"> | |
| <div class="row pt-2 row-cols-1"> | |
| <div class="col"> | |
| <h5> | |
| Solution summary | |
| </h5> | |
| <table class="table"> | |
| <tr> | |
| <td>Total driving time:</td> | |
| <td><span id="drivingTime">unknown</span></td> | |
| </tr> | |
| </table> | |
| </div> | |
| <div class="col mb-3"> | |
| <h5>Time Windows</h5> | |
| <div class="d-flex flex-column gap-1"> | |
| <div><i class="fas fa-utensils" style="color: #f59e0b; width: 20px;"></i> <strong>Restaurant</strong> <small class="text-muted">06:00-10:00 · 20-40 min</small></div> | |
| <div><i class="fas fa-building" style="color: #3b82f6; width: 20px;"></i> <strong>Business</strong> <small class="text-muted">09:00-17:00 · 15-30 min</small></div> | |
| <div><i class="fas fa-home" style="color: #10b981; width: 20px;"></i> <strong>Residential</strong> <small class="text-muted">17:00-20:00 · 5-10 min</small></div> | |
| </div> | |
| </div> | |
| <div class="col"> | |
| <div class="d-flex justify-content-between align-items-center mb-2"> | |
| <div> | |
| <h5 class="mb-0">Vehicles</h5> | |
| <small class="text-muted"><i class="fas fa-hand-pointer"></i> Click to highlight route</small> | |
| </div> | |
| <div class="btn-group btn-group-sm" role="group" aria-label="Vehicle management"> | |
| <button type="button" class="btn btn-outline-danger" id="removeVehicleBtn" title="Remove last vehicle"> | |
| <i class="fas fa-minus"></i> | |
| </button> | |
| <button type="button" class="btn btn-outline-success" id="addVehicleBtn" title="Add new vehicle"> | |
| <i class="fas fa-plus"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <table class="table-sm w-100"> | |
| <thead> | |
| <tr> | |
| <th class="col-1"></th> | |
| <th class="col-3">Name</th> | |
| <th class="col-3"> | |
| Cargo | |
| <i class="fas fa-info-circle" data-bs-toggle="tooltip" data-bs-placement="top" | |
| data-html="true" | |
| title="Units to deliver on this route. Each customer requires cargo units (e.g., packages, crates). Bar shows current load vs. vehicle capacity."></i> | |
| </th> | |
| <th class="col-2">Drive</th> | |
| <th class="col-1"></th> | |
| </tr> | |
| </thead> | |
| <tbody id="vehicles"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="tab-pane fade" id="byVehiclePanel" role="tabpanel" aria-labelledby="byVehicleTab"> | |
| </div> | |
| <div class="tab-pane fade" id="byVisitPanel" role="tabpanel" aria-labelledby="byVisitTab"> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="rest" class="tab-pane fade container-fluid"> | |
| <h1>REST API Guide</h1> | |
| <h2>Vehicle routing with vehicle capacity and time windows - integration via cURL</h2> | |
| <h3>1. Download demo data</h3> | |
| <pre> | |
| <button class="btn btn-outline-dark btn-sm float-end" | |
| onclick="copyTextToClipboard('curl1')">Copy</button> | |
| <code id="curl1">curl -X GET -H 'Accept:application/json' http://localhost:8080/demo-data/FIRENZE -o sample.json</code> | |
| </pre> | |
| <h3>2. Post the sample data for solving</h3> | |
| <p>The POST operation returns a <code>jobId</code> that should be used in subsequent commands.</p> | |
| <pre> | |
| <button class="btn btn-outline-dark btn-sm float-end" | |
| onclick="copyTextToClipboard('curl2')">Copy</button> | |
| <code id="curl2">curl -X POST -H 'Content-Type:application/json' http://localhost:8080/route-plans -d@sample.json</code> | |
| </pre> | |
| <h3>3. Get the current status and score</h3> | |
| <pre> | |
| <button class="btn btn-outline-dark btn-sm float-end" | |
| onclick="copyTextToClipboard('curl3')">Copy</button> | |
| <code id="curl3">curl -X GET -H 'Accept:application/json' http://localhost:8080/route-plans/{jobId}/status</code> | |
| </pre> | |
| <h3>4. Get the complete route plan</h3> | |
| <pre> | |
| <button class="btn btn-outline-dark btn-sm float-end" | |
| onclick="copyTextToClipboard('curl4')">Copy</button> | |
| <code id="curl4">curl -X GET -H 'Accept:application/json' http://localhost:8080/route-plans/{jobId}</code> | |
| </pre> | |
| <h3>5. Terminate solving early</h3> | |
| <pre> | |
| <button class="btn btn-outline-dark btn-sm float-end" | |
| onclick="copyTextToClipboard('curl5')">Copy</button> | |
| <code id="curl5">curl -X DELETE -H 'Accept:application/json' http://localhost:8080/route-plans/{jobId}</code> | |
| </pre> | |
| </div> | |
| <div id="openapi" class="tab-pane fade container-fluid"> | |
| <h1>REST API Reference</h1> | |
| <div class="ratio ratio-1x1"> | |
| <!-- "scrolling" attribute is obsolete, but e.g. Chrome does not support "overflow:hidden" --> | |
| <iframe src="/q/swagger-ui" style="overflow:hidden;" scrolling="no"></iframe> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="modal fadebd-example-modal-lg" id="scoreAnalysisModal" tabindex="-1" aria-labelledby="scoreAnalysisModalLabel" aria-hidden="true"> | |
| <div class="modal-dialog modal-lg modal-dialog-scrollable"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h1 class="modal-title fs-5" id="scoreAnalysisModalLabel">Score analysis <span id="scoreAnalysisScoreLabel"></span></h1> | |
| <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
| </div> | |
| <div class="modal-body" id="scoreAnalysisModalContent"> | |
| <!-- Filled in by app.js --> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <form id='visitForm' class='needs-validation' novalidate> | |
| <div class="modal fadebd-example-modal-lg" id="newVisitModal" tabindex="-1" | |
| aria-labelledby="newVisitModalLabel" | |
| aria-hidden="true"> | |
| <div class="modal-dialog modal-lg modal-dialog-scrollable"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h1 class="modal-title fs-5" id="newVisitModalLabel">Add New Visit</h1> | |
| <button type="button" class="btn-close" data-bs-dismiss="modal" | |
| aria-label="Close"></button> | |
| </div> | |
| <div class="modal-body" id="newVisitModalContent"> | |
| <!-- Filled in by app.js --> | |
| </div> | |
| <div class="modal-footer" id="newVisitModalFooter"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </form> | |
| <!-- Add Vehicle Modal --> | |
| <div class="modal fade" id="addVehicleModal" tabindex="-1" aria-labelledby="addVehicleModalLabel" aria-hidden="true"> | |
| <div class="modal-dialog"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title" id="addVehicleModalLabel"><i class="fas fa-truck"></i> Add New Vehicle</h5> | |
| <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
| </div> | |
| <div class="modal-body"> | |
| <div class="mb-3"> | |
| <label for="vehicleName" class="form-label">Name</label> | |
| <input type="text" class="form-control" id="vehicleName" placeholder="e.g., Kilo"> | |
| <div class="form-text">Unique name for the vehicle</div> | |
| </div> | |
| <div class="mb-3"> | |
| <label for="vehicleCapacity" class="form-label">Capacity</label> | |
| <input type="number" class="form-control" id="vehicleCapacity" value="25" min="1"> | |
| <div class="form-text">Maximum cargo the vehicle can carry</div> | |
| </div> | |
| <div class="mb-3"> | |
| <label for="vehicleDepartureTime" class="form-label">Departure Time</label> | |
| <input type="text" class="form-control" id="vehicleDepartureTime"> | |
| </div> | |
| <div class="mb-3"> | |
| <label class="form-label">Home Location</label> | |
| <div class="d-flex gap-2 mb-2"> | |
| <button type="button" class="btn btn-outline-primary btn-sm" id="pickLocationBtn"> | |
| <i class="fas fa-map-marker-alt"></i> Pick on Map | |
| </button> | |
| <span class="text-muted small align-self-center">or enter coordinates:</span> | |
| </div> | |
| <div class="row g-2"> | |
| <div class="col-6"> | |
| <input type="number" step="any" class="form-control" id="vehicleHomeLat" placeholder="Latitude"> | |
| </div> | |
| <div class="col-6"> | |
| <input type="number" step="any" class="form-control" id="vehicleHomeLng" placeholder="Longitude"> | |
| </div> | |
| </div> | |
| <div id="vehicleLocationPreview" class="mt-2 text-muted small"></div> | |
| </div> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> | |
| <button type="button" class="btn btn-success" id="confirmAddVehicle"><i class="fas fa-plus"></i> Add Vehicle</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Loading/Progress Overlay --> | |
| <div id="loadingOverlay" class="loading-overlay hidden"> | |
| <div class="loading-content"> | |
| <div class="loading-spinner"></div> | |
| <h5 id="loadingTitle">Loading Demo Data</h5> | |
| <p id="loadingMessage" class="text-muted mb-2">Initializing...</p> | |
| <div class="progress" style="width: 300px; height: 8px;"> | |
| <div id="loadingProgress" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div> | |
| </div> | |
| <small id="loadingDetail" class="text-muted mt-2 d-block"></small> | |
| </div> | |
| </div> | |
| <footer id="solverforge-auto-footer"></footer> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.8/umd/popper.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/js-joda/1.11.0/js-joda.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.2/standalone/umd/vis-timeline-graph2d.min.js" | |
| integrity="sha256-Jy2+UO7rZ2Dgik50z3XrrNpnc5+2PAx9MhL2CicodME=" crossorigin="anonymous"></script> | |
| <script src="/webjars/solverforge/js/solverforge-webui.js"></script> | |
| <script src="/score-analysis.js"></script> | |
| <script src="/recommended-fit.js"></script> | |
| <script src="/app.js"></script> | |
| </body> | |
| </html> | |