The MIME type for GeoJSON registered with IANA is application/geo+json`, replacing the old value `application/vnd.geo+json`. The change was published with RFC 7946 in 2016. BRouter reponses changed to `application/geo+json` in recent versions. With this commit BRouter-Web accepts either of both MIME type strings in BRouter responses.
549 lines
19 KiB
JavaScript
549 lines
19 KiB
JavaScript
L.BRouter = L.Class.extend({
|
|
statics: {
|
|
// NOTE: the routing API used here is not public!
|
|
// /brouter?lonlats=1.1,1.2|2.1,2.2|3.1,3.2|4.1,4.2&nogos=-1.1,-1.2,1|-2.1,-2.2,2&profile=shortest&alternativeidx=1&format=kml
|
|
URL_TEMPLATE:
|
|
'/brouter?lonlats={lonlats}&profile={profile}&alternativeidx={alternativeidx}&format={format}&nogos={nogos}&polylines={polylines}&polygons={polygons}',
|
|
URL_PROFILE_UPLOAD: BR.conf.host + '/brouter/profile',
|
|
PRECISION: 6,
|
|
NUMBER_SEPARATOR: ',',
|
|
GROUP_SEPARATOR: '|',
|
|
ABORTED_ERROR: 'aborted',
|
|
CUSTOM_PREFIX: 'custom_',
|
|
SUPPORTED_BROUTER_VERSIONS: '< 1.7.0 || >=1.7.2', // compatibility string should be in npm package versioning format
|
|
isCustomProfile: function (profileName) {
|
|
return profileName && profileName.substring(0, 7) === L.BRouter.CUSTOM_PREFIX;
|
|
},
|
|
},
|
|
|
|
options: {},
|
|
|
|
initialize: function (options) {
|
|
L.setOptions(this, options);
|
|
|
|
this.queue = async.queue(
|
|
L.bind(function (task, callback) {
|
|
this.getRoute(task.segment, callback);
|
|
}, this),
|
|
1
|
|
);
|
|
},
|
|
|
|
setOptions: function (options) {
|
|
L.setOptions(this, options);
|
|
},
|
|
|
|
getUrlParams: function (latLngs, beelineFlags, pois, circlego, format) {
|
|
params = {};
|
|
if (this._getLonLatsString(latLngs) != null) params.lonlats = this._getLonLatsString(latLngs);
|
|
|
|
if (beelineFlags && beelineFlags.length > 0) {
|
|
const beelineString = this._getBeelineString(beelineFlags);
|
|
if (beelineString.length > 0) params.straight = beelineString;
|
|
}
|
|
|
|
if (this.options.nogos && this._getNogosString(this.options.nogos).length > 0)
|
|
params.nogos = this._getNogosString(this.options.nogos);
|
|
|
|
if (this.options.polylines && this._getNogosPolylinesString(this.options.polylines).length > 0)
|
|
params.polylines = this._getNogosPolylinesString(this.options.polylines);
|
|
|
|
if (this.options.polygons && this._getNogosPolygonsString(this.options.polygons).length > 0)
|
|
params.polygons = this._getNogosPolygonsString(this.options.polygons);
|
|
|
|
if (this.options.profile != null) params.profile = this.options.profile;
|
|
|
|
if (pois && this._getLonLatsNameString(pois) != null) params.pois = this._getLonLatsNameString(pois);
|
|
|
|
if (circlego) params.circlego = circlego;
|
|
|
|
params.alternativeidx = this.options.alternative;
|
|
|
|
if (format != null) {
|
|
params.format = format;
|
|
} else {
|
|
// do not put values in URL if this is the default value (format===null)
|
|
if (params.profile === BR.conf.profiles[0]) delete params.profile;
|
|
if (params.alternativeidx == 0) delete params.alternativeidx;
|
|
|
|
// don't add custom profile, as these are only stored temporarily
|
|
if (params.profile && L.BRouter.isCustomProfile(params.profile)) {
|
|
delete params.profile;
|
|
}
|
|
}
|
|
|
|
return params;
|
|
},
|
|
|
|
parseUrlParams: function (params) {
|
|
var opts = {};
|
|
if (params.lonlats) {
|
|
opts.lonlats = this._parseLonLats(params.lonlats);
|
|
}
|
|
if (params.straight) {
|
|
opts.beelineFlags = this._parseBeelines(params.straight, opts.lonlats);
|
|
}
|
|
if (params.nogos) {
|
|
opts.nogos = this._parseNogos(params.nogos);
|
|
}
|
|
if (params.polylines) {
|
|
opts.polylines = this._parseNogosPolylines(params.polylines);
|
|
}
|
|
if (params.polygons) {
|
|
opts.polygons = this._parseNogosPolygons(params.polygons);
|
|
}
|
|
if (params.alternativeidx) {
|
|
opts.alternative = params.alternativeidx;
|
|
}
|
|
if (params.profile) {
|
|
opts.profile = this._parseProfile(params.profile);
|
|
}
|
|
if (params.pois) {
|
|
opts.pois = this._parseLonLatNames(params.pois);
|
|
}
|
|
if (params.ringgo || params.circlego) {
|
|
var paramRinggo = params.ringgo || params.circlego;
|
|
var circlego = paramRinggo.split(',');
|
|
if (circlego.length == 3) {
|
|
circlego = [
|
|
Number.parseFloat(circlego[0]),
|
|
Number.parseFloat(circlego[1]),
|
|
Number.parseInt(circlego[2]),
|
|
];
|
|
opts.circlego = circlego;
|
|
}
|
|
}
|
|
return opts;
|
|
},
|
|
|
|
getUrl: function (latLngs, beelineFlags, pois, circlego, format, trackname, exportWaypoints) {
|
|
var urlParams = this.getUrlParams(latLngs, beelineFlags, pois, circlego, format);
|
|
var args = [];
|
|
if (urlParams.lonlats != null && urlParams.lonlats.length > 0)
|
|
args.push(L.Util.template('lonlats={lonlats}', urlParams));
|
|
if (urlParams.straight != null) args.push(L.Util.template('straight={straight}', urlParams));
|
|
if (urlParams.pois != null && urlParams.pois.length > 0) args.push(L.Util.template('pois={pois}', urlParams));
|
|
if (urlParams.circlego != null) args.push(L.Util.template('ringgo={circlego}', urlParams));
|
|
if (urlParams.nogos != null) args.push(L.Util.template('nogos={nogos}', urlParams));
|
|
if (urlParams.polylines != null) args.push(L.Util.template('polylines={polylines}', urlParams));
|
|
if (urlParams.polygons != null) args.push(L.Util.template('polygons={polygons}', urlParams));
|
|
if (urlParams.profile != null) args.push(L.Util.template('profile={profile}', urlParams));
|
|
if (urlParams.alternativeidx != null) args.push(L.Util.template('alternativeidx={alternativeidx}', urlParams));
|
|
if (urlParams.format != null) args.push(L.Util.template('format={format}', urlParams));
|
|
if (trackname)
|
|
args.push(
|
|
L.Util.template('trackname={trackname}', {
|
|
trackname: trackname,
|
|
})
|
|
);
|
|
if (exportWaypoints) args.push('exportWaypoints=1');
|
|
|
|
var prepend_host = format != null;
|
|
|
|
return (prepend_host ? BR.conf.host : '') + '/brouter?' + args.join('&');
|
|
},
|
|
|
|
getRoute: function (latLngs, cb) {
|
|
var url = this.getUrl(latLngs, null, null, null, 'geojson'),
|
|
xhr = new XMLHttpRequest();
|
|
|
|
if (!url) {
|
|
return cb(new Error(i18next.t('warning.cannot-get-route')));
|
|
}
|
|
|
|
xhr.open('GET', url, true);
|
|
xhr.onload = L.bind(this._handleRouteResponse, this, xhr, cb);
|
|
xhr.onerror = L.bind(
|
|
function (xhr, cb) {
|
|
cb(BR.Util.getError(xhr));
|
|
},
|
|
this,
|
|
xhr,
|
|
cb
|
|
);
|
|
xhr.send();
|
|
},
|
|
|
|
_handleRouteResponse: function (xhr, cb) {
|
|
var layer, geojson;
|
|
|
|
if (
|
|
xhr.status === 200 &&
|
|
xhr.responseText &&
|
|
// application error when not GeoJSON format (text/plain for errors)
|
|
(xhr.getResponseHeader('Content-Type').split(';')[0] === 'application/geo+json' ||
|
|
xhr.getResponseHeader('Content-Type').split(';')[0] === 'application/vnd.geo+json')
|
|
) {
|
|
// leaflet.spin
|
|
//gpxLayer.fire('data:loaded');
|
|
|
|
try {
|
|
geojson = JSON.parse(xhr.responseText);
|
|
layer = this._assignFeatures(L.geoJSON(geojson).getLayers()[0]);
|
|
this.checkBRouterVersion(layer.feature.properties.creator);
|
|
|
|
return cb(null, layer);
|
|
} catch (e) {
|
|
console.error(e, xhr.responseText);
|
|
return cb(e);
|
|
}
|
|
} else {
|
|
cb(BR.Util.getError(xhr));
|
|
}
|
|
},
|
|
|
|
versionCheckDone: false,
|
|
checkBRouterVersion: function (creator) {
|
|
if (this.versionCheckDone) {
|
|
return;
|
|
}
|
|
this.versionCheckDone = true;
|
|
|
|
try {
|
|
const actualBRouterVersion = creator.replace(/^BRouter-/, '');
|
|
if (!compareVersions.satisfies(actualBRouterVersion, L.BRouter.SUPPORTED_BROUTER_VERSIONS)) {
|
|
console.warn(
|
|
'BRouter-Web ' +
|
|
BR.version +
|
|
' requires BRouter versions ' +
|
|
L.BRouter.SUPPORTED_BROUTER_VERSIONS +
|
|
', but only ' +
|
|
creator +
|
|
' was found.'
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
},
|
|
|
|
getRouteSegment: function (l1, l2, cb) {
|
|
this.queue.push({ segment: [l1, l2] }, cb);
|
|
},
|
|
|
|
uploadProfile: function (profileId, profileText, cb) {
|
|
var url = L.BRouter.URL_PROFILE_UPLOAD;
|
|
xhr = new XMLHttpRequest();
|
|
|
|
// reuse existing profile file
|
|
if (profileId) {
|
|
url += '/' + profileId;
|
|
}
|
|
|
|
xhr.open('POST', url, true);
|
|
xhr.onload = L.bind(this._handleProfileResponse, this, xhr, cb);
|
|
xhr.onerror = function (evt) {
|
|
var xhr = this;
|
|
cb(i18next.t('warning.upload-error', { error: xhr.statusText }));
|
|
};
|
|
|
|
// send profile text only, as text/plain;charset=UTF-8
|
|
xhr.send(profileText);
|
|
},
|
|
|
|
_assignFeatures: function (segment) {
|
|
if (segment.feature.properties.messages) {
|
|
var featureMessages = segment.feature.properties.messages,
|
|
segmentLatLngs = segment.getLatLngs(),
|
|
segmentLength = segmentLatLngs.length;
|
|
var featureSegmentIndex = 0;
|
|
|
|
for (var mi = 1; mi < featureMessages.length; mi++) {
|
|
var featureLatLng = this._getFeatureLatLng(featureMessages[mi]);
|
|
|
|
for (var fi = featureSegmentIndex; fi < segmentLength; fi++) {
|
|
var segmentLatLng = segmentLatLngs[fi],
|
|
featureMessage = featureMessages[mi];
|
|
|
|
segmentLatLng.feature = BR.TrackEdges.getFeature(featureMessage);
|
|
segmentLatLng.message = featureMessage;
|
|
|
|
if (featureLatLng.equals(segmentLatLngs[fi])) {
|
|
featureSegmentIndex = fi + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return segment;
|
|
},
|
|
|
|
_getFeatureLatLng: function (message) {
|
|
var lon = message[0] / 1000000,
|
|
lat = message[1] / 1000000;
|
|
|
|
return L.latLng(lat, lon);
|
|
},
|
|
|
|
_handleProfileResponse: function (xhr, cb) {
|
|
var response;
|
|
|
|
if (xhr.status === 200 && xhr.responseText && xhr.responseText.length > 0) {
|
|
response = JSON.parse(xhr.responseText);
|
|
cb(response.error, response.profileid);
|
|
} else {
|
|
cb(i18next.t('warning.profile-error'));
|
|
}
|
|
},
|
|
|
|
_getLonLatsString: function (latLngs) {
|
|
var s = '';
|
|
for (var i = 0; i < latLngs.length; i++) {
|
|
s += this._formatLatLng(latLngs[i]);
|
|
if (i < latLngs.length - 1) {
|
|
s += L.BRouter.GROUP_SEPARATOR;
|
|
}
|
|
}
|
|
return s;
|
|
},
|
|
|
|
_parseLonLats: function (s) {
|
|
var groups,
|
|
numbers,
|
|
lonlats = [];
|
|
|
|
if (!s) {
|
|
return lonlats;
|
|
}
|
|
|
|
groups = s.split(L.BRouter.GROUP_SEPARATOR);
|
|
for (var i = 0; i < groups.length; i++) {
|
|
// lng,lat
|
|
numbers = groups[i].split(L.BRouter.NUMBER_SEPARATOR);
|
|
lonlats.push(L.latLng(numbers[1], numbers[0]));
|
|
}
|
|
|
|
return lonlats;
|
|
},
|
|
|
|
_getBeelineString: function (beelineFlags) {
|
|
var indexes = [];
|
|
for (var i = 0; i < beelineFlags.length; i++) {
|
|
if (beelineFlags[i]) {
|
|
indexes.push(i);
|
|
}
|
|
}
|
|
return indexes.join(',');
|
|
},
|
|
|
|
_parseBeelines: function (s, lonlats) {
|
|
if (!lonlats || lonlats.length < 2) return [];
|
|
|
|
const beelineFlags = new Array(lonlats.length - 1);
|
|
beelineFlags.fill(false);
|
|
for (const i of s.split(',')) {
|
|
beelineFlags[i] = true;
|
|
}
|
|
return beelineFlags;
|
|
},
|
|
|
|
_getLonLatsNameString: function (latLngNames) {
|
|
var s = '';
|
|
for (var i = 0; i < latLngNames.length; i++) {
|
|
s += this._formatLatLng(latLngNames[i].latlng);
|
|
s += L.BRouter.NUMBER_SEPARATOR;
|
|
s += encodeURIComponent(latLngNames[i].name);
|
|
|
|
if (i < latLngNames.length - 1) {
|
|
s += L.BRouter.GROUP_SEPARATOR;
|
|
}
|
|
}
|
|
return s;
|
|
},
|
|
|
|
_parseLonLatNames: function (s) {
|
|
var groups,
|
|
part,
|
|
lonlatnames = [];
|
|
|
|
if (!s) {
|
|
return lonlatnames;
|
|
}
|
|
|
|
groups = s.split(L.BRouter.GROUP_SEPARATOR);
|
|
for (var i = 0; i < groups.length; i++) {
|
|
// lng,lat,name
|
|
part = groups[i].split(L.BRouter.NUMBER_SEPARATOR);
|
|
lonlatnames.push({ latlng: L.latLng(part[1], part[0]), name: decodeURIComponent(part[2]) });
|
|
}
|
|
|
|
return lonlatnames;
|
|
},
|
|
|
|
_getNogosString: function (nogos) {
|
|
var s = '';
|
|
for (var i = 0, circle; i < nogos.length; i++) {
|
|
circle = nogos[i];
|
|
s += this._formatLatLng(circle.getLatLng());
|
|
s += L.BRouter.NUMBER_SEPARATOR;
|
|
s += Math.round(circle.getRadius());
|
|
// -1 is default nogo exclusion, it should not be passed as a URL parameter.
|
|
if (
|
|
circle.options.nogoWeight !== undefined &&
|
|
circle.options.nogoWeight !== null &&
|
|
circle.options.nogoWeight !== -1
|
|
) {
|
|
s += L.BRouter.NUMBER_SEPARATOR;
|
|
s += circle.options.nogoWeight;
|
|
}
|
|
if (i < nogos.length - 1) {
|
|
s += L.BRouter.GROUP_SEPARATOR;
|
|
}
|
|
}
|
|
return s;
|
|
},
|
|
|
|
_parseNogos: function (s) {
|
|
var groups,
|
|
numbers,
|
|
nogos = [];
|
|
|
|
if (!s) {
|
|
return nogos;
|
|
}
|
|
|
|
groups = s.split(L.BRouter.GROUP_SEPARATOR);
|
|
for (var i = 0; i < groups.length; i++) {
|
|
// lng,lat,radius(,weight)
|
|
numbers = groups[i].split(L.BRouter.NUMBER_SEPARATOR);
|
|
// TODO refactor: pass simple obj, create circle in NogoAreas; use shapeOptions of instance
|
|
// [lat,lng],radius
|
|
// Parse as a nogo circle
|
|
var nogoOptions = { radius: numbers[2] };
|
|
if (numbers.length > 3) {
|
|
nogoOptions.nogoWeight = numbers[3];
|
|
}
|
|
nogos.push(L.circle([numbers[1], numbers[0]], nogoOptions));
|
|
}
|
|
|
|
return nogos;
|
|
},
|
|
|
|
_getNogosPolylinesString: function (nogos) {
|
|
var s = '';
|
|
for (var i = 0, polyline, vertices; i < nogos.length; i++) {
|
|
polyline = nogos[i];
|
|
vertices = polyline.getLatLngs();
|
|
for (var j = 0; j < vertices.length; j++) {
|
|
if (j > 0) {
|
|
s += L.BRouter.NUMBER_SEPARATOR;
|
|
}
|
|
s += this._formatLatLng(vertices[j]);
|
|
}
|
|
// -1 is default nogo exclusion, it should not be passed as a URL parameter.
|
|
if (
|
|
polyline.options.nogoWeight !== undefined &&
|
|
polyline.options.nogoWeight !== null &&
|
|
polyline.options.nogoWeight !== -1
|
|
) {
|
|
s += L.BRouter.NUMBER_SEPARATOR;
|
|
s += polyline.options.nogoWeight;
|
|
}
|
|
if (i < nogos.length - 1) {
|
|
s += L.BRouter.GROUP_SEPARATOR;
|
|
}
|
|
}
|
|
return s;
|
|
},
|
|
|
|
_parseNogosPolylines: function (s) {
|
|
var groups,
|
|
numbers,
|
|
latlngs,
|
|
nogos = [];
|
|
|
|
groups = s.split(L.BRouter.GROUP_SEPARATOR);
|
|
for (var i = 0; i < groups.length; i++) {
|
|
numbers = groups[i].split(L.BRouter.NUMBER_SEPARATOR);
|
|
if (numbers.length > 1) {
|
|
latlngs = [];
|
|
for (var j = 0; j < numbers.length - 1; ) {
|
|
var lng = Number.parseFloat(numbers[j++]);
|
|
var lat = Number.parseFloat(numbers[j++]);
|
|
latlngs.push([lat, lng]);
|
|
}
|
|
var nogoWeight;
|
|
if (j < numbers.length) {
|
|
nogoWeight = Number.parseFloat(numbers[j++]);
|
|
}
|
|
var options = L.extend(BR.NogoAreas.prototype.polylineOptions, { nogoWeight: nogoWeight });
|
|
nogos.push(L.polyline(latlngs, options));
|
|
}
|
|
}
|
|
return nogos;
|
|
},
|
|
|
|
_getNogosPolygonsString: function (nogos) {
|
|
var s = '';
|
|
for (var i = 0, polygon, vertices; i < nogos.length; i++) {
|
|
polygon = nogos[i];
|
|
vertices = polygon.getLatLngs()[0];
|
|
for (var j = 0; j < vertices.length; j++) {
|
|
if (j > 0) {
|
|
s += L.BRouter.NUMBER_SEPARATOR;
|
|
}
|
|
s += this._formatLatLng(vertices[j]);
|
|
}
|
|
// -1 is default nogo exclusion, it should not be passed as a URL parameter.
|
|
if (
|
|
polygon.options.nogoWeight !== undefined &&
|
|
polygon.options.nogoWeight !== null &&
|
|
polygon.options.nogoWeight !== -1
|
|
) {
|
|
s += L.BRouter.NUMBER_SEPARATOR;
|
|
s += polygon.options.nogoWeight;
|
|
}
|
|
if (i < nogos.length - 1) {
|
|
s += L.BRouter.GROUP_SEPARATOR;
|
|
}
|
|
}
|
|
return s;
|
|
},
|
|
|
|
_parseNogosPolygons: function (s) {
|
|
var groups,
|
|
numbers,
|
|
latlngs,
|
|
nogos = [];
|
|
|
|
groups = s.split(L.BRouter.GROUP_SEPARATOR);
|
|
for (var i = 0; i < groups.length; i++) {
|
|
numbers = groups[i].split(L.BRouter.NUMBER_SEPARATOR);
|
|
if (numbers.length > 1) {
|
|
latlngs = [];
|
|
for (var j = 0; j < numbers.length - 1; ) {
|
|
var lng = Number.parseFloat(numbers[j++]);
|
|
var lat = Number.parseFloat(numbers[j++]);
|
|
latlngs.push([lat, lng]);
|
|
}
|
|
var nogoWeight;
|
|
if (j < numbers.length) {
|
|
nogoWeight = Number.parseFloat(numbers[j++]);
|
|
}
|
|
nogos.push(L.polygon(latlngs, { nogoWeight: nogoWeight }));
|
|
}
|
|
}
|
|
return nogos;
|
|
},
|
|
|
|
_parseProfile: function (profile) {
|
|
if (BR.conf.profilesRename?.[profile]) {
|
|
return BR.conf.profilesRename[profile];
|
|
}
|
|
|
|
return profile;
|
|
},
|
|
|
|
// formats L.LatLng object as lng,lat string
|
|
_formatLatLng: function (latLng) {
|
|
var s = '';
|
|
s += L.Util.formatNum(latLng.lng ?? latLng[1], L.BRouter.PRECISION);
|
|
s += L.BRouter.NUMBER_SEPARATOR;
|
|
s += L.Util.formatNum(latLng.lat ?? latLng[0], L.BRouter.PRECISION);
|
|
return s;
|
|
},
|
|
});
|
|
|
|
L.bRouter = function (options) {
|
|
return new L.BRouter(options);
|
|
};
|