diff --git a/core/src/main/java/com/graphhopper/GraphHopper.java b/core/src/main/java/com/graphhopper/GraphHopper.java index f39edee2578..f34b2850209 100644 --- a/core/src/main/java/com/graphhopper/GraphHopper.java +++ b/core/src/main/java/com/graphhopper/GraphHopper.java @@ -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.*; @@ -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.*; @@ -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 buildSubnetworkRemovalJobs() { List 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; } diff --git a/core/src/main/java/com/graphhopper/reader/osm/PrepareDeadEnds.java b/core/src/main/java/com/graphhopper/reader/osm/PrepareDeadEnds.java new file mode 100644 index 00000000000..dbedd934405 --- /dev/null +++ b/core/src/main/java/com/graphhopper/reader/osm/PrepareDeadEnds.java @@ -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); + } +} diff --git a/core/src/main/java/com/graphhopper/routing/DefaultWeightingFactory.java b/core/src/main/java/com/graphhopper/routing/DefaultWeightingFactory.java index accbf5653d8..d095158db9e 100644 --- a/core/src/main/java/com/graphhopper/routing/DefaultWeightingFactory.java +++ b/core/src/main/java/com/graphhopper/routing/DefaultWeightingFactory.java @@ -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; } diff --git a/core/src/main/java/com/graphhopper/routing/ev/DeadEnd.java b/core/src/main/java/com/graphhopper/routing/ev/DeadEnd.java new file mode 100644 index 00000000000..041f5efc6da --- /dev/null +++ b/core/src/main/java/com/graphhopper/routing/ev/DeadEnd.java @@ -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 { + + public static String key(String prefix) { + return getKey(prefix, "dead_end"); + } + + public static BooleanEncodedValue create(String name) { + return new SimpleBooleanEncodedValue(key(name), false); + } +} diff --git a/core/src/main/java/com/graphhopper/routing/util/VehicleEncodedValues.java b/core/src/main/java/com/graphhopper/routing/util/VehicleEncodedValues.java index 3b4ac26ed47..675fc716e6a 100644 --- a/core/src/main/java/com/graphhopper/routing/util/VehicleEncodedValues.java +++ b/core/src/main/java/com/graphhopper/routing/util/VehicleEncodedValues.java @@ -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"); @@ -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) { @@ -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) { @@ -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) { @@ -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 registerNewEncodedValue) { @@ -122,6 +128,8 @@ public void createEncodedValues(List registerNewEncodedValue) { public void createTurnCostEncodedValues(List registerNewTurnCostEncodedValues) { if (turnRestrictionEnc != null) registerNewTurnCostEncodedValues.add(turnRestrictionEnc); + if (deadEndEnc != null) + registerNewTurnCostEncodedValues.add(deadEndEnc); } public BooleanEncodedValue getAccessEnc() { @@ -140,6 +148,10 @@ public BooleanEncodedValue getTurnRestrictionEnc() { return turnRestrictionEnc; } + public BooleanEncodedValue getDeadEndEnc() { + return deadEndEnc; + } + public String getName() { return name; } diff --git a/core/src/main/java/com/graphhopper/routing/weighting/DefaultTurnCostProvider.java b/core/src/main/java/com/graphhopper/routing/weighting/DefaultTurnCostProvider.java index 299dd04a658..ccdabcac4be 100644 --- a/core/src/main/java/com/graphhopper/routing/weighting/DefaultTurnCostProvider.java +++ b/core/src/main/java/com/graphhopper/routing/weighting/DefaultTurnCostProvider.java @@ -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)"); } @@ -49,6 +50,7 @@ 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; } @@ -56,20 +58,20 @@ 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 diff --git a/core/src/main/java/com/graphhopper/util/Constants.java b/core/src/main/java/com/graphhopper/util/Constants.java index 498479f8d1a..0d63f885c53 100644 --- a/core/src/main/java/com/graphhopper/util/Constants.java +++ b/core/src/main/java/com/graphhopper/util/Constants.java @@ -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; /** diff --git a/core/src/test/java/com/graphhopper/GraphHopperTest.java b/core/src/test/java/com/graphhopper/GraphHopperTest.java index c983a20fd5c..841783f8dd0 100644 --- a/core/src/test/java/com/graphhopper/GraphHopperTest.java +++ b/core/src/test/java/com/graphhopper/GraphHopperTest.java @@ -23,10 +23,7 @@ import com.graphhopper.reader.ReaderWay; import com.graphhopper.reader.dem.SRTMProvider; import com.graphhopper.reader.dem.SkadiProvider; -import com.graphhopper.routing.ev.EdgeIntAccess; -import com.graphhopper.routing.ev.EncodedValueLookup; -import com.graphhopper.routing.ev.RoadEnvironment; -import com.graphhopper.routing.ev.Subnetwork; +import com.graphhopper.routing.ev.*; import com.graphhopper.routing.util.AllEdgesIterator; import com.graphhopper.routing.util.DefaultSnapFilter; import com.graphhopper.routing.util.EdgeFilter; @@ -1726,7 +1723,7 @@ public void testLMConstraints() { @Test public void testCreateWeightingHintsMerging() { final String profile = "profile"; - final String vehicle = "mtb"; + final String vehicle = "car"; GraphHopper hopper = new GraphHopper(). setGraphHopperLocation(GH_LOCATION). @@ -1734,13 +1731,19 @@ public void testCreateWeightingHintsMerging() { setProfiles(new Profile(profile).setVehicle(vehicle).setTurnCosts(true).putHint(U_TURN_COSTS, 123)); hopper.importOrLoad(); + BooleanEncodedValue deadEndEnc = hopper.getEncodingManager().getTurnBooleanEncodedValue(DeadEnd.key(vehicle)); + final int edge = 389; + final int node = 57; + // make sure we chose an edge that is a dead-end for this test + assertTrue(hopper.getBaseGraph().getEdgeIteratorState(edge, node).get(deadEndEnc)); + // if we do not pass u_turn_costs with the request hints we get those from the profile Weighting w = hopper.createWeighting(hopper.getProfiles().get(0), new PMap()); - assertEquals(123.0, w.calcTurnWeight(5, 6, 5)); + assertEquals(123.0, w.calcTurnWeight(edge, node, edge)); // we can overwrite the u_turn_costs given in the profile w = hopper.createWeighting(hopper.getProfiles().get(0), new PMap().putObject(U_TURN_COSTS, 46)); - assertEquals(46.0, w.calcTurnWeight(5, 6, 5)); + assertEquals(46.0, w.calcTurnWeight(edge, node, edge)); } @Test @@ -1950,7 +1953,7 @@ public void testTurnCostsOnOff() { req.setProfile("profile_turn_costs"); best = hopper.route(req).getBest(); - assertEquals(476, best.getDistance(), 1); + assertEquals(1043, best.getDistance(), 1); consistenceCheck(best); } @@ -2275,14 +2278,14 @@ public void testCHWithFiniteUTurnCosts() { GHPoint q = new GHPoint(43.73222, 7.415557); GHRequest req = new GHRequest(p, q); req.setProfile("my_profile"); - // we force the start/target directions such that there are u-turns right after we start and right before - // we reach the target. at the start location we do a u-turn at the crossing with the *steps* ('ghost junction') + // we force the start/target directions such that we need to turn around after the start and + // before we reach the target req.setCurbsides(Arrays.asList("right", "right")); GHResponse res = h.route(req); assertFalse(res.hasErrors(), "routing should not fail"); - assertEquals(242.5, res.getBest().getRouteWeight(), 0.1); - assertEquals(1917, res.getBest().getDistance(), 1); - assertEquals(243000, res.getBest().getTime(), 1000); + assertEquals(292.6, res.getBest().getRouteWeight(), 0.1); + assertEquals(2667, res.getBest().getDistance(), 1); + assertEquals(292000, res.getBest().getTime(), 1000); } @Test @@ -2667,6 +2670,7 @@ void curbsideWithSubnetwork_issue2502() { hopper.importOrLoad(); GHPoint pointA = new GHPoint(28.77428, -81.61593); GHPoint pointB = new GHPoint(28.773038, -81.611595); + GHPoint pointC = new GHPoint(28.773235, -81.613038); { // A->B GHRequest request = new GHRequest(pointA, pointB); @@ -2690,6 +2694,19 @@ void curbsideWithSubnetwork_issue2502() { double distance = response.getBest().getDistance(); assertEquals(2318, distance, 1); } + { + // A->C + // Stoneybrook Hills is only a 'dead-end' because of the subnetwork exclusion, but it + // still is a dead-end and needs to be marked as such, otherwise there would be a + // connection-not-found error. + GHRequest request = new GHRequest(pointA, pointC); + request.setProfile(profile); + request.setCurbsides(Arrays.asList("right", "right")); + GHResponse response = hopper.route(request); + assertFalse(response.hasErrors(), response.getErrors().toString()); + double distance = response.getBest().getDistance(); + assertEquals(488, distance, 1); + } } @Test diff --git a/core/src/test/java/com/graphhopper/reader/osm/PrepareDeadEndsTest.java b/core/src/test/java/com/graphhopper/reader/osm/PrepareDeadEndsTest.java new file mode 100644 index 00000000000..f94ac00a867 --- /dev/null +++ b/core/src/test/java/com/graphhopper/reader/osm/PrepareDeadEndsTest.java @@ -0,0 +1,117 @@ +/* + * 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.*; +import com.graphhopper.routing.util.EncodingManager; +import com.graphhopper.routing.weighting.SpeedWeighting; +import com.graphhopper.routing.weighting.Weighting; +import com.graphhopper.storage.BaseGraph; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PrepareDeadEndsTest { + + private BooleanEncodedValue subnetworkEnc; + private BooleanEncodedValue deadEndEnc; + private DecimalEncodedValue speedEnc; + private BaseGraph graph; + private Weighting weighting; + private PrepareDeadEnds prepareDeadEnds; + + @BeforeEach + void setup() { + subnetworkEnc = Subnetwork.create("car"); + deadEndEnc = DeadEnd.create("car"); + speedEnc = new DecimalEncodedValueImpl("speed", 5, 5, true); + EncodingManager encodingManager = EncodingManager.start() + .add(speedEnc) + .add(subnetworkEnc) + .addTurnCostEncodedValue(deadEndEnc) + .build(); + graph = new BaseGraph.Builder(encodingManager).withTurnCosts(true).create(); + prepareDeadEnds = new PrepareDeadEnds(graph); + weighting = new SpeedWeighting(speedEnc); + } + + + @Test + void basic() { + // 0 - 1 - 2 - 3 + graph.edge(0, 1).set(speedEnc, 10, 10); + graph.edge(1, 2).set(speedEnc, 10, 10); + graph.edge(2, 3).set(speedEnc, 10, 10); + prepareDeadEnds.findDeadEndUTurns(weighting, deadEndEnc, subnetworkEnc); + assertFalse(graph.getTurnCostStorage().get(deadEndEnc, 1, 2, 1)); + assertTrue(graph.getTurnCostStorage().get(deadEndEnc, 2, 3, 2)); + assertFalse(graph.getTurnCostStorage().get(deadEndEnc, 2, 2, 2)); + } + + @Test + void oneway() { + // 0 - 1 - 2 - 3 + // | | + // --------4 + graph.edge(0, 1).set(speedEnc, 10, 10); + graph.edge(1, 2).set(speedEnc, 10, 10); + graph.edge(2, 3).set(speedEnc, 10, 10); + graph.edge(1, 4).set(speedEnc, 10, 10); + graph.edge(4, 3).set(speedEnc, 10, 0); + prepareDeadEnds.findDeadEndUTurns(weighting, deadEndEnc, subnetworkEnc); + // arriving at 3 coming from 2 is a dead-end bc 4-3 is a one-way + // but arriving at 3 coming from 4 is not + assertTrue(graph.getTurnCostStorage().get(deadEndEnc, 2, 3, 2)); + assertFalse(graph.getTurnCostStorage().get(deadEndEnc, 4, 3, 4)); + } + + @Test + void inaccessibleEdge() { + // 0 - 1 - 2 + // \-- | + // \---3 + graph.edge(0, 1).set(speedEnc, 10, 10); + graph.edge(1, 2).set(speedEnc, 10, 10); + // 2-3 is not accessible, so there is a dead-end at node 2. This is often the case when a + // residential road ends in a path for example + graph.edge(2, 3).set(speedEnc, 0, 0); + graph.edge(0, 3).set(speedEnc, 10, 10); + prepareDeadEnds.findDeadEndUTurns(weighting, deadEndEnc, subnetworkEnc); + assertTrue(graph.getTurnCostStorage().get(deadEndEnc, 1, 2, 1)); + assertFalse(graph.getTurnCostStorage().get(deadEndEnc, 1, 1, 1)); + } + + @Test + void subnetwork() { + // 0 - 1 - 2 - 3 + graph.edge(0, 1).set(speedEnc, 10, 10); + graph.edge(1, 2).set(speedEnc, 10, 10); + // Here edge 2->3 is a oneway dead-end and thus forms another subnetwork. + // In this case there is also a dead-end at node 2, because going to 2-3 is not + // an option. + graph.edge(2, 3).set(speedEnc, 10, 0).set(subnetworkEnc, true); + prepareDeadEnds.findDeadEndUTurns(weighting, deadEndEnc, subnetworkEnc); + assertTrue(graph.getTurnCostStorage().get(deadEndEnc, 1, 2, 1)); + assertFalse(graph.getTurnCostStorage().get(deadEndEnc, 1, 1, 1)); + assertFalse(graph.getTurnCostStorage().get(deadEndEnc, 2, 2, 2)); + } + +} diff --git a/core/src/test/java/com/graphhopper/routing/weighting/FastestWeightingTest.java b/core/src/test/java/com/graphhopper/routing/weighting/FastestWeightingTest.java index 5011517e23b..b830799188f 100644 --- a/core/src/test/java/com/graphhopper/routing/weighting/FastestWeightingTest.java +++ b/core/src/test/java/com/graphhopper/routing/weighting/FastestWeightingTest.java @@ -40,7 +40,9 @@ public class FastestWeightingTest { private final BooleanEncodedValue accessEnc = new SimpleBooleanEncodedValue("access", true); private final DecimalEncodedValue speedEnc = new DecimalEncodedValueImpl("speed", 5, 5, false); private final BooleanEncodedValue turnRestrictionEnc = TurnRestriction.create("car"); - private final EncodingManager encodingManager = EncodingManager.start().add(accessEnc).add(speedEnc).addTurnCostEncodedValue(turnRestrictionEnc).build(); + private final BooleanEncodedValue deadEndEnc = DeadEnd.create("car"); + private final EncodingManager encodingManager = EncodingManager.start().add(accessEnc).add(speedEnc) + .addTurnCostEncodedValue(turnRestrictionEnc).addTurnCostEncodedValue(deadEndEnc).build(); private final BaseGraph graph = new BaseGraph.Builder(encodingManager).create(); @Test @@ -108,7 +110,7 @@ public void testTime() { @Test public void calcWeightAndTime_withTurnCosts() { BaseGraph graph = new BaseGraph.Builder(encodingManager).withTurnCosts(true).create(); - Weighting weighting = new FastestWeighting(accessEnc, speedEnc, new DefaultTurnCostProvider(turnRestrictionEnc, graph.getTurnCostStorage())); + Weighting weighting = new FastestWeighting(accessEnc, speedEnc, new DefaultTurnCostProvider(turnRestrictionEnc, deadEndEnc, graph.getTurnCostStorage())); GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(0, 1).setDistance(100)); EdgeIteratorState edge = GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(1, 2).setDistance(100)); setTurnRestriction(graph, 0, 1, 2); @@ -119,8 +121,11 @@ public void calcWeightAndTime_withTurnCosts() { @Test public void calcWeightAndTime_uTurnCosts() { BaseGraph graph = new BaseGraph.Builder(encodingManager).withTurnCosts(true).create(); - Weighting weighting = new FastestWeighting(accessEnc, speedEnc, new DefaultTurnCostProvider(turnRestrictionEnc, graph.getTurnCostStorage(), 40)); + Weighting weighting = new FastestWeighting(accessEnc, speedEnc, new DefaultTurnCostProvider(turnRestrictionEnc, deadEndEnc, graph.getTurnCostStorage(), 40)); EdgeIteratorState edge = GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(0, 1).setDistance(100)); + assertEquals(Double.POSITIVE_INFINITY, GHUtility.calcWeightWithTurnWeight(weighting, edge, false, 0), 1.e-6); + assertEquals(Long.MAX_VALUE, GHUtility.calcMillisWithTurnMillis(weighting, edge, false, 0), 1.e-6); + graph.getTurnCostStorage().set(deadEndEnc, 0, 0, 0, true); assertEquals(6 + 40, GHUtility.calcWeightWithTurnWeight(weighting, edge, false, 0), 1.e-6); assertEquals((6 + 40) * 1000, GHUtility.calcMillisWithTurnMillis(weighting, edge, false, 0), 1.e-6); } @@ -129,7 +134,7 @@ public void calcWeightAndTime_uTurnCosts() { public void calcWeightAndTime_withTurnCosts_shortest() { BaseGraph graph = new BaseGraph.Builder(encodingManager).withTurnCosts(true).create(); Weighting weighting = new ShortestWeighting(accessEnc, speedEnc, - new DefaultTurnCostProvider(turnRestrictionEnc, graph.getTurnCostStorage())); + new DefaultTurnCostProvider(turnRestrictionEnc, deadEndEnc, graph.getTurnCostStorage())); GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(0, 1).setDistance(100)); EdgeIteratorState edge = GHUtility.setSpeed(60, true, true, accessEnc, speedEnc, graph.edge(1, 2).setDistance(100)); setTurnRestriction(graph, 0, 1, 2); diff --git a/core/src/test/java/com/graphhopper/routing/weighting/custom/CustomWeightingTest.java b/core/src/test/java/com/graphhopper/routing/weighting/custom/CustomWeightingTest.java index 942715ac2e4..b844206f4ff 100644 --- a/core/src/test/java/com/graphhopper/routing/weighting/custom/CustomWeightingTest.java +++ b/core/src/test/java/com/graphhopper/routing/weighting/custom/CustomWeightingTest.java @@ -32,6 +32,7 @@ class CustomWeightingTest { EnumEncodedValue roadClassEnc; EncodingManager encodingManager; BooleanEncodedValue turnRestrictionEnc = TurnRestriction.create("car"); + BooleanEncodedValue deadEndEnc = DeadEnd.create("car"); @BeforeEach public void setup() { @@ -42,6 +43,7 @@ public void setup() { .add(Hazmat.create()) .add(RouteNetwork.create(BikeNetwork.KEY)) .addTurnCostEncodedValue(turnRestrictionEnc) + .addTurnCostEncodedValue(deadEndEnc) .build(); maxSpeedEnc = encodingManager.getDecimalEncodedValue(MaxSpeed.KEY); roadClassEnc = encodingManager.getEnumEncodedValue(KEY, RoadClass.class); @@ -409,7 +411,7 @@ public void testTime() { public void calcWeightAndTime_withTurnCosts() { BaseGraph graph = new BaseGraph.Builder(encodingManager).withTurnCosts(true).create(); Weighting weighting = CustomModelParser.createWeighting(accessEnc, avSpeedEnc, null, encodingManager, - new DefaultTurnCostProvider(turnRestrictionEnc, graph.getTurnCostStorage()), new CustomModel()); + new DefaultTurnCostProvider(turnRestrictionEnc, deadEndEnc, graph.getTurnCostStorage()), new CustomModel()); GHUtility.setSpeed(60, true, true, accessEnc, avSpeedEnc, graph.edge(0, 1).setDistance(100)); EdgeIteratorState edge = GHUtility.setSpeed(60, true, true, accessEnc, avSpeedEnc, graph.edge(1, 2).setDistance(100)); setTurnRestriction(graph, 0, 1, 2); @@ -421,8 +423,11 @@ public void calcWeightAndTime_withTurnCosts() { public void calcWeightAndTime_uTurnCosts() { BaseGraph graph = new BaseGraph.Builder(encodingManager).withTurnCosts(true).create(); Weighting weighting = CustomModelParser.createWeighting(accessEnc, avSpeedEnc, null, - encodingManager, new DefaultTurnCostProvider(turnRestrictionEnc, graph.getTurnCostStorage(), 40), new CustomModel()); + encodingManager, new DefaultTurnCostProvider(turnRestrictionEnc, deadEndEnc, graph.getTurnCostStorage(), 40), new CustomModel()); EdgeIteratorState edge = GHUtility.setSpeed(60, true, true, accessEnc, avSpeedEnc, graph.edge(0, 1).setDistance(100)); + assertEquals(Double.POSITIVE_INFINITY, GHUtility.calcWeightWithTurnWeight(weighting, edge, false, 0), 1.e-6); + assertEquals(Long.MAX_VALUE, GHUtility.calcMillisWithTurnMillis(weighting, edge, false, 0), 1.e-6); + graph.getTurnCostStorage().set(deadEndEnc, 0, 0, 0, true); assertEquals(6 + 40, GHUtility.calcWeightWithTurnWeight(weighting, edge, false, 0), 1.e-6); assertEquals((6 + 40) * 1000, GHUtility.calcMillisWithTurnMillis(weighting, edge, false, 0), 1.e-6); } @@ -431,7 +436,7 @@ public void calcWeightAndTime_uTurnCosts() { public void calcWeightAndTime_withTurnCosts_shortest() { BaseGraph graph = new BaseGraph.Builder(encodingManager).withTurnCosts(true).create(); Weighting weighting = new ShortestWeighting(accessEnc, avSpeedEnc, - new DefaultTurnCostProvider(turnRestrictionEnc, graph.getTurnCostStorage())); + new DefaultTurnCostProvider(turnRestrictionEnc, deadEndEnc, graph.getTurnCostStorage())); GHUtility.setSpeed(60, true, true, accessEnc, avSpeedEnc, graph.edge(0, 1).setDistance(100)); EdgeIteratorState edge = GHUtility.setSpeed(60, true, true, accessEnc, avSpeedEnc, graph.edge(1, 2).setDistance(100)); setTurnRestriction(graph, 0, 1, 2); diff --git a/example/src/main/java/com/graphhopper/example/RoutingExampleTC.java b/example/src/main/java/com/graphhopper/example/RoutingExampleTC.java index 4cda68fb83e..6e639ab608d 100644 --- a/example/src/main/java/com/graphhopper/example/RoutingExampleTC.java +++ b/example/src/main/java/com/graphhopper/example/RoutingExampleTC.java @@ -40,14 +40,14 @@ public static void routeWithTurnCostsAndCurbsides(GraphHopper hopper) { } public static void routeWithTurnCostsAndOtherUTurnCosts(GraphHopper hopper) { - GHRequest req = new GHRequest(42.50822, 1.533966, 42.506899, 1.525372) + GHRequest req = new GHRequest(42.497644, 1.500471, 42.498669, 1.501275) .setCurbsides(Arrays.asList(CURBSIDE_ANY, CURBSIDE_RIGHT)) // to change u-turn costs per request we have to disable CH. otherwise the u-turn costs we set per request // will be ignored and those set for our profile will be used. .putHint(Parameters.CH.DISABLE, true) .setProfile("car"); - route(hopper, req.putHint(Parameters.Routing.U_TURN_COSTS, 10), 1370, 98_700); - route(hopper, req.putHint(Parameters.Routing.U_TURN_COSTS, 15), 1370, 103_700); + route(hopper, req.putHint(Parameters.Routing.U_TURN_COSTS, 10), 443, 63_100); + route(hopper, req.putHint(Parameters.Routing.U_TURN_COSTS, 15), 443, 68_100); } private static void route(GraphHopper hopper, GHRequest req, int expectedDistance, int expectedTime) { @@ -71,7 +71,7 @@ static GraphHopper createGraphHopperInstance(String ghLoc) { // enabling turn costs means OSM turn restriction constraints like 'no_left_turn' will be taken into account .setTurnCosts(true) // we can also set u_turn_costs (in seconds). by default no u-turns are allowed, but with this setting - // we will consider u-turns at all junctions with a 40s time penalty + // we will consider u-turns at all dead-ends with a 40s time penalty .putHint("u_turn_costs", 40); hopper.setProfiles(profile); // enable CH for our profile. since turn costs are enabled this will take more time and memory to prepare than diff --git a/tools/src/main/java/com/graphhopper/tools/Measurement.java b/tools/src/main/java/com/graphhopper/tools/Measurement.java index 2554d348531..c613884295b 100644 --- a/tools/src/main/java/com/graphhopper/tools/Measurement.java +++ b/tools/src/main/java/com/graphhopper/tools/Measurement.java @@ -296,7 +296,7 @@ private GraphHopperConfig createConfigFromArgs(PMap args) { GraphHopperConfig ghConfig = new GraphHopperConfig(args); vehicle = args.getString("measurement.vehicle", "car"); boolean turnCosts = args.getBool("measurement.turn_costs", false); - int uTurnCosts = args.getInt("measurement.u_turn_costs", 40); + int uTurnCosts = args.getInt("measurement.u_turn_costs", 0); String weighting = args.getString("measurement.weighting", "custom"); boolean useCHEdge = args.getBool("measurement.ch.edge", true); boolean useCHNode = args.getBool("measurement.ch.node", true);