From 2189d68af91b1ab1dd7126fcbe3bb9d5a4f00693 Mon Sep 17 00:00:00 2001 From: Norbert Renner Date: Tue, 16 Mar 2021 19:56:02 +0100 Subject: [PATCH] Parse voicehint modes form profile --- js/control/Export.js | 90 ++------------------- js/control/Profile.js | 66 ++++++++++++--- js/format/Gpx.js | 2 +- js/format/VoiceHints.js | 2 +- js/index.js | 2 +- js/util/Diff.js | 124 ++++++++++++++++++++++++++++ tests/control/Profile.test.js | 148 ++++++++++++++++++++++++++++++++++ 7 files changed, 336 insertions(+), 98 deletions(-) create mode 100644 js/util/Diff.js create mode 100644 tests/control/Profile.test.js diff --git a/js/control/Export.js b/js/control/Export.js index b9645f2..64f8337 100644 --- a/js/control/Export.js +++ b/js/control/Export.js @@ -7,9 +7,10 @@ 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; @@ -61,7 +62,8 @@ BR.Export = L.Class.extend({ //} else { const track = this._formatTrack(format, name, includeWaypoints); - BR.Export.diff(uri, track, format); + console.log('track: ', track); + BR.Diff.diff(uri, track, format); } }, @@ -71,8 +73,9 @@ BR.Export = L.Class.extend({ //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 + const turnInstructionMode = +this.profile.getProfileVar('turnInstructionMode'); + const transportMode = this.profile.getTransportMode(); + return BR.Gpx.format(track, turnInstructionMode, transportMode); case 'geojson': return JSON.stringify(track, null, 2); case 'kml': @@ -236,82 +239,3 @@ BR.Export._concatTotalTrack = function (segments) { 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}([^<]*)<\//g, (match, p1, p2) => `${p1}>${(+p2).toFixed(3)}\n?\s*/, ''); // ignore (invalid) double tag - - return gpx; -}; diff --git a/js/control/Profile.js b/js/control/Profile.js index e380d56..8de56bc 100644 --- a/js/control/Profile.js +++ b/js/control/Profile.js @@ -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(); + }, }); diff --git a/js/format/Gpx.js b/js/format/Gpx.js index 9b8593f..89772f4 100644 --- a/js/format/Gpx.js +++ b/js/format/Gpx.js @@ -16,7 +16,7 @@ BR.Gpx = { }; let gpxTransform = trkNameTransform; - if (turnInstructionMode > 0) { + if (turnInstructionMode > 1) { const voiceHints = BR.voiceHints(geoJson, turnInstructionMode, transportMode); gpxTransform = voiceHints.getGpxTransform(); } diff --git a/js/format/VoiceHints.js b/js/format/VoiceHints.js index 6dc3c64..d60cda3 100644 --- a/js/format/VoiceHints.js +++ b/js/format/VoiceHints.js @@ -21,7 +21,7 @@ } } - class RoundaboutLeftCommand extends RoundaboutCommand { + class RoundaboutLeftCommand extends Command { constructor(command, exitNumber) { super( command.name + -exitNumber, diff --git a/js/index.js b/js/index.js index 264637e..b9a4dd5 100644 --- a/js/index.js +++ b/js/index.js @@ -262,7 +262,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(); diff --git a/js/util/Diff.js b/js/util/Diff.js new file mode 100644 index 0000000..a1c4a95 --- /dev/null +++ b/js/util/Diff.js @@ -0,0 +1,124 @@ +BR.Diff = {}; + +// +BR.Diff.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.Diff.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); + if (i + 1 < diff.length && diff[i + 1][0] === 0) { + diff[i + 1][1] = diff[i][1] + diff[i + 1][1]; + diff.splice(i, 1); + continue; + } + } + } + i++; + } + } + + if (dmp.diff_levenshtein(diff) > 0) { + //console.log('server: ', text); + //console.log('client: ', track); + console.log(diff); + bootbox.alert(BR.Diff.diffPrettyHtml(diff)); + } else { + console.log('diff equal'); + } + }).bind(this) + ); +}; + +// diff_match_patch.prototype.diff_prettyHtml modified to only show specified number of context lines +BR.Diff.diffPrettyHtml = function (diffs, contextLen = 2) { + var html = []; + var pattern_amp = /&/g; + var pattern_lt = //g; + var pattern_para = /\n/g; + for (var x = 0; x < diffs.length; x++) { + var op = diffs[x][0]; // Operation (insert, delete, equal) + var data = diffs[x][1]; // Text of change. + var text = data + .replace(pattern_amp, '&') + .replace(pattern_lt, '<') + .replace(pattern_gt, '>') + //.replace(pattern_para, '¶
'); + .replace(pattern_para, '
'); + switch (op) { + case DIFF_INSERT: + html[x] = '' + text + ''; + break; + case DIFF_DELETE: + html[x] = '' + text + ''; + break; + case DIFF_EQUAL: + const lines = text.split('
'); + const len = lines.length; + if (len > contextLen * 2) { + text = [...lines.slice(0, contextLen), '...', ...lines.slice(-contextLen)].join('
'); + } + + html[x] = '' + text + ''; + break; + } + } + return html.join(''); +}; + +// TODO remove +// copied from Gpx.test.js +BR.Diff.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="(?!OsmAndRouter)[^"]*"/, `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 + // remove trailing zeros comment-style voicehints + gpx = gpx.replace(/;\s*([-0-9]+.[0-9]+?)0+;/g, (match, p1) => `;${p1.padStart(10)};`); + gpx = gpx.replace('\n', ''); + + // added + gpx = gpx.replace(/>([-.0-9]+?0+)<\//g, (match, p1) => `>${+p1}([^<]*)<\//g, (match, p1, p2) => `${p1}>${(+p2).toFixed(3)}\n?\s*/, ''); // ignore (invalid) double tag + + return gpx; +}; diff --git a/tests/control/Profile.test.js b/tests/control/Profile.test.js new file mode 100644 index 0000000..2e216b1 --- /dev/null +++ b/tests/control/Profile.test.js @@ -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 = ` + + + +`; + +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/); +});