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}`); // 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/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}`); // 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/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/);
+});