diff --git a/src/main/java/org/opentripplanner/routing/linking/VertexLinker.java b/src/main/java/org/opentripplanner/routing/linking/VertexLinker.java index 9616fbab792..3540052f533 100644 --- a/src/main/java/org/opentripplanner/routing/linking/VertexLinker.java +++ b/src/main/java/org/opentripplanner/routing/linking/VertexLinker.java @@ -17,6 +17,7 @@ import org.locationtech.jts.operation.distance.DistanceOp; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.geometry.GeometryUtils; +import org.opentripplanner.framework.geometry.HashGridSpatialIndex; import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.index.EdgeSpatialIndex; @@ -69,6 +70,12 @@ public class VertexLinker { */ private final EdgeSpatialIndex edgeSpatialIndex; + /** + * Spatial index of permanent splitter vertices (only used during graph build) to reuse split + * vertices for forward and backward edges. + */ + private final HashGridSpatialIndex permanentSplitterVertices; + private final Graph graph; private final StopModel stopModel; @@ -86,6 +93,7 @@ public VertexLinker(Graph graph, StopModel stopModel, EdgeSpatialIndex edgeSpati this.graph = graph; this.vertexFactory = new VertexFactory(graph); this.stopModel = stopModel; + this.permanentSplitterVertices = new HashGridSpatialIndex<>(); } public void linkVertexPermanently( @@ -456,6 +464,18 @@ private SplitterVertex split( return v; } + private SplitterVertex existingSplitterVertexAt(double x, double y) { + List splitterVerticesAtLocation = permanentSplitterVertices + .query(new Envelope(x, x, y, y)) + .stream() + .filter(c -> c.getX() == x && c.getY() == y) + .toList(); + if (!splitterVerticesAtLocation.isEmpty()) { + return (SplitterVertex) splitterVerticesAtLocation.getFirst(); + } + return null; + } + private SplitterVertex splitVertex( StreetEdge originalEdge, Scope scope, @@ -477,7 +497,13 @@ private SplitterVertex splitVertex( tsv.setWheelchairAccessible(originalEdge.isWheelchairAccessible()); v = tsv; } else { - v = vertexFactory.splitter(originalEdge, x, y, uniqueSplitLabel); + SplitterVertex existingSplitterVertex = existingSplitterVertexAt(x, y); + if (existingSplitterVertex == null) { + v = vertexFactory.splitter(originalEdge, x, y, uniqueSplitLabel); + permanentSplitterVertices.insert(new Envelope(v.getCoordinate()), v); + } else { + v = existingSplitterVertex; + } } v.addRentalRestriction(originalEdge.getFromVertex().rentalRestrictions()); v.addRentalRestriction(originalEdge.getToVertex().rentalRestrictions()); diff --git a/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java b/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java index 06b10575ef9..48de5c41eba 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java @@ -82,12 +82,10 @@ void linkFlexStop() { SplitterVertex walkSplit = (SplitterVertex) linkToWalk.getToVertex(); assertTrue(walkSplit.isConnectedToWalkingEdge()); - assertFalse(walkSplit.isConnectedToDriveableEdge()); var linkToCar = model.outgoingLinks().getLast(); SplitterVertex carSplit = (SplitterVertex) linkToCar.getToVertex(); - assertFalse(carSplit.isConnectedToWalkingEdge()); assertTrue(carSplit.isConnectedToDriveableEdge()); }); } diff --git a/src/test/java/org/opentripplanner/graph_builder/module/osm/UnconnectedAreasTest.java b/src/test/java/org/opentripplanner/graph_builder/module/osm/UnconnectedAreasTest.java index 01221f16c31..b907f595ac8 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/osm/UnconnectedAreasTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/osm/UnconnectedAreasTest.java @@ -49,7 +49,7 @@ public void unconnectedCarParkAndRide() { int nParkAndRideEdge = gg.getEdgesOfType(VehicleParkingEdge.class).size(); assertEquals(12, nParkAndRide); - assertEquals(38, nParkAndRideLink); + assertEquals(30, nParkAndRideLink); assertEquals(42, nParkAndRideEdge); } @@ -66,7 +66,7 @@ public void unconnectedBikeParkAndRide() { int nParkAndRideEdge = gg.getEdgesOfType(VehicleParkingEdge.class).size(); assertEquals(13, nParkAndRideEntrances); - assertEquals(32, nParkAndRideLink); + assertEquals(26, nParkAndRideLink); assertEquals(33, nParkAndRideEdge); } diff --git a/src/test/java/org/opentripplanner/routing/TestHalfEdges.java b/src/test/java/org/opentripplanner/routing/TestHalfEdges.java index ba7e024e57a..1321377c282 100644 --- a/src/test/java/org/opentripplanner/routing/TestHalfEdges.java +++ b/src/test/java/org/opentripplanner/routing/TestHalfEdges.java @@ -675,16 +675,16 @@ public void testNetworkLinker() { int numVerticesBefore = graph.getVertices().size(); TestStreetLinkerModule.link(graph, transitModel); int numVerticesAfter = graph.getVertices().size(); - assertEquals(4, numVerticesAfter - numVerticesBefore); + assertEquals(2, numVerticesAfter - numVerticesBefore); Collection outgoing = station1.getOutgoing(); - assertEquals(2, outgoing.size()); + assertEquals(1, outgoing.size()); Edge edge = outgoing.iterator().next(); Vertex midpoint = edge.getToVertex(); assertTrue(Math.abs(midpoint.getCoordinate().y - 40.01) < 0.00000001); outgoing = station2.getOutgoing(); - assertEquals(2, outgoing.size()); + assertEquals(1, outgoing.size()); edge = outgoing.iterator().next(); Vertex station2point = edge.getToVertex(); diff --git a/src/test/java/org/opentripplanner/routing/linking/LinkStopToPlatformTest.java b/src/test/java/org/opentripplanner/routing/linking/LinkStopToPlatformTest.java index 23607d2e0b3..ca2f139fe70 100644 --- a/src/test/java/org/opentripplanner/routing/linking/LinkStopToPlatformTest.java +++ b/src/test/java/org/opentripplanner/routing/linking/LinkStopToPlatformTest.java @@ -142,11 +142,11 @@ public void testLinkStopOutsideArea() { LOG.debug("Edge {}", e); } - // Two bottom edges gets split into half (+2 edges) - // both split points are linked to the stop bidirectonally (+4 edges). - // both split points also link to 2 visibility points at opposite side (+8 edges) - // 14 new edges in total - assertEquals(22, graph.getEdges().size()); + // Two bottom edges gets split into half (+2 edges) by one (reused) split vertex + // the split point is linked to the stop bidirectonally (+2 edges). + // the split point also links to 2 visibility points at opposite side (+4 edges) + // 8 new edges in total + assertEquals(16, graph.getEdges().size()); } /** diff --git a/src/test/java/org/opentripplanner/street/model/UTurnTest.java b/src/test/java/org/opentripplanner/street/model/UTurnTest.java new file mode 100644 index 00000000000..9fa961504b4 --- /dev/null +++ b/src/test/java/org/opentripplanner/street/model/UTurnTest.java @@ -0,0 +1,233 @@ +package org.opentripplanner.street.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.street.model._data.StreetModelForTest.intersectionVertex; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; +import org.opentripplanner.astar.model.GraphPath; +import org.opentripplanner.astar.model.ShortestPathTree; +import org.opentripplanner.framework.geometry.GeometryUtils; +import org.opentripplanner.model.GenericLocation; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.routing.api.request.StreetMode; +import org.opentripplanner.routing.api.request.request.StreetRequest; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.linking.LinkingDirection; +import org.opentripplanner.street.model.edge.Edge; +import org.opentripplanner.street.model.edge.StreetEdge; +import org.opentripplanner.street.model.edge.StreetEdgeBuilder; +import org.opentripplanner.street.model.edge.StreetTransitStopLink; +import org.opentripplanner.street.model.vertex.StreetVertex; +import org.opentripplanner.street.model.vertex.TransitStopVertex; +import org.opentripplanner.street.model.vertex.Vertex; +import org.opentripplanner.street.search.StreetSearchBuilder; +import org.opentripplanner.street.search.TraverseMode; +import org.opentripplanner.street.search.TraverseModeSet; +import org.opentripplanner.street.search.state.State; +import org.opentripplanner.street.search.strategy.EuclideanRemainingWeightHeuristic; +import org.opentripplanner.transit.model._data.TransitModelForTest; + +public class UTurnTest { + + private Graph graph; + private Vertex topRight; + + private Vertex topLeft; + + private StreetEdge maple_main1, main_broad1; + + /* + This test constructs a simplified graph to test u turn avoidance. + Note: the coordinates are smaller than for other tests, as their distance is + important, especially for isCloseToStartOrEnd checks of the dominance function. + + b1 <--100-- ma1 <--100-- mp1 + ^ ^ I + 100 100 100 + I v v + b2 <--300-- ma2 <--800-- mp2 + + */ + @BeforeEach + public void before() { + graph = new Graph(); + // Graph for a fictional grid city with turn restrictions + StreetVertex maple1 = intersectionVertex("maple_1st", 0.002, 0.002); + graph.addVertex(maple1); + StreetVertex maple2 = intersectionVertex("maple_2nd", 0.001, 0.002); + graph.addVertex(maple2); + + StreetVertex main1 = intersectionVertex("main_1st", 0.002, 0.001); + graph.addVertex(main1); + StreetVertex main2 = intersectionVertex("main_2nd", 0.001, 0.001); + graph.addVertex(main2); + StreetVertex broad1 = intersectionVertex("broad_1st", 0.002, 0.0); + graph.addVertex(broad1); + StreetVertex broad2 = intersectionVertex("broad_2nd", 0.001, 0.0); + graph.addVertex(broad2); + + // Each block along the main streets has unit length and is one-way + StreetEdge maple1_2 = edge(maple1, maple2, 100.0, false); + StreetEdge main1_2 = edge(main1, main2, 100.0, false); + StreetEdge main2_1 = edge(main2, main1, 100.0, true); + StreetEdge broad2_1 = edge(broad2, broad1, 100.0, false); + + // Each cross-street connects + maple_main1 = edge(maple1, main1, 100.0, false); + main_broad1 = edge(main1, broad1, 100.0, false); + + StreetEdge maple_main2 = edge(maple2, main2, 800.0, false); + StreetEdge main_broad2 = edge(main2, broad2, 300.0, false); + + graph.index(null); + // Hold onto some vertices for the tests + topRight = maple1; + topLeft = broad1; + } + + @Test + public void testDefault() { + GraphPath path = getPath(); + + // The shortest path is 1st to Main, Main to Broad, 1st to 2nd. + + assertVertexSequence(path, new String[] { "maple_1st", "main_1st", "broad_1st" }); + } + + @Test + public void testNoUTurn() { + DisallowTurn(maple_main1, main_broad1); + + GraphPath path = getPath(); + + // Since there is a turn restrictions applied car mode, + // the shortest path is 1st to Main, Main to 2nd, 2nd to Broad. + // U turns usually are prevented by StreetEdge.doTraverse's isReversed check and + // the dominanceFunction which usually prevents that the same vertex is visited multiple times + // with the same mode. + + assertVertexSequence( + path, + new String[] { "maple_1st", "main_1st", "main_2nd", "broad_2nd", "broad_1st" } + ); + } + + @Test + public void testNoUTurnWithLinkedStop() { + DisallowTurn(maple_main1, main_broad1); + TransitStopVertex stop = TransitStopVertex + .of() + .withStop(TransitModelForTest.of().stop("UTurnTest:1234", 0.0015, 0.0011).build()) + .build(); + + // Stop linking splits forward and backward edge, currently with to distinct split vertices. + graph + .getLinker() + .linkVertexPermanently( + stop, + new TraverseModeSet(TraverseMode.WALK), + LinkingDirection.BOTH_WAYS, + (vertex, streetVertex) -> + List.of( + StreetTransitStopLink.createStreetTransitStopLink( + (TransitStopVertex) vertex, + streetVertex + ), + StreetTransitStopLink.createStreetTransitStopLink( + streetVertex, + (TransitStopVertex) vertex + ) + ) + ); + + GraphPath path = getPath(); + + // Since there is a turn restrictions applied car mode, + // the shortest path (without u-turn) should be 1st to Main, Main to 2nd, 2nd to Broad, back to 1st. + + assertVertexSequence( + path, + new String[] { "maple_1st", "main_1st", "split_", "main_2nd", "broad_2nd", "broad_1st" } + ); + } + + private GraphPath getPath() { + var request = new RouteRequest(); + // We set From/To explicitly, so that fromEnvelope/toEnvelope + request.setFrom(new GenericLocation(topRight.getLat(), topRight.getLon())); + request.setTo(new GenericLocation(topLeft.getLat(), topLeft.getLon())); + + ShortestPathTree tree = StreetSearchBuilder + .of() + .setHeuristic(new EuclideanRemainingWeightHeuristic()) + .setRequest(request) + .setStreetRequest(new StreetRequest(StreetMode.CAR)) + // It is necessary to set From/To explicitly, though it is provided via request already + .setFrom(topRight) + .setTo(topLeft) + .getShortestPathTree(); + + return tree.getPath(topLeft); + } + + private void assertVertexSequence(GraphPath path, String[] vertexLabels) { + assertNotNull(path); + List states = path.states; + assertEquals(vertexLabels.length, states.size()); + + for (int i = 0; i < vertexLabels.length; i++) { + // we check via startsWith, as splitting order is not deterministic. In consequence split_0 / split_1 both + // would be possible names of a visited node. + + String labelString = states.get(i).getVertex().getLabelString(); + assertTrue( + labelString.startsWith(vertexLabels[i]), + "state " + + i + + " does not match expected state: " + + labelString + + " should start with " + + vertexLabels[i] + ); + } + } + + /** + * Create an edge. If twoWay, create two edges (back and forth). + * + * @param back true if this is a reverse edge + */ + private StreetEdge edge(StreetVertex vA, StreetVertex vB, double length, boolean back) { + var labelA = vA.getLabel(); + var labelB = vB.getLabel(); + String name = String.format("%s_%s", labelA, labelB); + Coordinate[] coords = new Coordinate[2]; + coords[0] = vA.getCoordinate(); + coords[1] = vB.getCoordinate(); + LineString geom = GeometryUtils.getGeometryFactory().createLineString(coords); + + StreetTraversalPermission perm = StreetTraversalPermission.ALL; + return new StreetEdgeBuilder<>() + .withFromVertex(vA) + .withToVertex(vB) + .withGeometry(geom) + .withName(name) + .withMeterLength(length) + .withPermission(perm) + .withBack(back) + .buildAndConnect(); + } + + private void DisallowTurn(StreetEdge from, StreetEdge to) { + TurnRestrictionType rType = TurnRestrictionType.NO_TURN; + TraverseModeSet restrictedModes = new TraverseModeSet(TraverseMode.CAR); + TurnRestriction restrict = new TurnRestriction(from, to, rType, restrictedModes, null); + from.addTurnRestriction(restrict); + } +}