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 = `
`
+
+
`;
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; }
-
+