Skip to content

Commit d1d6082

Browse files
boldtrnkarussell
authored andcommitted
Add U-Turns (graphhopper#1073)
* Added heading for first continue * Added U-turn for contextual information * Fixed angle calculation issue * Added translations fo de+en * Angle improvement * Updated U_TURN translation and added it to the frontend * Updated translation files * Recognize U-turns over an additional edge * Updated max distance * Fixed Typo * Removed comment * Updated Translations * Changed Orientation Diff * Tmp Commit * Improved the U-Turn recognition * Limit U-turns to one-ways * Fixed some checkstyle warnings * Propose U-Turn left/right differentiation * Merge main.js
1 parent e34c1e9 commit d1d6082

File tree

12 files changed

+259
-16
lines changed

12 files changed

+259
-16
lines changed

client-hc/src/main/java/com/graphhopper/api/GraphHopperWeb.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@ PathWrapper createPathWrapper(JsonNode path,
174174
instr = new FinishInstruction(text, instPL, 0);
175175
} else {
176176
instr = new Instruction(sign, text, ia, instPL);
177+
if(sign == Instruction.CONTINUE_ON_STREET){
178+
if(jsonObj.has("heading")){
179+
instr.setExtraInfo("heading", jsonObj.get("heading").asDouble());
180+
}
181+
}
177182
}
178183

179184
// Usually, the translation is done from the routing service so just use the provided string

core/src/main/java/com/graphhopper/GraphHopper.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,9 @@ else if (ALT_ROUTE.equalsIgnoreCase(algoStr))
10741074
setPathDetailsBuilders(pathBuilderFactory, request.getPathDetails()).
10751075
setSimplifyResponse(simplifyResponse && wayPointMaxDistance > 0);
10761076

1077+
if(request.hasFavoredHeading(0))
1078+
pathMerger.setFavoredHeading(request.getFavoredHeading(0));
1079+
10771080
if (routingTemplate.isReady(pathMerger, tr))
10781081
break;
10791082
}

