Merge pull request #399 from nrenner/68-sl-formatting

Client-side track formatting
This commit is contained in:
Norbert Renner 2021-04-10 13:13:34 +02:00 committed by GitHub
commit 06f1c77774
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 3686 additions and 83 deletions

View file

@ -12,3 +12,6 @@ layers/
locales/*.json
resources/boundaries/
resources/standalone/*.sh
profiles2/
tests/**/*.json
tests/**/*.gpx

View file

@ -1,3 +1,8 @@
{
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "ms-azuretools.vscode-docker"]
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"ms-azuretools.vscode-docker",
"msjsdiag.debugger-for-chrome"
]
}

24
.vscode/launch.json vendored
View file

@ -5,11 +5,33 @@
"version": "0.2.0",
"configurations": [
{
"name": "Launch Chrome against localhost",
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
},
{
"name": "Debug Chromium (Snap) localhost",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}",
"runtimeExecutable": "/snap/bin/chromium",
"runtimeArgs": ["--new-window", "--remote-debugging-port=9222", "--disable-background-networking"],
"sourceMaps": true,
"sourceMapPathOverrides": {
"*": "${webRoot}/*"
}
},
{
"name": "Debug Jest tests",
"type": "node",
"request": "launch",
"runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
}
]
}

View file

@ -130,6 +130,10 @@ Copyright (c) 2018 Norbert Renner and [contributors](https://github.com/nrenner/
Copyright (c) 2014-2021 Denis Pushkarev; [MIT License](https://github.com/zloirock/core-js/blob/master/LICENSE)
- [regenerator-runtime](https://github.com/facebook/regenerator/tree/master/packages/regenerator-runtime)
Copyright (c) 2014-present, Facebook, Inc.; [MIT License](https://github.com/facebook/regenerator/blob/master/packages/regenerator-runtime/LICENSE)
- [tokml](https://github.com/mapbox/tokml)
Copyright (c) 2015, Mapbox All rights reserved; [BSD-2-Clause License](https://github.com/mapbox/tokml/blob/master/LICENSE.md)
- [Jest](https://github.com/facebook/jest)
Copyright (c) Facebook, Inc. and its affiliates; [MIT License](https://github.com/facebook/jest/blob/master/LICENSE)
- [overpass-layer](https://github.com/plepe/overpass-layer)
Copyright (c) 2020 Stephan Bösch-Plepelits; [MIT License](https://github.com/plepe/overpass-layer/blob/master/LICENSE)
- [maki](https://github.com/mapbox/maki)

View file

@ -71,6 +71,7 @@ var paths = {
'js/LayersConfig.js',
'js/router/BRouter.js',
'js/util/*.js',
'js/format/*.js',
'js/plugin/*.js',
'js/control/*.js',
'js/index.js',
@ -285,14 +286,11 @@ gulp.task('bump:json', function () {
});
gulp.task('bump:html', function () {
const version = nextVersion || pkg.version;
return gulp
.src('./index.html')
.pipe(
replace(
/<sup class="version">(.*)<\/sup>/,
'<sup class="version">' + (nextVersion || pkg.version) + '</sup>'
)
)
.pipe(replace(/<sup class="version">(.*)<\/sup>/, '<sup class="version">' + version + '</sup>'))
.pipe(replace(/BR.version = '(.*?)';/, "BR.version = '" + version + "';"))
.pipe(gulp.dest('.'));
});

View file

@ -1227,6 +1227,7 @@
<script>
// global package prefix for BRouter web application
BR = {};
BR.version = '0.16.0';
console.log(
'\r\n###\r\n### BRouter-Web\r\n###\r\n### Please note that the routing API used here is not public!\r\n###\r\n'

View file

@ -22,5 +22,6 @@
touchScreen: touchScreen,
touchScreenDetectable: touchScreenDetectable,
touch: touch,
download: 'Blob' in window && 'createObjectURL' in URL && 'download' in document.createElement('a'),
};
})();

View file

@ -7,14 +7,16 @@ BR.Export = L.Class.extend({
},
},
initialize: function (router, pois) {
initialize: function (router, pois, profile) {
this.router = router;
this.pois = pois;
this.profile = profile;
this.exportButton = $('#exportButton');
var trackname = (this.trackname = document.getElementById('trackname'));
this.tracknameAllowedChars = BR.conf.tracknameAllowedChars;
if (this.tracknameAllowedChars) {
// a.download attribute automatically replaces invalid characters
if (!BR.Browser.download && this.tracknameAllowedChars) {
this.tracknameMessage = document.getElementById('trackname-message');
var patternRegex = new RegExp('[' + this.tracknameAllowedChars + ']+');
@ -31,8 +33,9 @@ BR.Export = L.Class.extend({
this.update([]);
},
update: function (latLngs) {
update: function (latLngs, segments) {
this.latLngs = latLngs;
this.segments = segments;
if (latLngs.length < 2) {
this.exportButton.addClass('disabled');
@ -44,18 +47,92 @@ BR.Export = L.Class.extend({
_export: function (e) {
var exportForm = document.forms['export'];
var format = exportForm['format'].value || $('#export-format input:radio:checked').val();
var name = encodeURIComponent(exportForm['trackname'].value);
var name = exportForm['trackname'].value;
var nameUri = encodeURIComponent(name);
var includeWaypoints = exportForm['include-waypoints'].checked;
var uri = this.router.getUrl(this.latLngs, this.pois.getMarkers(), null, format, name, includeWaypoints);
e.preventDefault();
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');
link.href = uri;
link.dispatchEvent(evt);
if (BR.Browser.download) {
const track = this._formatTrack(format, name, includeWaypoints);
const mimeTypeMap = {
gpx: 'application/gpx+xml',
kml: 'application/vnd.google-earth.kml+xml',
geojson: 'application/vnd.geo+json',
csv: 'text/tab-separated-values',
};
const mimeType = mimeTypeMap[format];
const blob = new Blob([track], {
type: mimeType + ';charset=utf-8',
});
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = objectUrl;
link.download = (name || 'brouter') + '.' + format;
link.click();
} else {
var uri = this.router.getUrl(this.latLngs, 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');
link.href = uri;
link.dispatchEvent(evt);
}
},
_formatTrack: function (format, name, includeWaypoints) {
const track = BR.Export._concatTotalTrack(this.segments);
if (name) {
track.features[0].properties.name = name;
}
this._addPois(track);
if (includeWaypoints) {
this._addRouteWaypoints(track);
}
switch (format) {
case 'gpx':
const turnInstructionMode = +this.profile.getProfileVar('turnInstructionMode');
const transportMode = this.profile.getTransportMode();
return BR.Gpx.format(track, turnInstructionMode, transportMode);
case 'kml':
return BR.Kml.format(track);
case 'geojson':
return JSON.stringify(track, null, 2);
case 'csv':
return BR.Csv.format(track);
default:
break;
}
console.error('Export format not implemented: ' + format);
},
_addPois: function (track) {
const markers = this.pois.getMarkers();
for (const poi of markers) {
const properties = { name: poi.name, type: 'poi' };
const point = turf.point([poi.latlng.lng, poi.latlng.lat], properties);
track.features.push(point);
}
},
_addRouteWaypoints: function (track) {
for (const [i, latLng] of this.latLngs.entries()) {
let name = 'via' + i;
let type = 'via';
if (i === 0) {
name = 'from';
type = 'from';
} else if (i === this.latLngs.length - 1) {
name = 'to';
type = 'to';
}
const properties = { name, type };
const point = turf.point([latLng.lng, latLng.lat], properties);
track.features.push(point);
}
},
_validationMessage: function () {
@ -72,7 +149,7 @@ BR.Export = L.Class.extend({
},
_selectTrackname: function () {
trackname.setSelectionRange(0, trackname.value.lastIndexOf(' - '));
trackname.setSelectionRange(0, trackname.value.lastIndexOf(BR.Browser.download ? ' (' : ' - '));
},
_generateTrackname: function () {
@ -84,7 +161,7 @@ BR.Export = L.Class.extend({
this.latLngs[this.latLngs.length - 1],
L.bind(function (to) {
var distance = document.getElementById('distance').innerHTML;
if (this.tracknameAllowedChars) {
if (!BR.Browser.download && this.tracknameAllowedChars) {
distance = distance.replace(',', '.'); // temp. fix (#202)
}
if (!from || !to) {
@ -102,7 +179,7 @@ BR.Export = L.Class.extend({
});
}
if (this.tracknameAllowedChars) {
if (!BR.Browser.download && this.tracknameAllowedChars) {
// temp. fix: replace and remove characters that will get removed by server quick fix (#194)
trackname.value = trackname.value.replace(/[>)]/g, '').replace(/ \(/g, ' - ');
this._validationMessage();
@ -149,3 +226,69 @@ BR.Export = L.Class.extend({
BR.export = function () {
return new BR.Export();
};
BR.Export._concatTotalTrack = function (segments) {
const sumProperties = (p, fp, keys) => {
for (const key of keys) {
p[key] = (+p[key] + +fp[key]).toString();
}
};
let coordinates = [];
let properties;
for (const [segmentIndex, segment] of segments.entries()) {
const feature = segment.feature;
if (!feature) continue;
const coordOffset = coordinates.length > 0 ? coordinates.length - 1 : 0;
if (properties) {
const p = properties;
const fp = feature.properties;
sumProperties(p, fp, [
'cost',
'filtered ascend',
'plain-ascend',
'total-energy',
'total-time',
'track-length',
]);
p.messages = p.messages.concat(fp.messages.slice(1));
if (p.times && fp.times) {
const lastTime = p.times[p.times.length - 1];
for (const [timeIndex, time] of fp.times.entries()) {
if (timeIndex > 0) {
p.times.push(+(lastTime + time).toFixed(3));
}
}
}
if (fp.voicehints) {
if (!p.voicehints) p.voicehints = [];
for (const fpHint of fp.voicehints) {
const hint = fpHint.slice();
hint[0] += coordOffset;
p.voicehints.push(hint);
}
}
} else {
// clone
properties = Object.assign({}, feature.properties);
if (properties.voicehints) {
properties.voicehints = properties.voicehints.slice();
}
if (properties.times) {
properties.times = properties.times.slice();
}
}
let featureCoordinates = feature.geometry.coordinates;
if (segmentIndex > 0) {
// remove first segment coordinate, same as previous last
featureCoordinates = featureCoordinates.slice(1);
}
coordinates = coordinates.concat(featureCoordinates);
}
return turf.featureCollection([turf.lineString(coordinates, properties)]);
};

View file

@ -90,9 +90,38 @@ BR.Profile = L.Evented.extend({
this.editor.refresh();
},
// Returns the initial value of the given profile variable as String, as defined by the assign statement.
// Intended for all assigned variables, not just parameters with a comment declaration, i.e. no type information used.
getProfileVar: function (name) {
let value;
if (this._isParamsFormActive()) {
const formValues = this._getFormValues();
if (formValues.hasOwnProperty(name)) {
return formValues[name];
}
}
const profileText = this._getProfileText();
if (!profileText) return value;
const regex = new RegExp(`assign\\s*${name}\\s*=?\\s*([\\w\\.]*)`);
const match = profileText.match(regex);
if (match) {
value = match[1];
}
return value;
},
// Returns car|bike|foot, default is foot
getTransportMode: function () {
const isCar = !!this.getProfileVar('validForCars');
const isBike = !!this.getProfileVar('validForBikes');
return isCar ? 'car' : isBike ? 'bike' : 'foot';
},
_upload: function (evt) {
var button = evt.target || evt.srcElement,
profile = this.editor.getValue();
profile = this._getProfileText();
this.message.hide();
evt.preventDefault();
@ -115,15 +144,9 @@ BR.Profile = L.Evented.extend({
},
_buildCustomProfile: function (profileText) {
document.querySelectorAll('#profile_params input, #profile_params select').forEach(function (input) {
var name = input.name;
var value;
if (input.type == 'checkbox') {
value = input.checked;
} else {
value = input.value;
}
const formValues = this._getFormValues();
Object.keys(formValues).forEach((name) => {
const value = formValues[name];
var re = new RegExp(
'(assign\\s*' +
name +
@ -136,8 +159,23 @@ BR.Profile = L.Evented.extend({
return profileText;
},
_getFormValues: function () {
const obj = {};
document.querySelectorAll('#profile_params input, #profile_params select').forEach((input) => {
const name = input.name;
let value;
if (input.type == 'checkbox') {
value = input.checked;
} else {
value = input.value;
}
obj[name] = value;
});
return obj;
},
_save: function (evt) {
var profileText = this._buildCustomProfile(this.editor.getValue());
var profileText = this._buildCustomProfile(this._getProfileText());
var that = this;
this.fire('update', {
profileText: profileText,
@ -313,7 +351,7 @@ BR.Profile = L.Evented.extend({
},
_activateSecondaryTab: function () {
var profileText = this.editor.getValue();
var profileText = this._getProfileText();
if (this._isParamsFormActive()) {
this._buildParamsForm(profileText);
@ -321,4 +359,8 @@ BR.Profile = L.Evented.extend({
this._setValue(this._buildCustomProfile(profileText));
}
},
_getProfileText: function () {
return this.editor.getValue();
},
});

14
js/format/Csv.js Normal file
View file

@ -0,0 +1,14 @@
BR.Csv = {
format: function (geoJson) {
const separator = '\t';
const newline = '\n';
const messages = geoJson.features[0].properties.messages;
let csv = '';
for (const entries of messages) {
csv += entries.join(separator) + newline;
}
return csv;
},
};

98
js/format/Gpx.js Normal file
View file

@ -0,0 +1,98 @@
// derived from BRouter btools.router.OsmTrack.formatAsGpx
BR.Gpx = {
format: function (geoJson, turnInstructionMode = 0, transportMode = 'bike') {
if (!geoJson?.features) return '';
class GpxTransform {
constructor(voiceHintsTransform) {
this.voiceHintsTransform = voiceHintsTransform;
this.comment = voiceHintsTransform?.comment || '';
if (this.voiceHintsTransform) {
Object.keys(this.voiceHintsTransform).forEach((member) => {
if (!GpxTransform.prototype.hasOwnProperty(member)) {
this[member] = this.voiceHintsTransform[member];
}
});
}
}
wpt(wpt, feature, coord, index) {
// not in use right now, just to be safe in case of future overrides
wpt = (voiceHintsTransform?.wpt && voiceHintsTransform.wpt(wpt, feature, coord, index)) || wpt;
if (feature.properties.name) {
wpt.name = feature.properties.name;
}
const type = feature.properties.type;
if (type && type !== 'poi') {
wpt.type = type;
}
return wpt;
}
trk(trk, feature, coordsList) {
trk = (voiceHintsTransform?.trk && voiceHintsTransform.trk(trk, feature, coordsList)) || trk;
// name as first tag, by using assign and in this order
return Object.assign(
{
name: feature.properties.name,
},
trk
);
}
}
let voiceHintsTransform;
if (turnInstructionMode > 1) {
const voiceHints = BR.voiceHints(geoJson, turnInstructionMode, transportMode);
voiceHintsTransform = voiceHints.getGpxTransform();
}
const gpxTransform = new GpxTransform(voiceHintsTransform);
let gpx = togpx(geoJson, {
creator: 'BRouter-Web ' + BR.version,
featureTitle: function () {},
featureDescription: function () {},
featureCoordTimes: function () {},
transform: gpxTransform,
});
const statsComment = BR.Gpx._statsComment(geoJson);
gpx = '<?xml version="1.0" encoding="UTF-8"?>' + statsComment + gpxTransform.comment + gpx;
gpx = BR.Xml.pretty(gpx);
return gpx;
},
// <!-- track-length = 319 filtered ascend = 2 plain-ascend = -1 cost=533 energy=.0kwh time=44s -->
_statsComment: function (geoJson) {
const props = geoJson.features?.[0].properties;
if (!props) return '';
let comment = '<!--';
comment += ' track-length = ' + props['track-length'];
comment += ' filtered ascend = ' + props['filtered ascend'];
comment += ' plain-ascend = ' + props['plain-ascend'];
comment += ' cost=' + props['cost'];
if (props['total-energy']) {
// see brouter OsmTrack.getFormattedEnergy
comment += ' energy=' + (props['total-energy'] / 3600000).toFixed(1) + 'kwh';
}
if (props['total-time']) {
comment += ' time=' + BR.Gpx.formatTime(props['total-time']);
}
comment += ' -->';
return comment;
},
// 14833 -> 4h 7m 13s
// see BRouter OsmTrack.getFormattedTime2
formatTime(seconds) {
const hours = Math.trunc(seconds / 3600);
const minutes = Math.trunc((seconds - hours * 3600) / 60);
seconds = seconds - hours * 3600 - minutes * 60;
let time = '';
if (hours != 0) time += hours + 'h ';
if (minutes != 0) time += minutes + 'm ';
if (seconds != 0) time += seconds + 's';
return time;
},
};

7
js/format/Kml.js Normal file
View file

@ -0,0 +1,7 @@
BR.Kml = {
format: function (geoJson) {
// don't export properties as <ExtendedData>, probably no need for it
geoJson.features[0].properties = { name: geoJson.features[0].properties.name };
return BR.Xml.pretty(tokml(geoJson));
},
};

370
js/format/VoiceHints.js Normal file
View file

@ -0,0 +1,370 @@
(function () {
class Command {
constructor(name, locus, orux, symbol, message) {
this.name = name;
this.locus = locus;
this.orux = orux;
this.symbol = symbol;
this.message = message;
}
}
class RoundaboutCommand extends Command {
constructor(command, exitNumber) {
super(
command.name + exitNumber,
command.locus + exitNumber,
command.orux + exitNumber,
command.symbol + exitNumber,
command.message + exitNumber
);
}
}
class RoundaboutLeftCommand extends Command {
constructor(command, exitNumber) {
super(
command.name + -exitNumber,
command.locus + -exitNumber,
command.orux + exitNumber,
command.symbol + -exitNumber,
command.message + -exitNumber
);
}
}
class VoiceHints {
constructor(geoJson, turnInstructionMode, transportMode) {
this.geoJson = geoJson;
this.turnInstructionMode = turnInstructionMode;
this.transportMode = transportMode;
this.track = geoJson.features?.[0];
this.voicehints = this.track?.properties?.voicehints;
}
getGpxTransform() {
const transform = {
comment: '',
trk: function (trk, feature, coordsList) {
const properties = this._getTrk();
return Object.assign(properties, trk);
}.bind(this),
};
this._addToTransform(transform);
return transform;
}
_getDuration(voicehintsIndex) {
const times = this.track.properties.times;
if (!times) return 0;
const indexInTrack = this.voicehints[voicehintsIndex][0];
const currentTime = times[indexInTrack];
const len = this.voicehints.length;
const nextIndex = voicehintsIndex < len - 1 ? this.voicehints[voicehintsIndex + 1][0] : times.length - 1;
const nextTime = times[nextIndex];
return nextTime - currentTime;
}
_loopHints(hintCallback) {
if (!this.voicehints) return;
for (const [i, values] of this.voicehints.entries()) {
const [indexInTrack, commandId, exitNumber, distance, angle, geometry] = values;
const hint = { indexInTrack, commandId, exitNumber, distance, angle, geometry };
hint.time = this._getDuration(i);
if (hint.time > 0) {
hint.speed = distance / hint.time;
}
const coord = this.track.geometry.coordinates[indexInTrack];
const cmd = this.getCommand(commandId, exitNumber);
if (!cmd) {
console.error(`no voicehint command for id: ${commandId} (${values})`);
continue;
}
hintCallback(hint, cmd, coord);
}
}
getCommand(id, exitNumber) {
let command = VoiceHints.commands[id];
if (id === 13) {
command = new RoundaboutCommand(command, exitNumber);
} else if (id === 14) {
command = new RoundaboutLeftCommand(command, exitNumber);
}
return command;
}
// override in subclass
_addToTransform(transform) {}
// override in subclass
_getTrk() {
return {};
}
}
// from BRouter btools.router.VoiceHint
VoiceHints.commands = (function () {
return {
1: new Command('C', 1, 1002, 'Straight', 'straight'),
2: new Command('TL', 4, 1000, 'Left', 'left'),
3: new Command('TSLL', 3, 1017, 'TSLL', 'slight left'),
4: new Command('TSHL', 5, 1019, 'TSHL', 'sharp left'),
5: new Command('TR', 7, 1001, 'Right', 'right'),
6: new Command('TSLR', 6, 1016, 'TSLR', 'slight right'),
7: new Command('TSHR', 8, 1018, 'TSHR', 'sharp right'),
8: new Command('KL', 9, 1015, 'TSLL', 'keep left'),
9: new Command('KR', 10, 1014, 'TSLR', 'keep right'),
10: new Command('TU', 13, 1003, 'TU', 'u-turn'),
11: new Command('TRU', 14, 1003, 'TU', 'u-turn'), // Right U-turn
12: new Command('OFFR'), // Off route
13: new Command('RNDB', 26, 1008, 'RNDB', 'Take exit '), // Roundabout
14: new Command('RNLB', 26, 1008, 'RNLB', 'Take exit '), // Roundabout left
};
})();
class WaypointVoiceHints extends VoiceHints {
_addToTransform(transform) {
transform.gpx = function (gpx, features) {
this._addWaypoints(gpx);
return gpx;
}.bind(this);
}
_addWaypoints(gpx) {
const waypoints = [];
this._loopHints((hint, cmd, coord) => {
const properties = this._getWpt(hint, cmd, coord);
const wpt = this._createWpt(coord, properties);
waypoints.push(wpt);
});
gpx.wpt.unshift(...waypoints);
}
_createWpt(coord, properties) {
return Object.assign(
{
'@lat': coord[1],
'@lon': coord[0],
},
properties
);
}
// override in subclass
_getWpt(hint, cmd, coord) {
return {};
}
}
class GpsiesVoiceHints extends WaypointVoiceHints {
_getWpt(hint, cmd, coord) {
return { name: cmd.message, sym: cmd.symbol.toLowerCase(), type: cmd.symbol };
}
}
class OruxVoiceHints extends WaypointVoiceHints {
_getWpt(hint, cmd, coord) {
const wpt = {
ele: coord[2],
extensions: {
'om:oruxmapsextensions': {
'@xmlns:om': 'http://www.oruxmaps.com/oruxmapsextensions/1/0',
'om:ext': { '@type': 'ICON', '@subtype': 0, _: cmd.orux },
},
},
};
if (wpt.ele === undefined || wpt.ele === null) {
delete wpt.ele;
}
return wpt;
}
}
class LocusVoiceHints extends WaypointVoiceHints {
_addToTransform(transform) {
transform.gpx = function (gpx, features) {
// hack to insert attribute after the other `xmlns`s
gpx = Object.assign(
{
'@xmlns': gpx['@xmlns'],
'@xmlns:xsi': gpx['@xmlns:xsi'],
'@xmlns:locus': 'http://www.locusmap.eu',
},
gpx
);
this._addWaypoints(gpx);
return gpx;
}.bind(this);
}
_getWpt(hint, cmd, coord) {
const extensions = {};
extensions['locus:rteDistance'] = hint.distance;
if (hint.time > 0) {
extensions['locus:rteTime'] = hint.time.toFixed(3);
extensions['locus:rteSpeed'] = hint.speed.toFixed(3);
}
extensions['locus:rtePointAction'] = cmd.locus;
const wpt = {
ele: coord[2],
name: cmd.message,
extensions: extensions,
};
if (wpt.ele === undefined || wpt.ele === null) {
delete wpt.ele;
}
return wpt;
}
_getTrk() {
return {
extensions: {
'locus:rteComputeType': this._getLocusRouteType(this.transportMode),
'locus:rteSimpleRoundabouts': 1,
},
};
}
_getLocusRouteType(transportMode) {
switch (transportMode) {
case 'car':
return 0;
case 'bike':
return 5;
default:
return 3; // foot
}
}
}
class CommentVoiceHints extends VoiceHints {
_addToTransform(transform) {
let comment = `
<!-- $transport-mode$${this.transportMode}$ -->
<!-- cmd idx lon lat d2next geometry -->
<!-- $turn-instruction-start$`;
this._loopHints((hint, cmd, coord) => {
const pad = (obj = '', len) => {
return new String(obj).padStart(len) + ';';
};
let turn = '';
turn += pad(cmd.name, 6);
turn += pad(hint.indexInTrack, 6);
turn += pad(coord[0], 10);
turn += pad(coord[1], 10);
turn += pad(hint.distance, 6);
turn += hint.geometry;
comment += `
$turn$${turn}$`;
});
comment += `
$turn-instruction-end$ -->
`;
transform.comment = comment;
}
}
class OsmAndVoiceHints extends VoiceHints {
_addToTransform(transform) {
transform.gpx = function (gpx, features) {
gpx['@creator'] = 'OsmAndRouter';
gpx.rte.push({
rtept: this._createRoutePoints(gpx),
});
return gpx;
}.bind(this);
}
_createRoutePoints(gpx) {
const rteptList = [];
const trkseg = gpx.trk[0].trkseg[0];
let trkpt = trkseg.trkpt[0];
const startPt = {
'@lat': trkpt['@lat'],
'@lon': trkpt['@lon'],
desc: 'start',
};
const times = this.track?.properties?.times;
if (this.voicehints && times) {
const startTime = times[this.voicehints[0][0]];
startPt.extensions = { time: Math.round(startTime), offset: 0 };
}
rteptList.push(startPt);
this._loopHints((hint, cmd, coord) => {
const rtept = {
'@lat': coord[1],
'@lon': coord[0],
desc: cmd.message,
extensions: {
time: Math.round(hint.time),
turn: cmd.name,
'turn-angle': hint.angle,
offset: hint.indexInTrack,
},
};
if (hint.time === 0) {
delete properties.extensions.time;
}
rteptList.push(rtept);
});
const lastIndex = trkseg.trkpt.length - 1;
trkpt = trkseg.trkpt[lastIndex];
rteptList.push({
'@lat': trkpt['@lat'],
'@lon': trkpt['@lon'],
desc: 'destination',
extensions: { time: 0, offset: lastIndex },
});
return rteptList;
}
}
BR.voiceHints = function (geoJson, turnInstructionMode, transportMode) {
switch (turnInstructionMode) {
case 2:
return new LocusVoiceHints(geoJson, turnInstructionMode, transportMode);
case 3:
return new OsmAndVoiceHints(geoJson, turnInstructionMode, transportMode);
case 4:
return new CommentVoiceHints(geoJson, turnInstructionMode, transportMode);
case 5:
return new GpsiesVoiceHints(geoJson, turnInstructionMode, transportMode);
case 6:
return new OruxVoiceHints(geoJson, turnInstructionMode, transportMode);
default:
console.error('unhandled turnInstructionMode: ' + turnInstructionMode);
return new VoiceHints(geoJson, turnInstructionMode, transportMode);
}
};
})();

69
js/format/Xml.js Normal file
View file

@ -0,0 +1,69 @@
BR.Xml = {
// modified version of
// https://gist.github.com/sente/1083506#gistcomment-2254622
// MIT License, Copyright (c) 2016 Stuart Powers, ES6 version by Jonathan Gruber
pretty: function (xml, indentSize = 1) {
const PADDING = ' '.repeat(indentSize);
const newline = '\n';
// Remove all the newlines and then remove all the spaces between tags
xml = xml.replace(/>\s*(\r\n|\n|\r)\s*</gm, '><').replace(/>\s+</g, '><');
xml = xml.replace('<metadata/>', '');
// break into lines, keeping defined tags on a single line
const reg = /><(\/?)([\w!?][^ />]*)/g;
const singleLineTagList = ['trkpt', 'wpt'];
let lines = [];
let singleLineTag = null;
let startIndex = 0;
let match;
while ((match = reg.exec(xml)) !== null) {
const tag = match[2];
if (singleLineTag) {
if (singleLineTag === tag) {
singleLineTag = null;
}
} else {
if (singleLineTagList.includes(tag)) {
singleLineTag = tag;
}
let endIndex = match.index + 1;
lines.push(xml.substring(startIndex, endIndex));
startIndex = endIndex;
}
}
lines.push(xml.substring(startIndex));
// indent
const startTextEnd = /.+<\/\w[^>]*>$/;
const endTag = /^<\/\w/;
const startTag = /^<\w[^>]*[^\/]>.*$/;
let pad = 0;
lines = lines.map((node, index) => {
let indent = 0;
if (node.match(startTextEnd)) {
indent = 0;
} else if (node.match(endTag) && pad > 0) {
pad -= 1;
} else if (node.match(startTag)) {
indent = 1;
} else {
indent = 0;
}
pad += indent;
return PADDING.repeat(pad - indent) + node;
});
// break gpx attributes into separate lines
for (const [i, line] of lines.entries()) {
if (line.includes('<gpx ') && !line.includes(newline)) {
lines[i] = line.replace(/ (\w[^=" ]*=")/g, ` ${newline}${PADDING}$1`);
break;
}
}
return lines.join(newline);
},
};

View file

@ -265,7 +265,7 @@
pois = new BR.PoiMarkers(routing);
exportRoute = new BR.Export(router, pois);
exportRoute = new BR.Export(router, pois, profile);
routing.on('routing:routeWaypointEnd routing:setWaypointsEnd', function (evt) {
search.clear();
@ -303,7 +303,7 @@
trackMessages.update(track, segments);
trackAnalysis.update(track, segments);
exportRoute.update(latLngs);
exportRoute.update(latLngs, segments);
}
routing.addTo(map);

View file

@ -8,7 +8,7 @@
"transifex-push": "gulp i18next && tx push --source",
"transifex-pull": "tx pull --all --minimum-perc 1 --force && (git add locales/*.json && git commit locales/*.json -m 'Update translations' || true)",
"layers": "node layers/josm/extract.js && node layers/collection/extract.js",
"test": "gulp",
"test": "jest",
"lint": "eslint .",
"prettier": "prettier --write '**/*'",
"serve": "gulp serve watch",
@ -79,7 +79,8 @@
"osmtogeojson": "^3.0.0-beta.4",
"overpass-layer": "^3.0.0",
"regenerator-runtime": "^0.13.7",
"togpx": "^0.5.4",
"togpx": "nrenner/togpx#722d291",
"tokml": "^0.4.0",
"topojson-client": "^3.1.0",
"url-search-params": "~0.5.0"
},
@ -116,6 +117,7 @@
"gulp-zip": "^5.0.2",
"husky": "^4.3.4",
"i18next-scanner": "^3.0.0",
"jest": "^26.6.3",
"marked": "^2.0.0",
"merge-stream": "^2.0.0",
"node-fetch": "^2.6.1",
@ -282,6 +284,11 @@
"minified.js"
]
},
"tokml": {
"main": [
"tokml.js"
]
},
"overpass-layer": {
"main": [
"dist/overpass-layer.js"

View file

@ -0,0 +1,107 @@
BR = {};
BR.conf = {};
$ = require('jquery');
require('leaflet');
turf = require('@turf/turf');
require('../../js/Browser.js');
require('../../js/control/Export.js');
const fs = require('fs');
const indexHtmlString = fs.readFileSync('index.html', 'utf8');
const indexHtml = new DOMParser().parseFromString(indexHtmlString, 'text/html');
// &lonlats=8.467712,49.488117;8.469354,49.488394;8.470556,49.488946;8.469982,49.489176 + turnInstructionMode=2
const segments = require('./data/segments.json');
const brouterTotal = require('./data/brouterTotal.json');
// resolve intended/accepted differences before comparing
function adopt(total, brouterTotal) {
// BRouter total aggregates messages over segments, client total does not,
// but that's Ok, so just fix for the test comparison
const messages = total.features[0].properties.messages;
const message = messages[4].slice();
messages[4] = message;
message[3] = (+message[3] + +messages[2][3] + +messages[3][3]).toString();
message[6] = (+message[6] + +messages[2][6] + +messages[3][6]).toString();
messages.splice(2, 2);
// fix minor float rounding difference
total.features[0].properties.times[6] = 28.833; // 28.832
total.features[0].properties.name = brouterTotal.features[0].properties.name;
}
let track;
const getLngCoord = (i) => track.features[i].geometry.coordinates[0];
const getProperty = (i, p) => track.features[i].properties[p];
beforeEach(() => {
document.body = indexHtml.body.cloneNode(true);
track = turf.featureCollection([
turf.lineString([
[0, 0],
[1, 1],
[2, 2],
]),
]);
});
test('total track', () => {
const segmentsString = JSON.stringify(segments, null, 2);
let total = BR.Export._concatTotalTrack(segments);
adopt(total, brouterTotal);
expect(total).toEqual(brouterTotal);
// test original segments are not modified
expect(JSON.stringify(segments, null, 2)).toEqual(segmentsString);
// should be repeatable
total = BR.Export._concatTotalTrack(segments);
adopt(total, brouterTotal);
expect(total).toEqual(brouterTotal);
});
test('include route points', () => {
const latLngs = [L.latLng(0, 0), L.latLng(1, 1), L.latLng(2, 2)];
const exportRoute = new BR.Export();
exportRoute.update(latLngs, null);
exportRoute._addRouteWaypoints(track);
expect(track.features[0].geometry.type).toEqual('LineString');
expect(getLngCoord(1)).toEqual(0);
expect(getLngCoord(2)).toEqual(1);
expect(getLngCoord(3)).toEqual(2);
expect(getProperty(1, 'name')).toEqual('from');
expect(getProperty(2, 'name')).toEqual('via1');
expect(getProperty(3, 'name')).toEqual('to');
expect(getProperty(1, 'type')).toEqual('from');
expect(getProperty(2, 'type')).toEqual('via');
expect(getProperty(3, 'type')).toEqual('to');
});
test('pois', () => {
const markers = [
{
latlng: L.latLng(1, 1),
name: 'poi 1',
},
{
latlng: L.latLng(2, 2),
name: 'poi 2',
},
];
const pois = { getMarkers: () => markers };
const exportRoute = new BR.Export(null, pois, null);
exportRoute._addPois(track);
expect(track.features[0].geometry.type).toEqual('LineString');
expect(getLngCoord(1)).toEqual(1);
expect(getLngCoord(2)).toEqual(2);
expect(getProperty(1, 'name')).toEqual('poi 1');
expect(getProperty(2, 'name')).toEqual('poi 2');
expect(getProperty(1, 'type')).toEqual('poi');
expect(getProperty(2, 'type')).toEqual('poi');
});

View file

@ -0,0 +1,148 @@
BR = {};
$ = require('jquery');
i18next = require('i18next');
require('leaflet');
require('../../js/control/Profile.js');
require('../../js/format/Gpx.js');
const fs = require('fs');
class CodeMirrorMock {
static fromTextArea() {
return new CodeMirrorMock();
}
setValue(value) {
this.value = value;
}
getValue() {
return this.value;
}
isClean() {
return true;
}
markClean() {}
}
CodeMirror = CodeMirrorMock;
BR.Message = jest.fn();
const indexHtmlString = fs.readFileSync('index.html', 'utf8');
const indexHtml = new DOMParser().parseFromString(indexHtmlString, 'text/html');
function toggleSecondaryTab() {
L.DomUtil.get('profile_params_container').classList.toggle('active');
profile._activateSecondaryTab();
}
const profileText = `
---context:global # following code refers to global config
# abc settings
assign isOne = true # %isOne% | first var | boolean
assign optTwo = 2 # %varTwo% | second var | [0=none, 1=opt1, 2=opt2]
assign three = 3 # %three% | third var | number
`;
const paramsHtml = `
<input name="isOne" id="customize-profile-isOne" type="checkbox">
<select name="optTwo" class="form-control form-control-sm" id="customize-profile-optTwo">
<option value="0">none</option>
<option value="1">opt1</option>
<option value="2">opt2</option>
</select>
<input name="three" id="customize-profile-three" type="number" class="form-control form-control-sm">
`;
let profile;
beforeEach(() => {
document.body = indexHtml.body.cloneNode(true);
profile = new BR.Profile();
});
describe('getProfileVar', () => {
test('with comment', () => {
toggleSecondaryTab();
profile._setValue(profileText);
expect(profile.getProfileVar('isOne')).toEqual('true');
expect(profile.getProfileVar('optTwo')).toEqual('2');
expect(profile.getProfileVar('three')).toEqual('3');
});
test('without comment', () => {
profile._setValue(' assign foo=1');
const value = profile.getProfileVar('foo');
expect(value).toEqual('1');
});
test('without "="', () => {
profile._setValue('assign foo 1');
const value = profile.getProfileVar('foo');
expect(value).toEqual('1');
});
test('not defined', () => {
profile._setValue('');
const value = profile.getProfileVar('foo');
expect(value).toEqual(undefined);
});
test('text undefined', () => {
profile._setValue(undefined);
const value = profile.getProfileVar('foo');
expect(value).toEqual(undefined);
});
test('options tab active', () => {
profile._setValue(profileText);
document.getElementById('customize-profile-optTwo').value = '1';
expect(profile.getProfileVar('isOne')).toEqual(true);
expect(profile.getProfileVar('optTwo')).toEqual('1');
expect(profile.getProfileVar('three')).toEqual('3');
});
});
describe('getTransportMode', () => {
test('bike true', () => {
const profileText = `
# Bike profile
assign validForBikes = true
# comment`;
profile._setValue(profileText);
expect(profile.getTransportMode()).toEqual('bike');
});
test('car 1', () => {
profile._setValue('assign validForCars 1');
expect(profile.getTransportMode()).toEqual('car');
});
test('default foot', () => {
profile._setValue('');
expect(profile.getTransportMode()).toEqual('foot');
});
});
test('_buildParamsForm', () => {
profile._buildParamsForm(profileText);
const getValue = (name) => {
const input = document.getElementById('customize-profile-' + name);
return input.type === 'checkbox' ? input.checked : input.value;
};
expect(getValue('isOne')).toEqual(true);
expect(getValue('optTwo')).toEqual('2');
expect(getValue('three')).toEqual('3');
});
test('_buildCustomProfile', () => {
document.getElementById('profile_params').innerHTML = paramsHtml;
document.getElementById('customize-profile-isOne').checked = true;
document.getElementById('customize-profile-optTwo').value = '2';
document.getElementById('customize-profile-three').value = '3';
const result = profile._buildCustomProfile(profileText).split('\n');
expect(result[3]).toMatch(/isOne = true/);
expect(result[4]).toMatch(/optTwo = 2/);
expect(result[5]).toMatch(/three = 3/);
});

View file

@ -0,0 +1,45 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"creator": "BRouter-1.1",
"name": "brouter_1615489279610_0",
"track-length": "388",
"filtered ascend": "1",
"plain-ascend": "0",
"total-time": "44",
"total-energy": "4420",
"cost": "703",
"voicehints": [
[1,5,0,88.0,89],
[6,2,0,99.0,-90],
[7,2,0,10.0,-90]
],
"messages": [
["Longitude", "Latitude", "Elevation", "Distance", "CostPerKm", "ElevCost", "TurnCost", "NodeCost", "InitialCost", "WayTags", "NodeTags"],
["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", ""],
["8469852", "49489230", "100", "299", "1150", "0", "270", "0", "0", "highway=residential surface=asphalt oneway=yes smoothness=good", ""]
],
"times": [0,9.592,12.271,14.13,19.406,22.134,28.833,37.817,38.938,44.217]
},
"geometry": {
"type": "LineString",
"coordinates": [
[8.467714, 49.488115, 101.5],
[8.468340, 49.488794, 101.5],
[8.468586, 49.488698, 101.5],
[8.468743, 49.488636, 101.5],
[8.469161, 49.488473, 101.75],
[8.469355, 49.488395, 102.0],
[8.469971, 49.488151, 103.5],
[8.470671, 49.488909, 99.5],
[8.470561, 49.488951, 99.5],
[8.469984, 49.489178, 100.0]
]
}
}
]
}

View file

@ -0,0 +1,97 @@
[{
"feature": {
"type": "Feature",
"properties": {
"creator": "BRouter-1.1",
"name": "brouter_1615393581719_0",
"track-length": "177",
"filtered ascend": "0",
"plain-ascend": "1",
"total-time": "22",
"total-energy": "2213",
"cost": "280",
"voicehints": [
[1,5,0,88.0,89]
],
"messages": [
["Longitude", "Latitude", "Elevation", "Distance", "CostPerKm", "ElevCost", "TurnCost", "NodeCost", "InitialCost", "WayTags", "NodeTags"],
["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", ""],
["8469971", "49488151", "102", "88", "1150", "0", "90", "0", "0", "highway=residential surface=asphalt oneway=yes smoothness=good", ""]
],
"times": [0,9.592,12.271,14.13,19.406,22.134]
},
"geometry": {
"type": "LineString",
"coordinates": [
[8.467714, 49.488115, 101.5],
[8.468340, 49.488794, 101.5],
[8.468586, 49.488698, 101.5],
[8.468743, 49.488636, 101.5],
[8.469161, 49.488473, 101.75],
[8.469355, 49.488395, 102.0]
]
}
}
},
{
"feature": {
"type": "Feature",
"properties": {
"creator": "BRouter-1.1",
"name": "brouter_1615393581719_0",
"track-length": "162",
"filtered ascend": "1",
"plain-ascend": "-2",
"total-time": "17",
"total-energy": "1680",
"cost": "367",
"voicehints": [
[1,2,0,99.0,-90],
[2,2,0,10.0,-90]
],
"messages": [
["Longitude", "Latitude", "Elevation", "Distance", "CostPerKm", "ElevCost", "TurnCost", "NodeCost", "InitialCost", "WayTags", "NodeTags"],
["8469852", "49489230", "99", "162", "1150", "0", "180", "0", "0", "highway=residential surface=asphalt oneway=yes smoothness=good", ""]
],
"times": [0,6.698,15.683,16.804]
},
"geometry": {
"type": "LineString",
"coordinates": [
[8.469355, 49.488395, 102.0],
[8.469971, 49.488151, 103.5],
[8.470671, 49.488909, 99.5],
[8.470561, 49.488951, 99.5]
]
}
}
},
{
"feature": {
"type": "Feature",
"properties": {
"creator": "BRouter-1.1",
"name": "brouter_1615393581719_0",
"track-length": "49",
"filtered ascend": "0",
"plain-ascend": "1",
"total-time": "5",
"total-energy": "527",
"cost": "56",
"messages": [
["Longitude", "Latitude", "Elevation", "Distance", "CostPerKm", "ElevCost", "TurnCost", "NodeCost", "InitialCost", "WayTags", "NodeTags"],
["8469852", "49489230", "100", "49", "1150", "0", "0", "0", "0", "highway=residential surface=asphalt oneway=yes smoothness=good", ""]
],
"times": [0,5.279]
},
"geometry": {
"type": "LineString",
"coordinates": [
[8.470561, 49.488951, 99.5],
[8.469984, 49.489178, 100.0]
]
}
}
}
]

97
tests/format/Gpx.test.js Normal file
View file

@ -0,0 +1,97 @@
BR = {};
BR.version = '1.5.1';
turf = require('@turf/turf');
togpx = require('togpx');
require('leaflet');
require('../../js/Browser.js');
require('../../js/format/VoiceHints.js');
require('../../js/format/Xml.js');
require('../../js/format/Gpx.js');
const fs = require('fs');
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
// console log in Export._formatTrack
const waypointsGeoJson = require('./data/waypoints.json');
const path = 'tests/format/data/';
// resolve intended/accepted differences before comparing
function adoptGpx(gpx, replaceCreator = true) {
const creator = 'BRouter-Web 1.5.1';
const name = 'Track';
const newline = '\n';
gpx = gpx.replace('=.0', '=0.0');
if (replaceCreator) {
gpx = gpx.replace(/creator="[^"]*"/, `creator="${creator}"`);
}
gpx = gpx.replace(/creator="([^"]*)" version="1.1"/, 'version="1.1" \n creator="$1"');
gpx = gpx.replace(/<trk>\n <name>[^<]*<\/name>/, `<trk>\n <name>${name}</name>`);
gpx = gpx
.split(newline)
.map((line) => line.replace(/lon="([^"]*)" lat="([^"]*)"/, 'lat="$2" lon="$1"'))
.join(newline);
gpx = gpx.replace(/(lon|lat)="([-0-9]+\.[0-9]+?)0+"/g, '$1="$2"'); // remove trailing zeros
gpx = gpx.replace(/>([-0-9]+\.\d*0+)<\//g, (match, p1) => `>${+p1}</`); // remove trailing zeros
gpx = gpx.replace('</gpx>\n', '</gpx>');
return gpx;
}
function read(fileName, replaceCreator) {
return adoptGpx(fs.readFileSync(path + fileName, 'utf8'), replaceCreator);
}
test('simple track', () => {
const brouterGpx = read('track.gpx');
const gpx = BR.Gpx.format(geoJson);
expect(gpx).toEqual(brouterGpx);
});
test('waypoints', () => {
const brouterGpx = BR.Xml.pretty(read('waypoints.gpx'));
const gpx = BR.Gpx.format(waypointsGeoJson, 5);
expect(gpx).toEqual(brouterGpx);
});
describe('voice hints', () => {
test('2-locus', () => {
let brouterGpx = BR.Xml.pretty(read('2-locus.gpx'));
brouterGpx = brouterGpx.replace(/\n\s*<\/extensions>\n\s*<extensions>/, ''); // ignore (invalid) double tag
// ignore float rounding differences
brouterGpx = brouterGpx.replace(
/:(rteTime|rteSpeed)>([\d.]*)<\//g,
(match, p1, p2) => `:${p1}>${(+p2).toFixed(3)}</`
);
const gpx = BR.Gpx.format(geoJson, 2);
expect(gpx).toEqual(brouterGpx);
});
test('3-osmand', () => {
const brouterGpx = BR.Xml.pretty(read('3-osmand.gpx', false));
const gpx = BR.Gpx.format(geoJson, 3);
expect(gpx).toEqual(brouterGpx);
});
test('4-comment', () => {
let brouterGpx = read('4-comment.gpx');
brouterGpx = brouterGpx.replace(/;\s*([-0-9]+.[0-9]+?)0+;/g, (match, p1) => `;${p1.padStart(10)};`); // remove trailing zeros
const gpx = BR.Gpx.format(geoJson, 4);
expect(gpx).toEqual(brouterGpx);
});
test('5-gpsies', () => {
const brouterGpx = read('5-gpsies.gpx');
const gpx = BR.Gpx.format(geoJson, 5);
expect(gpx).toEqual(brouterGpx);
});
test('6-orux', () => {
let brouterGpx = BR.Xml.pretty(read('6-orux.gpx'));
const gpx = BR.Gpx.format(geoJson, 6);
expect(gpx).toEqual(brouterGpx);
});
});

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- track-length = 319 filtered ascend = 2 plain-ascend = -1 cost=533 energy=.0kwh time=44s -->
<gpx
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:locus="http://www.locusmap.eu"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
creator="BRouter-1.6.1" version="1.1">
<wpt lon="8.468340" lat="49.488794"><ele>101.5</ele><name>right</name><extensions><locus:rteDistance>140.0</locus:rteDistance><locus:rteTime>24.90994644165039</locus:rteTime><locus:rteSpeed>5.620244922161478</locus:rteSpeed><locus:rtePointAction>7</locus:rtePointAction></extensions></wpt>
<wpt lon="8.469971" lat="49.488151"><ele>103.5</ele><name>left</name><extensions><locus:rteDistance>90.0</locus:rteDistance><locus:rteTime>9.614852905273438</locus:rteTime><locus:rteSpeed>9.360517616513706</locus:rteSpeed><locus:rtePointAction>4</locus:rtePointAction></extensions></wpt>
<trk>
<name>2-locus</name>
<extensions><locus:rteComputeType>5</locus:rteComputeType></extensions>
<extensions><locus:rteSimpleRoundabouts>1</locus:rteSimpleRoundabouts></extensions>
<trkseg>
<trkpt lon="8.467714" lat="49.488115"><ele>101.5</ele></trkpt>
<trkpt lon="8.468340" lat="49.488794"><ele>101.5</ele></trkpt>
<trkpt lon="8.468586" lat="49.488698"><ele>101.5</ele></trkpt>
<trkpt lon="8.468743" lat="49.488636"><ele>101.5</ele></trkpt>
<trkpt lon="8.469161" lat="49.488473"><ele>101.75</ele></trkpt>
<trkpt lon="8.469971" lat="49.488151"><ele>103.5</ele></trkpt>
<trkpt lon="8.470610" lat="49.488842"><ele>99.75</ele></trkpt>
</trkseg>
</trk>
</gpx>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- track-length = 319 filtered ascend = 2 plain-ascend = -1 cost=533 energy=.0kwh time=44s -->
<gpx
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
creator="OsmAndRouter" version="1.1">
<rte>
<rtept lat="49.488115" lon="8.467714">
<desc>start</desc>
<extensions>
<time>10</time>
<offset>0</offset>
</extensions>
</rtept>
<rtept lat="49.488794" lon="8.468340">
<desc>right</desc>
<extensions>
<time>25</time>
<turn>TR</turn>
<turn-angle>89</turn-angle>
<offset>1</offset>
</extensions>
</rtept>
<rtept lat="49.488151" lon="8.469971">
<desc>left</desc>
<extensions>
<time>10</time>
<turn>TL</turn>
<turn-angle>-90</turn-angle>
<offset>5</offset>
</extensions>
</rtept>
<rtept lat="49.488842" lon="8.470610">
<desc>destination</desc>
<extensions>
<time>0</time>
<offset>6</offset>
</extensions>
</rtept>
</rte>
<trk>
<name>3-osmand</name>
<trkseg>
<trkpt lon="8.467714" lat="49.488115"><ele>101.5</ele></trkpt>
<trkpt lon="8.468340" lat="49.488794"><ele>101.5</ele></trkpt>
<trkpt lon="8.468586" lat="49.488698"><ele>101.5</ele></trkpt>
<trkpt lon="8.468743" lat="49.488636"><ele>101.5</ele></trkpt>
<trkpt lon="8.469161" lat="49.488473"><ele>101.75</ele></trkpt>
<trkpt lon="8.469971" lat="49.488151"><ele>103.5</ele></trkpt>
<trkpt lon="8.470610" lat="49.488842"><ele>99.75</ele></trkpt>
</trkseg>
</trk>
</gpx>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- track-length = 319 filtered ascend = 2 plain-ascend = -1 cost=533 energy=.0kwh time=44s -->
<!-- $transport-mode$bike$ -->
<!-- cmd idx lon lat d2next geometry -->
<!-- $turn-instruction-start$
$turn$ TR; 1; 8.468340; 49.488794; 140; 6(90)6 (0)6 (-89)2$
$turn$ TL; 5; 8.469971; 49.488151; 90; 6(-89)6 (0)6 (89)6$
$turn-instruction-end$ -->
<gpx
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
creator="BRouter-1.6.1" version="1.1">
<trk>
<name>4-comment</name>
<trkseg>
<trkpt lon="8.467714" lat="49.488115"><ele>101.5</ele></trkpt>
<trkpt lon="8.468340" lat="49.488794"><ele>101.5</ele></trkpt>
<trkpt lon="8.468586" lat="49.488698"><ele>101.5</ele></trkpt>
<trkpt lon="8.468743" lat="49.488636"><ele>101.5</ele></trkpt>
<trkpt lon="8.469161" lat="49.488473"><ele>101.75</ele></trkpt>
<trkpt lon="8.469971" lat="49.488151"><ele>103.5</ele></trkpt>
<trkpt lon="8.470610" lat="49.488842"><ele>99.75</ele></trkpt>
</trkseg>
</trk>
</gpx>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- track-length = 319 filtered ascend = 2 plain-ascend = -1 cost=533 energy=.0kwh time=44s -->
<gpx
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
creator="BRouter-1.6.1" version="1.1">
<wpt lon="8.468340" lat="49.488794"><name>right</name><sym>right</sym><type>Right</type></wpt>
<wpt lon="8.469971" lat="49.488151"><name>left</name><sym>left</sym><type>Left</type></wpt>
<trk>
<name>5-gpsies</name>
<trkseg>
<trkpt lon="8.467714" lat="49.488115"><ele>101.5</ele></trkpt>
<trkpt lon="8.468340" lat="49.488794"><ele>101.5</ele></trkpt>
<trkpt lon="8.468586" lat="49.488698"><ele>101.5</ele></trkpt>
<trkpt lon="8.468743" lat="49.488636"><ele>101.5</ele></trkpt>
<trkpt lon="8.469161" lat="49.488473"><ele>101.75</ele></trkpt>
<trkpt lon="8.469971" lat="49.488151"><ele>103.5</ele></trkpt>
<trkpt lon="8.470610" lat="49.488842"><ele>99.75</ele></trkpt>
</trkseg>
</trk>
</gpx>

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- track-length = 319 filtered ascend = 2 plain-ascend = -1 cost=533 energy=.0kwh time=44s -->
<gpx
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
creator="BRouter-1.6.1" version="1.1">
<wpt lat="49.488794" lon="8.468340"><ele>101.5</ele><extensions>
<om:oruxmapsextensions xmlns:om="http://www.oruxmaps.com/oruxmapsextensions/1/0">
<om:ext type="ICON" subtype="0">1001</om:ext>
</om:oruxmapsextensions>
</extensions>
</wpt> <wpt lat="49.488151" lon="8.469971"><ele>103.5</ele><extensions>
<om:oruxmapsextensions xmlns:om="http://www.oruxmaps.com/oruxmapsextensions/1/0">
<om:ext type="ICON" subtype="0">1000</om:ext>
</om:oruxmapsextensions>
</extensions>
</wpt> <trk>
<name>6-orux</name>
<trkseg>
<trkpt lon="8.467714" lat="49.488115"><ele>101.5</ele></trkpt>
<trkpt lon="8.468340" lat="49.488794"><ele>101.5</ele></trkpt>
<trkpt lon="8.468586" lat="49.488698"><ele>101.5</ele></trkpt>
<trkpt lon="8.468743" lat="49.488636"><ele>101.5</ele></trkpt>
<trkpt lon="8.469161" lat="49.488473"><ele>101.75</ele></trkpt>
<trkpt lon="8.469971" lat="49.488151"><ele>103.5</ele></trkpt>
<trkpt lon="8.470610" lat="49.488842"><ele>99.75</ele></trkpt>
</trkseg>
</trk>
</gpx>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- track-length = 319 filtered ascend = 2 plain-ascend = -1 cost=533 energy=.0kwh time=44s -->
<gpx
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
creator="BRouter-1.6.1" version="1.1">
<trk>
<name>Track</name>
<trkseg>
<trkpt lon="8.467714" lat="49.488115"><ele>101.5</ele></trkpt>
<trkpt lon="8.468340" lat="49.488794"><ele>101.5</ele></trkpt>
<trkpt lon="8.468586" lat="49.488698"><ele>101.5</ele></trkpt>
<trkpt lon="8.468743" lat="49.488636"><ele>101.5</ele></trkpt>
<trkpt lon="8.469161" lat="49.488473"><ele>101.75</ele></trkpt>
<trkpt lon="8.469971" lat="49.488151"><ele>103.5</ele></trkpt>
<trkpt lon="8.470610" lat="49.488842"><ele>99.75</ele></trkpt>
</trkseg>
</trk>
</gpx>

View file

@ -0,0 +1,40 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"creator": "BRouter-1.1",
"name": "Track",
"track-length": "319",
"filtered ascend": "2",
"plain-ascend": "-1",
"total-time": "44",
"total-energy": "4412",
"cost": "533",
"voicehints": [
[1,5,0,140.0,89," 6(90)6 (0)6 (-89)2"],
[5,2,0,90.0,-90," 6(-89)6 (0)6 (89)6"]
],
"messages": [
["Longitude", "Latitude", "Elevation", "Distance", "CostPerKm", "ElevCost", "TurnCost", "NodeCost", "InitialCost", "WayTags", "NodeTags"],
["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", ""],
["8470671", "49488909", "99", "230", "1150", "0", "180", "0", "0", "highway=residential surface=asphalt oneway=yes smoothness=good", ""]
],
"times": [0.0,9.592433,12.270765,14.129882,19.406338,34.50238,44.117233]
},
"geometry": {
"type": "LineString",
"coordinates": [
[8.467714, 49.488115, 101.5],
[8.468340, 49.488794, 101.5],
[8.468586, 49.488698, 101.5],
[8.468743, 49.488636, 101.5],
[8.469161, 49.488473, 101.75],
[8.469971, 49.488151, 103.5],
[8.470610, 49.488842, 99.75]
]
}
}
]
}

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- track-length = 388 filtered ascend = 1 plain-ascend = 0 cost=703 energy=.0kwh time=44s -->
<gpx
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
creator="BRouter-1.6.1" version="1.1">
<wpt lon="8.468340" lat="49.488794"><name>right</name><sym>right</sym><type>Right</type></wpt>
<wpt lon="8.469971" lat="49.488151"><name>left</name><sym>left</sym><type>Left</type></wpt>
<wpt lon="8.470671" lat="49.488909"><name>left</name><sym>left</sym><type>Left</type></wpt>
<wpt lon="8.469279" lat="49.487399">
<name>poi 1</name>
</wpt>
<wpt lon="8.469021" lat="49.489532">
<name>poi 2</name>
</wpt>
<wpt lon="8.467712" lat="49.488117">
<name>from</name>
<type>from</type>
</wpt>
<wpt lon="8.469354" lat="49.488394">
<name>via1</name>
<type>via</type>
</wpt>
<wpt lon="8.470556" lat="49.488946">
<name>via2</name>
<type>via</type>
</wpt>
<wpt lon="8.469982" lat="49.489176">
<name>to</name>
<type>to</type>
</wpt>
<trk>
<name>waypoints + 5-gpsies</name>
<trkseg>
<trkpt lon="8.467714" lat="49.488115"><ele>101.5</ele></trkpt>
<trkpt lon="8.468340" lat="49.488794"><ele>101.5</ele></trkpt>
<trkpt lon="8.468586" lat="49.488698"><ele>101.5</ele></trkpt>
<trkpt lon="8.468743" lat="49.488636"><ele>101.5</ele></trkpt>
<trkpt lon="8.469161" lat="49.488473"><ele>101.75</ele></trkpt>
<trkpt lon="8.469355" lat="49.488395"><ele>102.0</ele></trkpt>
<trkpt lon="8.469971" lat="49.488151"><ele>103.5</ele></trkpt>
<trkpt lon="8.470671" lat="49.488909"><ele>99.5</ele></trkpt>
<trkpt lon="8.470561" lat="49.488951"><ele>99.5</ele></trkpt>
<trkpt lon="8.469984" lat="49.489178"><ele>100.0</ele></trkpt>
</trkseg>
</trk>
</gpx>

View file

@ -0,0 +1,259 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"creator": "BRouter-1.1",
"name": "Track",
"track-length": "388",
"filtered ascend": "1",
"plain-ascend": "0",
"total-time": "44",
"total-energy": "4420",
"cost": "703",
"voicehints": [
[
1,
5,
0,
88,
89
],
[
6,
2,
0,
99,
-90
],
[
7,
2,
0,
10,
-90
]
],
"messages": [
[
"Longitude",
"Latitude",
"Elevation",
"Distance",
"CostPerKm",
"ElevCost",
"TurnCost",
"NodeCost",
"InitialCost",
"WayTags",
"NodeTags"
],
[
"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",
""
],
[
"8469971",
"49488151",
"102",
"88",
"1150",
"0",
"90",
"0",
"0",
"highway=residential surface=asphalt oneway=yes smoothness=good",
""
],
[
"8469852",
"49489230",
"99",
"162",
"1150",
"0",
"180",
"0",
"0",
"highway=residential surface=asphalt oneway=yes smoothness=good",
""
],
[
"8469852",
"49489230",
"100",
"49",
"1150",
"0",
"0",
"0",
"0",
"highway=residential surface=asphalt oneway=yes smoothness=good",
""
]
],
"times": [
0,
9.592,
12.271,
14.13,
19.406,
22.134,
28.832,
37.817,
38.938,
44.217
]
},
"geometry": {
"type": "LineString",
"coordinates": [
[
8.467714,
49.488115,
101.5
],
[
8.46834,
49.488794,
101.5
],
[
8.468586,
49.488698,
101.5
],
[
8.468743,
49.488636,
101.5
],
[
8.469161,
49.488473,
101.75
],
[
8.469355,
49.488395,
102
],
[
8.469971,
49.488151,
103.5
],
[
8.470671,
49.488909,
99.5
],
[
8.470561,
49.488951,
99.5
],
[
8.469984,
49.489178,
100
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "poi 1",
"type": "poi"
},
"geometry": {
"type": "Point",
"coordinates": [
8.469279,
49.487399
]
}
},
{
"type": "Feature",
"properties": {
"name": "poi 2",
"type": "poi"
},
"geometry": {
"type": "Point",
"coordinates": [
8.469021,
49.489532
]
}
},
{
"type": "Feature",
"properties": {
"name": "from",
"type": "from"
},
"geometry": {
"type": "Point",
"coordinates": [
8.467712,
49.488117
]
}
},
{
"type": "Feature",
"properties": {
"name": "via1",
"type": "via"
},
"geometry": {
"type": "Point",
"coordinates": [
8.469354,
49.488394
]
}
},
{
"type": "Feature",
"properties": {
"name": "via2",
"type": "via"
},
"geometry": {
"type": "Point",
"coordinates": [
8.470556,
49.488946
]
}
},
{
"type": "Feature",
"properties": {
"name": "to",
"type": "to"
},
"geometry": {
"type": "Point",
"coordinates": [
8.469982,
49.489176
]
}
}
]
}

1891
yarn.lock

File diff suppressed because it is too large Load diff