diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/StreetLinkerModule.java b/application/src/main/java/org/opentripplanner/graph_builder/module/StreetLinkerModule.java index 83a6f282204..d50659f275c 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/StreetLinkerModule.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/StreetLinkerModule.java @@ -103,7 +103,7 @@ public void linkTransitStops(Graph graph, TimetableRepository timetableRepositor continue; } // check if stop is already linked, to allow multiple idempotent linking cycles - if (tStop.isConnectedToGraph()) { + if (isAlreadyLinked(tStop, stopLocationsUsedForFlexTrips)) { continue; } @@ -127,6 +127,26 @@ public void linkTransitStops(Graph graph, TimetableRepository timetableRepositor LOG.info(progress.completeMessage()); } + /** + * Determines if a given transit stop vertex is already linked to the street network, taking into + * account that flex stops need special linking to both a walkable and drivable edge. For example, + * the {@link OsmBoardingLocationsModule}, which runs before this one, often links stops to + * walkable edges only. + * + * @param stopVertex The transit stop vertex to be checked. + * @param stopLocationsUsedForFlexTrips A set of stop locations that are used for flexible trips. + */ + private static boolean isAlreadyLinked( + TransitStopVertex stopVertex, + Set stopLocationsUsedForFlexTrips + ) { + if (stopLocationsUsedForFlexTrips.contains(stopVertex.getStop())) { + return stopVertex.isLinkedToDrivableEdge() && stopVertex.isLinkedToWalkableEdge(); + } else { + return stopVertex.isConnectedToGraph(); + } + } + /** * Link a stop to the nearest "relevant" edges. *

diff --git a/application/src/main/java/org/opentripplanner/street/model/vertex/TransitStopVertex.java b/application/src/main/java/org/opentripplanner/street/model/vertex/TransitStopVertex.java index 82c977902b0..5b1b1dfa6a8 100644 --- a/application/src/main/java/org/opentripplanner/street/model/vertex/TransitStopVertex.java +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/TransitStopVertex.java @@ -1,9 +1,14 @@ package org.opentripplanner.street.model.vertex; +import static org.opentripplanner.street.search.TraverseMode.CAR; +import static org.opentripplanner.street.search.TraverseMode.WALK; + import java.util.HashSet; import java.util.Set; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.edge.PathwayEdge; +import org.opentripplanner.street.model.edge.StreetTransitEntityLink; +import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.transit.model.basic.Accessibility; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.site.RegularStop; @@ -94,4 +99,37 @@ public StationElement getStationElement() { public boolean isConnectedToGraph() { return getDegreeOut() + getDegreeIn() > 0; } + + /** + * Determines if this vertex is linked (via a {@link StreetTransitEntityLink}) to a drivable edge + * in the street network. + *

+ * This method is slow: only use this during graph build. + */ + public boolean isLinkedToDrivableEdge() { + return isLinkedToEdgeWhichAllows(CAR); + } + + /** + * Determines if this vertex is linked (via a {@link StreetTransitEntityLink}) to a walkable edge + * in the street network. + *

+ * This method is slow: only use this during graph build. + */ + public boolean isLinkedToWalkableEdge() { + return isLinkedToEdgeWhichAllows(WALK); + } + + private boolean isLinkedToEdgeWhichAllows(TraverseMode traverseMode) { + return getOutgoing() + .stream() + .anyMatch(edge -> + edge instanceof StreetTransitEntityLink link && + link + .getToVertex() + .getOutgoingStreetEdges() + .stream() + .anyMatch(se -> se.canTraverse(traverseMode)) + ); + } } diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java index 4f54581fdb8..10427812ac2 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java @@ -11,6 +11,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Set; import org.junit.jupiter.api.Test; import org.opentripplanner.ext.flex.trip.UnscheduledTrip; import org.opentripplanner.framework.application.OTPFeature; @@ -20,8 +21,10 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository; import org.opentripplanner.street.model._data.StreetModelForTest; +import org.opentripplanner.street.model.edge.BoardingLocationToStopLink; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.edge.StreetTransitStopLink; +import org.opentripplanner.street.model.vertex.OsmBoardingLocationVertex; import org.opentripplanner.street.model.vertex.SplitterVertex; import org.opentripplanner.street.model.vertex.TransitStopVertex; import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; @@ -103,6 +106,47 @@ void linkFlexStop() { }); } + @Test + void linkFlexStopWithBoardingLocation() { + OTPFeature.FlexRouting.testOn(() -> { + var model = new TestModel().withStopLinkedToBoardingLocation(); + var flexTrip = TimetableRepositoryForTest.of().unscheduledTrip("flex", model.stop()); + model.withFlexTrip(flexTrip); + + var module = model.streetLinkerModule(); + + module.buildGraph(); + + assertTrue(model.stopVertex().isConnectedToGraph()); + + // stop is used by a flex trip, needs to be linked to both the walk and car edge, + // also linked to the boarding location + assertThat(model.stopVertex().getOutgoing()).hasSize(3); + + // while the order of the link doesn't matter, it _is_ deterministic. + // first we have the link to the boarding location where the passengers are expected + // to wait. + var links = model.outgoingLinks(); + assertInstanceOf(BoardingLocationToStopLink.class, links.getFirst()); + + // the second link is the link to the walkable street network. this is not really necessary + // because the boarding location is walkable. this will be refactored away in the future. + var linkToWalk = links.get(1); + SplitterVertex walkSplit = (SplitterVertex) linkToWalk.getToVertex(); + + assertTrue(walkSplit.isConnectedToWalkingEdge()); + assertFalse(walkSplit.isConnectedToDriveableEdge()); + + // lastly we have the link to the drivable street network because vehicles also need to + // reach the stop if it's part of a flex trip. + var linkToCar = links.getLast(); + SplitterVertex carSplit = (SplitterVertex) linkToCar.getToVertex(); + + assertFalse(carSplit.isConnectedToWalkingEdge()); + assertTrue(carSplit.isConnectedToDriveableEdge()); + }); + } + @Test void linkCarsAllowedStop() { var model = new TestModel(); @@ -140,6 +184,7 @@ private static class TestModel { private final StreetLinkerModule module; private final RegularStop stop; private final TimetableRepository timetableRepository; + private final Graph graph; public TestModel() { var from = StreetModelForTest.intersectionVertex( @@ -151,7 +196,7 @@ public TestModel() { KONGSBERG_PLATFORM_1.x + DELTA ); - Graph graph = new Graph(); + this.graph = new Graph(); graph.addVertex(from); graph.addVertex(to); @@ -232,5 +277,23 @@ public void withCarsAllowedTrip(Trip trip, StopLocation... stops) { timetableRepository.addTripPattern(tripPattern.getId(), tripPattern); } + + /** + * Links the stop to a boarding location as can happen during regular graph build. + */ + public TestModel withStopLinkedToBoardingLocation() { + var boardingLocation = new OsmBoardingLocationVertex( + "boarding-location", + KONGSBERG_PLATFORM_1.x - 0.0001, + KONGSBERG_PLATFORM_1.y - 0.0001, + null, + Set.of(stop.getId().getId()) + ); + graph.addVertex(boardingLocation); + + BoardingLocationToStopLink.createBoardingLocationToStopLink(boardingLocation, stopVertex); + BoardingLocationToStopLink.createBoardingLocationToStopLink(stopVertex, boardingLocation); + return this; + } } }