Merge pull request #508 from afischerdev/update-lib-five

Update lib part five - change voicehints
This commit is contained in:
afischerdev 2023-02-25 12:18:41 +01:00 committed by GitHub
commit b735cd3e4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 459 additions and 58 deletions

View file

@ -121,11 +121,26 @@ abstract class OsmPath implements OsmLinkHolder {
protected abstract void resetState();
static int seg = 1;
protected void addAddionalPenalty(OsmTrack refTrack, boolean detailMode, OsmPath origin, OsmLink link, RoutingContext rc) {
byte[] description = link.descriptionBitmap;
if (description == null) {
return; // could be a beeline path
if (description == null) { // could be a beeline path
message = new MessageData();
if (message != null) {
message.turnangle = 0;
message.time = (float) 1;
message.energy = (float) 0;
message.priorityclassifier = 0;
message.classifiermask = 0;
message.lon = targetNode.getILon();
message.lat = targetNode.getILat();
message.ele = Short.MIN_VALUE;
message.linkdist = sourceNode.calcDistance(targetNode);
message.wayKeyValues = "direct_segment=" + seg;
}
seg++;
return;
}
boolean recordTransferNodes = detailMode || rc.countTraffic;
@ -208,16 +223,18 @@ abstract class OsmPath implements OsmLinkHolder {
int lon2;
int lat2;
short ele2;
short originEle2;
if (transferNode == null) {
lon2 = targetNode.ilon;
lat2 = targetNode.ilat;
ele2 = targetNode.selev;
originEle2 = targetNode.selev;
} else {
lon2 = transferNode.ilon;
lat2 = transferNode.ilat;
ele2 = transferNode.selev;
originEle2 = transferNode.selev;
}
ele2 = originEle2;
boolean isStartpoint = lon0 == -1 && lat0 == -1;
@ -334,13 +351,13 @@ abstract class OsmPath implements OsmLinkHolder {
message.classifiermask = classifiermask;
message.lon = lon2;
message.lat = lat2;
message.ele = ele2;
message.ele = originEle2;
message.wayKeyValues = rc.expctxWay.getKeyValueDescription(isReverse, description);
}
if (stopAtEndpoint) {
if (recordTransferNodes) {
originElement = OsmPathElement.create(rc.ilonshortest, rc.ilatshortest, ele2, originElement, rc.countTraffic);
originElement = OsmPathElement.create(rc.ilonshortest, rc.ilatshortest, originEle2, originElement, rc.countTraffic);
originElement.cost = cost;
if (message != null) {
originElement.message = message;
@ -366,7 +383,7 @@ abstract class OsmPath implements OsmLinkHolder {
transferNode = transferNode.next;
if (recordTransferNodes) {
originElement = OsmPathElement.create(lon2, lat2, ele2, originElement, rc.countTraffic);
originElement = OsmPathElement.create(lon2, lat2, originEle2, originElement, rc.countTraffic);
originElement.cost = cost;
originElement.addTraffic(traffic);
traffic = 0;

View file

@ -1080,7 +1080,9 @@ public final class OsmTrack {
VoiceHintProcessor vproc = new VoiceHintProcessor(rc.turnInstructionCatchingRange, rc.turnInstructionRoundabouts);
List<VoiceHint> results = vproc.process(inputs);
for (VoiceHint hint : results) {
double minDistance = getMinDistance();
List < VoiceHint > resultsLast = vproc.postProcess(results, rc.turnInstructionCatchingRange, minDistance);
for (VoiceHint hint: resultsLast) {
voiceHints.list.add(hint);
}

View file

@ -297,7 +297,7 @@ public class RoutingEngine extends Thread {
int ind = s.indexOf("%");
if (ind != -1)
s = s.substring(0, ind);
ind = s.indexOf("<EFBFBD>");
ind = s.indexOf("°");
if (ind != -1)
s = s.substring(0, ind);
tmpincline = Double.parseDouble(s.trim());
@ -313,7 +313,7 @@ public class RoutingEngine extends Thread {
if (startincline == 0) {
startincline = tmpincline;
} else if (startincline < 0 && tmpincline > 0) {
// for the way ?p find the exit point
// for the way up find the exit point
double diff = endElev - selev;
tmpincline = diff / (distRest / 100.);
}
@ -733,7 +733,7 @@ public class RoutingEngine extends Thread {
lon2 = t.nodes.get(i).getILon();
lat2 = t.nodes.get(i).getILat();
nLast = t.nodes.get(0);
dist = routingContext.calcDistance(lon0, lat0, lon1, lat1);
dist = nLast.calcDistance(n);
} else {
lon0 = t.nodes.get(i - 2).getILon();
lat0 = t.nodes.get(i - 2).getILat();
@ -742,7 +742,7 @@ public class RoutingEngine extends Thread {
lon2 = t.nodes.get(i).getILon();
lat2 = t.nodes.get(i).getILat();
nLast = t.nodes.get(i - 1);
dist = routingContext.calcDistance(lon1, lat1, lon2, lat2);
dist = nLast.calcDistance(n);
}
angle = routingContext.anglemeter.calcAngle(lon0, lat0, lon1, lat1, lon2, lat2);
n.message.linkdist = dist;

View file

@ -19,11 +19,13 @@ public class VoiceHint {
static final int TSHR = 7; // turn sharply right
static final int KL = 8; // keep left
static final int KR = 9; // keep right
static final int TU = 10; // U-turn
static final int TRU = 11; // Right U-turn
static final int OFFR = 12; // Off route
static final int RNDB = 13; // Roundabout
static final int RNLB = 14; // Roundabout left
static final int TLU = 10; // U-turn
static final int TU = 11; // 180 degree u-turn
static final int TRU = 12; // Right U-turn
static final int OFFR = 13; // Off route
static final int RNDB = 14; // Roundabout
static final int RNLB = 15; // Roundabout left
static final int BL = 16; // Beeline routing
int ilon;
int ilat;
@ -39,7 +41,7 @@ public class VoiceHint {
return oldWay == null ? 0.f : oldWay.time;
}
float angle;
float angle = Float.MAX_VALUE;
boolean turnAngleConsumed;
boolean needsRealTurn;
@ -67,8 +69,13 @@ public class VoiceHint {
return roundaboutExit;
}
/*
* used by comment style, osmand style
*/
public String getCommandString() {
switch (cmd) {
case TLU:
return "TU"; // should be changed to TLU when osmand uses new voice hint constants
case TU:
return "TU";
case TSHL:
@ -100,8 +107,51 @@ public class VoiceHint {
}
}
/*
* used by trkpt/sym style
*/
public String getCommandString(int c) {
switch (c) {
case TLU:
return "TLU";
case TU:
return "TU";
case TSHL:
return "TSHL";
case TL:
return "TL";
case TSLL:
return "TSLL";
case KL:
return "KL";
case C:
return "C";
case KR:
return "KR";
case TSLR:
return "TSLR";
case TR:
return "TR";
case TSHR:
return "TSHR";
case TRU:
return "TRU";
case RNDB:
return "RNDB" + roundaboutExit;
case RNLB:
return "RNLB" + (-roundaboutExit);
default:
return "unknown command: " + c;
}
}
/*
* used by gpsies style
*/
public String getSymbolString() {
switch (cmd) {
case TLU:
return "TU";
case TU:
return "TU";
case TSHL:
@ -133,8 +183,53 @@ public class VoiceHint {
}
}
/*
* used by new locus trkpt style
*/
public String getLocusSymbolString() {
switch (cmd) {
case TLU:
return "u-turn_left";
case TU:
return "u-turn";
case TSHL:
return "left_sharp";
case TL:
return "left";
case TSLL:
return "left_slight";
case KL:
return "stay_left"; // ?
case C:
return "straight";
case KR:
return "stay_right"; // ?
case TSLR:
return "right_slight";
case TR:
return "right";
case TSHR:
return "right_sharp";
case TRU:
return "u-turn_right";
case RNDB:
return "roundabout_e" + roundaboutExit;
case RNLB:
return "roundabout_e" + (-roundaboutExit);
case BL:
return "beeline";
default:
throw new IllegalArgumentException("unknown command: " + cmd);
}
}
/*
* used by osmand style
*/
public String getMessageString() {
switch (cmd) {
case TLU:
return "u-turn"; // should be changed to u-turn-left when osmand uses new voice hint constants
case TU:
return "u-turn";
case TSHL:
@ -156,7 +251,7 @@ public class VoiceHint {
case TSHR:
return "sharp right";
case TRU:
return "u-turn";
return "u-turn"; // should be changed to u-turn-right when osmand uses new voice hint constants
case RNDB:
return "Take exit " + roundaboutExit;
case RNLB:
@ -166,10 +261,15 @@ public class VoiceHint {
}
}
/*
* used by old locus style
*/
public int getLocusAction() {
switch (cmd) {
case TU:
case TLU:
return 13;
case TU:
return 12;
case TSHL:
return 5;
case TL:
@ -199,8 +299,13 @@ public class VoiceHint {
}
}
/*
* used by orux style
*/
public int getOruxAction() {
switch (cmd) {
case TLU:
return 1003;
case TU:
return 1003;
case TSHL:
@ -232,6 +337,86 @@ public class VoiceHint {
}
}
/*
* used by cruiser, equivalent to getCommandString() - osmand style - when osmand changes the voice hint constants
*/
public String getCruiserCommandString() {
switch (cmd) {
case TLU:
return "TLU";
case TU:
return "TU";
case TSHL:
return "TSHL";
case TL:
return "TL";
case TSLL:
return "TSLL";
case KL:
return "KL";
case C:
return "C";
case KR:
return "KR";
case TSLR:
return "TSLR";
case TR:
return "TR";
case TSHR:
return "TSHR";
case TRU:
return "TRU";
case RNDB:
return "RNDB" + roundaboutExit;
case RNLB:
return "RNLB" + (-roundaboutExit);
case BL:
return "BL";
default:
throw new IllegalArgumentException("unknown command: " + cmd);
}
}
/*
* used by cruiser, equivalent to getMessageString() - osmand style - when osmand changes the voice hint constants
*/
public String getCruiserMessageString() {
switch (cmd) {
case TLU:
return "u-turn left";
case TU:
return "u-turn";
case TSHL:
return "sharp left";
case TL:
return "left";
case TSLL:
return "slight left";
case KL:
return "keep left";
case C:
return "straight";
case KR:
return "keep right";
case TSLR:
return "slight right";
case TR:
return "right";
case TSHR:
return "sharp right";
case TRU:
return "u-turn right";
case RNDB:
return "Take exit " + roundaboutExit;
case RNLB:
return "Take exit " + (-roundaboutExit);
case BL:
return "Beeline";
default:
throw new IllegalArgumentException("unknown command: " + cmd);
}
}
public void calcCommand() {
float lowerBadWayAngle = -181;
float higherBadWayAngle = 181;
@ -252,58 +437,98 @@ public class VoiceHint {
float cmdAngle = angle;
// fall back to local angle if otherwise inconsistent
if (lowerBadWayAngle > angle || higherBadWayAngle < angle) {
//if ( lowerBadWayAngle > angle || higherBadWayAngle < angle )
//{
//cmdAngle = goodWay.turnangle;
//}
if (angle == Float.MAX_VALUE) {
cmdAngle = goodWay.turnangle;
}
if (cmd == BL) return;
if (roundaboutExit > 0) {
cmd = RNDB;
} else if (roundaboutExit < 0) {
cmd = RNLB;
} else if (cmdAngle < -159.) {
} else if (is180DegAngle(cmdAngle) && cmdAngle <= -179.f && higherBadWayAngle == 181.f && lowerBadWayAngle == -181.f) {
cmd = TU;
} else if (cmdAngle < -135.) {
} else if (cmdAngle < -159.f) {
cmd = TLU;
} else if (cmdAngle < -135.f) {
cmd = TSHL;
} else if (cmdAngle < -45.) {
} else if (cmdAngle < -45.f) {
// a TL can be pushed in either direction by a close-by alternative
if (higherBadWayAngle > -90. && higherBadWayAngle < -15. && lowerBadWayAngle < -180.) {
if (cmdAngle < -95.f && higherBadWayAngle < -30.f && lowerBadWayAngle < -180.f) {
cmd = TSHL;
} else if (lowerBadWayAngle > -180. && lowerBadWayAngle < -90. && higherBadWayAngle > 0.) {
} else if (cmdAngle > -85.f && lowerBadWayAngle > -180.f && higherBadWayAngle > -10.f) {
cmd = TSLL;
} else {
if (cmdAngle < -110.f) {
cmd = TSHL;
} else if (cmdAngle > -60.f) {
cmd = TSLL;
} else {
cmd = TL;
}
} else if (cmdAngle < -21.) {
if (cmd != KR) // don't overwrite KR with TSLL
{
}
} else if (cmdAngle < -21.f) {
if (cmd != KR) { // don't overwrite KR with TSLL
cmd = TSLL;
}
} else if (cmdAngle < 21.) {
if (cmd != KR && cmd != KL) // don't overwrite KL/KR hints!
{
} else if (cmdAngle < -5.f) {
if (lowerBadWayAngle < -100.f && higherBadWayAngle < 45.f) {
cmd = TSLL;
} else if (lowerBadWayAngle >= -100.f && higherBadWayAngle < 45.f) {
cmd = KL;
} else {
cmd = C;
}
} else if (cmdAngle < 45.) {
if (cmd != KL) // don't overwrite KL with TSLR
{
cmd = TSLR;
} else if (cmdAngle < 5.f) {
if (lowerBadWayAngle > -30.f) {
cmd = KR;
} else if (higherBadWayAngle < 30.f) {
cmd = KL;
} else {
cmd = C;
}
} else if (cmdAngle < 135.) {
} else if (cmdAngle < 21.f) {
// a TR can be pushed in either direction by a close-by alternative
if (higherBadWayAngle > 90. && higherBadWayAngle < 180. && lowerBadWayAngle < 0.) {
if (lowerBadWayAngle > -45.f && higherBadWayAngle > 100.f) {
cmd = TSLR;
} else if (lowerBadWayAngle > 15. && lowerBadWayAngle < 90. && higherBadWayAngle > 180.) {
} else if (lowerBadWayAngle > -45.f && higherBadWayAngle <= 100.f) {
cmd = KR;
} else {
cmd = C;
}
} else if (cmdAngle < 45.f) {
cmd = TSLR;
} else if (cmdAngle < 135.f) {
if (cmdAngle < 85.f && higherBadWayAngle < 180.f && lowerBadWayAngle < 10.f) {
cmd = TSLR;
} else if (cmdAngle > 95.f && lowerBadWayAngle > 30.f && higherBadWayAngle > 180.f) {
cmd = TSHR;
} else {
if (cmdAngle > 110.) {
cmd = TSHR;
} else if (cmdAngle < 60.) {
cmd = TSLR;
} else {
cmd = TR;
}
} else if (cmdAngle < 159.) {
}
} else if (cmdAngle < 159.f) {
cmd = TSHR;
} else if (is180DegAngle(cmdAngle) && cmdAngle >= 179.f && higherBadWayAngle == 181.f && lowerBadWayAngle == -181.f) {
cmd = TU;
} else {
cmd = TRU;
}
}
static boolean is180DegAngle(float angle) {
return (Math.abs(angle) <= 180.f && Math.abs(angle) >= 179.f);
}
public String formatGeometry() {
float oldPrio = oldWay == null ? 0.f : oldWay.priorityclassifier;
StringBuilder sb = new StringBuilder(30);

View file

@ -9,18 +9,22 @@ import java.util.ArrayList;
import java.util.List;
public final class VoiceHintProcessor {
private double catchingRange; // range to catch angles and merge turns
double SIGNIFICANT_ANGLE = 22.5;
double INTERNAL_CATCHING_RANGE = 2.;
// private double catchingRange; // range to catch angles and merge turns
private boolean explicitRoundabouts;
public VoiceHintProcessor(double catchingRange, boolean explicitRoundabouts) {
this.catchingRange = catchingRange;
// this.catchingRange = catchingRange;
this.explicitRoundabouts = explicitRoundabouts;
}
private float sumNonConsumedWithinCatchingRange(List<VoiceHint> inputs, int offset) {
double distance = 0.;
float angle = 0.f;
while (offset >= 0 && distance < catchingRange) {
while (offset >= 0 && distance < INTERNAL_CATCHING_RANGE) {
VoiceHint input = inputs.get(offset--);
if (input.turnAngleConsumed) {
break;
@ -60,6 +64,10 @@ public final class VoiceHintProcessor {
for (int hintIdx = 0; hintIdx < inputs.size(); hintIdx++) {
VoiceHint input = inputs.get(hintIdx);
if (input.cmd == VoiceHint.BL) {
results.add(input);
continue;
}
float turnAngle = input.goodWay.turnangle;
distance += input.goodWay.linkdist;
int currentPrio = input.goodWay.getPrio();
@ -67,6 +75,7 @@ public final class VoiceHintProcessor {
int minPrio = Math.min(oldPrio, currentPrio);
boolean isLink2Highway = input.oldWay.isLinktType() && !input.goodWay.isLinktType();
boolean isHighway2Link = !input.oldWay.isLinktType() && input.goodWay.isLinktType();
if (input.oldWay.isRoundabout()) {
roundAboutTurnAngle += sumNonConsumedWithinCatchingRange(inputs, hintIdx);
@ -101,14 +110,18 @@ public final class VoiceHintProcessor {
float minAngle = 180.f;
float minAbsAngeRaw = 180.f;
boolean isBadwayLink = false;
if (input.badWays != null) {
for (MessageData badWay : input.badWays) {
int badPrio = badWay.getPrio();
float badTurn = badWay.turnangle;
if (badWay.isLinktType()) {
isBadwayLink = true;
}
boolean isBadHighway2Link = !input.oldWay.isLinktType() && badWay.isLinktType();
boolean isHighway2Link = !input.oldWay.isLinktType() && badWay.isLinktType();
if (badPrio > maxPrioAll && !isHighway2Link) {
if (badPrio > maxPrioAll && !isBadHighway2Link) {
maxPrioAll = badPrio;
}
@ -140,12 +153,17 @@ public final class VoiceHintProcessor {
}
}
boolean hasSomethingMoreStraight = Math.abs(turnAngle) - minAbsAngeRaw > 20.;
boolean hasSomethingMoreStraight = (Math.abs(turnAngle) - minAbsAngeRaw) > 20.;
// unconditional triggers are all junctions with
// - higher detour prios than the minimum route prio (except link->highway junctions)
// - or candidate detours with higher prio then the route exit leg
boolean unconditionalTrigger = hasSomethingMoreStraight || (maxPrioAll > minPrio && !isLink2Highway) || (maxPrioCandidates > currentPrio);
boolean unconditionalTrigger = hasSomethingMoreStraight ||
(maxPrioAll > minPrio && !isLink2Highway) ||
(maxPrioCandidates > currentPrio) ||
VoiceHint.is180DegAngle(turnAngle) ||
(!isHighway2Link && isBadwayLink && Math.abs(turnAngle) > 5.f) ||
(isHighway2Link && !isBadwayLink && Math.abs(turnAngle) < 5.f);
// conditional triggers (=real turning angle required) are junctions
// with candidate detours equal in priority than the route exit leg
@ -158,19 +176,21 @@ public final class VoiceHintProcessor {
input.needsRealTurn = (!unconditionalTrigger) && isStraight;
// check for KR/KL
if (maxAngle < turnAngle && maxAngle > turnAngle - 45.f - (turnAngle > 0.f ? turnAngle : 0.f)) {
if (Math.abs(turnAngle) > 5.) { // don't use to small angles
if (maxAngle < turnAngle && maxAngle > turnAngle - 45.f - (Math.max(turnAngle, 0.f))) {
input.cmd = VoiceHint.KR;
}
if (minAngle > turnAngle && minAngle < turnAngle + 45.f - (turnAngle < 0.f ? turnAngle : 0.f)) {
if (minAngle > turnAngle && minAngle < turnAngle + 45.f - (Math.min(turnAngle, 0.f))) {
input.cmd = VoiceHint.KL;
}
}
input.angle = sumNonConsumedWithinCatchingRange(inputs, hintIdx);
input.distanceToNext = distance;
distance = 0.;
results.add(input);
}
if (results.size() > 0 && distance < catchingRange) {
if (results.size() > 0 && distance < INTERNAL_CATCHING_RANGE) { //catchingRange
results.get(results.size() - 1).angle += sumNonConsumedWithinCatchingRange(inputs, hintIdx);
}
}
@ -185,10 +205,10 @@ public final class VoiceHintProcessor {
if (hint.cmd == 0) {
hint.calcCommand();
}
if (!(hint.needsRealTurn && hint.cmd == VoiceHint.C)) {
if (!(hint.needsRealTurn && (hint.cmd == VoiceHint.C || hint.cmd == VoiceHint.BL))) {
double dist = hint.distanceToNext;
// sum up other hints within the catching range (e.g. 40m)
while (dist < catchingRange && i > 0) {
while (dist < INTERNAL_CATCHING_RANGE && i > 0) {
VoiceHint h2 = results.get(i - 1);
dist = h2.distanceToNext;
hint.distanceToNext += dist;
@ -207,9 +227,92 @@ public final class VoiceHintProcessor {
}
hint.calcCommand();
results2.add(hint);
} else if (hint.cmd == VoiceHint.BL) {
results2.add(hint);
} else {
if (results2.size() > 0)
results2.get(results2.size() - 1).distanceToNext += hint.distanceToNext;
}
}
return results2;
}
public List<VoiceHint> postProcess(List<VoiceHint> inputs, double catchingRange, double minRange) {
List<VoiceHint> results = new ArrayList<VoiceHint>();
double distance = 0;
VoiceHint inputLast = null;
ArrayList<VoiceHint> tmpList = new ArrayList<>();
for (int hintIdx = 0; hintIdx < inputs.size(); hintIdx++) {
VoiceHint input = inputs.get(hintIdx);
if (input.cmd == VoiceHint.C && !input.goodWay.isLinktType()) {
int badWayPrio = 0;
for (MessageData md : input.badWays) {
badWayPrio = Math.max(badWayPrio, md.getPrio());
}
if (input.goodWay.getPrio() < badWayPrio) {
results.add(input);
} else {
if (inputLast != null) { // when drop add distance to last
inputLast.distanceToNext += input.distanceToNext;
}
continue;
}
} else {
if (input.distanceToNext < catchingRange) {
double dist = input.distanceToNext;
float angles = input.angle;
int i = 1;
boolean save = true;
tmpList.clear();
while (dist < catchingRange && hintIdx + i < inputs.size()) {
VoiceHint h2 = inputs.get(hintIdx + i);
dist += h2.distanceToNext;
angles += h2.angle;
if (VoiceHint.is180DegAngle(input.angle) || VoiceHint.is180DegAngle(h2.angle)) { // u-turn, 180 degree
save = true;
break;
} else if (Math.abs(angles) > 180 - SIGNIFICANT_ANGLE) { // u-turn, collects e.g. two left turns in range
input.angle = angles;
input.calcCommand();
input.distanceToNext += h2.distanceToNext;
save = true;
hintIdx++;
break;
} else if (Math.abs(angles) < SIGNIFICANT_ANGLE && input.distanceToNext < minRange) {
input.angle = angles;
input.calcCommand();
input.distanceToNext += h2.distanceToNext;
save = true;
hintIdx++;
break;
} else if (Math.abs(input.angle) > SIGNIFICANT_ANGLE) {
tmpList.add(h2);
hintIdx++;
} else if (dist > catchingRange) { // distance reached
break;
} else {
if (inputLast != null) { // when drop add distance to last
inputLast.distanceToNext += input.distanceToNext;
}
save = false;
}
i++;
}
if (save) {
results.add(input); // add when last
if (tmpList.size() > 0) { // add when something in stock
results.addAll(tmpList);
hintIdx += tmpList.size() - 1;
}
}
} else {
results.add(input);
}
inputLast = input;
}
}
return results;
}
}

View file

@ -0,0 +1,54 @@
---
parent: Features
---
# Remarks on Voice Hints
BRouter calculates voice hints but they are not present in all export formats. And within formats,
how they are presented will vary.
There are gpx formats for
* OsmAnd
* Locus
* Comment-style
* Gpsies
* Orux
The calculation starts with angles and comparing with the 'bad ways' (ways that are not
used on this junction). So e.g. an almost 90 degree hint can become "sharp right turn" if another
way is at 110 degrees.
And there are other rules
* show 'continue' only if the way crosses a higher priority way
* roundabouts have an exit marker
* u-turn between -179 and +179 degree
* merge two hints when near to each other - e.g. left, left to u-turn left
* marker when highway exit and continue nearly same direction
* beeline goes direct from via to via point
There are some variables in the profiles that affect on the voice hint generation:
* considerTurnRestrictions -
* turnInstructionCatchingRange - check distance to merge voice hints
* turnInstructionRoundabouts - use voice hints on roundabouts
Voice hint variables
| short | description |
| :----- | :----- |
| C | continue (go straight) |
| TL | turn left |
| TSLL | turn slightly left |
| TSHL | turn sharply left |
| TR | turn right |
| TSLR | turn slightly right |
| TSHR | turn sharply right |
| KL | keep left |
| KR | keep right |
| TLU | u-turn left |
| TU | 180 degree u-turn |
| TRU | u-turn right |
| OFFR | off route |
| RNDB | roundabout |
| RNLB | roundabout left |
| BL | beeline routing |