core/src/main/java/com/graphhopper/routing/InstructionsFromEdges.java

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,17 @@ public class InstructionsFromEdges implements Path.EdgeVisitor {
6666
private double doublePrevLat, doublePrevLon; // Lat and Lon of node t-2
6767
private int prevNode;
6868
private double prevOrientation;
69+
private double prevInstructionPrevOrientation = Double.NaN;
6970
private Instruction prevInstruction;
7071
private boolean prevInRoundabout;
7172
private String prevName;
73+
private String prevInstructionName;
7274
private InstructionAnnotation prevAnnotation;
7375
private EdgeExplorer outEdgeExplorer;
7476
private EdgeExplorer crossingExplorer;
7577

78+
private final int MAX_U_TURN_DISTANCE = 35;
79+
7680
public InstructionsFromEdges(int tmpNode, Graph graph, Weighting weighting, FlagEncoder encoder, NodeAccess nodeAccess, Translation tr, InstructionList ways) {
7781
this.weighting = weighting;
7882
this.encoder = encoder;
@@ -119,6 +123,10 @@ public void next(EdgeIteratorState edge, int index, int prevEdgeId) {
119123
{
120124
int sign = Instruction.CONTINUE_ON_STREET;
121125
prevInstruction = new Instruction(sign, name, annotation, new PointList(10, nodeAccess.is3D()));
126+
double startLat = nodeAccess.getLat(baseNode);
127+
double startLon = nodeAccess.getLon(baseNode);
128+
double heading = Helper.ANGLE_CALC.calcAzimuth(startLat, startLon, latitude, longitude);
129+
prevInstruction.setExtraInfo("heading", Helper.round(heading, 2));
122130
ways.add(prevInstruction);
123131
prevName = name;
124132
prevAnnotation = annotation;
@@ -130,6 +138,7 @@ public void next(EdgeIteratorState edge, int index, int prevEdgeId) {
130138
int sign = Instruction.USE_ROUNDABOUT;
131139
RoundaboutInstruction roundaboutInstruction = new RoundaboutInstruction(sign, name,
132140
annotation, new PointList(10, nodeAccess.is3D()));
141+
prevInstructionPrevOrientation = prevOrientation;
133142
if (prevName != null) {
134143
// check if there is an exit at the same node the roundabout was entered
135144
EdgeIterator edgeIter = outEdgeExplorer.setBaseNode(baseNode);
@@ -192,16 +201,64 @@ public void next(EdgeIteratorState edge, int index, int prevEdgeId) {
192201
.setDirOfRotation(deltaOut)
193202
.setExited();
194203

204+
prevInstructionName = prevName;
195205
prevName = name;
196206
prevAnnotation = annotation;
197207

198208
} else {
199209
int sign = getTurn(edge, baseNode, prevNode, adjNode, annotation, name);
200210

201211
if (sign != Instruction.IGNORE) {
202-
prevInstruction = new Instruction(sign, name, annotation, new PointList(10, nodeAccess.is3D()));
203-
ways.add(prevInstruction);
204-
prevAnnotation = annotation;
212+
/*
213+
Check if the next instruction is likely to only be a short connector to execute a u-turn
214+
--A->--
215+
| <-- This is the short connector
216+
--B-<--
217+
Road A and Road B have to have the same name and roughly the same, but opposite orientation, otherwise we are assuming this is no u-turn.
218+
219+
Note: This approach only works if there a turn instruction fro A->Connector and Connector->B.
220+
Currently we don't create a turn instruction if there is no other possible turn
221+
We only create a u-turn if edge B is a one-way, see #1073 for more details.
222+
*/
223+
224+
boolean isUTurn = false;
225+
int uTurnType = Instruction.U_TURN_UNKNOWN;
226+
if (!Double.isNaN(prevInstructionPrevOrientation)
227+
&& prevInstruction.getDistance() < MAX_U_TURN_DISTANCE
228+
&& (sign < 0) == (prevInstruction.getSign() < 0)
229+
&& (Math.abs(sign) == Instruction.TURN_SLIGHT_RIGHT || Math.abs(sign) == Instruction.TURN_RIGHT || Math.abs(sign) == Instruction.TURN_SHARP_RIGHT)
230+
&& (Math.abs(prevInstruction.getSign()) == Instruction.TURN_SLIGHT_RIGHT || Math.abs(prevInstruction.getSign()) == Instruction.TURN_RIGHT || Math.abs(prevInstruction.getSign()) == Instruction.TURN_SHARP_RIGHT)
231+
&& edge.isForward(encoder) != edge.isBackward(encoder)
232+
&& InstructionsHelper.isNameSimilar(prevInstructionName, name)) {
233+
// Chances are good that this is a u-turn, we only need to check if the orientation matches
234+
GHPoint point = InstructionsHelper.getPointForOrientationCalculation(edge, nodeAccess);
235+
double lat = point.getLat();
236+
double lon = point.getLon();
237+
double currentOrientation = Helper.ANGLE_CALC.calcOrientation(prevLat, prevLon, lat, lon, false);
238+
239+
double diff = Math.abs(prevInstructionPrevOrientation - currentOrientation);
240+
if (diff > (Math.PI * .9) && diff < (Math.PI * 1.1)) {
241+
isUTurn = true;
242+
if (sign < 0) {
243+
uTurnType = Instruction.U_TURN_LEFT;
244+
} else {
245+
uTurnType = Instruction.U_TURN_RIGHT;
246+
}
247+
}
248+
249+
}
250+
251+
if (isUTurn) {
252+
prevInstruction.setSign(uTurnType);
253+
prevInstruction.setName(name);
254+
} else {
255+
prevInstruction = new Instruction(sign, name, annotation, new PointList(10, nodeAccess.is3D()));
256+
// Remember the Orientation and name of the road, before doing this maneuver
257+
prevInstructionPrevOrientation = prevOrientation;
258+
prevInstructionName = prevName;
259+
ways.add(prevInstruction);
260+
prevAnnotation = annotation;
261+
}
205262
}
206263
// Updated the prevName, since we don't always create an instruction on name changes the previous
207264
// name can be an old name. This leads to incorrect turn instructions due to name changes
@@ -236,7 +293,11 @@ public void finish() {
236293
((RoundaboutInstruction) prevInstruction).setRadian(delta);
237294

238295
}
239-
ways.add(new FinishInstruction(nodeAccess, prevEdge.getAdjNode()));
296+
297+
Instruction finishInstruction = new FinishInstruction(nodeAccess, prevEdge.getAdjNode());
298+
// This is the heading how the edge ended
299+
finishInstruction.setExtraInfo("last_heading", Helper.ANGLE_CALC.calcAzimuth(doublePrevLat, doublePrevLon, prevLat, prevLon));
300+
ways.add(finishInstruction);
240301
}
241302

242303
private int getTurn(EdgeIteratorState edge, int baseNode, int prevNode, int adjNode, InstructionAnnotation annotation, String name) {

core/src/main/java/com/graphhopper/routing/InstructionsHelper.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,6 @@ static int calculateSign(double prevLatitude, double prevLongitude, double latit
3939
double delta = calculateOrientationDelta(prevLatitude, prevLongitude, latitude, longitude, prevOrientation);
4040
double absDelta = Math.abs(delta);
4141

42-
// TODO not only calculate the mathematical orientation, but also compare to other streets
43-
// TODO If there is one street turning slight right and one right, but no straight street
44-
// TODO We can assume the slight right street would be a continue
4542
if (absDelta < 0.2) {
4643
// 0.2 ~= 11°
4744
return Instruction.CONTINUE_ON_STREET;

core/src/main/java/com/graphhopper/util/AngleCalc.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public double calcAzimuth(double lat1, double lon1, double lat2, double lon2) {
115115
if (orientation < 0)
116116
orientation += 2 * Math.PI;
117117

118-
return Math.toDegrees(Helper.round4(orientation))%360;
118+
return Math.toDegrees(Helper.round4(orientation)) % 360;
119119
}
120120

121121
String azimuth2compassPoint(double azimuth) {

core/src/main/java/com/graphhopper/util/Instruction.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
*/
1818
package com.graphhopper.util;
1919

20-
import java.util.Collections;
20+
import java.util.HashMap;
2121
import java.util.List;
2222
import java.util.Map;
2323

2424
public class Instruction {
2525
public static final int UNKNOWN = -99;
26+
public static final int U_TURN_UNKNOWN = -98;
27+
public static final int U_TURN_LEFT = -8;
2628
public static final int KEEP_LEFT = -7;
2729
public static final int LEAVE_ROUNDABOUT = -6; // for future use
2830
public static final int TURN_SHARP_LEFT = -3;
@@ -37,6 +39,7 @@ public class Instruction {
3739
public static final int USE_ROUNDABOUT = 6;
3840
public static final int IGNORE = Integer.MIN_VALUE;
3941
public static final int KEEP_RIGHT = 7;
42+
public static final int U_TURN_RIGHT = 8;
4043
public static final int PT_START_TRIP = 101;
4144
public static final int PT_TRANSFER = 102;
4245
public static final int PT_END_TRIP = 103;
@@ -48,6 +51,7 @@ public class Instruction {
4851
protected String name;
4952
protected double distance;
5053
protected long time;
54+
protected Map<String, Object> extraInfo = new HashMap<>(3);
5155

5256
/**
5357
* The points, distances and times have exactly the same count. The last point of this
@@ -79,6 +83,10 @@ public int getSign() {
7983
return sign;
8084
}
8185

86+
public void setSign(int sign) {
87+
this.sign = sign;
88+
}
89+
8290
public String getName() {
8391
return name;
8492
}
@@ -88,11 +96,11 @@ public void setName(String name) {
8896
}
8997

9098
public Map<String, Object> getExtraInfoJSON() {
91-
return Collections.<String, Object>emptyMap();
99+
return extraInfo;
92100
}
93101

94102
public void setExtraInfo(String key, Object value) {
95-
throw new IllegalArgumentException("Key" + key + " is not a valid option");
103+
extraInfo.put(key, value);
96104
}
97105

98106
/**
@@ -270,6 +278,15 @@ public String getTurnDescription(Translation tr) {
270278
} else {
271279
String dir = null;
272280
switch (indi) {
281+
case Instruction.U_TURN_UNKNOWN:
282+
dir = tr.tr("u_turn");
283+
break;
284+
case Instruction.U_TURN_LEFT:
285+
dir = tr.tr("u_turn");
286+
break;
287+
case Instruction.U_TURN_RIGHT:
288+
dir = tr.tr("u_turn");
289+
break;
273290
case Instruction.KEEP_LEFT:
274291
dir = tr.tr("keep_left");
275292
break;

core/src/main/java/com/graphhopper/util/PathMerger.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public class PathMerger {
4848
private boolean calcPoints = true;
4949
private PathDetailsBuilderFactory pathBuilderFactory;
5050
private List<String> requestedPathDetails = Collections.EMPTY_LIST;
51+
private double favoredHeading = Double.NaN;
5152

5253
public PathMerger setCalcPoints(boolean calcPoints) {
5354
this.calcPoints = calcPoints;
@@ -135,8 +136,10 @@ public void doWork(PathWrapper altRsp, List<Path> paths, Translation tr) {
135136
calcAscendDescend(altRsp, fullPoints);
136137
}
137138

138-
if (enableInstructions)
139+
if (enableInstructions) {
140+
fullInstructions = updateInstructionsWithContext(fullInstructions);
139141
altRsp.setInstructions(fullInstructions);
142+
}
140143

141144
if (!allFound) {
142145
altRsp.addError(new ConnectionNotFoundException("Connection between locations not found", Collections.<String, Object>emptyMap()));
@@ -173,6 +176,51 @@ public static void merge(List<PathDetail> pathDetails, List<PathDetail> otherDet
173176
pathDetails.addAll(otherDetails);
174177
}
175178

179+
/**
180+
* This method iterates over all instructions and uses the available context to improve the instructions.
181+
* If the requests contains a heading, this method can transform the first continue to a u-turn if the heading
182+
* points into the opposite direction of the route.
183+
* At a waypoint it can transform the continue to a u-turn if the route involves turning.
184+
*/
185+
private InstructionList updateInstructionsWithContext(InstructionList instructions) {
186+
Instruction instruction;
187+
Instruction nextInstruction;
188+
189+
for (int i = 0; i < instructions.size() - 1; i++) {
190+
instruction = instructions.get(i);
191+
192+
if (i == 0 && !Double.isNaN(favoredHeading) && instruction.extraInfo.containsKey("heading")) {
193+
double heading = (double) instruction.extraInfo.get("heading");
194+
double diff = Math.abs(heading - favoredHeading) % 360;
195+
if (diff > 170 && diff < 190) {
196+
// The requested heading points into the opposite direction of the calculated heading
197+
// therefore we change the continue instruction to a u-turn
198+
instruction.setSign(Instruction.U_TURN_UNKNOWN);
199+
}
200+
}
201+
202+
if (instruction.getSign() == Instruction.REACHED_VIA) {
203+
nextInstruction = instructions.get(i + 1);
204+
if (nextInstruction.getSign() != Instruction.CONTINUE_ON_STREET
205+
|| !instruction.extraInfo.containsKey("last_heading")
206+
|| !nextInstruction.extraInfo.containsKey("heading")) {
207+
// TODO throw exception?
208+
continue;
209+
}
210+
double lastHeading = (double) instruction.extraInfo.get("last_heading");
211+
double heading = (double) nextInstruction.extraInfo.get("heading");
212+
213+
// Since it's supposed to go back the same edge, we can be very strict with the diff
214+
double diff = Math.abs(lastHeading - heading) % 360;
215+
if (diff > 179 && diff < 181) {
216+
nextInstruction.setSign(Instruction.U_TURN_UNKNOWN);
217+
}
218+
}
219+
}
220+
221+
return instructions;
222+
}
223+
176224
private void calcAscendDescend(final PathWrapper rsp, final PointList pointList) {
177225
double ascendMeters = 0;
178226
double descendMeters = 0;
@@ -192,4 +240,8 @@ private void calcAscendDescend(final PathWrapper rsp, final PointList pointList)
192240
rsp.setAscend(ascendMeters);
193241
rsp.setDescend(descendMeters);
194242
}
243+
244+
public void setFavoredHeading(double favoredHeading) {
245+
this.favoredHeading = favoredHeading;
246+
}
195247
}

core/src/main/java/com/graphhopper/util/ViaInstruction.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public ViaInstruction(Instruction instr) {
3131
this(instr.getName(), instr.getAnnotation(), instr.getPoints());
3232
setDistance(instr.getDistance());
3333
setTime(instr.getTime());
34+
this.extraInfo = instr.extraInfo;
3435
}
3536

3637
@Override

core/src/test/java/com/graphhopper/routing/PathTest.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,76 @@ public void testCalcInstructionSlightTurn() {
597597
assertEquals(-1, wayList.get(1).getSign());
598598
}
599599

600+
@Test
601+
public void testUTurnLeft() {
602+
final Graph g = new GraphBuilder(carManager).create();
603+
final NodeAccess na = g.getNodeAccess();
604+
605+
// Real Situation: point=48.402116%2C9.994367&point=48.402198%2C9.99507
606+
// 7
607+
// |
608+
// 4----5----6
609+
// |
610+
// 1----2----3
611+
na.setNode(1, 48.402116, 9.994367);
612+
na.setNode(2, 48.402198, 9.99507);
613+
na.setNode(3, 48.402344, 9.996266);
614+
na.setNode(4, 48.402191, 9.994351);
615+
na.setNode(5, 48.402298, 9.995053);
616+
na.setNode(6, 48.402422, 9.996067);
617+
na.setNode(7, 48.402604, 9.994962);
618+
619+
g.edge(1, 2, 5, false).setName("Olgastraße");
620+
g.edge(2, 3, 5, false).setName("Olgastraße");
621+
g.edge(6, 5, 5, false).setName("Olgastraße");
622+
g.edge(5, 4, 5, false).setName("Olgastraße");
623+
g.edge(2, 5, 5, true).setName("Neithardtstraße");
624+
g.edge(5, 7, 5, true).setName("Neithardtstraße");
625+
626+
Path p = new Dijkstra(g, new ShortestWeighting(encoder), TraversalMode.NODE_BASED)
627+
.calcPath(1, 4);
628+
assertTrue(p.isFound());
629+
InstructionList wayList = p.calcInstructions(tr);
630+
631+
assertEquals(3, wayList.size());
632+
assertEquals(Instruction.U_TURN_LEFT, wayList.get(1).getSign());
633+
}
634+
635+
@Test
636+
public void testUTurnRight() {
637+
final Graph g = new GraphBuilder(carManager).create();
638+
final NodeAccess na = g.getNodeAccess();
639+
640+
// Real Situation: point=-33.885758,151.181472&point=-33.885692,151.181445
641+
// 7
642+
// |
643+
// 4----5----6
644+
// |
645+
// 3----2----1
646+
na.setNode(1, -33.885758,151.181472);
647+
na.setNode(2, -33.885852,151.180968);
648+
na.setNode(3, -33.885968,151.180501);
649+
na.setNode(4, -33.885883,151.180442);
650+
na.setNode(5, -33.885772,151.180941);
651+
na.setNode(6, -33.885692,151.181445);
652+
na.setNode(7, -33.885692,151.181445);
653+
654+
g.edge(1, 2, 5, false).setName("Parramatta Road");
655+
g.edge(2, 3, 5, false).setName("Parramatta Road");
656+
g.edge(4, 5, 5, false).setName("Parramatta Road");
657+
g.edge(5, 6, 5, false).setName("Parramatta Road");
658+
g.edge(2, 5, 5, true).setName("Larkin Street");
659+
g.edge(5, 7, 5, true).setName("Larkin Street");
660+
661+
Path p = new Dijkstra(g, new ShortestWeighting(encoder), TraversalMode.NODE_BASED)
662+
.calcPath(1, 6);
663+
assertTrue(p.isFound());
664+
InstructionList wayList = p.calcInstructions(tr);
665+
666+
assertEquals(3, wayList.size());
667+
assertEquals(Instruction.U_TURN_RIGHT, wayList.get(1).getSign());
668+
}
669+
600670
@Test
601671
public void testCalcInstructionsForTurn() {
602672
// The street turns left, but there is not turn

0 commit comments

Comments
 (0)