Planning a Round Tour with Multi-depot Assignment

This use case describes how to plan round tours allowing stops at multiple depots during a tour.

VehiclesThe term vehicle describes what is being routed or planned for. Vehicles are used in route calculation, distance matrix calculation and effectively also in tour planning. In route calculation, vehicle properties like overall size, weight and speed are in focus. In tour planning, it is vehicle properties like capacity and availability. Commonly a vehicle is motorized, like a truck - including its trailer or a car. However also a bike or even a pedestrian are included in this definition. usually execute the orders of one particular depot site, where they are located. This situation leads to classic round trips in most cases. However, sometimes it might be useful to execute orders of a depot site with a vehicle not located there in a second (or any subsequent) trip of a tour, in order to optimize the use of resources or avoid empty drives on route legsA route leg is a part of a route between two consecutive waypoints..

Example | The following picture shows an example of the desired tour structure

Geographical tour structure and stop sequence for round trips with multi-depot assingment.

Benefits

Prerequisites

Please ensure following prerequisites are fulfilled before you start with the use case:

Concepts

Programming Guide

This example explains how the request has to look like in order to to influence the tour structure as mentioned in the use-case description above.

First we define our locations as OffRoadRouteLocations in the default EPSG:4326 format (see RequestBase.coordinateFormat). You could also use OnRoadRouteLocations, see the technical concept on waypoints and route legs for more information on the different types route locationsA route location is the position of an object to be routed to or from, which contains additional information on how to link this position to the road network and whether or or not this position is actually reached. Route locations are used as the input for route calculation and optimization throughout PTV xServer.. Since the use case describes deliveries from different depots to customers, we define some locations as depot and some as customer sites, see Orders, Locations, and Stops for more details. Most likely the locations have specific opening intervals, which may lead to multiple tours if two locations cannot be served by the same vehicle within their opening hours.

The fleet in this example consists of two types of vehicle, each with one available vehicle instance (defined by the number of vehicle ids). The two types only differ in the start and end locations which are located at different depots. To simplify the example code, we assume that the vehicles use direct distance (can be set in the distance mode of the request).

The last part of the example request are the orders, consisting of pickup and delivery orders that model the transportation of goods from a depot to a customer and visit orders if no goods should be transported to the customer.

We pass the request on to planTours. Once xTour has processed the request a callback is invoked which gives us access to the result of the calculation in form of a ToursResponse object.

