Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
62 changes: 39 additions & 23 deletions java/vehicle-routing/src/main/resources/META-INF/resources/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment on lines +26 to 32

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vis-timeline is being fed HTML strings (icons, spans) via content: ...html(), but xss: { disabled: false } enables XSS filtering in vis-timeline and may escape/strip this HTML (breaking icon rendering). This also diverges from the pattern used in other quickstarts (they set disabled: true). If you need HTML content, keep xss.disabled set to true here and rely on the existing jQuery .text() usage to keep user-provided values escaped.

Copilot uses AI. Check for mistakes.
zoomMin: 1000 * 60 * 60, // A single hour in milliseconds
Expand All @@ -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

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above for the visit timeline: xss: { disabled: false } can cause vis-timeline to sanitize/escape the HTML content strings, leading to broken rendering. Align this with the other quickstarts and set xss.disabled to true if HTML content is required.

Suggested change
xss: {disabled: false}, // Items are XSS safe through JQuery
xss: {disabled: true}, // Items are XSS safe through JQuery

Copilot uses AI. Check for mistakes.
stack: false,
stackSubgroups: false,
zoomMin: 1000 * 60 * 60, // A single hour in milliseconds
Expand Down Expand Up @@ -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`;
}
Expand Down Expand Up @@ -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));
}
Expand All @@ -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 = `<h5 class="card-title mb-1">vehicle-${vehicle.id}</h5>
<div class="progress" data-bs-toggle="tooltip-load" data-bs-placement="left"
data-html="true" title="Cargo: ${totalDemand} / Capacity: ${capacity}">
<div class="progress-bar" role="progressbar" style="width: ${percentage}%">
${totalDemand}/${capacity}
const color = colorByVehicle(vehicle);
const vehicleWithLoad = `<div style="padding:2px 0">
<div class="d-flex align-items-center gap-1 mb-1">
<span style="width:9px;height:9px;border-radius:50%;background:${color.bg};display:inline-block;flex-shrink:0"></span>
<span class="small fw-semibold">Vehicle ${id}</span>
</div>
</div>`
<div class="progress" style="height:4px;width:70px" data-bs-toggle="tooltip-load"
data-bs-placement="left" data-html="true" title="Cargo: ${totalDemand} / Capacity: ${capacity}">
<div class="progress-bar" role="progressbar" style="width: ${percentage}%"></div>
</div>
</div>`;
byVehicleGroupData.add({id: vehicle.id, content: vehicleWithLoad});
});

Expand All @@ -238,8 +247,8 @@ function renderTimelines(routePlan) {
const maxEndTime = JSJoda.LocalDateTime.parse(visit.maxEndTime);
const serviceDuration = JSJoda.Duration.ofSeconds(visit.serviceDuration);

const visitGroupElement = $(`<div/>`)
.append($(`<h5 class="card-title mb-1"/>`).text(`${visit.name}`));
const visitGroupElement = $(`<div style="padding:2px 0"/>`)
.append($(`<span class="small fw-semibold"/>`).text(visit.name));
byVisitGroupData.add({
id: visit.id,
content: visitGroupElement.html()
Expand All @@ -257,7 +266,8 @@ function renderTimelines(routePlan) {

if (visit.vehicle == null) {
const byJobJobElement = $(`<div/>`)
.append($(`<h5 class="card-title mb-1"/>`).text(`Unassigned`));
.append($(`<i class="fas fa-times fa-fw me-1"/>`))
.append($(`<span class="small fw-semibold"/>`).text('Unassigned'));

// Unassigned are shown at the beginning of the visit's time window; the length is the service duration.
byVisitItemData.add({
Expand All @@ -275,15 +285,16 @@ function renderTimelines(routePlan) {
const afterDue = arrivalPlusService.isAfter(maxEndTime);

const byVehicleElement = $(`<div/>`)
.append('<div/>')
.append($(`<h5 class="card-title mb-1"/>`).text(visit.name));
.append($(`<i class="fas fa-map-marker-alt fa-fw me-1"/>`))
.append($(`<span class="small fw-semibold"/>`).text(visit.name));

const byVisitElement = $(`<div/>`)
// visit.vehicle is the vehicle.id due to Jackson serialization
.append($(`<h5 class="card-title mb-1"/>`).text('vehicle-' + visit.vehicle));
.append($(`<i class="fas fa-truck fa-fw me-1"/>`))
.append($(`<span class="small fw-semibold"/>`).text('Vehicle ' + visit.vehicle));

const byVehicleTravelElement = $(`<div/>`)
.append($(`<h5 class="card-title mb-1"/>`).text('Travel'));
.append($(`<i class="fas fa-car fa-fw me-1"/>`))
.append($(`<span class="small fw-semibold"/>`).text('Travel'));

const previousDeparture = arrivalTime.minusSeconds(visit.drivingTimeSecondsFromPreviousStandstill);
byVehicleItemData.add({
Expand All @@ -297,13 +308,14 @@ function renderTimelines(routePlan) {
});
if (beforeReady) {
const byVehicleWaitElement = $(`<div/>`)
.append($(`<h5 class="card-title mb-1"/>`).text('Wait'));
.append($(`<i class="fas fa-hourglass-half fa-fw me-1"/>`))
.append($(`<span class="small fw-semibold"/>`).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
});
Expand Down Expand Up @@ -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: $(`<div/>`).append($(`<h5 class="card-title mb-1"/>`).text('Travel')).html(),
content: $(`<div/>`).append($(`<i class="fas fa-home fa-fw me-1"/>`)).append($(`<span class="small fw-semibold"/>`).text('Return')).html(),
start: lastVisit.departureTime,
end: vehicle.arrivalTime,
style: "background-color: #f7dd8f90"
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new viewport-filling CSS sets body and the top-level .tab-content to overflow: hidden, which prevents scrolling in non-demo panes such as the REST API Guide (#rest) and REST API iframe tab. Consider limiting the no-scroll layout rules to the Demo UI pane only (e.g., apply overflow hiding under #demo), or explicitly enable scrolling on #rest/#openapi (e.g., overflow-y: auto with a constrained height).

Suggested change
body > .tab-content { flex: 1; min-height: 0; overflow: hidden; }
body > .tab-content { flex: 1; min-height: 0; overflow-y: auto; }

Copilot uses AI. Check for mistakes.

/* 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; }
</style>
</head>
<body>

<header class="bg-white shadow">
<header class="bg-white shadow mb-3">
<div class="container-fluid">
<nav class="navbar navbar-expand-lg mb-3">
<a class="navbar-brand" style="width: 200px;overflow: hidden;">
Expand Down Expand Up @@ -72,8 +108,8 @@
<blockquote>Generate optimal route plan of a vehicle fleet with limited vehicle capacity and time windows. <span id="info" class="fw-bold"></span><br/>New visits may be added by clicking anywhere inside the map. </blockquote>
<div class="container-fluid mb-2">
<div class="row justify-content-start">
<div class="col-9">
<ul class="nav nav-pills col" role="tablist">
<div class="col-6">
<ul class="nav nav-pills col-auto" 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"
Expand All @@ -92,14 +128,13 @@
</li>
</ul>
</div>
<div class="col-3">
<div class="col-6 d-flex justify-content-end">
<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="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>
Expand All @@ -111,15 +146,19 @@
<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">
<div id="map" style="width: 100%; height: 100vh;"></div>
<div id="map"></div>
</div>
<div class="col-5 col-lg-4 col-xl-3" style="height: 100vh; overflow-y: scroll;">
<div class="col-5 col-lg-4 col-xl-3">
<div class="row pt-2 row-cols-1">
<div class="col">
<h5>
Solution summary
</h5>
<table class="table">
<tr>
<td>Score:</td>
<td><span id="score" class="fw-bold">?</span></td>
</tr>
<tr>
<td>Total driving time:</td>
<td><span id="drivingTime">unknown</span></td>
Expand Down Expand Up @@ -152,8 +191,27 @@ <h5>Vehicles</h5>


<div class="tab-pane fade" id="byVehiclePanel" role="tabpanel" aria-labelledby="byVehicleTab">
<div class="d-flex align-items-center flex-wrap gap-2 px-3 py-2 border-bottom bg-light">
<span class="text-muted small">Each row shows a vehicle's day: travel blocks, optional wait, and service stops.</span>
<div class="ms-auto d-flex gap-3 flex-wrap">
<span class="d-flex align-items-center gap-1 small"><span class="legend-swatch" style="background:#f7dd8f;"></span>Travel</span>
<span class="d-flex align-items-center gap-1 small"><span class="legend-swatch" style="background:#97C2FC;"></span>Wait</span>
<span class="d-flex align-items-center gap-1 small"><span class="legend-swatch" style="background:#83C159;"></span>Service</span>
<span class="d-flex align-items-center gap-1 small"><span class="legend-swatch" style="background:#EF2929;"></span>Late</span>
</div>
</div>
<div id="byVehicleTimeline"></div>
</div>
<div class="tab-pane fade" id="byVisitPanel" role="tabpanel" aria-labelledby="byVisitTab">
<div class="d-flex align-items-center flex-wrap gap-2 px-3 py-2 border-bottom bg-light">
<span class="text-muted small">Each row shows a visit's assigned vehicle and where it falls relative to its time window.</span>
<div class="ms-auto d-flex gap-3 flex-wrap">
<span class="d-flex align-items-center gap-1 small"><span class="legend-swatch" style="background:#8AE234;opacity:0.4;"></span>Time window</span>
<span class="d-flex align-items-center gap-1 small"><span class="legend-swatch" style="background:#83C159;"></span>Assigned</span>
<span class="d-flex align-items-center gap-1 small"><span class="legend-swatch" style="background:#EF2929;"></span>Late / Unassigned</span>
</div>
</div>
<div id="byVisitTimeline"></div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ function visualizeConstraintAnalysis(analysisTBody, constraintIndex, constraintA

let row = $(`<tr/>`);
row.append($(`<td/>`).html(icon))
.append($(`<td/>`).text(constraintAnalysis.name).css({textAlign: 'left'}))
.append($(`<td/>`).text(constraintAnalysis.name ?? constraintAnalysis.id).css({textAlign: 'left'}))
.append($(`<td/>`).text(constraintAnalysis.type))
.append($(`<td/>`).html(`<b>${constraintAnalysis.matches.length}</b>`))
.append($(`<td/>`).text(constraintAnalysis.weight))
Expand Down
Loading