Planning a Tour for Close-by Warehouses and Subcontractor Delivery
In this use case there are several types of goods to be delivered, which are stored in different, but close-by warehouses. Customers often order products from different warehouses. The execution of the tours is done by a subcontracting carrier which leaves the aspect of empty when returning to the location of no interest to the dispatcher.
Example | The following picture shows an example of the desired tour structure
Benefits
- Users learn how to parametrize the current use-case.
- Basic insight into how to influence the course of a tour.
Prerequisites
Please ensure following prerequisites are fulfilled before you start with the use case:
- Installed and licensed PTV xTour service
- License for as many vehicles as the plan should contain
Concepts
Programming Guide
This example explains how the request should look like in order 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 of .
Since the use case describes deliveries from several depots, we define some locations as depot and some as customer sites, see Orders, Locations, and Stops for more details.
The fleet in this example consists of one type of vehicle
, containing one available vehicle instance (defined by the number of vehicle ids
).
This vehicle has unspecified start and end locations since the return to the carrier location before and after delivering all goods is of no importance for the further planning. To simplify the example code, we assume that the vehicle uses direct distance
(can be set in the distance mode of the request
).
In this example we do not want to return to the depot(s) in between stops at customers, so we need to activate the tour restriction singleTripPerTour
. This restriction could be omitted if other tour structures should be allowed.
The last part of the example request are the orders
, in this case only consisting of pickup and delivery orders
.
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.098742,
"y": 49.618829
}
}
};
var DepotB = {
"$type": "DepotSite",
"id": "DepotB",
"routeLocation" : {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.115694,
"y": 49.619941
}
}
};
var DepotC = {
"$type": "DepotSite",
"id": "DepotC",
"routeLocation" : {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.114835,
"y": 49.616771
}
}
};
var Customer1 = {
"$type": "CustomerSite",
"id": "Customer1",
"routeLocation": {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.138353,
"y": 49.608040
}
}
};
var Customer2 = {
"$type": "CustomerSite",
"id": "Customer2",
"routeLocation": {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.138353,
"y": 49.593632
}
}
};
var Customer3 = {
"$type": "CustomerSite",
"id": "Customer3",
"routeLocation": {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.121702,
"y": 49.588736
}
}
};
var Customer4 = {
"$type": "CustomerSite",
"id": "Customer4",
"routeLocation": {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.097755,
"y": 49.595135
}
}
};
var locations = [DepotA, DepotB, DepotC, Customer1, Customer2, Customer3, Customer4];
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": "DepotA",
"deliveryLocationId": "Customer1"
}, {
"$type": "PickupDeliveryOrder",
"id": "PDOrder2",
"quantities": [50.0],
"pickupLocationId": "DepotB",
"deliveryLocationId": "Customer1"
}, {
"$type": "PickupDeliveryOrder",
"id": "PDOrder3",
"quantities": [50.0],
"pickupLocationId": "DepotC",
"deliveryLocationId": "Customer1"
}, {
"$type": "PickupDeliveryOrder",
"id": "PDOrder4",
"quantities": [100.0],
"pickupLocationId": "DepotA",
"deliveryLocationId": "Customer2"
}, {
"$type": "PickupDeliveryOrder",
"id": "PDOrder5",
"quantities": [20.0],
"pickupLocationId": "DepotC",
"deliveryLocationId": "Customer2"
}, {
"$type": "PickupDeliveryOrder",
"id": "PDOrder6",
"quantities": [30.0],
"pickupLocationId": "DepotB",
"deliveryLocationId": "Customer3"
}, {
"$type": "PickupDeliveryOrder",
"id": "PDOrder7",
"quantities": [100.0],
"pickupLocationId": "DepotB",
"deliveryLocationId": "Customer4"
}, {
"$type": "PickupDeliveryOrder",
"id": "PDOrder8",
"quantities": [50.0],
"pickupLocationId": "DepotC",
"deliveryLocationId": "Customer4"
}],
"fleet": {
"vehicles": [
{
"ids": ["vehicle1"],
"maximumQuantityScenarios":
[
{
"quantities": [500.0]
}
]
}
]
},
"distanceMode": {
"$type": "DirectDistance"
},
"planToursOptions": {
"restrictions": {
"singleTripPerTour": true
}
}
}, 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, xServerUrl + '/dashboard/Content/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, xServerUrl + '/dashboard/Content/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 tour of the result is displayed in gray.
The vehicle starts at the first depot (DepotA), carries out all pick-up tasks at the three depots and the delivers everything to the customers. The tour ends at the last customer, in this case Customer4, because the return to the carriers vehicle location is of no interest for this kind of trip planning.