From fd0ece0d312bbc7e0613bb1222e685db0dcd1187 Mon Sep 17 00:00:00 2001 From: Norbert Renner Date: Wed, 24 Feb 2021 19:52:45 +0100 Subject: [PATCH] Format Locus voice hint, with additional hint data --- js/format/Gpx.js | 43 +++++++------- js/format/VoiceHints.js | 104 ++++++++++++++++++++++++++++++---- tests/format/Gpx.test.js | 14 ++++- tests/format/data/2-locus.gpx | 25 ++++++++ tests/format/data/track.json | 5 +- 5 files changed, 152 insertions(+), 39 deletions(-) create mode 100644 tests/format/data/2-locus.gpx diff --git a/js/format/Gpx.js b/js/format/Gpx.js index 63afb2f..b7eddcc 100644 --- a/js/format/Gpx.js +++ b/js/format/Gpx.js @@ -1,25 +1,29 @@ BR.Gpx = { - format: function (geoJson, turnInstructionMode = 0) { - if (!geoJson) return ''; + format: function (geoJson, turnInstructionMode = 0, transportMode = 'bike') { + if (!geoJson?.features) return ''; + + const trkNameTransform = { + trk: function (trk, feature, coordsList) { + // name as first tag, by using assign and in this order + return Object.assign( + { + name: feature.properties.name, + }, + trk + ); + }, + }; + let gpxTransform = trkNameTransform; if (turnInstructionMode > 0) { - BR.Gpx._addVoiceHints(geoJson, turnInstructionMode); + const voiceHints = BR.voiceHints(geoJson); + gpxTransform = voiceHints.getGpxTransform(turnInstructionMode, transportMode); } let gpx = togpx(geoJson, { featureTitle: function () {}, featureDescription: function () {}, - transform: { - trk: function (trk, feature, coordsList) { - return { - name: feature.properties.name, - trkseg: trk.trkseg, - }; - }, - wpt: function (wpt, feature, coord, index) { - return Object.assign(wpt, feature.properties); - }, - }, + transform: gpxTransform, }); const statsComment = BR.Gpx._statsComment(geoJson); gpx = '' + statsComment + gpx; @@ -27,13 +31,6 @@ BR.Gpx = { return gpx; }, - _addVoiceHints: function (geoJson, turnInstructionMode) { - if (!geoJson.features) return; - - const voiceHints = BR.voiceHints(geoJson); - voiceHints.add(turnInstructionMode); - }, - // _statsComment: function (geoJson) { const props = geoJson.features?.[0].properties; @@ -68,8 +65,8 @@ BR.Gpx = { // Remove all the newlines and then remove all the spaces between tags xml = xml.replace(/\s*(\r\n|\n|\r)\s*/gm, ' ').replace(/>\s+<'); - // break into lines, keep trkpt with subelement ele in single line - const reg = /(>)(<)(?!ele|\/trkpt)(\/?)/g; + // break into lines + const reg = /(>)(<)(\/?)/g; let pad = 0; xml = xml.replace('', ''); diff --git a/js/format/VoiceHints.js b/js/format/VoiceHints.js index 8321e89..1fcbfb6 100644 --- a/js/format/VoiceHints.js +++ b/js/format/VoiceHints.js @@ -9,7 +9,7 @@ } } - class RoundaboutCommand { + class RoundaboutCommand extends Command { constructor(command, exitNumber) { this.name = command.name + exitNumber; this.locus = command.locus + exitNumber; @@ -19,7 +19,7 @@ } } - class RoundaboutLeftCommand { + class RoundaboutLeftCommand extends RoundaboutCommand { constructor(command, exitNumber) { this.name = command.name + -exitNumber; this.locus = command.locus + -exitNumber; @@ -64,17 +64,99 @@ } }, - add: function (turnInstructionMode) { - if (!this.voicehints) return; + createWpt: function (coord, properties) { + return Object.assign( + { + '@lat': coord[1], + '@lon': coord[0], + }, + properties + ); + }, - for (const hint of this.voicehints) { - const [indexInTrack, commandId, exitNumber] = hint; - const coord = this.track.geometry.coordinates[indexInTrack]; - const cmd = this.getCommand(commandId, exitNumber); - const properties = { name: cmd.message, sym: cmd.symbol.toLowerCase(), type: cmd.symbol }; + getGpxTransform: function (turnInstructionMode, transportMode) { + const mode = turnInstructionMode; + const transform = { + gpx: function (gpx, features) { + for (const hint of this.voicehints) { + const [indexInTrack, commandId, exitNumber, distance, time] = hint; + let speed; + if (time > 0) { + speed = distance / time; + } - this.geoJson.features.push(turf.point(coord.slice(0, 2), properties)); - } + const coord = this.track.geometry.coordinates[indexInTrack]; + const cmd = this.getCommand(commandId, exitNumber); + if (!cmd) { + console.error(`no voicehint command for id: ${commandId} (${hint})`); + continue; + } + + let properties; + if (mode === 2) { + const extensions = {}; + // TODO 'locus:' namespace gets removed + extensions['locus:rteDistance'] = distance; + if (time > 0) { + extensions['locus:rteTime'] = time; + extensions['locus:rteSpeed'] = speed; + } + extensions['locus:rtePointAction'] = cmd.locus; + + properties = { + ele: coord[2], + name: cmd.message, + extensions: extensions, + }; + } else if (mode === 5) { + properties = { name: cmd.message, sym: cmd.symbol.toLowerCase(), type: cmd.symbol }; + } else { + console.error('unhandled turnInstructionMode: ' + mode); + } + + const wpt = this.createWpt(coord, properties); + gpx.wpt.push(wpt); + } + + if (mode === 2) { + // 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 + ); + } + return gpx; + }.bind(this), + trk: function (trk, feature, coordsList) { + const properties = { + name: feature.properties.name, + }; + + const getLocusRouteType = (transportMode) => { + switch (transportMode) { + case 'car': + return 0; + case 'bike': + return 5; + default: + return 3; // foot + } + }; + + if (mode === 2) { + properties.extensions = { + 'locus:rteComputeType': getLocusRouteType(transportMode), + 'locus:rteSimpleRoundabouts': 1, + }; + } + return Object.assign(properties, trk); + }, + }; + return transform; }, getCommand: function (id, exitNumber) { diff --git a/tests/format/Gpx.test.js b/tests/format/Gpx.test.js index 2b3e232..e6ea31b 100644 --- a/tests/format/Gpx.test.js +++ b/tests/format/Gpx.test.js @@ -31,7 +31,7 @@ function adoptGpx(gpx) { } function read(fileName) { - return adoptGpx(fs.readFileSync(path + fileName, 'utf8')); + return BR.Gpx.pretty(adoptGpx(fs.readFileSync(path + fileName, 'utf8'))); } test('simple track', () => { @@ -41,8 +41,18 @@ test('simple track', () => { }); describe('voice hints', () => { + test('2-locus', () => { + let brouterGpx = read('2-locus.gpx'); + brouterGpx = brouterGpx.replace(/<(\/?)locus:/g, '<$1'); // TODO 'locus:' namespace + brouterGpx = brouterGpx.replace(/.0<\/rteDistance/g, '\n\s*/, ''); // ignore (invalid) double tag + + const gpx = BR.Gpx.format(geoJson, 2); + expect(gpx).toEqual(brouterGpx); + }); + test('5-gpsies', () => { - const brouterGpx = BR.Gpx.pretty(read('5-gpsies.gpx')); + const brouterGpx = read('5-gpsies.gpx'); const gpx = BR.Gpx.format(geoJson, 5); expect(gpx).toEqual(brouterGpx); }); diff --git a/tests/format/data/2-locus.gpx b/tests/format/data/2-locus.gpx new file mode 100644 index 0000000..c90df93 --- /dev/null +++ b/tests/format/data/2-locus.gpx @@ -0,0 +1,25 @@ + + + + 101.5right140.024.909946441650395.6202449221614787 + 103.5left90.09.6148529052734389.3605176165137064 + + 2-locus + 5 + 1 + + 101.5 + 101.5 + 101.5 + 101.5 + 101.75 + 103.5 + 99.75 + + + diff --git a/tests/format/data/track.json b/tests/format/data/track.json index 6c8902f..fd1c324 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], - [5,2,0] + [1,5,0,140.0,24.90994644165039], + [5,2,0,90.0,9.614852905273438] ], "messages": [ ["Longitude", "Latitude", "Elevation", "Distance", "CostPerKm", "ElevCost", "TurnCost", "NodeCost", "InitialCost", "WayTags", "NodeTags"], @@ -37,4 +37,3 @@ } ] } -