var DepotA = { "$type": "DepotSite", "id": "DepotA", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.113033, "y": 49.608040 } }, "openingIntervals": [{ "$type": "StartEndInterval", "start": "2016-12-06T08:00:00+01:00", "end": "2016-12-06T18:00:00+01:00" }] }; var DepotB = { "$type": "DepotSite", "id": "DepotB", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.159553, "y": 49.595135 } }, "openingIntervals": [{ "$type": "StartEndInterval", "start": "2016-12-06T08:00:00+01:00", "end": "2016-12-06T18:00:00+01:00" }] }; var Customer1 = { "$type": "CustomerSite", "id": "Customer1", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.097412, "y": 49.609597 } }, "openingIntervals": [{ "$type": "StartDurationInterval", "start": "2016-12-06T10:00:00+01:00", "duration": "300.0" }] }; var Customer2 = { "$type": "CustomerSite", "id": "Customer2", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.097755, "y": 49.614325 } }, "openingIntervals": [{ "$type": "StartDurationInterval", "start": "2016-12-06T10:00:00+01:00", "duration": "300.0" }] }; var Customer3 = { "$type": "CustomerSite", "id": "Customer3", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.098742, "y": 49.618829 } }, "openingIntervals": [{ "$type": "StartDurationInterval", "start": "2016-12-06T10:00:00+01:00", "duration": "7200.0" }] }; var Customer4 = { "$type": "CustomerSite", "id": "Customer4", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.115694, "y": 49.619941 } }, "openingIntervals": [{ "$type": "StartDurationInterval", "start": "2016-12-06T10:00:00+01:00", "duration": "7200.0" }] }; var Customer5 = { "$type": "CustomerSite", "id": "Customer5", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.114835, "y": 49.616771 } }, "openingIntervals": [{ "$type": "StartDurationInterval", "start": "2016-12-06T10:00:00+01:00", "duration": "7200.0" }] }; var Customer6 = { "$type": "CustomerSite", "id": "Customer6", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.155176, "y": 49.607094 } }, "openingIntervals": [{ "$type": "StartDurationInterval", "start": "2016-12-06T10:00:00+01:00", "duration": "300.0" }] }; var Customer7 = { "$type": "CustomerSite", "id": "Customer7", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.159896, "y": 49.605704 } }, "openingIntervals": [{ "$type": "StartDurationInterval", "start": "2016-12-06T10:00:00+01:00", "duration": "7200.0" }] }; var Customer8 = { "$type": "CustomerSite", "id": "Customer8", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.169338, "y": 49.605926 } }, "openingIntervals": [{ "$type": "StartDurationInterval", "start": "2016-12-06T10:00:00+01:00", "duration": "7200.0" }] }; var Customer9 = { "$type": "CustomerSite", "id": "Customer9", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.173543, "y": 49.598973 } }, "openingIntervals": [{ "$type": "StartDurationInterval", "start": "2016-12-06T10:00:00+01:00", "duration": "7200.0" }] }; var Customer10 = { "$type": "CustomerSite", "id": "Customer10", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.165390, "y": 49.591852 } }, "openingIntervals": [{ "$type": "StartDurationInterval", "start": "2016-12-06T10:00:00+01:00", "duration": "7200.0" }] }; var Customer11 = { "$type": "CustomerSite", "id": "Customer11", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.155605, "y": 49.588736 } }, "openingIntervals": [{ "$type": "StartDurationInterval", "start": "2016-12-06T10:00:00+01:00", "duration": "7200.0" }] }; var Customer12 = { "$type": "CustomerSite", "id": "Customer12", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.138353, "y": 49.593632 } }, "openingIntervals": [{ "$type": "StartDurationInterval", "start": "2016-12-06T10:00:00+01:00", "duration": "7200.0" }] }; var Customer13 = { "$type": "CustomerSite", "id": "Customer13", "routeLocation": { "$type": "OffRoadRouteLocation", "offRoadCoordinate": { "x": 6.121702, "y": 49.599919 } }, "openingIntervals": [{ "$type": "StartDurationInterval", "start": "2016-12-06T10:00:00+01:00", "duration": "7200.0" }] }; var locations = [DepotA, DepotB, Customer1, Customer2, Customer3, Customer4, Customer5, Customer6, Customer7, Customer8, Customer9, Customer10, Customer11, Customer12, Customer13]; var map = new L.Map('map', { center: [49.61, 6.125], zoom: 13 }); // Add tile layer to map var tileUrl = xServerUrl + '/services/rest/XMap/tile/{z}/{x}/{y}'; var tileLayer = new L.TileLayer(tileUrl, { minZoom: 3, maxZoom: 18, noWrap: true }).addTo(map); var markers = L.layerGroup().addTo(map); function planSpecificTours() { xtour.planTours({ "locations": locations, "orders": [{ "$type": "PickupDeliveryOrder", "id": "PDOrder1", "quantities": [50.0], "pickupLocationId": "DepotB", "deliveryLocationId": "Customer9" }, { "$type": "PickupDeliveryOrder", "id": "PDOrder2", "quantities": [100.0], "pickupLocationId": "Customer12", "deliveryLocationId": "DepotA" }, { "$type": "PickupDeliveryOrder", "id": "PDOrder3", "quantities": [50.0], "pickupLocationId": "Customer6", "deliveryLocationId": "DepotB" }, { "$type": "PickupDeliveryOrder", "id": "PDOrder4", "quantities": [100.0], "pickupLocationId": "Customer5", "deliveryLocationId": "DepotB" }, { "$type": "PickupDeliveryOrder", "id": "PDOrder5", "quantities": [0.0], "pickupLocationId": "Customer2", "deliveryLocationId": "Customer4" }, { "$type": "PickupDeliveryOrder", "id": "PDOrder6", "quantities": [0.0], "pickupLocationId": "Customer10", "deliveryLocationId": "Customer12" }, { "$type": "PickupDeliveryOrder", "id": "PDOrder7", "quantities": [0.0], "pickupLocationId": "Customer12", "deliveryLocationId": "Customer13" },{ "$type": "VisitOrder", "id": "VisitOrder1", "locationId": "Customer1" },{ "$type": "VisitOrder", "id": "VisitOrder2", "locationId": "Customer3" }, { "$type": "VisitOrder", "id": "VisitOrder3", "locationId": "Customer7" }, { "$type": "VisitOrder", "id": "VisitOrder4", "locationId": "Customer8" }, { "$type": "VisitOrder", "id": "VisitOrder5", "locationId": "Customer11" }], "fleet": { "vehicles": [ { "ids": ["vehicle1"], "maximumQuantityScenarios": [ { "quantities": [100.0] } ], "startLocationId": "DepotA", "endLocationId": "DepotA" }, { "ids": ["vehicle2"], "maximumQuantityScenarios": [ { "quantities": [100.0] } ], "startLocationId": "DepotB", "endLocationId": "DepotB" } ] }, "distanceMode": { "$type": "DirectDistance" } }, function(toursResponse, exception) { displayLocations(); displayTours(toursResponse.tours); }); } function displayLocations() { markers.clearLayers(); for(var i = 0; i < locations.length; i++ ){ var location = locations[i]; switch(location.$type) { case "CustomerSite": displayCustomerSite(location); break; case "DepotSite": displayDepotSite(location); break; default: // VehicleLocation should not be existent in this use-case break; } } } function displayCustomerSite(location) { var point = getLatLngOfLocation(location); var marker = L.marker(point, { icon: getCircleIcon(24, '../../Resources/Images/Showcases/circle_gray.png'), title: location.id }).addTo(map); markers.addLayer(marker); } function displayDepotSite(location) { var point = getLatLngOfLocation(location); var marker = L.marker(point, { icon: getCircleIcon(24, '../../Resources/Images/Showcases/circle_orange.png'), title: location.id }).addTo(map); markers.addLayer(marker); } function getLatLngOfLocation(location) { // Only OffRoadRouteLocations are supported in this use-case var coordinateX = location.routeLocation.offRoadCoordinate.x; var coordinateY = location.routeLocation.offRoadCoordinate.y; var coordinate = L.latLng(coordinateY, coordinateX); return coordinate; } function getCircleIcon(size, colorUrl) { return L.icon({ iconUrl: colorUrl, iconSize: [size, size], iconAnchor: [Math.floor(size / 2), Math.floor(size / 2)] }); } function displayTours(tours){ var bounds = new L.latLngBounds(); var layer = null; for(var i = 0; i < tours.length; i++){ var tour = tours[i]; var latLongsOfTour = getLatLongsOfTour(tour); if (i == 0){ layer = L.polyline(latLongsOfTour, {color: "#575757", weight: 8}).addTo(map); } else{ layer = L.polyline(latLongsOfTour, {color: "#2882C8", weight: 8}).addTo(map); } layer.bindTooltip(tour.vehicleId, {direction: 'top'}); bounds.extend(layer.getBounds()); } map.fitBounds(bounds); } function getLatLongsOfTour(tour) { var trips = tour.trips; var latLongsOfTour = []; var locationLatLong = null; if (tour.vehicleStartLocationId != null){ locationLatLong = getLocationLatLongOfLocationId(tour.vehicleStartLocationId); latLongsOfTour.push(locationLatLong); } for (var tripIndex = 0; tripIndex < trips.length; tripIndex++) { var stopSequence = trips[tripIndex].stops; for (var i = 0; i < stopSequence.length; i++) { var locationIdOfStop = stopSequence[i].locationId; locationLatLong = getLocationLatLongOfLocationId(locationIdOfStop); latLongsOfTour.push(locationLatLong); } } if (tour.vehicleEndLocationId != null){ locationLatLong = getLocationLatLongOfLocationId(tour.vehicleEndLocationId); latLongsOfTour.push(locationLatLong); } return latLongsOfTour; } function getLocationLatLongOfLocationId(locationId){ for(var i = 0; i < locations.length; i++){ var location = locations[i]; if(locationId == location.id){ var coordinate = getLatLngOfLocation(location); return coordinate; } } } planSpecificTours();

The customer sites of the request are displayed in gray, the depot sites in orange. Since the ToursResponse only contains references to the given locations, the corresponding coordinates are taken from the request. The first tour of the result is displayed in gray, the second one in blue.

Each vehicle starts its tour at the assigned start location, which is DepotA for vehicle1 and DepotB for vehicle2. While vehicle2 only serves orders from its home depot (DepotB), vehicle2 also takes some orders from DepotB. This way, all orders can be delivered in 2 tours.

Related Topics

The following topics might be relevant for this use-case.