Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow u-turns only at dead ends #2883

Open
wants to merge 34 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3af92a8
Allow u-turns only at deadends
easbar Aug 24, 2023
0d8e956
unrelated to subnetworks since they are running afterwards (first the…
easbar Aug 24, 2023
caecb8f
Merge branch 'master' into dead_end_u_turns
easbar Aug 25, 2023
ee2b9bf
storedUTurnCostsAreDeadEndFlags... (sigh)
easbar Aug 25, 2023
7e320bb
Disable failing tests.
easbar Aug 25, 2023
8ba5040
tmp
easbar Aug 25, 2023
1dfb794
Merge branch 'master' into dead_end_u_turns
easbar Sep 6, 2023
6c3de4b
Merge branch 'master' into dead_end_u_turns
easbar Sep 12, 2023
5d88e15
up
easbar Sep 12, 2023
07bd1b5
Remove max_turn_costs encoder parameter
easbar Sep 12, 2023
e0b308f
tmp
easbar Sep 12, 2023
e9c8f2c
continue
easbar Sep 13, 2023
564ab4d
update
easbar Sep 13, 2023
01479d3
Merge branch 'master' into tctr
easbar Sep 13, 2023
ff9a8d8
Use boolean turn restriction enc instead of decimal turn cost enc
easbar Sep 13, 2023
22e0d59
Merge branch 'turn_restriction_enc' into dead_end_enc
easbar Sep 13, 2023
b91304a
Merge branch 'dead_end_enc' into dead_end_u_turns
easbar Sep 13, 2023
1a251bc
Use dead end enc
easbar Sep 13, 2023
fa3dc17
Merge branch 'master' into turn_restriction_enc
easbar Sep 18, 2023
2544c6f
Merge branch 'turn_restriction_enc' into dead_end_u_turns
easbar Sep 18, 2023
ba08fea
Merge branch 'master' into dead_end_u_turns
easbar Sep 25, 2023
60a6ba3
fix some tests
easbar Sep 25, 2023
750388f
tmp
easbar Sep 25, 2023
1b7ea5d
reverse order / green
easbar Sep 25, 2023
103b098
green
easbar Sep 25, 2023
c6dd1d0
Fix dead ends at subnetwork boundaries
easbar Sep 25, 2023
50a607b
move + test
easbar Sep 25, 2023
eadc81e
Try lower u-turn costs
easbar Sep 25, 2023
6a2e143
Try higher u-turn costs
easbar Sep 25, 2023
ae0cf77
try zero u-turn costs
easbar Sep 25, 2023
14ffa5b
Merge branch 'master' into dead_end_u_turns
karussell Sep 26, 2023
8db91a7
minor fixes
karussell Sep 26, 2023
a733535
Merge branch 'master' into dead_end_u_turns
easbar Oct 4, 2023
efd095b
Merge branch 'master' into dead_end_u_turns
easbar Oct 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 36 additions & 3 deletions core/src/main/java/com/graphhopper/GraphHopper.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.graphhopper.jackson.Jackson;
import com.graphhopper.reader.dem.*;
import com.graphhopper.reader.osm.OSMReader;
import com.graphhopper.reader.osm.PrepareDeadEnds;
import com.graphhopper.reader.osm.RestrictionTagParser;
import com.graphhopper.reader.osm.conditional.DateRangeParser;
import com.graphhopper.routing.*;
Expand All @@ -40,6 +41,7 @@
import com.graphhopper.routing.util.*;
import com.graphhopper.routing.util.countryrules.CountryRuleFactory;
import com.graphhopper.routing.util.parsers.*;
import com.graphhopper.routing.weighting.AbstractAdjustedWeighting;
import com.graphhopper.routing.weighting.Weighting;
import com.graphhopper.routing.weighting.custom.CustomWeighting;
import com.graphhopper.storage.*;
Expand Down Expand Up @@ -1409,14 +1411,45 @@ protected void cleanUp() {
preparation.doWork();
properties.put("profiles", getProfilesString());
logger.info("nodes: " + Helper.nf(baseGraph.getNodes()) + ", edges: " + Helper.nf(baseGraph.getEdges()));

logger.info("Start marking dead-ends");
StopWatch sw = StopWatch.started();
PrepareDeadEnds prepareDeadEnds = new PrepareDeadEnds(baseGraph);
for (Profile profile : profilesByName.values()) {
if (profile.isTurnCosts()) {
BooleanEncodedValue deadEndEnc = encodingManager.getTurnBooleanEncodedValue(DeadEnd.key(profile.getVehicle()));
BooleanEncodedValue subnetworkEnc = encodingManager.getBooleanEncodedValue(Subnetwork.key(profile.getName()));
// We disable turn costs for the dead-end search.
Weighting weighting = createWeighting(profile, new PMap(), true);
prepareDeadEnds.findDeadEndUTurns(weighting, deadEndEnc, subnetworkEnc);
}
}
logger.info("Finished marking dead-ends, took: " + sw.stop().getSeconds() + "s");
}



private List<PrepareJob> buildSubnetworkRemovalJobs() {
List<PrepareJob> jobs = new ArrayList<>();
for (Profile profile : profilesByName.values()) {
// if turn costs are enabled use u-turn costs of zero as we only want to make sure the graph is fully connected assuming finite u-turn costs
Weighting weighting = createWeighting(profile, new PMap().putObject(Parameters.Routing.U_TURN_COSTS, 0));
jobs.add(new PrepareJob(encodingManager.getBooleanEncodedValue(Subnetwork.key(profile.getName())), weighting));
Weighting weighting = createWeighting(profile, new PMap());
Weighting w = new AbstractAdjustedWeighting(weighting) {
@Override
public double calcTurnWeight(int inEdge, int viaNode, int outEdge) {
// We only want to make sure the graph is fully connected assuming finite u-turn
// costs. Here we have to set it to zero explicitly, because otherwise the
// u-turn costs would be infinite everywhere except at dead-ends. But we run the
// dead-end detection after the subnetwork search.
if (inEdge == outEdge) return 0;
return weighting.calcTurnWeight(inEdge, viaNode, outEdge);
}

@Override
public String getName() {
return weighting.getName();
}
};
jobs.add(new PrepareJob(encodingManager.getBooleanEncodedValue(Subnetwork.key(profile.getName())), w));
}
return jobs;
}
Expand Down
61 changes: 61 additions & 0 deletions core/src/main/java/com/graphhopper/reader/osm/PrepareDeadEnds.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Licensed to GraphHopper GmbH under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*
* GraphHopper GmbH licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.graphhopper.reader.osm;

import com.graphhopper.routing.ev.BooleanEncodedValue;
import com.graphhopper.routing.weighting.Weighting;
import com.graphhopper.storage.BaseGraph;
import com.graphhopper.util.EdgeExplorer;
import com.graphhopper.util.EdgeIterator;
import com.graphhopper.util.GHUtility;

public class PrepareDeadEnds {
private final BaseGraph baseGraph;

public PrepareDeadEnds(BaseGraph baseGraph) {
this.baseGraph = baseGraph;
}

public void findDeadEndUTurns(Weighting weighting, BooleanEncodedValue deadEndEnc, BooleanEncodedValue subnetworkEnc) {
EdgeExplorer inExplorer = baseGraph.createEdgeExplorer();
EdgeExplorer outExplorer = baseGraph.createEdgeExplorer();
for (int node = 0; node < baseGraph.getNodes(); node++) {
EdgeIterator fromEdge = inExplorer.setBaseNode(node);
OUTER:
while (fromEdge.next()) {
if (Double.isFinite(weighting.calcEdgeWeight(fromEdge, true))) {
boolean subnetworkFrom = fromEdge.get(subnetworkEnc);
EdgeIterator toEdge = outExplorer.setBaseNode(node);
while (toEdge.next()) {
if (toEdge.getEdge() != fromEdge.getEdge()
&& Double.isFinite(GHUtility.calcWeightWithTurnWeight(weighting, toEdge, false, fromEdge.getEdge()))
&& subnetworkFrom == toEdge.get(subnetworkEnc))
continue OUTER;
}
// the only way to continue from fromEdge is a u-turn. this is a dead-end u-turn
setDeadEndUTurn(baseGraph, deadEndEnc, fromEdge.getEdge(), node);
}
}
}
}

private void setDeadEndUTurn(BaseGraph baseGraph, BooleanEncodedValue deadEndEnc, int fromEdge, int viaNode) {
baseGraph.getTurnCostStorage().set(deadEndEnc, fromEdge, viaNode, fromEdge, true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ public Weighting createWeighting(Profile profile, PMap requestHints, boolean dis
TurnCostProvider turnCostProvider;
if (profile.isTurnCosts() && !disableTurnCosts) {
BooleanEncodedValue turnRestrictionEnc = encodingManager.getTurnBooleanEncodedValue(TurnRestriction.key(vehicle));
if (turnRestrictionEnc == null)
BooleanEncodedValue deadEndEnc = encodingManager.getTurnBooleanEncodedValue(DeadEnd.key(vehicle));
if (turnRestrictionEnc == null || deadEndEnc == null)
throw new IllegalArgumentException("Vehicle " + vehicle + " does not support turn costs");
int uTurnCosts = hints.getInt(Parameters.Routing.U_TURN_COSTS, INFINITE_U_TURN_COSTS);
turnCostProvider = new DefaultTurnCostProvider(turnRestrictionEnc, graph.getTurnCostStorage(), uTurnCosts);
turnCostProvider = new DefaultTurnCostProvider(turnRestrictionEnc, deadEndEnc, graph.getTurnCostStorage(), uTurnCosts);
} else {
turnCostProvider = NO_TURN_COST_PROVIDER;
}
Expand Down
32 changes: 32 additions & 0 deletions core/src/main/java/com/graphhopper/routing/ev/DeadEnd.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Licensed to GraphHopper GmbH under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*
* GraphHopper GmbH licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.graphhopper.routing.ev;

import static com.graphhopper.routing.util.EncodingManager.getKey;

public class DeadEnd {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case there will be u-turns other than dead-ends that we want to explicitly allow in the future, maybe we should use a more general name(?).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Information. Sorry as (non dev) GPS user I have to describe it less technically.

U-turns are sure very useful.
In the Kurviger and Cruiser app 180° u-turns are very usefully used.
See example in the image and attached gpx navigation track.
By Notepad++ or gpx editor, find turn instructions in trackpoints.

Cruiser_GraphHopper planning.

  • This 180° u-turn example here is indeed correctly placed and you indeed are expected to make a -u turn at this location. A such 180° U-turn NOT defined to be a Left nor a Right u-turn command.
  • U-turns are shown as a red Icons in the Cruiser turnlist.
  • U-turns can usefully be used as alert signal that a shaping or via point may have been falsely placed onto a side road in the planner and causes false annoying turn commands immediate than to be corrected by an 180° u-turn.

Cruiser_Brouter planning.
Same as above but with an extra.
A "correct misplaced via points" tool can presumably thus automatically prevent false out and return track glitches with 180° u-turn commands. The correction distance is adjustable between 0 and 100m (default 40m).
Provided additional shaping point, a 180° u-turn can still be enforced even in this correction range.

Cruiser_Brouter navigation.
If you ignore the u-turn command during navigation, the system does not insist on a 180° u-turn but suggests a forward alternative (large diversions if necessary). As a person, you can of course still turn back.

RouteYou website planner. (osrm router).
Sure allows 180° U-turns. In addition, there is a "correct misplaced via points" tool. The correction distance at which the tool intervenes is 100m. Track glitches shorter than 100m are so automatically prevented.
Usefull u-turn example
Usefull u-turn navtrack.gpx.txt


public static String key(String prefix) {
return getKey(prefix, "dead_end");
}

public static BooleanEncodedValue create(String name) {
return new SimpleBooleanEncodedValue(key(name), false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class VehicleEncodedValues {
private final DecimalEncodedValue avgSpeedEnc;
private final DecimalEncodedValue priorityEnc;
private final BooleanEncodedValue turnRestrictionEnc;
private final BooleanEncodedValue deadEndEnc;

public static VehicleEncodedValues foot(PMap properties) {
String name = properties.getString("name", "foot");
Expand All @@ -45,7 +46,8 @@ public static VehicleEncodedValues foot(PMap properties) {
DecimalEncodedValue speedEnc = VehicleSpeed.create(name, speedBits, speedFactor, speedTwoDirections);
DecimalEncodedValue priorityEnc = VehiclePriority.create(name, 4, PriorityCode.getFactor(1), false);
BooleanEncodedValue turnRestrictionEnc = turnCosts ? TurnRestriction.create(name) : null;
return new VehicleEncodedValues(name, accessEnc, speedEnc, priorityEnc, turnRestrictionEnc);
BooleanEncodedValue deadEndEnc = turnCosts ? DeadEnd.create(name) : null;
return new VehicleEncodedValues(name, accessEnc, speedEnc, priorityEnc, turnRestrictionEnc, deadEndEnc);
}

public static VehicleEncodedValues wheelchair(PMap properties) {
Expand All @@ -67,7 +69,8 @@ public static VehicleEncodedValues bike(PMap properties) {
DecimalEncodedValue speedEnc = VehicleSpeed.create(name, speedBits, speedFactor, speedTwoDirections);
DecimalEncodedValue priorityEnc = VehiclePriority.create(name, 4, PriorityCode.getFactor(1), false);
BooleanEncodedValue turnRestrictionEnc = turnCosts ? TurnRestriction.create(name) : null;
return new VehicleEncodedValues(name, accessEnc, speedEnc, priorityEnc, turnRestrictionEnc);
BooleanEncodedValue deadEndEnc = turnCosts ? DeadEnd.create(name) : null;
return new VehicleEncodedValues(name, accessEnc, speedEnc, priorityEnc, turnRestrictionEnc, deadEndEnc);
}

public static VehicleEncodedValues racingbike(PMap properties) {
Expand All @@ -86,7 +89,8 @@ public static VehicleEncodedValues car(PMap properties) {
BooleanEncodedValue accessEnc = VehicleAccess.create(name);
DecimalEncodedValue speedEnc = VehicleSpeed.create(name, speedBits, speedFactor, true);
BooleanEncodedValue turnRestrictionEnc = turnCosts ? TurnRestriction.create(name) : null;
return new VehicleEncodedValues(name, accessEnc, speedEnc, null, turnRestrictionEnc);
BooleanEncodedValue deadEndEnc = turnCosts ? DeadEnd.create(name) : null;
return new VehicleEncodedValues(name, accessEnc, speedEnc, null, turnRestrictionEnc, deadEndEnc);
}

public static VehicleEncodedValues roads(PMap properties) {
Expand All @@ -98,16 +102,18 @@ public static VehicleEncodedValues roads(PMap properties) {
BooleanEncodedValue accessEnc = VehicleAccess.create(name);
DecimalEncodedValue speedEnc = VehicleSpeed.create(name, speedBits, speedFactor, speedTwoDirections);
BooleanEncodedValue turnRestrictionEnc = turnCosts ? TurnRestriction.create(name) : null;
return new VehicleEncodedValues(name, accessEnc, speedEnc, null, turnRestrictionEnc);
BooleanEncodedValue deadEndEnc = turnCosts ? DeadEnd.create(name) : null;
return new VehicleEncodedValues(name, accessEnc, speedEnc, null, turnRestrictionEnc, deadEndEnc);
}

public VehicleEncodedValues(String name, BooleanEncodedValue accessEnc, DecimalEncodedValue avgSpeedEnc,
DecimalEncodedValue priorityEnc, BooleanEncodedValue turnRestrictionEnc) {
DecimalEncodedValue priorityEnc, BooleanEncodedValue turnRestrictionEnc, BooleanEncodedValue deadEndEnc) {
this.name = name;
this.accessEnc = accessEnc;
this.avgSpeedEnc = avgSpeedEnc;
this.priorityEnc = priorityEnc;
this.turnRestrictionEnc = turnRestrictionEnc;
this.deadEndEnc = deadEndEnc;
}

public void createEncodedValues(List<EncodedValue> registerNewEncodedValue) {
Expand All @@ -122,6 +128,8 @@ public void createEncodedValues(List<EncodedValue> registerNewEncodedValue) {
public void createTurnCostEncodedValues(List<EncodedValue> registerNewTurnCostEncodedValues) {
if (turnRestrictionEnc != null)
registerNewTurnCostEncodedValues.add(turnRestrictionEnc);
if (deadEndEnc != null)
registerNewTurnCostEncodedValues.add(deadEndEnc);
}

public BooleanEncodedValue getAccessEnc() {
Expand All @@ -140,6 +148,10 @@ public BooleanEncodedValue getTurnRestrictionEnc() {
return turnRestrictionEnc;
}

public BooleanEncodedValue getDeadEndEnc() {
return deadEndEnc;
}

public String getName() {
return name;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,20 @@

public class DefaultTurnCostProvider implements TurnCostProvider {
private final BooleanEncodedValue turnRestrictionEnc;
private final BooleanEncodedValue deadEndEnc;
private final TurnCostStorage turnCostStorage;
private final int uTurnCostsInt;
private final double uTurnCosts;

public DefaultTurnCostProvider(BooleanEncodedValue turnRestrictionEnc, TurnCostStorage turnCostStorage) {
this(turnRestrictionEnc, turnCostStorage, Weighting.INFINITE_U_TURN_COSTS);
public DefaultTurnCostProvider(BooleanEncodedValue turnRestrictionEnc, BooleanEncodedValue deadEndEnc, TurnCostStorage turnCostStorage) {
this(turnRestrictionEnc, deadEndEnc, turnCostStorage, Weighting.INFINITE_U_TURN_COSTS);
}

/**
* @param uTurnCosts the costs of a u-turn in seconds, for {@link Weighting#INFINITE_U_TURN_COSTS} the u-turn costs
* will be infinite
* will be infinite. u-turns are only allowed at dead ends.
*/
public DefaultTurnCostProvider(BooleanEncodedValue turnRestrictionEnc, TurnCostStorage turnCostStorage, int uTurnCosts) {
public DefaultTurnCostProvider(BooleanEncodedValue turnRestrictionEnc, BooleanEncodedValue deadEndEnc, TurnCostStorage turnCostStorage, int uTurnCosts) {
if (uTurnCosts < 0 && uTurnCosts != INFINITE_U_TURN_COSTS) {
throw new IllegalArgumentException("u-turn costs must be positive, or equal to " + INFINITE_U_TURN_COSTS + " (=infinite costs)");
}
Expand All @@ -49,27 +50,28 @@ public DefaultTurnCostProvider(BooleanEncodedValue turnRestrictionEnc, TurnCostS
}
// if null the TurnCostProvider can be still useful for edge-based routing
this.turnRestrictionEnc = turnRestrictionEnc;
this.deadEndEnc = deadEndEnc;
this.turnCostStorage = turnCostStorage;
}

public BooleanEncodedValue getTurnRestrictionEnc() {
return turnRestrictionEnc;
}

public BooleanEncodedValue getDeadEndEnc() {
return deadEndEnc;
}

@Override
public double calcTurnWeight(int edgeFrom, int nodeVia, int edgeTo) {
if (!EdgeIterator.Edge.isValid(edgeFrom) || !EdgeIterator.Edge.isValid(edgeTo)) {
if (!EdgeIterator.Edge.isValid(edgeFrom) || !EdgeIterator.Edge.isValid(edgeTo))
return 0;
if (turnCostStorage.get(turnRestrictionEnc, edgeFrom, nodeVia, edgeTo))
return Double.POSITIVE_INFINITY;
else if (edgeFrom == edgeTo)
return turnCostStorage.get(deadEndEnc, edgeFrom, nodeVia, edgeTo) ? uTurnCosts : Double.POSITIVE_INFINITY;
else
return 0;
}
double tCost = 0;
if (edgeFrom == edgeTo) {
// note that the u-turn costs overwrite any turn costs set in TurnCostStorage
tCost = uTurnCosts;
} else {
if (turnRestrictionEnc != null)
tCost = turnCostStorage.get(turnRestrictionEnc, edgeFrom, nodeVia, edgeTo) ? Double.POSITIVE_INFINITY : 0;
}
return tCost;
}

@Override
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/com/graphhopper/util/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public class Constants {
public static final int VERSION_SHORTCUT = 9;
public static final int VERSION_NODE_CH = 0;
public static final int VERSION_GEOMETRY = 6;
public static final int VERSION_TURN_COSTS = 0;
public static final int VERSION_TURN_COSTS = 1;
public static final int VERSION_LOCATION_IDX = 5;
public static final int VERSION_KV_STORAGE = 2;
/**
Expand Down