Parse voicehint modes form profile

This commit is contained in:
Norbert Renner 2021-03-16 19:56:02 +01:00
parent 25f8828ae7
commit 2189d68af9
7 changed files with 336 additions and 98 deletions

View file

@ -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)]);
};
// <script src="https://unpkg.com/googlediff@0.1.0/javascript/diff_match_patch.js"></script>
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(/<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('</gpx>\n', '</gpx>');
// 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*<extensions>/, ''); // ignore (invalid) double tag
return gpx;
};

View file

@ -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();
},
});

View file

@ -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();
}

View file

@ -21,7 +21,7 @@
}
}
class RoundaboutLeftCommand extends RoundaboutCommand {
class RoundaboutLeftCommand extends Command {
constructor(command, exitNumber) {
super(
command.name + -exitNumber,

View file

@ -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();

124
js/util/Diff.js Normal file
View file

@ -0,0 +1,124 @@
BR.Diff = {};
// <script src="https://unpkg.com/googlediff@0.1.0/javascript/diff_match_patch.js"></script>
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_gt = />/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, '&amp;')
.replace(pattern_lt, '&lt;')
.replace(pattern_gt, '&gt;')
//.replace(pattern_para, '&para;<br>');
.replace(pattern_para, '<br>');
switch (op) {
case DIFF_INSERT:
html[x] = '<ins style="background:#e6ffe6;">' + text + '</ins>';
break;
case DIFF_DELETE:
html[x] = '<del style="background:#ffe6e6;">' + text + '</del>';
break;
case DIFF_EQUAL:
const lines = text.split('<br>');
const len = lines.length;
if (len > contextLen * 2) {
text = [...lines.slice(0, contextLen), '...', ...lines.slice(-contextLen)].join('<br>');
}
html[x] = '<span>' + text + '</span>';
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(/<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
// remove trailing zeros comment-style voicehints
gpx = gpx.replace(/;\s*([-0-9]+.[0-9]+?)0+;/g, (match, p1) => `;${p1.padStart(10)};`);
gpx = gpx.replace('</gpx>\n', '</gpx>');
// 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*<extensions>/, ''); // ignore (invalid) double tag
return gpx;
};

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