Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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);
}
}