Skip to content

Commit

Permalink
Merge branch 'issue_5955_reuse_split_vertices' into aachen
Browse files Browse the repository at this point in the history
  • Loading branch information
hbruch committed Jul 27, 2024
2 parents 2787322 + 66534b5 commit e9d28f5
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<SplitterVertex> permanentSplitterVertices;

private final Graph graph;

private final StopModel stopModel;
Expand All @@ -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(
Expand Down Expand Up @@ -456,6 +464,18 @@ private SplitterVertex split(
return v;
}

private SplitterVertex existingSplitterVertexAt(double x, double y) {
List<SplitterVertex> 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,
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand Down
6 changes: 3 additions & 3 deletions src/test/java/org/opentripplanner/routing/TestHalfEdges.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Edge> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

/**
Expand Down
233 changes: 233 additions & 0 deletions src/test/java/org/opentripplanner/street/model/UTurnTest.java
Original file line number Diff line number Diff line change
@@ -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<State, Edge, Vertex> 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<State, Edge, Vertex> 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<State, Edge, Vertex> 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<State, Edge, Vertex> 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<State, Edge, Vertex> 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<State, Edge, Vertex> path, String[] vertexLabels) {
assertNotNull(path);
List<State> 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);
}
}

0 comments on commit e9d28f5

Please sign in to comment.