diff --git a/index.html b/index.html
index 939bbb4..278cacb 100644
--- a/index.html
+++ b/index.html
@@ -1149,6 +1149,8 @@
);
+
+
diff --git a/js/control/Export.js b/js/control/Export.js
index 3740c8b..b9645f2 100644
--- a/js/control/Export.js
+++ b/js/control/Export.js
@@ -31,8 +31,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');
@@ -47,15 +48,38 @@ BR.Export = L.Class.extend({
var name = encodeURIComponent(exportForm['trackname'].value);
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 (true) {
+ var uri = this.router.getUrl(this.latLngs, this.pois.getMarkers(), null, format, name, 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);
+ //} else {
+
+ const track = this._formatTrack(format, name, includeWaypoints);
+ BR.Export.diff(uri, track, format);
+ }
+ },
+
+ _formatTrack: function (format, name, includeWaypoints) {
+ const track = BR.Export._concatTotalTrack(this.segments);
+ //console.log('GeoJson: ', trackGeoJson);
+ //console.log('GeoJson: ', JSON.stringify(trackGeoJson, null, 4));
+ switch (format) {
+ case 'gpx':
+ //console.log('gpx: ', gpx);
+ return BR.Gpx.format(track, 2, 'bike'); // TODO parse turnInstructionMode, transportMode
+ case 'geojson':
+ return JSON.stringify(track, null, 2);
+ case 'kml':
+ default:
+ break;
+ }
+ console.error('Export format not implemented: ' + format);
},
_validationMessage: function () {
@@ -72,6 +96,7 @@ BR.Export = L.Class.extend({
},
_generateTrackname: function () {
+ return; // TODO remove
var trackname = this.trackname;
this._getCityAtPosition(
this.latLngs[0],
@@ -143,3 +168,150 @@ 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;
+
+ //console.time('_concatTotalTrack');
+ 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);
+ }
+ //console.timeEnd('_concatTotalTrack');
+
+ return turf.featureCollection([turf.lineString(coordinates, properties)]);
+};
+
+//
+BR.Export.diff = function (uri, track, format) {
+ BR.Util.get(
+ uri,
+ ((err, text) => {
+ if (err) {
+ console.error('Error exporting "' + profileUrl + '": ' + err);
+ return;
+ }
+
+ if (format === 'gpx') {
+ text = BR.Gpx.pretty(BR.Export.adoptGpx(text));
+ } else if (format === 'geojson') {
+ text = JSON.stringify(JSON.parse(text), null, 2);
+ }
+ var dmp = new diff_match_patch();
+ var diff = dmp.diff_main(text, track);
+ dmp.diff_cleanupSemantic(diff);
+
+ if (dmp.diff_levenshtein(diff) > 0) {
+ let i = 0;
+ while (i < diff.length - 2) {
+ if (
+ diff[i][0] === 0 &&
+ diff[i + 1][0] === -1 &&
+ diff[i + 2][0] === 1 &&
+ (/(rteTime|rteSpeed)>\d+\.\d{0,2}$/.test(diff[i][1]) || /time=[0-9h ]*m \d$/.test(diff[i][1]))
+ ) {
+ const del = +diff[i + 1][1];
+ const ins = +diff[i + 2][1];
+ if (Number.isInteger(del) && Number.isInteger(ins) && Math.abs(del - ins) === 1) {
+ diff.splice(i + 1, 2);
+ }
+ }
+ i++;
+ }
+ }
+
+ if (dmp.diff_levenshtein(diff) > 0) {
+ //console.log('server: ', text);
+ //console.log('client: ', track);
+ console.log(diff);
+ bootbox.alert(dmp.diff_prettyHtml(diff));
+ } else {
+ console.log('diff equal');
+ }
+ }).bind(this)
+ );
+};
+
+// TODO remove
+// copied from Gpx.test.js
+BR.Export.adoptGpx = function (gpx, replaceCreator = true) {
+ const creator = 'togpx';
+ const name = 'Track';
+ const newline = '\n';
+
+ gpx = gpx.replace(/=\.(?=\d)/, '=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(/\n [^<]*<\/name>/, `\n ${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('\n', '');
+
+ // added
+ gpx = gpx.replace(/>([-.0-9]+?0+)<\//g, (match, p1) => `>${+p1}`); // remove trailing zeros
+ // trunc bc. float precision diffs
+ gpx = gpx.replace(/(rteTime|rteSpeed)>([^<]*)<\//g, (match, p1, p2) => `${p1}>${(+p2).toFixed(3)}`);
+ gpx = gpx.replace(/\n?\s*<\/extensions>\n?\s*/, ''); // ignore (invalid) double tag
+
+ return gpx;
+};
diff --git a/js/format/Gpx.js b/js/format/Gpx.js
index a56d139..9b8593f 100644
--- a/js/format/Gpx.js
+++ b/js/format/Gpx.js
@@ -49,14 +49,25 @@ BR.Gpx = {
comment += ' energy=' + (props['total-energy'] / 3600000).toFixed(1) + 'kwh';
}
if (props['total-time']) {
- // TODO format, e.g. total-time=14833 -> time=4h 7m 13s
- // see brouter OsmTrack.getFormattedTime2
- comment += ' time=' + props['total-time'] + 's';
+ 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;
+ },
+
// modified version of
// https://gist.github.com/sente/1083506#gistcomment-2254622
// MIT License, Copyright (c) 2016 Stuart Powers, ES6 version by Jonathan Gruber
diff --git a/js/format/VoiceHints.js b/js/format/VoiceHints.js
index 3be14c2..6dc3c64 100644
--- a/js/format/VoiceHints.js
+++ b/js/format/VoiceHints.js
@@ -11,21 +11,25 @@
class RoundaboutCommand extends Command {
constructor(command, exitNumber) {
- this.name = command.name + exitNumber;
- this.locus = command.locus + exitNumber;
- this.orux = command.orux + exitNumber;
- this.symbol = command.symbol + exitNumber;
- this.message = command.message + exitNumber;
+ super(
+ command.name + exitNumber,
+ command.locus + exitNumber,
+ command.orux + exitNumber,
+ command.symbol + exitNumber,
+ command.message + exitNumber
+ );
}
}
class RoundaboutLeftCommand extends RoundaboutCommand {
constructor(command, exitNumber) {
- this.name = command.name + -exitNumber;
- this.locus = command.locus + -exitNumber;
- this.orux = command.orux + exitNumber;
- this.symbol = command.symbol + -exitNumber;
- this.message = command.message + -exitNumber;
+ super(
+ command.name + -exitNumber,
+ command.locus + -exitNumber,
+ command.orux + exitNumber,
+ command.symbol + -exitNumber,
+ command.message + -exitNumber
+ );
}
}
@@ -82,36 +86,25 @@
},
_getDuration: function (voicehintsIndex) {
- const timeList = this.track.properties.times;
+ const times = this.track.properties.times;
+ if (!times) return 0;
+
const indexInTrack = this.voicehints[voicehintsIndex][0];
- const currTime = timeList[indexInTrack];
+ const currentTime = times[indexInTrack];
const len = this.voicehints.length;
- const nextIndex = voicehintsIndex < len - 1 ? this.voicehints[voicehintsIndex + 1][0] : timeList.length - 1;
- const nextTime = timeList[nextIndex];
+ const nextIndex = voicehintsIndex < len - 1 ? this.voicehints[voicehintsIndex + 1][0] : times.length - 1;
+ const nextTime = times[nextIndex];
- const duration = nextTime - currTime;
-
- // TODO remove
- const time = this.voicehints[voicehintsIndex][4];
- const p = 5;
- if (!(time.toPrecision(p) === duration.toPrecision(p))) {
- console.error(
- `${voicehintsIndex}: ${time.toPrecision(p)} =? ${duration.toPrecision(p)}, ${time} =? ${duration}`
- );
- }
-
- return duration;
+ return nextTime - currentTime;
},
_loopHints: function (hintCallback) {
if (!this.voicehints) return;
for (const [i, values] of this.voicehints.entries()) {
- const [indexInTrack, commandId, exitNumber, distance, time, angle, geometry] = values;
- const hint = { indexInTrack, commandId, exitNumber, distance, time, angle, geometry };
+ const [indexInTrack, commandId, exitNumber, distance, angle, geometry] = values;
+ const hint = { indexInTrack, commandId, exitNumber, distance, angle, geometry };
- // TODO remove server hint time
- //hint.time = this._getDuration(i);
- this._getDuration(i);
+ hint.time = this._getDuration(i);
if (hint.time > 0) {
hint.speed = distance / hint.time;
}
@@ -229,8 +222,8 @@
extensions['locus:rteDistance'] = hint.distance;
if (hint.time > 0) {
- extensions['locus:rteTime'] = hint.time;
- extensions['locus:rteSpeed'] = hint.speed;
+ extensions['locus:rteTime'] = hint.time.toFixed(3);
+ extensions['locus:rteSpeed'] = hint.speed.toFixed(3);
}
extensions['locus:rtePointAction'] = cmd.locus;
diff --git a/js/index.js b/js/index.js
index aaa7037..264637e 100644
--- a/js/index.js
+++ b/js/index.js
@@ -300,7 +300,7 @@
trackMessages.update(track, segments);
trackAnalysis.update(track, segments);
- exportRoute.update(latLngs);
+ exportRoute.update(latLngs, segments);
}
routing.addTo(map);
diff --git a/tests/control/Export.test.js b/tests/control/Export.test.js
new file mode 100644
index 0000000..5d1f8a4
--- /dev/null
+++ b/tests/control/Export.test.js
@@ -0,0 +1,40 @@
+BR = {};
+require('leaflet');
+turf = require('@turf/turf');
+require('../../js/control/Export.js');
+
+// &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;
+}
+
+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);
+});
diff --git a/tests/control/data/brouterTotal.json b/tests/control/data/brouterTotal.json
new file mode 100644
index 0000000..8ebefc4
--- /dev/null
+++ b/tests/control/data/brouterTotal.json
@@ -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]
+ ]
+ }
+ }
+ ]
+ }
+
\ No newline at end of file
diff --git a/tests/control/data/segments.json b/tests/control/data/segments.json
new file mode 100644
index 0000000..e8d59e2
--- /dev/null
+++ b/tests/control/data/segments.json
@@ -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]
+ ]
+ }
+ }
+}
+]
\ No newline at end of file
diff --git a/tests/format/Gpx.test.js b/tests/format/Gpx.test.js
index d0959d1..7e83640 100644
--- a/tests/format/Gpx.test.js
+++ b/tests/format/Gpx.test.js
@@ -47,6 +47,11 @@ describe('voice hints', () => {
let brouterGpx = read('2-locus.gpx');
brouterGpx = brouterGpx.replace(/.0<\/locus:rteDistance/g, '\n\s*/, ''); // 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);
diff --git a/tests/format/data/track.json b/tests/format/data/track.json
index c39541e..b2c989c 100644
--- a/tests/format/data/track.json
+++ b/tests/format/data/track.json
@@ -13,8 +13,8 @@
"total-energy": "4412",
"cost": "533",
"voicehints": [
- [1,5,0,140.0,24.90994644165039,89," 6(90)6 (0)6 (-89)2"],
- [5,2,0,90.0,9.614852905273438,-90," 6(-89)6 (0)6 (89)6"]
+ [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"],