Tutorial
This tutorial shows how to consume Rinkai's Map API REST/Json with javascript inside a browser. For a java SOAP example please see the last section.
Minimal
Geocoding
Displaying a map
Adding time windows and capacities
Dealing with errors
Further
Minimal
NOTE: The minimal section is also available as a standalone page
Let 's start with the simplest case possible. We create a
VrpRequest to define the depot plus 3 locations to be visited, without any constraints such as timewindows, capacities, etc.
Request
// define the optimization request
var optRequest =
{
"depot": {
// the depot is always the starting point
"location": {
"coordinates": {
"x": 13.6438122354029,
"y": 50.5146082763126
}
}
},
"locationsAndOrders": [
{
"locationInfo": {
"location": {
"coordinates": {
"label": "",
"x": 13.5636822222738,
"y": 50.4025882579593
}
}
}
},
{
"locationInfo": {
"location": {
"coordinates": {
"x": 13.5431322189068,
"y": 50.3294782459803
}
}
}
},
{
"locationInfo": {
"location": {
"coordinates": {
"x": 13.7914225958854,
"y": 50.3569882504888
}
}
}
}
]
};
Next we send the optimization request to Rinkai's Map API server. The simplest way to do that is to call the
getOptimization method which is synchronous, i.e. the server will not respond until the results are available. The following snippet perform the request, using jquery:
// function to call the rest API against a given request
function callServer(op,request, callback) {
$.post({
url: "http://api.rinkai.eu/map/v1_3c/api/rest/"+op+"/?key=tutorial3",
contentType: "application/json",
data: JSON.stringify(request, 2),
context: document.body
}).done(function (response) {
// just dump the response to the console log
console.log(JSON.stringify(response, null, 2));
if (callback)
callback(response);
});
}
// call the rest API for optimization
function optimize(request, callback) {
callServer("optimization/sync",request,callback);
}
// perform the request
optimize(optRequest);
Response
Finally we get the
VrpResponse from the server. The response contains a list of routes, and each route is assigned to a vehicle. Since we did not specify any vehicles in the request, the server assumed we have one vehicle, with default constraints and costs.
The main results is the routes' sequence. In this case, because there were no time nor capacity constrants, all the orders are assigned onto a unique route and the sequence is in the field response.plan.routes[0].stops
.
Other fields which should be considered before processing the result are:
-
response.invalidLocations
: shows the locations and orders which could not be included in the solution, and the reason.
-
response.generalError
: describes the error in case a solution could not be provided at all.
Please see the Dealing with errors section for more information.
Geocoding
Automatic geocoding
The simplest way to geocode is to take advantage of the automatic geocoding, i,e,
geocoding is performed everytime a location is specified with an address but without coordinates.
Please note that although simpler, this is not the recommended way to perform geocoding (see next section).
The following request will yield the same result as the previous one, using automatic geocoding:
Request
// define the optimization request
var optRequest =
{
"depot": {
"location": {
// defining a location with an address but without coordinates
// will trigger the geocoding for this location
"address": {
"country": "cz",
"city": "Most",
"street": "Chomutovská",
"houseNumber": " 2261/2",
"postCode": "43401"
}
}
},
"locationsAndOrders": [
{
"locationInfo": {
// With both an address and coordinates
// the coordinates is used
"location": {
"address": {
"country": "cz",
"city": "Velemyšleves",
"postCode": "43801"
},
"coordinates": {
"label": "",
"x": 13.5636822222738,
"y": 50.4025882579593
}
}
}
},
{
"locationInfo": {
"location": {
"coordinates": {
"x": 13.5431322189068,
"y": 50.3294782459803
}
}
}
},
{
"locationInfo": {
"location": {
"address": {
"country": "cz",
"city": "Louny",
"street": "Suzdalské nám. ",
"houseNumber": "2281",
"postCode": "44001"
}
}
}
}
]
};
Response
Geocoding as a separate operation
Because geocoding may yields uncorrect results, mostly due to incomplete or wrongly typed addresses,
it is recommended to perform geocoding as a separate operation, before the optimization. This enables to assess the quality of the geocoding results.
GeocodingResponse
contains indicators of the quality, such as area, distance from the depot, whether the location is routable or not, etc.
With these indicators, you may filter the results programatically.
You can also choose for example to display some of them to the user for further validation.
A
GeocodingRequest
contains a list of locations to be geocoded (Note that if the coordinates field is already populated, geocoding wont be performed)
and a reference location for checking the routability of the results.
Request
// define the geocoding request
var geoRequest =
{
"refLocation": {
"coordinates": {
"x": 13.6438122354029,
"y": 50.5146082763126
}
},
"addresses": [
// address specified using free text
{
// optional id
"id":"123abc",
"freeText": "12,Pekarska,Praha,Czech Republic"
},
// address with detailed fields - yields slightly better results
{
"houseNumber":"12",
"street": "Pekarska",
"city":"prague",
"country":"Czech Republic"
}
]
//, "basicVehicleInfo":{}, // not specified here, so a default vehicle will be used
};
Using previously defined function callServer
. the request is run on the server with:
// perform the request
callServer("locate",geoRequest);
Response
Displaying a map
Next we will display a map with the orders.
Retrieving and decoding the traces
To also display the road planned from one order to another, we set the field optRequest.detailedTraces=true
. This will cause the server to include in the optimalization response the full list of points that form the traces on the map.
The actual traces are returned encoded in the google polyline format. In order to encode/decode this format onto
Coordinates which can be used in Rinkai's Map API, please include the polyline.js snippet in your code, which will define the top functions decodePolyline()
and encodePolyline()
Choose a map library
In order to display a map you can either:
-
use rinkai-osm map service to produce a static map image
-
use a 3rd party library like leaflet or openlayers to display a dynamic map
Here we show the steps to display the map with Leaflet. We define a bunch of helpers to translate data from Rinkai's Map API to Leaflet, and to get a textual representation from an order location.
/**
* Transform an array of Coordinates (from Rinkai's Map API) into an array of coordinates for leaflets
*/
function coordinates2leaflets(coords) {
return coords.map(function (coords) {
return [coords.y, coords.x]
});
}
/**
* Transform Coordinates (from Rinkai's Map API) into coordinates for leaflets
*/
function coordinates2leaflet(coords) {
return [coords.y, coords.x];
}
/**
* return a short string representation of the location, using fields of the address if defined, otherwise using the coordinates
*/
function location2string(location) {
return location.address ? (location.address.street + "," + location.address.city)
: ( location.coordinates.y.toFixed(3)+","+location.coordinates.x.toFixed(3))
}
Now the following function will create a Leaflet map showing the results from the optimalization response:
// hack: keep a reference to the leaflet map, so that we dont instantiate it twice
var dynamicMap;
function leafletMap( optResponse) {
console.log("leafletMap()")
if (dynamicMap) dynamicMap.remove();
dynamicMap = L.map('leafletmap');
// add the tiles, that form the background map, with roads, cities, rivers, etc
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(dynamicMap);
// add stops as waypoints
optResponse.plan.routes.forEach(function (route) {
route.stops.forEach(function (stop) {
console.log(stop);
L.marker(coordinates2leaflet(stop.location.coordinates)).addTo(dynamicMap).bindPopup(location2string(stop.location));
//,{autoClose:false}).openPopup()
});
});
// add depot as waypoint
L.marker(coordinates2leaflet(optResponse.request.depot.location.coordinates)).addTo(dynamicMap).bindPopup("DEPOT", { autoClose: false }).openPopup();
// add polylines
if (optResponse.request.detailedTraces) {
optResponse.plan.routes.forEach(function (route, idx) {
var latlngs = route.legs.reduce(function (a, b) {
return a.concat(coordinates2leaflets(decodePolyline(b.coordinatesPolyline)));
}, []);
var polyline = L.polyline(latlngs, { color: "red"}).addTo(dynamicMap);
});
}
// zoom the map to the polyline
console.log(optResponse.plan);
if (optResponse.plan.bbox)
dynamicMap.fitBounds([coordinates2leaflet(optResponse.plan.bbox.topLeft), coordinates2leaflet(optResponse.plan.bbox.bottomRight)], { padding: [50, 50] });
}
Leaflet map
Adding time windows and capacities
Time windows
TimeWindows have a start and end time and are specified in hours/minutes/seconds since midnight. It is also possible to specify them using only seconds since midnight, e.g. {"hour":1,"minutes":0,"seconds":0}
is the same as {"seconds":3600}
Time windows can be specified on each order's
locationInfo , to specify the
Depot opening times, and each
Vehicle 's availability.
By default, a time window is available the whole day. The exception is the field locationInfo.timeWindow2
which by default is considered as not available.
Workload of vehicles
A VehicleLoad object can be used inside the vehicle.maxLoad
field to specify the vehicle maximum load (i.e. maximum Volume, Weight and a 3rd arbitrary dimension)
On the order side, the load is specified on the
OrderInfo object using the fields load
and loadBack
.
Please note that if you define an order with a dimension such as weight or volume, you will also need to specify the vehicles' maxLoad.
Special requirements
You can also specify that an order should only be picked up/delivered with a vehicle with a specific ability, e.g. 'crane' or 'fridge'.
{
...
"vehicles":[{
"id": ...
"vehicleTypes":["crane","fridge"],
...
"locationsAndOrders": [
{
"orderId": ...
"vehicleTypeRestrictions":["fridge"]
...
Dealing with errors
There are 3 kind of errors:
Low Level
Low level error will cause the server to reply with a 4xx or 5xx code. For example, sending a request which is missing required fields, has extra unknown fields, or wrongly typed, will cause such an error. In this case, the response header 'exception'
will be populated with the error message. Note that this messag is just provided as a help during development and is not meant to be displayed to end users.
When using asynchronous optimization, some Low Level errors are instead returned as General Errors.
General Error
This error occurs when the optimization plan cannot be generated at all. The response will have a status 200, and will contain a
VrpResponse object with the field generalError
populated
Non planned Orders
This error occurs when some of the orders could not be included into the optimization plan. As for the general error, the response will have a status 200, and contain a VrpResponse. The field invalidLocations
will contain an array of
LocationAndOrderErrorMessages for the non planned orders.
Going further
Using asynchronous optimization
Calling the optimization asynchronously provides 2 benefits:
-
You are not limited by network timeouts
-
You can provide your user with actual progress of the optimization
You can look at the source of the sample page to see an example of asynchronous usage.
Improving computation speed
Try to limit the number of different
BasicVehicleInfo . This is because for each vehicle a distance matrix must be calculated prior to the optimization process. When two vehicles have identical vehicle.basicVehicleInfo
field, then the distance matrix is calculated only once.
Using SOAP
Rinkai's map API is also available as a SOAP endpoint.
For a simple java/mvn sample please unzip this archive, and run with:
mvn package exec:java