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><metaname="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><linkhref="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><inputtype="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 = vehicleColorlet routeDescription = `${stop.type.toUpperCase()} ${stop.location}`;let markerSymbol = jlet priority = 1 switch (stop.type) {case "start":markerSymbol = "A";priority = 0break;case "end":markerSymbol = "B";priority = 0break;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>