TrackAsiaTrackAsia GL JS DocsExamplesVehicle Routing Problem solver

Vehicle Routing Problem solver

Example solving Vehicle Routing Problem.

See the detail documentation at Api Integration > Vehicle Routing Problem.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Vehicle Routing Problem solver</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<script src="https://unpkg.com/trackasia-gl@1.0.5/dist/trackasia-gl.js"></script>
<link href="https://unpkg.com/trackasia-gl@1.0.5/dist/trackasia-gl.css" rel="stylesheet" />
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Vehicle Routing Example</title>
<meta
name="viewport"
content="initial-scale=1,maximum-scale=1,user-scalable=no"
/>
<script src="https://unpkg.com/trackasia-gl@1.0.5/dist/trackasia-gl.js"></script>
<script src="https://unpkg.com/@mapbox/polyline@1.2.1/src/polyline.js"></script>
<link
href="https://unpkg.com/trackasia-gl@1.0.5/dist/trackasia-gl.css"
rel="stylesheet"
/>
<style>
body {
margin: 0;
padding: 0;
}
#map {
top: 0;
bottom: 0;
position: fixed;
width: 70%;
}
#file {
margin: 10px 0;
}
#features {
width: 30%;
margin-left: 70%;
font-family: sans-serif;
overflow-y: scroll;
background-color: #fafafa;
}
section {
padding: 5px 15px;
line-height: 15px;
border-bottom: 1px solid #ddd;
opacity: 0.25;
font-size: 13px;
}
section.active {
opacity: 1;
}
li {
margin-left: -20px;
}
.custom-marker {
width: 32px;
height: 32px;
border-radius: 80%;
background: orange;
display: inline-block;
position: relative;
border-bottom-left-radius: 0;
transform: rotate(-45deg);
}
.custom-marker-content {
top: 50%;
left: 30%;
font-size: 10px;
position: absolute;
transform: rotate(45deg) translate(-100%, -25%);
opacity: 0.8;
}
.custom-marker::before {
content: '';
background: white;
width: 75%;
height: 75%;
border-radius: 100%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.custom-popup {
z-index: 3;
}
#example-download {
color: -webkit-link;
cursor: pointer;
text-decoration: underline;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="features">
<section class="active">
<h3>Input</h3>
<input
type="file"
id="file"
name="file"
accept="application/json,.json"
/>
<a id="example-download" onclick="downloadExample(this);"
>Download Example</a
>
</section>
<section id="vehicles" class="active">
<h3>Vehicles</h3>
<ul id="vehicles-list"></ul>
</section>
<section id="jobs" class="active">
<h3>Jobs</h3>
<ul id="jobs-list"></ul>
</section>
<section id="summary">
<h3>Summary</h3>
<ul id="summary-list"></ul>
</section>
<section id="routes">
<h3>Routes</h3>
<ul id="routes-list"></ul>
</section>
<section id="unassigned">
<h3>Unassigned</h3>
<ul id="unassigned-list"></ul>
</section>
</div>
<script>
const map = new trackasiagl.Map({
container: 'map',
style: 'https://maps.track-asia.com/styles/v1/streets.json?key=public_key',
center: {"lat":10.762622,"lng":106.660172},
zoom: 5
});
const example = {"vehicles":[{"id":1,"description":"Van 1","start":[106.6177357,10.7409972],"end":[106.5983012,10.8879148],"profile":"car","time_window":[1685953800,1686418200],"skills":[1,7000],"capacity":[5]},{"id":2,"description":"Van 2","start":[106.6177357,10.7409972],"end":[106.7079045,10.8152603],"profile":"car","time_window":[1685953800,1686418200],"skills":[1,7000],"breaks":[{"id":1000,"time_windows":[[1685966400,1685970000]],"service":3600},{"id":1,"time_windows":[[1685986200,1685988000]],"service":54000}],"capacity":[10]},{"id":3,"description":"Truck 1","start":[106.620553,10.729023],"profile":"car","time_window":[1685953800,1686418200],"skills":[1,1000],"speed_factor":0.6,"capacity":[20]}],"shipments":[{"amount":[1],"pickup":{"id":1,"service":60,"location":[106.655077,10.750909],"description":"Chợ Kim Biên, 26/4 Hẻm 24 Trang Tử, Phường 13, Quận 5, Thành phố Hồ Chí Minh","time_windows":[[1685986200,1685988000]]},"delivery":{"id":1,"service":300,"location":[106.682258,10.759913],"description":"Trường Đại Học Sài Gòn, 273 An Dương Vương, Phường 3, Quận 5, Thành phố Hồ Chí Minh","time_windows":[[1685948400,1685980800],[1686034800,1686067200],[1686121200,1686153600],[1686207600,1686240000],[1686294000,1686326400],[1686380400,1686412800]]},"skills":[1]},{"amount":[1],"pickup":{"id":2,"service":60,"location":[106.655077,10.750909],"description":"Chợ Kim Biên, 26/4 Hẻm 24 Trang Tử, Phường 13, Quận 5, Thành phố Hồ Chí Minh","time_windows":[[1685986200,1685988000]]},"delivery":{"id":2,"service":300,"location":[106.713724,10.722809],"description":"Crescent Mall, 101 Đường Tôn Dật Tiên, Phường Tân Phong, Quận 7, Thành phố Hồ Chí Minh","time_windows":[[1685948400,1685980800],[1686034800,1686067200],[1686121200,1686153600],[1686207600,1686240000],[1686294000,1686326400],[1686380400,1686412800]]},"skills":[1000]},{"amount":[1],"pickup":{"id":3,"service":60,"location":[106.661068,10.757621],"description":"Hùng Vương Plaza, 130 Đường Hồng Bàng, Phường 12, Quận 5, Thành phố Hồ Chí Minh","time_windows":[[1685953800,1686253800]]},"delivery":{"id":3,"service":300,"location":[106.704779,10.786424],"description":"Tòa Nhà Petrolimex, 1 Lê Duẩn, Phường Bến Nghé, Quận 1, Thành phố Hồ Chí Minh","time_windows":[[1685953800,1686418200]]},"skills":[7000]}]}
map.on('load', function () {
solve(example)
})
function downloadExample(el) {
var data = "text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(example));
// what to return in order to show download window?
el.setAttribute("href", "data:"+data);
el.setAttribute("download", "vrp.json");
}
const vehicleColors = ["blue", "purple", "teal", "indigo", "amber", "orange", "deepOrange", "brown", "gray", "blueGray", "lightGreen", "lime", "cyan"];
const vehicleColorMap = {};
document.getElementById('file').addEventListener('change', handleFileSelect, false);
function createHTMLNode(htmlCode, color) {
const htmlNode = document.createElement('span');
htmlNode.innerHTML = htmlCode;
htmlNode.className = 'vehicle';
htmlNode.style.color = color;
return htmlNode;
}
function createMarkerElement(vehicleID, textContent, color, contentColor) {
const markerEl = document.createElement('div');
const customMarkerEl = document.createElement('div');
const contentEl = document.createElement('div');
if (color) customMarkerEl.style.backgroundColor = color;
if (contentColor) contentEl.style.color = contentColor;
customMarkerEl.classList.add('custom-marker', `draw-vehicle-${vehicleID}`);
contentEl.classList.add('custom-marker-content');
contentEl.textContent = textContent;
const markerSize = textContent === "A" || textContent === "B" ? '25px' : '20px';
customMarkerEl.style.width = markerSize;
customMarkerEl.style.height = markerSize;
customMarkerEl.appendChild(contentEl);
markerEl.appendChild(customMarkerEl);
return markerEl;
}
function createPopup(route, stop, j) {
const vehicleTitle = route.description ? `Vehicle ${route.description}` : `Vehicle ${route.vehicle}`;
let htmlContent = `<h3>${vehicleTitle}</h3> ${j}. ${stop.type.toUpperCase()}`;
if (stop.id) htmlContent += ` ${stop.id}`;
Object.keys(stop).forEach((key) => {
if (key !== "location" && key !== "type" && key !== "id") {
if (key === "arrival") {
htmlContent += `<p>- ${key}: ${stop[key]} (${new Date(stop[key] * 1000).toLocaleString()})</p>`;
} else if (['duration', 'setup', 'service', 'waiting_time'].includes(key)) {
htmlContent += `<p>- ${key}: ${stop[key]}s (${(stop[key] / 3600).toFixed(2)}h)</p>`;
}
else if (key === "distance") {
htmlContent += `<p>- ${key}: ${stop[key]} (${(stop[key] / 1000).toFixed(2)}km)</p>`;
}
else {
htmlContent += `<p>- ${key}: ${stop[key]}</p>`;
}
}
});
return new trackasiagl.Popup({ offset: 5, className: 'custom-popup' }).setHTML(htmlContent);
}
function toggleCheckbox(element) {
const vehicleID = element.getAttribute("data-vehicle-id");
const markers = document.getElementsByClassName(`draw-vehicle-${vehicleID}`);
const visibility = element.checked ? "visible" : "hidden";
const layoutVisibility = element.checked ? 'visible' : 'none';
for (let marker of markers) {
marker.style.visibility = visibility
}
map.setLayoutProperty(`route-${vehicleID}`, 'visibility', layoutVisibility);
}
function handleFileSelect(evt) {
clearLists();
const file = evt.target.files[0];
const reader = new FileReader();
reader.onload = (theFile) => {
const geoJSONcontent = JSON.parse(theFile.target.result);
solve(geoJSONcontent);
};
reader.readAsText(file, 'UTF-8');
}
function clearLists() {
document.getElementById("vehicles-list").innerHTML = "";
document.getElementById("jobs-list").innerHTML = "";
document.getElementById("routes-list").innerHTML = "";
document.getElementById("unassigned-list").innerHTML = "";
document.getElementById("summary-list").innerHTML = "";
document.querySelectorAll(".custom-marker").forEach(el => el.remove());
map.getStyle().layers.forEach((layer) => {
if(layer.id.match(/route-/g)){
console.log(layer.id)
map.removeLayer(layer.id);
map.removeSource(layer.id);
}
});
}
function solve(input) {
const focusPoint = input.shipments?.[0]?.pickup?.location || input.jobs?.[0]?.location || input.vehicles?.[0]?.start;
if (focusPoint) map.flyTo({ center: focusPoint, zoom: 10, speed: 5 });
input.options = { g: true };
populateVehicles(input.vehicles);
if (input.shipments) populateShipments(input.shipments);
if (input.jobs) populateJobs(input.jobs);
fetchRoutes(input);
}
function populateVehicles(vehicles) {
const vehiclesUl = document.getElementById("vehicles-list");
vehicles.forEach((vehicle, i) => {
const color = vehicleColors[i % vehicleColors.length];
vehicleColorMap[vehicle.id] = color;
const li = document.createElement("li");
const title = vehicle.description || `Vehicle ${vehicle.id}`;
li.appendChild(createHTMLNode(title, color));
vehiclesUl.appendChild(li);
});
}
function populateShipments(shipments) {
const shipmentsUl = document.getElementById("jobs-list");
shipments.forEach((shipment, i) => {
const jobsUl = document.createElement("ul");
const pickupJobLi = createJobListItem(`PICKUP ${shipment.pickup.id}: ${shipment.pickup.description}`);
const deliveryJobLi = createJobListItem(`DELIVERY ${shipment.delivery.id}: ${shipment.delivery.description}`);
jobsUl.append(pickupJobLi, deliveryJobLi);
const shipmentLi = document.createElement("li");
shipmentLi.appendChild(document.createTextNode(`Shipment ${i}`));
shipmentLi.appendChild(jobsUl);
shipmentsUl.appendChild(shipmentLi);
});
}
function populateJobs(jobs) {
const jobsUl = document.getElementById("jobs-list");
jobs.forEach((job, i) => {
const jobLi = document.createElement("li");
const jobTitle = job.description ? `Job ${i}: ${job.description}` : `Job ${i}: ${job.location}`;
jobLi.appendChild(document.createTextNode(jobTitle));
jobsUl.appendChild(jobLi);
});
}
function createJobListItem(text) {
const li = document.createElement("li");
li.appendChild(document.createTextNode(text));
return li;
}
async function fetchRoutes(input) {
try {
const target = "https://maps.track-asia.com/api/v1/vrp/?key=public_key";
const response = await fetch(target, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
});
if (response.ok) {
const data = await response.json();
handleRoutesResponse(data);
} else {
alert('Error: ' + response.status);
}
} catch (error) {
console.error('Request failed', error);
alert('Request failed');
}
}
function handleRoutesResponse(response) {
if (response.routes.length > 0) {
document.getElementById("routes").classList.add("active");
response.routes.forEach((route) => {
const vehicleColor = vehicleColorMap[route.vehicle];
const routeCoordinates = polyline.decode(route.geometry).map(c => c.reverse());
const routeUl = document.createElement("ul");
const vehicleTitle = route.description ? `Vehicle ${route.description}` : `Vehicle ${route.vehicle}`;
route.steps.filter(stop => stop.type !== "break").forEach((stop, j) => {
let markerColor = vehicleColor
let routeDescription = `${stop.type.toUpperCase()} ${stop.location}`;
let markerSymbol = j
let priority = 1
switch (stop.type) {
case "start":
markerSymbol = "A";
priority = 0
break;
case "end":
markerSymbol = "B";
priority = 0
break;
case "job":
markerColor = 'green';
break;
case "pickup":
markerColor = 'green';
routeDescription = `${stop.type.toUpperCase()} ${stop.id}: ${stop.description}`;
break;
case "delivery":
markerColor = 'red';
routeDescription = `${stop.type.toUpperCase()} ${stop.id}: ${stop.description}`;
break;
}
const markerElement = createMarkerElement(route.vehicle, markerSymbol, markerColor, vehicleColor);
markerElement.style.zIndex = priority;
const marker = new trackasiagl.Marker(markerElement)
.setLngLat(stop.location)
.setPopup(createPopup(route, stop, j))
.addTo(map);
const stopLi = document.createElement("li");
const staticMarkerElement = markerElement.cloneNode(true);
staticMarkerElement.style.position = "static";
staticMarkerElement.style.transform = "";
staticMarkerElement.firstChild.style.height = "18px";
staticMarkerElement.firstChild.style.width = "18px";
staticMarkerElement.firstChild.className = "custom-marker";
staticMarkerElement.appendChild(document.createTextNode(routeDescription));
stopLi.appendChild(staticMarkerElement);
routeUl.appendChild(stopLi);
});
const routeLi = document.createElement("li");
routeLi.appendChild(createHTMLNode(`<label><input type="checkbox" class="route-vehicle" name="route-vehicle-${route.vehicle}" data-vehicle-id="${route.vehicle}" checked onchange="toggleCheckbox(this)"><span>${vehicleTitle}</span></label>`, vehicleColor));
routeLi.appendChild(routeUl);
document.getElementById("routes-list").appendChild(routeLi);
map.addSource(`route-${route.vehicle}`, {
type: 'geojson',
data: {
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: routeCoordinates
}
}
});
map.addLayer({
id: `route-${route.vehicle}`,
type: 'line',
source: `route-${route.vehicle}`,
layout: {
'line-join': 'round',
'line-cap': 'round'
},
paint: {
'line-color': vehicleColor,
'line-width': 5,
'line-opacity': 0.6
}
});
});
}
if (response.unassigned.length > 0) {
document.getElementById("unassigned").classList.add("active");
response.unassigned.forEach((job) => {
const jobLi = document.createElement("li");
jobLi.appendChild(document.createTextNode(`${job.type.toUpperCase()} ${job.id}: ${job.description}`));
document.getElementById("unassigned-list").appendChild(jobLi);
});
}
const summary = response.summary;
document.getElementById("summary").classList.add("active");
const durationLi = createSummaryListItem(`Duration: ${(summary.duration / 3600).toFixed(2)}h`);
const distanceLi = createSummaryListItem(`Distance: ${summary.distance / 1000}km`);
document.getElementById("summary-list").append(durationLi, distanceLi);
}
function createSummaryListItem(text) {
const li = document.createElement("li");
li.appendChild(document.createTextNode(text));
return li;
}
</script>
</body>
</html>
</body>
</html>