Merge pull request #497 from nrenner/68-sl-routing
Add straight line support to routing
This commit is contained in:
commit
e5ea9173ae
29 changed files with 950 additions and 113 deletions
|
|
@ -100,6 +100,18 @@
|
||||||
nodata: {
|
nodata: {
|
||||||
color: 'darkred',
|
color: 'darkred',
|
||||||
},
|
},
|
||||||
|
beeline: {
|
||||||
|
weight: 5,
|
||||||
|
dashArray: [1, 10],
|
||||||
|
color: 'magenta',
|
||||||
|
opacity: BR.conf.defaultOpacity,
|
||||||
|
},
|
||||||
|
beelineTrailer: {
|
||||||
|
weight: 5,
|
||||||
|
dashArray: [1, 10],
|
||||||
|
opacity: 0.6,
|
||||||
|
color: 'magenta',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
BR.conf.markerColors = {
|
BR.conf.markerColors = {
|
||||||
|
|
|
||||||
|
|
@ -443,6 +443,22 @@ button.btn {
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.activate-beeline-active,
|
||||||
|
button.deactivate-beeline-active {
|
||||||
|
transition-duration: 0.3s;
|
||||||
|
}
|
||||||
|
button.activate-beeline-active.disabled,
|
||||||
|
button.deactivate-beeline-active.disabled {
|
||||||
|
height: 0;
|
||||||
|
border-bottom-width: 0px;
|
||||||
|
}
|
||||||
|
.mdi.active {
|
||||||
|
fill: #2074b6;
|
||||||
|
}
|
||||||
|
.mdi {
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
/* smaller tab height */
|
/* smaller tab height */
|
||||||
.nav > li > a {
|
.nav > li > a {
|
||||||
padding: 2px 15px;
|
padding: 2px 15px;
|
||||||
|
|
|
||||||
10
index.html
10
index.html
|
|
@ -1132,6 +1132,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="stats-container" class="flexrow flexgrow">
|
<div id="stats-container" class="flexrow flexgrow">
|
||||||
<ul id="stats">
|
<ul id="stats">
|
||||||
|
<li id="beeline-warning" hidden>
|
||||||
|
<div class="text-muted small d-none d-md-block"> </div>
|
||||||
|
<p class="stats-label">
|
||||||
|
<abbr
|
||||||
|
class="fa fa-exclamation-triangle"
|
||||||
|
data-i18n="[title]footer.beeline-warning"
|
||||||
|
title="Warning: no data for straight lines, values interpolated"
|
||||||
|
></abbr>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div class="text-muted small d-none d-md-block" data-i18n="footer.distance">Distance</div>
|
<div class="text-muted small d-none d-md-block" data-i18n="footer.distance">Distance</div>
|
||||||
<p class="stats-label">
|
<p class="stats-label">
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,15 @@ BR.Map = {
|
||||||
|
|
||||||
var maxZoom = 19;
|
var maxZoom = 19;
|
||||||
|
|
||||||
|
if (BR.Browser.touch) {
|
||||||
|
L.Draggable.prototype.options.clickTolerance = 10;
|
||||||
|
}
|
||||||
|
|
||||||
map = new L.Map('map', {
|
map = new L.Map('map', {
|
||||||
zoomControl: false, // add it manually so that we can translate it
|
zoomControl: false, // add it manually so that we can translate it
|
||||||
worldCopyJump: true,
|
worldCopyJump: true,
|
||||||
minZoom: 0,
|
minZoom: 0,
|
||||||
maxZoom: maxZoom,
|
maxZoom: maxZoom,
|
||||||
// fix for route drag on mobile (#285), until next Leaflet version released (> 1.6.0)
|
|
||||||
tap: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (BR.Util.getResponsiveBreakpoint() >= '3md') {
|
if (BR.Util.getResponsiveBreakpoint() >= '3md') {
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,15 @@ BR.Export = L.Class.extend({
|
||||||
link.download = (name || 'brouter') + '.' + format;
|
link.download = (name || 'brouter') + '.' + format;
|
||||||
link.click();
|
link.click();
|
||||||
} else {
|
} 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');
|
var evt = document.createEvent('MouseEvents');
|
||||||
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
|
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
|
||||||
var link = document.createElement('a');
|
var link = document.createElement('a');
|
||||||
|
|
@ -284,8 +292,15 @@ BR.Export._concatTotalTrack = function (segments) {
|
||||||
|
|
||||||
let featureCoordinates = feature.geometry.coordinates;
|
let featureCoordinates = feature.geometry.coordinates;
|
||||||
if (segmentIndex > 0) {
|
if (segmentIndex > 0) {
|
||||||
// remove first segment coordinate, same as previous last
|
// remove duplicate coordinate: first segment coordinate same as previous last,
|
||||||
featureCoordinates = featureCoordinates.slice(1);
|
// 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);
|
coordinates = coordinates.concat(featureCoordinates);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,48 +38,41 @@ BR.Profile = L.Evented.extend({
|
||||||
button.blur();
|
button.blur();
|
||||||
},
|
},
|
||||||
|
|
||||||
update: function (options) {
|
update: function (options, cb) {
|
||||||
var profileName = options.profile,
|
var profileName = options.profile,
|
||||||
profileUrl,
|
profileUrl,
|
||||||
empty = !this.editor.getValue(),
|
loading = false;
|
||||||
clean = this.editor.isClean();
|
|
||||||
|
|
||||||
if (profileName && BR.conf.profilesUrl) {
|
if (profileName && BR.conf.profilesUrl) {
|
||||||
// only synchronize profile editor/parameters with selection if no manual changes in full editor,
|
this.selectedProfileName = profileName;
|
||||||
// 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.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
|
this.cache[profileName] = profileText;
|
||||||
if (!this.profileName || this.profileName === profileName) {
|
|
||||||
this._setValue(profileText);
|
|
||||||
}
|
|
||||||
}, this)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this._setValue(this.cache[profileName]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.pinned.hidden) {
|
// don't set when option has changed while loading
|
||||||
this.pinned.hidden = true;
|
if (!this.profileName || this.selectedProfileName === profileName) {
|
||||||
}
|
this._updateProfile(profileName, profileText);
|
||||||
|
}
|
||||||
|
if (cb) cb();
|
||||||
|
}, this)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (this.pinned.hidden) {
|
this._updateProfile(profileName, this.cache[profileName]);
|
||||||
this.pinned.hidden = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cb && !loading) cb();
|
||||||
},
|
},
|
||||||
|
|
||||||
show: function () {
|
show: function () {
|
||||||
|
|
@ -101,7 +94,7 @@ BR.Profile = L.Evented.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const profileText = this._getProfileText();
|
const profileText = this._getSelectedProfileText();
|
||||||
if (!profileText) return value;
|
if (!profileText) return value;
|
||||||
|
|
||||||
const regex = new RegExp(`assign\\s*${name}\\s*=?\\s*([\\w\\.]*)`);
|
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) {
|
_setValue: function (profileText) {
|
||||||
profileText = profileText || '';
|
profileText = profileText || '';
|
||||||
|
|
||||||
|
|
@ -363,4 +376,8 @@ BR.Profile = L.Evented.extend({
|
||||||
_getProfileText: function () {
|
_getProfileText: function () {
|
||||||
return this.editor.getValue();
|
return this.editor.getValue();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_getSelectedProfileText: function () {
|
||||||
|
return this.cache[this.selectedProfileName] ?? this.editor.getValue();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -496,9 +496,11 @@ BR.TrackAnalysis = L.Class.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
return typeof parsed.tracktype === 'string' && parsed.tracktype === trackType;
|
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':
|
case 'surface':
|
||||||
return this.singleWayTagMatchesData('surface', parsed, dataName);
|
return this.singleWayTagMatchesData('surface', parsed, dataName);
|
||||||
case 'smoothness':
|
case 'smoothness':
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ BR.TrackStats = L.Class.extend({
|
||||||
$('#stats-container').show();
|
$('#stats-container').show();
|
||||||
$('#stats-info').hide();
|
$('#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),
|
var stats = this.calcStats(polyline, segments),
|
||||||
length1 = L.Util.formatNum(stats.trackLength / 1000, 1).toLocaleString(),
|
length1 = L.Util.formatNum(stats.trackLength / 1000, 1).toLocaleString(),
|
||||||
length3 = L.Util.formatNum(stats.trackLength / 1000, 3).toLocaleString(undefined, {
|
length3 = L.Util.formatNum(stats.trackLength / 1000, 3).toLocaleString(undefined, {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,11 @@ BR.Xml = {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (singleLineTagList.includes(tag)) {
|
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;
|
let endIndex = match.index + 1;
|
||||||
lines.push(xml.substring(startIndex, endIndex));
|
lines.push(xml.substring(startIndex, endIndex));
|
||||||
|
|
|
||||||
77
js/index.js
77
js/index.js
|
|
@ -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(
|
var reverseRouteButton = L.easyButton(
|
||||||
'fa-random',
|
'fa-random',
|
||||||
function () {
|
function () {
|
||||||
|
|
@ -192,9 +226,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
routingOptions = new BR.RoutingOptions();
|
routingOptions = new BR.RoutingOptions();
|
||||||
routingOptions.on('update', updateRoute);
|
|
||||||
routingOptions.on('update', function (evt) {
|
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', {
|
BR.NogoAreas.MSG_BUTTON = i18next.t('keyboard.generic-shortcut', {
|
||||||
|
|
@ -256,7 +293,7 @@
|
||||||
|
|
||||||
routingPathQuality = new BR.RoutingPathQuality(map, layersControl);
|
routingPathQuality = new BR.RoutingPathQuality(map, layersControl);
|
||||||
|
|
||||||
routing = new BR.Routing({
|
routing = new BR.Routing(profile, {
|
||||||
routing: {
|
routing: {
|
||||||
router: L.bind(router.getRouteSegment, router),
|
router: L.bind(router.getRouteSegment, router),
|
||||||
},
|
},
|
||||||
|
|
@ -267,15 +304,17 @@
|
||||||
|
|
||||||
exportRoute = new BR.Export(router, pois, profile);
|
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();
|
search.clear();
|
||||||
onUpdate(evt && evt.err);
|
onUpdate(evt && evt.err);
|
||||||
});
|
});
|
||||||
map.on('routing:draw-start', function () {
|
map.on('routing:draw-start', function () {
|
||||||
drawButton.state('deactivate-draw');
|
drawButton.state('deactivate-draw');
|
||||||
|
beelineButton.enable();
|
||||||
});
|
});
|
||||||
map.on('routing:draw-end', function () {
|
map.on('routing:draw-end', function () {
|
||||||
drawButton.state('activate-draw');
|
drawButton.state('activate-draw');
|
||||||
|
beelineButton.disable();
|
||||||
});
|
});
|
||||||
|
|
||||||
function onUpdate(err) {
|
function onUpdate(err) {
|
||||||
|
|
@ -330,7 +369,7 @@
|
||||||
circlego.addTo(map);
|
circlego.addTo(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
var buttons = [drawButton, reverseRouteButton, nogos.getButton()];
|
var buttons = [drawButton, beelineButton, reverseRouteButton, nogos.getButton()];
|
||||||
if (circlego) buttons.push(circlego.getButton());
|
if (circlego) buttons.push(circlego.getButton());
|
||||||
buttons.push(deletePointButton, deleteRouteButton);
|
buttons.push(deletePointButton, deleteRouteButton);
|
||||||
|
|
||||||
|
|
@ -363,11 +402,12 @@
|
||||||
// initial option settings (after controls are added and initialized with onAdd)
|
// initial option settings (after controls are added and initialized with onAdd)
|
||||||
router.setOptions(nogos.getOptions());
|
router.setOptions(nogos.getOptions());
|
||||||
router.setOptions(routingOptions.getOptions());
|
router.setOptions(routingOptions.getOptions());
|
||||||
profile.update(routingOptions.getOptions());
|
|
||||||
|
|
||||||
// restore active layers from local storage when called without hash
|
|
||||||
// (check before hash plugin init)
|
// (check before hash plugin init)
|
||||||
if (!location.hash) {
|
if (!location.hash) {
|
||||||
|
profile.update(routingOptions.getOptions());
|
||||||
|
|
||||||
|
// restore active layers from local storage when called without hash
|
||||||
layersControl.loadActiveLayers();
|
layersControl.loadActiveLayers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,13 +431,16 @@
|
||||||
router.setOptions(opts);
|
router.setOptions(opts);
|
||||||
routingOptions.setOptions(opts);
|
routingOptions.setOptions(opts);
|
||||||
nogos.setOptions(opts);
|
nogos.setOptions(opts);
|
||||||
profile.update(opts);
|
|
||||||
|
|
||||||
if (opts.lonlats) {
|
const optsOrDefault = Object.assign({}, routingOptions.getOptions(), opts);
|
||||||
routing.draw(false);
|
profile.update(optsOrDefault, () => {
|
||||||
routing.clear();
|
if (opts.lonlats) {
|
||||||
routing.setWaypoints(opts.lonlats);
|
routing.draw(false);
|
||||||
}
|
routing.clear();
|
||||||
|
routing.setWaypoints(opts.lonlats, opts.beelineFlags);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (opts.pois) {
|
if (opts.pois) {
|
||||||
pois.setMarkers(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
|
// this callback is used to append anything in URL after L.Hash wrote #map=zoom/lat/lng/layer
|
||||||
urlHash.additionalCb = function () {
|
urlHash.additionalCb = function () {
|
||||||
var url = router
|
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);
|
.substr('brouter?'.length + 1);
|
||||||
|
|
||||||
// by default brouter use | as separator. To make URL more human-readable, we remplace them with ; for users
|
// by default brouter use | as separator. To make URL more human-readable, we remplace them with ; for users
|
||||||
|
|
|
||||||
|
|
@ -341,7 +341,7 @@ BR.routeLoader = function (map, layersControl, routing, pois) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (routingPoints.length > 0) {
|
if (routingPoints.length > 0) {
|
||||||
routing.setWaypoints(routingPoints, function (event) {
|
routing.setWaypoints(routingPoints, null, function (event) {
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
var err = event.error;
|
var err = event.error;
|
||||||
BR.message.showError(
|
BR.message.showError(
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,23 @@ BR.Routing = L.Routing.extend({
|
||||||
draw: {
|
draw: {
|
||||||
enable: 68, // char code for 'd'
|
enable: 68, // char code for 'd'
|
||||||
disable: 27, // char code for 'ESC'
|
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'
|
reverse: 82, // char code for 'r'
|
||||||
deleteLastPoint: 90, // char code for 'z'
|
deleteLastPoint: 90, // char code for 'z'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
initialize: function (profile, options) {
|
||||||
|
L.Routing.prototype.initialize.call(this, options);
|
||||||
|
|
||||||
|
this.profile = profile;
|
||||||
|
},
|
||||||
|
|
||||||
onAdd: function (map) {
|
onAdd: function (map) {
|
||||||
this.options.tooltips.waypoint = i18next.t('map.route-tooltip-waypoint');
|
this.options.tooltips.waypoint = i18next.t('map.route-tooltip-waypoint');
|
||||||
this.options.tooltips.segment = i18next.t('map.route-tooltip-segment');
|
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._waypoints.on('layeradd', this._setMarkerOpacity, this);
|
||||||
|
|
||||||
this.on('routing:routeWaypointStart routing:rerouteAllSegmentsStart', function (evt) {
|
// flag if (re-)routing of all segments is ongoing
|
||||||
this._removeDistanceMarkers();
|
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.on(
|
||||||
this._updateDistanceMarkers(evt);
|
'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
|
// turn line mouse marker off while over waypoint marker
|
||||||
this.on(
|
this.on(
|
||||||
|
|
@ -65,7 +99,7 @@ BR.Routing = L.Routing.extend({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._mouseMarker.setOpacity(0.0);
|
this._hideMouseMarker();
|
||||||
this._map.off('mousemove', this._segmentOnMousemove, this);
|
this._map.off('mousemove', this._segmentOnMousemove, this);
|
||||||
this._suspended = true;
|
this._suspended = true;
|
||||||
},
|
},
|
||||||
|
|
@ -141,22 +175,27 @@ BR.Routing = L.Routing.extend({
|
||||||
this._show();
|
this._show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function hide() {
|
var hide = function () {
|
||||||
if (!this._hidden && this._parent._waypoints._first) {
|
if (!this._hidden && this._parent._waypoints._first) {
|
||||||
this._hide();
|
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._draw.on('enabled', function () {
|
||||||
this._map.on('mouseout', hide, this);
|
this._map.on('mouseout', hide, this);
|
||||||
this._map.on('mouseover', show, this);
|
this._map.on('mouseover', show, this);
|
||||||
L.DomEvent.on(this._map._controlContainer, 'mouseout', 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._draw.on('disabled', function () {
|
||||||
this._map.off('mouseout', hide, this);
|
this._map.off('mouseout', hide, this);
|
||||||
this._map.off('mouseover', show, this);
|
this._map.off('mouseover', show, this);
|
||||||
L.DomEvent.off(this._map._controlContainer, 'mouseout', 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.
|
// Call show after deleting last waypoint, but hide trailer.
|
||||||
|
|
@ -175,6 +214,21 @@ BR.Routing = L.Routing.extend({
|
||||||
this._draw
|
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, 'keydown', this._keydownListener, this);
|
||||||
L.DomEvent.addListener(document, 'keyup', this._keyupListener, this);
|
L.DomEvent.addListener(document, 'keyup', this._keyupListener, this);
|
||||||
|
|
||||||
|
|
@ -185,7 +239,9 @@ BR.Routing = L.Routing.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
_addSegmentCasing: function (e) {
|
_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);
|
this._segmentsCasing.addLayer(casing);
|
||||||
e.layer._casing = casing;
|
e.layer._casing = casing;
|
||||||
this._segments.bringToFront();
|
this._segments.bringToFront();
|
||||||
|
|
@ -262,7 +318,7 @@ BR.Routing = L.Routing.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setWaypoints: function (latLngs, cb) {
|
setWaypoints: function (latLngs, beelineFlags, cb) {
|
||||||
var i;
|
var i;
|
||||||
var callbackCount = 0;
|
var callbackCount = 0;
|
||||||
var firstErr;
|
var firstErr;
|
||||||
|
|
@ -291,7 +347,8 @@ BR.Routing = L.Routing.extend({
|
||||||
this._loadingTrailerGroup._map = null;
|
this._loadingTrailerGroup._map = null;
|
||||||
|
|
||||||
for (i = 0; latLngs && i < latLngs.length; i++) {
|
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;
|
this._loadingTrailerGroup._map = this._map;
|
||||||
|
|
@ -374,14 +431,16 @@ BR.Routing = L.Routing.extend({
|
||||||
this.reverse();
|
this.reverse();
|
||||||
} else if (e.keyCode === this.options.shortcut.deleteLastPoint) {
|
} else if (e.keyCode === this.options.shortcut.deleteLastPoint) {
|
||||||
this.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) {
|
_keyupListener: function (e) {
|
||||||
// Prevent Leaflet from triggering drawing a second time on keyup,
|
if (e.keyCode === this.options.shortcut.draw.beelineModifier) {
|
||||||
// since this is already done in _keydownListener
|
this._draw._setTrailerStyle(false);
|
||||||
if (e.keyCode === this.options.shortcut.draw.enable) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -390,10 +449,12 @@ BR.Routing = L.Routing.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
reverse: function () {
|
reverse: function () {
|
||||||
var waypoints = this.getWaypoints();
|
const waypoints = this.getWaypoints();
|
||||||
|
const beelineFlags = this.getBeelineFlags();
|
||||||
waypoints.reverse();
|
waypoints.reverse();
|
||||||
|
beelineFlags.reverse();
|
||||||
this.clear();
|
this.clear();
|
||||||
this.setWaypoints(waypoints);
|
this.setWaypoints(waypoints, beelineFlags);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteLastPoint: function () {
|
deleteLastPoint: function () {
|
||||||
|
|
@ -417,4 +478,166 @@ BR.Routing = L.Routing.extend({
|
||||||
this._map.addLayer(this._distanceMarkers);
|
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;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,7 @@ var HotLineQualityProvider = L.Class.extend({
|
||||||
var flatLines = [];
|
var flatLines = [];
|
||||||
for (var i = 0; segments && i < segments.length; i++) {
|
for (var i = 0; segments && i < segments.length; i++) {
|
||||||
var segment = segments[i];
|
var segment = segments[i];
|
||||||
|
if (segment._routing?.beeline) continue;
|
||||||
var vals = this._computeLatLngVals(segment);
|
var vals = this._computeLatLngVals(segment);
|
||||||
segmentLatLngs.push(vals);
|
segmentLatLngs.push(vals);
|
||||||
Array.prototype.push.apply(flatLines, vals);
|
Array.prototype.push.apply(flatLines, vals);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
var tracksLoaderControl = new TracksLoader();
|
||||||
tracksLoaderControl.addTo(map);
|
tracksLoaderControl.addTo(map);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,15 @@ L.BRouter = L.Class.extend({
|
||||||
L.setOptions(this, options);
|
L.setOptions(this, options);
|
||||||
},
|
},
|
||||||
|
|
||||||
getUrlParams: function (latLngs, pois, circlego, format) {
|
getUrlParams: function (latLngs, beelineFlags, pois, circlego, format) {
|
||||||
params = {};
|
params = {};
|
||||||
if (this._getLonLatsString(latLngs) != null) params.lonlats = this._getLonLatsString(latLngs);
|
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)
|
if (this.options.nogos && this._getNogosString(this.options.nogos).length > 0)
|
||||||
params.nogos = this._getNogosString(this.options.nogos);
|
params.nogos = this._getNogosString(this.options.nogos);
|
||||||
|
|
||||||
|
|
@ -84,6 +89,9 @@ L.BRouter = L.Class.extend({
|
||||||
if (params.lonlats) {
|
if (params.lonlats) {
|
||||||
opts.lonlats = this._parseLonLats(params.lonlats);
|
opts.lonlats = this._parseLonLats(params.lonlats);
|
||||||
}
|
}
|
||||||
|
if (params.straight) {
|
||||||
|
opts.beelineFlags = this._parseBeelines(params.straight, opts.lonlats);
|
||||||
|
}
|
||||||
if (params.nogos) {
|
if (params.nogos) {
|
||||||
opts.nogos = this._parseNogos(params.nogos);
|
opts.nogos = this._parseNogos(params.nogos);
|
||||||
}
|
}
|
||||||
|
|
@ -117,11 +125,12 @@ L.BRouter = L.Class.extend({
|
||||||
return opts;
|
return opts;
|
||||||
},
|
},
|
||||||
|
|
||||||
getUrl: function (latLngs, pois, circlego, format, trackname, exportWaypoints) {
|
getUrl: function (latLngs, beelineFlags, pois, circlego, format, trackname, exportWaypoints) {
|
||||||
var urlParams = this.getUrlParams(latLngs, pois, circlego, format);
|
var urlParams = this.getUrlParams(latLngs, beelineFlags, pois, circlego, format);
|
||||||
var args = [];
|
var args = [];
|
||||||
if (urlParams.lonlats != null && urlParams.lonlats.length > 0)
|
if (urlParams.lonlats != null && urlParams.lonlats.length > 0)
|
||||||
args.push(L.Util.template('lonlats={lonlats}', urlParams));
|
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.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.circlego != null) args.push(L.Util.template('ringgo={circlego}', urlParams));
|
||||||
if (urlParams.nogos != null) args.push(L.Util.template('nogos={nogos}', 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) {
|
getRoute: function (latLngs, cb) {
|
||||||
var url = this.getUrl(latLngs, null, null, 'geojson'),
|
var url = this.getUrl(latLngs, null, null, null, 'geojson'),
|
||||||
xhr = new XMLHttpRequest();
|
xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
|
|
@ -228,7 +237,7 @@ L.BRouter = L.Class.extend({
|
||||||
var segmentLatLng = segmentLatLngs[fi],
|
var segmentLatLng = segmentLatLngs[fi],
|
||||||
featureMessage = featureMessages[mi];
|
featureMessage = featureMessages[mi];
|
||||||
|
|
||||||
segmentLatLng.feature = this._getFeature(featureMessage);
|
segmentLatLng.feature = BR.TrackEdges.getFeature(featureMessage);
|
||||||
segmentLatLng.message = featureMessage;
|
segmentLatLng.message = featureMessage;
|
||||||
|
|
||||||
if (featureLatLng.equals(segmentLatLngs[fi])) {
|
if (featureLatLng.equals(segmentLatLngs[fi])) {
|
||||||
|
|
@ -241,22 +250,6 @@ L.BRouter = L.Class.extend({
|
||||||
return segment;
|
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) {
|
_getFeatureLatLng: function (message) {
|
||||||
var lon = message[0] / 1000000,
|
var lon = message[0] / 1000000,
|
||||||
lat = message[1] / 1000000;
|
lat = message[1] / 1000000;
|
||||||
|
|
@ -305,6 +298,27 @@ L.BRouter = L.Class.extend({
|
||||||
return lonlats;
|
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) {
|
_getLonLatsNameString: function (latLngNames) {
|
||||||
var s = '';
|
var s = '';
|
||||||
for (var i = 0; i < latLngNames.length; i++) {
|
for (var i = 0; i < latLngNames.length; i++) {
|
||||||
|
|
|
||||||
125
js/util/CheapRuler.js
Normal file
125
js/util/CheapRuler.js
Normal 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->meter scale for given latitude
|
||||||
|
*
|
||||||
|
* @return {double[]} [lon->meter,lat->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);
|
||||||
|
};
|
||||||
55
js/util/ClickTolerantBoxZoom.js
Normal file
55
js/util/ClickTolerantBoxZoom.js
Normal 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
16
js/util/LeafletPatches.js
Normal 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
164
js/util/StdPath.js
Normal 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;
|
||||||
|
})();
|
||||||
|
|
@ -32,6 +32,7 @@ BR.Track = {
|
||||||
zIndexOffset: -1000,
|
zIndexOffset: -1000,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
pane: 'tracks',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,24 @@
|
||||||
* @type {L.Class}
|
* @type {L.Class}
|
||||||
*/
|
*/
|
||||||
BR.TrackEdges = L.Class.extend({
|
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
|
* List of indexes for the track array where
|
||||||
* a segment with different features ends
|
* a segment with different features ends
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"ascend": "Ascend",
|
"ascend": "Ascend",
|
||||||
|
"beeline-warning": "Warning: no data for straight lines, values interpolated",
|
||||||
"cost": "Cost",
|
"cost": "Cost",
|
||||||
"distance": "Distance",
|
"distance": "Distance",
|
||||||
"elevation-chart": "Toggle elevation chart",
|
"elevation-chart": "Toggle elevation chart",
|
||||||
|
|
@ -159,11 +160,12 @@
|
||||||
"route-quality-cost": "Cost coding",
|
"route-quality-cost": "Cost coding",
|
||||||
"route-quality-incline": "Incline coding",
|
"route-quality-incline": "Incline coding",
|
||||||
"route-quality-shortcut": "{{action}} ({{key}} key to toggle)",
|
"route-quality-shortcut": "{{action}} ({{key}} key to toggle)",
|
||||||
"route-tooltip-segment": "Drag to create a new waypoint",
|
"route-tooltip-segment": "Drag to create a new waypoint. Click to toggle straight line.",
|
||||||
"route-tooltip-waypoint": "Waypoint. Drag to move; Click to remove.",
|
"route-tooltip-waypoint": "Waypoint. Drag to move; Click to remove.",
|
||||||
"strava-biking": "Show Strava biking segments",
|
"strava-biking": "Show Strava biking segments",
|
||||||
"strava-running": "Show Strava running segments",
|
"strava-running": "Show Strava running segments",
|
||||||
"strava-shortcut": "{{action}}\n({{key}} key to toggle layer, click to reload for current area)",
|
"strava-shortcut": "{{action}}\n({{key}} key to toggle layer, click to reload for current area)",
|
||||||
|
"toggle-beeline": "Toggle straight line",
|
||||||
"zoomInTitle": "Zoom in",
|
"zoomInTitle": "Zoom in",
|
||||||
"zoomOutTitle": "Zoom out"
|
"zoomOutTitle": "Zoom out"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ i18next.t('map.draw-poi-start');
|
||||||
i18next.t('map.draw-poi-stop');
|
i18next.t('map.draw-poi-stop');
|
||||||
i18next.t('map.draw-route-start');
|
i18next.t('map.draw-route-start');
|
||||||
i18next.t('map.draw-route-stop');
|
i18next.t('map.draw-route-stop');
|
||||||
|
i18next.t('map.toggle-beeline');
|
||||||
i18next.t('map.geocoder');
|
i18next.t('map.geocoder');
|
||||||
i18next.t('map.locate-me');
|
i18next.t('map.locate-me');
|
||||||
i18next.t('map.nogo.cancel');
|
i18next.t('map.nogo.cancel');
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
"jquery": "3.5.1",
|
"jquery": "3.5.1",
|
||||||
"jquery-i18next": "^1.2.1",
|
"jquery-i18next": "^1.2.1",
|
||||||
"jstree": "^3.3.8",
|
"jstree": "^3.3.8",
|
||||||
"leaflet": "~1.6.0",
|
"leaflet": "~1.7.1",
|
||||||
"leaflet-control-geocoder": "^2.2.0",
|
"leaflet-control-geocoder": "^2.2.0",
|
||||||
"leaflet-easybutton": "*",
|
"leaflet-easybutton": "*",
|
||||||
"leaflet-editable": "^1.1.0",
|
"leaflet-editable": "^1.1.0",
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
"leaflet-osm-notes": "osmlab/leaflet-osm-notes#af2aa811",
|
"leaflet-osm-notes": "osmlab/leaflet-osm-notes#af2aa811",
|
||||||
"leaflet-plugins": "~3.0.0",
|
"leaflet-plugins": "~3.0.0",
|
||||||
"leaflet-providers": "^1.10.2",
|
"leaflet-providers": "^1.10.2",
|
||||||
"leaflet-routing": "nrenner/leaflet-routing#e94e153",
|
"leaflet-routing": "nrenner/leaflet-routing#773314a",
|
||||||
"leaflet-sidebar-v2": "nrenner/leaflet-sidebar-v2#dev",
|
"leaflet-sidebar-v2": "nrenner/leaflet-sidebar-v2#dev",
|
||||||
"leaflet-triangle-marker": "^1.0.2",
|
"leaflet-triangle-marker": "^1.0.2",
|
||||||
"leaflet.heightgraph": "nrenner/Leaflet.Heightgraph#0757b2a",
|
"leaflet.heightgraph": "nrenner/Leaflet.Heightgraph#0757b2a",
|
||||||
|
|
@ -117,7 +117,7 @@
|
||||||
"gulp-util": "^3.0.7",
|
"gulp-util": "^3.0.7",
|
||||||
"gulp-zip": "^5.0.2",
|
"gulp-zip": "^5.0.2",
|
||||||
"husky": "^4.3.4",
|
"husky": "^4.3.4",
|
||||||
"i18next-scanner": "^3.0.0",
|
"i18next-scanner": "^3.1.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"marked": "^4.0.10",
|
"marked": "^4.0.10",
|
||||||
"merge-stream": "^2.0.0",
|
"merge-stream": "^2.0.0",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ require('../../js/format/Gpx.js');
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// lonlats=8.467712,49.488117;8.470598,49.488849 + turnInstructionMode = 4 (comment-style)
|
||||||
const geoJson = require('./data/track.json');
|
const geoJson = require('./data/track.json');
|
||||||
// lonlats=8.467712,49.488117;8.469354,49.488394;8.470556,49.488946;8.469982,49.489176 + turnInstructionMode = 5
|
// lonlats=8.467712,49.488117;8.469354,49.488394;8.470556,49.488946;8.469982,49.489176 + turnInstructionMode = 5
|
||||||
// console log in Export._formatTrack
|
// console log in Export._formatTrack
|
||||||
|
|
@ -64,6 +65,8 @@ describe('voice hints', () => {
|
||||||
/:(rteTime|rteSpeed)>([\d.]*)<\//g,
|
/:(rteTime|rteSpeed)>([\d.]*)<\//g,
|
||||||
(match, p1, p2) => `:${p1}>${(+p2).toFixed(3)}</`
|
(match, p1, p2) => `:${p1}>${(+p2).toFixed(3)}</`
|
||||||
);
|
);
|
||||||
|
// ignore off by one due to times passed with 3 decimals
|
||||||
|
brouterGpx = brouterGpx.replace('rteSpeed>9.361<', 'rteSpeed>9.360<');
|
||||||
|
|
||||||
const gpx = BR.Gpx.format(geoJson, 2);
|
const gpx = BR.Gpx.format(geoJson, 2);
|
||||||
expect(gpx).toEqual(brouterGpx);
|
expect(gpx).toEqual(brouterGpx);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
{
|
{
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"properties": {
|
"properties": {
|
||||||
"creator": "BRouter-1.1",
|
"creator": "BRouter-1.6.3",
|
||||||
"name": "Track",
|
"name": "Track",
|
||||||
"track-length": "319",
|
"track-length": "319",
|
||||||
"filtered ascend": "2",
|
"filtered ascend": "2",
|
||||||
|
|
@ -17,11 +17,11 @@
|
||||||
[5,2,0,90.0,-90," 6(-89)6 (0)6 (89)6"]
|
[5,2,0,90.0,-90," 6(-89)6 (0)6 (89)6"]
|
||||||
],
|
],
|
||||||
"messages": [
|
"messages": [
|
||||||
["Longitude", "Latitude", "Elevation", "Distance", "CostPerKm", "ElevCost", "TurnCost", "NodeCost", "InitialCost", "WayTags", "NodeTags"],
|
["Longitude", "Latitude", "Elevation", "Distance", "CostPerKm", "ElevCost", "TurnCost", "NodeCost", "InitialCost", "WayTags", "NodeTags", "Time", "Energy"],
|
||||||
["8468340", "49488794", "101", "89", "1000", "0", "0", "0", "0", "highway=residential surface=asphalt cycleway=lane oneway=yes lcn=yes smoothness=good route_bicycle_icn=yes route_bicycle_ncn=yes route_bicycle_rcn=yes", ""],
|
["8468340", "49488794", "101", "89", "1000", "0", "0", "0", "0", "highway=residential surface=asphalt cycleway=lane oneway=yes lcn=yes smoothness=good route_bicycle_icn=yes route_bicycle_ncn=yes route_bicycle_rcn=yes", "", "9", "959"],
|
||||||
["8470671", "49488909", "99", "230", "1150", "0", "180", "0", "0", "highway=residential surface=asphalt oneway=yes smoothness=good", ""]
|
["8470671", "49488909", "99", "230", "1150", "0", "180", "0", "0", "highway=residential surface=asphalt oneway=yes smoothness=good", "", "44", "4412"]
|
||||||
],
|
],
|
||||||
"times": [0.0,9.592433,12.270765,14.129882,19.406338,34.50238,44.117233]
|
"times": [0,9.592,12.271,14.13,19.406,34.502,44.117]
|
||||||
},
|
},
|
||||||
"geometry": {
|
"geometry": {
|
||||||
"type": "LineString",
|
"type": "LineString",
|
||||||
|
|
|
||||||
38
tests/util/CheapRuler.test.js
Normal file
38
tests/util/CheapRuler.test.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
require('../../js/util/CheapRuler.js');
|
||||||
|
const geoJson = require('../format/data/track.json');
|
||||||
|
|
||||||
|
test('distance', () => {
|
||||||
|
// https://github.com/abrensch/brouter/issues/3#issuecomment-440375918
|
||||||
|
const lngLat1 = [2.3158, 48.8124];
|
||||||
|
const lngLat2 = [2.321, 48.8204];
|
||||||
|
|
||||||
|
const [ilon1, ilat1] = btools.util.CheapRuler.toIntegerLngLat(lngLat1);
|
||||||
|
const [ilon2, ilat2] = btools.util.CheapRuler.toIntegerLngLat(lngLat2);
|
||||||
|
|
||||||
|
const distance = btools.util.CheapRuler.distance(ilon1, ilat1, ilon2, ilat2);
|
||||||
|
|
||||||
|
// 968.1670119067338 - issue #3 (App.java)
|
||||||
|
// 968.0593622374572 - CheapRuler.java
|
||||||
|
expect(distance).toBeCloseTo(968.0593622374572);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('total distance', () => {
|
||||||
|
const coordinates = geoJson.features[0].geometry.coordinates;
|
||||||
|
const properties = geoJson.features[0].properties;
|
||||||
|
let totalDistance = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < coordinates.length; i++) {
|
||||||
|
if (i === 0) continue;
|
||||||
|
|
||||||
|
const coord1 = coordinates[i - 1];
|
||||||
|
const coord2 = coordinates[i];
|
||||||
|
|
||||||
|
const [ilon1, ilat1] = btools.util.CheapRuler.toIntegerLngLat(coord1);
|
||||||
|
const [ilon2, ilat2] = btools.util.CheapRuler.toIntegerLngLat(coord2);
|
||||||
|
|
||||||
|
const distance = btools.util.CheapRuler.calcDistance(ilon1, ilat1, ilon2, ilat2);
|
||||||
|
totalDistance += distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(Math.round(totalDistance)).toEqual(+properties['track-length']);
|
||||||
|
});
|
||||||
36
tests/util/StdPath.test.js
Normal file
36
tests/util/StdPath.test.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
BR = {};
|
||||||
|
require('../../js/util/CheapRuler.js');
|
||||||
|
require('../../js/util/StdPath.js');
|
||||||
|
|
||||||
|
const geoJson = require('../format/data/track.json');
|
||||||
|
|
||||||
|
test('simple track', () => {
|
||||||
|
const coordinates = geoJson.features[0].geometry.coordinates;
|
||||||
|
const properties = geoJson.features[0].properties;
|
||||||
|
const dummyProfileVars = {
|
||||||
|
getProfileVar(name) {
|
||||||
|
const vars = { validForBikes: 1 };
|
||||||
|
return vars[name];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const rc = new BR.RoutingContext(dummyProfileVars);
|
||||||
|
const stdPath = new BR.StdPath();
|
||||||
|
|
||||||
|
for (let i = 0; i < coordinates.length; i++) {
|
||||||
|
if (i === 0) continue;
|
||||||
|
|
||||||
|
const coord1 = coordinates[i - 1];
|
||||||
|
const coord2 = coordinates[i];
|
||||||
|
|
||||||
|
const [ilon1, ilat1] = btools.util.CheapRuler.toIntegerLngLat(coord1);
|
||||||
|
const [ilon2, ilat2] = btools.util.CheapRuler.toIntegerLngLat(coord2);
|
||||||
|
|
||||||
|
const distance = btools.util.CheapRuler.calcDistance(ilon1, ilat1, ilon2, ilat2);
|
||||||
|
const deltaHeight = coord2[2] - coord1[2];
|
||||||
|
|
||||||
|
stdPath.computeKinematic(rc, distance, deltaHeight, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(Math.round(stdPath.getTotalEnergy())).toEqual(+properties['total-energy']);
|
||||||
|
expect(Math.round(stdPath.getTotalTime())).toEqual(+properties['total-time']);
|
||||||
|
});
|
||||||
25
yarn.lock
25
yarn.lock
|
|
@ -5106,6 +5106,11 @@ esprima-fb@3001.1.0-dev-harmony-fb:
|
||||||
resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz#b77d37abcd38ea0b77426bb8bc2922ce6b426411"
|
resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz#b77d37abcd38ea0b77426bb8bc2922ce6b426411"
|
||||||
integrity sha1-t303q8046gt3Qmu4vCkizmtCZBE=
|
integrity sha1-t303q8046gt3Qmu4vCkizmtCZBE=
|
||||||
|
|
||||||
|
esprima-next@^5.7.0:
|
||||||
|
version "5.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/esprima-next/-/esprima-next-5.8.1.tgz#e670c9e807dce91075160d7cd7735c4b74581338"
|
||||||
|
integrity sha512-jPuleZ9j065A9xGKreFh9YSgPlbL9/miG/l4KslkwEb7Ilwl5Ct7BmDkSTHA0rW0qnqLx+hsZWIB66s1XaMAyA==
|
||||||
|
|
||||||
esprima@^4.0.0, esprima@^4.0.1:
|
esprima@^4.0.0, esprima@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||||
|
|
@ -6487,10 +6492,10 @@ i18next-browser-languagedetector@^6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.5.5"
|
"@babel/runtime" "^7.5.5"
|
||||||
|
|
||||||
i18next-scanner@^3.0.0:
|
i18next-scanner@^3.1.0:
|
||||||
version "3.0.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/i18next-scanner/-/i18next-scanner-3.0.0.tgz#16024fa7f6dc5fd73d91545bd01566f86a76529a"
|
resolved "https://registry.yarnpkg.com/i18next-scanner/-/i18next-scanner-3.1.0.tgz#35d00d945637c1a2b90124b0fd327040ac197598"
|
||||||
integrity sha512-cm4Ch3VqicGZS8y+4xSvXoOsnE/iWhHZi6AZEyAgLLm3EDZ/eY21gDbLfbnwKVY6wCghzAEO9LfRNlxwTo8KMQ==
|
integrity sha512-dHLXUJIiF1CYJNslCkJFDYJySk5fg+dzdg9O73XXqHcdZwJ2947SWusqq8HdNFB7LpkBi8oTG6TWLZPmqbAh8Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn "^8.0.4"
|
acorn "^8.0.4"
|
||||||
acorn-dynamic-import "^4.0.0"
|
acorn-dynamic-import "^4.0.0"
|
||||||
|
|
@ -6503,7 +6508,7 @@ i18next-scanner@^3.0.0:
|
||||||
deepmerge "^4.0.0"
|
deepmerge "^4.0.0"
|
||||||
ensure-array "^1.0.0"
|
ensure-array "^1.0.0"
|
||||||
eol "^0.9.1"
|
eol "^0.9.1"
|
||||||
esprima "^4.0.0"
|
esprima-next "^5.7.0"
|
||||||
gulp-sort "^2.0.0"
|
gulp-sort "^2.0.0"
|
||||||
i18next "*"
|
i18next "*"
|
||||||
lodash "^4.0.0"
|
lodash "^4.0.0"
|
||||||
|
|
@ -7776,9 +7781,9 @@ leaflet-providers@^1.10.2:
|
||||||
resolved "https://registry.yarnpkg.com/leaflet-providers/-/leaflet-providers-1.10.2.tgz#763c8e6655f26caf1afe3a1ef4add6c3e32de663"
|
resolved "https://registry.yarnpkg.com/leaflet-providers/-/leaflet-providers-1.10.2.tgz#763c8e6655f26caf1afe3a1ef4add6c3e32de663"
|
||||||
integrity sha512-1l867LObxwuFBeyPeBewip8PAXKOnvEoujq4/9y2TKTiZNHH76ksBD6dfktGjgUrOF+IdjsGHkpASPE+v2DQLw==
|
integrity sha512-1l867LObxwuFBeyPeBewip8PAXKOnvEoujq4/9y2TKTiZNHH76ksBD6dfktGjgUrOF+IdjsGHkpASPE+v2DQLw==
|
||||||
|
|
||||||
leaflet-routing@nrenner/leaflet-routing#e94e153:
|
leaflet-routing@nrenner/leaflet-routing#773314a:
|
||||||
version "0.1.3"
|
version "0.1.4-beta"
|
||||||
resolved "https://codeload.github.com/nrenner/leaflet-routing/tar.gz/e94e153b7574510313cb0bfefcd8776edebf627e"
|
resolved "https://codeload.github.com/nrenner/leaflet-routing/tar.gz/773314a37940b32b2fec84886611ecbd4d6f3df9"
|
||||||
|
|
||||||
leaflet-sidebar-v2@nrenner/leaflet-sidebar-v2#dev:
|
leaflet-sidebar-v2@nrenner/leaflet-sidebar-v2#dev:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
|
|
@ -7831,12 +7836,12 @@ leaflet@^1.0.1, leaflet@^1.3.4:
|
||||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.4.0.tgz#d5f56eeb2aa32787c24011e8be4c77e362ae171b"
|
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.4.0.tgz#d5f56eeb2aa32787c24011e8be4c77e362ae171b"
|
||||||
integrity sha512-x9j9tGY1+PDLN9pcWTx9/y6C5nezoTMB8BLK5jTakx+H7bPlnbCHfi9Hjg+Qt36sgDz/cb9lrSpNQXmk45Tvhw==
|
integrity sha512-x9j9tGY1+PDLN9pcWTx9/y6C5nezoTMB8BLK5jTakx+H7bPlnbCHfi9Hjg+Qt36sgDz/cb9lrSpNQXmk45Tvhw==
|
||||||
|
|
||||||
leaflet@^1.5.0:
|
leaflet@^1.5.0, leaflet@~1.7.1:
|
||||||
version "1.7.1"
|
version "1.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19"
|
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19"
|
||||||
integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw==
|
integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw==
|
||||||
|
|
||||||
leaflet@^1.6.0, leaflet@~1.6.0:
|
leaflet@^1.6.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308"
|
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308"
|
||||||
integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ==
|
integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ==
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue