abrensch/brouter@c9ae7c8681 added support for two new voice hints: A hint for beelines (`BL`), and a hint for 180 degree u-turns (`TU`). By adding support for both, we now know about all types of voice hints as defined in BRouter again. What makes things confusing is that the `TU` name for the respective `static final int` constant in BRouter's `VoiceHint.java` was repurposed for 180 degree u-turns, with left u-turns now being mapped to the new `TLU` constant name. Also note that originally the indexing of voice hints as used in BRouter's GeoJSON API has been changed as well due to inserting new commands in the middle of the numbering scheme instead of at the end. This API break has been fixed only in abrensch/brouter@82fecf9. Here we will rely on the fixed indexing, BRouter versions 1.7.0 and 1.7.1 without the re-indexing revert will not be supported. In addition, the voice hint mapping table has been checked to be identical to BRouter (this led to adding a missing `OFFR` symbol), and clarifying comments for planned future changes (e.g. changing the `TU` output token to `TLU` for OsmAnd) have been added. Note that beelines and 180 degree u-turns are only added to the mapping table for completeness. As BRouter-Web is handling straight lines on the client-side exclusively (which makes sense performance-wise when loading a route from a pasted URL with lots of them tracing an unmapped path), they are not expected to be in any GeoJSON response from BRouter, at least as of now. The same is true for 180 degree u-turn voice hints at cul-de-sac-style vias. If and when to emit voice hints for both cases in BRouter-Web itself is a different question, though it could likely also use the table for lookup. Test Plan: - `yarn test` - Confirm voice hints for routes with roundabouts and u-turns are unchanged.
381 lines
13 KiB
JavaScript
381 lines
13 KiB
JavaScript
(function () {
|
|
class Command {
|
|
constructor(name, locus, orux, symbol, fit, message) {
|
|
this.name = name;
|
|
this.locus = locus;
|
|
this.orux = orux;
|
|
this.symbol = symbol;
|
|
(this.fit = fit), (this.message = message);
|
|
}
|
|
}
|
|
|
|
class RoundaboutCommand extends Command {
|
|
constructor(command, exitNumber) {
|
|
super(
|
|
command.name + exitNumber,
|
|
command.locus + exitNumber,
|
|
command.orux + exitNumber,
|
|
command.symbol + exitNumber,
|
|
command.fit,
|
|
command.message + exitNumber
|
|
);
|
|
}
|
|
}
|
|
|
|
class RoundaboutLeftCommand extends Command {
|
|
constructor(command, exitNumber) {
|
|
super(
|
|
command.name + -exitNumber,
|
|
command.locus + -exitNumber,
|
|
command.orux + exitNumber,
|
|
command.symbol + -exitNumber,
|
|
command.fit,
|
|
command.message + -exitNumber
|
|
);
|
|
}
|
|
}
|
|
|
|
class VoiceHints {
|
|
constructor(geoJson, turnInstructionMode, transportMode) {
|
|
this.geoJson = geoJson;
|
|
this.turnInstructionMode = turnInstructionMode;
|
|
this.transportMode = transportMode;
|
|
|
|
this.track = geoJson.features?.[0];
|
|
this.voicehints = this.track?.properties?.voicehints;
|
|
}
|
|
|
|
getGpxTransform() {
|
|
const transform = {
|
|
comment: '',
|
|
trk: function (trk, feature, coordsList) {
|
|
const properties = this._getTrk();
|
|
return Object.assign(properties, trk);
|
|
}.bind(this),
|
|
};
|
|
|
|
this._addToTransform(transform);
|
|
|
|
return transform;
|
|
}
|
|
|
|
_getDuration(voicehintsIndex) {
|
|
const times = this.track.properties.times;
|
|
if (!times) return 0;
|
|
|
|
const indexInTrack = this.voicehints[voicehintsIndex][0];
|
|
const currentTime = times[indexInTrack];
|
|
const len = this.voicehints.length;
|
|
const nextIndex = voicehintsIndex < len - 1 ? this.voicehints[voicehintsIndex + 1][0] : times.length - 1;
|
|
const nextTime = times[nextIndex];
|
|
|
|
return nextTime - currentTime;
|
|
}
|
|
|
|
_loopHints(hintCallback) {
|
|
if (!this.voicehints) return;
|
|
for (const [i, values] of this.voicehints.entries()) {
|
|
const [indexInTrack, commandId, exitNumber, distance, angle, geometry] = values;
|
|
const hint = { indexInTrack, commandId, exitNumber, distance, angle, geometry };
|
|
|
|
hint.time = this._getDuration(i);
|
|
if (hint.time > 0) {
|
|
hint.speed = distance / hint.time;
|
|
}
|
|
|
|
const coord = this.track.geometry.coordinates[indexInTrack];
|
|
const cmd = this.getCommand(commandId, exitNumber);
|
|
if (!cmd) {
|
|
console.error(`no voicehint command for id: ${commandId} (${values})`);
|
|
continue;
|
|
}
|
|
|
|
hintCallback(hint, cmd, coord);
|
|
}
|
|
}
|
|
|
|
getCommand(id, exitNumber) {
|
|
let command = VoiceHints.commands[id];
|
|
if (id === 13) {
|
|
command = new RoundaboutCommand(command, exitNumber);
|
|
} else if (id === 14) {
|
|
command = new RoundaboutLeftCommand(command, exitNumber);
|
|
}
|
|
return command;
|
|
}
|
|
|
|
// override in subclass
|
|
_addToTransform(transform) {}
|
|
|
|
// override in subclass
|
|
_getTrk() {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// from BRouter btools.router.VoiceHint
|
|
VoiceHints.commands = (function () {
|
|
return {
|
|
// Command(name, locus, orux, symbol, fit, message)
|
|
1: new Command('C', 1, 1002, 'Straight', 'straight', 'straight'),
|
|
2: new Command('TL', 4, 1000, 'Left', 'left', 'left'),
|
|
3: new Command('TSLL', 3, 1017, 'TSLL', 'slight_left', 'slight left'),
|
|
4: new Command('TSHL', 5, 1019, 'TSHL', 'sharp_left', 'sharp left'),
|
|
5: new Command('TR', 7, 1001, 'Right', 'right', 'right'),
|
|
6: new Command('TSLR', 6, 1016, 'TSLR', 'slight_right', 'slight right'),
|
|
7: new Command('TSHR', 8, 1018, 'TSHR', 'sharp_right', 'sharp right'),
|
|
8: new Command('KL', 9, 1015, 'TSLL', 'left_fork', 'keep left'),
|
|
9: new Command('KR', 10, 1014, 'TSLR', 'right_fork', 'keep right'),
|
|
// According to getCommandString() in BRouter, Command.name==TU for index TLU
|
|
// "should be changed to TLU when osmand uses new voice hint constants"
|
|
// According to getMessageString() in BRouter, Command.message==u-turn for index TLU
|
|
// "should be changed to u-turn-left when osmand uses new voice hint constants"
|
|
10: new Command('TU', 13, 1003, 'TU', 'u_turn', 'u-turn'), // Left U-turn
|
|
// According to getMessageString() in BRouter, Command.message==u-turn for index TRU
|
|
// "should be changed to u-turn-right when osmand uses new voice hint constants"
|
|
11: new Command('TRU', 14, 1003, 'TU', 'u_turn', 'u-turn'), // Right U-turn
|
|
12: new Command('OFFR', undefined, undefined, 'OFFR', 'danger', undefined), // Off route
|
|
13: new Command('RNDB', 26, 1008, 'RNDB', 'generic', 'Take exit '), // Roundabout
|
|
14: new Command('RNLB', 26, 1008, 'RNLB', 'generic', 'Take exit '), // Roundabout left
|
|
15: new Command('TU', 12, 1003, 'TU', 'u_turn', 'u-turn'), // 180 degree u-turn
|
|
16: new Command('BL', undefined, undefined, 'BL', 'danger', undefined), // Beeline
|
|
};
|
|
})();
|
|
|
|
class WaypointVoiceHints extends VoiceHints {
|
|
_addToTransform(transform) {
|
|
transform.gpx = function (gpx, features) {
|
|
this._addWaypoints(gpx);
|
|
return gpx;
|
|
}.bind(this);
|
|
}
|
|
|
|
_addWaypoints(gpx) {
|
|
const waypoints = [];
|
|
this._loopHints((hint, cmd, coord) => {
|
|
const properties = this._getWpt(hint, cmd, coord);
|
|
|
|
const wpt = this._createWpt(coord, properties);
|
|
waypoints.push(wpt);
|
|
});
|
|
gpx.wpt.unshift(...waypoints);
|
|
}
|
|
|
|
_createWpt(coord, properties) {
|
|
return Object.assign(
|
|
{
|
|
'@lat': coord[1],
|
|
'@lon': coord[0],
|
|
},
|
|
properties
|
|
);
|
|
}
|
|
|
|
// override in subclass
|
|
_getWpt(hint, cmd, coord) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
class GpsiesVoiceHints extends WaypointVoiceHints {
|
|
_getWpt(hint, cmd, coord) {
|
|
return { name: cmd.message, sym: cmd.symbol.toLowerCase(), type: cmd.symbol };
|
|
}
|
|
}
|
|
|
|
class OruxVoiceHints extends WaypointVoiceHints {
|
|
_getWpt(hint, cmd, coord) {
|
|
const wpt = {
|
|
ele: coord[2],
|
|
extensions: {
|
|
'om:oruxmapsextensions': {
|
|
'@xmlns:om': 'http://www.oruxmaps.com/oruxmapsextensions/1/0',
|
|
'om:ext': { '@type': 'ICON', '@subtype': 0, _: cmd.orux },
|
|
},
|
|
},
|
|
};
|
|
|
|
if (wpt.ele === undefined || wpt.ele === null) {
|
|
delete wpt.ele;
|
|
}
|
|
|
|
return wpt;
|
|
}
|
|
}
|
|
|
|
class LocusVoiceHints extends WaypointVoiceHints {
|
|
_addToTransform(transform) {
|
|
transform.gpx = function (gpx, features) {
|
|
// 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
|
|
);
|
|
|
|
this._addWaypoints(gpx);
|
|
|
|
return gpx;
|
|
}.bind(this);
|
|
}
|
|
|
|
_getWpt(hint, cmd, coord) {
|
|
const extensions = {};
|
|
|
|
extensions['locus:rteDistance'] = hint.distance;
|
|
if (hint.time > 0) {
|
|
extensions['locus:rteTime'] = hint.time.toFixed(3);
|
|
extensions['locus:rteSpeed'] = hint.speed.toFixed(3);
|
|
}
|
|
extensions['locus:rtePointAction'] = cmd.locus;
|
|
|
|
const wpt = {
|
|
ele: coord[2],
|
|
name: cmd.message,
|
|
extensions: extensions,
|
|
};
|
|
|
|
if (wpt.ele === undefined || wpt.ele === null) {
|
|
delete wpt.ele;
|
|
}
|
|
|
|
return wpt;
|
|
}
|
|
|
|
_getTrk() {
|
|
return {
|
|
extensions: {
|
|
'locus:rteComputeType': this._getLocusRouteType(this.transportMode),
|
|
'locus:rteSimpleRoundabouts': 1,
|
|
},
|
|
};
|
|
}
|
|
|
|
_getLocusRouteType(transportMode) {
|
|
switch (transportMode) {
|
|
case 'car':
|
|
return 0;
|
|
case 'bike':
|
|
return 5;
|
|
default:
|
|
return 3; // foot
|
|
}
|
|
}
|
|
}
|
|
|
|
class CommentVoiceHints extends VoiceHints {
|
|
_addToTransform(transform) {
|
|
let comment = `
|
|
<!-- $transport-mode$${this.transportMode}$ -->
|
|
<!-- cmd idx lon lat d2next geometry -->
|
|
<!-- $turn-instruction-start$`;
|
|
|
|
this._loopHints((hint, cmd, coord) => {
|
|
const pad = (obj = '', len) => {
|
|
return new String(obj).padStart(len) + ';';
|
|
};
|
|
|
|
let turn = '';
|
|
turn += pad(cmd.name, 6);
|
|
turn += pad(hint.indexInTrack, 6);
|
|
turn += pad(coord[0], 10);
|
|
turn += pad(coord[1], 10);
|
|
turn += pad(hint.distance, 6);
|
|
turn += hint.geometry;
|
|
|
|
comment += `
|
|
$turn$${turn}$`;
|
|
});
|
|
|
|
comment += `
|
|
$turn-instruction-end$ -->
|
|
`;
|
|
|
|
transform.comment = comment;
|
|
}
|
|
}
|
|
|
|
class OsmAndVoiceHints extends VoiceHints {
|
|
_addToTransform(transform) {
|
|
transform.gpx = function (gpx, features) {
|
|
gpx['@creator'] = 'OsmAndRouter';
|
|
|
|
gpx.rte.push({
|
|
rtept: this._createRoutePoints(gpx),
|
|
});
|
|
|
|
return gpx;
|
|
}.bind(this);
|
|
}
|
|
|
|
_createRoutePoints(gpx) {
|
|
const rteptList = [];
|
|
|
|
const trkseg = gpx.trk[0].trkseg[0];
|
|
let trkpt = trkseg.trkpt[0];
|
|
const startPt = {
|
|
'@lat': trkpt['@lat'],
|
|
'@lon': trkpt['@lon'],
|
|
desc: 'start',
|
|
};
|
|
const times = this.track?.properties?.times;
|
|
if (this.voicehints && times) {
|
|
const startTime = times[this.voicehints[0][0]];
|
|
startPt.extensions = { time: Math.round(startTime), offset: 0 };
|
|
}
|
|
rteptList.push(startPt);
|
|
|
|
this._loopHints((hint, cmd, coord) => {
|
|
const rtept = {
|
|
'@lat': coord[1],
|
|
'@lon': coord[0],
|
|
desc: cmd.message,
|
|
extensions: {
|
|
time: Math.round(hint.time),
|
|
turn: cmd.name,
|
|
'turn-angle': hint.angle,
|
|
offset: hint.indexInTrack,
|
|
},
|
|
};
|
|
|
|
if (hint.time === 0) {
|
|
delete properties.extensions.time;
|
|
}
|
|
|
|
rteptList.push(rtept);
|
|
});
|
|
|
|
const lastIndex = trkseg.trkpt.length - 1;
|
|
trkpt = trkseg.trkpt[lastIndex];
|
|
rteptList.push({
|
|
'@lat': trkpt['@lat'],
|
|
'@lon': trkpt['@lon'],
|
|
desc: 'destination',
|
|
extensions: { time: 0, offset: lastIndex },
|
|
});
|
|
|
|
return rteptList;
|
|
}
|
|
}
|
|
|
|
BR.voiceHints = function (geoJson, turnInstructionMode, transportMode) {
|
|
switch (turnInstructionMode) {
|
|
case 2:
|
|
return new LocusVoiceHints(geoJson, turnInstructionMode, transportMode);
|
|
case 3:
|
|
return new OsmAndVoiceHints(geoJson, turnInstructionMode, transportMode);
|
|
case 4:
|
|
return new CommentVoiceHints(geoJson, turnInstructionMode, transportMode);
|
|
case 5:
|
|
return new GpsiesVoiceHints(geoJson, turnInstructionMode, transportMode);
|
|
case 6:
|
|
return new OruxVoiceHints(geoJson, turnInstructionMode, transportMode);
|
|
default:
|
|
console.error('unhandled turnInstructionMode: ' + turnInstructionMode);
|
|
return new VoiceHints(geoJson, turnInstructionMode, transportMode);
|
|
}
|
|
};
|
|
})();
|