369 lines
9.6 KiB
JavaScript
369 lines
9.6 KiB
JavaScript
(function() {
|
|
'use strict';
|
|
|
|
var L = require('leaflet'),
|
|
corslite = require('corslite'),
|
|
polyline = require('polyline'),
|
|
osrmTextInstructions = require('osrm-text-instructions');
|
|
|
|
// Ignore camelcase naming for this file, since OSRM's API uses
|
|
// underscores.
|
|
/* jshint camelcase: false */
|
|
|
|
var Waypoint = require('./waypoint');
|
|
|
|
/**
|
|
* Works against OSRM's new API in version 5.0; this has
|
|
* the API version v1.
|
|
*/
|
|
module.exports = L.Class.extend({
|
|
options: {
|
|
serviceUrl: 'https://router.project-osrm.org/route/v1',
|
|
profile: 'driving',
|
|
timeout: 30 * 1000,
|
|
routingOptions: {
|
|
alternatives: true,
|
|
steps: true
|
|
},
|
|
polylinePrecision: 5,
|
|
useHints: true,
|
|
suppressDemoServerWarning: false,
|
|
language: 'en'
|
|
},
|
|
|
|
initialize: function(options) {
|
|
L.Util.setOptions(this, options);
|
|
this._hints = {
|
|
locations: {}
|
|
};
|
|
|
|
if (!this.options.suppressDemoServerWarning &&
|
|
this.options.serviceUrl.indexOf('//router.project-osrm.org') >= 0) {
|
|
console.warn('You are using OSRM\'s demo server. ' +
|
|
'Please note that it is **NOT SUITABLE FOR PRODUCTION USE**.\n' +
|
|
'Refer to the demo server\'s usage policy: ' +
|
|
'https://github.com/Project-OSRM/osrm-backend/wiki/Api-usage-policy\n\n' +
|
|
'To change, set the serviceUrl option.\n\n' +
|
|
'Please do not report issues with this server to neither ' +
|
|
'Leaflet Routing Machine or OSRM - it\'s for\n' +
|
|
'demo only, and will sometimes not be available, or work in ' +
|
|
'unexpected ways.\n\n' +
|
|
'Please set up your own OSRM server, or use a paid service ' +
|
|
'provider for production.');
|
|
}
|
|
},
|
|
|
|
route: function(waypoints, callback, context, options) {
|
|
var timedOut = false,
|
|
wps = [],
|
|
url,
|
|
timer,
|
|
wp,
|
|
i,
|
|
xhr;
|
|
|
|
options = L.extend({}, this.options.routingOptions, options);
|
|
url = this.buildRouteUrl(waypoints, options);
|
|
if (this.options.requestParameters) {
|
|
url += L.Util.getParamString(this.options.requestParameters, url);
|
|
}
|
|
|
|
timer = setTimeout(function() {
|
|
timedOut = true;
|
|
callback.call(context || callback, {
|
|
status: -1,
|
|
message: 'OSRM request timed out.'
|
|
});
|
|
}, this.options.timeout);
|
|
|
|
// Create a copy of the waypoints, since they
|
|
// might otherwise be asynchronously modified while
|
|
// the request is being processed.
|
|
for (i = 0; i < waypoints.length; i++) {
|
|
wp = waypoints[i];
|
|
wps.push(new Waypoint(wp.latLng, wp.name, wp.options));
|
|
}
|
|
|
|
return xhr = corslite(url, L.bind(function(err, resp) {
|
|
var data,
|
|
error = {};
|
|
|
|
clearTimeout(timer);
|
|
if (!timedOut) {
|
|
if (!err) {
|
|
try {
|
|
data = JSON.parse(resp.responseText);
|
|
try {
|
|
return this._routeDone(data, wps, options, callback, context);
|
|
} catch (ex) {
|
|
error.status = -3;
|
|
error.message = ex.toString();
|
|
}
|
|
} catch (ex) {
|
|
error.status = -2;
|
|
error.message = 'Error parsing OSRM response: ' + ex.toString();
|
|
}
|
|
} else {
|
|
error.message = 'HTTP request failed: ' + err.type +
|
|
(err.target && err.target.status ? ' HTTP ' + err.target.status + ': ' + err.target.statusText : '');
|
|
error.url = url;
|
|
error.status = -1;
|
|
error.target = err;
|
|
}
|
|
|
|
callback.call(context || callback, error);
|
|
} else {
|
|
xhr.abort();
|
|
}
|
|
}, this));
|
|
},
|
|
|
|
requiresMoreDetail: function(route, zoom, bounds) {
|
|
if (!route.properties.isSimplified) {
|
|
return false;
|
|
}
|
|
|
|
var waypoints = route.inputWaypoints,
|
|
i;
|
|
for (i = 0; i < waypoints.length; ++i) {
|
|
if (!bounds.contains(waypoints[i].latLng)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
_routeDone: function(response, inputWaypoints, options, callback, context) {
|
|
var alts = [],
|
|
actualWaypoints,
|
|
i,
|
|
route;
|
|
|
|
context = context || callback;
|
|
if (response.code !== 'Ok') {
|
|
callback.call(context, {
|
|
status: response.code
|
|
});
|
|
return;
|
|
}
|
|
|
|
actualWaypoints = this._toWaypoints(inputWaypoints, response.waypoints);
|
|
|
|
for (i = 0; i < response.routes.length; i++) {
|
|
route = this._convertRoute(response.routes[i]);
|
|
route.inputWaypoints = inputWaypoints;
|
|
route.waypoints = actualWaypoints;
|
|
route.properties = {isSimplified: !options || !options.geometryOnly || options.simplifyGeometry};
|
|
alts.push(route);
|
|
}
|
|
|
|
this._saveHintData(response.waypoints, inputWaypoints);
|
|
|
|
callback.call(context, null, alts);
|
|
},
|
|
|
|
_convertRoute: function(responseRoute) {
|
|
var result = {
|
|
name: '',
|
|
coordinates: [],
|
|
instructions: [],
|
|
summary: {
|
|
totalDistance: responseRoute.distance,
|
|
totalTime: responseRoute.duration
|
|
}
|
|
},
|
|
legNames = [],
|
|
waypointIndices = [],
|
|
index = 0,
|
|
legCount = responseRoute.legs.length,
|
|
hasSteps = responseRoute.legs[0].steps.length > 0,
|
|
i,
|
|
j,
|
|
leg,
|
|
step,
|
|
geometry,
|
|
type,
|
|
modifier,
|
|
text,
|
|
stepToText;
|
|
|
|
if (this.options.stepToText) {
|
|
stepToText = this.options.stepToText;
|
|
} else {
|
|
var textInstructions = osrmTextInstructions('v5', this.options.language);
|
|
stepToText = textInstructions.compile.bind(textInstructions);
|
|
}
|
|
|
|
for (i = 0; i < legCount; i++) {
|
|
leg = responseRoute.legs[i];
|
|
legNames.push(leg.summary && leg.summary.charAt(0).toUpperCase() + leg.summary.substring(1));
|
|
for (j = 0; j < leg.steps.length; j++) {
|
|
step = leg.steps[j];
|
|
geometry = this._decodePolyline(step.geometry);
|
|
result.coordinates.push.apply(result.coordinates, geometry);
|
|
type = this._maneuverToInstructionType(step.maneuver, i === legCount - 1);
|
|
modifier = this._maneuverToModifier(step.maneuver);
|
|
text = stepToText(step);
|
|
|
|
if (type) {
|
|
if ((i == 0 && step.maneuver.type == 'depart') || step.maneuver.type == 'arrive') {
|
|
waypointIndices.push(index);
|
|
}
|
|
|
|
result.instructions.push({
|
|
type: type,
|
|
distance: step.distance,
|
|
time: step.duration,
|
|
road: step.name,
|
|
direction: this._bearingToDirection(step.maneuver.bearing_after),
|
|
exit: step.maneuver.exit,
|
|
index: index,
|
|
mode: step.mode,
|
|
modifier: modifier,
|
|
text: text
|
|
});
|
|
}
|
|
|
|
index += geometry.length;
|
|
}
|
|
}
|
|
|
|
result.name = legNames.join(', ');
|
|
if (!hasSteps) {
|
|
result.coordinates = this._decodePolyline(responseRoute.geometry);
|
|
} else {
|
|
result.waypointIndices = waypointIndices;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
_bearingToDirection: function(bearing) {
|
|
var oct = Math.round(bearing / 45) % 8;
|
|
return ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'][oct];
|
|
},
|
|
|
|
_maneuverToInstructionType: function(maneuver, lastLeg) {
|
|
switch (maneuver.type) {
|
|
case 'new name':
|
|
return 'Continue';
|
|
case 'depart':
|
|
return 'Head';
|
|
case 'arrive':
|
|
return lastLeg ? 'DestinationReached' : 'WaypointReached';
|
|
case 'roundabout':
|
|
case 'rotary':
|
|
return 'Roundabout';
|
|
case 'merge':
|
|
case 'fork':
|
|
case 'on ramp':
|
|
case 'off ramp':
|
|
case 'end of road':
|
|
return this._camelCase(maneuver.type);
|
|
// These are all reduced to the same instruction in the current model
|
|
//case 'turn':
|
|
//case 'ramp': // deprecated in v5.1
|
|
default:
|
|
return this._camelCase(maneuver.modifier);
|
|
}
|
|
},
|
|
|
|
_maneuverToModifier: function(maneuver) {
|
|
var modifier = maneuver.modifier;
|
|
|
|
switch (maneuver.type) {
|
|
case 'merge':
|
|
case 'fork':
|
|
case 'on ramp':
|
|
case 'off ramp':
|
|
case 'end of road':
|
|
modifier = this._leftOrRight(modifier);
|
|
}
|
|
|
|
return modifier && this._camelCase(modifier);
|
|
},
|
|
|
|
_camelCase: function(s) {
|
|
var words = s.split(' '),
|
|
result = '';
|
|
for (var i = 0, l = words.length; i < l; i++) {
|
|
result += words[i].charAt(0).toUpperCase() + words[i].substring(1);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
_leftOrRight: function(d) {
|
|
return d.indexOf('left') >= 0 ? 'Left' : 'Right';
|
|
},
|
|
|
|
_decodePolyline: function(routeGeometry) {
|
|
var cs = polyline.decode(routeGeometry, this.options.polylinePrecision),
|
|
result = new Array(cs.length),
|
|
i;
|
|
for (i = cs.length - 1; i >= 0; i--) {
|
|
result[i] = L.latLng(cs[i]);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
_toWaypoints: function(inputWaypoints, vias) {
|
|
var wps = [],
|
|
i,
|
|
viaLoc;
|
|
for (i = 0; i < vias.length; i++) {
|
|
viaLoc = vias[i].location;
|
|
wps.push(new Waypoint(L.latLng(viaLoc[1], viaLoc[0]),
|
|
inputWaypoints[i].name,
|
|
inputWaypoints[i].options));
|
|
}
|
|
|
|
return wps;
|
|
},
|
|
|
|
buildRouteUrl: function(waypoints, options) {
|
|
var locs = [],
|
|
hints = [],
|
|
wp,
|
|
latLng,
|
|
computeInstructions,
|
|
computeAlternative = true;
|
|
|
|
for (var i = 0; i < waypoints.length; i++) {
|
|
wp = waypoints[i];
|
|
latLng = wp.latLng;
|
|
locs.push(latLng.lng + ',' + latLng.lat);
|
|
hints.push(this._hints.locations[this._locationKey(latLng)] || '');
|
|
}
|
|
|
|
computeInstructions =
|
|
true;
|
|
|
|
return this.options.serviceUrl + '/' + this.options.profile + '/' +
|
|
locs.join(';') + '?' +
|
|
(options.geometryOnly ? (options.simplifyGeometry ? '' : 'overview=full') : 'overview=false') +
|
|
'&alternatives=' + computeAlternative.toString() +
|
|
'&steps=' + computeInstructions.toString() +
|
|
(this.options.useHints ? '&hints=' + hints.join(';') : '') +
|
|
(options.allowUTurns ? '&continue_straight=' + !options.allowUTurns : '');
|
|
},
|
|
|
|
_locationKey: function(location) {
|
|
return location.lat + ',' + location.lng;
|
|
},
|
|
|
|
_saveHintData: function(actualWaypoints, waypoints) {
|
|
var loc;
|
|
this._hints = {
|
|
locations: {}
|
|
};
|
|
for (var i = actualWaypoints.length - 1; i >= 0; i--) {
|
|
loc = waypoints[i].latLng;
|
|
this._hints.locations[this._locationKey(loc)] = actualWaypoints[i].hint;
|
|
}
|
|
},
|
|
});
|
|
})();
|