Merge pull request #497 from nrenner/68-sl-routing

Add straight line support to routing
This commit is contained in:
Norbert Renner 2022-05-12 16:26:00 +02:00 committed by GitHub
commit e5ea9173ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 950 additions and 113 deletions

View file

@ -12,13 +12,15 @@ BR.Map = {
var maxZoom = 19;
if (BR.Browser.touch) {
L.Draggable.prototype.options.clickTolerance = 10;
}
map = new L.Map('map', {
zoomControl: false, // add it manually so that we can translate it
worldCopyJump: true,
minZoom: 0,
maxZoom: maxZoom,
// fix for route drag on mobile (#285), until next Leaflet version released (> 1.6.0)
tap: false,
});
if (BR.Util.getResponsiveBreakpoint() >= '3md') {

View file

@ -74,7 +74,15 @@ BR.Export = L.Class.extend({
link.download = (name || 'brouter') + '.' + format;
link.click();
} else {
var uri = this.router.getUrl(this.latLngs, this.pois.getMarkers(), null, format, nameUri, includeWaypoints);
var uri = this.router.getUrl(
this.latLngs,
null,
this.pois.getMarkers(),
null,
format,
nameUri,
includeWaypoints
);
var evt = document.createEvent('MouseEvents');
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
var link = document.createElement('a');
@ -284,8 +292,15 @@ BR.Export._concatTotalTrack = function (segments) {
let featureCoordinates = feature.geometry.coordinates;
if (segmentIndex > 0) {
// remove first segment coordinate, same as previous last
featureCoordinates = featureCoordinates.slice(1);
// remove duplicate coordinate: first segment coordinate same as previous last,
// remove the one without ele value (e.g. beeline)
const prevLast = coordinates[coordinates.length - 1];
const first = featureCoordinates[0];
if (prevLast.length < first.length) {
coordinates.pop();
} else {
featureCoordinates = featureCoordinates.slice(1);
}
}
coordinates = coordinates.concat(featureCoordinates);
}

View file

@ -38,48 +38,41 @@ BR.Profile = L.Evented.extend({
button.blur();
},
update: function (options) {
update: function (options, cb) {
var profileName = options.profile,
profileUrl,
empty = !this.editor.getValue(),
clean = this.editor.isClean();
loading = false;
if (profileName && BR.conf.profilesUrl) {
// only synchronize profile editor/parameters with selection if no manual changes in full editor,
// else keep custom profile pinned - to prevent changes in another profile overwriting previous ones
if (empty || clean) {
this.profileName = profileName;
if (!(profileName in this.cache)) {
profileUrl = BR.conf.profilesUrl + profileName + '.brf';
BR.Util.get(
profileUrl,
L.bind(function (err, profileText) {
if (err) {
console.warn('Error getting profile from "' + profileUrl + '": ' + err);
return;
}
this.selectedProfileName = profileName;
this.cache[profileName] = profileText;
if (!(profileName in this.cache)) {
profileUrl = BR.conf.profilesUrl + profileName + '.brf';
loading = true;
BR.Util.get(
profileUrl,
L.bind(function (err, profileText) {
if (err) {
console.warn('Error getting profile from "' + profileUrl + '": ' + err);
if (cb) cb();
return;
}
// don't set when option has changed while loading
if (!this.profileName || this.profileName === profileName) {
this._setValue(profileText);
}
}, this)
);
} else {
this._setValue(this.cache[profileName]);
}
this.cache[profileName] = profileText;
if (!this.pinned.hidden) {
this.pinned.hidden = true;
}
// don't set when option has changed while loading
if (!this.profileName || this.selectedProfileName === profileName) {
this._updateProfile(profileName, profileText);
}
if (cb) cb();
}, this)
);
} else {
if (this.pinned.hidden) {
this.pinned.hidden = false;
}
this._updateProfile(profileName, this.cache[profileName]);
}
}
if (cb && !loading) cb();
},
show: function () {
@ -101,7 +94,7 @@ BR.Profile = L.Evented.extend({
}
}
const profileText = this._getProfileText();
const profileText = this._getSelectedProfileText();
if (!profileText) return value;
const regex = new RegExp(`assign\\s*${name}\\s*=?\\s*([\\w\\.]*)`);
@ -188,6 +181,26 @@ BR.Profile = L.Evented.extend({
});
},
_updateProfile: function (profileName, profileText) {
const empty = !this.editor.getValue();
const clean = this.editor.isClean();
// only synchronize profile editor/parameters with selection if no manual changes in full editor,
// else keep custom profile pinned - to prevent changes in another profile overwriting previous ones
if (empty || clean) {
this.profileName = profileName;
this._setValue(profileText);
if (!this.pinned.hidden) {
this.pinned.hidden = true;
}
} else {
if (this.pinned.hidden) {
this.pinned.hidden = false;
}
}
},
_setValue: function (profileText) {
profileText = profileText || '';
@ -363,4 +376,8 @@ BR.Profile = L.Evented.extend({
_getProfileText: function () {
return this.editor.getValue();
},
_getSelectedProfileText: function () {
return this.cache[this.selectedProfileName] ?? this.editor.getValue();
},
});

View file

@ -496,9 +496,11 @@ BR.TrackAnalysis = L.Class.extend({
}
return typeof parsed.tracktype === 'string' && parsed.tracktype === trackType;
} else if (dataName === 'internal-unknown' && typeof parsed.highway !== 'string') {
return true;
}
return parsed.highway === dataName;
return typeof parsed.highway === 'string' && parsed.highway === dataName;
case 'surface':
return this.singleWayTagMatchesData('surface', parsed, dataName);
case 'smoothness':

View file

@ -9,6 +9,9 @@ BR.TrackStats = L.Class.extend({
$('#stats-container').show();
$('#stats-info').hide();
const hasBeeline = segments.filter((line) => line?._routing?.beeline).length > 0;
document.getElementById('beeline-warning').hidden = !hasBeeline;
var stats = this.calcStats(polyline, segments),
length1 = L.Util.formatNum(stats.trackLength / 1000, 1).toLocaleString(),
length3 = L.Util.formatNum(stats.trackLength / 1000, 3).toLocaleString(undefined, {

View file

@ -25,7 +25,11 @@ BR.Xml = {
}
} else {
if (singleLineTagList.includes(tag)) {
singleLineTag = tag;
const closeIndex = xml.indexOf('>', match.index + 1);
const selfClosing = xml.charAt(closeIndex - 1) === '/';
if (!selfClosing) {
singleLineTag = tag;
}
}
let endIndex = match.index + 1;
lines.push(xml.substring(startIndex, endIndex));

View file

@ -83,6 +83,40 @@
],
});
// https://github.com/Templarian/MaterialDesign/blob/d0b28330af6648ca4c50c14d55043d71f813b3ae/svg/vector-line.svg
// Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0), https://github.com/Templarian/MaterialDesign/blob/master/LICENSE
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
id="mdi-vector-line" width="24" height="24" viewBox="0 0 24 24" class="mdi active">
<path d="M15,3V7.59L7.59,15H3V21H9V16.42L16.42,9H21V3M17,5H19V7H17M5,17H7V19H5" />
</svg>`;
const beelineClickHandler = function (control) {
const enabled = routing.toggleBeelineDrawing();
control.state(enabled ? 'deactivate-beeline' : 'activate-beeline');
};
const title = i18next.t('keyboard.generic-shortcut', {
action: '$t(map.toggle-beeline)',
key: 'B',
});
const beelineButton = L.easyButton({
states: [
{
stateName: 'activate-beeline',
icon: svg.replace(' active', ''),
onClick: beelineClickHandler,
title: title,
},
{
stateName: 'deactivate-beeline',
icon: svg,
onClick: beelineClickHandler,
title: title,
},
],
});
map.on('routing:beeline-start', () => beelineButton.state('deactivate-beeline'));
map.on('routing:beeline-end', () => beelineButton.state('activate-beeline'));
var reverseRouteButton = L.easyButton(
'fa-random',
function () {
@ -192,9 +226,12 @@
}
routingOptions = new BR.RoutingOptions();
routingOptions.on('update', updateRoute);
routingOptions.on('update', function (evt) {
profile.update(evt.options);
if (urlHash.movingMap) return;
profile.update(evt.options, () => {
updateRoute(evt);
});
});
BR.NogoAreas.MSG_BUTTON = i18next.t('keyboard.generic-shortcut', {
@ -256,7 +293,7 @@
routingPathQuality = new BR.RoutingPathQuality(map, layersControl);
routing = new BR.Routing({
routing = new BR.Routing(profile, {
routing: {
router: L.bind(router.getRouteSegment, router),
},
@ -267,15 +304,17 @@
exportRoute = new BR.Export(router, pois, profile);
routing.on('routing:routeWaypointEnd routing:setWaypointsEnd', function (evt) {
routing.on('routing:routeWaypointEnd routing:setWaypointsEnd routing:rerouteSegmentEnd', function (evt) {
search.clear();
onUpdate(evt && evt.err);
});
map.on('routing:draw-start', function () {
drawButton.state('deactivate-draw');
beelineButton.enable();
});
map.on('routing:draw-end', function () {
drawButton.state('activate-draw');
beelineButton.disable();
});
function onUpdate(err) {
@ -330,7 +369,7 @@
circlego.addTo(map);
}
var buttons = [drawButton, reverseRouteButton, nogos.getButton()];
var buttons = [drawButton, beelineButton, reverseRouteButton, nogos.getButton()];
if (circlego) buttons.push(circlego.getButton());
buttons.push(deletePointButton, deleteRouteButton);
@ -363,11 +402,12 @@
// initial option settings (after controls are added and initialized with onAdd)
router.setOptions(nogos.getOptions());
router.setOptions(routingOptions.getOptions());
profile.update(routingOptions.getOptions());
// restore active layers from local storage when called without hash
// (check before hash plugin init)
if (!location.hash) {
profile.update(routingOptions.getOptions());
// restore active layers from local storage when called without hash
layersControl.loadActiveLayers();
}
@ -391,13 +431,16 @@
router.setOptions(opts);
routingOptions.setOptions(opts);
nogos.setOptions(opts);
profile.update(opts);
if (opts.lonlats) {
routing.draw(false);
routing.clear();
routing.setWaypoints(opts.lonlats);
}
const optsOrDefault = Object.assign({}, routingOptions.getOptions(), opts);
profile.update(optsOrDefault, () => {
if (opts.lonlats) {
routing.draw(false);
routing.clear();
routing.setWaypoints(opts.lonlats, opts.beelineFlags);
}
});
if (opts.pois) {
pois.setMarkers(opts.pois);
}
@ -419,7 +462,13 @@
// this callback is used to append anything in URL after L.Hash wrote #map=zoom/lat/lng/layer
urlHash.additionalCb = function () {
var url = router
.getUrl(routing.getWaypoints(), pois.getMarkers(), circlego ? circlego.getCircle() : null, null)
.getUrl(
routing.getWaypoints(),
routing.getBeelineFlags(),
pois.getMarkers(),
circlego ? circlego.getCircle() : null,
null
)
.substr('brouter?'.length + 1);
// by default brouter use | as separator. To make URL more human-readable, we remplace them with ; for users

View file

@ -341,7 +341,7 @@ BR.routeLoader = function (map, layersControl, routing, pois) {
}
if (routingPoints.length > 0) {
routing.setWaypoints(routingPoints, function (event) {
routing.setWaypoints(routingPoints, null, function (event) {
if (!event) return;
var err = event.error;
BR.message.showError(

View file

@ -28,12 +28,23 @@ BR.Routing = L.Routing.extend({
draw: {
enable: 68, // char code for 'd'
disable: 27, // char code for 'ESC'
beelineMode: 66, // char code for 'b'
// char code for 'Shift', same key as `beelineModifierName`
beelineModifier: 16,
// modifier key to draw straight line on click [shiftKey|null] (others don't work everywhere)
beelineModifierName: 'shiftKey',
},
reverse: 82, // char code for 'r'
deleteLastPoint: 90, // char code for 'z'
},
},
initialize: function (profile, options) {
L.Routing.prototype.initialize.call(this, options);
this.profile = profile;
},
onAdd: function (map) {
this.options.tooltips.waypoint = i18next.t('map.route-tooltip-waypoint');
this.options.tooltips.segment = i18next.t('map.route-tooltip-segment');
@ -48,13 +59,36 @@ BR.Routing = L.Routing.extend({
this._waypoints.on('layeradd', this._setMarkerOpacity, this);
this.on('routing:routeWaypointStart routing:rerouteAllSegmentsStart', function (evt) {
this._removeDistanceMarkers();
// flag if (re-)routing of all segments is ongoing
this._routingAll = false;
this.on('routing:rerouteAllSegmentsStart routing:setWaypointsStart', function (evt) {
this._routingAll = true;
});
this.on('routing:rerouteAllSegmentsEnd routing:setWaypointsEnd', function (evt) {
this._routingAll = false;
});
this.on('routing:routeWaypointEnd routing:setWaypointsEnd routing:rerouteAllSegmentsEnd', function (evt) {
this._updateDistanceMarkers(evt);
});
this.on(
'routing:routeWaypointStart routing:rerouteAllSegmentsStart routing:rerouteSegmentStart',
function (evt) {
if (!this._routingAll || evt.type === 'routing:rerouteAllSegmentsStart') {
this._removeDistanceMarkers();
}
}
);
this.on(
'routing:routeWaypointEnd routing:setWaypointsEnd routing:rerouteAllSegmentsEnd routing:rerouteSegmentEnd',
function (evt) {
if (
!this._routingAll ||
evt.type === 'routing:rerouteAllSegmentsEnd' ||
evt.type === 'routing:setWaypointsEnd'
) {
this._updateDistanceMarkers(evt);
}
}
);
// turn line mouse marker off while over waypoint marker
this.on(
@ -65,7 +99,7 @@ BR.Routing = L.Routing.extend({
return;
}
this._mouseMarker.setOpacity(0.0);
this._hideMouseMarker();
this._map.off('mousemove', this._segmentOnMousemove, this);
this._suspended = true;
},
@ -141,22 +175,27 @@ BR.Routing = L.Routing.extend({
this._show();
}
}
function hide() {
var hide = function () {
if (!this._hidden && this._parent._waypoints._first) {
this._hide();
}
}.bind(this._draw);
function hideOverControl(e) {
hide();
// prevent showing trailer when clicking state buttons (causes event that propagates to map)
L.DomEvent.stopPropagation(e);
}
this._draw.on('enabled', function () {
this._map.on('mouseout', hide, this);
this._map.on('mouseover', show, this);
L.DomEvent.on(this._map._controlContainer, 'mouseout', show, this);
L.DomEvent.on(this._map._controlContainer, 'mouseover', hide, this);
L.DomEvent.on(this._map._controlContainer, 'mouseover', hideOverControl, this);
});
this._draw.on('disabled', function () {
this._map.off('mouseout', hide, this);
this._map.off('mouseover', show, this);
L.DomEvent.off(this._map._controlContainer, 'mouseout', show, this);
L.DomEvent.off(this._map._controlContainer, 'mouseover', hide, this);
L.DomEvent.off(this._map._controlContainer, 'mouseover', hideOverControl, this);
});
// Call show after deleting last waypoint, but hide trailer.
@ -175,6 +214,21 @@ BR.Routing = L.Routing.extend({
this._draw
);
// avoid accidental shift-drag zooms while drawing beeline with shift-click
this._map.boxZoom.disable();
this._map.addHandler('boxZoom', BR.ClickTolerantBoxZoom);
this._draw.on('enabled', function () {
this._map.boxZoom.tolerant = true;
});
this._draw.on('disabled', function () {
this._map.boxZoom.tolerant = false;
});
// remove listeners registered in super.onAdd, keys not working when map container lost focus
// (by navbar/sidebar interaction), use document instead
L.DomEvent.removeListener(this._container, 'keydown', this._keydownListener, this);
L.DomEvent.removeListener(this._container, 'keyup', this._keyupListener, this);
L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);
L.DomEvent.addListener(document, 'keyup', this._keyupListener, this);
@ -185,7 +239,9 @@ BR.Routing = L.Routing.extend({
},
_addSegmentCasing: function (e) {
var casing = L.polyline(e.layer.getLatLngs(), this.options.styles.trackCasing);
// extend layer style to inherit beeline dashArray
const casingStyle = Object.assign({}, e.layer.options, this.options.styles.trackCasing);
const casing = L.polyline(e.layer.getLatLngs(), Object.assign({}, casingStyle, { interactive: false }));
this._segmentsCasing.addLayer(casing);
e.layer._casing = casing;
this._segments.bringToFront();
@ -262,7 +318,7 @@ BR.Routing = L.Routing.extend({
}
},
setWaypoints: function (latLngs, cb) {
setWaypoints: function (latLngs, beelineFlags, cb) {
var i;
var callbackCount = 0;
var firstErr;
@ -291,7 +347,8 @@ BR.Routing = L.Routing.extend({
this._loadingTrailerGroup._map = null;
for (i = 0; latLngs && i < latLngs.length; i++) {
this.addWaypoint(latLngs[i], this._waypoints._last, null, callback);
const beeline = beelineFlags && i < beelineFlags.length ? beelineFlags[i] : null;
this.addWaypoint(latLngs[i], beeline, this._waypoints._last, null, callback);
}
this._loadingTrailerGroup._map = this._map;
@ -374,14 +431,16 @@ BR.Routing = L.Routing.extend({
this.reverse();
} else if (e.keyCode === this.options.shortcut.deleteLastPoint) {
this.deleteLastPoint();
} else if (e.keyCode === this.options.shortcut.draw.beelineMode) {
this.toggleBeelineDrawing();
} else if (e.keyCode === this.options.shortcut.draw.beelineModifier) {
this._draw._setTrailerStyle(true);
}
},
_keyupListener: function (e) {
// Prevent Leaflet from triggering drawing a second time on keyup,
// since this is already done in _keydownListener
if (e.keyCode === this.options.shortcut.draw.enable) {
return;
if (e.keyCode === this.options.shortcut.draw.beelineModifier) {
this._draw._setTrailerStyle(false);
}
},
@ -390,10 +449,12 @@ BR.Routing = L.Routing.extend({
},
reverse: function () {
var waypoints = this.getWaypoints();
const waypoints = this.getWaypoints();
const beelineFlags = this.getBeelineFlags();
waypoints.reverse();
beelineFlags.reverse();
this.clear();
this.setWaypoints(waypoints);
this.setWaypoints(waypoints, beelineFlags);
},
deleteLastPoint: function () {
@ -417,4 +478,166 @@ BR.Routing = L.Routing.extend({
this._map.addLayer(this._distanceMarkers);
}
},
_distance: function (latLng1, latLng2) {
//return Math.round(latLng1.distanceTo(latLng2));
const [ilon1, ilat1] = btools.util.CheapRuler.toIntegerLngLat([latLng1.lng, latLng1.lat]);
const [ilon2, ilat2] = btools.util.CheapRuler.toIntegerLngLat([latLng2.lng, latLng2.lat]);
return btools.util.CheapRuler.calcDistance(ilon1, ilat1, ilon2, ilat2);
},
_computeKinematic: function (distance, deltaHeight, costFactor) {
const rc = new BR.RoutingContext(this.profile);
rc.expctxWay = new BR.BExpressionContextWay(undefined, costFactor);
const stdPath = new BR.StdPath();
stdPath.computeKinematic(rc, distance, deltaHeight, true);
return stdPath;
},
_getCostFactor: function (line) {
let costFactor;
if (line) {
const props = line.feature.properties;
const length = props['track-length'];
const cost = props['cost'];
if (length) {
costFactor = cost / length;
}
}
return costFactor;
},
_interpolateBeelines: function (serialBeelines, before, after) {
let altStart = before?.getLatLngs()[before.getLatLngs().length - 1].alt;
const altEnd = after?.getLatLngs()[0].alt ?? altStart;
altStart ?? (altStart = altEnd);
let serialDelta = 0;
if (altStart != null && altEnd != null) {
serialDelta = altEnd - altStart;
}
const serialDistance = serialBeelines.reduce(
(dist, line) => (dist += line.feature.properties['track-length']),
0
);
let beforeCostFactor = this._getCostFactor(before);
let afterCostFactor = this._getCostFactor(after);
let costFactor;
if (beforeCostFactor != null && afterCostFactor != null) {
costFactor = Math.max(beforeCostFactor, afterCostFactor);
} else {
costFactor = beforeCostFactor ?? afterCostFactor;
}
for (const beeline of serialBeelines) {
const props = beeline.feature.properties;
const distance = props['track-length'];
const deltaHeight = (serialDelta * distance) / serialDistance;
const stdPath = this._computeKinematic(distance, deltaHeight, costFactor);
props['total-energy'] = stdPath.getTotalEnergy();
props['total-time'] = stdPath.getTotalTime();
// match BRouter/Java rounding where `(int)` cast truncates decimals
// https://github.com/abrensch/brouter/blob/14d5a2c4e6b101a2eab711e70151142881df95c6/brouter-core/src/main/java/btools/router/RoutingEngine.java#L1216-L1217
if (deltaHeight > 0) {
// no filtering for simplicity for now
props['filtered ascend'] = Math.trunc(deltaHeight);
}
props['plain-ascend'] = Math.trunc(deltaHeight + 0.5);
// do not set interpolated alt value, to explicitly show missing data, e.g. in height graph
props['cost'] = Math.round(distance * (costFactor ?? 0));
}
},
_updateBeelines: function () {
L.Routing.prototype._updateBeelines.call(this);
let serialBeelines = [];
let before = null;
this._eachSegment(function (m1, m2, line) {
if (line?._routing?.beeline) {
serialBeelines.push(line);
} else {
if (serialBeelines.length > 0) {
this._interpolateBeelines(serialBeelines, before, line);
}
before = line;
serialBeelines = [];
}
});
if (serialBeelines.length > 0) {
this._interpolateBeelines(serialBeelines, before, null);
}
},
createBeeline: function (latLng1, latLng2) {
const layer = L.Routing.prototype.createBeeline.call(this, latLng1, latLng2);
// remove alt from cloned LatLngs to show gap in elevation graph to indicate no data inbetween
delete layer.getLatLngs()[0].alt;
delete layer.getLatLngs()[1].alt;
const distance = this._distance(latLng1, latLng2);
const props = {
cost: 0,
'filtered ascend': 0,
'plain-ascend': 0,
'total-energy': 0,
'total-time': 0,
'track-length': distance,
messages: [
[
'Longitude',
'Latitude',
'Elevation',
'Distance',
'CostPerKm',
'ElevCost',
'TurnCost',
'NodeCost',
'InitialCost',
'WayTags',
'NodeTags',
'Time',
'Energy',
],
[
latLng2.lng * 1000000,
latLng2.lat * 1000000,
null,
distance,
null,
null,
null,
null,
null,
'',
'',
null,
null,
],
],
};
layer.feature = turf.lineString(
[
[latLng1.lng, latLng1.lat],
[latLng2.lng, latLng2.lat],
],
props
);
// corresponding to BRouter._assignFeatures
for (const latLng of layer.getLatLngs()) {
const featureMessage = props.messages[1];
latLng.feature = BR.TrackEdges.getFeature(featureMessage);
latLng.message = featureMessage;
}
return layer;
},
});

View file

@ -227,6 +227,7 @@ var HotLineQualityProvider = L.Class.extend({
var flatLines = [];
for (var i = 0; segments && i < segments.length; i++) {
var segment = segments[i];
if (segment._routing?.beeline) continue;
var vals = this._computeLatLngVals(segment);
segmentLatLngs.push(vals);
Array.prototype.push.apply(flatLines, vals);

View file

@ -70,6 +70,11 @@ BR.tracksLoader = function (map, layersControl, routing, pois) {
}
},
});
// make sure tracks are always shown below route by adding a custom pane below `leaflet-overlay-pane`
map.createPane('tracks');
map.getPane('tracks').style.zIndex = 350;
var tracksLoaderControl = new TracksLoader();
tracksLoaderControl.addTo(map);

View file

@ -42,10 +42,15 @@ L.BRouter = L.Class.extend({
L.setOptions(this, options);
},
getUrlParams: function (latLngs, pois, circlego, format) {
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);
@ -84,6 +89,9 @@ L.BRouter = L.Class.extend({
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);
}
@ -117,11 +125,12 @@ L.BRouter = L.Class.extend({
return opts;
},
getUrl: function (latLngs, pois, circlego, format, trackname, exportWaypoints) {
var urlParams = this.getUrlParams(latLngs, pois, circlego, format);
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));
@ -144,7 +153,7 @@ L.BRouter = L.Class.extend({
},
getRoute: function (latLngs, cb) {
var url = this.getUrl(latLngs, null, null, 'geojson'),
var url = this.getUrl(latLngs, null, null, null, 'geojson'),
xhr = new XMLHttpRequest();
if (!url) {
@ -228,7 +237,7 @@ L.BRouter = L.Class.extend({
var segmentLatLng = segmentLatLngs[fi],
featureMessage = featureMessages[mi];
segmentLatLng.feature = this._getFeature(featureMessage);
segmentLatLng.feature = BR.TrackEdges.getFeature(featureMessage);
segmentLatLng.message = featureMessage;
if (featureLatLng.equals(segmentLatLngs[fi])) {
@ -241,22 +250,6 @@ L.BRouter = L.Class.extend({
return segment;
},
_getFeature: function (featureMessage) {
//["Longitude", "Latitude", "Elevation", "Distance", "CostPerKm", "ElevCost", "TurnCost", "NodeCost", "InitialCost", "WayTags", "NodeTags"]
return {
cost: {
perKm: parseInt(featureMessage[4]),
elev: parseInt(featureMessage[5]),
turn: parseInt(featureMessage[6]),
node: parseInt(featureMessage[7]),
initial: parseInt(featureMessage[8]),
},
distance: parseInt(featureMessage[3]),
wayTags: featureMessage[9],
nodeTags: featureMessage[10],
};
},
_getFeatureLatLng: function (message) {
var lon = message[0] / 1000000,
lat = message[1] / 1000000;
@ -305,6 +298,27 @@ L.BRouter = L.Class.extend({
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++) {

125
js/util/CheapRuler.js Normal file
View file

@ -0,0 +1,125 @@
/* Generated from Java with JSweet 3.1.0 - http://www.jsweet.org */
//var btools;
btools = {};
(function (btools) {
var util;
(function (util) {
class CheapRuler {
static __static_initialize() {
if (!CheapRuler.__static_initialized) {
CheapRuler.__static_initialized = true;
CheapRuler.__static_initializer_0();
}
}
static DEG_TO_RAD_$LI$() {
CheapRuler.__static_initialize();
if (CheapRuler.DEG_TO_RAD == null) {
CheapRuler.DEG_TO_RAD = Math.PI / 180.0;
}
return CheapRuler.DEG_TO_RAD;
}
static SCALE_CACHE_$LI$() {
CheapRuler.__static_initialize();
if (CheapRuler.SCALE_CACHE == null) {
CheapRuler.SCALE_CACHE = ((s) => {
let a = [];
while (s-- > 0) a.push(null);
return a;
})(CheapRuler.SCALE_CACHE_LENGTH);
}
return CheapRuler.SCALE_CACHE;
}
static __static_initializer_0() {
for (let i = 0; i < CheapRuler.SCALE_CACHE_LENGTH; i++) {
{
CheapRuler.SCALE_CACHE_$LI$()[i] = CheapRuler.calcKxKyFromILat(
i * CheapRuler.SCALE_CACHE_INCREMENT + ((CheapRuler.SCALE_CACHE_INCREMENT / 2) | 0)
);
}
}
}
/*private*/ static calcKxKyFromILat(ilat) {
const lat = CheapRuler.DEG_TO_RAD_$LI$() * (ilat * CheapRuler.ILATLNG_TO_LATLNG - 90);
const cos = Math.cos(lat);
const cos2 = 2 * cos * cos - 1;
const cos3 = 2 * cos * cos2 - cos;
const cos4 = 2 * cos * cos3 - cos2;
const cos5 = 2 * cos * cos4 - cos3;
const kxky = [0, 0];
kxky[0] =
(111.41513 * cos - 0.09455 * cos3 + 1.2e-4 * cos5) *
CheapRuler.ILATLNG_TO_LATLNG *
CheapRuler.KILOMETERS_TO_METERS;
kxky[1] =
(111.13209 - 0.56605 * cos2 + 0.0012 * cos4) *
CheapRuler.ILATLNG_TO_LATLNG *
CheapRuler.KILOMETERS_TO_METERS;
return kxky;
}
/**
* Calculate the degree-&gt;meter scale for given latitude
*
* @return {double[]} [lon-&gt;meter,lat-&gt;meter]
* @param {number} ilat
*/
static getLonLatToMeterScales(ilat) {
return CheapRuler.SCALE_CACHE_$LI$()[(ilat / CheapRuler.SCALE_CACHE_INCREMENT) | 0];
}
/**
* Compute the distance (in meters) between two points represented by their
* (integer) latitude and longitude.
*
* @param {number} ilon1 Integer longitude for the start point. this is (longitude in degrees + 180) * 1e6.
* @param {number} ilat1 Integer latitude for the start point, this is (latitude + 90) * 1e6.
* @param {number} ilon2 Integer longitude for the end point, this is (longitude + 180) * 1e6.
* @param {number} ilat2 Integer latitude for the end point, this is (latitude + 90) * 1e6.
* @return {number} The distance between the two points, in meters.
*
* Note:
* Integer longitude is ((longitude in degrees) + 180) * 1e6.
* Integer latitude is ((latitude in degrees) + 90) * 1e6.
*/
static distance(ilon1, ilat1, ilon2, ilat2) {
const kxky = CheapRuler.getLonLatToMeterScales((ilat1 + ilat2) >> 1);
const dlon = (ilon1 - ilon2) * kxky[0];
const dlat = (ilat1 - ilat2) * kxky[1];
return Math.sqrt(dlat * dlat + dlon * dlon);
}
}
CheapRuler.__static_initialized = false;
/**
* Cheap-Ruler Java implementation
* See
* https://blog.mapbox.com/fast-geodesic-approximations-with-cheap-ruler-106f229ad016
* for more details.
*
* Original code is at https://github.com/mapbox/cheap-ruler under ISC license.
*
* This is implemented as a Singleton to have a unique cache for the cosine
* values across all the code.
*/
CheapRuler.ILATLNG_TO_LATLNG = 1.0e-6;
CheapRuler.KILOMETERS_TO_METERS = 1000;
CheapRuler.SCALE_CACHE_LENGTH = 1800;
CheapRuler.SCALE_CACHE_INCREMENT = 100000;
util.CheapRuler = CheapRuler;
CheapRuler['__class'] = 'btools.util.CheapRuler';
})((util = btools.util || (btools.util = {})));
})(btools || (btools = {}));
btools.util.CheapRuler.__static_initialize();
btools.util.CheapRuler.toIntegerLngLat = (coordinate) => {
const ilon = Math.round((coordinate[0] + 180) * 1e6);
const ilat = Math.round((coordinate[1] + 90) * 1e6);
return [ilon, ilat];
};
btools.util.CheapRuler.calcDistance = (ilon1, ilat1, ilon2, ilat2) => {
const distanceFloat = btools.util.CheapRuler.distance(ilon1, ilat1, ilon2, ilat2);
// Convert to integer (no decimals) values to match BRouter OsmPathElement.calcDistance:
// `(int)(CheapRuler.distance(ilon, ilat, p.getILon(), p.getILat()) + 1.0 );`
// https://github.com/abrensch/brouter/blob/1640bafa800f8bab7aebde797edc99fdbeea3b07/brouter-core/src/main/java/btools/router/OsmPathElement.java#L81
return Math.trunc(distanceFloat + 1.0);
};

View file

@ -0,0 +1,55 @@
/**
* Avoids conflict between shift-click and shift-drag.
* Extends BoxZoom to support a small click tolerance like in Draggable and
* a larger drag tolerance as a "neutral zone" before starting with box zoom dragging,
* to avoid accidental zooms.
*/
BR.ClickTolerantBoxZoom = L.Map.BoxZoom.extend({
clickTolerance: L.Draggable.prototype.options.clickTolerance,
// use more than clickTolerance before starting box zoom to surely avoid accidental zooms
dragTolerance: 15,
// flag to enable or disable click/drag tolerance, classic BoxZoom behaviour when false
tolerant: true,
// "neutral zone", state between clickTolerance and dragTolerance,
// already signals dragging to map and thus prevents click
_preMoved: false,
moved: function () {
return this._preMoved || this._moved;
},
_resetState: function () {
L.Map.BoxZoom.prototype._resetState.call(this);
this._preMoved = false;
},
_onMouseMove: function (e) {
if (!this._moved) {
const point = this._map.mouseEventToContainerPoint(e);
// derived from L.Draggable._onMove
var offsetPoint = point.clone()._subtract(this._startPoint);
var offset = Math.abs(offsetPoint.x || 0) + Math.abs(offsetPoint.y || 0);
if (this.tolerant && offset < this.dragTolerance) {
if (!this._preMoved && offset >= this.clickTolerance) {
this._preMoved = true;
}
return;
}
}
L.Map.BoxZoom.prototype._onMouseMove.call(this, e);
},
_onMouseUp: function (e) {
L.Map.BoxZoom.prototype._onMouseUp.call(this, e);
if (!this._moved && this._preMoved) {
this._clearDeferredResetState();
this._resetStateTimeout = setTimeout(L.Util.bind(this._resetState, this), 0);
}
},
});

16
js/util/LeafletPatches.js Normal file
View file

@ -0,0 +1,16 @@
// Fixes wrong added offset when dragging, which can leave mouse off the marker
// after dragging and cause a map click
// see https://github.com/Leaflet/Leaflet/pull/7446
// see https://github.com/Leaflet/Leaflet/issues/4457
L.Draggable.prototype._onMoveOrig = L.Draggable.prototype._onMove;
L.Draggable.prototype._onMove = function (e) {
var start = !this._moved;
this._onMoveOrig.call(this, e);
if (start && this._moved) {
var offset = this._newPos.subtract(this._startPos);
this._startPos = this._startPos.add(offset);
this._newPos = this._newPos.add(offset);
}
};

164
js/util/StdPath.js Normal file
View file

@ -0,0 +1,164 @@
(function () {
// Calculates time and energy stats
class BExpressionContextWay {
constructor(maxspeed = 45.0, costfactor = 1.0) {
this.maxspeed = maxspeed;
this.costfactor = costfactor;
}
getMaxspeed() {
return this.maxspeed;
}
getCostfactor() {
return this.costfactor;
}
}
class BExpressionContext {
constructor(profile) {
this.profile = profile;
}
getVariableValue(name, defaultValue) {
let value = this.profile?.getProfileVar(name) ?? defaultValue;
if (value === 'true') {
value = 1;
} else if (value === 'false') {
value = 0;
}
return +value;
}
}
// from BRouter btools.router.RoutingContext
class RoutingContext {
constructor(profile) {
this.expctxGlobal = new BExpressionContext(profile);
this.expctxWay = new BExpressionContextWay();
this.bikeMode = 0 !== this.expctxGlobal.getVariableValue('validForBikes', 0);
this.footMode = 0 !== this.expctxGlobal.getVariableValue('validForFoot', 0);
this.totalMass = this.expctxGlobal.getVariableValue('totalMass', 90.0);
this.maxSpeed = this.expctxGlobal.getVariableValue('maxSpeed', this.footMode ? 6.0 : 45.0) / 3.6;
this.S_C_x = this.expctxGlobal.getVariableValue('S_C_x', 0.5 * 0.45);
this.defaultC_r = this.expctxGlobal.getVariableValue('C_r', 0.01);
this.bikerPower = this.expctxGlobal.getVariableValue('bikerPower', 100.0);
}
}
// from BRouter btools.router.StdPath
class StdPath {
constructor() {
this.totalTime = 0;
this.totalEnergy = 0;
this.elevation_buffer = 0;
}
/**
* Approximation to Math.exp for small negative arguments
* @param {number} e
* @return {number}
*/
static exp(e) {
var x = e;
var f = 1.0;
while (e < -1.0) {
{
e += 1.0;
f *= 0.367879;
}
}
return f * (1.0 + x * (1.0 + x * (0.5 + x * (0.166667 + 0.0416667 * x))));
}
static solveCubic(a, c, d) {
var v = 8.0;
var findingStartvalue = true;
for (var i = 0; i < 10; i++) {
{
var y = (a * v * v + c) * v - d;
if (y < 0.1) {
if (findingStartvalue) {
v *= 2.0;
continue;
}
break;
}
findingStartvalue = false;
var y_prime = 3 * a * v * v + c;
v -= y / y_prime;
}
}
return v;
}
resetState() {
this.totalTime = 0.0;
this.totalEnergy = 0.0;
this.elevation_buffer = 0.0;
}
calcIncline(dist) {
var min_delta = 3.0;
var shift;
if (this.elevation_buffer > min_delta) {
shift = -min_delta;
} else if (this.elevation_buffer < min_delta) {
shift = -min_delta;
} else {
return 0.0;
}
var decayFactor = StdPath.exp(-dist / 100.0);
var new_elevation_buffer = (this.elevation_buffer + shift) * decayFactor - shift;
var incline = (this.elevation_buffer - new_elevation_buffer) / dist;
this.elevation_buffer = new_elevation_buffer;
return incline;
}
computeKinematic(rc, dist, delta_h, detailMode) {
if (!detailMode) {
return;
}
this.elevation_buffer += delta_h;
var incline = this.calcIncline(dist);
var wayMaxspeed;
wayMaxspeed = rc.expctxWay.getMaxspeed() / 3.6;
if (wayMaxspeed === 0) {
wayMaxspeed = rc.maxSpeed;
}
wayMaxspeed = Math.min(wayMaxspeed, rc.maxSpeed);
var speed;
var f_roll = rc.totalMass * StdPath.GRAVITY * (rc.defaultC_r + incline);
if (rc.footMode || rc.expctxWay.getCostfactor() > 4.9) {
speed = rc.maxSpeed * 3.6;
speed = (speed * StdPath.exp(-3.5 * Math.abs(incline + 0.05))) / 3.6;
} else if (rc.bikeMode) {
speed = StdPath.solveCubic(rc.S_C_x, f_roll, rc.bikerPower);
speed = Math.min(speed, wayMaxspeed);
} else {
speed = wayMaxspeed;
}
var dt = dist / speed;
this.totalTime += dt;
var energy = dist * (rc.S_C_x * speed * speed + f_roll);
if (energy > 0.0) {
this.totalEnergy += energy;
}
}
getTotalTime() {
return this.totalTime;
}
getTotalEnergy() {
return this.totalEnergy;
}
}
StdPath.GRAVITY = 9.81;
BR.StdPath = StdPath;
BR.RoutingContext = RoutingContext;
BR.BExpressionContextWay = BExpressionContextWay;
})();

View file

@ -32,6 +32,7 @@ BR.Track = {
zIndexOffset: -1000,
});
},
pane: 'tracks',
};
},

View file

@ -5,6 +5,24 @@
* @type {L.Class}
*/
BR.TrackEdges = L.Class.extend({
statics: {
getFeature: function (featureMessage) {
//["Longitude", "Latitude", "Elevation", "Distance", "CostPerKm", "ElevCost", "TurnCost", "NodeCost", "InitialCost", "WayTags", "NodeTags"]
return {
cost: {
perKm: parseInt(featureMessage[4]),
elev: parseInt(featureMessage[5]),
turn: parseInt(featureMessage[6]),
node: parseInt(featureMessage[7]),
initial: parseInt(featureMessage[8]),
},
distance: parseInt(featureMessage[3]),
wayTags: featureMessage[9],
nodeTags: featureMessage[10],
};
},
},
/**
* List of indexes for the track array where
* a segment with different features ends