diff --git a/java/vehicle-routing/src/main/java/org/acme/vehiclerouting/rest/VehicleRouteDemoResource.java b/java/vehicle-routing/src/main/java/org/acme/vehiclerouting/rest/VehicleRouteDemoResource.java index dfe113d468..98bb982472 100644 --- a/java/vehicle-routing/src/main/java/org/acme/vehiclerouting/rest/VehicleRouteDemoResource.java +++ b/java/vehicle-routing/src/main/java/org/acme/vehiclerouting/rest/VehicleRouteDemoResource.java @@ -44,10 +44,13 @@ public class VehicleRouteDemoResource { private static final LocalTime AFTERNOON_WINDOW_END = LocalTime.of(18, 0); public enum DemoData { - PHILADELPHIA(0, 55, 6, LocalTime.of(7, 30), + PHILADELPHIA(2, 55, 6, LocalTime.of(7, 30), 1, 2, 15, 30, new Location(39.7656099067391, -76.83782328143754), new Location(40.77636644354855, -74.9300739430771)), + GHENT(1, 65, 6, LocalTime.of(7, 30), + 1, 2, 15, 30, + new Location(50.990000, 3.620000), new Location(51.130000, 3.840000)), HARTFORT(1, 50, 6, LocalTime.of(7, 30), 1, 3, 20, 30, new Location(41.48366520850297, -73.15901689943055), diff --git a/java/vehicle-routing/src/main/resources/META-INF/resources/app.js b/java/vehicle-routing/src/main/resources/META-INF/resources/app.js index c9f8300abe..16cdbd174e 100644 --- a/java/vehicle-routing/src/main/resources/META-INF/resources/app.js +++ b/java/vehicle-routing/src/main/resources/META-INF/resources/app.js @@ -23,11 +23,11 @@ const routeGroup = L.layerGroup().addTo(map); /************************************ Time line constants and variable definitions ************************************/ -const byVehiclePanel = document.getElementById("byVehiclePanel"); +const byVehiclePanel = document.getElementById("byVehicleTimeline"); const byVehicleTimelineOptions = { timeAxis: {scale: "hour"}, orientation: {axis: "top"}, - xss: {disabled: true}, // Items are XSS safe through JQuery + xss: {disabled: false}, // Items are XSS safe through JQuery stack: false, stackSubgroups: false, zoomMin: 1000 * 60 * 60, // A single hour in milliseconds @@ -37,12 +37,12 @@ const byVehicleGroupData = new vis.DataSet(); const byVehicleItemData = new vis.DataSet(); const byVehicleTimeline = new vis.Timeline(byVehiclePanel, byVehicleItemData, byVehicleGroupData, byVehicleTimelineOptions); -const byVisitPanel = document.getElementById("byVisitPanel"); +const byVisitPanel = document.getElementById("byVisitTimeline"); const byVisitTimelineOptions = { timeAxis: {scale: "hour"}, orientation: {axis: "top"}, verticalScroll: true, - xss: {disabled: true}, // Items are XSS safe through JQuery + xss: {disabled: false}, // Items are XSS safe through JQuery stack: false, stackSubgroups: false, zoomMin: 1000 * 60 * 60, // A single hour in milliseconds @@ -107,6 +107,11 @@ function colorByVehicle(vehicle) { return vehicle === null ? null : pickColor('vehicle' + vehicle.id); } +function formatScore(score) { + if (!score) return '?'; + return score.replace('hard', 'H').replace('medium', 'M').replace('soft', 'S'); +} + function formatDrivingTime(drivingTimeInSeconds) { return `${Math.floor(drivingTimeInSeconds / 3600)}h ${Math.round((drivingTimeInSeconds % 3600) / 60)}m`; } @@ -209,7 +214,7 @@ function renderRoutes(solution) { } // Summary - $('#score').text(solution.score); + $('#score').text(formatScore(solution.score)); $("#info").text(`This dataset has ${solution.visits.length} visits who need to be assigned to ${solution.vehicles.length} vehicles.`); $('#drivingTime').text(formatDrivingTime(solution.totalDrivingTimeSeconds)); } @@ -221,15 +226,19 @@ function renderTimelines(routePlan) { byVisitItemData.clear(); $.each(routePlan.vehicles, function (index, vehicle) { - const {totalDemand, capacity} = vehicle + const {id, totalDemand, capacity} = vehicle; const percentage = totalDemand / capacity * 100; - const vehicleWithLoad = `
vehicle-${vehicle.id}
-
-
- ${totalDemand}/${capacity} + const color = colorByVehicle(vehicle); + const vehicleWithLoad = `
+
+ + Vehicle ${id}
-
` +
+
+
+
`; byVehicleGroupData.add({id: vehicle.id, content: vehicleWithLoad}); }); @@ -238,8 +247,8 @@ function renderTimelines(routePlan) { const maxEndTime = JSJoda.LocalDateTime.parse(visit.maxEndTime); const serviceDuration = JSJoda.Duration.ofSeconds(visit.serviceDuration); - const visitGroupElement = $(`
`) - .append($(`
`).text(`${visit.name}`)); + const visitGroupElement = $(`
`) + .append($(``).text(visit.name)); byVisitGroupData.add({ id: visit.id, content: visitGroupElement.html() @@ -257,7 +266,8 @@ function renderTimelines(routePlan) { if (visit.vehicle == null) { const byJobJobElement = $(`
`) - .append($(`
`).text(`Unassigned`)); + .append($(``)) + .append($(``).text('Unassigned')); // Unassigned are shown at the beginning of the visit's time window; the length is the service duration. byVisitItemData.add({ @@ -275,15 +285,16 @@ function renderTimelines(routePlan) { const afterDue = arrivalPlusService.isAfter(maxEndTime); const byVehicleElement = $(`
`) - .append('
') - .append($(`
`).text(visit.name)); + .append($(``)) + .append($(``).text(visit.name)); const byVisitElement = $(`
`) - // visit.vehicle is the vehicle.id due to Jackson serialization - .append($(`
`).text('vehicle-' + visit.vehicle)); + .append($(``)) + .append($(``).text('Vehicle ' + visit.vehicle)); const byVehicleTravelElement = $(`
`) - .append($(`
`).text('Travel')); + .append($(``)) + .append($(``).text('Travel')); const previousDeparture = arrivalTime.minusSeconds(visit.drivingTimeSecondsFromPreviousStandstill); byVehicleItemData.add({ @@ -297,13 +308,14 @@ function renderTimelines(routePlan) { }); if (beforeReady) { const byVehicleWaitElement = $(`
`) - .append($(`
`).text('Wait')); + .append($(``)) + .append($(``).text('Wait')); byVehicleItemData.add({ id: visit.id + '_wait', group: visit.vehicle, // visit.vehicle is the vehicle.id due to Jackson serialization subgroup: visit.vehicle, - content: byVehicleWaitElement.ahtml(), + content: byVehicleWaitElement.html(), start: visit.arrivalTime, end: visit.minStartTime }); @@ -340,7 +352,7 @@ function renderTimelines(routePlan) { id: vehicle.id + '_travelBackToHomeLocation', group: vehicle.id, // visit.vehicle is the vehicle.id due to Jackson serialization subgroup: vehicle.id, - content: $(`
`).append($(`
`).text('Travel')).html(), + content: $(`
`).append($(``)).append($(``).text('Return')).html(), start: lastVisit.departureTime, end: vehicle.arrivalTime, style: "background-color: #f7dd8f90" @@ -353,6 +365,10 @@ function renderTimelines(routePlan) { byVehicleTimeline.setWindow(routePlan.startDateTime, routePlan.endDateTime); byVisitTimeline.setWindow(routePlan.startDateTime, routePlan.endDateTime); } + requestAnimationFrame(() => { + if ($('#byVehiclePanel').hasClass('active')) byVehicleTimeline.redraw(); + if ($('#byVisitPanel').hasClass('active')) byVisitTimeline.redraw(); + }); } function analyze() { diff --git a/java/vehicle-routing/src/main/resources/META-INF/resources/index.html b/java/vehicle-routing/src/main/resources/META-INF/resources/index.html index 0c2cc91742..b22ce9dc74 100644 --- a/java/vehicle-routing/src/main/resources/META-INF/resources/index.html +++ b/java/vehicle-routing/src/main/resources/META-INF/resources/index.html @@ -22,11 +22,47 @@ .nav-pills { --bs-nav-pills-link-active-bg: var(--ts-violet-1-rgb); } + .legend-swatch { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 3px; + flex-shrink: 0; + } + + /* Viewport-filling layout — no page-level scrollbar */ + html, body { height: 100%; } + body { + display: flex; + flex-direction: column; + overflow: hidden; + } + body > header, body > footer { flex-shrink: 0; height: 4rem; } + body > .tab-content { flex: 1; min-height: 0; overflow: hidden; } + + /* Demo tab: flex column so blockquote + nav stay fixed, content fills rest */ + #demo { + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + } + #demo > .sticky-top, + #demo > blockquote, + #demo > .container-fluid { flex-shrink: 0; } + #demo > .tab-content { flex: 1; min-height: 0; overflow: hidden; } + + /* Map panel */ + #mapPanel { height: 100%; overflow: hidden; } + #mapPanel > .row { height: 100%; margin: 0; } + #mapPanel > .row > [class*="col-"] { height: 100%; } + #mapPanel > .row > [class*="col-"]:last-child { overflow-y: auto; } + #map { width: 100%; height: 100% !important; } -
+