Team Ai
Apppublic

blackopsrepl/vehicle-routing-python

sourceHugging Faceapache-2.0updated 10mo agoView on Hugging Face
2likes
app.js1608 linesDownload Raw Back to static
1let autoRefreshIntervalId = null;2let initialized = false;3let optimizing = false;4let demoDataId = null;5let scheduleId = null;6let loadedRoutePlan = null;7let newVisit = null;8let visitMarker = null;9let routeGeometries = null;  // Cache for encoded polyline geometries10let useRealRoads = false;    // Routing mode toggle state11const solveButton = $("#solveButton");12const stopSolvingButton = $("#stopSolvingButton");13const vehiclesTable = $("#vehicles");14const analyzeButton = $("#analyzeButton");15 16/**17 * Decode an encoded polyline string into an array of [lat, lng] coordinates.18 * This is the Google polyline encoding algorithm.19 * @param {string} encoded - The encoded polyline string20 * @returns {Array<Array<number>>} Array of [lat, lng] coordinate pairs21 */22function decodePolyline(encoded) {23  if (!encoded) return [];24 25  const points = [];26  let index = 0;27  let lat = 0;28  let lng = 0;29 30  while (index < encoded.length) {31    // Decode latitude32    let shift = 0;33    let result = 0;34    let byte;35    do {36      byte = encoded.charCodeAt(index++) - 63;37      result |= (byte & 0x1f) << shift;38      shift += 5;39    } while (byte >= 0x20);40    const dlat = (result & 1) ? ~(result >> 1) : (result >> 1);41    lat += dlat;42 43    // Decode longitude44    shift = 0;45    result = 0;46    do {47      byte = encoded.charCodeAt(index++) - 63;48      result |= (byte & 0x1f) << shift;49      shift += 5;50    } while (byte >= 0x20);51    const dlng = (result & 1) ? ~(result >> 1) : (result >> 1);52    lng += dlng;53 54    // Polyline encoding uses precision of 5 decimal places55    points.push([lat / 1e5, lng / 1e5]);56  }57 58  return points;59}60 61/**62 * Fetch route geometries for the current schedule from the backend.63 * @returns {Promise<Object|null>} The geometries object or null if unavailable64 */65async function fetchRouteGeometries() {66  if (!scheduleId) return null;67 68  try {69    const response = await fetch(`/route-plans/${scheduleId}/geometry`);70    if (response.ok) {71      const data = await response.json();72      return data.geometries || null;73    }74  } catch (e) {75    console.warn('Could not fetch route geometries:', e);76  }77  return null;78}79 80/*************************************** Loading Overlay Functions **************************************/81 82function showLoadingOverlay(title = "Loading Demo Data", message = "Initializing...") {83  $("#loadingTitle").text(title);84  $("#loadingMessage").text(message);85  $("#loadingProgress").css("width", "0%");86  $("#loadingDetail").text("");87  $("#loadingOverlay").removeClass("hidden");88}89 90function hideLoadingOverlay() {91  $("#loadingOverlay").addClass("hidden");92}93 94function updateLoadingProgress(message, percent, detail = "") {95  $("#loadingMessage").text(message);96  $("#loadingProgress").css("width", `${percent}%`);97  $("#loadingDetail").text(detail);98}99 100/**101 * Load demo data with progress updates via Server-Sent Events.102 * Used when Real Roads mode is enabled.103 */104function loadDemoDataWithProgress(demoId) {105  return new Promise((resolve, reject) => {106    const routingMode = useRealRoads ? "real_roads" : "haversine";107    const url = `/demo-data/${demoId}/stream?routing=${routingMode}`;108 109    showLoadingOverlay(110      useRealRoads ? "Loading Real Road Data" : "Loading Demo Data",111      "Connecting..."112    );113 114    const eventSource = new EventSource(url);115    let solution = null;116 117    eventSource.onmessage = function(event) {118      try {119        const data = JSON.parse(event.data);120 121        if (data.event === "progress") {122          let statusIcon = "";123          if (data.phase === "network") {124            statusIcon = '<i class="fas fa-download me-2"></i>';125          } else if (data.phase === "routes") {126            statusIcon = '<i class="fas fa-route me-2"></i>';127          } else if (data.phase === "complete") {128            statusIcon = '<i class="fas fa-check-circle me-2 text-success"></i>';129          }130          updateLoadingProgress(data.message, data.percent, data.detail || "");131        } else if (data.event === "complete") {132          solution = data.solution;133          // Store geometries from the response if available134          if (data.geometries) {135            routeGeometries = data.geometries;136          }137          eventSource.close();138          hideLoadingOverlay();139          resolve(solution);140        } else if (data.event === "error") {141          eventSource.close();142          hideLoadingOverlay();143          reject(new Error(data.message));144        }145      } catch (e) {146        console.error("Error parsing SSE event:", e);147      }148    };149 150    eventSource.onerror = function(error) {151      eventSource.close();152      hideLoadingOverlay();153      reject(new Error("Connection lost while loading data"));154    };155  });156}157 158/*************************************** Map constants and variable definitions  **************************************/159 160const homeLocationMarkerByIdMap = new Map();161const visitMarkerByIdMap = new Map();162 163const map = L.map("map", { doubleClickZoom: false }).setView(164  [51.505, -0.09],165  13,166);167const visitGroup = L.layerGroup().addTo(map);168const homeLocationGroup = L.layerGroup().addTo(map);169const routeGroup = L.layerGroup().addTo(map);170 171/************************************ Time line constants and variable definitions ************************************/172 173let byVehicleTimeline;174let byVisitTimeline;175const byVehicleGroupData = new vis.DataSet();176const byVehicleItemData = new vis.DataSet();177const byVisitGroupData = new vis.DataSet();178const byVisitItemData = new vis.DataSet();179 180const byVehicleTimelineOptions = {181  timeAxis: { scale: "hour" },182  orientation: { axis: "top" },183  xss: { disabled: true }, // Items are XSS safe through JQuery184  stack: false,185  stackSubgroups: false,186  zoomMin: 1000 * 60 * 60, // A single hour in milliseconds187  zoomMax: 1000 * 60 * 60 * 24, // A single day in milliseconds188};189 190const byVisitTimelineOptions = {191  timeAxis: { scale: "hour" },192  orientation: { axis: "top" },193  verticalScroll: true,194  xss: { disabled: true }, // Items are XSS safe through JQuery195  stack: false,196  stackSubgroups: false,197  zoomMin: 1000 * 60 * 60, // A single hour in milliseconds198  zoomMax: 1000 * 60 * 60 * 24, // A single day in milliseconds199};200 201/************************************ Initialize ************************************/202 203// Vehicle management state204let addingVehicleMode = false;205let pickingVehicleLocation = false;206let tempVehicleMarker = null;207let vehicleDeparturePicker = null;208 209// Route highlighting state210let highlightedVehicleId = null;211let routeNumberMarkers = [];  // Markers showing 1, 2, 3... on route stops212 213 214$(document).ready(function () {215  replaceQuickstartSolverForgeAutoHeaderFooter();216 217  // Initialize timelines after DOM is ready with a small delay to ensure Bootstrap tabs are rendered218  setTimeout(function () {219    const byVehiclePanel = document.getElementById("byVehiclePanel");220    const byVisitPanel = document.getElementById("byVisitPanel");221 222    if (byVehiclePanel) {223      byVehicleTimeline = new vis.Timeline(224        byVehiclePanel,225        byVehicleItemData,226        byVehicleGroupData,227        byVehicleTimelineOptions,228      );229    }230 231    if (byVisitPanel) {232      byVisitTimeline = new vis.Timeline(233        byVisitPanel,234        byVisitItemData,235        byVisitGroupData,236        byVisitTimelineOptions,237      );238    }239  }, 100);240 241  L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {242    maxZoom: 19,243    attribution:244      '&copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors',245  }).addTo(map);246 247  solveButton.click(solve);248  stopSolvingButton.click(stopSolving);249  analyzeButton.click(analyze);250  refreshSolvingButtons(false);251 252  // HACK to allow vis-timeline to work within Bootstrap tabs253  $("#byVehicleTab").on("shown.bs.tab", function (event) {254    if (byVehicleTimeline) {255      byVehicleTimeline.redraw();256    }257  });258  $("#byVisitTab").on("shown.bs.tab", function (event) {259    if (byVisitTimeline) {260      byVisitTimeline.redraw();261    }262  });263 264  // Map click handler - context aware265  map.on("click", function (e) {266    if (addingVehicleMode) {267      // Set vehicle home location268      setVehicleHomeLocation(e.latlng.lat, e.latlng.lng);269    } else if (!optimizing) {270      // Add new visit271      visitMarker = L.circleMarker(e.latlng);272      visitMarker.setStyle({ color: "green" });273      visitMarker.addTo(map);274      openRecommendationModal(e.latlng.lat, e.latlng.lng);275    }276  });277 278  // Remove visit marker when modal closes279  $("#newVisitModal").on("hidden.bs.modal", function () {280    if (visitMarker) {281      map.removeLayer(visitMarker);282    }283  });284 285  // Vehicle management286  $("#addVehicleBtn").click(openAddVehicleModal);287  $("#removeVehicleBtn").click(removeLastVehicle);288  $("#confirmAddVehicle").click(confirmAddVehicle);289  $("#pickLocationBtn").click(pickVehicleLocationOnMap);290 291  // Clean up when add vehicle modal closes (only if not picking location)292  $("#addVehicleModal").on("hidden.bs.modal", function () {293    if (!pickingVehicleLocation) {294      addingVehicleMode = false;295      if (tempVehicleMarker) {296        map.removeLayer(tempVehicleMarker);297        tempVehicleMarker = null;298      }299    }300  });301 302  // Real Roads toggle handler303  $(document).on('change', '#realRoadRouting', function() {304    useRealRoads = $(this).is(':checked');305 306    // If we have a demo dataset loaded, reload it with the new routing mode307    if (demoDataId && !optimizing) {308      scheduleId = null;309      initialized = false;310      homeLocationGroup.clearLayers();311      homeLocationMarkerByIdMap.clear();312      visitGroup.clearLayers();313      visitMarkerByIdMap.clear();314      routeGeometries = null;315      refreshRoutePlan();316    }317  });318 319  setupAjax();320  fetchDemoData();321});322 323/*************************************** Vehicle Management **************************************/324 325function openAddVehicleModal() {326  if (optimizing) {327    alert("Cannot add vehicles while solving. Please stop solving first.");328    return;329  }330  if (!loadedRoutePlan) {331    alert("Please load a dataset first.");332    return;333  }334 335  addingVehicleMode = true;336 337  // Suggest next vehicle name338  $("#vehicleName").val("").attr("placeholder", `e.g., ${getNextVehicleName()}`);339 340  // Set default values based on existing vehicles341  const existingVehicle = loadedRoutePlan.vehicles[0];342  if (existingVehicle) {343    $("#vehicleCapacity").val(existingVehicle.capacity || 25);344    const defaultLat = existingVehicle.homeLocation[0];345    const defaultLng = existingVehicle.homeLocation[1];346    $("#vehicleHomeLat").val(defaultLat.toFixed(6));347    $("#vehicleHomeLng").val(defaultLng.toFixed(6));348  }349 350  // Initialize departure time picker351  const tomorrow = JSJoda.LocalDate.now().plusDays(1);352  const defaultDeparture = tomorrow.atTime(JSJoda.LocalTime.of(6, 0));353 354  if (vehicleDeparturePicker) {355    vehicleDeparturePicker.destroy();356  }357  vehicleDeparturePicker = flatpickr("#vehicleDepartureTime", {358    enableTime: true,359    dateFormat: "Y-m-d H:i",360    defaultDate: defaultDeparture.format(JSJoda.DateTimeFormatter.ofPattern('yyyy-M-d HH:mm'))361  });362 363  $("#addVehicleModal").modal("show");364}365 366function pickVehicleLocationOnMap() {367  // Hide modal temporarily while user picks location368  pickingVehicleLocation = true;369  addingVehicleMode = true;370  $("#addVehicleModal").modal("hide");371 372  // Show hint on map373  $("#mapHint").html('<i class="fas fa-crosshairs"></i> Click on the map to set vehicle depot location').removeClass("hidden");374}375 376function setVehicleHomeLocation(lat, lng) {377  $("#vehicleHomeLat").val(lat.toFixed(6));378  $("#vehicleHomeLng").val(lng.toFixed(6));379  $("#vehicleLocationPreview").html(`<i class="fas fa-check text-success"></i> Location set: ${lat.toFixed(4)}, ${lng.toFixed(4)}`);380 381  // Show temporary marker382  if (tempVehicleMarker) {383    map.removeLayer(tempVehicleMarker);384  }385  tempVehicleMarker = L.marker([lat, lng], {386    icon: L.divIcon({387      className: 'temp-vehicle-marker',388      html: `<div style="389        background-color: #6366f1;390        border: 3px solid white;391        border-radius: 4px;392        width: 28px;393        height: 28px;394        display: flex;395        align-items: center;396        justify-content: center;397        box-shadow: 0 2px 4px rgba(0,0,0,0.4);398        animation: pulse 1s infinite;399      "><i class="fas fa-warehouse" style="color: white; font-size: 12px;"></i></div>`,400      iconSize: [28, 28],401      iconAnchor: [14, 14]402    })403  });404  tempVehicleMarker.addTo(map);405 406  // If we were picking location, re-open the modal407  if (pickingVehicleLocation) {408    pickingVehicleLocation = false;409    addingVehicleMode = false;410    $("#addVehicleModal").modal("show");411    // Restore normal map hint412    $("#mapHint").html('<i class="fas fa-mouse-pointer"></i> Click on the map to add a new visit');413  }414}415 416// Extended phonetic alphabet for generating vehicle names417const PHONETIC_NAMES = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike", "November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra", "Tango", "Uniform", "Victor", "Whiskey", "X-ray", "Yankee", "Zulu"];418 419function getNextVehicleName() {420  if (!loadedRoutePlan) return "Alpha";421  const usedNames = new Set(loadedRoutePlan.vehicles.map(v => v.name));422  for (const name of PHONETIC_NAMES) {423    if (!usedNames.has(name)) return name;424  }425  // Fallback if all names used426  return `Vehicle ${loadedRoutePlan.vehicles.length + 1}`;427}428 429async function confirmAddVehicle() {430  const vehicleName = $("#vehicleName").val().trim() || getNextVehicleName();431  const capacity = parseInt($("#vehicleCapacity").val());432  const lat = parseFloat($("#vehicleHomeLat").val());433  const lng = parseFloat($("#vehicleHomeLng").val());434  const departureTime = $("#vehicleDepartureTime").val();435 436  if (!capacity || capacity < 1) {437    alert("Please enter a valid capacity (minimum 1).");438    return;439  }440  if (isNaN(lat) || isNaN(lng)) {441    alert("Please set a valid home location by clicking on the map or entering coordinates.");442    return;443  }444  if (!departureTime) {445    alert("Please set a departure time.");446    return;447  }448 449  // Generate new vehicle ID450  const maxId = Math.max(...loadedRoutePlan.vehicles.map(v => parseInt(v.id)), 0);451  const newId = String(maxId + 1);452 453  // Format departure time454  const formattedDeparture = JSJoda.LocalDateTime.parse(455    departureTime,456    JSJoda.DateTimeFormatter.ofPattern('yyyy-M-d HH:mm')457  ).format(JSJoda.DateTimeFormatter.ISO_LOCAL_DATE_TIME);458 459  // Create new vehicle460  const newVehicle = {461    id: newId,462    name: vehicleName,463    capacity: capacity,464    homeLocation: [lat, lng],465    departureTime: formattedDeparture,466    visits: [],467    totalDemand: 0,468    totalDrivingTimeSeconds: 0,469    arrivalTime: formattedDeparture470  };471 472  // Add to solution473  loadedRoutePlan.vehicles.push(newVehicle);474 475  // Close modal and refresh476  $("#addVehicleModal").modal("hide");477  addingVehicleMode = false;478 479  if (tempVehicleMarker) {480    map.removeLayer(tempVehicleMarker);481    tempVehicleMarker = null;482  }483 484  // Refresh display485  await renderRoutes(loadedRoutePlan);486  renderTimelines(loadedRoutePlan);487 488  showNotification(`Vehicle "${vehicleName}" added successfully!`, "success");489}490 491async function removeLastVehicle() {492  if (optimizing) {493    alert("Cannot remove vehicles while solving. Please stop solving first.");494    return;495  }496  if (!loadedRoutePlan || loadedRoutePlan.vehicles.length <= 1) {497    alert("Cannot remove the last vehicle. At least one vehicle is required.");498    return;499  }500 501  const lastVehicle = loadedRoutePlan.vehicles[loadedRoutePlan.vehicles.length - 1];502 503  if (lastVehicle.visits && lastVehicle.visits.length > 0) {504    if (!confirm(`Vehicle ${lastVehicle.id} has ${lastVehicle.visits.length} assigned visits. These will become unassigned. Continue?`)) {505      return;506    }507    // Unassign visits from the vehicle508    lastVehicle.visits.forEach(visitId => {509      const visit = loadedRoutePlan.visits.find(v => v.id === visitId);510      if (visit) {511        visit.vehicle = null;512        visit.previousVisit = null;513        visit.nextVisit = null;514        visit.arrivalTime = null;515        visit.departureTime = null;516      }517    });518  }519 520  // Remove vehicle521  loadedRoutePlan.vehicles.pop();522 523  // Remove marker524  const marker = homeLocationMarkerByIdMap.get(lastVehicle.id);525  if (marker) {526    homeLocationGroup.removeLayer(marker);527    homeLocationMarkerByIdMap.delete(lastVehicle.id);528  }529 530  // Refresh display531  await renderRoutes(loadedRoutePlan);532  renderTimelines(loadedRoutePlan);533 534  showNotification(`Vehicle "${lastVehicle.name || lastVehicle.id}" removed.`, "info");535}536 537async function removeVehicle(vehicleId) {538  if (optimizing) {539    alert("Cannot remove vehicles while solving. Please stop solving first.");540    return;541  }542 543  const vehicleIndex = loadedRoutePlan.vehicles.findIndex(v => v.id === vehicleId);544  if (vehicleIndex === -1) return;545 546  if (loadedRoutePlan.vehicles.length <= 1) {547    alert("Cannot remove the last vehicle. At least one vehicle is required.");548    return;549  }550 551  const vehicle = loadedRoutePlan.vehicles[vehicleIndex];552 553  if (vehicle.visits && vehicle.visits.length > 0) {554    if (!confirm(`Vehicle ${vehicle.id} has ${vehicle.visits.length} assigned visits. These will become unassigned. Continue?`)) {555      return;556    }557    // Unassign visits558    vehicle.visits.forEach(visitId => {559      const visit = loadedRoutePlan.visits.find(v => v.id === visitId);560      if (visit) {561        visit.vehicle = null;562        visit.previousVisit = null;563        visit.nextVisit = null;564        visit.arrivalTime = null;565        visit.departureTime = null;566      }567    });568  }569 570  // Remove vehicle571  loadedRoutePlan.vehicles.splice(vehicleIndex, 1);572 573  // Remove marker574  const marker = homeLocationMarkerByIdMap.get(vehicleId);575  if (marker) {576    homeLocationGroup.removeLayer(marker);577    homeLocationMarkerByIdMap.delete(vehicleId);578  }579 580  // Refresh display581  await renderRoutes(loadedRoutePlan);582  renderTimelines(loadedRoutePlan);583 584  showNotification(`Vehicle "${vehicle.name || vehicleId}" removed.`, "info");585}586 587function showNotification(message, type = "info") {588  const alertClass = type === "success" ? "alert-success" : type === "error" ? "alert-danger" : "alert-info";589  const icon = type === "success" ? "fa-check-circle" : type === "error" ? "fa-exclamation-circle" : "fa-info-circle";590 591  const notification = $(`592    <div class="alert ${alertClass} alert-dismissible fade show" role="alert" style="min-width: 300px;">593      <i class="fas ${icon} me-2"></i>${message}594      <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>595    </div>596  `);597 598  $("#notificationPanel").append(notification);599 600  // Auto-dismiss after 3 seconds601  setTimeout(() => {602    notification.alert('close');603  }, 3000);604}605 606/*************************************** Route Highlighting **************************************/607 608function toggleVehicleHighlight(vehicleId) {609  if (highlightedVehicleId === vehicleId) {610    // Already highlighted - clear it611    clearRouteHighlight();612  } else {613    // Highlight this vehicle's route614    highlightVehicleRoute(vehicleId);615  }616}617 618function clearRouteHighlight() {619  // Remove number markers620  routeNumberMarkers.forEach(marker => map.removeLayer(marker));621  routeNumberMarkers = [];622 623  // Reset all vehicle icons to normal and restore opacity624  if (loadedRoutePlan) {625    loadedRoutePlan.vehicles.forEach(vehicle => {626      const marker = homeLocationMarkerByIdMap.get(vehicle.id);627      if (marker) {628        marker.setIcon(createVehicleHomeIcon(vehicle, false));629        marker.setOpacity(1);630      }631    });632 633    // Reset all visit markers to normal and restore opacity634    loadedRoutePlan.visits.forEach(visit => {635      const marker = visitMarkerByIdMap.get(visit.id);636      if (marker) {637        const customerType = getCustomerType(visit);638        const isAssigned = visit.vehicle != null;639        marker.setIcon(createCustomerTypeIcon(customerType, isAssigned, false));640        marker.setOpacity(1);641      }642    });643  }644 645  // Reset route lines646  renderRouteLines();647 648  // Update vehicle table highlighting649  $("#vehicles tr").removeClass("table-active");650 651  highlightedVehicleId = null;652}653 654function highlightVehicleRoute(vehicleId) {655  // Clear any existing highlight first656  clearRouteHighlight();657 658  highlightedVehicleId = vehicleId;659 660  if (!loadedRoutePlan) return;661 662  const vehicle = loadedRoutePlan.vehicles.find(v => v.id === vehicleId);663  if (!vehicle) return;664 665  const vehicleColor = colorByVehicle(vehicle);666 667  // Highlight the vehicle's home marker668  const homeMarker = homeLocationMarkerByIdMap.get(vehicleId);669  if (homeMarker) {670    homeMarker.setIcon(createVehicleHomeIcon(vehicle, true));671  }672 673  // Dim other vehicles674  loadedRoutePlan.vehicles.forEach(v => {675    if (v.id !== vehicleId) {676      const marker = homeLocationMarkerByIdMap.get(v.id);677      if (marker) {678        marker.setIcon(createVehicleHomeIcon(v, false));679        marker.setOpacity(0.3);680      }681    }682  });683 684  // Get visit order for this vehicle685  const visitByIdMap = new Map(loadedRoutePlan.visits.map(v => [v.id, v]));686  const vehicleVisits = vehicle.visits.map(visitId => visitByIdMap.get(visitId)).filter(v => v);687 688  // Highlight and number the visits on this route689  let stopNumber = 1;690  vehicleVisits.forEach(visit => {691    const marker = visitMarkerByIdMap.get(visit.id);692    if (marker) {693      const customerType = getCustomerType(visit);694      marker.setIcon(createCustomerTypeIcon(customerType, true, true, vehicleColor));695      marker.setOpacity(1);696 697      // Add number marker698      const numberMarker = L.marker(visit.location, {699        icon: createRouteNumberIcon(stopNumber, vehicleColor),700        interactive: false,701        zIndexOffset: 1000702      });703      numberMarker.addTo(map);704      routeNumberMarkers.push(numberMarker);705      stopNumber++;706    }707  });708 709  // Dim visits not on this route710  loadedRoutePlan.visits.forEach(visit => {711    if (!vehicle.visits.includes(visit.id)) {712      const marker = visitMarkerByIdMap.get(visit.id);713      if (marker) {714        marker.setOpacity(0.25);715      }716    }717  });718 719  // Highlight just this route, dim others720  renderRouteLines(vehicleId);721 722  // Highlight the row in the vehicle table723  $("#vehicles tr").removeClass("table-active");724  $(`#vehicle-row-${vehicleId}`).addClass("table-active");725 726  // Add start marker (S) at depot727  const startMarker = L.marker(vehicle.homeLocation, {728    icon: createRouteNumberIcon("S", vehicleColor),729    interactive: false,730    zIndexOffset: 1000731  });732  startMarker.addTo(map);733  routeNumberMarkers.push(startMarker);734}735 736function createRouteNumberIcon(number, color) {737  return L.divIcon({738    className: 'route-number-marker',739    html: `<div style="740      background-color: ${color};741      color: white;742      font-weight: bold;743      font-size: 12px;744      width: 22px;745      height: 22px;746      border-radius: 50%;747      border: 2px solid white;748      display: flex;749      align-items: center;750      justify-content: center;751      box-shadow: 0 2px 4px rgba(0,0,0,0.4);752      margin-left: 16px;753      margin-top: -28px;754    ">${number}</div>`,755    iconSize: [22, 22],756    iconAnchor: [0, 0]757  });758}759 760async function renderRouteLines(highlightedId = null) {761  routeGroup.clearLayers();762 763  if (!loadedRoutePlan) return;764 765  // Fetch geometries during solving (routes change)766  if (scheduleId) {767    routeGeometries = await fetchRouteGeometries();768  }769 770  const visitByIdMap = new Map(loadedRoutePlan.visits.map(visit => [visit.id, visit]));771 772  for (let vehicle of loadedRoutePlan.vehicles) {773    const homeLocation = vehicle.homeLocation;774    const locations = vehicle.visits.map(visitId => visitByIdMap.get(visitId)?.location).filter(l => l);775 776    const isHighlighted = highlightedId === null || vehicle.id === highlightedId;777    const color = colorByVehicle(vehicle);778    const weight = isHighlighted && highlightedId !== null ? 5 : 3;779    const opacity = isHighlighted ? 1 : 0.2;780 781    const vehicleGeometry = routeGeometries?.[vehicle.id];782 783    if (vehicleGeometry && vehicleGeometry.length > 0) {784      // Draw real road routes using decoded polylines785      for (const encodedSegment of vehicleGeometry) {786        if (encodedSegment) {787          const points = decodePolyline(encodedSegment);788          if (points.length > 0) {789            L.polyline(points, {790              color: color,791              weight: weight,792              opacity: opacity793            }).addTo(routeGroup);794          }795        }796      }797    } else if (locations.length > 0) {798      // Fallback to straight lines if no geometry available799      L.polyline([homeLocation, ...locations, homeLocation], {800        color: color,801        weight: weight,802        opacity: opacity803      }).addTo(routeGroup);804    }805  }806}807 808function colorByVehicle(vehicle) {809  return vehicle === null ? null : pickColor("vehicle" + vehicle.id);810}811 812// Customer type definitions matching demo_data.py813const CUSTOMER_TYPES = {814  RESTAURANT: { label: "Restaurant", icon: "fa-utensils", color: "#f59e0b", windowStart: "06:00", windowEnd: "10:00", minService: 20, maxService: 40 },815  BUSINESS: { label: "Business", icon: "fa-building", color: "#3b82f6", windowStart: "09:00", windowEnd: "17:00", minService: 15, maxService: 30 },816  RESIDENTIAL: { label: "Residential", icon: "fa-home", color: "#10b981", windowStart: "17:00", windowEnd: "20:00", minService: 5, maxService: 10 },817};818 819function getCustomerType(visit) {820  const startTime = showTimeOnly(visit.minStartTime).toString();821  const endTime = showTimeOnly(visit.maxEndTime).toString();822 823  for (const [type, config] of Object.entries(CUSTOMER_TYPES)) {824    if (startTime === config.windowStart && endTime === config.windowEnd) {825      return { type, ...config };826    }827  }828  return { type: "UNKNOWN", label: "Custom", icon: "fa-question", color: "#6b7280", windowStart: startTime, windowEnd: endTime };829}830 831function formatDrivingTime(drivingTimeInSeconds) {832  return `${Math.floor(drivingTimeInSeconds / 3600)}h ${Math.round((drivingTimeInSeconds % 3600) / 60)}m`;833}834 835function homeLocationPopupContent(vehicle) {836  const color = colorByVehicle(vehicle);837  const visitCount = vehicle.visits ? vehicle.visits.length : 0;838  const vehicleName = vehicle.name || `Vehicle ${vehicle.id}`;839  return `<div style="min-width: 150px;">840    <h5 style="color: ${color};"><i class="fas fa-truck"></i> ${vehicleName}</h5>841    <p class="mb-1"><strong>Depot Location</strong></p>842    <p class="mb-1"><i class="fas fa-box"></i> Capacity: ${vehicle.capacity}</p>843    <p class="mb-1"><i class="fas fa-route"></i> Visits: ${visitCount}</p>844    <p class="mb-0"><i class="fas fa-clock"></i> Departs: ${showTimeOnly(vehicle.departureTime)}</p>845  </div>`;846}847 848function visitPopupContent(visit) {849  const customerType = getCustomerType(visit);850  const serviceDurationMinutes = Math.round(visit.serviceDuration / 60);851  const arrival = visit.arrivalTime852    ? `<h6>Arrival at ${showTimeOnly(visit.arrivalTime)}.</h6>`853    : "";854  return `<h5><i class="fas ${customerType.icon}" style="color: ${customerType.color}"></i> ${visit.name}</h5>855    <h6><span class="badge" style="background-color: ${customerType.color}">${customerType.label}</span></h6>856    <h6>Cargo: ${visit.demand} units</h6>857    <h6>Service time: ${serviceDurationMinutes} min</h6>858    <h6>Window: ${showTimeOnly(visit.minStartTime)} - ${showTimeOnly(visit.maxEndTime)}</h6>859    ${arrival}`;860}861 862function showTimeOnly(localDateTimeString) {863  return JSJoda.LocalDateTime.parse(localDateTimeString).toLocalTime();864}865 866function createVehicleHomeIcon(vehicle, isHighlighted = false) {867  const color = colorByVehicle(vehicle);868  const size = isHighlighted ? 36 : 28;869  const fontSize = isHighlighted ? 14 : 11;870  const borderWidth = isHighlighted ? 4 : 3;871  const shadow = isHighlighted872    ? `0 0 0 4px ${color}40, 0 4px 8px rgba(0,0,0,0.5)`873    : '0 2px 4px rgba(0,0,0,0.4)';874 875  return L.divIcon({876    className: 'vehicle-home-marker',877    html: `<div style="878      background-color: ${color};879      border: ${borderWidth}px solid white;880      border-radius: 50%;881      width: ${size}px;882      height: ${size}px;883      display: flex;884      align-items: center;885      justify-content: center;886      box-shadow: ${shadow};887      transition: all 0.2s ease;888    "><i class="fas fa-truck" style="color: white; font-size: ${fontSize}px;"></i></div>`,889    iconSize: [size, size],890    iconAnchor: [size/2, size/2],891    popupAnchor: [0, -size/2]892  });893}894 895function getHomeLocationMarker(vehicle) {896  let marker = homeLocationMarkerByIdMap.get(vehicle.id);897  if (marker) {898    marker.setIcon(createVehicleHomeIcon(vehicle));899    return marker;900  }901  marker = L.marker(vehicle.homeLocation, {902    icon: createVehicleHomeIcon(vehicle)903  });904  marker.addTo(homeLocationGroup).bindPopup();905  homeLocationMarkerByIdMap.set(vehicle.id, marker);906  return marker;907}908 909function createCustomerTypeIcon(customerType, isAssigned = false, isHighlighted = false, highlightColor = null) {910  const borderColor = isHighlighted && highlightColor911    ? highlightColor912    : (isAssigned ? customerType.color : '#6b7280');913  const size = isHighlighted ? 38 : 32;914  const fontSize = isHighlighted ? 16 : 14;915  const borderWidth = isHighlighted ? 4 : 3;916  const shadow = isHighlighted917    ? `0 0 0 4px ${highlightColor}40, 0 4px 8px rgba(0,0,0,0.4)`918    : '0 2px 4px rgba(0,0,0,0.3)';919 920  return L.divIcon({921    className: 'customer-marker',922    html: `<div style="923      background-color: white;924      border: ${borderWidth}px solid ${borderColor};925      border-radius: 50%;926      width: ${size}px;927      height: ${size}px;928      display: flex;929      align-items: center;930      justify-content: center;931      box-shadow: ${shadow};932      transition: all 0.2s ease;933    "><i class="fas ${customerType.icon}" style="color: ${customerType.color}; font-size: ${fontSize}px;"></i></div>`,934    iconSize: [size, size],935    iconAnchor: [size/2, size/2],936    popupAnchor: [0, -size/2]937  });938}939 940function getVisitMarker(visit) {941  let marker = visitMarkerByIdMap.get(visit.id);942  const customerType = getCustomerType(visit);943  const isAssigned = visit.vehicle != null;944 945  if (marker) {946    // Update icon if assignment status changed947    marker.setIcon(createCustomerTypeIcon(customerType, isAssigned));948    return marker;949  }950 951  marker = L.marker(visit.location, {952    icon: createCustomerTypeIcon(customerType, isAssigned)953  });954  marker.addTo(visitGroup).bindPopup();955  visitMarkerByIdMap.set(visit.id, marker);956  return marker;957}958 959async function renderRoutes(solution) {960  if (!initialized) {961    const bounds = [solution.southWestCorner, solution.northEastCorner];962    map.fitBounds(bounds);963  }964  // Vehicles965  vehiclesTable.children().remove();966  const canRemove = solution.vehicles.length > 1;967  solution.vehicles.forEach(function (vehicle) {968    getHomeLocationMarker(vehicle).setPopupContent(969      homeLocationPopupContent(vehicle),970    );971    const { id, capacity, totalDemand, totalDrivingTimeSeconds } = vehicle;972    const percentage = Math.min((totalDemand / capacity) * 100, 100);973    const overCapacity = totalDemand > capacity;974    const color = colorByVehicle(vehicle);975    const progressBarColor = overCapacity ? 'bg-danger' : '';976    const isHighlighted = highlightedVehicleId === id;977    const visitCount = vehicle.visits ? vehicle.visits.length : 0;978    const vehicleName = vehicle.name || `Vehicle ${id}`;979 980    vehiclesTable.append(`981      <tr id="vehicle-row-${id}" class="vehicle-row ${isHighlighted ? 'table-active' : ''}" style="cursor: pointer;">982        <td onclick="toggleVehicleHighlight('${id}')">983          <div style="background-color: ${color}; width: 1.5rem; height: 1.5rem; border-radius: 50%; display: flex; align-items: center; justify-content: center; ${isHighlighted ? 'box-shadow: 0 0 0 3px ' + color + '40;' : ''}">984            <i class="fas fa-truck" style="color: white; font-size: 0.65rem;"></i>985          </div>986        </td>987        <td onclick="toggleVehicleHighlight('${id}')">988          <strong>${vehicleName}</strong>989          <br><small class="text-muted">${visitCount} stops</small>990        </td>991        <td onclick="toggleVehicleHighlight('${id}')">992          <div class="progress" style="height: 18px;" data-bs-toggle="tooltip" data-bs-placement="left"993            title="Cargo: ${totalDemand} / Capacity: ${capacity}${overCapacity ? ' (OVER CAPACITY!)' : ''}">994            <div class="progress-bar ${progressBarColor}" role="progressbar" style="width: ${percentage}%; font-size: 0.7rem; transition: width 0.3s ease;">995              ${totalDemand}/${capacity}996            </div>997          </div>998        </td>999        <td onclick="toggleVehicleHighlight('${id}')" style="font-size: 0.85rem;">1000          ${formatDrivingTime(totalDrivingTimeSeconds)}1001        </td>1002        <td>1003          ${canRemove ? `<button class="btn btn-sm btn-outline-danger p-0 px-1" onclick="event.stopPropagation(); removeVehicle('${id}')" title="Remove vehicle ${vehicleName}">1004            <i class="fas fa-times" style="font-size: 0.7rem;"></i>1005          </button>` : ''}1006        </td>1007      </tr>`);1008  });1009  // Visits1010  solution.visits.forEach(function (visit) {1011    getVisitMarker(visit).setPopupContent(visitPopupContent(visit));1012  });1013  // Route - use the dedicated function which handles highlighting (await to ensure geometries load)1014  await renderRouteLines(highlightedVehicleId);1015 1016  // Summary1017  $("#score").text(solution.score ? `Score: ${solution.score}` : "Score: ?");1018  $("#drivingTime").text(formatDrivingTime(solution.totalDrivingTimeSeconds));1019}1020 1021function renderTimelines(routePlan) {1022  byVehicleGroupData.clear();1023  byVisitGroupData.clear();1024  byVehicleItemData.clear();1025  byVisitItemData.clear();1026 1027  // Build lookup maps for O(1) access1028  const vehicleById = new Map(routePlan.vehicles.map(v => [v.id, v]));1029  const visitById = new Map(routePlan.visits.map(v => [v.id, v]));1030  const visitOrderMap = new Map();1031 1032  // Build stop order for each visit1033  routePlan.vehicles.forEach(vehicle => {1034    vehicle.visits.forEach((visitId, index) => {1035      visitOrderMap.set(visitId, index + 1);1036    });1037  });1038 1039  // Vehicle groups with names and status summary1040  $.each(routePlan.vehicles, function (index, vehicle) {1041    const vehicleName = vehicle.name || `Vehicle ${vehicle.id}`;1042    const { totalDemand, capacity } = vehicle;1043    const percentage = Math.min((totalDemand / capacity) * 100, 100);1044    const overCapacity = totalDemand > capacity;1045 1046    // Count late visits for this vehicle1047    const vehicleVisits = vehicle.visits.map(id => visitById.get(id)).filter(v => v);1048    const lateCount = vehicleVisits.filter(v => {1049      if (!v.departureTime) return false;1050      const departure = JSJoda.LocalDateTime.parse(v.departureTime);1051      const maxEnd = JSJoda.LocalDateTime.parse(v.maxEndTime);1052      return departure.isAfter(maxEnd);1053    }).length;1054 1055    const statusIcon = lateCount > 01056      ? `<i class="fas fa-exclamation-triangle timeline-status-late timeline-status-icon" title="${lateCount} late"></i>`1057      : vehicle.visits.length > 01058        ? `<i class="fas fa-check-circle timeline-status-ontime timeline-status-icon" title="All on-time"></i>`1059        : '';1060 1061    const progressBarClass = overCapacity ? 'bg-danger' : '';1062 1063    const vehicleWithLoad = `1064      <h5 class="card-title mb-1">${vehicleName}${statusIcon}</h5>1065      <div class="progress" style="height: 16px;" title="Cargo: ${totalDemand} / ${capacity}">1066        <div class="progress-bar ${progressBarClass}" role="progressbar" style="width: ${percentage}%">1067          ${totalDemand}/${capacity}1068        </div>1069      </div>`;1070    byVehicleGroupData.add({ id: vehicle.id, content: vehicleWithLoad });1071  });1072 1073  $.each(routePlan.visits, function (index, visit) {1074    const minStartTime = JSJoda.LocalDateTime.parse(visit.minStartTime);1075    const maxEndTime = JSJoda.LocalDateTime.parse(visit.maxEndTime);1076    const serviceDuration = JSJoda.Duration.ofSeconds(visit.serviceDuration);1077    const customerType = getCustomerType(visit);1078    const stopNumber = visitOrderMap.get(visit.id);1079 1080    const visitGroupElement = $(`<div/>`).append(1081      $(`<h5 class="card-title mb-1"/>`).html(1082        `<i class="fas ${customerType.icon}" style="color: ${customerType.color}"></i> ${visit.name}`1083      ),1084    ).append(1085      $(`<small class="text-muted"/>`).text(customerType.label)1086    );1087    byVisitGroupData.add({1088      id: visit.id,1089      content: visitGroupElement.html(),1090    });1091 1092    // Time window per visit.1093    byVisitItemData.add({1094      id: visit.id + "_readyToDue",1095      group: visit.id,1096      start: visit.minStartTime,1097      end: visit.maxEndTime,1098      type: "background",1099      style: "background-color: #8AE23433",1100    });1101 1102    if (visit.vehicle == null) {1103      const byJobJobElement = $(`<div/>`).append(1104        $(`<span/>`).html(`<i class="fas fa-exclamation-circle text-danger me-1"></i>Unassigned`),1105      );1106 1107      // Unassigned are shown at the beginning of the visit's time window; the length is the service duration.1108      byVisitItemData.add({1109        id: visit.id + "_unassigned",1110        group: visit.id,1111        content: byJobJobElement.html(),1112        start: minStartTime.toString(),1113        end: minStartTime.plus(serviceDuration).toString(),1114        style: "background-color: #EF292999",1115      });1116    } else {1117      const arrivalTime = JSJoda.LocalDateTime.parse(visit.arrivalTime);1118      const beforeReady = arrivalTime.isBefore(minStartTime);1119      const departureTime = JSJoda.LocalDateTime.parse(visit.departureTime);1120      const afterDue = departureTime.isAfter(maxEndTime);1121 1122      // Get vehicle info for display1123      const vehicleInfo = vehicleById.get(visit.vehicle);1124      const vehicleName = vehicleInfo ? (vehicleInfo.name || `Vehicle ${visit.vehicle}`) : `Vehicle ${visit.vehicle}`;1125 1126      // Stop badge for service segment1127      const stopBadge = stopNumber ? `<span class="timeline-stop-badge">${stopNumber}</span>` : '';1128 1129      // Status icon based on timing1130      const statusIcon = afterDue1131        ? `<i class="fas fa-exclamation-triangle timeline-status-late timeline-status-icon" title="Late"></i>`1132        : `<i class="fas fa-check timeline-status-ontime timeline-status-icon" title="On-time"></i>`;1133 1134      const byVehicleElement = $(`<div/>`)1135        .append($(`<span/>`).html(1136          `${stopBadge}<i class="fas ${customerType.icon}" style="color: ${customerType.color}"></i> ${visit.name}${statusIcon}`1137        ));1138 1139      const byVisitElement = $(`<div/>`)1140        .append(1141          $(`<span/>`).html(1142            `${stopBadge}${vehicleName}${statusIcon}`1143          ),1144        );1145 1146      const byVehicleTravelElement = $(`<div/>`).append(1147        $(`<span/>`).html(`<i class="fas fa-route text-warning me-1"></i>Travel`),1148      );1149 1150      const previousDeparture = arrivalTime.minusSeconds(1151        visit.drivingTimeSecondsFromPreviousStandstill,1152      );1153      byVehicleItemData.add({1154        id: visit.id + "_travel",1155        group: visit.vehicle,1156        subgroup: visit.vehicle,1157        content: byVehicleTravelElement.html(),1158        start: previousDeparture.toString(),1159        end: visit.arrivalTime,1160        style: "background-color: #f7dd8f90",1161      });1162 1163      if (beforeReady) {1164        const byVehicleWaitElement = $(`<div/>`).append(1165          $(`<span/>`).html(`<i class="fas fa-clock timeline-status-early me-1"></i>Wait`),1166        );1167 1168        byVehicleItemData.add({1169          id: visit.id + "_wait",1170          group: visit.vehicle,1171          subgroup: visit.vehicle,1172          content: byVehicleWaitElement.html(),1173          start: visit.arrivalTime,1174          end: visit.minStartTime,1175          style: "background-color: #93c5fd80",1176        });1177      }1178 1179      let serviceElementBackground = afterDue ? "#EF292999" : "#83C15955";1180 1181      byVehicleItemData.add({1182        id: visit.id + "_service",1183        group: visit.vehicle,1184        subgroup: visit.vehicle,1185        content: byVehicleElement.html(),1186        start: visit.startServiceTime,1187        end: visit.departureTime,1188        style: "background-color: " + serviceElementBackground,1189      });1190      byVisitItemData.add({1191        id: visit.id,1192        group: visit.id,1193        content: byVisitElement.html(),1194        start: visit.startServiceTime,1195        end: visit.departureTime,1196        style: "background-color: " + serviceElementBackground,1197      });1198    }1199  });1200 

Showing the first 1,200 of 1608 lines. Download the file for the rest.