From 29d3319730f4b8a8e287adee0904cb783e54b9a0 Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Mon, 24 Nov 2025 10:28:24 +0200 Subject: [PATCH 01/15] Rename DirectStreetRouter -> DefaultDirectStreetRouter --- .../opentripplanner/routing/algorithm/RoutingWorker.java | 8 ++++++-- ...ctStreetRouter.java => DefaultDirectStreetRouter.java} | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) rename application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/{DirectStreetRouter.java => DefaultDirectStreetRouter.java} (99%) diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java b/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java index 66d9d66405b..3168ef405a0 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java @@ -23,8 +23,8 @@ import org.opentripplanner.routing.algorithm.raptoradapter.router.AdditionalSearchDays; import org.opentripplanner.routing.algorithm.raptoradapter.router.FilterTransitWhenDirectModeIsEmpty; import org.opentripplanner.routing.algorithm.raptoradapter.router.TransitRouter; +import org.opentripplanner.routing.algorithm.raptoradapter.router.street.DefaultDirectStreetRouter; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.DirectFlexRouter; -import org.opentripplanner.routing.algorithm.raptoradapter.router.street.DirectStreetRouter; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.api.request.request.StreetRequest; @@ -254,7 +254,11 @@ private RoutingResult routeDirectStreet() { debugTimingAggregator.startedDirectStreetRouter(); try { return RoutingResult.ok( - DirectStreetRouter.route(serverContext, directBuilder.buildRequest(), linkingContext()), + DefaultDirectStreetRouter.route( + serverContext, + directBuilder.buildRequest(), + linkingContext() + ), emptyDirectModeHandler.removeWalkAllTheWayResults() ); } catch (RoutingValidationException e) { diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java similarity index 99% rename from application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java rename to application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java index afa491332d9..c80ce2673be 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java @@ -25,7 +25,7 @@ * * @see DirectFlexRouter */ -public class DirectStreetRouter { +public class DefaultDirectStreetRouter { public static List route( OtpServerRequestContext serverContext, From 7250c773054a9517f8a7dbea4852ddc9287074e1 Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Mon, 24 Nov 2025 12:42:58 +0200 Subject: [PATCH 02/15] Refactor direct street router to use template method pattern --- .../routing/algorithm/RoutingWorker.java | 7 +- .../street/DefaultDirectStreetRouter.java | 105 +++----------- .../router/street/DirectStreetRouter.java | 133 ++++++++++++++++++ 3 files changed, 156 insertions(+), 89 deletions(-) create mode 100644 application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java b/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java index 3168ef405a0..256d8856bdb 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java @@ -253,12 +253,9 @@ private RoutingResult routeDirectStreet() { debugTimingAggregator.startedDirectStreetRouter(); try { + var directRouter = new DefaultDirectStreetRouter(); return RoutingResult.ok( - DefaultDirectStreetRouter.route( - serverContext, - directBuilder.buildRequest(), - linkingContext() - ), + directRouter.route(serverContext, directBuilder.buildRequest(), linkingContext()), emptyDirectModeHandler.removeWalkAllTheWayResults() ); } catch (RoutingValidationException e) { diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java index c80ce2673be..611d74bcc93 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java @@ -1,17 +1,10 @@ package org.opentripplanner.routing.algorithm.raptoradapter.router.street; -import java.util.Collections; import java.util.List; import org.opentripplanner.astar.model.GraphPath; -import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; -import org.opentripplanner.model.plan.Itinerary; -import org.opentripplanner.routing.algorithm.mapping.GraphPathToItineraryMapper; -import org.opentripplanner.routing.algorithm.mapping.ItinerariesHelper; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; -import org.opentripplanner.routing.error.PathNotFoundException; -import org.opentripplanner.routing.graphfinder.TransitServiceResolver; import org.opentripplanner.routing.impl.GraphPathFinder; import org.opentripplanner.routing.linking.LinkingContext; import org.opentripplanner.standalone.api.OtpServerRequestContext; @@ -21,96 +14,40 @@ /** * Generates "direct" street routes, i.e. those that do not use transit and are on the street - * network for the entire itinerary. - * - * @see DirectFlexRouter + * network for the entire itinerary. Doesn't support via locations or flex. */ -public class DefaultDirectStreetRouter { +public class DefaultDirectStreetRouter extends DirectStreetRouter { - public static List route( + List> findPaths( OtpServerRequestContext serverContext, + LinkingContext linkingContext, RouteRequest request, - LinkingContext linkingContext + float maxCarSpeed ) { - if (request.journey().direct().mode() == StreetMode.NOT_SET) { - return Collections.emptyList(); - } - OTPRequestTimeoutException.checkForTimeout(); - try { - var maxCarSpeed = serverContext.streetLimitationParametersService().maxCarSpeed(); - if (!straightLineDistanceIsWithinLimit(request, maxCarSpeed, linkingContext)) { - return Collections.emptyList(); - } - - // we could also get a persistent router-scoped GraphPathFinder but there's no setup cost here - GraphPathFinder gpFinder = new GraphPathFinder( - serverContext.traverseVisitor(), - serverContext.listExtensionRequestContexts(request), - maxCarSpeed - ); - List> paths = gpFinder.graphPathFinderEntryPoint( - request, - linkingContext - ); + // we could also get a persistent router-scoped GraphPathFinder but there's no setup cost here + GraphPathFinder gpFinder = new GraphPathFinder( + serverContext.traverseVisitor(), + serverContext.listExtensionRequestContexts(request), + maxCarSpeed + ); + return gpFinder.graphPathFinderEntryPoint(request, linkingContext); + } - // Convert the internal GraphPaths to itineraries - final GraphPathToItineraryMapper graphPathToItineraryMapper = new GraphPathToItineraryMapper( - new TransitServiceResolver(serverContext.transitService()), - serverContext.transitService().getTimeZone(), - serverContext.graph().streetNotesService, - serverContext.streetDetailsService(), - serverContext.graph().ellipsoidToGeoidDifference - ); - List response = graphPathToItineraryMapper.mapItineraries(paths, request); - response = ItinerariesHelper.decorateItinerariesWithRequestData( - response, - request.journey().wheelchair(), - request.preferences().wheelchair() - ); - return response; - } catch (PathNotFoundException e) { - return Collections.emptyList(); - } + boolean isRequestValidForRouting(RouteRequest request) { + return request.journey().direct().mode() == StreetMode.NOT_SET; } - private static boolean straightLineDistanceIsWithinLimit( + boolean isStraightLineDistanceIsWithinLimit( + LinkingContext linkingContext, RouteRequest request, - float maxCarSpeed, - LinkingContext linkingContext + double maxDistanceLimit ) { // TODO This currently only calculates the distances between the first fromVertex // and the first toVertex double distance = SphericalDistanceLibrary.distance( - linkingContext.findVertices(request.from()).iterator().next().getCoordinate(), - linkingContext.findVertices(request.to()).iterator().next().getCoordinate() + getFirstCoordinateForLocation(linkingContext, request.from()), + getFirstCoordinateForLocation(linkingContext, request.to()) ); - return distance < calculateDistanceMaxLimit(request, maxCarSpeed); - } - - /** - * Calculates the maximum distance in meters based on the maxDirectStreetDuration and the - * fastest mode available. This assumes that it is not possible to exceed the speed defined in the - * RouteRequest. - */ - private static double calculateDistanceMaxLimit(RouteRequest request, float maxCarSpeed) { - var preferences = request.preferences(); - double distanceLimit; - StreetMode mode = request.journey().direct().mode(); - - double durationLimit = preferences.street().maxDirectDuration().valueOf(mode).toSeconds(); - - if (mode.includesDriving()) { - distanceLimit = durationLimit * maxCarSpeed; - } else if (mode.includesBiking()) { - distanceLimit = durationLimit * preferences.bike().speed(); - } else if (mode.includesScooter()) { - distanceLimit = durationLimit * preferences.scooter().speed(); - } else if (mode.includesWalking()) { - distanceLimit = durationLimit * preferences.walk().speed(); - } else { - throw new IllegalStateException("Could not set max limit for StreetMode"); - } - - return distanceLimit; + return distance < maxDistanceLimit; } } diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java new file mode 100644 index 00000000000..eef32d7a2a1 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java @@ -0,0 +1,133 @@ +package org.opentripplanner.routing.algorithm.raptoradapter.router.street; + +import java.util.Collections; +import java.util.List; +import org.locationtech.jts.geom.Coordinate; +import org.opentripplanner.astar.model.GraphPath; +import org.opentripplanner.framework.application.OTPRequestTimeoutException; +import org.opentripplanner.model.GenericLocation; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.routing.algorithm.mapping.GraphPathToItineraryMapper; +import org.opentripplanner.routing.algorithm.mapping.ItinerariesHelper; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.routing.api.request.StreetMode; +import org.opentripplanner.routing.error.PathNotFoundException; +import org.opentripplanner.routing.graphfinder.TransitServiceResolver; +import org.opentripplanner.routing.linking.LinkingContext; +import org.opentripplanner.standalone.api.OtpServerRequestContext; +import org.opentripplanner.street.model.edge.Edge; +import org.opentripplanner.street.model.vertex.Vertex; +import org.opentripplanner.street.search.state.State; + +/** + * Abstract class for generating "direct" street routes, i.e. those that do not use transit and are + * on the street network for the entire itinerary. For flex routing, use {@link DirectFlexRouter}. + * Follows template method pattern. + */ +public abstract class DirectStreetRouter { + + /** + * @return direct street itineraries. + */ + public List route( + OtpServerRequestContext serverContext, + RouteRequest request, + LinkingContext linkingContext + ) { + if (isRequestValidForRouting(request)) { + return Collections.emptyList(); + } + OTPRequestTimeoutException.checkForTimeout(); + + var maxCarSpeed = serverContext.streetLimitationParametersService().maxCarSpeed(); + var maxDistanceLimit = calculateDistanceMaxLimit(request, maxCarSpeed); + if (!isStraightLineDistanceIsWithinLimit(linkingContext, request, maxDistanceLimit)) { + return Collections.emptyList(); + } + + try { + var paths = findPaths(serverContext, linkingContext, request, maxCarSpeed); + return mapToItineraries(serverContext, request, paths); + } catch (PathNotFoundException e) { + return Collections.emptyList(); + } + } + + /** + * Checks that the route request is configured to allow direct street results. + */ + abstract boolean isRequestValidForRouting(RouteRequest request); + + /** + * Checks that as the crow flies distance between locations in the search are within the maximum + * distance limit. + */ + abstract boolean isStraightLineDistanceIsWithinLimit( + LinkingContext linkingContext, + RouteRequest request, + double maxDistanceLimit + ); + + /** + * Find graph paths between the locations in the request. + */ + abstract List> findPaths( + OtpServerRequestContext serverContext, + LinkingContext linkingContext, + RouteRequest request, + float maxCarSpeed + ); + + static Coordinate getFirstCoordinateForLocation( + LinkingContext context, + GenericLocation location + ) { + return context.findVertices(location).iterator().next().getCoordinate(); + } + + /** + * Calculates the maximum distance in meters based on the maxDirectStreetDuration and the + * fastest mode available. This assumes that it is not possible to exceed the speed defined in the + * RouteRequest. + */ + private static double calculateDistanceMaxLimit(RouteRequest request, float maxCarSpeed) { + var preferences = request.preferences(); + StreetMode mode = request.journey().direct().mode(); + + double durationLimit = preferences.street().maxDirectDuration().valueOf(mode).toSeconds(); + + if (mode.includesDriving()) { + return durationLimit * maxCarSpeed; + } + if (mode.includesBiking()) { + return durationLimit * preferences.bike().speed(); + } + if (mode.includesScooter()) { + return durationLimit * preferences.scooter().speed(); + } + if (mode.includesWalking()) { + return durationLimit * preferences.walk().speed(); + } + throw new IllegalStateException("Could not set max limit for StreetMode"); + } + + private static List mapToItineraries( + OtpServerRequestContext serverContext, + RouteRequest request, + List> paths + ) { + final GraphPathToItineraryMapper graphPathToItineraryMapper = new GraphPathToItineraryMapper( + new TransitServiceResolver(serverContext.transitService()), + serverContext.transitService().getTimeZone(), + serverContext.graph().streetNotesService, + serverContext.streetDetailsService(), + serverContext.graph().ellipsoidToGeoidDifference + ); + List response = graphPathToItineraryMapper.mapItineraries(paths, request); + return ItinerariesHelper.decorateItinerariesWithRequestData( + response, + request.journey().wheelchair(), + request.preferences().wheelchair() + ); + } +} From af836f6a1754f1692c31e753efb50f32a50051a6 Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Mon, 24 Nov 2025 20:15:47 +0200 Subject: [PATCH 03/15] Simplify direct routing interface so that paths and itineraries are not lists --- .../routing/CarpoolStreetRouter.java | 10 +-- .../routing/algorithm/RoutingWorker.java | 5 +- .../mapping/GraphPathToItineraryMapper.java | 17 ++-- .../algorithm/mapping/ItinerariesHelper.java | 31 +++---- .../street/DefaultDirectStreetRouter.java | 3 +- .../router/street/DirectStreetRouter.java | 35 ++++---- .../routing/impl/GraphPathFinder.java | 82 ++++++++++--------- .../visualizer/GraphVisualizer.java | 6 +- .../module/osm/OsmModuleTest.java | 9 +- .../integration/BarrierRoutingTest.java | 50 ++++++----- .../integration/BicycleRoutingTest.java | 26 +++--- .../street/integration/CarRoutingTest.java | 26 +++--- .../SplitEdgeTurnRestrictionsTest.java | 26 +++--- .../street/integration/WalkRoutingTest.java | 16 ++-- .../java/org/opentripplanner/astar/AStar.java | 15 ++-- .../opentripplanner/astar/AStarBuilder.java | 5 +- 16 files changed, 168 insertions(+), 194 deletions(-) diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/CarpoolStreetRouter.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/CarpoolStreetRouter.java index e11b7542bca..4e573b8d00b 100644 --- a/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/CarpoolStreetRouter.java +++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/CarpoolStreetRouter.java @@ -4,7 +4,6 @@ import java.util.Set; import org.opentripplanner.astar.model.GraphPath; import org.opentripplanner.astar.strategy.DurationSkipEdgeStrategy; -import org.opentripplanner.astar.strategy.PathComparator; import org.opentripplanner.core.model.i18n.NonLocalizedString; import org.opentripplanner.model.GenericLocation; import org.opentripplanner.routing.api.request.RouteRequest; @@ -195,13 +194,6 @@ private GraphPath carpoolRouting( .withFrom(fromVertices) .withTo(toVertices); - List> paths = streetSearch.getPathsToTarget(); - paths.sort(new PathComparator(request.arriveBy())); - - if (paths.isEmpty()) { - return null; - } - - return paths.getFirst(); + return streetSearch.getPathToTarget().orElse(null); } } diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java b/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java index 256d8856bdb..b31a38f9109 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java @@ -255,7 +255,10 @@ private RoutingResult routeDirectStreet() { try { var directRouter = new DefaultDirectStreetRouter(); return RoutingResult.ok( - directRouter.route(serverContext, directBuilder.buildRequest(), linkingContext()), + directRouter + .route(serverContext, directBuilder.buildRequest(), linkingContext()) + .stream() + .toList(), emptyDirectModeHandler.removeWalkAllTheWayResults() ); } catch (RoutingValidationException e) { diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapper.java b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapper.java index f5c1f7e0ec5..9ef3e037122 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapper.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapper.java @@ -8,6 +8,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.LineString; @@ -103,20 +104,16 @@ public static boolean isFloatingRentalDropoff(State state) { /** * Generates a TripPlan from a set of paths */ - public List mapItineraries( - List> paths, + public Optional mapToItinerary( + GraphPath path, RouteRequest request ) { - List itineraries = new LinkedList<>(); - for (GraphPath path : paths) { - Itinerary itinerary = generateItinerary(path, request); - if (itinerary.legs().isEmpty()) { - continue; - } - itineraries.add(itinerary); + Itinerary itinerary = generateItinerary(path, request); + if (itinerary.legs().isEmpty()) { + return Optional.empty(); } - return itineraries; + return Optional.of(itinerary); } /** diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/ItinerariesHelper.java b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/ItinerariesHelper.java index 7d23d9b0c90..6f2487a9ccc 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/ItinerariesHelper.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/ItinerariesHelper.java @@ -1,6 +1,5 @@ package org.opentripplanner.routing.algorithm.mapping; -import java.util.ArrayList; import java.util.List; import java.util.OptionalDouble; import org.opentripplanner.model.plan.Itinerary; @@ -11,31 +10,25 @@ public class ItinerariesHelper { - public static List decorateItinerariesWithRequestData( - List itineraries, + public static Itinerary decorateItineraryWithRequestData( + Itinerary itinerary, boolean wheelchairEnabled, WheelchairPreferences wheelchairPreferences ) { if (!wheelchairEnabled) { - return itineraries; + return itinerary; } - var result = new ArrayList(); - boolean dirty = false; - for (Itinerary it : itineraries) { - // Communicate the fact that the only way we were able to get a response - // was by removing a slope limit. - OptionalDouble maxSlope = getMaxSlope(it); - if (maxSlope.isPresent()) { - dirty = true; - itineraries.add( - it.copyOf().withMaxSlope(wheelchairPreferences.maxSlope(), maxSlope.getAsDouble()).build() - ); - } else { - result.add(it); - } + // Communicate the fact that the only way we were able to get a response + // was by removing a slope limit. + OptionalDouble maxSlope = getMaxSlope(itinerary); + if (maxSlope.isPresent()) { + return itinerary + .copyOf() + .withMaxSlope(wheelchairPreferences.maxSlope(), maxSlope.getAsDouble()) + .build(); } - return dirty ? result : itineraries; + return itinerary; } private static OptionalDouble getMaxSlope(Itinerary it) { diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java index 611d74bcc93..ffff7f7a7f1 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java @@ -1,6 +1,5 @@ package org.opentripplanner.routing.algorithm.raptoradapter.router.street; -import java.util.List; import org.opentripplanner.astar.model.GraphPath; import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; import org.opentripplanner.routing.api.request.RouteRequest; @@ -18,7 +17,7 @@ */ public class DefaultDirectStreetRouter extends DirectStreetRouter { - List> findPaths( + GraphPath findPath( OtpServerRequestContext serverContext, LinkingContext linkingContext, RouteRequest request, diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java index eef32d7a2a1..260e341b3f1 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java @@ -1,7 +1,6 @@ package org.opentripplanner.routing.algorithm.raptoradapter.router.street; -import java.util.Collections; -import java.util.List; +import java.util.Optional; import org.locationtech.jts.geom.Coordinate; import org.opentripplanner.astar.model.GraphPath; import org.opentripplanner.framework.application.OTPRequestTimeoutException; @@ -29,27 +28,27 @@ public abstract class DirectStreetRouter { /** * @return direct street itineraries. */ - public List route( + public Optional route( OtpServerRequestContext serverContext, RouteRequest request, LinkingContext linkingContext ) { if (isRequestValidForRouting(request)) { - return Collections.emptyList(); + return Optional.empty(); } OTPRequestTimeoutException.checkForTimeout(); var maxCarSpeed = serverContext.streetLimitationParametersService().maxCarSpeed(); var maxDistanceLimit = calculateDistanceMaxLimit(request, maxCarSpeed); if (!isStraightLineDistanceIsWithinLimit(linkingContext, request, maxDistanceLimit)) { - return Collections.emptyList(); + return Optional.empty(); } try { - var paths = findPaths(serverContext, linkingContext, request, maxCarSpeed); - return mapToItineraries(serverContext, request, paths); + var path = findPath(serverContext, linkingContext, request, maxCarSpeed); + return mapToItinerary(serverContext, request, path); } catch (PathNotFoundException e) { - return Collections.emptyList(); + return Optional.empty(); } } @@ -69,9 +68,9 @@ abstract boolean isStraightLineDistanceIsWithinLimit( ); /** - * Find graph paths between the locations in the request. + * Find a graph path between the locations in the request. */ - abstract List> findPaths( + abstract GraphPath findPath( OtpServerRequestContext serverContext, LinkingContext linkingContext, RouteRequest request, @@ -111,10 +110,10 @@ private static double calculateDistanceMaxLimit(RouteRequest request, float maxC throw new IllegalStateException("Could not set max limit for StreetMode"); } - private static List mapToItineraries( + private static Optional mapToItinerary( OtpServerRequestContext serverContext, RouteRequest request, - List> paths + GraphPath path ) { final GraphPathToItineraryMapper graphPathToItineraryMapper = new GraphPathToItineraryMapper( new TransitServiceResolver(serverContext.transitService()), @@ -123,11 +122,13 @@ private static List mapToItineraries( serverContext.streetDetailsService(), serverContext.graph().ellipsoidToGeoidDifference ); - List response = graphPathToItineraryMapper.mapItineraries(paths, request); - return ItinerariesHelper.decorateItinerariesWithRequestData( - response, - request.journey().wheelchair(), - request.preferences().wheelchair() + var response = graphPathToItineraryMapper.mapToItinerary(path, request); + return response.map(itinerary -> + ItinerariesHelper.decorateItineraryWithRequestData( + itinerary, + request.journey().wheelchair(), + request.preferences().wheelchair() + ) ); } } diff --git a/application/src/main/java/org/opentripplanner/routing/impl/GraphPathFinder.java b/application/src/main/java/org/opentripplanner/routing/impl/GraphPathFinder.java index 2f50b57d0cc..d407038362c 100644 --- a/application/src/main/java/org/opentripplanner/routing/impl/GraphPathFinder.java +++ b/application/src/main/java/org/opentripplanner/routing/impl/GraphPathFinder.java @@ -1,15 +1,14 @@ package org.opentripplanner.routing.impl; import java.util.Collection; -import java.util.Iterator; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; import org.opentripplanner.astar.model.GraphPath; import org.opentripplanner.astar.spi.TraverseVisitor; import org.opentripplanner.astar.strategy.DurationSkipEdgeStrategy; -import org.opentripplanner.astar.strategy.PathComparator; import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.preference.StreetPreferences; @@ -77,7 +76,7 @@ public GraphPathFinder( * This no longer does "trip banning" to find multiple itineraries. It just searches once trying * to find a non-transit path. */ - public List> getPaths( + public Optional> getPath( RouteRequest request, Set from, Set to @@ -111,18 +110,21 @@ public List> getPaths( long searchBeginTime = System.currentTimeMillis(); LOG.debug("BEGIN SEARCH"); - List> paths = aStar.getPathsToTarget(); + var path = aStar.getPathToTarget(); - LOG.debug("we have {} paths", paths.size()); + if (path.isPresent()) { + LOG.debug("Found a path"); + } else { + LOG.debug("Found no paths"); + } LOG.debug("END SEARCH ({} msec)", System.currentTimeMillis() - searchBeginTime); - paths.sort(new PathComparator(request.arriveBy())); - return paths; + return path; } /** * Try to find N paths through the Graph */ - public List> graphPathFinderEntryPoint( + public GraphPath graphPathFinderEntryPoint( RouteRequest request, LinkingContext linkingContext ) { @@ -133,7 +135,7 @@ public List> graphPathFinderEntryPoint( ); } - public List> graphPathFinderEntryPoint( + public GraphPath graphPathFinderEntryPoint( RouteRequest request, Set from, Set to @@ -141,42 +143,42 @@ public List> graphPathFinderEntryPoint( OTPRequestTimeoutException.checkForTimeout(); var reqTime = request.dateTime() == null ? RouteRequest.normalizeNow() : request.dateTime(); - List> paths = getPaths(request, from, to); + var path = getPath(request, from, to).orElseThrow(() -> { + logNoPathsFound(request); + return new PathNotFoundException(); + }); // Detect and report that most obnoxious of bugs: path reversal asymmetry. - // Removing paths might result in an empty list, so do this check before the empty list check. - if (paths != null) { - Iterator> gpi = paths.iterator(); - while (gpi.hasNext()) { - GraphPath graphPath = gpi.next(); - // TODO check, is it possible that arriveBy and time are modifed in-place by the search? - if (request.arriveBy()) { - if (graphPath.states.getLast().getTimeAccurate().isAfter(reqTime)) { - LOG.error( - "A graph path arrives {} after the requested time {}. This implies a bug.", - graphPath.states.getLast().getTimeAccurate(), - reqTime - ); - gpi.remove(); - } - } else { - if (graphPath.states.getFirst().getTimeAccurate().isBefore(reqTime)) { - LOG.error( - "A graph path leaves {} before the requested time {}. This implies a bug.", - graphPath.states.getFirst().getTimeAccurate(), - reqTime - ); - gpi.remove(); - } - } + // TODO check, is it possible that arriveBy and time are modifed in-place by the search? + if (request.arriveBy()) { + if (path.states.getLast().getTimeAccurate().isAfter(reqTime)) { + LOG.error( + "A graph path arrives {} after the requested time {}. This implies a bug.", + path.states.getLast().getTimeAccurate(), + reqTime + ); + logAndThrowNoPathsFound(request); + } + } else { + if (path.states.getFirst().getTimeAccurate().isBefore(reqTime)) { + LOG.error( + "A graph path leaves {} before the requested time {}. This implies a bug.", + path.states.getFirst().getTimeAccurate(), + reqTime + ); + logAndThrowNoPathsFound(request); } } - if (paths == null || paths.isEmpty()) { - LOG.debug("Path not found: {} : {}", request.from(), request.to()); - throw new PathNotFoundException(); - } + return path; + } + + private void logNoPathsFound(RouteRequest request) { + LOG.debug("Path not found: {} : {}", request.from(), request.to()); + } - return paths; + private void logAndThrowNoPathsFound(RouteRequest request) { + logNoPathsFound(request); + throw new PathNotFoundException(); } } diff --git a/application/src/main/java/org/opentripplanner/visualizer/GraphVisualizer.java b/application/src/main/java/org/opentripplanner/visualizer/GraphVisualizer.java index ec6b390fb01..310bc5e72b5 100644 --- a/application/src/main/java/org/opentripplanner/visualizer/GraphVisualizer.java +++ b/application/src/main/java/org/opentripplanner/visualizer/GraphVisualizer.java @@ -530,7 +530,7 @@ protected void route(String from, String to) { ); var linkingRequest = LinkingContextRequestMapper.map(request); var linkingContext = linkingContextFactory.create(temporaryVerticesContainer, linkingRequest); - List> paths = finder.graphPathFinderEntryPoint( + GraphPath path = finder.graphPathFinderEntryPoint( request, linkingContext ); @@ -546,7 +546,7 @@ protected void route(String from, String to) { System.out.println( "got spt:"+spt ); */ - if (paths == null) { + if (path == null) { System.out.println("no path"); showGraph.highlightGraphPath(null); return; @@ -555,7 +555,7 @@ protected void route(String from, String to) { // now's a convenient time to set graphical SPT weights showGraph.simpleSPT.setWeights(); - showPathsInPanel(paths); + showPathsInPanel(List.of(path)); // now's a good time to set showGraph's SPT drawing weights showGraph.setSPTFlattening(Float.parseFloat(sptFlattening.getText())); diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/OsmModuleTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/OsmModuleTest.java index ac98655f874..ebc1d76cc95 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/osm/OsmModuleTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/osm/OsmModuleTest.java @@ -280,17 +280,14 @@ private void testBuildingAreas(boolean skipVisibility) { Vertex topV = graph.getVertex(VertexLabel.osm(559271124)); GraphPathFinder graphPathFinder = new GraphPathFinder(null); - List> pathList = graphPathFinder.graphPathFinderEntryPoint( + GraphPath path = graphPathFinder.graphPathFinderEntryPoint( request, Set.of(bottomV), Set.of(topV) ); - assertNotNull(pathList); - assertFalse(pathList.isEmpty()); - for (GraphPath path : pathList) { - assertFalse(path.states.isEmpty()); - } + assertNotNull(path); + assertFalse(path.states.isEmpty()); } private record VertexPair(Vertex v0, Vertex v1) {} diff --git a/application/src/test/java/org/opentripplanner/street/integration/BarrierRoutingTest.java b/application/src/test/java/org/opentripplanner/street/integration/BarrierRoutingTest.java index c6e4e879b6f..57588499138 100644 --- a/application/src/test/java/org/opentripplanner/street/integration/BarrierRoutingTest.java +++ b/application/src/test/java/org/opentripplanner/street/integration/BarrierRoutingTest.java @@ -79,26 +79,22 @@ public void shouldWalkForBarriers() { rr.withPreferences(p -> p.withBike(it -> it.withWalking(walking -> walking.withReluctance(1d))) ), - itineraries -> - itineraries - .stream() - .flatMap(i -> - Stream.of( - () -> assertEquals(1, i.legs().size()), - () -> assertEquals(TraverseMode.BICYCLE, i.streetLeg(0).getMode()), - () -> - assertEquals( - List.of(false, true, false, true, false), - i - .legs() - .get(0) - .listWalkSteps() - .stream() - .map(WalkStep::isWalkingBike) - .collect(Collectors.toList()) - ) + itinerary -> + Stream.of( + () -> assertEquals(1, itinerary.legs().size()), + () -> assertEquals(TraverseMode.BICYCLE, itinerary.streetLeg(0).getMode()), + () -> + assertEquals( + List.of(false, true, false, true, false), + itinerary + .legs() + .get(0) + .listWalkSteps() + .stream() + .map(WalkStep::isWalkingBike) + .collect(Collectors.toList()) ) - ) + ) ); assertThatPolylinesAreEqual(polyline2, "o~qgH_ccu@Bi@Bk@Bi@Bg@NaA@_@Dm@Dq@a@KJy@@I@M@E??"); } @@ -148,10 +144,10 @@ private static String computePolyline( to, streetMode, ignored -> {}, - itineraries -> - itineraries + itinerary -> + itinerary + .legs() .stream() - .flatMap(i -> i.legs().stream()) .map( l -> () -> @@ -179,7 +175,7 @@ private static String computePolyline( GenericLocation to, StreetMode streetMode, Consumer options, - Function, Stream> assertions + Function> assertions ) { var builder = RouteRequest.of() .withDateTime(dateTime) @@ -197,7 +193,7 @@ private static String computePolyline( var linkingRequest = LinkingContextRequestMapper.map(request); var linkingContext = linkingContextFactory.create(temporaryVerticesContainer, linkingRequest); var gpf = new GraphPathFinder(null); - var paths = gpf.graphPathFinderEntryPoint(request, linkingContext); + var path = gpf.graphPathFinderEntryPoint(request, linkingContext); GraphPathToItineraryMapper graphPathToItineraryMapper = new GraphPathToItineraryMapper( new NoopSiteResolver(), @@ -207,11 +203,11 @@ private static String computePolyline( graph.ellipsoidToGeoidDifference ); - var itineraries = graphPathToItineraryMapper.mapItineraries(paths, request); + var itinerary = graphPathToItineraryMapper.mapToItinerary(path, request).get(); - assertAll(assertions.apply(itineraries)); + assertAll(assertions.apply(itinerary)); - Geometry legGeometry = itineraries.get(0).legs().get(0).legGeometry(); + Geometry legGeometry = itinerary.legs().getFirst().legGeometry(); temporaryVerticesContainer.close(); return EncodedPolyline.of(legGeometry).points(); diff --git a/application/src/test/java/org/opentripplanner/street/integration/BicycleRoutingTest.java b/application/src/test/java/org/opentripplanner/street/integration/BicycleRoutingTest.java index 7352c6355ce..52918181ea0 100644 --- a/application/src/test/java/org/opentripplanner/street/integration/BicycleRoutingTest.java +++ b/application/src/test/java/org/opentripplanner/street/integration/BicycleRoutingTest.java @@ -98,7 +98,7 @@ private static String computePolyline(Graph graph, GenericLocation from, Generic var linkingRequest = LinkingContextRequestMapper.map(request); var linkingContext = linkingContextFactory.create(temporaryVerticesContainer, linkingRequest); var gpf = new GraphPathFinder(null); - var paths = gpf.graphPathFinderEntryPoint(request, linkingContext); + var path = gpf.graphPathFinderEntryPoint(request, linkingContext); GraphPathToItineraryMapper graphPathToItineraryMapper = new GraphPathToItineraryMapper( new NoopSiteResolver(), @@ -108,22 +108,20 @@ private static String computePolyline(Graph graph, GenericLocation from, Generic graph.ellipsoidToGeoidDifference ); - var itineraries = graphPathToItineraryMapper.mapItineraries(paths, request); + var itinerary = graphPathToItineraryMapper.mapToItinerary(path, request).get(); temporaryVerticesContainer.close(); // make sure that we only get BICYCLE legs - itineraries.forEach(i -> - i - .legs() - .forEach(l -> { - if (l instanceof StreetLeg stLeg) { - assertEquals(TraverseMode.BICYCLE, stLeg.getMode()); - } else { - fail("Expected StreetLeg (BICYCLE): " + l); - } - }) - ); - Geometry legGeometry = itineraries.get(0).legs().get(0).legGeometry(); + itinerary + .legs() + .forEach(l -> { + if (l instanceof StreetLeg stLeg) { + assertEquals(TraverseMode.BICYCLE, stLeg.getMode()); + } else { + fail("Expected StreetLeg (BICYCLE): " + l); + } + }); + Geometry legGeometry = itinerary.legs().getFirst().legGeometry(); return EncodedPolyline.of(legGeometry).points(); } } diff --git a/application/src/test/java/org/opentripplanner/street/integration/CarRoutingTest.java b/application/src/test/java/org/opentripplanner/street/integration/CarRoutingTest.java index 5afd6399a3c..052680ee558 100644 --- a/application/src/test/java/org/opentripplanner/street/integration/CarRoutingTest.java +++ b/application/src/test/java/org/opentripplanner/street/integration/CarRoutingTest.java @@ -147,7 +147,7 @@ private static String computePolyline(Graph graph, GenericLocation from, Generic var linkingRequest = LinkingContextRequestMapper.map(request); var linkingContext = linkingContextFactory.create(temporaryVerticesContainer, linkingRequest); var gpf = new GraphPathFinder(null); - var paths = gpf.graphPathFinderEntryPoint(request, linkingContext); + var path = gpf.graphPathFinderEntryPoint(request, linkingContext); GraphPathToItineraryMapper graphPathToItineraryMapper = new GraphPathToItineraryMapper( new NoopSiteResolver(), @@ -157,22 +157,20 @@ private static String computePolyline(Graph graph, GenericLocation from, Generic graph.ellipsoidToGeoidDifference ); - var itineraries = graphPathToItineraryMapper.mapItineraries(paths, request); + var itinerary = graphPathToItineraryMapper.mapToItinerary(path, request).get(); temporaryVerticesContainer.close(); // make sure that we only get CAR legs - itineraries.forEach(i -> - i - .legs() - .forEach(l -> { - if (l instanceof StreetLeg stLeg) { - assertEquals(TraverseMode.CAR, stLeg.getMode()); - } else { - fail("Expected StreetLeg (CAR): " + l); - } - }) - ); - Geometry legGeometry = itineraries.get(0).legs().get(0).legGeometry(); + itinerary + .legs() + .forEach(l -> { + if (l instanceof StreetLeg stLeg) { + assertEquals(TraverseMode.CAR, stLeg.getMode()); + } else { + fail("Expected StreetLeg (CAR): " + l); + } + }); + Geometry legGeometry = itinerary.legs().getFirst().legGeometry(); return EncodedPolyline.of(legGeometry).points(); } } diff --git a/application/src/test/java/org/opentripplanner/street/integration/SplitEdgeTurnRestrictionsTest.java b/application/src/test/java/org/opentripplanner/street/integration/SplitEdgeTurnRestrictionsTest.java index 6504d084d47..532336db9a3 100644 --- a/application/src/test/java/org/opentripplanner/street/integration/SplitEdgeTurnRestrictionsTest.java +++ b/application/src/test/java/org/opentripplanner/street/integration/SplitEdgeTurnRestrictionsTest.java @@ -179,7 +179,7 @@ private static String computeCarPolyline(Graph graph, GenericLocation from, Gene var linkingRequest = LinkingContextRequestMapper.map(request); var linkingContext = linkingContextFactory.create(temporaryVerticesContainer, linkingRequest); var gpf = new GraphPathFinder(null); - var paths = gpf.graphPathFinderEntryPoint(request, linkingContext); + var path = gpf.graphPathFinderEntryPoint(request, linkingContext); GraphPathToItineraryMapper graphPathToItineraryMapper = new GraphPathToItineraryMapper( new NoopSiteResolver(), @@ -189,21 +189,19 @@ private static String computeCarPolyline(Graph graph, GenericLocation from, Gene graph.ellipsoidToGeoidDifference ); - var itineraries = graphPathToItineraryMapper.mapItineraries(paths, request); + var itinerary = graphPathToItineraryMapper.mapToItinerary(path, request).get(); // make sure that we only get CAR legs - itineraries.forEach(i -> - i - .legs() - .forEach(l -> { - if (l instanceof StreetLeg stLeg) { - assertEquals(TraverseMode.CAR, stLeg.getMode()); - } else { - fail("Expected StreetLeg (CAR): " + l); - } - }) - ); - Geometry geometry = itineraries.get(0).legs().get(0).legGeometry(); + itinerary + .legs() + .forEach(l -> { + if (l instanceof StreetLeg stLeg) { + assertEquals(TraverseMode.CAR, stLeg.getMode()); + } else { + fail("Expected StreetLeg (CAR): " + l); + } + }); + Geometry geometry = itinerary.legs().getFirst().legGeometry(); return EncodedPolyline.of(geometry).points(); } } diff --git a/application/src/test/java/org/opentripplanner/street/integration/WalkRoutingTest.java b/application/src/test/java/org/opentripplanner/street/integration/WalkRoutingTest.java index 24f12756a98..f7abfa94461 100644 --- a/application/src/test/java/org/opentripplanner/street/integration/WalkRoutingTest.java +++ b/application/src/test/java/org/opentripplanner/street/integration/WalkRoutingTest.java @@ -2,10 +2,10 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -63,16 +63,16 @@ void pathReversalWorks(int offset) { var end = GenericLocation.fromCoordinate(59.94641, 10.77522); var base = dateTime.truncatedTo(ChronoUnit.SECONDS); var time = base.plusMillis(offset); - var forwardResults = route(roundabout, start, end, time, false); - assertEquals(1, forwardResults.size()); - var forwardStates = forwardResults.getFirst().states; + var forwardResult = route(roundabout, start, end, time, false); + assertNotNull(forwardResult); + var forwardStates = forwardResult.states; var forwardDiff = ChronoUnit.MILLIS.between( forwardStates.getFirst().getTimeAccurate(), forwardStates.getLast().getTimeAccurate() ); - var backwardResults = route(roundabout, start, end, time, true); - assertEquals(1, backwardResults.size()); - var backwardStates = forwardResults.getFirst().states; + var backwardResult = route(roundabout, start, end, time, true); + assertNotNull(backwardResult); + var backwardStates = backwardResult.states; var backwardDiff = ChronoUnit.MILLIS.between( backwardStates.getFirst().getTimeAccurate(), backwardStates.getLast().getTimeAccurate() @@ -83,7 +83,7 @@ void pathReversalWorks(int offset) { assertEquals(expected, backwardDiff); } - private static List> route( + private static GraphPath route( Graph graph, GenericLocation from, GenericLocation to, diff --git a/astar/src/main/java/org/opentripplanner/astar/AStar.java b/astar/src/main/java/org/opentripplanner/astar/AStar.java index 2d372461212..2ec0d5de6b0 100644 --- a/astar/src/main/java/org/opentripplanner/astar/AStar.java +++ b/astar/src/main/java/org/opentripplanner/astar/AStar.java @@ -5,8 +5,8 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; import org.opentripplanner.astar.model.BinHeap; import org.opentripplanner.astar.model.GraphPath; import org.opentripplanner.astar.model.ShortestPathTree; @@ -18,6 +18,7 @@ import org.opentripplanner.astar.spi.SearchTerminationStrategy; import org.opentripplanner.astar.spi.SkipEdgeStrategy; import org.opentripplanner.astar.spi.TraverseVisitor; +import org.opentripplanner.astar.strategy.PathComparator; import org.opentripplanner.utils.time.DateUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -95,14 +96,14 @@ ShortestPathTree getShortestPathTree() { return spt; } - List> getPathsToTarget() { + Optional> getPathToTarget() { runSearch(); - return targetAcceptedStates - .stream() - .filter(State::isFinal) - .map(GraphPath::new) - .collect(Collectors.toList()); + var finalStates = targetAcceptedStates.stream().filter(State::isFinal).toList(); + if (finalStates.size() > 1) { + LOG.warn("Found multiple paths when one was expected."); + } + return finalStates.stream().map(GraphPath::new).min(new PathComparator(arriveBy)); } private boolean iterate() { diff --git a/astar/src/main/java/org/opentripplanner/astar/AStarBuilder.java b/astar/src/main/java/org/opentripplanner/astar/AStarBuilder.java index 1e788239e97..f73492d5443 100644 --- a/astar/src/main/java/org/opentripplanner/astar/AStarBuilder.java +++ b/astar/src/main/java/org/opentripplanner/astar/AStarBuilder.java @@ -3,7 +3,6 @@ import java.time.Duration; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Optional; import java.util.Set; import org.opentripplanner.astar.model.GraphPath; @@ -122,8 +121,8 @@ public ShortestPathTree getShortestPathTree() { return build().getShortestPathTree(); } - public List> getPathsToTarget() { - return build().getPathsToTarget(); + public Optional> getPathToTarget() { + return build().getPathToTarget(); } private AStar build() { From 2d70bb66596692c90a091864e666394079358ca4 Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Sun, 30 Nov 2025 00:39:33 +0200 Subject: [PATCH 04/15] Add support for direct routing with coordinate via locations --- .../routing/algorithm/RoutingWorker.java | 12 +- .../mapping/GraphPathToItineraryMapper.java | 87 +++++-- .../street/DefaultDirectStreetRouter.java | 21 +- .../router/street/DirectStreetRouter.java | 38 ++- .../street/DirectStreetRouterFactory.java | 17 ++ .../router/street/ViaDirectStreetRouter.java | 240 ++++++++++++++++++ 6 files changed, 352 insertions(+), 63 deletions(-) create mode 100644 application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouterFactory.java create mode 100644 application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java b/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java index b31a38f9109..70fa05e61dc 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java @@ -23,8 +23,8 @@ import org.opentripplanner.routing.algorithm.raptoradapter.router.AdditionalSearchDays; import org.opentripplanner.routing.algorithm.raptoradapter.router.FilterTransitWhenDirectModeIsEmpty; import org.opentripplanner.routing.algorithm.raptoradapter.router.TransitRouter; -import org.opentripplanner.routing.algorithm.raptoradapter.router.street.DefaultDirectStreetRouter; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.DirectFlexRouter; +import org.opentripplanner.routing.algorithm.raptoradapter.router.street.DirectStreetRouterFactory; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.api.request.request.StreetRequest; @@ -226,14 +226,6 @@ private Duration searchWindowUsed() { } private RoutingResult routeDirectStreet() { - // TODO: Add support for via search to the direct-street search and remove this. - // The direct search is used to prune away silly transit results and it - // would be nice to also support via as a feature in the direct-street - // search. - if (request.isViaSearch()) { - return RoutingResult.empty(); - } - // If no direct mode is set, then we set one. // See {@link FilterTransitWhenDirectModeIsEmpty} var emptyDirectModeHandler = new FilterTransitWhenDirectModeIsEmpty( @@ -253,7 +245,7 @@ private RoutingResult routeDirectStreet() { debugTimingAggregator.startedDirectStreetRouter(); try { - var directRouter = new DefaultDirectStreetRouter(); + var directRouter = DirectStreetRouterFactory.create(request); return RoutingResult.ok( directRouter .route(serverContext, directBuilder.buildRequest(), linkingContext()) diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapper.java b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapper.java index 9ef3e037122..594ab23e551 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapper.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/GraphPathToItineraryMapper.java @@ -102,13 +102,14 @@ public static boolean isFloatingRentalDropoff(State state) { } /** - * Generates a TripPlan from a set of paths + * Generates a TripPlan from a set of paths. Each path generates a leg in the itinerary. */ + public Optional mapToItinerary( - GraphPath path, + List> paths, RouteRequest request ) { - Itinerary itinerary = generateItinerary(path, request); + Itinerary itinerary = generateItinerary(paths, request); if (itinerary.legs().isEmpty()) { return Optional.empty(); } @@ -117,43 +118,77 @@ public Optional mapToItinerary( } /** - * Generate an itinerary from a {@link GraphPath}. This method first slices the list of states at - * the leg boundaries. These smaller state arrays are then used to generate legs. + * Generates a TripPlan from a path. + */ + public Optional mapToItinerary( + GraphPath path, + RouteRequest request + ) { + return mapToItinerary(List.of(path), request); + } + + /** + * Generate an itinerary from a list {@link GraphPath}s. Each path generates one more or more + * legs. This method first slices the list of states at the leg boundaries. These smaller state + * arrays are then used to generate legs. * - * @param path The graph path to base the itinerary on + * @param paths The graph paths to base the itinerary on * @return The generated itinerary */ - public Itinerary generateItinerary(GraphPath path, RouteRequest request) { + public Itinerary generateItinerary( + List> paths, + RouteRequest request + ) { List legs = new ArrayList<>(); - WalkStep previousStep = null; - for (List legStates : sliceStates(path.states)) { - if (OTPFeature.FlexRouting.isOn() && legStates.get(1).backEdge instanceof FlexTripEdge) { - legs.add(generateFlexLeg(legStates)); - previousStep = null; - continue; - } - StreetLeg leg = generateLeg(legStates, previousStep, request); - legs.add(leg); - - List walkSteps = leg.listWalkSteps(); - if (walkSteps.size() > 0) { - previousStep = walkSteps.get(walkSteps.size() - 1); - } else { - previousStep = null; + for (GraphPath path : paths) { + WalkStep previousStep = null; + for (List legStates : sliceStates(path.states)) { + if (OTPFeature.FlexRouting.isOn() && legStates.get(1).backEdge instanceof FlexTripEdge) { + legs.add(generateFlexLeg(legStates)); + previousStep = null; + continue; + } + StreetLeg leg = generateLeg(legStates, previousStep, request); + legs.add(leg); + + List walkSteps = leg.listWalkSteps(); + if (walkSteps.size() > 0) { + previousStep = walkSteps.get(walkSteps.size() - 1); + } else { + previousStep = null; + } } } - State lastState = path.states.getLast(); - var cost = Cost.costOfSeconds(lastState.weight); + var cost = Cost.costOfSeconds( + paths.stream().map(GraphPath::getWeight).reduce(0.0, Double::sum) + ); var builder = Itinerary.ofDirect(legs).withGeneralizedCost(cost); - builder.withArrivedAtDestinationWithRentedVehicle(lastState.isRentingVehicleFromStation()); + builder.withArrivedAtDestinationWithRentedVehicle( + paths.getLast().states.getLast().isRentingVehicleFromStation() + ); - calculateElevations(builder, path.edges); + var allEdges = paths + .stream() + .flatMap(p -> p.edges.stream()) + .toList(); + calculateElevations(builder, allEdges); return builder.build(); } + /** + * Generate an itinerary from a {@link GraphPath}. This method first slices the list of states at + * the leg boundaries. These smaller state arrays are then used to generate legs. + * + * @param path The graph path to base the itinerary on + * @return The generated itinerary + */ + public Itinerary generateItinerary(GraphPath path, RouteRequest request) { + return generateItinerary(List.of(path), request); + } + /** * Slice a {@link State} list at the leg boundaries. * diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java index ffff7f7a7f1..d0c947ac275 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java @@ -1,12 +1,12 @@ package org.opentripplanner.routing.algorithm.raptoradapter.router.street; +import java.util.List; import org.opentripplanner.astar.model.GraphPath; import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.impl.GraphPathFinder; import org.opentripplanner.routing.linking.LinkingContext; -import org.opentripplanner.standalone.api.OtpServerRequestContext; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.vertex.Vertex; import org.opentripplanner.street.search.state.State; @@ -17,26 +17,19 @@ */ public class DefaultDirectStreetRouter extends DirectStreetRouter { - GraphPath findPath( - OtpServerRequestContext serverContext, + List> findPaths( + GraphPathFinder graphPathFinder, LinkingContext linkingContext, - RouteRequest request, - float maxCarSpeed + RouteRequest request ) { - // we could also get a persistent router-scoped GraphPathFinder but there's no setup cost here - GraphPathFinder gpFinder = new GraphPathFinder( - serverContext.traverseVisitor(), - serverContext.listExtensionRequestContexts(request), - maxCarSpeed - ); - return gpFinder.graphPathFinderEntryPoint(request, linkingContext); + return List.of(graphPathFinder.graphPathFinderEntryPoint(request, linkingContext)); } - boolean isRequestValidForRouting(RouteRequest request) { + boolean isRequestInvalidForRouting(RouteRequest request) { return request.journey().direct().mode() == StreetMode.NOT_SET; } - boolean isStraightLineDistanceIsWithinLimit( + boolean isStraightLineDistanceWithinLimit( LinkingContext linkingContext, RouteRequest request, double maxDistanceLimit diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java index 260e341b3f1..00c798b71a4 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java @@ -1,5 +1,6 @@ package org.opentripplanner.routing.algorithm.raptoradapter.router.street; +import java.util.List; import java.util.Optional; import org.locationtech.jts.geom.Coordinate; import org.opentripplanner.astar.model.GraphPath; @@ -12,6 +13,7 @@ import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.error.PathNotFoundException; import org.opentripplanner.routing.graphfinder.TransitServiceResolver; +import org.opentripplanner.routing.impl.GraphPathFinder; import org.opentripplanner.routing.linking.LinkingContext; import org.opentripplanner.standalone.api.OtpServerRequestContext; import org.opentripplanner.street.model.edge.Edge; @@ -33,20 +35,26 @@ public Optional route( RouteRequest request, LinkingContext linkingContext ) { - if (isRequestValidForRouting(request)) { + if (isRequestInvalidForRouting(request)) { return Optional.empty(); } OTPRequestTimeoutException.checkForTimeout(); var maxCarSpeed = serverContext.streetLimitationParametersService().maxCarSpeed(); var maxDistanceLimit = calculateDistanceMaxLimit(request, maxCarSpeed); - if (!isStraightLineDistanceIsWithinLimit(linkingContext, request, maxDistanceLimit)) { + if (!isStraightLineDistanceWithinLimit(linkingContext, request, maxDistanceLimit)) { return Optional.empty(); } try { - var path = findPath(serverContext, linkingContext, request, maxCarSpeed); - return mapToItinerary(serverContext, request, path); + // we could also get a persistent router-scoped GraphPathFinder but there's no setup cost here + GraphPathFinder gpFinder = new GraphPathFinder( + serverContext.traverseVisitor(), + serverContext.listExtensionRequestContexts(request), + maxCarSpeed + ); + var paths = findPaths(gpFinder, linkingContext, request); + return mapToItinerary(serverContext, request, paths); } catch (PathNotFoundException e) { return Optional.empty(); } @@ -55,26 +63,27 @@ public Optional route( /** * Checks that the route request is configured to allow direct street results. */ - abstract boolean isRequestValidForRouting(RouteRequest request); + abstract boolean isRequestInvalidForRouting(RouteRequest request); /** * Checks that as the crow flies distance between locations in the search are within the maximum * distance limit. */ - abstract boolean isStraightLineDistanceIsWithinLimit( + abstract boolean isStraightLineDistanceWithinLimit( LinkingContext linkingContext, RouteRequest request, double maxDistanceLimit ); /** - * Find a graph path between the locations in the request. + * Find an ordered set of graph paths between the locations in the request starting from the + * origin and ending in the destination. If there are no via locations, there is exactly one path. + * With via locations, there is one path between each location. */ - abstract GraphPath findPath( - OtpServerRequestContext serverContext, + abstract List> findPaths( + GraphPathFinder graphPathFinder, LinkingContext linkingContext, - RouteRequest request, - float maxCarSpeed + RouteRequest request ); static Coordinate getFirstCoordinateForLocation( @@ -110,10 +119,13 @@ private static double calculateDistanceMaxLimit(RouteRequest request, float maxC throw new IllegalStateException("Could not set max limit for StreetMode"); } + /** + * Creates an itinerary where one graph path generates one or more legs. + */ private static Optional mapToItinerary( OtpServerRequestContext serverContext, RouteRequest request, - GraphPath path + List> paths ) { final GraphPathToItineraryMapper graphPathToItineraryMapper = new GraphPathToItineraryMapper( new TransitServiceResolver(serverContext.transitService()), @@ -122,7 +134,7 @@ private static Optional mapToItinerary( serverContext.streetDetailsService(), serverContext.graph().ellipsoidToGeoidDifference ); - var response = graphPathToItineraryMapper.mapToItinerary(path, request); + var response = graphPathToItineraryMapper.mapToItinerary(paths, request); return response.map(itinerary -> ItinerariesHelper.decorateItineraryWithRequestData( itinerary, diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouterFactory.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouterFactory.java new file mode 100644 index 00000000000..15a76bde618 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouterFactory.java @@ -0,0 +1,17 @@ +package org.opentripplanner.routing.algorithm.raptoradapter.router.street; + +import org.opentripplanner.routing.api.request.RouteRequest; + +/** + * This factory encapsulates the logic for deciding which direct street router to use. + */ +public class DirectStreetRouterFactory { + + /** + * @return {@link DefaultDirectStreetRouter} if there are no via locations, otherwise + * {@link ViaDirectStreetRouter}. + */ + public static DirectStreetRouter create(RouteRequest request) { + return request.isViaSearch() ? new ViaDirectStreetRouter() : new DefaultDirectStreetRouter(); + } +} diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java new file mode 100644 index 00000000000..a58b9f40293 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java @@ -0,0 +1,240 @@ +package org.opentripplanner.routing.algorithm.raptoradapter.router.street; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import org.opentripplanner.astar.model.GraphPath; +import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; +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.api.request.via.ViaLocation; +import org.opentripplanner.routing.impl.GraphPathFinder; +import org.opentripplanner.routing.linking.LinkingContext; +import org.opentripplanner.street.model.edge.Edge; +import org.opentripplanner.street.model.vertex.Vertex; +import org.opentripplanner.street.search.state.State; + +public class ViaDirectStreetRouter extends DirectStreetRouter { + + List> findPaths( + GraphPathFinder graphPathFinder, + LinkingContext linkingContext, + RouteRequest request + ) { + return request.arriveBy() + ? findArriveByPaths(linkingContext, graphPathFinder, request) + : findDepartAfterPaths(linkingContext, graphPathFinder, request); + } + + boolean isRequestInvalidForRouting(RouteRequest request) { + // No support for pass-through locations or visit via locations with just stops as they force + // you to use transit. + return ( + request.listViaLocations().size() != request.listViaLocationsWithCoordinates().size() || + request.journey().direct().mode() == StreetMode.NOT_SET + ); + } + + boolean isStraightLineDistanceWithinLimit( + LinkingContext linkingContext, + RouteRequest request, + double maxDistanceLimit + ) { + var vias = request.listViaLocationsWithCoordinates(); + // TODO This currently only calculates the distances between the first vertex from each + // location + double distance = SphericalDistanceLibrary.distance( + getFirstCoordinateForLocation(linkingContext, request.from()), + getFirstCoordinateForLocation(linkingContext, vias.getFirst()) + ); + for (int i = 0; i < vias.size() - 1; i++) { + distance += SphericalDistanceLibrary.distance( + getFirstCoordinateForLocation(linkingContext, vias.get(i)), + getFirstCoordinateForLocation(linkingContext, vias.get(i + 1)) + ); + } + distance += SphericalDistanceLibrary.distance( + getFirstCoordinateForLocation(linkingContext, vias.getLast()), + getFirstCoordinateForLocation(linkingContext, request.to()) + ); + return distance < maxDistanceLimit; + } + + private List> findArriveByPaths( + LinkingContext linkingContext, + GraphPathFinder graphPathFinder, + RouteRequest request + ) { + var baseRequest = getViaFriendlyRequest(request); + var mode = baseRequest.journey().direct().mode(); + var newStreetRequest = getStreetRequestAfterFirstVia(mode); + var requestWithNewMode = getRequestWithNewMode(baseRequest, newStreetRequest); + + var lastLocations = new ArrayList<>(request.listViaLocationsWithCoordinates()); + lastLocations.add(baseRequest.to()); + var minimumWaitTimes = getMinimumWaitTimes(baseRequest); + + var totalTravelDuration = 0; + var paths = new ArrayList>(); + var newStartTime = request.dateTime(); + var maxDuration = getMaximumDirectDuration(request, mode); + var maxDurationLeft = maxDuration; + for (int i = lastLocations.size() - 2; i >= 0; i--) { + var from = lastLocations.get(i); + var to = lastLocations.get(i + 1); + var patchedRequest = getRequest( + requestWithNewMode, + from, + to, + newStartTime, + newStreetRequest.mode(), + maxDurationLeft + ); + var path = graphPathFinder.graphPathFinderEntryPoint(patchedRequest, linkingContext); + paths.add(path); + + var minimumWaitTime = minimumWaitTimes.get(i); + newStartTime = Instant.ofEpochSecond(path.getStartTime()).minus(minimumWaitTime); + // Wait time is not counted here as it doesn't slow down routing or inconvenience travelers + // like travel time does + totalTravelDuration += path.getDuration(); + maxDurationLeft = maxDuration.minus(Duration.ofSeconds(totalTravelDuration)); + } + + var firstRequest = getRequest( + baseRequest, + baseRequest.from(), + baseRequest.listViaLocationsWithCoordinates().getFirst(), + newStartTime, + mode, + maxDuration + ); + paths.add(graphPathFinder.graphPathFinderEntryPoint(firstRequest, linkingContext)); + return paths.reversed(); + } + + private List> findDepartAfterPaths( + LinkingContext linkingContext, + GraphPathFinder graphPathFinder, + RouteRequest request + ) { + var vias = request.listViaLocationsWithCoordinates(); + var baseRequest = getViaFriendlyRequest(request); + var firstRequest = baseRequest.copyOf().withTo(vias.getFirst()).buildRequest(); + List> paths = new ArrayList<>(); + paths.add(graphPathFinder.graphPathFinderEntryPoint(firstRequest, linkingContext)); + + var mode = baseRequest.journey().direct().mode(); + var newStreetRequest = getStreetRequestAfterFirstVia(mode); + var requestWithNewMode = getRequestWithNewMode(firstRequest, newStreetRequest); + + var lastLocations = new ArrayList<>(vias); + lastLocations.add(baseRequest.to()); + var minimumWaitTimes = getMinimumWaitTimes(baseRequest); + + var totalTravelDuration = paths.getFirst().getDuration(); + var maxDuration = getMaximumDirectDuration(request, mode); + for (int i = 0; i < lastLocations.size() - 1; i++) { + var from = lastLocations.get(i); + var to = lastLocations.get(i + 1); + var maxDurationLeft = maxDuration.minus(Duration.ofSeconds(totalTravelDuration)); + var minimumWaitTime = minimumWaitTimes.get(i); + var newStartTime = Instant.ofEpochSecond(paths.getLast().getEndTime()).plus(minimumWaitTime); + var patchedRequest = getRequest( + requestWithNewMode, + from, + to, + newStartTime, + newStreetRequest.mode(), + maxDurationLeft + ); + var path = graphPathFinder.graphPathFinderEntryPoint(patchedRequest, linkingContext); + paths.add(path); + // Wait time is not counted here as it doesn't slow down routing or inconvenience travelers + // like travel time does + totalTravelDuration += path.getDuration(); + } + return paths; + } + + /** + * TODO we might want to continue on a vehicle if there is no wait time defined for a via point. + */ + private RouteRequest getViaFriendlyRequest(RouteRequest originalRequest) { + return originalRequest + .copyOf() + // TODO we might want to change this behaviour + .withPreferences(preferences -> + preferences + .withBike(bike -> + bike.withRental(rental -> rental.withAllowArrivingInRentedVehicleAtDestination(false)) + ) + .withScooter(scooter -> + scooter.withRental(rental -> + rental.withAllowArrivingInRentedVehicleAtDestination(false) + ) + ) + .withCar(car -> + car.withRental(rental -> rental.withAllowArrivingInRentedVehicleAtDestination(false)) + ) + ) + .buildRequest(); + } + + /** + * TODO we might want to continue on a vehicle if there is no wait time defined for a via point. + */ + private RouteRequest getRequestWithNewMode( + RouteRequest originalRequest, + StreetRequest newStreetRequest + ) { + return originalRequest + .copyOf() + .withJourney(journeyRequestBuilder -> journeyRequestBuilder.withDirect(newStreetRequest)) + .buildRequest(); + } + + private RouteRequest getRequest( + RouteRequest originalRequest, + GenericLocation from, + GenericLocation to, + Instant newStartTime, + StreetMode mode, + Duration maxDuration + ) { + return originalRequest + .copyOf() + .withFrom(from) + .withTo(to) + .withDateTime(newStartTime) + .withPreferences(preferences -> + preferences.withStreet(street -> + street.withMaxDirectDuration(streetModeBuilder -> + streetModeBuilder.with(mode, maxDuration) + ) + ) + ) + .buildRequest(); + } + + /** + * TODO we might want to continue on a vehicle if there is no wait time defined for a via point. + */ + private StreetRequest getStreetRequestAfterFirstVia(StreetMode mode) { + if (mode.includesParking() || mode.includesRenting()) { + return new StreetRequest(StreetMode.WALK); + } + return new StreetRequest(mode); + } + + private List getMinimumWaitTimes(RouteRequest request) { + return request.listViaLocations().stream().map(ViaLocation::minimumWaitTime).toList(); + } + + private Duration getMaximumDirectDuration(RouteRequest request, StreetMode mode) { + return request.preferences().street().maxDirectDuration().valueOf(mode); + } +} From c42e5dab769ea110698e7c29191abef7988be689 Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Fri, 12 Dec 2025 15:17:56 +0200 Subject: [PATCH 05/15] Add missing annotations --- .../raptoradapter/router/street/DefaultDirectStreetRouter.java | 3 +++ .../raptoradapter/router/street/ViaDirectStreetRouter.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java index d0c947ac275..9d8f0d5214b 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultDirectStreetRouter.java @@ -17,6 +17,7 @@ */ public class DefaultDirectStreetRouter extends DirectStreetRouter { + @Override List> findPaths( GraphPathFinder graphPathFinder, LinkingContext linkingContext, @@ -25,10 +26,12 @@ List> findPaths( return List.of(graphPathFinder.graphPathFinderEntryPoint(request, linkingContext)); } + @Override boolean isRequestInvalidForRouting(RouteRequest request) { return request.journey().direct().mode() == StreetMode.NOT_SET; } + @Override boolean isStraightLineDistanceWithinLimit( LinkingContext linkingContext, RouteRequest request, diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java index a58b9f40293..7836bc2c646 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java @@ -19,6 +19,7 @@ public class ViaDirectStreetRouter extends DirectStreetRouter { + @Override List> findPaths( GraphPathFinder graphPathFinder, LinkingContext linkingContext, @@ -29,6 +30,7 @@ List> findPaths( : findDepartAfterPaths(linkingContext, graphPathFinder, request); } + @Override boolean isRequestInvalidForRouting(RouteRequest request) { // No support for pass-through locations or visit via locations with just stops as they force // you to use transit. @@ -38,6 +40,7 @@ boolean isRequestInvalidForRouting(RouteRequest request) { ); } + @Override boolean isStraightLineDistanceWithinLimit( LinkingContext linkingContext, RouteRequest request, From b12e5f1e681c496aa56527dcabda99b41079f3a9 Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Fri, 12 Dec 2025 15:44:32 +0200 Subject: [PATCH 06/15] Fix duration limits --- .../router/street/ViaDirectStreetRouter.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java index 7836bc2c646..ba59fdc57ce 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java @@ -80,12 +80,11 @@ private List> findArriveByPaths( lastLocations.add(baseRequest.to()); var minimumWaitTimes = getMinimumWaitTimes(baseRequest); - var totalTravelDuration = 0; var paths = new ArrayList>(); var newStartTime = request.dateTime(); - var maxDuration = getMaximumDirectDuration(request, mode); - var maxDurationLeft = maxDuration; - for (int i = lastLocations.size() - 2; i >= 0; i--) { + var maxDurationLeft = getMaximumDirectDuration(request, mode); + int i = lastLocations.size() - 2; + while (i >= 0 && maxDurationLeft.isPositive()) { var from = lastLocations.get(i); var to = lastLocations.get(i + 1); var patchedRequest = getRequest( @@ -103,8 +102,8 @@ private List> findArriveByPaths( newStartTime = Instant.ofEpochSecond(path.getStartTime()).minus(minimumWaitTime); // Wait time is not counted here as it doesn't slow down routing or inconvenience travelers // like travel time does - totalTravelDuration += path.getDuration(); - maxDurationLeft = maxDuration.minus(Duration.ofSeconds(totalTravelDuration)); + maxDurationLeft = maxDurationLeft.minus(Duration.ofSeconds(path.getDuration())); + i--; } var firstRequest = getRequest( @@ -113,7 +112,7 @@ private List> findArriveByPaths( baseRequest.listViaLocationsWithCoordinates().getFirst(), newStartTime, mode, - maxDuration + maxDurationLeft ); paths.add(graphPathFinder.graphPathFinderEntryPoint(firstRequest, linkingContext)); return paths.reversed(); @@ -137,13 +136,13 @@ private List> findDepartAfterPaths( var lastLocations = new ArrayList<>(vias); lastLocations.add(baseRequest.to()); var minimumWaitTimes = getMinimumWaitTimes(baseRequest); - - var totalTravelDuration = paths.getFirst().getDuration(); - var maxDuration = getMaximumDirectDuration(request, mode); - for (int i = 0; i < lastLocations.size() - 1; i++) { + var maxDurationLeft = getMaximumDirectDuration(request, mode).minus( + Duration.ofSeconds(paths.getFirst().getDuration()) + ); + int i = 0; + while (i < lastLocations.size() - 1 && maxDurationLeft.isPositive()) { var from = lastLocations.get(i); var to = lastLocations.get(i + 1); - var maxDurationLeft = maxDuration.minus(Duration.ofSeconds(totalTravelDuration)); var minimumWaitTime = minimumWaitTimes.get(i); var newStartTime = Instant.ofEpochSecond(paths.getLast().getEndTime()).plus(minimumWaitTime); var patchedRequest = getRequest( @@ -158,7 +157,8 @@ private List> findDepartAfterPaths( paths.add(path); // Wait time is not counted here as it doesn't slow down routing or inconvenience travelers // like travel time does - totalTravelDuration += path.getDuration(); + maxDurationLeft = maxDurationLeft.minus(Duration.ofSeconds(path.getDuration())); + i++; } return paths; } From a73ff5428ba3b4d35e6c2b077b2a9b4cdf9cc9c3 Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Mon, 1 Dec 2025 14:42:19 +0200 Subject: [PATCH 07/15] Refactor RoutingAccessEgress to have a list of last states instead of singular last state --- .../ridehailing/RideHailingAccessAdapter.java | 2 +- .../ridehailing/RideHailingAccessShifter.java | 3 +- .../mapping/RaptorPathToItineraryMapper.java | 9 ++--- .../transit/DefaultAccessEgress.java | 34 ++++++++++++------- .../transit/RoutingAccessEgress.java | 7 ++-- .../_data/transit/TestAccessEgress.java | 11 +++--- .../transit/DefaultAccessEgressTest.java | 14 +++++--- 7 files changed, 50 insertions(+), 30 deletions(-) diff --git a/application/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessAdapter.java b/application/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessAdapter.java index 257cf0a7ca5..2b4e0957f69 100644 --- a/application/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessAdapter.java +++ b/application/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessAdapter.java @@ -14,7 +14,7 @@ public final class RideHailingAccessAdapter extends DefaultAccessEgress { private final Duration arrival; public RideHailingAccessAdapter(RoutingAccessEgress access, Duration arrival) { - super(access.stop(), access.getLastState()); + super(access.stop(), access.getLastStates()); this.arrival = arrival; } diff --git a/application/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessShifter.java b/application/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessShifter.java index ca3552d26cc..6b8744216db 100644 --- a/application/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessShifter.java +++ b/application/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessShifter.java @@ -12,6 +12,7 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; +import org.opentripplanner.street.search.state.State; import org.opentripplanner.transit.model.framework.Result; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,7 +46,7 @@ public static List shiftAccesses( .map(ae -> { // only time-shift access legs on a car // (there could be walk-only accesses if you're close to the stop) - if (isAccess && ae.getLastState().containsModeCar()) { + if (isAccess && ae.getLastStates().stream().allMatch(State::containsModeCar)) { var duration = fetchArrivalDelay(services, request, now); if (duration.isSuccess()) { return new RideHailingAccessAdapter(ae, duration.successValue()); diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java index 8bcdc5ced5c..efa7a21eff1 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java @@ -489,12 +489,9 @@ private boolean includeTransferInItinerary(Leg transitLegBeforeTransfer) { } private Itinerary mapAccessEgressPathLeg(RaptorAccessEgress accessEgress) { - return accessEgress - .findOriginal(RoutingAccessEgress.class) - .map(RoutingAccessEgress::getLastState) - .map(GraphPath::new) - .map(path -> graphPathToItineraryMapper.generateItinerary(path, request)) - .orElseThrow(); + var routingAccessEgress = accessEgress.findOriginal(RoutingAccessEgress.class).orElseThrow(); + var paths = routingAccessEgress.getLastStates().stream().map(GraphPath::new).toList(); + return graphPathToItineraryMapper.generateItinerary(paths, request); } private TimeAndCost mapAccessEgressPenalty(RaptorAccessEgress accessEgress) { diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java index 564603a1f61..021476bd0a0 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java @@ -1,10 +1,12 @@ package org.opentripplanner.routing.algorithm.raptoradapter.transit; +import java.util.List; import java.util.Objects; import org.opentripplanner.framework.model.TimeAndCost; import org.opentripplanner.raptor.api.model.RaptorConstants; import org.opentripplanner.raptor.api.model.RaptorCostConverter; import org.opentripplanner.street.search.state.State; +import org.opentripplanner.utils.collection.ListUtils; /** * Default implementation of the RaptorAccessEgress interface. @@ -20,9 +22,11 @@ public class DefaultAccessEgress implements RoutingAccessEgress { private final TimeAndCost penalty; /** - * This should be the last state both in the case of access and egress. + * For access, this is a list of states starting from origin to the access stop split at via + * locations visited inside the access. For egress, this is a list starting at the egress stop + * ending at the destination split at the via locations visited inside the egress. */ - private final State lastState; + private final List lastStates; /** * This is public to allow unit-tests full control over the field values. @@ -32,26 +36,32 @@ public DefaultAccessEgress( int durationInSeconds, int generalizedCost, TimeAndCost penalty, - State lastState + List lastState ) { this.stop = stop; this.durationInSeconds = durationInSeconds; this.generalizedCost = generalizedCost; this.timePenalty = penalty.isZero() ? RaptorConstants.TIME_NOT_SET : penalty.timeInSeconds(); this.penalty = penalty; - this.lastState = Objects.requireNonNull(lastState); + this.lastStates = ListUtils.requireAtLeastNElements(lastState, 1); } - public DefaultAccessEgress(int stop, State lastState) { + public DefaultAccessEgress(int stop, List lastStates) { this( stop, - (int) lastState.getElapsedTimeSeconds(), - RaptorCostConverter.toRaptorCost(lastState.getWeight()), + (int) lastStates.stream().mapToLong(State::getElapsedTimeSeconds).reduce(0, Long::sum), + RaptorCostConverter.toRaptorCost( + lastStates.stream().mapToDouble(State::getWeight).reduce(0, Double::sum) + ), TimeAndCost.ZERO, - lastState + lastStates ); } + public DefaultAccessEgress(int stop, State lastState) { + this(stop, List.of(Objects.requireNonNull(lastState))); + } + protected DefaultAccessEgress(RoutingAccessEgress other, TimeAndCost penalty) { // In the API we have a cost associated with the time-penalty. In Raptor, there is no // association between the time-penalty and the cost. So, we add the time-penalty cost to @@ -61,7 +71,7 @@ protected DefaultAccessEgress(RoutingAccessEgress other, TimeAndCost penalty) { other.durationInSeconds(), other.c1() + penalty.cost().toCentiSeconds(), penalty, - other.getLastState() + other.getLastStates() ); if (other.penalty() != TimeAndCost.ZERO) { throw new IllegalStateException("Can not add penalty twice..."); @@ -94,13 +104,13 @@ public boolean hasOpeningHours() { } @Override - public State getLastState() { - return lastState; + public List getLastStates() { + return lastStates; } @Override public boolean isWalkOnly() { - return lastState.containsOnlyWalkMode(); + return lastStates.stream().allMatch(State::containsOnlyWalkMode); } @Override diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/RoutingAccessEgress.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/RoutingAccessEgress.java index dbc260d3d90..f2d00ad00e7 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/RoutingAccessEgress.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/RoutingAccessEgress.java @@ -1,5 +1,6 @@ package org.opentripplanner.routing.algorithm.raptoradapter.transit; +import java.util.List; import org.opentripplanner.framework.model.TimeAndCost; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; import org.opentripplanner.street.search.state.State; @@ -18,9 +19,11 @@ public interface RoutingAccessEgress extends RaptorAccessEgress { RoutingAccessEgress withPenalty(TimeAndCost penalty); /** - * Return the last state both in the case of access and egress. + * For access, this is a list of states starting from origin to the access stop split at via + * locations visited inside the access. For egress, this is a list starting at the egress stop + * ending at the destination split at the via locations visited inside the egress. */ - State getLastState(); + List getLastStates(); /** * Return true if all edges are traversed on foot. diff --git a/application/src/test/java/org/opentripplanner/raptorlegacy/_data/transit/TestAccessEgress.java b/application/src/test/java/org/opentripplanner/raptorlegacy/_data/transit/TestAccessEgress.java index 02ecddd94db..1a9c12a0bad 100644 --- a/application/src/test/java/org/opentripplanner/raptorlegacy/_data/transit/TestAccessEgress.java +++ b/application/src/test/java/org/opentripplanner/raptorlegacy/_data/transit/TestAccessEgress.java @@ -2,6 +2,7 @@ import static org.opentripplanner.raptor.api.model.RaptorCostConverter.toRaptorCost; +import java.util.List; import org.opentripplanner._support.geometry.Coordinates; import org.opentripplanner.core.model.i18n.I18NString; import org.opentripplanner.framework.model.TimeAndCost; @@ -109,16 +110,18 @@ Builder stopReachedOnBoard() { RoutingAccessEgress build() { var stopId = "Stop:" + stop; - var lastState = new State( - new StreetLocation(stopId, Coordinates.BOSTON, I18NString.of(stopId)), - StreetSearchRequest.of().build() + var lastStates = List.of( + new State( + new StreetLocation(stopId, Coordinates.BOSTON, I18NString.of(stopId)), + StreetSearchRequest.of().build() + ) ); return new DefaultAccessEgress( stop, durationInSeconds, generalizedCost, TimeAndCost.ZERO, - lastState + lastStates ) { // TODO - Use the domain type FlexAccessEgressAdapter here instead [if numberOfRides > 0] @Override diff --git a/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgressTest.java b/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgressTest.java index 0453fa8c0ac..67ad117ec61 100644 --- a/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgressTest.java +++ b/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgressTest.java @@ -1,11 +1,13 @@ package org.opentripplanner.routing.algorithm.raptoradapter.transit; +import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Duration; +import java.util.List; import org.junit.jupiter.api.Test; import org.opentripplanner.framework.model.Cost; import org.opentripplanner.framework.model.TimeAndCost; @@ -62,8 +64,8 @@ void hasOpeningHours() { } @Test - void getLastState() { - assertEquals(LAST_STATE, subject.getLastState()); + void getLastStates() { + assertEquals(List.of(LAST_STATE), subject.getLastStates()); } /** @@ -75,7 +77,9 @@ void getLastState() { void containsDriving() { var state = TestStateBuilder.ofDriving().streetEdge().streetEdge().streetEdge().build(); var access = new DefaultAccessEgress(0, state); - assertTrue(access.getLastState().containsModeCar()); + var lastStates = access.getLastStates(); + assertThat(lastStates).hasSize(1); + assertTrue(lastStates.getFirst().containsModeCar()); } /** @@ -87,7 +91,9 @@ void containsDriving() { void walking() { var state = TestStateBuilder.ofWalking().streetEdge().streetEdge().streetEdge().build(); var access = new DefaultAccessEgress(0, state); - assertFalse(access.getLastState().containsModeCar()); + var lastStates = access.getLastStates(); + assertThat(lastStates).hasSize(1); + assertFalse(lastStates.getFirst().containsModeCar()); } @Test From 8f88a134b2adf9f4dda3ce8889f7f0ff4b766f15 Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Mon, 1 Dec 2025 16:53:24 +0200 Subject: [PATCH 08/15] Refactor NearbyStop to support multiple last states --- .../ext/flex/template/ClosestTripTest.java | 2 +- .../flex/template/AbstractFlexTemplate.java | 8 +++- .../ext/flex/template/FlexAccessTemplate.java | 8 ++-- .../flex/template/FlexDirectPathFactory.java | 13 +++-- .../ext/flex/template/FlexEgressTemplate.java | 8 ++-- .../apis/gtfs/datafetchers/QueryTypeImpl.java | 2 +- .../apis/gtfs/datafetchers/StopImpl.java | 7 ++- .../router/street/AccessEgressRouter.java | 2 +- .../transit/mappers/AccessEgressMapper.java | 5 +- .../graphfinder/DirectGraphFinder.java | 2 +- .../routing/graphfinder/NearbyStop.java | 48 ++++++++++++------- .../DefaultViaCoordinateTransferFactory.java | 4 +- ...reetNearbyStopFinderMultipleLinksTest.java | 10 ++-- .../StreetNearbyStopFinderTest.java | 10 ++-- .../router/street/AccessEgressRouterTest.java | 2 +- .../graphfinder/DirectGraphFinderTest.java | 4 +- .../routing/graphfinder/NearbyStopTest.java | 7 +-- .../graphfinder/StreetGraphFinderTest.java | 6 +-- 18 files changed, 95 insertions(+), 53 deletions(-) diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/template/ClosestTripTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/template/ClosestTripTest.java index d9885a7844f..ae099cb634a 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/flex/template/ClosestTripTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/template/ClosestTripTest.java @@ -94,7 +94,7 @@ void filter() { private static Collection closestTrips(Matcher matcher) { return ClosestTrip.of( ADAPTER, - List.of(new NearbyStop(STOP, 100, List.of(), null)), + List.of(new NearbyStop(STOP, 100, List.of(), List.of())), matcher, List.of(FSD), true diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java b/application/src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java index 4382000582c..8b99a381988 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java @@ -107,7 +107,8 @@ Stream createFlexAccessEgressStream(FlexAccessEgressCallbackAd // transferStop is Location Area/Line else { double maxDistanceMeters = - maxTransferDuration.getSeconds() * accessEgress.state.getRequest().walk().speed(); + maxTransferDuration.getSeconds() * + accessEgress.lastStates.getFirst().getRequest().walk().speed(); return getTransfersFromTransferStop(callback) .stream() @@ -192,7 +193,10 @@ private FlexAccessEgress createFlexAccessEgress( // this code is a little repetitive but needed as a performance improvement. previously // the flex path was checked before this method was called. this meant that every path // was traversed twice, leading to a noticeable slowdown. - final var afterFlexState = flexEdge.traverse(accessEgress.state); + + // TODO flex routing doesn't support via locations yet + var lastState = accessEgress.lastStates.getFirst(); + final var afterFlexState = flexEdge.traverse(lastState); if (State.isEmpty(afterFlexState)) { return null; } diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessTemplate.java b/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessTemplate.java index cc85b169709..584c7565878 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessTemplate.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessTemplate.java @@ -58,7 +58,7 @@ protected Vertex getFlexVertex(Edge edge) { } protected FlexPathDurations calculateFlexPathDurations(FlexTripEdge flexEdge, State state) { - int preFlexTime = (int) accessEgress.state.getElapsedTimeSeconds(); + int preFlexTime = (int) accessEgress.duration().getSeconds(); int edgeTimeInSeconds = flexEdge.getTimeInSeconds(); int postFlexTime = (int) state.getElapsedTimeSeconds() - preFlexTime - edgeTimeInSeconds; return new FlexPathDurations( @@ -70,8 +70,10 @@ protected FlexPathDurations calculateFlexPathDurations(FlexTripEdge flexEdge, St } protected FlexTripEdge getFlexEdge(Vertex flexToVertex, StopLocation transferStop) { + // TODO flex doesn't support via locations yet + var lastVertex = accessEgress.lastStates.getLast().getVertex(); var flexPath = calculator.calculateFlexPath( - accessEgress.state.getVertex(), + lastVertex, flexToVertex, boardStopPosition, alightStopPosition @@ -82,7 +84,7 @@ protected FlexTripEdge getFlexEdge(Vertex flexToVertex, StopLocation transferSto } return new FlexTripEdge( - accessEgress.state.getVertex(), + lastVertex, flexToVertex, accessEgress.stop, transferStop, diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java b/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java index f7d5e34ff17..7a576e21ca4 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java @@ -105,7 +105,8 @@ private Optional createDirectGraphPath( int accessAlightStopPosition = accessTemplate.alightStopPosition; int requestedBookingTime = accessTemplate.requestedBookingTime; - var flexToVertex = egress.state.getVertex(); + // TODO flex doesn't support via locations yet + var flexToVertex = egress.lastStates.getLast().getVertex(); if (!isRouteable(accessTemplate, flexToVertex)) { return Optional.empty(); @@ -117,7 +118,9 @@ private Optional createDirectGraphPath( return Optional.empty(); } - final State[] afterFlexState = flexEdge.traverse(accessNearbyStop.state); + // TODO flex doesn't support via locations yet + var lastState = accessNearbyStop.lastStates.getLast(); + final State[] afterFlexState = flexEdge.traverse(lastState); var finalStateOpt = EdgeTraverser.traverseEdges(afterFlexState[0], egress.edges); @@ -183,12 +186,14 @@ private Optional createDirectGraphPath( } protected boolean isRouteable(FlexAccessTemplate accessTemplate, Vertex flexVertex) { - if (accessTemplate.accessEgress.state.getVertex() == flexVertex) { + // TODO flex doesn't support via locations yet + var lastVertex = accessTemplate.accessEgress.lastStates.getLast().getVertex(); + if (lastVertex == flexVertex) { return false; } else { return ( accessTemplate.calculator.calculateFlexPath( - accessTemplate.accessEgress.state.getVertex(), + lastVertex, flexVertex, accessTemplate.boardStopPosition, accessTemplate.alightStopPosition diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressTemplate.java b/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressTemplate.java index 9dbbd9c0c79..e05830ea644 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressTemplate.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressTemplate.java @@ -59,7 +59,7 @@ protected Vertex getFlexVertex(Edge edge) { } protected FlexPathDurations calculateFlexPathDurations(FlexTripEdge flexEdge, State state) { - int postFlexTime = (int) accessEgress.state.getElapsedTimeSeconds(); + int postFlexTime = (int) accessEgress.duration().getSeconds(); int edgeTimeInSeconds = flexEdge.getTimeInSeconds(); int preFlexTime = (int) state.getElapsedTimeSeconds() - postFlexTime - edgeTimeInSeconds; return new FlexPathDurations( @@ -71,9 +71,11 @@ protected FlexPathDurations calculateFlexPathDurations(FlexTripEdge flexEdge, St } protected FlexTripEdge getFlexEdge(Vertex flexFromVertex, StopLocation transferStop) { + // TODO flex doesn't support via locations yet + var lastVertex = accessEgress.lastStates.getLast().getVertex(); var flexPath = calculator.calculateFlexPath( flexFromVertex, - accessEgress.state.getVertex(), + lastVertex, boardStopPosition, alightStopPosition ); @@ -84,7 +86,7 @@ protected FlexTripEdge getFlexEdge(Vertex flexFromVertex, StopLocation transferS return new FlexTripEdge( flexFromVertex, - accessEgress.state.getVertex(), + lastVertex, transferStop, accessEgress.stop, trip, diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java index 3127aae9ef1..8a38a550d71 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java @@ -461,7 +461,7 @@ public DataFetcher node() { var stop = transitService.getRegularStop(FeedScopedId.parse(parts[1])); // TODO: Add geometry - return new NearbyStop(stop, Integer.parseInt(parts[0]), null, null); + return new NearbyStop(stop, Integer.parseInt(parts[0]), List.of(), List.of()); } case "TicketType": // TODO diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java index a4b7a700752..58d6d8457f8 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java @@ -444,7 +444,12 @@ public DataFetcher> transfers() { .filter(transfer -> maxDistance == null || transfer.getDistanceMeters() < maxDistance) .filter(transfer -> transfer.to instanceof RegularStop) .map(transfer -> - new NearbyStop(transfer.to, transfer.getDistanceMeters(), transfer.getEdges(), null) + new NearbyStop( + transfer.to, + transfer.getDistanceMeters(), + transfer.getEdges(), + List.of() + ) ) .collect(Collectors.toList()); }, diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java index 33bbd3c7437..657d3144f03 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java @@ -58,7 +58,7 @@ public Collection findAccessEgresses( // When looking for street accesses/egresses we ignore the already found direct accesses/egresses var ignoreVertices = zeroDistanceAccessEgress .stream() - .map(nearbyStop -> nearbyStop.state.getVertex()) + .map(nearbyStop -> nearbyStop.lastStates.getLast().getVertex()) .collect(Collectors.toSet()); var originVertices = accessOrEgress.isAccess() diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/AccessEgressMapper.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/AccessEgressMapper.java index 2205a2feb7b..85b1c9dd788 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/AccessEgressMapper.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/AccessEgressMapper.java @@ -10,6 +10,7 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.FlexAccessEgressAdapter; import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; import org.opentripplanner.routing.graphfinder.NearbyStop; +import org.opentripplanner.street.search.state.State; import org.opentripplanner.transit.model.site.RegularStop; public class AccessEgressMapper { @@ -45,7 +46,9 @@ private static RoutingAccessEgress mapNearbyStop( return new DefaultAccessEgress( nearbyStop.stop.getIndex(), - accessOrEgress.isEgress() ? nearbyStop.state.reverse() : nearbyStop.state + accessOrEgress.isEgress() + ? nearbyStop.lastStates.stream().map(State::reverse).toList().reversed() + : nearbyStop.lastStates ); } } diff --git a/application/src/main/java/org/opentripplanner/routing/graphfinder/DirectGraphFinder.java b/application/src/main/java/org/opentripplanner/routing/graphfinder/DirectGraphFinder.java index 54e161da6e4..dc88b4ccd39 100644 --- a/application/src/main/java/org/opentripplanner/routing/graphfinder/DirectGraphFinder.java +++ b/application/src/main/java/org/opentripplanner/routing/graphfinder/DirectGraphFinder.java @@ -41,7 +41,7 @@ public List findClosestStops(Coordinate coordinate, double radiusMet SphericalDistanceLibrary.distance(coordinate, it.getCoordinate().asJtsCoordinate()) ); if (distance < radiusMeters) { - NearbyStop sd = new NearbyStop(it, distance, null, null); + NearbyStop sd = new NearbyStop(it, distance, List.of(), List.of()); stopsFound.add(sd); } } diff --git a/application/src/main/java/org/opentripplanner/routing/graphfinder/NearbyStop.java b/application/src/main/java/org/opentripplanner/routing/graphfinder/NearbyStop.java index 2af052df9cc..df6b5f40a5a 100644 --- a/application/src/main/java/org/opentripplanner/routing/graphfinder/NearbyStop.java +++ b/application/src/main/java/org/opentripplanner/routing/graphfinder/NearbyStop.java @@ -20,14 +20,26 @@ public class NearbyStop implements Comparable { public final StopLocation stop; public final double distance; + /** + * All edges that are needed to reach the stop. + */ public final List edges; - public final State state; - public NearbyStop(StopLocation stop, double distance, List edges, State state) { + /** + * This a list of states where the last state in the list is always at the stop and states before + * that state end at a via location. + */ + public final List lastStates; + + public NearbyStop(StopLocation stop, double distance, List edges, List lastStates) { this.stop = Objects.requireNonNull(stop); this.distance = distance; this.edges = edges; - this.state = state; + this.lastStates = lastStates; + } + + public NearbyStop(StopLocation stop, double distance, List edges, State lastState) { + this(stop, distance, edges, List.of(Objects.requireNonNull(lastState))); } /** @@ -63,14 +75,9 @@ public boolean isBetter(NearbyStop other) { @Override public int compareTo(NearbyStop that) { - if ((this.state == null) != (that.state == null)) { - throw new IllegalStateException( - "Only NearbyStops which both contain or lack a state may be compared." - ); - } - - if (this.state != null) { - return (int) (this.state.getWeight()) - (int) (that.state.getWeight()); + var weightDifference = (int) (this.weight()) - (int) (that.weight()); + if (weightDifference != 0) { + return weightDifference; } return (int) (this.distance) - (int) (that.distance); } @@ -79,12 +86,21 @@ public int compareTo(NearbyStop that) { * Duration it took to reach the stop. */ public Duration duration() { - return Duration.ofSeconds(state.getElapsedTimeSeconds()); + return Duration.ofSeconds( + lastStates.stream().mapToLong(State::getElapsedTimeSeconds).reduce(0, Long::sum) + ); + } + + /** + * Weight (cost) of reaching the stop. + */ + public double weight() { + return lastStates.stream().mapToDouble(State::getWeight).reduce(0.0, Double::sum); } @Override public int hashCode() { - return Objects.hash(stop, distance, edges, state); + return Objects.hash(stop, distance, edges, lastStates); } @Override @@ -100,7 +116,7 @@ public boolean equals(Object o) { Double.compare(that.distance, distance) == 0 && stop.equals(that.stop) && Objects.equals(edges, that.edges) && - Objects.equals(state, that.state) + Objects.equals(lastStates, that.lastStates) ); } @@ -110,8 +126,8 @@ public String toString() { "stop %s at %.1f meters%s%s", stop, distance, - edges != null ? " (" + edges.size() + " edges)" : "", - state != null ? " w/state" : "" + " (" + edges.size() + " edges)", + " (" + lastStates.size() + " lastStates)" ); } } diff --git a/application/src/main/java/org/opentripplanner/routing/via/service/DefaultViaCoordinateTransferFactory.java b/application/src/main/java/org/opentripplanner/routing/via/service/DefaultViaCoordinateTransferFactory.java index b49d6729aff..6118cbc5d94 100644 --- a/application/src/main/java/org/opentripplanner/routing/via/service/DefaultViaCoordinateTransferFactory.java +++ b/application/src/main/java/org/opentripplanner/routing/via/service/DefaultViaCoordinateTransferFactory.java @@ -61,8 +61,8 @@ public List createViaTransfers( to.stop.getIndex(), from.edges, to.edges, - (int) (from.state.getElapsedTimeSeconds() + to.state.getElapsedTimeSeconds()), - from.state.getWeight() + to.state.getWeight() + (int) (from.duration().plus(to.duration()).getSeconds()), + from.weight() + to.weight() ) ); } diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/nearbystops/StreetNearbyStopFinderMultipleLinksTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/nearbystops/StreetNearbyStopFinderMultipleLinksTest.java index fae7be48608..e03157d14fd 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/nearbystops/StreetNearbyStopFinderMultipleLinksTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/nearbystops/StreetNearbyStopFinderMultipleLinksTest.java @@ -81,8 +81,9 @@ void assertZeroDistanceStop(TransitStopVertex expected, NearbyStop nearbyStop) { assertEquals(stopResolver.getRegularStop(expected.getId()), nearbyStop.stop); assertEquals(0, nearbyStop.distance); assertEquals(0, nearbyStop.edges.size()); - assertEquals(expected, nearbyStop.state.getVertex()); - assertNull(nearbyStop.state.getBackState()); + var lastState = nearbyStop.lastStates.getLast(); + assertEquals(expected, lastState.getVertex()); + assertNull(lastState.getBackState()); } /** @@ -95,8 +96,9 @@ void assertStopAtDistance( ) { assertEquals(stopResolver.getRegularStop(expected.getId()), nearbyStop.stop); assertEquals(expectedDistance, nearbyStop.distance); - assertEquals(expected, nearbyStop.state.getVertex()); + var lastState = nearbyStop.lastStates.getLast(); + assertEquals(expected, lastState.getVertex()); assertFalse(nearbyStop.edges.isEmpty()); - assertNotNull(nearbyStop.state.getBackState()); + assertNotNull(lastState.getBackState()); } } diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/nearbystops/StreetNearbyStopFinderTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/nearbystops/StreetNearbyStopFinderTest.java index 2579e3ec635..055fb110db7 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/nearbystops/StreetNearbyStopFinderTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/nearbystops/StreetNearbyStopFinderTest.java @@ -190,8 +190,9 @@ void assertZeroDistanceStop(TransitStopVertex expected, NearbyStop nearbyStop) { assertEquals(stopResolver.getRegularStop(expected.getId()), nearbyStop.stop); assertEquals(0, nearbyStop.distance); assertEquals(0, nearbyStop.edges.size()); - assertEquals(expected, nearbyStop.state.getVertex()); - assertNull(nearbyStop.state.getBackState()); + var lastState = nearbyStop.lastStates.getLast(); + assertEquals(expected, lastState.getVertex()); + assertNull(lastState.getBackState()); } /** @@ -204,8 +205,9 @@ void assertStopAtDistance( ) { assertEquals(stopResolver.getRegularStop(expected.getId()), nearbyStop.stop); assertEquals(expectedDistance, nearbyStop.distance); - assertEquals(expected, nearbyStop.state.getVertex()); + var lastState = nearbyStop.lastStates.getLast(); + assertEquals(expected, lastState.getVertex()); assertFalse(nearbyStop.edges.isEmpty()); - assertNotNull(nearbyStop.state.getBackState()); + assertNotNull(lastState.getBackState()); } } diff --git a/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouterTest.java b/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouterTest.java index da5fbd42947..7b6dc417894 100644 --- a/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouterTest.java +++ b/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouterTest.java @@ -229,7 +229,7 @@ private String nearbyStopDescription(NearbyStop nearbyStop) { if (nearbyStop.edges.isEmpty()) { return "direct[" + nearbyStop.stop.getName() + "]"; } else { - return "street[" + stateDescription(nearbyStop.state) + "]"; + return "street" + nearbyStop.lastStates.stream().map(this::stateDescription).toList(); } } diff --git a/application/src/test/java/org/opentripplanner/routing/graphfinder/DirectGraphFinderTest.java b/application/src/test/java/org/opentripplanner/routing/graphfinder/DirectGraphFinderTest.java index f77521215cd..f758b5b4621 100644 --- a/application/src/test/java/org/opentripplanner/routing/graphfinder/DirectGraphFinderTest.java +++ b/application/src/test/java/org/opentripplanner/routing/graphfinder/DirectGraphFinderTest.java @@ -36,8 +36,8 @@ public void build() { @Test void findClosestStops() { - var ns1 = new NearbyStop(siteRepository.getRegularStop(S1.getId()), 0, null, null); - var ns2 = new NearbyStop(siteRepository.getRegularStop(S2.getId()), 1112, null, null); + var ns1 = new NearbyStop(siteRepository.getRegularStop(S1.getId()), 0, List.of(), List.of()); + var ns2 = new NearbyStop(siteRepository.getRegularStop(S2.getId()), 1112, List.of(), List.of()); var subject = new DirectGraphFinder(siteRepository::findRegularStops); var coordinate = new Coordinate(19.000, 47.500); diff --git a/application/src/test/java/org/opentripplanner/routing/graphfinder/NearbyStopTest.java b/application/src/test/java/org/opentripplanner/routing/graphfinder/NearbyStopTest.java index 6f5fe5c3760..dc603dfc00b 100644 --- a/application/src/test/java/org/opentripplanner/routing/graphfinder/NearbyStopTest.java +++ b/application/src/test/java/org/opentripplanner/routing/graphfinder/NearbyStopTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; import org.junit.jupiter.api.Test; import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; @@ -16,13 +17,13 @@ class NearbyStopTest { void testIsBetter() { // We only test the distance here, since the compareTo method used should have a more complete // unit-test including tests on state weight. - var a = new NearbyStop(MODEL.stop("A").build(), 20.0, null, null); - var b = new NearbyStop(MODEL.stop("A").build(), 30.0, null, null); + var a = new NearbyStop(MODEL.stop("A").build(), 20.0, List.of(), List.of()); + var b = new NearbyStop(MODEL.stop("A").build(), 30.0, List.of(), List.of()); assertTrue(a.isBetter(b)); assertFalse(b.isBetter(a)); - var sameDistance = new NearbyStop(MODEL.stop("A").build(), 20.0, null, null); + var sameDistance = new NearbyStop(MODEL.stop("A").build(), 20.0, List.of(), List.of()); assertFalse(a.isBetter(sameDistance)); assertFalse(sameDistance.isBetter(a)); } diff --git a/application/src/test/java/org/opentripplanner/routing/graphfinder/StreetGraphFinderTest.java b/application/src/test/java/org/opentripplanner/routing/graphfinder/StreetGraphFinderTest.java index de364988b6b..362dc272465 100644 --- a/application/src/test/java/org/opentripplanner/routing/graphfinder/StreetGraphFinderTest.java +++ b/application/src/test/java/org/opentripplanner/routing/graphfinder/StreetGraphFinderTest.java @@ -138,8 +138,8 @@ public void build() { @Test void findClosestStops() { - var ns1 = new NearbyStop(stop(S1), 0, null, null); - var ns2 = new NearbyStop(stop(S2), 100, null, null); + var ns1 = new NearbyStop(stop(S1), 0, List.of(), List.of()); + var ns2 = new NearbyStop(stop(S2), 100, List.of(), List.of()); var coordinate = new Coordinate(19.000, 47.500); assertEquals(List.of(ns1), simplify(graphFinder.findClosestStops(coordinate, 10))); @@ -487,7 +487,7 @@ void findClosestPlacesWithACarParkFilter() { private List simplify(List closestStops) { return closestStops .stream() - .map(ns -> new NearbyStop(ns.stop, ns.distance, null, null)) + .map(ns -> new NearbyStop(ns.stop, ns.distance, List.of(), List.of())) .collect(Collectors.toList()); } From 11b04034f2fa5e1e04379ef1a58ea9e8e41c3e13 Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Mon, 1 Dec 2025 17:02:42 +0200 Subject: [PATCH 09/15] Rename AccessEgressRouter -> DefaultAccessEgressRouter --- .../algorithm/raptoradapter/router/TransitRouter.java | 6 +++--- ...cessEgressRouter.java => DefaultAccessEgressRouter.java} | 6 +++--- .../raptoradapter/router/street/DirectFlexRouter.java | 2 +- .../raptoradapter/router/street/FlexAccessEgressRouter.java | 2 +- ...ssRouterTest.java => DefaultAccessEgressRouterTest.java} | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) rename application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/{AccessEgressRouter.java => DefaultAccessEgressRouter.java} (94%) rename application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/{AccessEgressRouterTest.java => DefaultAccessEgressRouterTest.java} (98%) diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java index 9f0a1f3db8d..e4103989a3f 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java @@ -24,9 +24,9 @@ import org.opentripplanner.raptor.spi.ExtraMcRouterSearch; import org.opentripplanner.routing.algorithm.mapping.RaptorPathToItineraryMapper; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressPenaltyDecorator; -import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressRouter; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressType; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgresses; +import org.opentripplanner.routing.algorithm.raptoradapter.router.street.DefaultAccessEgressRouter; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.FlexAccessEgressRouter; import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitData; import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; @@ -63,7 +63,7 @@ public class TransitRouter { private final AdditionalSearchDays additionalSearchDays; private final ViaCoordinateTransferFactory viaTransferResolver; private final LinkingContext linkingContext; - private final AccessEgressRouter accessEgressRouter; + private final DefaultAccessEgressRouter accessEgressRouter; private TransitRouter( RouteRequest request, @@ -82,7 +82,7 @@ private TransitRouter( this.debugTimingAggregator = debugTimingAggregator; this.viaTransferResolver = serverContext.viaTransferResolver(); this.linkingContext = linkingContext; - this.accessEgressRouter = new AccessEgressRouter( + this.accessEgressRouter = new DefaultAccessEgressRouter( new TransitServiceResolver(serverContext.transitService()) ); } diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouter.java similarity index 94% rename from application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java rename to application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouter.java index 657d3144f03..7aa0c48fe91 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouter.java @@ -20,13 +20,13 @@ /** * This uses a street search to find paths to all the access/egress stop within range */ -public class AccessEgressRouter { +public class DefaultAccessEgressRouter { - private static final Logger LOG = LoggerFactory.getLogger(AccessEgressRouter.class); + private static final Logger LOG = LoggerFactory.getLogger(DefaultAccessEgressRouter.class); private final StopResolver stopResolver; private final NearbyStopFactory nearbyStopFactory; - public AccessEgressRouter(StopResolver stopResolver) { + public DefaultAccessEgressRouter(StopResolver stopResolver) { this.stopResolver = stopResolver; this.nearbyStopFactory = new NearbyStopFactory(stopResolver::getRegularStop); } diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java index 43cf9eb5661..39522fbd496 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java @@ -24,7 +24,7 @@ public static List route( AdditionalSearchDays additionalSearchDays, LinkingContext linkingContext ) { - var accessEgressRouter = new AccessEgressRouter( + var accessEgressRouter = new DefaultAccessEgressRouter( new TransitServiceResolver(serverContext.transitService()) ); if (!StreetMode.FLEXIBLE.equals(request.journey().direct().mode())) { diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java index 140b202cfb7..f41248876c4 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java @@ -23,7 +23,7 @@ private FlexAccessEgressRouter() {} public static Collection routeAccessEgress( RouteRequest request, - AccessEgressRouter accessEgressRouter, + DefaultAccessEgressRouter accessEgressRouter, OtpServerRequestContext serverContext, AdditionalSearchDays searchDays, FlexParameters config, diff --git a/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouterTest.java b/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouterTest.java similarity index 98% rename from application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouterTest.java rename to application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouterTest.java index 7b6dc417894..f19f5772be5 100644 --- a/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouterTest.java +++ b/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouterTest.java @@ -28,7 +28,7 @@ import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TimetableRepository; -class AccessEgressRouterTest extends GraphRoutingTest { +class DefaultAccessEgressRouterTest extends GraphRoutingTest { private Graph graph; private TimetableRepository timetableRepository; @@ -272,7 +272,7 @@ private Collection findAccessEgressFromTo( var linkingRequest = LinkingContextRequestMapper.map(request); var linkingContext = linkingContextFactory.create(verticesContainer, linkingRequest); - return new AccessEgressRouter( + return new DefaultAccessEgressRouter( new SiteRepositoryResolver(timetableRepository.getSiteRepository()) ).findAccessEgresses( request, From 2dc1d43479429121e4df551891a3414ad9eb699f Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Mon, 1 Dec 2025 17:53:35 +0200 Subject: [PATCH 10/15] Refactor AccessEgressRouter to use template method pattern --- .../router/street/AccessEgressRouter.java | 115 ++++++++++++++++++ .../street/DefaultAccessEgressRouter.java | 70 ++--------- 2 files changed, 126 insertions(+), 59 deletions(-) create mode 100644 application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java new file mode 100644 index 00000000000..7fe6021e2e1 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java @@ -0,0 +1,115 @@ +package org.opentripplanner.routing.algorithm.raptoradapter.router.street; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentripplanner.framework.application.OTPRequestTimeoutException; +import org.opentripplanner.graph_builder.module.nearbystops.StopResolver; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.routing.api.request.request.StreetRequest; +import org.opentripplanner.routing.graphfinder.NearbyStop; +import org.opentripplanner.routing.graphfinder.NearbyStopFactory; +import org.opentripplanner.routing.linking.LinkingContext; +import org.opentripplanner.street.model.edge.ExtensionRequestContext; +import org.opentripplanner.street.model.vertex.Vertex; +import org.opentripplanner.utils.collection.ListUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract class for a street search to find paths to all the access/egress stop within range. + * Follows template method pattern. + */ +public abstract class AccessEgressRouter { + + private static final Logger LOG = LoggerFactory.getLogger(AccessEgressRouter.class); + private final NearbyStopFactory nearbyStopFactory; + + public AccessEgressRouter(StopResolver stopResolver) { + this.nearbyStopFactory = new NearbyStopFactory(stopResolver::getRegularStop); + } + + /** + * Find accesses or egresses. + */ + public Collection findAccessEgresses( + RouteRequest request, + StreetRequest streetRequest, + Collection extensionRequestContexts, + AccessEgressType accessOrEgress, + Duration durationLimit, + int maxStopCount, + LinkingContext linkingContext + ) { + OTPRequestTimeoutException.checkForTimeout(); + + // Note: We calculate access/egresses in two parts. First we fetch the stops with zero distance. + // Then we do street search. This is because some stations might use the centroid for street + // routing, but should still give zero distance access/egresses to its child-stops. + var zeroDistanceAccessEgress = findAccessEgressWithZeroDistance( + request, + streetRequest, + accessOrEgress, + linkingContext + ); + + // When looking for street accesses/egresses we ignore the already found direct accesses/egresses + var ignoreVertices = zeroDistanceAccessEgress + .stream() + .map(nearbyStop -> nearbyStop.lastStates.getLast().getVertex()) + .collect(Collectors.toSet()); + + var streetAccessEgress = findStreetAccessEgresses( + request, + streetRequest, + extensionRequestContexts, + accessOrEgress, + durationLimit, + maxStopCount, + linkingContext, + ignoreVertices + ); + + var results = ListUtils.combine(zeroDistanceAccessEgress, streetAccessEgress); + LOG.debug("Found {} {} stops", results.size(), accessOrEgress); + return results; + } + + /** + * Find accesses or egresses using street routing. + */ + abstract Collection findStreetAccessEgresses( + RouteRequest request, + StreetRequest streetRequest, + Collection extensionRequestContexts, + AccessEgressType accessOrEgress, + Duration durationLimit, + int maxStopCount, + LinkingContext linkingContext, + Set ignoreVertices + ); + + /** + * Return a list of direct accesses/egresses that do not require any street search. This will + * return an empty list if the source/destination is not a stopId. + */ + private List findAccessEgressWithZeroDistance( + RouteRequest routeRequest, + StreetRequest streetRequest, + AccessEgressType accessOrEgress, + LinkingContext linkingContext + ) { + var transitStopVertices = accessOrEgress.isAccess() + ? linkingContext.fromStopVertices() + : linkingContext.toStopVertices(); + + return nearbyStopFactory.nearbyStopsForTransitStopVerticesFiltered( + transitStopVertices, + accessOrEgress.isEgress(), + routeRequest, + streetRequest + ); + } +} diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouter.java index 7aa0c48fe91..3d090c4387e 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouter.java @@ -2,98 +2,50 @@ import java.time.Duration; import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; -import org.opentripplanner.framework.application.OTPRequestTimeoutException; +import java.util.Set; import org.opentripplanner.graph_builder.module.nearbystops.StopResolver; import org.opentripplanner.graph_builder.module.nearbystops.StreetNearbyStopFinder; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.request.StreetRequest; import org.opentripplanner.routing.graphfinder.NearbyStop; -import org.opentripplanner.routing.graphfinder.NearbyStopFactory; import org.opentripplanner.routing.linking.LinkingContext; import org.opentripplanner.street.model.edge.ExtensionRequestContext; -import org.opentripplanner.utils.collection.ListUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.opentripplanner.street.model.vertex.Vertex; /** - * This uses a street search to find paths to all the access/egress stop within range + * This uses a street search to find paths to all the access/egress stop within range. Doesn't + * support routing through via locations. */ -public class DefaultAccessEgressRouter { +public class DefaultAccessEgressRouter extends AccessEgressRouter { - private static final Logger LOG = LoggerFactory.getLogger(DefaultAccessEgressRouter.class); private final StopResolver stopResolver; - private final NearbyStopFactory nearbyStopFactory; public DefaultAccessEgressRouter(StopResolver stopResolver) { + super(stopResolver); this.stopResolver = stopResolver; - this.nearbyStopFactory = new NearbyStopFactory(stopResolver::getRegularStop); } /** * Find accesses or egresses. */ - public Collection findAccessEgresses( + @Override + Collection findStreetAccessEgresses( RouteRequest request, StreetRequest streetRequest, Collection extensionRequestContexts, AccessEgressType accessOrEgress, Duration durationLimit, int maxStopCount, - LinkingContext linkingContext + LinkingContext linkingContext, + Set ignoreVertices ) { - OTPRequestTimeoutException.checkForTimeout(); - - // Note: We calculate access/egresses in two parts. First we fetch the stops with zero distance. - // Then we do street search. This is because some stations might use the centroid for street - // routing, but should still give zero distance access/egresses to its child-stops. - var zeroDistanceAccessEgress = findAccessEgressWithZeroDistance( - request, - streetRequest, - accessOrEgress, - linkingContext - ); - - // When looking for street accesses/egresses we ignore the already found direct accesses/egresses - var ignoreVertices = zeroDistanceAccessEgress - .stream() - .map(nearbyStop -> nearbyStop.lastStates.getLast().getVertex()) - .collect(Collectors.toSet()); - var originVertices = accessOrEgress.isAccess() ? linkingContext.findVertices(request.from()) : linkingContext.findVertices(request.to()); - var streetAccessEgress = StreetNearbyStopFinder.of(stopResolver, durationLimit, maxStopCount) + return StreetNearbyStopFinder.of(stopResolver, durationLimit, maxStopCount) .withIgnoreVertices(ignoreVertices) .withExtensionRequestContexts(extensionRequestContexts) .build() .findNearbyStops(originVertices, request, streetRequest, accessOrEgress.isEgress()); - - var results = ListUtils.combine(zeroDistanceAccessEgress, streetAccessEgress); - LOG.debug("Found {} {} stops", results.size(), accessOrEgress); - return results; - } - - /** - * Return a list of direct accesses/egresses that do not require any street search. This will - * return an empty list if the source/destination is not a stopId. - */ - private List findAccessEgressWithZeroDistance( - RouteRequest routeRequest, - StreetRequest streetRequest, - AccessEgressType accessOrEgress, - LinkingContext linkingContext - ) { - var transitStopVertices = accessOrEgress.isAccess() - ? linkingContext.fromStopVertices() - : linkingContext.toStopVertices(); - - return nearbyStopFactory.nearbyStopsForTransitStopVerticesFiltered( - transitStopVertices, - accessOrEgress.isEgress(), - routeRequest, - streetRequest - ); } } From b30fcd77a2c0086e2befa297db2c0345b6d38b8b Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Wed, 3 Dec 2025 13:20:23 +0200 Subject: [PATCH 11/15] Implement numberOfViaLocationsVisited in DefaultAccessEgress --- .../algorithm/raptoradapter/transit/DefaultAccessEgress.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java index 021476bd0a0..d857af51e2d 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java @@ -138,6 +138,11 @@ public int latestArrivalTime(int requestedArrivalTime) { return requestedArrivalTime; } + @Override + public int numberOfViaLocationsVisited() { + return Math.max(0, lastStates.size() - 1); + } + @Override public String toString() { return asString(true, true, summary()); From 1b1a2064163f50fb336ef547013fd577adef6357 Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Fri, 5 Dec 2025 00:15:39 +0200 Subject: [PATCH 12/15] Remove unused parameters --- .../algorithm/mapping/RaptorPathToItineraryMapper.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java index efa7a21eff1..a4c4c8aa45c 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java @@ -353,7 +353,7 @@ private List mapTransferLeg(TransferPathLeg pathLeg, TraverseMode transf return mapTransferLeg(pathLeg, dftTx.transfer(), transferMode, from, to); } if (raptorTransfer instanceof ViaCoordinateTransfer viaTx) { - return mapViaCoordinateTransferLeg(pathLeg, viaTx, transferMode, from, to); + return mapViaCoordinateTransferLeg(pathLeg, viaTx); } throw new IllegalArgumentException("Unknown transfer type: " + raptorTransfer.getClass()); } @@ -402,10 +402,7 @@ private List mapTransferLeg( private List mapViaCoordinateTransferLeg( PathLeg pathLeg, - ViaCoordinateTransfer transfer, - TraverseMode transferMode, - Place from, - Place to + ViaCoordinateTransfer transfer ) { var fromLegs = mapTransferLegWithEdges(pathLeg.fromTime(), transfer.fromEdges()); var toLegs = mapTransferLegWithEdges(pathLeg.toTime(), transfer.toEdges()); From b7a692987a795008d3bc039d8f97f29df93ec763 Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Fri, 5 Dec 2025 12:39:56 +0200 Subject: [PATCH 13/15] Add support for via locations in access/egress --- .../raptoradapter/router/TransitRouter.java | 12 +- .../router/street/AccessEgressRouter.java | 17 +- .../street/AccessEgressRouterFactory.java | 20 ++ .../street/DefaultAccessEgressRouter.java | 12 +- .../router/street/DirectFlexRouter.java | 8 +- .../router/street/FlexAccessEgressRouter.java | 10 +- .../router/street/ViaAccessEgressRouter.java | 311 ++++++++++++++++++ .../router/street/ViaDirectStreetRouter.java | 154 +++++---- .../transit/DefaultAccessEgress.java | 4 +- application/src/main/resources/logback.xml | 2 +- .../street/DefaultAccessEgressRouterTest.java | 4 +- 11 files changed, 467 insertions(+), 87 deletions(-) create mode 100644 application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouterFactory.java create mode 100644 application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaAccessEgressRouter.java diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java index e4103989a3f..ceba01f7e6b 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java @@ -24,9 +24,10 @@ import org.opentripplanner.raptor.spi.ExtraMcRouterSearch; import org.opentripplanner.routing.algorithm.mapping.RaptorPathToItineraryMapper; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressPenaltyDecorator; +import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressRouter; +import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressRouterFactory; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressType; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgresses; -import org.opentripplanner.routing.algorithm.raptoradapter.router.street.DefaultAccessEgressRouter; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.FlexAccessEgressRouter; import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitData; import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; @@ -63,7 +64,7 @@ public class TransitRouter { private final AdditionalSearchDays additionalSearchDays; private final ViaCoordinateTransferFactory viaTransferResolver; private final LinkingContext linkingContext; - private final DefaultAccessEgressRouter accessEgressRouter; + private final AccessEgressRouter accessEgressRouter; private TransitRouter( RouteRequest request, @@ -82,7 +83,8 @@ private TransitRouter( this.debugTimingAggregator = debugTimingAggregator; this.viaTransferResolver = serverContext.viaTransferResolver(); this.linkingContext = linkingContext; - this.accessEgressRouter = new DefaultAccessEgressRouter( + this.accessEgressRouter = AccessEgressRouterFactory.create( + request, new TransitServiceResolver(serverContext.transitService()) ); } @@ -275,11 +277,13 @@ private Collection fetchAccessEgresses(AccessEgre var nearbyStops = accessEgressRouter.findAccessEgresses( accessRequest, streetRequest, + serverContext.traverseVisitor(), serverContext.listExtensionRequestContexts(accessRequest), type, durationLimit, stopCountLimit, - linkingContext + linkingContext, + serverContext.streetLimitationParametersService().maxCarSpeed() ); var accessEgresses = AccessEgressMapper.mapNearbyStops(nearbyStops, type); accessEgresses = timeshiftRideHailing(streetRequest, type, accessEgresses); diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java index 7fe6021e2e1..e8d971fc67a 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouter.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import org.opentripplanner.astar.spi.TraverseVisitor; import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.graph_builder.module.nearbystops.StopResolver; import org.opentripplanner.routing.api.request.RouteRequest; @@ -12,8 +13,10 @@ import org.opentripplanner.routing.graphfinder.NearbyStop; import org.opentripplanner.routing.graphfinder.NearbyStopFactory; import org.opentripplanner.routing.linking.LinkingContext; +import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.edge.ExtensionRequestContext; import org.opentripplanner.street.model.vertex.Vertex; +import org.opentripplanner.street.search.state.State; import org.opentripplanner.utils.collection.ListUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,7 +30,7 @@ public abstract class AccessEgressRouter { private static final Logger LOG = LoggerFactory.getLogger(AccessEgressRouter.class); private final NearbyStopFactory nearbyStopFactory; - public AccessEgressRouter(StopResolver stopResolver) { + AccessEgressRouter(StopResolver stopResolver) { this.nearbyStopFactory = new NearbyStopFactory(stopResolver::getRegularStop); } @@ -37,11 +40,13 @@ public AccessEgressRouter(StopResolver stopResolver) { public Collection findAccessEgresses( RouteRequest request, StreetRequest streetRequest, + TraverseVisitor traverseVisitor, Collection extensionRequestContexts, AccessEgressType accessOrEgress, Duration durationLimit, int maxStopCount, - LinkingContext linkingContext + LinkingContext linkingContext, + float maxCarSpeed ) { OTPRequestTimeoutException.checkForTimeout(); @@ -64,12 +69,14 @@ public Collection findAccessEgresses( var streetAccessEgress = findStreetAccessEgresses( request, streetRequest, + traverseVisitor, extensionRequestContexts, accessOrEgress, durationLimit, maxStopCount, linkingContext, - ignoreVertices + ignoreVertices, + maxCarSpeed ); var results = ListUtils.combine(zeroDistanceAccessEgress, streetAccessEgress); @@ -83,12 +90,14 @@ public Collection findAccessEgresses( abstract Collection findStreetAccessEgresses( RouteRequest request, StreetRequest streetRequest, + TraverseVisitor traverseVisitor, Collection extensionRequestContexts, AccessEgressType accessOrEgress, Duration durationLimit, int maxStopCount, LinkingContext linkingContext, - Set ignoreVertices + Set ignoreVertices, + float maxCarSpeed ); /** diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouterFactory.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouterFactory.java new file mode 100644 index 00000000000..156bd6b222a --- /dev/null +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressRouterFactory.java @@ -0,0 +1,20 @@ +package org.opentripplanner.routing.algorithm.raptoradapter.router.street; + +import org.opentripplanner.graph_builder.module.nearbystops.StopResolver; +import org.opentripplanner.routing.api.request.RouteRequest; + +/** + * This factory encapsulates the logic for deciding which access/egress router to use. + */ +public class AccessEgressRouterFactory { + + /** + * @return {@link DefaultAccessEgressRouter} if there are no via locations, otherwise + * {@link ViaAccessEgressRouter}. + */ + public static AccessEgressRouter create(RouteRequest request, StopResolver stopResolver) { + return request.isViaSearch() + ? new ViaAccessEgressRouter(stopResolver) + : new DefaultAccessEgressRouter(stopResolver); + } +} diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouter.java index 3d090c4387e..7b046eceb6e 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouter.java @@ -3,14 +3,17 @@ import java.time.Duration; import java.util.Collection; import java.util.Set; +import org.opentripplanner.astar.spi.TraverseVisitor; import org.opentripplanner.graph_builder.module.nearbystops.StopResolver; import org.opentripplanner.graph_builder.module.nearbystops.StreetNearbyStopFinder; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.request.StreetRequest; import org.opentripplanner.routing.graphfinder.NearbyStop; import org.opentripplanner.routing.linking.LinkingContext; +import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.edge.ExtensionRequestContext; import org.opentripplanner.street.model.vertex.Vertex; +import org.opentripplanner.street.search.state.State; /** * This uses a street search to find paths to all the access/egress stop within range. Doesn't @@ -20,24 +23,23 @@ public class DefaultAccessEgressRouter extends AccessEgressRouter { private final StopResolver stopResolver; - public DefaultAccessEgressRouter(StopResolver stopResolver) { + DefaultAccessEgressRouter(StopResolver stopResolver) { super(stopResolver); this.stopResolver = stopResolver; } - /** - * Find accesses or egresses. - */ @Override Collection findStreetAccessEgresses( RouteRequest request, StreetRequest streetRequest, + TraverseVisitor traverseVisitor, Collection extensionRequestContexts, AccessEgressType accessOrEgress, Duration durationLimit, int maxStopCount, LinkingContext linkingContext, - Set ignoreVertices + Set ignoreVertices, + float maxCarSpeed ) { var originVertices = accessOrEgress.isAccess() ? linkingContext.findVertices(request.from()) diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java index 39522fbd496..d84816d31c3 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java @@ -35,20 +35,24 @@ public static List route( Collection accessStops = accessEgressRouter.findAccessEgresses( request, request.journey().direct(), + serverContext.traverseVisitor(), serverContext.listExtensionRequestContexts(request), AccessEgressType.ACCESS, serverContext.flexParameters().maxAccessWalkDuration(), 0, - linkingContext + linkingContext, + serverContext.streetLimitationParametersService().maxCarSpeed() ); Collection egressStops = accessEgressRouter.findAccessEgresses( request, request.journey().direct(), + serverContext.traverseVisitor(), serverContext.listExtensionRequestContexts(request), AccessEgressType.EGRESS, serverContext.flexParameters().maxEgressWalkDuration(), 0, - linkingContext + linkingContext, + serverContext.streetLimitationParametersService().maxCarSpeed() ); var flexRouter = new FlexRouter( diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java index f41248876c4..81e3be48675 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java @@ -23,7 +23,7 @@ private FlexAccessEgressRouter() {} public static Collection routeAccessEgress( RouteRequest request, - DefaultAccessEgressRouter accessEgressRouter, + AccessEgressRouter accessEgressRouter, OtpServerRequestContext serverContext, AdditionalSearchDays searchDays, FlexParameters config, @@ -39,11 +39,13 @@ public static Collection routeAccessEgress( ? accessEgressRouter.findAccessEgresses( request, new StreetRequest(StreetMode.WALK), + serverContext.traverseVisitor(), extensionRequestContexts, AccessEgressType.ACCESS, serverContext.flexParameters().maxAccessWalkDuration(), 0, - linkingContext + linkingContext, + serverContext.streetLimitationParametersService().maxCarSpeed() ) : List.of(); @@ -51,11 +53,13 @@ public static Collection routeAccessEgress( ? accessEgressRouter.findAccessEgresses( request, new StreetRequest(StreetMode.WALK), + serverContext.traverseVisitor(), extensionRequestContexts, AccessEgressType.EGRESS, serverContext.flexParameters().maxEgressWalkDuration(), 0, - linkingContext + linkingContext, + serverContext.streetLimitationParametersService().maxCarSpeed() ) : List.of(); diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaAccessEgressRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaAccessEgressRouter.java new file mode 100644 index 00000000000..17dc06d3e81 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaAccessEgressRouter.java @@ -0,0 +1,311 @@ +package org.opentripplanner.routing.algorithm.raptoradapter.router.street; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.OptionalInt; +import java.util.Set; +import org.opentripplanner.astar.spi.TraverseVisitor; +import org.opentripplanner.graph_builder.module.nearbystops.StopResolver; +import org.opentripplanner.graph_builder.module.nearbystops.StreetNearbyStopFinder; +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.api.request.via.VisitViaLocation; +import org.opentripplanner.routing.graphfinder.NearbyStop; +import org.opentripplanner.routing.impl.GraphPathFinder; +import org.opentripplanner.routing.linking.LinkingContext; +import org.opentripplanner.street.model.edge.Edge; +import org.opentripplanner.street.model.edge.ExtensionRequestContext; +import org.opentripplanner.street.model.vertex.Vertex; +import org.opentripplanner.street.search.state.State; +import org.opentripplanner.utils.collection.ListUtils; + +public class ViaAccessEgressRouter extends AccessEgressRouter { + + private final StopResolver stopResolver; + private final ViaDirectStreetRouter directRouter; + + ViaAccessEgressRouter(StopResolver stopResolver) { + super(stopResolver); + this.stopResolver = stopResolver; + this.directRouter = new ViaDirectStreetRouter(); + } + + @Override + Collection findStreetAccessEgresses( + RouteRequest request, + StreetRequest streetRequest, + TraverseVisitor traverseVisitor, + Collection extensionRequestContexts, + AccessEgressType accessOrEgress, + Duration durationLimit, + int maxStopCount, + LinkingContext linkingContext, + Set ignoreVertices, + float maxCarSpeed + ) { + var lastViaWithCoordinates = lastCoordinateViaLocationIndex(request, accessOrEgress); + // There are no coordinate locations to route to/from + if (lastViaWithCoordinates.isEmpty()) { + return List.of(); + } + var graphPathFinder = new GraphPathFinder( + traverseVisitor, + extensionRequestContexts, + maxCarSpeed + ); + var directRequest = getDirectRequest( + request, + accessOrEgress, + lastViaWithCoordinates.getAsInt() + ); + var accessEgressRequest = getViaFriendlyRequest(request); + return accessOrEgress.isAccess() + ? findStreetAccesses( + accessEgressRequest, + directRequest, + streetRequest, + extensionRequestContexts, + durationLimit, + maxStopCount, + linkingContext, + ignoreVertices, + graphPathFinder + ) + : findStreetEgresses( + accessEgressRequest, + directRequest, + streetRequest, + extensionRequestContexts, + durationLimit, + maxStopCount, + linkingContext, + ignoreVertices, + graphPathFinder + ); + } + + private Collection findStreetAccesses( + RouteRequest accessEgressRequest, + RouteRequest directRequest, + StreetRequest streetRequest, + Collection extensionRequestContexts, + Duration durationLimit, + int maxStopCount, + LinkingContext linkingContext, + Set ignoreVertices, + GraphPathFinder graphPathFinder + ) { + var originVertices = linkingContext.findVertices(accessEgressRequest.from()); + var stopsFromOrigin = StreetNearbyStopFinder.of(stopResolver, durationLimit, maxStopCount) + .withIgnoreVertices(ignoreVertices) + .withExtensionRequestContexts(extensionRequestContexts) + .build() + .findNearbyStops(originVertices, accessEgressRequest, streetRequest, false); + var nearbyStops = new ArrayList<>(stopsFromOrigin); + var paths = directRouter.findDepartAfterPaths( + linkingContext, + graphPathFinder, + directRequest, + true, + true + ); + var vias = accessEgressRequest.listViaLocationsWithCoordinates(); + var durationLeft = durationLimit; + var i = 0; + var accumulatedDistance = 0.0; + var accumulatedEdges = new ArrayList(); + var accumulatedLastStates = new ArrayList(); + while (i < paths.size() && durationLeft.isPositive()) { + var path = paths.get(i); + accumulatedDistance += path.edges.stream().mapToDouble(Edge::getDistanceMeters).sum(); + accumulatedEdges.addAll(path.edges); + accumulatedLastStates.add(path.states.getLast()); + durationLeft = durationLeft.minus(Duration.ofSeconds(path.getDuration())); + var via = vias.get(i); + var vertices = linkingContext.findVertices(via); + var viaRequest = getViaRequest( + accessEgressRequest, + via, + Instant.ofEpochSecond(path.getEndTime()) + ); + // TODO implement some algorithm for lowering the maximum stop count. + var stopsFromVia = StreetNearbyStopFinder.of(stopResolver, durationLeft, maxStopCount) + .withIgnoreVertices(ignoreVertices) + .withExtensionRequestContexts(extensionRequestContexts) + .build() + .findNearbyStops(vertices, viaRequest, streetRequest, false); + for (NearbyStop nearbyStop : stopsFromVia) { + var distance = accumulatedDistance + nearbyStop.distance; + var edges = ListUtils.combine(accumulatedEdges, nearbyStop.edges); + var lastStates = ListUtils.combine(accumulatedLastStates, nearbyStop.lastStates); + var adjustedStop = new NearbyStop(nearbyStop.stop, distance, edges, lastStates); + nearbyStops.add(adjustedStop); + } + i += 1; + } + return nearbyStops; + } + + private Collection findStreetEgresses( + RouteRequest accessEgressRequest, + RouteRequest directRequest, + StreetRequest streetRequest, + Collection extensionRequestContexts, + Duration durationLimit, + int maxStopCount, + LinkingContext linkingContext, + Set ignoreVertices, + GraphPathFinder graphPathFinder + ) { + var originVertices = linkingContext.findVertices(accessEgressRequest.to()); + var stopsFromOrigin = StreetNearbyStopFinder.of(stopResolver, durationLimit, maxStopCount) + .withIgnoreVertices(ignoreVertices) + .withExtensionRequestContexts(extensionRequestContexts) + .build() + .findNearbyStops(originVertices, accessEgressRequest, streetRequest, true); + var nearbyStops = new ArrayList<>(stopsFromOrigin); + var paths = directRouter.findArriveByPaths( + linkingContext, + graphPathFinder, + directRequest, + true, + true + ); + var vias = accessEgressRequest.listViaLocationsWithCoordinates(); + var durationLeft = durationLimit; + var i = 0; + var accumulatedDistance = 0.0; + var accumulatedEdges = new ArrayList(); + var accumulatedLastStates = new ArrayList(); + while (i < paths.size() && durationLeft.isPositive()) { + var path = paths.get(i); + accumulatedDistance += path.edges.stream().mapToDouble(Edge::getDistanceMeters).sum(); + accumulatedEdges.addAll(path.edges); + // TODO try to get rid of this reverse since now the state is reversed three times in total + accumulatedLastStates.add(path.states.getLast().reverse()); + durationLeft = durationLeft.minus(Duration.ofSeconds(path.getDuration())); + var via = vias.get(i); + var vertices = linkingContext.findVertices(via); + var viaRequest = getViaRequest( + accessEgressRequest, + via, + Instant.ofEpochSecond(path.getStartTime()) + ); + // TODO implement some algorithm for lowering the maximum stop count. + var stopsFromVia = StreetNearbyStopFinder.of(stopResolver, durationLeft, maxStopCount) + .withIgnoreVertices(ignoreVertices) + .withExtensionRequestContexts(extensionRequestContexts) + .build() + .findNearbyStops(vertices, viaRequest, streetRequest, true); + for (NearbyStop nearbyStop : stopsFromVia) { + var distance = accumulatedDistance + nearbyStop.distance; + var edges = ListUtils.combine(nearbyStop.edges, accumulatedEdges); + var lastStates = ListUtils.combine(accumulatedLastStates, nearbyStop.lastStates); + var adjustedStop = new NearbyStop(nearbyStop.stop, distance, edges, lastStates); + nearbyStops.add(adjustedStop); + } + i += 1; + } + return nearbyStops; + } + + /** + * @return the last index of the last consecutive via location from the beginning (access) or from + * the end (egress), or empty if the first via location doesn't have a coordinate. + */ + private OptionalInt lastCoordinateViaLocationIndex(RouteRequest request, AccessEgressType type) { + var vias = type.isAccess() ? request.listViaLocations() : request.listViaLocations().reversed(); + int viaIndex = -1; + for (int i = 0; i < vias.size(); i++) { + if (vias.get(i) instanceof VisitViaLocation visit) { + if (!visit.coordinates().isEmpty()) { + viaIndex = i; + } else { + break; + } + } else { + break; + } + } + return viaIndex == -1 ? OptionalInt.empty() : OptionalInt.of(viaIndex); + } + + /** + * TODO we might want to continue on a vehicle if there is no wait time defined for a via point. + */ + private RouteRequest getViaFriendlyRequest(RouteRequest originalRequest) { + return originalRequest + .copyOf() + // TODO we might want to change this behaviour + .withPreferences(preferences -> + preferences + .withBike(bike -> + bike.withRental(rental -> rental.withAllowArrivingInRentedVehicleAtDestination(false)) + ) + .withScooter(scooter -> + scooter.withRental(rental -> + rental.withAllowArrivingInRentedVehicleAtDestination(false) + ) + ) + .withCar(car -> + car.withRental(rental -> rental.withAllowArrivingInRentedVehicleAtDestination(false)) + ) + ) + .buildRequest(); + } + + private RouteRequest getDirectRequest( + RouteRequest request, + AccessEgressType type, + int lastViaWithCoordinates + ) { + var originalVias = request.listViaLocations(); + // Filter out all via locations after the last consecutive one with coordinates + var vias = type.isAccess() + ? originalVias.subList(0, lastViaWithCoordinates + 1) + : originalVias.subList(originalVias.size() - lastViaWithCoordinates - 1, originalVias.size()); + var mode = getStreetModeAfterOrigin(request.journey().access().mode()); + var maxDuration = getMaxDuration(request); + return request + .copyOf() + .withJourney(journey -> journey.withDirect(new StreetRequest(mode))) + .withPreferences(preferences -> + preferences.withStreet(street -> + street.withMaxDirectDuration(streetModeBuilder -> + streetModeBuilder.with(mode, maxDuration) + ) + ) + ) + .withViaLocations(vias) + .withArriveBy(type.isEgress()) + .buildRequest(); + } + + private RouteRequest getViaRequest(RouteRequest request, GenericLocation via, Instant startTime) { + return request.copyOf().withDateTime(startTime).withFrom(via).buildRequest(); + } + + /** + * TODO we might want to continue on a vehicle if there is no wait time defined for a via point. + */ + private StreetMode getStreetModeAfterOrigin(StreetMode mode) { + if (mode.includesParking() || mode.includesRenting()) { + return StreetMode.WALK; + } + return mode; + } + + private Duration getMaxDuration(RouteRequest request) { + return request + .preferences() + .street() + .accessEgress() + .maxDuration() + .valueOf(request.journey().access().mode()); + } +} diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java index ba59fdc57ce..7dc0fbc79fd 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaDirectStreetRouter.java @@ -11,6 +11,7 @@ import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.api.request.request.StreetRequest; import org.opentripplanner.routing.api.request.via.ViaLocation; +import org.opentripplanner.routing.error.PathNotFoundException; import org.opentripplanner.routing.impl.GraphPathFinder; import org.opentripplanner.routing.linking.LinkingContext; import org.opentripplanner.street.model.edge.Edge; @@ -26,8 +27,8 @@ List> findPaths( RouteRequest request ) { return request.arriveBy() - ? findArriveByPaths(linkingContext, graphPathFinder, request) - : findDepartAfterPaths(linkingContext, graphPathFinder, request); + ? findArriveByPaths(linkingContext, graphPathFinder, request, false, false) + : findDepartAfterPaths(linkingContext, graphPathFinder, request, false, false); } @Override @@ -66,10 +67,12 @@ boolean isStraightLineDistanceWithinLimit( return distance < maxDistanceLimit; } - private List> findArriveByPaths( + List> findArriveByPaths( LinkingContext linkingContext, GraphPathFinder graphPathFinder, - RouteRequest request + RouteRequest request, + boolean allowPartialResults, + boolean skipLastLeg ) { var baseRequest = getViaFriendlyRequest(request); var mode = baseRequest.journey().direct().mode(); @@ -84,81 +87,102 @@ private List> findArriveByPaths( var newStartTime = request.dateTime(); var maxDurationLeft = getMaximumDirectDuration(request, mode); int i = lastLocations.size() - 2; - while (i >= 0 && maxDurationLeft.isPositive()) { - var from = lastLocations.get(i); - var to = lastLocations.get(i + 1); - var patchedRequest = getRequest( - requestWithNewMode, - from, - to, - newStartTime, - newStreetRequest.mode(), - maxDurationLeft - ); - var path = graphPathFinder.graphPathFinderEntryPoint(patchedRequest, linkingContext); - paths.add(path); - - var minimumWaitTime = minimumWaitTimes.get(i); - newStartTime = Instant.ofEpochSecond(path.getStartTime()).minus(minimumWaitTime); - // Wait time is not counted here as it doesn't slow down routing or inconvenience travelers - // like travel time does - maxDurationLeft = maxDurationLeft.minus(Duration.ofSeconds(path.getDuration())); - i--; - } + try { + while (i >= 0 && maxDurationLeft.isPositive()) { + var from = lastLocations.get(i); + var to = lastLocations.get(i + 1); + var patchedRequest = getRequest( + requestWithNewMode, + from, + to, + newStartTime, + newStreetRequest.mode(), + maxDurationLeft + ); + var path = graphPathFinder.graphPathFinderEntryPoint(patchedRequest, linkingContext); + paths.add(path); - var firstRequest = getRequest( - baseRequest, - baseRequest.from(), - baseRequest.listViaLocationsWithCoordinates().getFirst(), - newStartTime, - mode, - maxDurationLeft - ); - paths.add(graphPathFinder.graphPathFinderEntryPoint(firstRequest, linkingContext)); + var minimumWaitTime = minimumWaitTimes.get(i); + newStartTime = Instant.ofEpochSecond(path.getStartTime()).minus(minimumWaitTime); + // Wait time is not counted here as it doesn't slow down routing or inconvenience travelers + // like travel time does + maxDurationLeft = maxDurationLeft.minus(Duration.ofSeconds(path.getDuration())); + i--; + } + + if (!skipLastLeg) { + var firstRequest = getRequest( + baseRequest, + baseRequest.from(), + baseRequest.listViaLocationsWithCoordinates().getFirst(), + newStartTime, + mode, + maxDurationLeft + ); + paths.add(graphPathFinder.graphPathFinderEntryPoint(firstRequest, linkingContext)); + } + } catch (PathNotFoundException e) { + if (!allowPartialResults) { + throw e; + } + } return paths.reversed(); } - private List> findDepartAfterPaths( + List> findDepartAfterPaths( LinkingContext linkingContext, GraphPathFinder graphPathFinder, - RouteRequest request + RouteRequest request, + boolean allowPartialResults, + boolean skipLastLeg ) { var vias = request.listViaLocationsWithCoordinates(); var baseRequest = getViaFriendlyRequest(request); var firstRequest = baseRequest.copyOf().withTo(vias.getFirst()).buildRequest(); List> paths = new ArrayList<>(); - paths.add(graphPathFinder.graphPathFinderEntryPoint(firstRequest, linkingContext)); + try { + paths.add(graphPathFinder.graphPathFinderEntryPoint(firstRequest, linkingContext)); - var mode = baseRequest.journey().direct().mode(); - var newStreetRequest = getStreetRequestAfterFirstVia(mode); - var requestWithNewMode = getRequestWithNewMode(firstRequest, newStreetRequest); + var mode = baseRequest.journey().direct().mode(); + var newStreetRequest = getStreetRequestAfterFirstVia(mode); + var requestWithNewMode = getRequestWithNewMode(firstRequest, newStreetRequest); - var lastLocations = new ArrayList<>(vias); - lastLocations.add(baseRequest.to()); - var minimumWaitTimes = getMinimumWaitTimes(baseRequest); - var maxDurationLeft = getMaximumDirectDuration(request, mode).minus( - Duration.ofSeconds(paths.getFirst().getDuration()) - ); - int i = 0; - while (i < lastLocations.size() - 1 && maxDurationLeft.isPositive()) { - var from = lastLocations.get(i); - var to = lastLocations.get(i + 1); - var minimumWaitTime = minimumWaitTimes.get(i); - var newStartTime = Instant.ofEpochSecond(paths.getLast().getEndTime()).plus(minimumWaitTime); - var patchedRequest = getRequest( - requestWithNewMode, - from, - to, - newStartTime, - newStreetRequest.mode(), - maxDurationLeft + var lastLocations = new ArrayList<>(vias); + if (!skipLastLeg) { + lastLocations.add(baseRequest.to()); + } + var minimumWaitTimes = getMinimumWaitTimes(baseRequest); + + var maxDurationLeft = getMaximumDirectDuration(request, mode).minus( + Duration.ofSeconds(paths.getFirst().getDuration()) ); - var path = graphPathFinder.graphPathFinderEntryPoint(patchedRequest, linkingContext); - paths.add(path); - // Wait time is not counted here as it doesn't slow down routing or inconvenience travelers - // like travel time does - maxDurationLeft = maxDurationLeft.minus(Duration.ofSeconds(path.getDuration())); - i++; + int i = 1; + while (i < lastLocations.size() && maxDurationLeft.isPositive()) { + var from = lastLocations.get(i - 1); + var to = lastLocations.get(i); + var minimumWaitTime = minimumWaitTimes.get(i - 1); + var newStartTime = Instant.ofEpochSecond(paths.getLast().getEndTime()).plus( + minimumWaitTime + ); + var patchedRequest = getRequest( + requestWithNewMode, + from, + to, + newStartTime, + newStreetRequest.mode(), + maxDurationLeft + ); + var path = graphPathFinder.graphPathFinderEntryPoint(patchedRequest, linkingContext); + paths.add(path); + // Wait time is not counted here as it doesn't slow down routing or inconvenience travelers + // like travel time does + maxDurationLeft = maxDurationLeft.minus(Duration.ofSeconds(path.getDuration())); + i++; + } + } catch (PathNotFoundException e) { + if (!allowPartialResults) { + throw e; + } } return paths; } diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java index d857af51e2d..af9c468f8aa 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java @@ -36,14 +36,14 @@ public DefaultAccessEgress( int durationInSeconds, int generalizedCost, TimeAndCost penalty, - List lastState + List lastStates ) { this.stop = stop; this.durationInSeconds = durationInSeconds; this.generalizedCost = generalizedCost; this.timePenalty = penalty.isZero() ? RaptorConstants.TIME_NOT_SET : penalty.timeInSeconds(); this.penalty = penalty; - this.lastStates = ListUtils.requireAtLeastNElements(lastState, 1); + this.lastStates = ListUtils.requireAtLeastNElements(lastStates, 1); } public DefaultAccessEgress(int stop, List lastStates) { diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index f2c4f20c078..ae48864c0d7 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -97,7 +97,7 @@ - + diff --git a/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouterTest.java b/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouterTest.java index f19f5772be5..1f118cb1fa9 100644 --- a/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouterTest.java +++ b/application/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DefaultAccessEgressRouterTest.java @@ -277,11 +277,13 @@ private Collection findAccessEgressFromTo( ).findAccessEgresses( request, StreetRequest.DEFAULT, + null, List.of(), accessEgress, durationLimit, maxStopCount, - linkingContext + linkingContext, + 30 ); } } From 04274c3753e9557691fe92836f8ecc8299b8cdad Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Fri, 5 Dec 2025 14:35:34 +0200 Subject: [PATCH 14/15] Fix access/egress when via locations don't have coordinates --- .../router/street/ViaAccessEgressRouter.java | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaAccessEgressRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaAccessEgressRouter.java index 17dc06d3e81..47b1c2ee179 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaAccessEgressRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaAccessEgressRouter.java @@ -4,7 +4,6 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Collection; -import java.util.List; import java.util.OptionalInt; import java.util.Set; import org.opentripplanner.astar.spi.TraverseVisitor; @@ -48,10 +47,28 @@ Collection findStreetAccessEgresses( Set ignoreVertices, float maxCarSpeed ) { + var accessEgressRequest = getViaFriendlyRequest(request); + var firstVertices = accessOrEgress.isAccess() + ? linkingContext.findVertices(accessEgressRequest.from()) + : linkingContext.findVertices(accessEgressRequest.to()); + var stopsFromFirstLocation = StreetNearbyStopFinder.of( + stopResolver, + durationLimit, + maxStopCount + ) + .withIgnoreVertices(ignoreVertices) + .withExtensionRequestContexts(extensionRequestContexts) + .build() + .findNearbyStops( + firstVertices, + accessEgressRequest, + streetRequest, + accessOrEgress.isEgress() + ); var lastViaWithCoordinates = lastCoordinateViaLocationIndex(request, accessOrEgress); // There are no coordinate locations to route to/from if (lastViaWithCoordinates.isEmpty()) { - return List.of(); + return stopsFromFirstLocation; } var graphPathFinder = new GraphPathFinder( traverseVisitor, @@ -63,13 +80,13 @@ Collection findStreetAccessEgresses( accessOrEgress, lastViaWithCoordinates.getAsInt() ); - var accessEgressRequest = getViaFriendlyRequest(request); return accessOrEgress.isAccess() ? findStreetAccesses( accessEgressRequest, directRequest, streetRequest, extensionRequestContexts, + stopsFromFirstLocation, durationLimit, maxStopCount, linkingContext, @@ -81,6 +98,7 @@ Collection findStreetAccessEgresses( directRequest, streetRequest, extensionRequestContexts, + stopsFromFirstLocation, durationLimit, maxStopCount, linkingContext, @@ -94,19 +112,14 @@ private Collection findStreetAccesses( RouteRequest directRequest, StreetRequest streetRequest, Collection extensionRequestContexts, + Collection stopsFromFirstLocation, Duration durationLimit, int maxStopCount, LinkingContext linkingContext, Set ignoreVertices, GraphPathFinder graphPathFinder ) { - var originVertices = linkingContext.findVertices(accessEgressRequest.from()); - var stopsFromOrigin = StreetNearbyStopFinder.of(stopResolver, durationLimit, maxStopCount) - .withIgnoreVertices(ignoreVertices) - .withExtensionRequestContexts(extensionRequestContexts) - .build() - .findNearbyStops(originVertices, accessEgressRequest, streetRequest, false); - var nearbyStops = new ArrayList<>(stopsFromOrigin); + var nearbyStops = new ArrayList<>(stopsFromFirstLocation); var paths = directRouter.findDepartAfterPaths( linkingContext, graphPathFinder, @@ -156,19 +169,14 @@ private Collection findStreetEgresses( RouteRequest directRequest, StreetRequest streetRequest, Collection extensionRequestContexts, + Collection stopsFromFirstLocation, Duration durationLimit, int maxStopCount, LinkingContext linkingContext, Set ignoreVertices, GraphPathFinder graphPathFinder ) { - var originVertices = linkingContext.findVertices(accessEgressRequest.to()); - var stopsFromOrigin = StreetNearbyStopFinder.of(stopResolver, durationLimit, maxStopCount) - .withIgnoreVertices(ignoreVertices) - .withExtensionRequestContexts(extensionRequestContexts) - .build() - .findNearbyStops(originVertices, accessEgressRequest, streetRequest, true); - var nearbyStops = new ArrayList<>(stopsFromOrigin); + var nearbyStops = new ArrayList<>(stopsFromFirstLocation); var paths = directRouter.findArriveByPaths( linkingContext, graphPathFinder, From 8ca066a139f37d90f0ac72e56193c2c949bf434f Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Sat, 13 Dec 2025 01:19:49 +0200 Subject: [PATCH 15/15] Fix order of egress legs --- .../router/street/ViaAccessEgressRouter.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaAccessEgressRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaAccessEgressRouter.java index 47b1c2ee179..1be47adb328 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaAccessEgressRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/ViaAccessEgressRouter.java @@ -177,13 +177,9 @@ private Collection findStreetEgresses( GraphPathFinder graphPathFinder ) { var nearbyStops = new ArrayList<>(stopsFromFirstLocation); - var paths = directRouter.findArriveByPaths( - linkingContext, - graphPathFinder, - directRequest, - true, - true - ); + var paths = directRouter + .findArriveByPaths(linkingContext, graphPathFinder, directRequest, true, true) + .reversed(); var vias = accessEgressRequest.listViaLocationsWithCoordinates(); var durationLeft = durationLimit; var i = 0;