Planning a Tour using a Distance Matrix
This use case describes how to plan tours with a precomputed distance matrix. Distance matrices play an important role in many tour planning use cases for determining realistic travel times.
For example not all are allowed to use every road which can lead to significant differences compared to plans estimated with direct distance. Also vehicle and road dependent speed limits play an important role for the travel time. Those can be precisely considered in a distance matrix.
Benefits
- Distance matrices in the tour planning context lead to more realistic and accurate plans.
- Estimation by reference matrix reduces the amount of consumed xServer resources for large tour planning problems.
- Estimation by reference matrix simplifies the usage of the same distance matrix for multiple tour planning problems.
Prerequisites
Please ensure following prerequisites are fulfilled before you start with the use case:
- Installed and licensed PTV xDima service
- Installed and licensed PTV xTour service
- License for as many vehicles as the plan should contain
- To improve the performance, increase
xtour.maxNumberOfThreads
in your xserver.conf
file.
Concepts
Programming Guide
Existing Distance Matrix
This example provides information on how to plan tours using a global distance matrix.
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 .
Furthermore 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 vehicle type
, containing one vehicle instance (defined by the number of vehicle ids
).
In this use case we do not want the vehicle to use direct distance
, but an existing distance matrix
(can be set in the distance mode of the request
).
So we need to define a CreateDistanceMatrixRequest
with all the locations needed for the PlanToursRequest
.
The id
of the distance matrix response summary returned by the createDistanceMatrix
operation of xDima is then used for the PlanToursRequest.
The last part of the example PlanToursRequest are the orders
, in this case only consisting of pickup and delivery orders
.
We pass on the request 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.
After displaying the tour results we delete the before calculated distance matrix with the deleteDistanceMatrix
operation to release the required memory.
var depotLocation = {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.066418,
"y": 49.628134
}
};
var customerLocation1 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.150012,
"y": 49.64080
}
};
var customerLocation2 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.1890624,
"y": 49.615019
}
};
var customerLocation3 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.130012,
"y": 49.598000
}
};
var customerLocation4 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.1000182065,
"y": 49.578810645
}
};
var customerLocation5 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.0080182065,
"y": 49.600810645
}
};
var locations = [
{
"$type": "DepotSite",
"id": "Depot",
"routeLocation" : depotLocation,
"openingIntervals": [
{
"$type": "StartDurationInterval",
"start": "2015-11-20T09:30:10+01:00",
"duration": 10000
}
]
},
{
"$type": "CustomerSite",
"id": "Customer1",
"routeLocation" : customerLocation1,
"openingIntervals": [
{
"$type": "StartDurationInterval",
"start": "2015-11-20T09:30:10+01:00",
"duration": 10000
}
]
},
{
"$type": "CustomerSite",
"id": "Customer2",
"routeLocation" : customerLocation2,
"openingIntervals": [
{
"$type": "StartDurationInterval",
"start": "2015-11-20T09:30:10+01:00",
"duration": 10000
}
]
},
{
"$type": "CustomerSite",
"id": "Customer3",
"routeLocation" : customerLocation3,
"openingIntervals": [
{
"$type": "StartDurationInterval",
"start": "2015-11-20T09:30:10+01:00",
"duration": 10000
}
]
},
{
"$type": "CustomerSite",
"id": "Customer4",
"routeLocation": customerLocation4,
"openingIntervals": [
{
"$type": "StartDurationInterval",
"start": "2015-11-20T09:30:10+01:00",
"duration": 10000
}
]
},
{
"$type": "CustomerSite",
"id": "Customer5",
"routeLocation" : customerLocation5,
"openingIntervals": [
{
"$type": "StartDurationInterval",
"start": "2015-11-20T09:30:10+01:00",
"duration": 10000
}
]
}
];
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 createSpecificDistanceMatrixAndPlanTours() {
xdima.createDistanceMatrix({
"startLocations" : [
depotLocation,
customerLocation1,
customerLocation2,
customerLocation3,
customerLocation4,
customerLocation5
]
}, function(dimaResponse, exception) {
planSpecificTours(dimaResponse.summary.id);
});
}
function planSpecificTours(dimaId) {
xtour.planTours({
"locations": locations,
"orders": [
{
"$type": "PickupDeliveryOrder",
"id": 1,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer1"
},
{
"$type": "PickupDeliveryOrder",
"id": 2,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer2"
},
{
"$type": "PickupDeliveryOrder",
"id": 3,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer3"
},
{
"$type": "PickupDeliveryOrder",
"id": 4,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer4"
},
{
"$type": "PickupDeliveryOrder",
"id": 5,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer5"
}
],
"fleet": {
"vehicles": [
{
"ids": ["vehicle1"],
"startLocationId": "Depot",
"endLocationId": "Depot",
"maximumQuantityScenarios": [
{
"quantities": [3.0]
}
]
}
]
},
"distanceMode": {
"$type": "ExistingDistanceMatrix", // change to "$type": "DirectDistance" and delete "id" to compare results
"id": dimaId
}
},
function(toursResponse, exception) {
displayLocations();
displayTours(toursResponse.tours);
xdima.deleteDistanceMatrix({
"id": dimaId
});
});
}
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;
}
}
}
createSpecificDistanceMatrixAndPlanTours();
The customer sites of the request are displayed in gray, the depot sites in orange. The tour of the result is displayed in gray.
The tour consists of two trips because the maximum quantity scenario
of the vehicle means that it can carry out three orders in one trip because of their quantities
.
The usage of a distance matrix leads to a trip including customers Customer2 and Customer4 that are connected via a highway. Therefore Customer3 who is located in town is planned on the second trip.
When changing to direct distance
(see comment in the code section) the highway does not play a role in the result. Instead only geographically close-by customers are planned on the same trip.
Existing Distance Matrix Per Vehicle
This example provides information on how to plan tours using a distance matrix per vehicle
. For the distance mode
of the request we now choose ExistingDistanceMatrixPerVehicle
. Therefore each vehicle
has to have a distance matrix id
set.
Using vehicle parametrization we create two distance matrices using the of a car and a truck with a total permitted weight
of 11,99t. For simplification, there are no opening interval restrictions.
var depotLocation = {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.161068081530488,
"y": 49.86627524673909
}
};
var customerLocation1 = {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.178680040757172,
"y": 49.83591428684965
}
};
var customerLocation2 = {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.228528122765314,
"y": 49.8609722643198
}
};
var customerLocation3 = {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.098359679926945,
"y": 49.83065877301268
}
};
var customerLocation4 = {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.222937608416341,
"y": 49.834876368662265
}
};
var customerLocation5 = {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.164198518234066,
"y": 49.81260738720211
}
};
var locations = [
{
"$type": "DepotSite",
"id": "Depot",
"routeLocation" : depotLocation
},
{
"$type": "CustomerSite",
"id": "Customer1",
"routeLocation" : customerLocation1
},
{
"$type": "CustomerSite",
"id": "Customer2",
"routeLocation" : customerLocation2
},
{
"$type": "CustomerSite",
"id": "Customer3",
"routeLocation" : customerLocation3
},
{
"$type": "CustomerSite",
"id": "Customer4",
"routeLocation": customerLocation4
},
{
"$type": "CustomerSite",
"id": "Customer5",
"routeLocation" : customerLocation5
}
];
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}?storedProfile=silkysand&layers=labels,transport,background,PTV_TruckAttributes';
var tileLayer = new L.TileLayer(tileUrl, {
minZoom: 3,
maxZoom: 18,
noWrap: true
}).addTo(map);
var markers = L.layerGroup().addTo(map);
function createSpecificDistanceMatrixOfTruckAndCarAndPlanTours() {
xdima.createDistanceMatrix({
"startLocations" : [
depotLocation,
customerLocation1,
customerLocation2,
customerLocation3,
customerLocation4,
customerLocation5
],
"storedProfile": "truck11_99t",
"requestProfile": {
"featureLayerProfile": {
"themes": [{
"id": "PTV_TruckAttributes",
"enabled": "true"
}]
}
}
}, function(dimaResponseTruck, exception) {
createSpecificDistanceMatrixOfCarAndPlanTours(dimaResponseTruck.summary.id);
});
}
function createSpecificDistanceMatrixOfCarAndPlanTours(dimaIdTruck) {
xdima.createDistanceMatrix({
"startLocations" : [
depotLocation,
customerLocation1,
customerLocation2,
customerLocation3,
customerLocation4,
customerLocation5
],
"storedProfile": "car"
}, function(dimaResponseCar, exception) {
planSpecificTours(dimaResponseCar.summary.id, dimaIdTruck);
});
}
function planSpecificTours(dimaIdCar, dimaIdTruck) {
xtour.planTours({
"locations": locations,
"orders": [
{
"$type": "PickupDeliveryOrder",
"id": 1,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer1"
},
{
"$type": "PickupDeliveryOrder",
"id": 2,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer2"
},
{
"$type": "PickupDeliveryOrder",
"id": 3,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer3"
},
{
"$type": "PickupDeliveryOrder",
"id": 4,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer4"
},
{
"$type": "PickupDeliveryOrder",
"id": 5,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer5"
}
],
"fleet": {
"vehicles": [
{
"ids": ["truck"],
"distanceMatrixId": dimaIdTruck, // change to "dimaIdCar" to compare results
"startLocationId": "Depot",
"endLocationId": "Depot",
"maximumQuantityScenarios": [
{
"quantities": [3.0]
}
]
},
{
"ids": ["car"],
"distanceMatrixId": dimaIdCar, // change to "dimaIdTruck" to compare results
"startLocationId": "Depot",
"endLocationId": "Depot",
"maximumQuantityScenarios": [
{
"quantities": [3.0]
}
]
}
]
},
"distanceMode": {
"$type": "ExistingDistanceMatrixPerVehicle"
},
"planToursOptions": {
"restrictions": {
"singleTripPerTour": true
}
}
},
function(toursResponse, exception) {
displayLocations();
displayTours(toursResponse.tours);
xdima.deleteDistanceMatrix({
"id": dimaIdCar
});
xdima.deleteDistanceMatrix({
"id": dimaIdTruck
});
});
}
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;
}
}
}
createSpecificDistanceMatrixOfTruckAndCarAndPlanTours();
The customer sites of the request are displayed in gray, the depot sites in orange. The tours of the result are displayed in gray and blue.
The response consists of two tours because we set single trip per tour
and the maximum quantity scenario
still restricts to execute at most three orders per trip.
As multiple streets that directly connect two locations are not allowed for trucks, the vehicle with the car as is driving the tour to the furthest away customers. If you exchange the vehicle profiles (see comment in the code section), the tours will also be exchanged because it is more efficient to drive the direct connecting between the furthest away sites with the vehicle with the car profile.
If both vehicles have a car as profile (see comment in the code section), the structure of the tours seems more intuitively as the tours are not crossing each other.
However if both vehicles have a truck as profile (see comment in the code section), the vehicles have to make the detours to get to each customers.
Estimate by Reference Matrix
This example provides information on how to reduce the size of the distance matrix by defining a nearby reference point for each location. Since multiple nearby locations can have the same reference point, fewer distance matrix relations need to be stored. A reference matrix is a distance matrix that contains all reference locations. The distance and travel time between two locations are estimated based on direct distance, average speed and the detour factor obtained from the reference matrix.
This example is similar to the Existing Distance Matrix example above. Locations, fleet and orders are defined in the same way. For simplification, there are just visit orders and no opening interval restrictions. Instead of considering all locations for the CreateDistanceMatrixRequest
we now only calculate relations between reference points. For the distance mode
of the request we now choose EstimateByReferenceMatrix
. Additionally to the id of the calculated distance matrix, we define a reference location
for each location
of the request. Note that all reference locations have to be part of the reference matrix but the locations themselves do not have to be considered.
var visitLocation1 = {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.103076934814454,
"y": 49.61371312890683
}
};
var visitLocation2 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.104106903076173,
"y": 49.60806808946219
}
};
var visitLocation3 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.114320755004883,
"y": 49.61032062201076
}
};
var visitLocation4 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.142559051513673,
"y": 49.61538148830292
}
};
var visitLocation5 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.15560531616211,
"y": 49.616549305899525
}
};
var locations = [
{
"$type": "CustomerSite",
"id": "Visit1",
"routeLocation" : visitLocation1
},
{
"$type": "CustomerSite",
"id": "Visit2",
"routeLocation" : visitLocation2
},
{
"$type": "CustomerSite",
"id": "Visit3",
"routeLocation" : visitLocation3
},
{
"$type": "CustomerSite",
"id": "Visit4",
"routeLocation" : visitLocation4
},
{
"$type": "CustomerSite",
"id": "Visit5",
"routeLocation" : visitLocation5
}
];
var referenceLocationForVisits_1_2_3 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.108140945434571,
"y": 49.61129390633925
}
};
var referenceLocationForVisits_4_5 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.149511337280274,
"y": 49.61382435464335
}
};
var referenceLocations = [
referenceLocationForVisits_1_2_3,
referenceLocationForVisits_4_5
];
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 createSpecificDistanceMatrixAndPlanTours() {
xdima.createDistanceMatrix({
"startLocations" : referenceLocations
}, function(dimaResponse, exception) {
planSpecificTours(dimaResponse.summary.id);
});
}
var referenceLocationMappings = [
{
"locationId": "Visit1",
"referenceLocation": referenceLocationForVisits_1_2_3
},
{
"locationId": "Visit2",
"referenceLocation": referenceLocationForVisits_1_2_3
},
{
"locationId": "Visit3",
"referenceLocation": referenceLocationForVisits_1_2_3
},
{
"locationId": "Visit4",
"referenceLocation": referenceLocationForVisits_4_5
},
{
"locationId": "Visit5",
"referenceLocation": referenceLocationForVisits_4_5
}
];
function planSpecificTours(dimaId) {
xtour.planTours({
"locations": locations,
"orders": [
{
"$type": "VisitOrder",
"id": 1,
"locationId": "Visit1"
},
{
"$type": "VisitOrder",
"id": 2,
"locationId": "Visit2"
},
{
"$type": "VisitOrder",
"id": 3,
"locationId": "Visit3"
},
{
"$type": "VisitOrder",
"id": 4,
"locationId": "Visit4"
},
{
"$type": "VisitOrder",
"id": 5,
"locationId": "Visit5"
}
],
"fleet": {
"vehicles": [
{
"ids": ["vehicle1"]
}
]
},
"distanceMode": {
"$type": "EstimateByReferenceMatrix", // If you change this type to "$type": "ExistingDistanceMatrix" and delete the "referenceLocationMappings" you will get an exception because not all locations of the request are in the distance matrix - just the reference locations are.
"id": dimaId,
"referenceLocationMappings": referenceLocationMappings
}
},
function(toursResponse, exception) {
displayLocations();
displayReferenceLocationMappings(referenceLocationMappings);
displayDimaRelations(referenceLocations);
displayTours(toursResponse.tours);
xdima.deleteDistanceMatrix({
"id": dimaId
});
});
}
function displayLocations() {
markers.clearLayers();
for(var i = 0; i < locations.length; i++ ){
var location = locations[i];
displayCustomerSite(location);
}
for(var i = 0; i < referenceLocations.length; i++ ){
var routeLocation = referenceLocations[i];
displayReferenceLocation(routeLocation);
}
}
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 displayReferenceLocation(routeLocation) {
var point = getLatLngOfRouteLocation(routeLocation);
var marker = L.marker(point, {
icon: getCircleIcon(18, xServerUrl + '/dashboard/Content/Resources/Images/Showcases/circle_blue.png'),
title: "Reference location"
}).addTo(map);
markers.addLayer(marker);
}
function getLatLngOfLocation(location) { // Only OffRoadRouteLocations are supported in this use case
return getLatLngOfRouteLocation(location.routeLocation);
}
function getLatLngOfRouteLocation(routeLocation) { // Only OffRoadRouteLocations are supported in this use case
var coordinateX = routeLocation.offRoadCoordinate.x;
var coordinateY = routeLocation.offRoadCoordinate.y;
var coordinate = L.latLng(coordinateY, coordinateX);
return coordinate;
}
function displayReferenceLocationMappings(referenceLocationMappings) {
var layer = null;
for(var i = 0; i < referenceLocationMappings.length; i++) {
var referenceLocationMapping = referenceLocationMappings[i];
var latLongsOfMapping = [];
latLongsOfMapping.push(getLocationLatLongOfLocationId(referenceLocationMapping.locationId));
latLongsOfMapping.push(getLatLngOfRouteLocation(referenceLocationMapping.referenceLocation));
layer = L.polyline(latLongsOfMapping, {color: "#38ACEC", weight: 5, dashArray: '5,10'}).addTo(map);
layer.bindTooltip("Mapping of "+referenceLocationMapping.locationId, {direction: 'top'});
}
}
function displayDimaRelations(referenceLocations) {
var layer = null;
for(var i = 0; i+1 < referenceLocations.length; i++) {
for(var j = i+1; j < referenceLocations.length; j++) {
var latLongsOfdimaRelation = [];
latLongsOfdimaRelation.push(getLatLngOfRouteLocation(referenceLocations[i]));
latLongsOfdimaRelation.push(getLatLngOfRouteLocation(referenceLocations[j]));
layer = L.polyline(latLongsOfdimaRelation, {color: "green", weight: 5, dashArray: '5,10'}).addTo(map);
layer.bindTooltip("Calculated relation in distance matrix", {direction: 'top'});
}
}
}
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 of " + 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;
}
}
}
createSpecificDistanceMatrixAndPlanTours();
The customer sites of the request are displayed in gray, the corresponding reference locations in blue. The dashed blue lines indicate the mapping of customer locations to reference locations. The dashed green line indicates the calculated relation in the distance matrix. As there are no restrictions, the result consists of just one visit tour.
Instead of five customer locations, just two reference locations are contained in the distance matrix. When changing to existing distance matix
and deleting the reference location mappings
(see comment in the code section) you will get an exception because the customer locations are not contained in the distance matrix.
Existing Distance Matrix (Multiple Travel Times Distance Matrix)
This example provides information on how to plan tours using a global distance matrix that is calculated by using multiple travel times consideration. The before mentioned distance modes ExistingDistanceMatrixPerVehicle and EstimateByReferenceMatrix can be modified accordingly but if one distance matrix is a multiple travel times distance matrix all other distance matrices of the PlanToursRequest must also be multiple travel times distance matrices.
The CreateDistanceMatrixRequest is extended by a MultipleTravelTimesConsideration
that specifies the horizon in which multiple travel times will be considered. A multiple travel times distance matrix cannot be used in a PlanToursRequest if a DrivingTimeRegulation
is set. In this example we use the WorkingTimeDirective
EU_2002_15_EC. As the working time directive is used to ensure regular breaks for truck drivers the distance matrix is calculated by using truck speed patterns and truck attributes.
In addition to the working time directive the PlanToursRequest must define a planning horizon in the PlanToursOptions
.
var depotLocation = {
"$type": "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.066418,
"y": 49.628134
}
};
var customerLocation1 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.150012,
"y": 49.64080
}
};
var customerLocation2 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.1890624,
"y": 49.615019
}
};
var customerLocation3 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.130012,
"y": 49.598000
}
};
var customerLocation4 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.1000182065,
"y": 49.578810645
}
};
var customerLocation5 = {
"$type" : "OffRoadRouteLocation",
"offRoadCoordinate": {
"x": 6.0080182065,
"y": 49.600810645
}
};
var locations = [
{
"$type": "DepotSite",
"id": "Depot",
"routeLocation" : depotLocation,
"openingIntervals": [
{
"$type": "StartDurationInterval",
"start": "2015-11-20T09:30:10+01:00",
"duration": 10000
}
]
},
{
"$type": "CustomerSite",
"id": "Customer1",
"routeLocation" : customerLocation1,
"openingIntervals": [
{
"$type": "StartDurationInterval",
"start": "2015-11-20T09:30:10+01:00",
"duration": 10000
}
]
},
{
"$type": "CustomerSite",
"id": "Customer2",
"routeLocation" : customerLocation2,
"openingIntervals": [
{
"$type": "StartDurationInterval",
"start": "2015-11-20T09:30:10+01:00",
"duration": 10000
}
]
},
{
"$type": "CustomerSite",
"id": "Customer3",
"routeLocation" : customerLocation3,
"openingIntervals": [
{
"$type": "StartDurationInterval",
"start": "2015-11-20T09:30:10+01:00",
"duration": 10000
}
]
},
{
"$type": "CustomerSite",
"id": "Customer4",
"routeLocation": customerLocation4,
"openingIntervals": [
{
"$type": "StartDurationInterval",
"start": "2015-11-20T09:30:10+01:00",
"duration": 10000
}
]
},
{
"$type": "CustomerSite",
"id": "Customer5",
"routeLocation" : customerLocation5,
"openingIntervals": [
{
"$type": "StartDurationInterval",
"start": "2015-11-20T09:30:10+01:00",
"duration": 10000
}
]
}
];
var planningHorizon = {
"$type": "StartEndInterval",
"start": "2015-11-20T00:00:00+01:00",
"end": "2015-11-21T00:00:00+01:00"
};
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 createSpecificDistanceMatrixAndPlanTours() {
xdima.createDistanceMatrix({
"startLocations" : [
depotLocation,
customerLocation1,
customerLocation2,
customerLocation3,
customerLocation4,
customerLocation5
],
"distanceMatrixOptions": {
"timeConsideration": {
"$type": "MultipleTravelTimesConsideration",
"horizon": planningHorizon
}
},
"storedProfile": "truck11_99t",
"requestProfile": {
"featureLayerProfile": {
"themes": [{
"id": "PTV_TruckSpeedPatterns",
"enabled": "true"
},
{
"id": "PTV_TruckAttributes",
"enabled": "true"
}]
}
}
}, function(dimaResponse, exception) {
planSpecificTours(dimaResponse.summary.id);
});
}
function planSpecificTours(dimaId) {
xtour.planTours({
"locations": locations,
"orders": [
{
"$type": "PickupDeliveryOrder",
"id": 1,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer1"
},
{
"$type": "PickupDeliveryOrder",
"id": 2,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer2"
},
{
"$type": "PickupDeliveryOrder",
"id": 3,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer3"
},
{
"$type": "PickupDeliveryOrder",
"id": 4,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer4"
},
{
"$type": "PickupDeliveryOrder",
"id": 5,
"quantities": [1.0],
"pickupLocationId": "Depot",
"deliveryLocationId": "Customer5"
}
],
"fleet": {
"vehicles": [
{
"ids": ["vehicle1"],
"startLocationId": "Depot",
"endLocationId": "Depot",
"maximumQuantityScenarios": [
{
"quantities": [3.0]
}
]
}
]
},
"planToursOptions": {
"planningHorizon": planningHorizon,
"restrictions": {
"workingHours": {
"$type": "SingleDayWorkingHours",
"workingTimeDirective": "EU_2002_15_EC"
}
}
},
"distanceMode": {
"$type": "ExistingDistanceMatrix",
"id": dimaId
}
},
function(toursResponse, exception) {
displayLocations();
displayTours(toursResponse.tours);
xdima.deleteDistanceMatrix({
"id": dimaId
});
});
}
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;
}
}
}
createSpecificDistanceMatrixAndPlanTours();
Only use a multiple travel times distance matrix if needed. The results differ significantly to the ones calculated with a normal distance matrix as the optimization problem gets much more difficult to solve! Have a look at the Technical Concept Working Hours to ensure a valid PlanToursRequest when using multiple travel times distance matrices.