Merge pull request #399 from nrenner/68-sl-formatting
Client-side track formatting
This commit is contained in:
commit
06f1c77774
31 changed files with 3686 additions and 83 deletions
|
|
@ -12,3 +12,6 @@ layers/
|
|||
locales/*.json
|
||||
resources/boundaries/
|
||||
resources/standalone/*.sh
|
||||
profiles2/
|
||||
tests/**/*.json
|
||||
tests/**/*.gpx
|
||||
|
|
|
|||
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
|
|
@ -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
24
.vscode/launch.json
vendored
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
10
gulpfile.js
10
gulpfile.js
|
|
@ -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('.'));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -22,5 +22,6 @@
|
|||
touchScreen: touchScreen,
|
||||
touchScreenDetectable: touchScreenDetectable,
|
||||
touch: touch,
|
||||
download: 'Blob' in window && 'createObjectURL' in URL && 'download' in document.createElement('a'),
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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)]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
14
js/format/Csv.js
Normal 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
98
js/format/Gpx.js
Normal 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
7
js/format/Kml.js
Normal 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
370
js/format/VoiceHints.js
Normal 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
69
js/format/Xml.js
Normal 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);
|
||||
},
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -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"
|
||||
|
|
|
|||
107
tests/control/Export.test.js
Normal file
107
tests/control/Export.test.js
Normal 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');
|
||||
});
|
||||
148
tests/control/Profile.test.js
Normal file
148
tests/control/Profile.test.js
Normal 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/);
|
||||
});
|
||||
45
tests/control/data/brouterTotal.json
Normal file
45
tests/control/data/brouterTotal.json
Normal 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]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
97
tests/control/data/segments.json
Normal file
97
tests/control/data/segments.json
Normal 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
97
tests/format/Gpx.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
25
tests/format/data/2-locus.gpx
Normal file
25
tests/format/data/2-locus.gpx
Normal 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>
|
||||
54
tests/format/data/3-osmand.gpx
Normal file
54
tests/format/data/3-osmand.gpx
Normal 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>
|
||||
26
tests/format/data/4-comment.gpx
Normal file
26
tests/format/data/4-comment.gpx
Normal 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>
|
||||
22
tests/format/data/5-gpsies.gpx
Normal file
22
tests/format/data/5-gpsies.gpx
Normal 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>
|
||||
30
tests/format/data/6-orux.gpx
Normal file
30
tests/format/data/6-orux.gpx
Normal 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>
|
||||
20
tests/format/data/track.gpx
Normal file
20
tests/format/data/track.gpx
Normal 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>
|
||||
40
tests/format/data/track.json
Normal file
40
tests/format/data/track.json
Normal 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]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
48
tests/format/data/waypoints.gpx
Normal file
48
tests/format/data/waypoints.gpx
Normal 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>
|
||||
259
tests/format/data/waypoints.json
Normal file
259
tests/format/data/waypoints.json
Normal 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
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue