From 5129f9f11de73466c924d4bf06be4866f47e08a5 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Wed, 5 Nov 2025 13:39:17 +0100 Subject: [PATCH 01/22] refactor: Enable injecting stops in GraphRoutingTest --- .../module/DirectTransferGeneratorTest.java | 12 +++++- .../routing/algorithm/GraphRoutingTest.java | 42 +++++++++---------- .../router/street/AccessEgressRouterTest.java | 12 ++---- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java index 171a9eecc5f..0ade012279a 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java @@ -37,6 +37,7 @@ import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; import org.opentripplanner.utils.tostring.ToStringBuilder; @@ -631,7 +632,12 @@ public void build() { builder.withTransfersNotAllowed(withNoTransfersOnStations) ); - S0 = stop("S0", 47.495, 19.001, station, TransitMode.RAIL); + S0 = stop("S0", b -> + b + .withCoordinate(47.495, 19.001) + .withParentStation(station) + .withVehicleType(TransitMode.RAIL) + ); S11 = stop("S11", 47.500, 19.001, station); S12 = stop("S12", 47.520, 19.001, station); S13 = stop("S13", 47.540, 19.001, station); @@ -760,6 +766,10 @@ public void build() { ); } } + + private TransitStopVertex stop(String id, double lat, double lon, Station parentStation) { + return stop(id, b -> b.withCoordinate(lat, lon).withParentStation(parentStation)); + } } ); } diff --git a/application/src/test/java/org/opentripplanner/routing/algorithm/GraphRoutingTest.java b/application/src/test/java/org/opentripplanner/routing/algorithm/GraphRoutingTest.java index 514fa3c447b..cb1a180ffe2 100644 --- a/application/src/test/java/org/opentripplanner/routing/algorithm/GraphRoutingTest.java +++ b/application/src/test/java/org/opentripplanner/routing/algorithm/GraphRoutingTest.java @@ -61,6 +61,7 @@ import org.opentripplanner.transit.model.site.Entrance; import org.opentripplanner.transit.model.site.PathwayMode; import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.RegularStopBuilder; import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.site.StationBuilder; import org.opentripplanner.transit.service.SiteRepository; @@ -244,16 +245,23 @@ RegularStop stopEntity( @Nullable Station parentStation, @Nullable TransitMode vehicleType ) { + return stopEntity(id, b -> { + b.withCoordinate(latitude, longitude); + if (parentStation != null) { + b.withParentStation(parentStation); + } + if (vehicleType != null) { + b.withVehicleType(vehicleType); + } + }); + } + + RegularStop stopEntity(String id, Consumer body) { var siteRepositoryBuilder = timetableRepository.getSiteRepository().withContext(); var testModel = new TimetableRepositoryForTest(siteRepositoryBuilder); - var stopBuilder = testModel.stop(id).withCoordinate(latitude, longitude); - if (parentStation != null) { - stopBuilder.withParentStation(parentStation); - } - if (vehicleType != null) { - stopBuilder.withVehicleType(vehicleType); - } + var stopBuilder = testModel.stop(id); + body.accept(stopBuilder); var stop = stopBuilder.build(); timetableRepository.mergeSiteRepositories( @@ -274,28 +282,20 @@ public Station stationEntity(String id, Consumer stationBuilder) return station; } - public TransitStopVertex stop(String id, WgsCoordinate coordinate, Station parentStation) { - return stop(id, coordinate.latitude(), coordinate.longitude(), parentStation); - } - public TransitStopVertex stop(String id, WgsCoordinate coordinate) { - return stop(id, coordinate, null); + return stop(id, b -> b.withCoordinate(coordinate)); } public TransitStopVertex stop(String id, double latitude, double longitude) { - return stop(id, latitude, longitude, null); + return stop(id, b -> b.withCoordinate(latitude, longitude)); } - public TransitStopVertex stop( - String id, - double latitude, - double longitude, - @Nullable Station parentStation - ) { - return stop(id, latitude, longitude, parentStation, null); + public TransitStopVertex stop(String id, Consumer body) { + var stop = stopEntity(id, body); + return vertexFactory.transitStop(ofStop(stop)); } - public TransitStopVertex stop( + public TransitStopVertex xstop( String id, double latitude, double longitude, 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 0513bcd3a3a..97fbe7f68de 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 @@ -65,17 +65,13 @@ public void build() { ); // StopForCentroidRoutingStation is a child of centroidRoutingStation - stopForCentroidRoutingStation = stop( - "StopForCentroidRoutingStation", - B.toWgsCoordinate(), - centroidRoutingStation + stopForCentroidRoutingStation = stop("StopForCentroidRoutingStation", b -> + b.withCoordinate(B.toWgsCoordinate()).withParentStation(centroidRoutingStation) ); // StopForNoCentroidRoutingStation is a child of noCentroidRoutingStation - stopForNoCentroidRoutingStation = stop( - "StopForNoCentroidRoutingStation", - C.toWgsCoordinate(), - noCentroidRoutingStation + stopForNoCentroidRoutingStation = stop("StopForNoCentroidRoutingStation", b -> + b.withCoordinate(C.toWgsCoordinate()).withParentStation(noCentroidRoutingStation) ); biLink(A, centroidRoutingStationVertex); From c71079ea86242583f41b51ef8cb64a23d99a1e00 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Wed, 5 Nov 2025 14:49:18 +0100 Subject: [PATCH 02/22] refactor: Split DirectTransferGeneratorTest, extract CAR cases --- .../DirectTransferGeneratorCarTest.java | 608 ++++++++++++++++++ .../module/DirectTransferGeneratorTest.java | 383 +---------- .../routing/algorithm/GraphRoutingTest.java | 11 - 3 files changed, 629 insertions(+), 373 deletions(-) create mode 100644 application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorCarTest.java diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorCarTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorCarTest.java new file mode 100644 index 00000000000..427ed52655d --- /dev/null +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorCarTest.java @@ -0,0 +1,608 @@ +package org.opentripplanner.graph_builder.module; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.opentripplanner.TestOtpModel; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.model.PathTransfer; +import org.opentripplanner.model.StopTime; +import org.opentripplanner.routing.algorithm.GraphRoutingTest; +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.graphfinder.StopResolver; +import org.opentripplanner.street.model.StreetTraversalPermission; +import org.opentripplanner.street.model.edge.Edge; +import org.opentripplanner.street.model.vertex.StreetVertex; +import org.opentripplanner.street.model.vertex.TransitStopVertex; +import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.network.BikeAccess; +import org.opentripplanner.transit.model.network.CarAccess; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.network.StopPattern; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.Station; +import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.utils.tostring.ToStringBuilder; + +/** + * This creates a graph with trip patterns +
+  S0 -  V0 ------------
+        |     \       |
+ S11 - V11 --------> V21 - S21
+        |      \      |
+ S12 - V12 --------> V22 - V22
+        |             |
+ S13 - V13 --------> V23 - V23
+ 
+ */ +class DirectTransferGeneratorCarTest extends GraphRoutingTest { + + private static final Duration MAX_TRANSFER_DURATION = Duration.ofHours(1); + private static final RouteRequest REQUEST_WITH_WALK_TRANSFER = RouteRequest.defaultValue(); + private static final RouteRequest REQUEST_WITH_BIKE_TRANSFER = RouteRequest.of() + .withJourney(jb -> jb.withTransfer(new StreetRequest(StreetMode.BIKE))) + .buildDefault(); + private static final RouteRequest REQUEST_WITH_CAR_TRANSFER = RouteRequest.of() + .withJourney(jb -> jb.withTransfer(new StreetRequest(StreetMode.CAR))) + .buildDefault(); + + private TransitStopVertex S0, S11, S12, S13, S21, S22, S23; + private StreetVertex V0, V11, V12, V13, V21, V22, V23; + + @Test + public void testRequestWithCarsAllowedPatterns() { + var transferRequests = List.of(REQUEST_WITH_CAR_TRANSFER); + + var otpModel = model(false); + var graph = otpModel.graph(); + graph.hasStreets = true; + var timetableRepository = otpModel.timetableRepository(); + + var transferParametersBuilder = new TransferParameters.Builder(); + transferParametersBuilder.withCarsAllowedStopMaxTransferDuration(Duration.ofMinutes(60)); + transferParametersBuilder.withDisableDefaultTransfers(true); + Map transferParametersForMode = new HashMap<>(); + transferParametersForMode.put(StreetMode.CAR, transferParametersBuilder.build()); + + new DirectTransferGenerator( + graph, + timetableRepository, + DataImportIssueStore.NOOP, + MAX_TRANSFER_DURATION, + transferRequests, + transferParametersForMode + ).buildGraph(); + + StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; + assertTransfers( + timetableRepository.getAllPathTransfers(), + tr(resolver, S0, 100, List.of(V0, V11), S11), + tr(resolver, S0, 200, List.of(V0, V12), S12) + ); + } + + @Test + public void testRequestWithCarsAllowedPatternsWithDurationLimit() { + var transferRequests = List.of(REQUEST_WITH_CAR_TRANSFER); + + var otpModel = model(false); + var graph = otpModel.graph(); + graph.hasStreets = true; + var timetableRepository = otpModel.timetableRepository(); + + TransferParameters.Builder transferParametersBuilder = new TransferParameters.Builder(); + transferParametersBuilder.withCarsAllowedStopMaxTransferDuration(Duration.ofSeconds(10)); + transferParametersBuilder.withDisableDefaultTransfers(true); + Map transferParametersForMode = new HashMap<>(); + transferParametersForMode.put(StreetMode.CAR, transferParametersBuilder.build()); + + new DirectTransferGenerator( + graph, + timetableRepository, + DataImportIssueStore.NOOP, + MAX_TRANSFER_DURATION, + transferRequests, + transferParametersForMode + ).buildGraph(); + + StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; + assertTransfers( + timetableRepository.getAllPathTransfers(), + tr(resolver, S0, 100, List.of(V0, V11), S11) + ); + } + + @Test + public void testMultipleRequestsWithPatternsAndWithCarsAllowedPatterns() { + var transferRequests = List.of( + REQUEST_WITH_WALK_TRANSFER, + REQUEST_WITH_BIKE_TRANSFER, + REQUEST_WITH_CAR_TRANSFER + ); + + var otpModel = model(true); + var graph = otpModel.graph(); + graph.hasStreets = true; + var timetableRepository = otpModel.timetableRepository(); + + TransferParameters.Builder transferParametersBuilder = new TransferParameters.Builder(); + transferParametersBuilder.withCarsAllowedStopMaxTransferDuration(Duration.ofMinutes(60)); + transferParametersBuilder.withDisableDefaultTransfers(true); + Map transferParametersForMode = new HashMap<>(); + transferParametersForMode.put(StreetMode.CAR, transferParametersBuilder.build()); + + new DirectTransferGenerator( + graph, + timetableRepository, + DataImportIssueStore.NOOP, + MAX_TRANSFER_DURATION, + transferRequests, + transferParametersForMode + ).buildGraph(); + + var walkTransfers = timetableRepository.findTransfers(StreetMode.WALK); + var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); + var carTransfers = timetableRepository.findTransfers(StreetMode.CAR); + + StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; + assertTransfers( + walkTransfers, + tr(resolver, S0, 100, List.of(V0, V11), S11), + tr(resolver, S0, 100, List.of(V0, V21), S21), + tr(resolver, S11, 100, List.of(V11, V21), S21), + tr(resolver, S0, 200, List.of(V0, V12), S12), + tr(resolver, S11, 100, List.of(V11, V12), S12) + ); + assertTransfers( + bikeTransfers, + tr(resolver, S0, 100, List.of(V0, V11), S11), + tr(resolver, S0, 100, List.of(V0, V21), S21), + tr(resolver, S11, 110, List.of(V11, V22), S22), + tr(resolver, S0, 200, List.of(V0, V12), S12), + tr(resolver, S11, 100, List.of(V11, V12), S12) + ); + assertTransfers( + carTransfers, + tr(resolver, S0, 100, List.of(V0, V11), S11), + tr(resolver, S0, 200, List.of(V0, V12), S12), + tr(resolver, S0, 100, List.of(V0, V21), S21) + ); + } + + @Test + public void testBikeRequestWithPatternsAndWithCarsAllowedPatterns() { + var transferRequests = List.of(REQUEST_WITH_BIKE_TRANSFER); + + var otpModel = model(true); + var graph = otpModel.graph(); + graph.hasStreets = true; + var timetableRepository = otpModel.timetableRepository(); + + TransferParameters.Builder transferParametersBuilder = new TransferParameters.Builder(); + transferParametersBuilder.withCarsAllowedStopMaxTransferDuration(Duration.ofMinutes(120)); + Map transferParametersForMode = new HashMap<>(); + transferParametersForMode.put(StreetMode.BIKE, transferParametersBuilder.build()); + + new DirectTransferGenerator( + graph, + timetableRepository, + DataImportIssueStore.NOOP, + Duration.ofSeconds(30), + transferRequests, + transferParametersForMode + ).buildGraph(); + + StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; + assertTransfers( + timetableRepository.getAllPathTransfers(), + tr(resolver, S0, 100, List.of(V0, V11), S11), + tr(resolver, S0, 100, List.of(V0, V21), S21), + tr(resolver, S0, 200, List.of(V0, V12), S12), + tr(resolver, S11, 110, List.of(V11, V22), S22), + tr(resolver, S11, 100, List.of(V11, V12), S12) + ); + } + + @Test + public void testBikeRequestWithPatternsAndWithCarsAllowedPatternsWithoutCarInTransferRequests() { + var transferRequests = List.of(REQUEST_WITH_BIKE_TRANSFER); + + var otpModel = model(true); + var graph = otpModel.graph(); + graph.hasStreets = true; + var timetableRepository = otpModel.timetableRepository(); + + new DirectTransferGenerator( + graph, + timetableRepository, + DataImportIssueStore.NOOP, + Duration.ofSeconds(30), + transferRequests + ).buildGraph(); + + StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; + assertTransfers( + timetableRepository.getAllPathTransfers(), + tr(resolver, S0, 100, List.of(V0, V11), S11), + tr(resolver, S0, 100, List.of(V0, V21), S21), + tr(resolver, S11, 110, List.of(V11, V22), S22) + ); + } + + @Test + public void testDisableDefaultTransfersForMode() { + var transferRequests = List.of( + REQUEST_WITH_WALK_TRANSFER, + REQUEST_WITH_BIKE_TRANSFER, + REQUEST_WITH_CAR_TRANSFER + ); + + var otpModel = model(true); + var graph = otpModel.graph(); + graph.hasStreets = true; + var timetableRepository = otpModel.timetableRepository(); + + TransferParameters.Builder transferParametersBuilderBike = new TransferParameters.Builder(); + transferParametersBuilderBike.withDisableDefaultTransfers(true); + TransferParameters.Builder transferParametersBuilderCar = new TransferParameters.Builder(); + transferParametersBuilderCar.withDisableDefaultTransfers(true); + Map transferParametersForMode = new HashMap<>(); + transferParametersForMode.put(StreetMode.BIKE, transferParametersBuilderBike.build()); + transferParametersForMode.put(StreetMode.CAR, transferParametersBuilderCar.build()); + + new DirectTransferGenerator( + graph, + timetableRepository, + DataImportIssueStore.NOOP, + MAX_TRANSFER_DURATION, + transferRequests, + transferParametersForMode + ).buildGraph(); + + var walkTransfers = timetableRepository.findTransfers(StreetMode.WALK); + var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); + var carTransfers = timetableRepository.findTransfers(StreetMode.CAR); + + StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; + assertTransfers( + walkTransfers, + tr(resolver, S0, 100, List.of(V0, V11), S11), + tr(resolver, S0, 100, List.of(V0, V21), S21), + tr(resolver, S11, 100, List.of(V11, V21), S21), + tr(resolver, S0, 200, List.of(V0, V12), S12), + tr(resolver, S11, 100, List.of(V11, V12), S12) + ); + assertTransfers(bikeTransfers); + assertTransfers(carTransfers); + } + + @Test + public void testMaxTransferDurationForMode() { + var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER, REQUEST_WITH_BIKE_TRANSFER); + + var otpModel = model(true); + var graph = otpModel.graph(); + graph.hasStreets = true; + var timetableRepository = otpModel.timetableRepository(); + + TransferParameters.Builder transferParametersBuilderWalk = new TransferParameters.Builder(); + transferParametersBuilderWalk.withMaxTransferDuration(Duration.ofSeconds(100)); + TransferParameters.Builder transferParametersBuilderBike = new TransferParameters.Builder(); + transferParametersBuilderBike.withMaxTransferDuration(Duration.ofSeconds(21)); + Map transferParametersForMode = new HashMap<>(); + transferParametersForMode.put(StreetMode.WALK, transferParametersBuilderWalk.build()); + transferParametersForMode.put(StreetMode.BIKE, transferParametersBuilderBike.build()); + + new DirectTransferGenerator( + graph, + timetableRepository, + DataImportIssueStore.NOOP, + MAX_TRANSFER_DURATION, + transferRequests, + transferParametersForMode + ).buildGraph(); + + var walkTransfers = timetableRepository.findTransfers(StreetMode.WALK); + var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); + var carTransfers = timetableRepository.findTransfers(StreetMode.CAR); + + StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; + assertTransfers( + walkTransfers, + tr(resolver, S0, 100, List.of(V0, V11), S11), + tr(resolver, S0, 100, List.of(V0, V21), S21), + tr(resolver, S11, 100, List.of(V11, V21), S21), + tr(resolver, S11, 100, List.of(V11, V12), S12) + ); + assertTransfers( + bikeTransfers, + tr(resolver, S0, 100, List.of(V0, V11), S11), + tr(resolver, S0, 100, List.of(V0, V21), S21) + ); + assertTransfers(carTransfers); + } + + private TestOtpModel model(boolean addPatterns) { + return modelOf( + new Builder() { + @Override + public void build() { + var station = stationEntity("1", s -> {}); + + S0 = stop("S0", b -> + b + .withCoordinate(47.495, 19.001) + .withParentStation(station) + .withVehicleType(TransitMode.RAIL) + ); + S11 = stop("S11", 47.500, 19.001, station); + S12 = stop("S12", 47.520, 19.001, station); + S13 = stop("S13", 47.540, 19.001, station); + S21 = stop("S21", 47.500, 19.011, station); + S22 = stop("S22", 47.520, 19.011, station); + S23 = stop("S23", 47.540, 19.011, station); + + V0 = intersection("V0", 47.495, 19.000); + V11 = intersection("V11", 47.500, 19.000); + V12 = intersection("V12", 47.510, 19.000); + V13 = intersection("V13", 47.520, 19.000); + V21 = intersection("V21", 47.500, 19.010); + V22 = intersection("V22", 47.510, 19.010); + V23 = intersection("V23", 47.520, 19.010); + + biLink(V0, S0); + biLink(V11, S11); + biLink(V12, S12); + biLink(V13, S13); + biLink(V21, S21); + biLink(V22, S22); + biLink(V23, S23); + + street(V0, V11, 100, StreetTraversalPermission.ALL); + street(V0, V12, 200, StreetTraversalPermission.ALL); + street(V0, V21, 100, StreetTraversalPermission.ALL); + street(V0, V22, 200, StreetTraversalPermission.ALL); + + street(V11, V12, 100, StreetTraversalPermission.PEDESTRIAN); + street(V12, V13, 100, StreetTraversalPermission.PEDESTRIAN); + street(V21, V22, 100, StreetTraversalPermission.PEDESTRIAN); + street(V22, V23, 100, StreetTraversalPermission.PEDESTRIAN); + street(V11, V21, 100, StreetTraversalPermission.PEDESTRIAN); + street(V11, V22, 110, StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE); + + if (addPatterns) { + var agency = TimetableRepositoryForTest.agency("Agency"); + + tripPattern( + TripPattern.of(TimetableRepositoryForTest.id("TP1")) + .withRoute(route("R1", TransitMode.BUS, agency)) + .withStopPattern(new StopPattern(List.of(st(S11, true, true), st(S12), st(S13)))) + .build() + ); + + tripPattern( + createTripPattern( + "TP2", + route("R2", TransitMode.BUS, agency), + List.of(st(S21), st(S22), st(S23)), + createBikesAllowedTrip(), + "00:00 01:00 02:00" + ) + ); + } + + var agency = TimetableRepositoryForTest.agency("FerryAgency"); + + tripPattern( + createTripPattern( + "TP3", + route("R3", TransitMode.FERRY, agency), + List.of(st(S11), st(S21)), + createCarsAllowedTrip(), + "00:00 01:00" + ) + ); + + tripPattern( + createTripPattern( + "TP4", + route("R4", TransitMode.FERRY, agency), + List.of(st(S0), st(S13)), + createCarsAllowedTrip(), + "00:00 01:00" + ) + ); + + tripPattern( + createTripPattern( + "TP5", + route("R5", TransitMode.FERRY, agency), + List.of(st(S12), st(S22)), + createCarsAllowedTrip(), + "00:00 01:00" + ) + ); + } + + private TransitStopVertex stop(String id, double lat, double lon, Station parentStation) { + return stop(id, b -> b.withCoordinate(lat, lon).withParentStation(parentStation)); + } + } + ); + } + + private static TripPattern createTripPattern( + String id, + Route route, + List times, + Trip trip, + String schedule + ) { + return TripPattern.of(TimetableRepositoryForTest.id(id)) + .withRoute(route) + .withStopPattern(new StopPattern(times)) + .withScheduledTimeTableBuilder(builder -> + builder.addTripTimes( + ScheduledTripTimes.of().withTrip(trip).withDepartureTimes(schedule).build() + ) + ) + .build(); + } + + private static Trip createBikesAllowedTrip() { + return TimetableRepositoryForTest.trip("bikesAllowedTrip") + .withBikesAllowed(BikeAccess.ALLOWED) + .build(); + } + + private static Trip createCarsAllowedTrip() { + return TimetableRepositoryForTest.trip("carsAllowedTrip") + .withCarsAllowed(CarAccess.ALLOWED) + .build(); + } + + private void assertTransfers( + Collection allPathTransfers, + TransferDescriptor... transfers + ) { + var matchedTransfers = new HashSet(); + var assertions = Stream.concat( + Arrays.stream(transfers).map(td -> td.matcher(allPathTransfers, matchedTransfers)), + Stream.of(allTransfersMatched(allPathTransfers, matchedTransfers)) + ); + + assertAll(assertions); + } + + private Executable allTransfersMatched( + Collection transfersByStop, + Set matchedTransfers + ) { + return () -> { + var missingTransfers = new HashSet<>(transfersByStop); + missingTransfers.removeAll(matchedTransfers); + + assertEquals(Set.of(), missingTransfers, "All transfers matched"); + }; + } + + private TransferDescriptor tr( + StopResolver resolver, + TransitStopVertex from, + double distance, + TransitStopVertex to + ) { + return new TransferDescriptor( + resolver.getStop(from.getId()), + distance, + resolver.getStop(to.getId()) + ); + } + + private TransferDescriptor tr( + StopResolver resolver, + TransitStopVertex from, + double distance, + List vertices, + TransitStopVertex to + ) { + return new TransferDescriptor(resolver, from, distance, vertices, to); + } + + private static class TransferDescriptor { + + private final StopLocation from; + private final StopLocation to; + private final Double distanceMeters; + private final List vertices; + + public TransferDescriptor(RegularStop from, Double distanceMeters, RegularStop to) { + this.from = from; + this.distanceMeters = distanceMeters; + this.vertices = null; + this.to = to; + } + + public TransferDescriptor( + StopResolver resolver, + TransitStopVertex from, + Double distanceMeters, + List vertices, + TransitStopVertex to + ) { + this.from = resolver.getStop(from.getId()); + this.distanceMeters = distanceMeters; + this.vertices = vertices; + this.to = resolver.getStop(to.getId()); + } + + @Override + public String toString() { + return ToStringBuilder.of(getClass()) + .addObj("from", from) + .addObj("to", to) + .addNum("distanceMeters", distanceMeters) + .addCol("vertices", vertices) + .toString(); + } + + boolean matches(PathTransfer transfer) { + if (!Objects.equals(from, transfer.from) || !Objects.equals(to, transfer.to)) { + return false; + } + + if (vertices == null) { + return distanceMeters == transfer.getDistanceMeters() && transfer.getEdges() == null; + } else { + var transferVertices = transfer + .getEdges() + .stream() + .map(Edge::getToVertex) + .filter(StreetVertex.class::isInstance) + .toList(); + + return ( + distanceMeters == transfer.getDistanceMeters() && + Objects.equals(vertices, transferVertices) + ); + } + } + + private Executable matcher( + Collection transfersByStop, + Set matchedTransfers + ) { + return () -> { + var matched = transfersByStop.stream().filter(this::matches).findFirst(); + + if (matched.isPresent()) { + assertTrue(true, "Found transfer for " + this); + matchedTransfers.add(matched.get()); + } else { + fail("Missing transfer for " + this); + } + }; + } + } +} diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java index 0ade012279a..ab19fea76e3 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java @@ -8,7 +8,6 @@ import java.time.Duration; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -33,7 +32,6 @@ import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.network.BikeAccess; -import org.opentripplanner.transit.model.network.CarAccess; import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.RegularStop; @@ -41,6 +39,7 @@ import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; import org.opentripplanner.utils.tostring.ToStringBuilder; +import org.rutebanken.netex.model.BusSubmodeEnumeration; /** * This creates a graph with trip patterns @@ -61,16 +60,13 @@ class DirectTransferGeneratorTest extends GraphRoutingTest { private static final RouteRequest REQUEST_WITH_BIKE_TRANSFER = RouteRequest.of() .withJourney(jb -> jb.withTransfer(new StreetRequest(StreetMode.BIKE))) .buildDefault(); - private static final RouteRequest REQUEST_WITH_CAR_TRANSFER = RouteRequest.of() - .withJourney(jb -> jb.withTransfer(new StreetRequest(StreetMode.CAR))) - .buildDefault(); private TransitStopVertex S0, S11, S12, S13, S21, S22, S23; private StreetVertex V0, V11, V12, V13, V21, V22, V23; @Test public void testDirectTransfersWithoutPatterns() { - var otpModel = model(false); + var otpModel = model(false, false, false); var graph = otpModel.graph(); var timetableRepository = otpModel.timetableRepository(); var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER); @@ -89,7 +85,7 @@ public void testDirectTransfersWithoutPatterns() { @Test public void testDirectTransfersWithPatterns() { - var otpModel = model(true); + var otpModel = model(true, false, false); var graph = otpModel.graph(); graph.hasStreets = false; var timetableRepository = otpModel.timetableRepository(); @@ -122,7 +118,7 @@ public void testDirectTransfersWithPatterns() { @Test public void testDirectTransfersWithRestrictedPatterns() { - var otpModel = model(true, true); + var otpModel = model(true, true, false); var graph = otpModel.graph(); graph.hasStreets = false; var timetableRepository = otpModel.timetableRepository(); @@ -157,7 +153,7 @@ public void testDirectTransfersWithRestrictedPatterns() { public void testSingleRequestWithoutPatterns() { var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER); - var otpModel = model(false); + var otpModel = model(false, false, false); var graph = otpModel.graph(); graph.hasStreets = true; var timetableRepository = otpModel.timetableRepository(); @@ -177,7 +173,7 @@ public void testSingleRequestWithoutPatterns() { public void testSingleRequestWithPatterns() { var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER); - var otpModel = model(true); + var otpModel = model(true, false, false); var graph = otpModel.graph(); graph.hasStreets = true; var timetableRepository = otpModel.timetableRepository(); @@ -203,7 +199,7 @@ public void testSingleRequestWithPatterns() { public void testMultipleRequestsWithoutPatterns() { var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER, REQUEST_WITH_BIKE_TRANSFER); - var otpModel = model(false); + var otpModel = model(false, false, false); var graph = otpModel.graph(); graph.hasStreets = true; var timetableRepository = otpModel.timetableRepository(); @@ -223,7 +219,7 @@ public void testMultipleRequestsWithoutPatterns() { public void testMultipleRequestsWithPatterns() { var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER, REQUEST_WITH_BIKE_TRANSFER); - TestOtpModel model = model(true); + TestOtpModel model = model(true, false, false); var graph = model.graph(); graph.hasStreets = true; var timetableRepository = model.timetableRepository(); @@ -258,7 +254,7 @@ public void testMultipleRequestsWithPatterns() { @Test public void testTransferOnIsolatedStations() { - var otpModel = model(true, false, true, false); + var otpModel = model(true, false, true); var graph = otpModel.graph(); graph.hasStreets = false; @@ -276,166 +272,12 @@ public void testTransferOnIsolatedStations() { assertTrue(timetableRepository.getAllPathTransfers().isEmpty()); } - @Test - public void testRequestWithCarsAllowedPatterns() { - var transferRequests = List.of(REQUEST_WITH_CAR_TRANSFER); - - var otpModel = model(false, false, false, true); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - var transferParametersBuilder = new TransferParameters.Builder(); - transferParametersBuilder.withCarsAllowedStopMaxTransferDuration(Duration.ofMinutes(60)); - transferParametersBuilder.withDisableDefaultTransfers(true); - Map transferParametersForMode = new HashMap<>(); - transferParametersForMode.put(StreetMode.CAR, transferParametersBuilder.build()); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests, - transferParametersForMode - ).buildGraph(); - - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - timetableRepository.getAllPathTransfers(), - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 200, List.of(V0, V12), S12) - ); - } - - @Test - public void testRequestWithCarsAllowedPatternsWithDurationLimit() { - var transferRequests = List.of(REQUEST_WITH_CAR_TRANSFER); - - var otpModel = model(false, false, false, true); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - TransferParameters.Builder transferParametersBuilder = new TransferParameters.Builder(); - transferParametersBuilder.withCarsAllowedStopMaxTransferDuration(Duration.ofSeconds(10)); - transferParametersBuilder.withDisableDefaultTransfers(true); - Map transferParametersForMode = new HashMap<>(); - transferParametersForMode.put(StreetMode.CAR, transferParametersBuilder.build()); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests, - transferParametersForMode - ).buildGraph(); - - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - timetableRepository.getAllPathTransfers(), - tr(resolver, S0, 100, List.of(V0, V11), S11) - ); - } - - @Test - public void testMultipleRequestsWithPatternsAndWithCarsAllowedPatterns() { - var transferRequests = List.of( - REQUEST_WITH_WALK_TRANSFER, - REQUEST_WITH_BIKE_TRANSFER, - REQUEST_WITH_CAR_TRANSFER - ); - - var otpModel = model(true, false, false, true); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - TransferParameters.Builder transferParametersBuilder = new TransferParameters.Builder(); - transferParametersBuilder.withCarsAllowedStopMaxTransferDuration(Duration.ofMinutes(60)); - transferParametersBuilder.withDisableDefaultTransfers(true); - Map transferParametersForMode = new HashMap<>(); - transferParametersForMode.put(StreetMode.CAR, transferParametersBuilder.build()); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests, - transferParametersForMode - ).buildGraph(); - - var walkTransfers = timetableRepository.findTransfers(StreetMode.WALK); - var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); - var carTransfers = timetableRepository.findTransfers(StreetMode.CAR); - - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - walkTransfers, - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S11, 100, List.of(V11, V21), S21), - tr(resolver, S0, 200, List.of(V0, V12), S12), - tr(resolver, S11, 100, List.of(V11, V12), S12) - ); - assertTransfers( - bikeTransfers, - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S11, 110, List.of(V11, V22), S22), - tr(resolver, S0, 200, List.of(V0, V12), S12), - tr(resolver, S11, 100, List.of(V11, V12), S12) - ); - assertTransfers( - carTransfers, - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 200, List.of(V0, V12), S12), - tr(resolver, S0, 100, List.of(V0, V21), S21) - ); - } - - @Test - public void testBikeRequestWithPatternsAndWithCarsAllowedPatterns() { - var transferRequests = List.of(REQUEST_WITH_BIKE_TRANSFER); - - var otpModel = model(true, false, false, true); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - TransferParameters.Builder transferParametersBuilder = new TransferParameters.Builder(); - transferParametersBuilder.withCarsAllowedStopMaxTransferDuration(Duration.ofMinutes(120)); - Map transferParametersForMode = new HashMap<>(); - transferParametersForMode.put(StreetMode.BIKE, transferParametersBuilder.build()); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - Duration.ofSeconds(30), - transferRequests, - transferParametersForMode - ).buildGraph(); - - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - timetableRepository.getAllPathTransfers(), - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S0, 200, List.of(V0, V12), S12), - tr(resolver, S11, 110, List.of(V11, V22), S22), - tr(resolver, S11, 100, List.of(V11, V12), S12) - ); - } - @Test public void testBikeRequestWithBikesAllowedTransfersWithIncludeEmptyRailStopsInTransfersOn() { OTPFeature.IncludeEmptyRailStopsInTransfers.testOn(() -> { var transferRequests = List.of(REQUEST_WITH_BIKE_TRANSFER); - TestOtpModel model = model(true); + TestOtpModel model = model(true, false, false); var graph = model.graph(); graph.hasStreets = true; @@ -462,7 +304,7 @@ public void testBikeRequestWithBikesAllowedTransfersWithConsiderPatternsForDirec OTPFeature.ConsiderPatternsForDirectTransfers.testOff(() -> { var transferRequests = List.of(REQUEST_WITH_BIKE_TRANSFER); - TestOtpModel model = model(true); + TestOtpModel model = model(true, false, false); var graph = model.graph(); graph.hasStreets = true; @@ -491,145 +333,17 @@ public void testBikeRequestWithBikesAllowedTransfersWithConsiderPatternsForDirec }); } - @Test - public void testBikeRequestWithPatternsAndWithCarsAllowedPatternsWithoutCarInTransferRequests() { - var transferRequests = List.of(REQUEST_WITH_BIKE_TRANSFER); - - var otpModel = model(true, false, false, true); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - Duration.ofSeconds(30), - transferRequests - ).buildGraph(); - - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - timetableRepository.getAllPathTransfers(), - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S11, 110, List.of(V11, V22), S22) - ); - } - - @Test - public void testDisableDefaultTransfersForMode() { - var transferRequests = List.of( - REQUEST_WITH_WALK_TRANSFER, - REQUEST_WITH_BIKE_TRANSFER, - REQUEST_WITH_CAR_TRANSFER - ); - - var otpModel = model(true, false, false, true); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - TransferParameters.Builder transferParametersBuilderBike = new TransferParameters.Builder(); - transferParametersBuilderBike.withDisableDefaultTransfers(true); - TransferParameters.Builder transferParametersBuilderCar = new TransferParameters.Builder(); - transferParametersBuilderCar.withDisableDefaultTransfers(true); - Map transferParametersForMode = new HashMap<>(); - transferParametersForMode.put(StreetMode.BIKE, transferParametersBuilderBike.build()); - transferParametersForMode.put(StreetMode.CAR, transferParametersBuilderCar.build()); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests, - transferParametersForMode - ).buildGraph(); - - var walkTransfers = timetableRepository.findTransfers(StreetMode.WALK); - var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); - var carTransfers = timetableRepository.findTransfers(StreetMode.CAR); - - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - walkTransfers, - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S11, 100, List.of(V11, V21), S21), - tr(resolver, S0, 200, List.of(V0, V12), S12), - tr(resolver, S11, 100, List.of(V11, V12), S12) - ); - assertTransfers(bikeTransfers); - assertTransfers(carTransfers); - } - - @Test - public void testMaxTransferDurationForMode() { - var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER, REQUEST_WITH_BIKE_TRANSFER); - - var otpModel = model(true, false, false, true); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - TransferParameters.Builder transferParametersBuilderWalk = new TransferParameters.Builder(); - transferParametersBuilderWalk.withMaxTransferDuration(Duration.ofSeconds(100)); - TransferParameters.Builder transferParametersBuilderBike = new TransferParameters.Builder(); - transferParametersBuilderBike.withMaxTransferDuration(Duration.ofSeconds(21)); - Map transferParametersForMode = new HashMap<>(); - transferParametersForMode.put(StreetMode.WALK, transferParametersBuilderWalk.build()); - transferParametersForMode.put(StreetMode.BIKE, transferParametersBuilderBike.build()); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests, - transferParametersForMode - ).buildGraph(); - - var walkTransfers = timetableRepository.findTransfers(StreetMode.WALK); - var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); - var carTransfers = timetableRepository.findTransfers(StreetMode.CAR); - - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - walkTransfers, - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S11, 100, List.of(V11, V21), S21), - tr(resolver, S11, 100, List.of(V11, V12), S12) - ); - assertTransfers( - bikeTransfers, - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21) - ); - assertTransfers(carTransfers); - } - - private TestOtpModel model(boolean addPatterns) { - return model(addPatterns, false); - } - - private TestOtpModel model(boolean addPatterns, boolean withBoardingConstraint) { - return model(addPatterns, withBoardingConstraint, false, false); - } - private TestOtpModel model( boolean addPatterns, boolean withBoardingConstraint, - boolean withNoTransfersOnStations, - boolean addCarsAllowedPatterns + boolean withNoTransfersOnStations ) { return modelOf( new Builder() { @Override public void build() { - var station = stationEntity("1", builder -> - builder.withTransfersNotAllowed(withNoTransfersOnStations) + var station = stationEntity("1", s -> + s.withTransfersNotAllowed(withNoTransfersOnStations) ); S0 = stop("S0", b -> @@ -642,7 +356,13 @@ public void build() { S12 = stop("S12", 47.520, 19.001, station); S13 = stop("S13", 47.540, 19.001, station); S21 = stop("S21", 47.500, 19.011, station); - S22 = stop("S22", 47.520, 19.011, station); + S22 = stop("S22", b -> + b + .withCoordinate(47.520, 19.011) + .withParentStation(station) + .withVehicleType(TransitMode.BUS) + .withNetexVehicleSubmode(BusSubmodeEnumeration.RAIL_REPLACEMENT_BUS.value()) + ); S23 = stop("S23", 47.540, 19.011, station); V0 = intersection("V0", 47.495, 19.000); @@ -704,67 +424,6 @@ public void build() { .build() ); } - - if (addCarsAllowedPatterns) { - var agency = TimetableRepositoryForTest.agency("FerryAgency"); - - tripPattern( - TripPattern.of(TimetableRepositoryForTest.id("TP3")) - .withRoute(route("R3", TransitMode.FERRY, agency)) - .withStopPattern(new StopPattern(List.of(st(S11), st(S21)))) - .withScheduledTimeTableBuilder(builder -> - builder.addTripTimes( - ScheduledTripTimes.of() - .withTrip( - TimetableRepositoryForTest.trip("carsAllowedTrip") - .withCarsAllowed(CarAccess.ALLOWED) - .build() - ) - .withDepartureTimes("00:00 01:00") - .build() - ) - ) - .build() - ); - - tripPattern( - TripPattern.of(TimetableRepositoryForTest.id("TP4")) - .withRoute(route("R4", TransitMode.FERRY, agency)) - .withStopPattern(new StopPattern(List.of(st(S0), st(S13)))) - .withScheduledTimeTableBuilder(builder -> - builder.addTripTimes( - ScheduledTripTimes.of() - .withTrip( - TimetableRepositoryForTest.trip("carsAllowedTrip") - .withCarsAllowed(CarAccess.ALLOWED) - .build() - ) - .withDepartureTimes("00:00 01:00") - .build() - ) - ) - .build() - ); - - tripPattern( - TripPattern.of(TimetableRepositoryForTest.id("TP5")) - .withRoute(route("R5", TransitMode.FERRY, agency)) - .withStopPattern(new StopPattern(List.of(st(S12), st(S22)))) - .withScheduledTimeTableBuilder(builder -> - builder.addTripTimes( - ScheduledTripTimes.of() - .withTrip( - TimetableRepositoryForTest.trip("carsAllowedTrip") - .withCarsAllowed(CarAccess.ALLOWED) - .build() - ) - .withDepartureTimes("00:00 01:00") - .build() - ) - ) - .build() - ); - } } private TransitStopVertex stop(String id, double lat, double lon, Station parentStation) { diff --git a/application/src/test/java/org/opentripplanner/routing/algorithm/GraphRoutingTest.java b/application/src/test/java/org/opentripplanner/routing/algorithm/GraphRoutingTest.java index cb1a180ffe2..72580fc7a2d 100644 --- a/application/src/test/java/org/opentripplanner/routing/algorithm/GraphRoutingTest.java +++ b/application/src/test/java/org/opentripplanner/routing/algorithm/GraphRoutingTest.java @@ -295,17 +295,6 @@ public TransitStopVertex stop(String id, Consumer body) { return vertexFactory.transitStop(ofStop(stop)); } - public TransitStopVertex xstop( - String id, - double latitude, - double longitude, - @Nullable Station parentStation, - @Nullable TransitMode vehicleType - ) { - var stop = stopEntity(id, latitude, longitude, parentStation, vehicleType); - return vertexFactory.transitStop(ofStop(stop)); - } - public TransitEntranceVertex entrance(String id, double latitude, double longitude) { return new TransitEntranceVertex(entranceEntity(id, latitude, longitude)); } From 35fb6ff9e3cb97d838e7b971c73a50fa9ff4211c Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Wed, 5 Nov 2025 10:46:49 +0100 Subject: [PATCH 03/22] feat: add support for real-time used stops in transfer generation --- .../framework/application/OTPFeature.java | 17 +++++--- .../PatternConsideringNearbyStopFinder.java | 30 +++++++------ .../netex/mapping/QuayMapper.java | 25 ++++++++++- .../transit/model/site/RegularStop.java | 42 +++++++++++++++---- .../model/site/RegularStopBuilder.java | 12 ++++++ .../module/DirectTransferGeneratorTest.java | 2 +- test/performance/norway/otp-config.json | 2 +- 7 files changed, 100 insertions(+), 30 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java b/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java index 79369e7dd29..2c0536e3cd3 100644 --- a/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java +++ b/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java @@ -22,15 +22,20 @@ public enum OTPFeature { ), APIServerInfo(true, false, "Enable the server info endpoint."), APIUpdaterStatus(true, false, "Enable endpoint for graph updaters status."), - IncludeEmptyRailStopsInTransfers( + IncludeStopsUsedRealtimeInTransfers( false, false, """ - Turning this on guarantees that Rail stops without scheduled departures still get included - when generating transfers using `ConsiderPatternsForDirectTransfers`. It is common for stops - to be assign at real-time for Rail. Turning this on will help to avoid dropping transfers which - are needed, when the stop is in use later. Turning this on, if - ConsiderPatternsForDirectTransfers is off has no effect. + When generating transfers, stops without any patterns are excluded to improve performance if + `ConsiderPatternsForDirectTransfers` is enabled. However, some stops are only used by trips + changed or added by real-time updates. Since transfer generation happens before real-time + updates are applied, OTP cannot know which stops will be needed. Instead, OTP will attempt to + identify stops likely to be used by real-time updates at import time. Common cases include rail + stops (which often have late platform assignments) and stops reserved for replacement services + (which can be detected in NeTEx by examining the stop sub-mode). This feature has no effect if + `ConsiderPatternsForDirectTransfers` is disabled. + + This feature is only supported for NeTEx feeds, not for GTFS feeds. """ ), ConsiderPatternsForDirectTransfers( diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java index 8df2a25b64e..f62c3a9d47a 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java @@ -1,6 +1,5 @@ package org.opentripplanner.graph_builder.module.nearbystops; -import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -61,23 +60,16 @@ public List findNearbyStops( StopLocation ts1 = nearbyStop.stop; if (ts1 instanceof RegularStop regularStop) { - /* Consider this destination stop as a candidate for every trip pattern passing through it. */ - Collection patternsForStop = transitService.findPatterns(ts1); + var patternsForStop = findPatternsForStop(regularStop, reverseDirection); - if (OTPFeature.IncludeEmptyRailStopsInTransfers.isOn()) { - if (patternsForStop.isEmpty() && regularStop.isRailStop()) { + if (OTPFeature.IncludeStopsUsedRealtimeInTransfers.isOn()) { + if (patternsForStop.isEmpty() && regularStop.isSometimesUsedRealtime()) { uniqueStopsResult.add(nearbyStop); } } - for (TripPattern pattern : patternsForStop) { - if ( - reverseDirection - ? pattern.canAlight(nearbyStop.stop) - : pattern.canBoard(nearbyStop.stop) - ) { - closestStopForPattern.putMin(pattern, nearbyStop); - } + for (var pattern : patternsForStop) { + closestStopForPattern.putMin(pattern, nearbyStop); } } @@ -100,4 +92,16 @@ public List findNearbyStops( // TODO: don't convert to list return uniqueStopsResult.stream().toList(); } + + /** + * Find all candidate patterns for the given destination {@code stop}. Only return patterns + * where we can board(forward direction) or alight(reverse direction) at the given stop. + */ + private List findPatternsForStop(RegularStop stop, boolean reverseDirection) { + return transitService + .findPatterns(stop) + .stream() + .filter(reverseDirection ? p -> p.canAlight(stop) : p -> p.canBoard(stop)) + .toList(); + } } diff --git a/application/src/main/java/org/opentripplanner/netex/mapping/QuayMapper.java b/application/src/main/java/org/opentripplanner/netex/mapping/QuayMapper.java index fdf2d73768a..22eff398616 100644 --- a/application/src/main/java/org/opentripplanner/netex/mapping/QuayMapper.java +++ b/application/src/main/java/org/opentripplanner/netex/mapping/QuayMapper.java @@ -1,7 +1,10 @@ package org.opentripplanner.netex.mapping; +import static org.opentripplanner.transit.model.basic.TransitMode.RAIL; + import java.util.Collection; import javax.annotation.Nullable; +import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.netex.mapping.support.FeedScopedIdFactory; @@ -12,11 +15,15 @@ import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.service.SiteRepositoryBuilder; +import org.rutebanken.netex.model.BusSubmodeEnumeration; import org.rutebanken.netex.model.MultilingualString; import org.rutebanken.netex.model.Quay; class QuayMapper { + private static final String RAIL_REPLACEMENT_BUS_VALUE = + BusSubmodeEnumeration.RAIL_REPLACEMENT_BUS.value(); + private final DataImportIssueStore issueStore; private final FeedScopedIdFactory idFactory; @@ -69,6 +76,21 @@ private RegularStop map( return null; } + String subMode = transitMode.subMode(); + boolean sometimesUsedRealtime = false; + + if (OTPFeature.IncludeStopsUsedRealtimeInTransfers.isOn()) { + if (transitMode.mainMode() == RAIL) { + sometimesUsedRealtime = true; + } + // We only consider rail and rail-replacement-bus stops when generating transfers. The reason + // we do not include all stops is due to perfomance reasons. This should be replaced by + // generating transfers as needed for realtime updates. + else if (subMode != null && subMode.equals(RAIL_REPLACEMENT_BUS_VALUE)) { + sometimesUsedRealtime = true; + } + } + var builder = siteRepositoryBuilder .regularStop(id) .withParentStation(parentStation) @@ -80,7 +102,8 @@ private RegularStop map( .withCoordinate(WgsCoordinateMapper.mapToDomain(quay.getCentroid())) .withWheelchairAccessibility(wheelchair) .withVehicleType(transitMode.mainMode()) - .withNetexVehicleSubmode(transitMode.subMode()); + .withNetexVehicleSubmode(subMode) + .withSometimesUsedRealtime(sometimesUsedRealtime); builder.fareZones().addAll(fareZones); diff --git a/application/src/main/java/org/opentripplanner/transit/model/site/RegularStop.java b/application/src/main/java/org/opentripplanner/transit/model/site/RegularStop.java index 8dd581b5699..c40c750e2dc 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/site/RegularStop.java +++ b/application/src/main/java/org/opentripplanner/transit/model/site/RegularStop.java @@ -33,6 +33,8 @@ public final class RegularStop private final SubMode netexVehicleSubmode; + private final boolean sometimesUsedRealtime; + private final Set boardingAreas; private final Set fareZones; @@ -45,6 +47,7 @@ public final class RegularStop this.timeZone = builder.timeZone(); this.vehicleType = builder.vehicleType(); this.netexVehicleSubmode = SubMode.getOrBuildAndCacheForever(builder.netexVehicleSubmode()); + this.sometimesUsedRealtime = builder.isSometimesUsedRealtime(); this.boardingAreas = setOfNullSafe(builder.boardingAreas()); this.fareZones = setOfNullSafe(builder.fareZones()); if (isPartOfStation()) { @@ -106,18 +109,40 @@ public TransitMode getVehicleType() { return vehicleType; } - /** - * Return {@code true} if the vehicle type is set in the import to be RAIL. Note! This does - * not check patterns visiting the stop. - */ - public boolean isRailStop() { - return vehicleType == TransitMode.RAIL; - } - public SubMode getNetexVehicleSubmode() { return netexVehicleSubmode; } + /** + * Indicates whether this stop might be used by real-time updated trips, even though it is NOT + * used by regular scheduled trips. OTP sometimes filters out unused stops during graph build + * or as a performance optimization. If this happens before real-time updates are applied, then + * the routing for these stops will not work. For example this is the case with transfers + * generation. + *

+ * Common use cases: + *

    + *
  • Rail platform assignment: Scheduled trips may reference a limited set of platforms, + * while real-time updates assign trips to all available platforms. This is common when the + * actual platform is assigned AFTER the trips are planned.
  • + *
  • Rail Replacement Bus Services: Some stops are reserved for replacement services that are + * added via real-time updates rather than scheduled in advance.
  • + *
+ *

+ * FOR INTERNAL USE ONLY + *

+ * DO NOT EXPOSE THIS PARAMETER ON ANY API. Business logic using this feature should only use it + * to improve routing by including these stops when stops with no trip patterns would otherwise be + * excluded for performance reasons. Incorrectly tagging stops with this flag is not critical, it + * will only degrade perfomance. + * + * @return {@code true} if this stop may be used by real-time trips despite having no scheduled + * patterns, {@code false} otherwise + */ + public boolean isSometimesUsedRealtime() { + return sometimesUsedRealtime; + } + @Override public Point getGeometry() { return GeometryUtils.getGeometryFactory().createPoint(getCoordinate().asJtsCoordinate()); @@ -158,6 +183,7 @@ public boolean sameAs(RegularStop other) { Objects.equals(timeZone, other.timeZone) && Objects.equals(vehicleType, other.vehicleType) && Objects.equals(netexVehicleSubmode, other.netexVehicleSubmode) && + Objects.equals(sometimesUsedRealtime, other.sometimesUsedRealtime) && Objects.equals(boardingAreas, other.boardingAreas) && Objects.equals(fareZones, other.fareZones) ); diff --git a/application/src/main/java/org/opentripplanner/transit/model/site/RegularStopBuilder.java b/application/src/main/java/org/opentripplanner/transit/model/site/RegularStopBuilder.java index 5e6cd01618b..01a0ae85c74 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/site/RegularStopBuilder.java +++ b/application/src/main/java/org/opentripplanner/transit/model/site/RegularStopBuilder.java @@ -30,6 +30,8 @@ public final class RegularStopBuilder private String netexVehicleSubmode; + private boolean sometimesUsedRealtime = false; + private final Set boardingAreas = new HashSet<>(); private final Set fareZones = new HashSet<>(); @@ -47,6 +49,7 @@ public final class RegularStopBuilder this.timeZone = original.getTimeZone(); this.vehicleType = original.getVehicleType(); this.netexVehicleSubmode = original.getNetexVehicleSubmode().name(); + this.sometimesUsedRealtime = original.isSometimesUsedRealtime(); } public String platformCode() { @@ -85,6 +88,15 @@ public RegularStopBuilder withNetexVehicleSubmode(String netexVehicleSubmode) { return this; } + public boolean isSometimesUsedRealtime() { + return sometimesUsedRealtime; + } + + public RegularStopBuilder withSometimesUsedRealtime(boolean sometimesUsedRealtime) { + this.sometimesUsedRealtime = sometimesUsedRealtime; + return this; + } + public ZoneId timeZone() { return timeZone; } diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java index ab19fea76e3..bb413e64a47 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java @@ -274,7 +274,7 @@ public void testTransferOnIsolatedStations() { @Test public void testBikeRequestWithBikesAllowedTransfersWithIncludeEmptyRailStopsInTransfersOn() { - OTPFeature.IncludeEmptyRailStopsInTransfers.testOn(() -> { + OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { var transferRequests = List.of(REQUEST_WITH_BIKE_TRANSFER); TestOtpModel model = model(true, false, false); diff --git a/test/performance/norway/otp-config.json b/test/performance/norway/otp-config.json index cee2e259f1b..8a26a4e6046 100644 --- a/test/performance/norway/otp-config.json +++ b/test/performance/norway/otp-config.json @@ -2,7 +2,7 @@ "otpFeatures" : { "FlexRouting" : true, "FloatingBike" : true, - "IncludeEmptyRailStopsInTransfers" : true, + "IncludeStopsUsedRealtimeInTransfers" : true, "MultiCriteriaGroupMaxFilter" : true, "OptimizeTransfers" : true, "ParallelRouting" : false, From b27bac7a659fa976b3a193ebe3e5c489ff9d8e28 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Sun, 9 Nov 2025 01:52:06 +0100 Subject: [PATCH 04/22] refactor: Improve assertions in DirectTransferGeneratorTest --- .../module/DirectTransferGeneratorTest.java | 146 ++++++++++-------- doc/user/Configuration.md | 72 ++++----- 2 files changed, 116 insertions(+), 102 deletions(-) diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java index bb413e64a47..0ad53aabe94 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java @@ -13,6 +13,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; @@ -39,7 +40,6 @@ import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; import org.opentripplanner.utils.tostring.ToStringBuilder; -import org.rutebanken.netex.model.BusSubmodeEnumeration; /** * This creates a graph with trip patterns @@ -80,7 +80,7 @@ public void testDirectTransfersWithoutPatterns() { transferRequests ).buildGraph(); - assertTransfers(timetableRepository.getAllPathTransfers()); + assertEquals("", toString(timetableRepository.getAllPathTransfers())); } @Test @@ -99,20 +99,19 @@ public void testDirectTransfersWithPatterns() { transferRequests ).buildGraph(); - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - - assertTransfers( - timetableRepository.getAllPathTransfers(), - tr(resolver, S0, 556, S11), - tr(resolver, S0, 935, S21), - tr(resolver, S11, 751, S21), - tr(resolver, S12, 751, S22), - tr(resolver, S13, 2224, S12), - tr(resolver, S13, 2347, S22), - tr(resolver, S21, 751, S11), - tr(resolver, S22, 751, S12), - tr(resolver, S23, 2347, S12), - tr(resolver, S23, 2224, S22) + assertEquals( + """ + S0 - S11, 556m + S0 - S21, 935m + S11 - S21, 751m + S12 - S22, 751m + S13 - S12, 2224m + S13 - S22, 2347m + S21 - S11, 751m + S22 - S12, 751m + S23 - S12, 2347m + S23 - S22, 2224m""", + toString(timetableRepository.getAllPathTransfers()) ); } @@ -132,20 +131,20 @@ public void testDirectTransfersWithRestrictedPatterns() { transferRequests ).buildGraph(); - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - timetableRepository.getAllPathTransfers(), - tr(resolver, S0, 2780, S12), - tr(resolver, S0, 935, S21), - tr(resolver, S11, 2224, S12), - tr(resolver, S11, 751, S21), - tr(resolver, S12, 751, S22), - tr(resolver, S13, 2224, S12), - tr(resolver, S13, 2347, S22), - tr(resolver, S21, 2347, S12), - tr(resolver, S22, 751, S12), - tr(resolver, S23, 2347, S12), - tr(resolver, S23, 2224, S22) + assertEquals( + """ + S0 - S12, 2780m + S0 - S21, 935m + S11 - S12, 2224m + S11 - S21, 751m + S12 - S22, 751m + S13 - S12, 2224m + S13 - S22, 2347m + S21 - S12, 2347m + S22 - S12, 751m + S23 - S12, 2347m + S23 - S22, 2224m""", + toString(timetableRepository.getAllPathTransfers()) ); } @@ -166,7 +165,7 @@ public void testSingleRequestWithoutPatterns() { transferRequests ).buildGraph(); - assertTransfers(timetableRepository.getAllPathTransfers()); + assertEquals("", toString(timetableRepository.getAllPathTransfers())); } @Test @@ -186,12 +185,12 @@ public void testSingleRequestWithPatterns() { transferRequests ).buildGraph(); - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - timetableRepository.getAllPathTransfers(), - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S11, 100, List.of(V11, V21), S21) + assertEquals( + """ + S0 - S11, 100m + S0 - S21, 100m + S11 - S21, 100m""", + toString(timetableRepository.getAllPathTransfers()) ); } @@ -212,7 +211,7 @@ public void testMultipleRequestsWithoutPatterns() { transferRequests ).buildGraph(); - assertTransfers(timetableRepository.getAllPathTransfers()); + assertEquals("", toString(timetableRepository.getAllPathTransfers())); } @Test @@ -236,20 +235,21 @@ public void testMultipleRequestsWithPatterns() { var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); var carTransfers = timetableRepository.findTransfers(StreetMode.CAR); - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - walkTransfers, - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S11, 100, List.of(V11, V21), S21) + assertEquals( + """ + S0 - S11, 100m + S0 - S21, 100m + S11 - S21, 100m""", + toString(walkTransfers) ); - assertTransfers( - bikeTransfers, - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S11, 110, List.of(V11, V22), S22) + assertEquals( + """ + S0 - S11, 100m + S0 - S21, 100m + S11 - S22, 110m""", + toString(bikeTransfers) ); - assertTransfers(carTransfers); + assertEquals("", toString(carTransfers)); } @Test @@ -269,7 +269,7 @@ public void testTransferOnIsolatedStations() { transferRequests ).buildGraph(); - assertTrue(timetableRepository.getAllPathTransfers().isEmpty()); + assertEquals("", toString(timetableRepository.getAllPathTransfers())); } @Test @@ -292,10 +292,7 @@ public void testBikeRequestWithBikesAllowedTransfersWithIncludeEmptyRailStopsInT ).buildGraph(); var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); - assertTransfers( - bikeTransfers, - tr(timetableRepository.getSiteRepository()::getRegularStop, S0, 100, List.of(V0, V21), S21) - ); + assertEquals(" S0 - S21, 100m", toString(bikeTransfers)); }); } @@ -319,16 +316,16 @@ public void testBikeRequestWithBikesAllowedTransfersWithConsiderPatternsForDirec ).buildGraph(); var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - bikeTransfers, - // no transfers involving S11, S12 and S13 - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S0, 200, List.of(V0, V22), S22), - tr(resolver, S0, 300, List.of(V0, V22, V23), S23), - tr(resolver, S21, 100, List.of(V21, V22), S22), - tr(resolver, S21, 200, List.of(V21, V22, V23), S23), - tr(resolver, S22, 100, List.of(V22, V23), S23) + // no transfers involving S11, S12 and S13 + assertEquals( + """ + S0 - S21, 100m + S0 - S22, 200m + S0 - S23, 300m + S21 - S22, 100m + S21 - S23, 200m + S22 - S23, 100m""", + toString(bikeTransfers) ); }); } @@ -361,7 +358,7 @@ public void build() { .withCoordinate(47.520, 19.011) .withParentStation(station) .withVehicleType(TransitMode.BUS) - .withNetexVehicleSubmode(BusSubmodeEnumeration.RAIL_REPLACEMENT_BUS.value()) + .withSometimesUsedRealtime(true) ); S23 = stop("S23", 47.540, 19.011, station); @@ -433,6 +430,23 @@ private TransitStopVertex stop(String id, double lat, double lon, Station parent ); } + private String toString(Collection transfers) { + if (transfers.isEmpty()) { + return ""; + } + return transfers + .stream() + .map(tx -> + "%3s - %3s, %dm".formatted( + tx.from.getName(), + tx.to.getName(), + Math.round(tx.getDistanceMeters()) + ) + ) + .sorted() + .collect(Collectors.joining("\n")); + } + private void assertTransfers( Collection allPathTransfers, TransferDescriptor... transfers diff --git a/doc/user/Configuration.md b/doc/user/Configuration.md index f8deb197f97..13c7e684fe3 100644 --- a/doc/user/Configuration.md +++ b/doc/user/Configuration.md @@ -222,42 +222,42 @@ Here is a list of all features which can be toggled on/off and their default val -| Feature | Description | Enabled by default | Sandbox | -|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------:|:-------:| -| `AlertMetrics` | Starts a background thread to continuously publish metrics about alerts. Needs to be enabled together with `ActuatorAPI`. | | | -| `APIServerInfo` | Enable the server info endpoint. | ✓️ | | -| `APIUpdaterStatus` | Enable endpoint for graph updaters status. | ✓️ | | -| `IncludeEmptyRailStopsInTransfers` | Turning this on guarantees that Rail stops without scheduled departures still get included when generating transfers using `ConsiderPatternsForDirectTransfers`. It is common for stops to be assign at real-time for Rail. Turning this on will help to avoid dropping transfers which are needed, when the stop is in use later. Turning this on, if ConsiderPatternsForDirectTransfers is off has no effect. | | | -| `ConsiderPatternsForDirectTransfers` | Enable limiting transfers so that there is only a single transfer to each pattern. | ✓️ | | -| `DebugUi` | Enable the debug GraphQL client and web UI and located at the root of the web server as well as the debug map tiles it uses. Be aware that the map tiles are not a stable API and can change without notice. Use the [vector tiles feature](sandbox/MapboxVectorTilesApi.md) if you want a stable map tiles API. | ✓️ | | -| `ExtraTransferLegOnSameStop` | Should there be a transfer leg when transferring on the very same stop. Note that for in-seat/interlined transfers no transfer leg will be generated. | | | -| `FloatingBike` | Enable floating bike routing. | ✓️ | | -| `GtfsGraphQlApi` | Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md). | ✓️ | | -| `MinimumTransferTimeIsDefinitive` | If the minimum transfer time is a lower bound (default) or the definitive time for the transfer. Set this to `true` if you want to set a transfer time lower than what OTP derives from OSM data. | | | -| `OnDemandRaptorTransfer` | Calculate transfers only when accessed by Raptor, instead of calculating and caching all transfers for the whole graph, for runtime requests which are not pre-cached in `transferCacheRequests` in router-config.json. This may help performance when doing local journey planning in a large graph. Requests which are specified in `transferCacheRequests` in router-config.json are not affected and are always pre-cached for the whole graph. | | | -| `OptimizeTransfers` | OTP will inspect all itineraries found and optimize where (which stops) the transfer will happen. Waiting time, priority and guaranteed transfers are taken into account. | ✓️ | | -| `ParallelRouting` | Enable performing parts of the trip planning in parallel. | | | -| `TransferConstraints` | Enforce transfers to happen according to the _transfers.txt_ (GTFS) and Interchanges (NeTEx). Turning this _off_ will increase the routing performance a little. | ✓️ | | -| `TransmodelGraphQlApi` | Enable the [Transmodel (NeTEx) GraphQL API](apis/TransmodelApi.md). | ✓️ | ✓️ | -| `ActuatorAPI` | Endpoint for actuators (service health status). | | ✓️ | -| `AsyncGraphQLFetchers` | Whether the @async annotation in the GraphQL schema should lead to the fetch being executed asynchronously. This allows batch or alias queries to run in parallel at the cost of consuming extra threads. | | | -| `WaitForGraphUpdateInPollingUpdaters` | Make all polling updaters wait for graph updates to complete before finishing. If this is not enabled, the updaters will finish after submitting the task to update the graph. | ✓️ | | -| `Emission` | Enable the emission sandbox module. | | ✓️ | -| `EmpiricalDelay` | Enable empirical delay sandbox module. | | ✓️ | -| `DataOverlay` | Enable usage of data overlay when calculating costs for the street network. | | ✓️ | -| `DebugRasterTiles` | Enable debug raster tile API. | | ✓️ | -| `FaresV2` | Enable import of GTFS-Fares v2 data. | | ✓️ | -| `FlexRouting` | Enable FLEX routing. | | ✓️ | -| `GoogleCloudStorage` | Enable Google Cloud Storage integration. | | ✓️ | -| `MultiCriteriaGroupMaxFilter` | Keep the best itinerary with respect to each criteria used in the transit-routing search. For example the itinerary with the lowest cost, fewest transfers, and each unique transit-group (transit-group-priority) is kept, even if the max-limit is exceeded. This is turned off by default for now, until this feature is well tested. | | | -| `RealtimeResolver` | When routing with ignoreRealtimeUpdates=true, add an extra step which populates results with real-time data | | ✓️ | -| `ReportApi` | Enable the report API. | | ✓️ | -| `SandboxAPIGeocoder` | Enable the Geocoder API. | | ✓️ | -| `SandboxAPIMapboxVectorTilesApi` | Enable Mapbox vector tiles API. | | ✓️ | -| `SandboxAPIParkAndRideApi` | Enable park-and-ride endpoint. | | ✓️ | -| `Sorlandsbanen` | Include train Sørlandsbanen in results when searching in south of Norway. Only relevant in Norway. | | ✓️ | -| `TransferAnalyzer` | Analyze transfers during graph build. | | ✓️ | -| `TriasApi` | TRIAS API. | | ✓️ | +| Feature | Description | Enabled by default | Sandbox | +|---------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------:|:-------:| +| `AlertMetrics` | Starts a background thread to continuously publish metrics about alerts. Needs to be enabled together with `ActuatorAPI`. | | | +| `APIServerInfo` | Enable the server info endpoint. | ✓️ | | +| `APIUpdaterStatus` | Enable endpoint for graph updaters status. | ✓️ | | +| `IncludeStopsUsedRealtimeInTransfers` | When generating transfers, stops without any patterns are excluded to improve performance if `ConsiderPatternsForDirectTransfers` is enabled. However, some stops are only used by trips changed or added by real-time updates. Since transfer generation happens before real-time updates are applied, OTP cannot know which stops will be needed. Instead, OTP will attempt to identify stops likely to be used by real-time updates at import time. Common cases include rail stops (which often have late platform assignments) and stops reserved for replacement services (which can be detected in NeTEx by examining the stop sub-mode). This feature has no effect if `ConsiderPatternsForDirectTransfers` is disabled. This feature is only supported for NeTEx feeds, not for GTFS feeds. | | | +| `ConsiderPatternsForDirectTransfers` | Enable limiting transfers so that there is only a single transfer to each pattern. | ✓️ | | +| `DebugUi` | Enable the debug GraphQL client and web UI and located at the root of the web server as well as the debug map tiles it uses. Be aware that the map tiles are not a stable API and can change without notice. Use the [vector tiles feature](sandbox/MapboxVectorTilesApi.md) if you want a stable map tiles API. | ✓️ | | +| `ExtraTransferLegOnSameStop` | Should there be a transfer leg when transferring on the very same stop. Note that for in-seat/interlined transfers no transfer leg will be generated. | | | +| `FloatingBike` | Enable floating bike routing. | ✓️ | | +| `GtfsGraphQlApi` | Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md). | ✓️ | | +| `MinimumTransferTimeIsDefinitive` | If the minimum transfer time is a lower bound (default) or the definitive time for the transfer. Set this to `true` if you want to set a transfer time lower than what OTP derives from OSM data. | | | +| `OnDemandRaptorTransfer` | Calculate transfers only when accessed by Raptor, instead of calculating and caching all transfers for the whole graph, for runtime requests which are not pre-cached in `transferCacheRequests` in router-config.json. This may help performance when doing local journey planning in a large graph. Requests which are specified in `transferCacheRequests` in router-config.json are not affected and are always pre-cached for the whole graph. | | | +| `OptimizeTransfers` | OTP will inspect all itineraries found and optimize where (which stops) the transfer will happen. Waiting time, priority and guaranteed transfers are taken into account. | ✓️ | | +| `ParallelRouting` | Enable performing parts of the trip planning in parallel. | | | +| `TransferConstraints` | Enforce transfers to happen according to the _transfers.txt_ (GTFS) and Interchanges (NeTEx). Turning this _off_ will increase the routing performance a little. | ✓️ | | +| `TransmodelGraphQlApi` | Enable the [Transmodel (NeTEx) GraphQL API](apis/TransmodelApi.md). | ✓️ | ✓️ | +| `ActuatorAPI` | Endpoint for actuators (service health status). | | ✓️ | +| `AsyncGraphQLFetchers` | Whether the @async annotation in the GraphQL schema should lead to the fetch being executed asynchronously. This allows batch or alias queries to run in parallel at the cost of consuming extra threads. | | | +| `WaitForGraphUpdateInPollingUpdaters` | Make all polling updaters wait for graph updates to complete before finishing. If this is not enabled, the updaters will finish after submitting the task to update the graph. | ✓️ | | +| `Emission` | Enable the emission sandbox module. | | ✓️ | +| `EmpiricalDelay` | Enable empirical delay sandbox module. | | ✓️ | +| `DataOverlay` | Enable usage of data overlay when calculating costs for the street network. | | ✓️ | +| `DebugRasterTiles` | Enable debug raster tile API. | | ✓️ | +| `FaresV2` | Enable import of GTFS-Fares v2 data. | | ✓️ | +| `FlexRouting` | Enable FLEX routing. | | ✓️ | +| `GoogleCloudStorage` | Enable Google Cloud Storage integration. | | ✓️ | +| `MultiCriteriaGroupMaxFilter` | Keep the best itinerary with respect to each criteria used in the transit-routing search. For example the itinerary with the lowest cost, fewest transfers, and each unique transit-group (transit-group-priority) is kept, even if the max-limit is exceeded. This is turned off by default for now, until this feature is well tested. | | | +| `RealtimeResolver` | When routing with ignoreRealtimeUpdates=true, add an extra step which populates results with real-time data | | ✓️ | +| `ReportApi` | Enable the report API. | | ✓️ | +| `SandboxAPIGeocoder` | Enable the Geocoder API. | | ✓️ | +| `SandboxAPIMapboxVectorTilesApi` | Enable Mapbox vector tiles API. | | ✓️ | +| `SandboxAPIParkAndRideApi` | Enable park-and-ride endpoint. | | ✓️ | +| `Sorlandsbanen` | Include train Sørlandsbanen in results when searching in south of Norway. Only relevant in Norway. | | ✓️ | +| `TransferAnalyzer` | Analyze transfers during graph build. | | ✓️ | +| `TriasApi` | TRIAS API. | | ✓️ | From 36e8853c4ffe11e7740ebef3e30f134cfb70135f Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Sun, 9 Nov 2025 02:02:42 +0100 Subject: [PATCH 05/22] refactor: Rename ts1 to stop PatternConsideringNearbyStopFinder --- .../PatternConsideringNearbyStopFinder.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java index f62c3a9d47a..1ee5a111577 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java @@ -57,9 +57,9 @@ public List findNearbyStops( streetRequest, reverseDirection )) { - StopLocation ts1 = nearbyStop.stop; + StopLocation stop = nearbyStop.stop; - if (ts1 instanceof RegularStop regularStop) { + if (stop instanceof RegularStop regularStop) { var patternsForStop = findPatternsForStop(regularStop, reverseDirection); if (OTPFeature.IncludeStopsUsedRealtimeInTransfers.isOn()) { @@ -74,12 +74,8 @@ public List findNearbyStops( } if (OTPFeature.FlexRouting.isOn()) { - for (FlexTrip trip : transitService.getFlexIndex().getFlexTripsByStop(ts1)) { - if ( - reverseDirection - ? trip.isAlightingPossible(nearbyStop.stop) - : trip.isBoardingPossible(nearbyStop.stop) - ) { + for (FlexTrip trip : transitService.getFlexIndex().getFlexTripsByStop(stop)) { + if (reverseDirection ? trip.isAlightingPossible(stop) : trip.isBoardingPossible(stop)) { closestStopForFlexTrip.putMin(trip, nearbyStop); } } From 765b5293baa939e45f447cfea28c4db78df6a223 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Mon, 10 Nov 2025 19:56:07 +0100 Subject: [PATCH 06/22] refactor: cleanup DirectTransferGeneratorTest --- .../module/DirectTransferGeneratorTest.java | 582 ++++++------------ 1 file changed, 204 insertions(+), 378 deletions(-) diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java index 0ad53aabe94..6cd8cdb95df 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java @@ -1,23 +1,16 @@ package org.opentripplanner.graph_builder.module; -import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.HashSet; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; -import org.opentripplanner.TestOtpModel; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.model.PathTransfer; @@ -25,9 +18,7 @@ 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.graphfinder.StopResolver; import org.opentripplanner.street.model.StreetTraversalPermission; -import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.vertex.StreetVertex; import org.opentripplanner.street.model.vertex.TransitStopVertex; import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; @@ -35,11 +26,9 @@ import org.opentripplanner.transit.model.network.BikeAccess; import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.Station; -import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; -import org.opentripplanner.utils.tostring.ToStringBuilder; +import org.opentripplanner.transit.service.TimetableRepository; /** * This creates a graph with trip patterns @@ -53,51 +42,32 @@ S13 - V13 --------> V23 - V23 */ -class DirectTransferGeneratorTest extends GraphRoutingTest { +class DirectTransferGeneratorTest { private static final Duration MAX_TRANSFER_DURATION = Duration.ofHours(1); private static final RouteRequest REQUEST_WITH_WALK_TRANSFER = RouteRequest.defaultValue(); private static final RouteRequest REQUEST_WITH_BIKE_TRANSFER = RouteRequest.of() .withJourney(jb -> jb.withTransfer(new StreetRequest(StreetMode.BIKE))) .buildDefault(); - - private TransitStopVertex S0, S11, S12, S13, S21, S22, S23; - private StreetVertex V0, V11, V12, V13, V21, V22, V23; + private static final TransferParameters TX_BIKES_ALLOWED_1H = new TransferParameters( + null, + null, + Duration.parse("PT1H"), + true + ); @Test public void testDirectTransfersWithoutPatterns() { - var otpModel = model(false, false, false); - var graph = otpModel.graph(); - var timetableRepository = otpModel.timetableRepository(); - var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER); - graph.hasStreets = false; - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests - ).buildGraph(); - - assertEquals("", toString(timetableRepository.getAllPathTransfers())); + var repository = testData().withTransferRequests(REQUEST_WITH_WALK_TRANSFER).build(); + assertEquals("", toString(repository.getAllPathTransfers())); } @Test public void testDirectTransfersWithPatterns() { - var otpModel = model(true, false, false); - var graph = otpModel.graph(); - graph.hasStreets = false; - var timetableRepository = otpModel.timetableRepository(); - var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests - ).buildGraph(); + var repository = testData() + .withPatterns() + .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) + .build(); assertEquals( """ @@ -111,25 +81,17 @@ public void testDirectTransfersWithPatterns() { S22 - S12, 751m S23 - S12, 2347m S23 - S22, 2224m""", - toString(timetableRepository.getAllPathTransfers()) + toString(repository.getAllPathTransfers()) ); } @Test public void testDirectTransfersWithRestrictedPatterns() { - var otpModel = model(true, true, false); - var graph = otpModel.graph(); - graph.hasStreets = false; - var timetableRepository = otpModel.timetableRepository(); - var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests - ).buildGraph(); + var repository = testData() + .withPatterns() + .withBoardingConstraint() + .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) + .build(); assertEquals( """ @@ -144,96 +106,58 @@ public void testDirectTransfersWithRestrictedPatterns() { S22 - S12, 751m S23 - S12, 2347m S23 - S22, 2224m""", - toString(timetableRepository.getAllPathTransfers()) + toString(repository.getAllPathTransfers()) ); } @Test public void testSingleRequestWithoutPatterns() { - var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER); - - var otpModel = model(false, false, false); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests - ).buildGraph(); - - assertEquals("", toString(timetableRepository.getAllPathTransfers())); + var repository = testData() + .withgraphHasStreets() + .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) + .build(); + + assertEquals("", toString(repository.getAllPathTransfers())); } @Test public void testSingleRequestWithPatterns() { - var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER); - - var otpModel = model(true, false, false); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests - ).buildGraph(); + var repository = testData() + .withPatterns() + .withgraphHasStreets() + .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) + .build(); assertEquals( """ S0 - S11, 100m S0 - S21, 100m S11 - S21, 100m""", - toString(timetableRepository.getAllPathTransfers()) + toString(repository.getAllPathTransfers()) ); } @Test public void testMultipleRequestsWithoutPatterns() { - var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER, REQUEST_WITH_BIKE_TRANSFER); - - var otpModel = model(false, false, false); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests - ).buildGraph(); - - assertEquals("", toString(timetableRepository.getAllPathTransfers())); + var repository = testData() + .withgraphHasStreets() + .withTransferRequests(REQUEST_WITH_WALK_TRANSFER, REQUEST_WITH_BIKE_TRANSFER) + .build(); + + assertEquals("", toString(repository.getAllPathTransfers())); } @Test public void testMultipleRequestsWithPatterns() { - var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER, REQUEST_WITH_BIKE_TRANSFER); + var repository = testData() + .withPatterns() + .withgraphHasStreets() + .withTransferRequests(REQUEST_WITH_WALK_TRANSFER, REQUEST_WITH_BIKE_TRANSFER) + .build(); - TestOtpModel model = model(true, false, false); - var graph = model.graph(); - graph.hasStreets = true; - var timetableRepository = model.timetableRepository(); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests - ).buildGraph(); - - var walkTransfers = timetableRepository.findTransfers(StreetMode.WALK); - var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); - var carTransfers = timetableRepository.findTransfers(StreetMode.CAR); + var walkTransfers = repository.findTransfers(StreetMode.WALK); + var bikeTransfers = repository.findTransfers(StreetMode.BIKE); + var carTransfers = repository.findTransfers(StreetMode.CAR); assertEquals( """ @@ -254,44 +178,26 @@ public void testMultipleRequestsWithPatterns() { @Test public void testTransferOnIsolatedStations() { - var otpModel = model(true, false, true); - var graph = otpModel.graph(); - graph.hasStreets = false; - - var timetableRepository = otpModel.timetableRepository(); - var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests - ).buildGraph(); - - assertEquals("", toString(timetableRepository.getAllPathTransfers())); + var repository = testData() + .withPatterns() + .withNoTransfersOnStations() + .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) + .build(); + + assertEquals("", toString(repository.getAllPathTransfers())); } @Test public void testBikeRequestWithBikesAllowedTransfersWithIncludeEmptyRailStopsInTransfersOn() { OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { - var transferRequests = List.of(REQUEST_WITH_BIKE_TRANSFER); - - TestOtpModel model = model(true, false, false); - var graph = model.graph(); - graph.hasStreets = true; - - var timetableRepository = model.timetableRepository(); - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests, - Map.of(StreetMode.BIKE, new TransferParameters(null, null, Duration.parse("PT1H"), true)) - ).buildGraph(); - - var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); + var repository = testData() + .withPatterns() + .withgraphHasStreets() + .withTransferRequests(REQUEST_WITH_BIKE_TRANSFER) + .addTransferParameters(StreetMode.BIKE, TX_BIKES_ALLOWED_1H) + .build(); + + var bikeTransfers = repository.findTransfers(StreetMode.BIKE); assertEquals(" S0 - S21, 100m", toString(bikeTransfers)); }); } @@ -299,23 +205,14 @@ public void testBikeRequestWithBikesAllowedTransfersWithIncludeEmptyRailStopsInT @Test public void testBikeRequestWithBikesAllowedTransfersWithConsiderPatternsForDirectTransfersOff() { OTPFeature.ConsiderPatternsForDirectTransfers.testOff(() -> { - var transferRequests = List.of(REQUEST_WITH_BIKE_TRANSFER); - - TestOtpModel model = model(true, false, false); - var graph = model.graph(); - graph.hasStreets = true; - - var timetableRepository = model.timetableRepository(); - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests, - Map.of(StreetMode.BIKE, new TransferParameters(null, null, Duration.parse("PT1H"), true)) - ).buildGraph(); - - var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); + var repository = testData() + .withPatterns() + .withgraphHasStreets() + .withTransferRequests(REQUEST_WITH_BIKE_TRANSFER) + .addTransferParameters(StreetMode.BIKE, TX_BIKES_ALLOWED_1H) + .build(); + + var bikeTransfers = repository.findTransfers(StreetMode.BIKE); // no transfers involving S11, S12 and S13 assertEquals( """ @@ -330,106 +227,6 @@ public void testBikeRequestWithBikesAllowedTransfersWithConsiderPatternsForDirec }); } - private TestOtpModel model( - boolean addPatterns, - boolean withBoardingConstraint, - boolean withNoTransfersOnStations - ) { - return modelOf( - new Builder() { - @Override - public void build() { - var station = stationEntity("1", s -> - s.withTransfersNotAllowed(withNoTransfersOnStations) - ); - - S0 = stop("S0", b -> - b - .withCoordinate(47.495, 19.001) - .withParentStation(station) - .withVehicleType(TransitMode.RAIL) - ); - S11 = stop("S11", 47.500, 19.001, station); - S12 = stop("S12", 47.520, 19.001, station); - S13 = stop("S13", 47.540, 19.001, station); - S21 = stop("S21", 47.500, 19.011, station); - S22 = stop("S22", b -> - b - .withCoordinate(47.520, 19.011) - .withParentStation(station) - .withVehicleType(TransitMode.BUS) - .withSometimesUsedRealtime(true) - ); - S23 = stop("S23", 47.540, 19.011, station); - - V0 = intersection("V0", 47.495, 19.000); - V11 = intersection("V11", 47.500, 19.000); - V12 = intersection("V12", 47.510, 19.000); - V13 = intersection("V13", 47.520, 19.000); - V21 = intersection("V21", 47.500, 19.010); - V22 = intersection("V22", 47.510, 19.010); - V23 = intersection("V23", 47.520, 19.010); - - biLink(V0, S0); - biLink(V11, S11); - biLink(V12, S12); - biLink(V13, S13); - biLink(V21, S21); - biLink(V22, S22); - biLink(V23, S23); - - street(V0, V11, 100, StreetTraversalPermission.ALL); - street(V0, V12, 200, StreetTraversalPermission.ALL); - street(V0, V21, 100, StreetTraversalPermission.ALL); - street(V0, V22, 200, StreetTraversalPermission.ALL); - - street(V11, V12, 100, StreetTraversalPermission.PEDESTRIAN); - street(V12, V13, 100, StreetTraversalPermission.PEDESTRIAN); - street(V21, V22, 100, StreetTraversalPermission.PEDESTRIAN); - street(V22, V23, 100, StreetTraversalPermission.PEDESTRIAN); - street(V11, V21, 100, StreetTraversalPermission.PEDESTRIAN); - street(V11, V22, 110, StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE); - - if (addPatterns) { - var agency = TimetableRepositoryForTest.agency("Agency"); - - tripPattern( - TripPattern.of(TimetableRepositoryForTest.id("TP1")) - .withRoute(route("R1", TransitMode.BUS, agency)) - .withStopPattern( - new StopPattern(List.of(st(S11, !withBoardingConstraint, true), st(S12), st(S13))) - ) - .build() - ); - - tripPattern( - TripPattern.of(TimetableRepositoryForTest.id("TP2")) - .withRoute(route("R2", TransitMode.BUS, agency)) - .withStopPattern(new StopPattern(List.of(st(S21), st(S22), st(S23)))) - .withScheduledTimeTableBuilder(builder -> - builder.addTripTimes( - ScheduledTripTimes.of() - .withTrip( - TimetableRepositoryForTest.trip("bikesAllowedTrip") - .withBikesAllowed(BikeAccess.ALLOWED) - .build() - ) - .withDepartureTimes("00:00 01:00 02:00") - .build() - ) - ) - .build() - ); - } - } - - private TransitStopVertex stop(String id, double lat, double lon, Station parentStation) { - return stop(id, b -> b.withCoordinate(lat, lon).withParentStation(parentStation)); - } - } - ); - } - private String toString(Collection transfers) { if (transfers.isEmpty()) { return ""; @@ -447,127 +244,156 @@ private String toString(Collection transfers) { .collect(Collectors.joining("\n")); } - private void assertTransfers( - Collection allPathTransfers, - TransferDescriptor... transfers - ) { - var matchedTransfers = new HashSet(); - var assertions = Stream.concat( - Arrays.stream(transfers).map(td -> td.matcher(allPathTransfers, matchedTransfers)), - Stream.of(allTransfersMatched(allPathTransfers, matchedTransfers)) - ); - - assertAll(assertions); + private TestData testData() { + return new TestData(); } - private Executable allTransfersMatched( - Collection transfersByStop, - Set matchedTransfers - ) { - return () -> { - var missingTransfers = new HashSet<>(transfersByStop); - missingTransfers.removeAll(matchedTransfers); + private static class TestData extends GraphRoutingTest { - assertEquals(Set.of(), missingTransfers, "All transfers matched"); - }; - } + private boolean addPatterns = false; + private boolean withBoardingConstraint = false; + private boolean withNoTransfersOnStations = false; + private boolean graphHasStreets = false; + private final List transferRequests = new ArrayList<>(); + private final Map transferParametersForMode = new HashMap<>(); - private TransferDescriptor tr( - StopResolver resolver, - TransitStopVertex from, - double distance, - TransitStopVertex to - ) { - return new TransferDescriptor( - resolver.getStop(from.getId()), - distance, - resolver.getStop(to.getId()) - ); - } + public TestData withPatterns() { + this.addPatterns = true; + return this; + } - private TransferDescriptor tr( - StopResolver resolver, - TransitStopVertex from, - double distance, - List vertices, - TransitStopVertex to - ) { - return new TransferDescriptor(resolver, from, distance, vertices, to); - } + public TestData withBoardingConstraint() { + this.withBoardingConstraint = true; + return this; + } - private static class TransferDescriptor { + public TestData withNoTransfersOnStations() { + this.withNoTransfersOnStations = true; + return this; + } - private final StopLocation from; - private final StopLocation to; - private final Double distanceMeters; - private final List vertices; + public TestData withgraphHasStreets() { + this.graphHasStreets = true; + return this; + } - public TransferDescriptor(RegularStop from, Double distanceMeters, RegularStop to) { - this.from = from; - this.distanceMeters = distanceMeters; - this.vertices = null; - this.to = to; + public TestData withTransferRequests(RouteRequest... request) { + this.transferRequests.addAll(Arrays.asList(request)); + return this; } - public TransferDescriptor( - StopResolver resolver, - TransitStopVertex from, - Double distanceMeters, - List vertices, - TransitStopVertex to - ) { - this.from = resolver.getStop(from.getId()); - this.distanceMeters = distanceMeters; - this.vertices = vertices; - this.to = resolver.getStop(to.getId()); + public TestData addTransferParameters(StreetMode mode, TransferParameters value) { + this.transferParametersForMode.put(mode, value); + return this; } - @Override - public String toString() { - return ToStringBuilder.of(getClass()) - .addObj("from", from) - .addObj("to", to) - .addNum("distanceMeters", distanceMeters) - .addCol("vertices", vertices) - .toString(); + public TimetableRepository build() { + var model = modelOf(new Builder()); + model.graph().hasStreets = graphHasStreets; + + new DirectTransferGenerator( + model.graph(), + model.timetableRepository(), + DataImportIssueStore.NOOP, + MAX_TRANSFER_DURATION, + transferRequests, + transferParametersForMode + ).buildGraph(); + + return model.timetableRepository(); } - boolean matches(PathTransfer transfer) { - if (!Objects.equals(from, transfer.from) || !Objects.equals(to, transfer.to)) { - return false; - } + private class Builder extends GraphRoutingTest.Builder { - if (vertices == null) { - return distanceMeters == transfer.getDistanceMeters() && transfer.getEdges() == null; - } else { - var transferVertices = transfer - .getEdges() - .stream() - .map(Edge::getToVertex) - .filter(StreetVertex.class::isInstance) - .toList(); - - return ( - distanceMeters == transfer.getDistanceMeters() && - Objects.equals(vertices, transferVertices) + @Override + public void build() { + var station = stationEntity("1", s -> s.withTransfersNotAllowed(withNoTransfersOnStations)); + TransitStopVertex S0, S11, S12, S13, S21, S22, S23; + StreetVertex V0, V11, V12, V13, V21, V22, V23; + + S0 = stop("S0", b -> + b + .withCoordinate(47.495, 19.001) + .withParentStation(station) + .withVehicleType(TransitMode.RAIL) ); - } - } + S11 = stop("S11", 47.500, 19.001, station); + S12 = stop("S12", 47.520, 19.001, station); + S13 = stop("S13", 47.540, 19.001, station); + S21 = stop("S21", 47.500, 19.011, station); + S22 = stop("S22", b -> + b + .withCoordinate(47.520, 19.011) + .withParentStation(station) + .withVehicleType(TransitMode.BUS) + .withSometimesUsedRealtime(true) + ); + S23 = stop("S23", 47.540, 19.011, station); + + V0 = intersection("V0", 47.495, 19.000); + V11 = intersection("V11", 47.500, 19.000); + V12 = intersection("V12", 47.510, 19.000); + V13 = intersection("V13", 47.520, 19.000); + V21 = intersection("V21", 47.500, 19.010); + V22 = intersection("V22", 47.510, 19.010); + V23 = intersection("V23", 47.520, 19.010); + + biLink(V0, S0); + biLink(V11, S11); + biLink(V12, S12); + biLink(V13, S13); + biLink(V21, S21); + biLink(V22, S22); + biLink(V23, S23); + + street(V0, V11, 100, StreetTraversalPermission.ALL); + street(V0, V12, 200, StreetTraversalPermission.ALL); + street(V0, V21, 100, StreetTraversalPermission.ALL); + street(V0, V22, 200, StreetTraversalPermission.ALL); + + street(V11, V12, 100, StreetTraversalPermission.PEDESTRIAN); + street(V12, V13, 100, StreetTraversalPermission.PEDESTRIAN); + street(V21, V22, 100, StreetTraversalPermission.PEDESTRIAN); + street(V22, V23, 100, StreetTraversalPermission.PEDESTRIAN); + street(V11, V21, 100, StreetTraversalPermission.PEDESTRIAN); + street(V11, V22, 110, StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE); + + if (addPatterns) { + var agency = TimetableRepositoryForTest.agency("Agency"); + + tripPattern( + TripPattern.of(TimetableRepositoryForTest.id("TP1")) + .withRoute(route("R1", TransitMode.BUS, agency)) + .withStopPattern( + new StopPattern(List.of(st(S11, !withBoardingConstraint, true), st(S12), st(S13))) + ) + .build() + ); - private Executable matcher( - Collection transfersByStop, - Set matchedTransfers - ) { - return () -> { - var matched = transfersByStop.stream().filter(this::matches).findFirst(); - - if (matched.isPresent()) { - assertTrue(true, "Found transfer for " + this); - matchedTransfers.add(matched.get()); - } else { - fail("Missing transfer for " + this); + tripPattern( + TripPattern.of(TimetableRepositoryForTest.id("TP2")) + .withRoute(route("R2", TransitMode.BUS, agency)) + .withStopPattern(new StopPattern(List.of(st(S21), st(S22), st(S23)))) + .withScheduledTimeTableBuilder(builder -> + builder.addTripTimes( + ScheduledTripTimes.of() + .withTrip( + TimetableRepositoryForTest.trip("bikesAllowedTrip") + .withBikesAllowed(BikeAccess.ALLOWED) + .build() + ) + .withDepartureTimes("00:00 01:00 02:00") + .build() + ) + ) + .build() + ); } - }; + } + + private TransitStopVertex stop(String id, double lat, double lon, Station parentStation) { + return stop(id, b -> b.withCoordinate(lat, lon).withParentStation(parentStation)); + } } } } From 72bbd0b08a2e6da67424463aa684d1f17e3948ea Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 11 Nov 2025 11:36:50 +0100 Subject: [PATCH 07/22] refactor: improve comments and test names in DirectTransferGenerator and PatternConsideringNearbyStopFinder --- .../module/DirectTransferGenerator.java | 1 + .../PatternConsideringNearbyStopFinder.java | 14 +- .../DirectTransferGeneratorTest.drawio.png | Bin 0 -> 65798 bytes .../module/DirectTransferGeneratorTest.java | 286 +++++++++++++----- 4 files changed, 211 insertions(+), 90 deletions(-) create mode 100644 application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.drawio.png diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java b/application/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java index 010b6233a1c..564adbb0bae 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java @@ -388,6 +388,7 @@ private void calculateDefaultTransfers( if (sd.stop == stop) { continue; } + // TODO FIX THIS - This is wrong! This will prune other options, because the test is done too late. if (sd.stop.transfersNotAllowed()) { continue; } diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java index 1ee5a111577..d94f903c93a 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java @@ -41,22 +41,24 @@ public List findNearbyStops( StreetRequest streetRequest, boolean reverseDirection ) { - /* Track the closest stop on each pattern passing nearby. */ + // Track the closest stop on each pattern passing nearby. MinMap closestStopForPattern = new MinMap<>(); - /* Track the closest stop on each flex trip nearby. */ + // Track the closest stop on each flex trip nearby. MinMap, NearbyStop> closestStopForFlexTrip = new MinMap<>(); - /* The end result */ + // The end result Set uniqueStopsResult = new HashSet<>(); - /* Iterate over nearby stops via the street network or using straight-line distance. */ - for (NearbyStop nearbyStop : delegateNearbyStopFinder.findNearbyStops( + // fetch nearby stops via the street network or using straight-line distance. + var nearbyStops = delegateNearbyStopFinder.findNearbyStops( vertex, routingRequest, streetRequest, reverseDirection - )) { + ); + + for (NearbyStop nearbyStop : nearbyStops) { StopLocation stop = nearbyStop.stop; if (stop instanceof RegularStop regularStop) { diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.drawio.png b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..f921b0148d557cd30822b18f00b7b728e7402c1e GIT binary patch literal 65798 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 MVlprDpKE#le_(2eD*ylh literal 0 HcmV?d00001 diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java index 6cd8cdb95df..e0c69d4cb09 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java @@ -31,16 +31,11 @@ import org.opentripplanner.transit.service.TimetableRepository; /** - * This creates a graph with trip patterns -

-  S0 -  V0 ------------
-        |     \       |
- S11 - V11 --------> V21 - S21
-        |      \      |
- S12 - V12 --------> V22 - V22
-        |             |
- S13 - V13 --------> V23 - V23
- 
+ * This test uses the following graph/network for testing the DirectTransfer generation. The + * fokus is on the filtering of the transfers, not on testing that the NearBySearch return the + * correct set of nearby stops. + *

+ * */ class DirectTransferGeneratorTest { @@ -57,63 +52,121 @@ class DirectTransferGeneratorTest { ); @Test - public void testDirectTransfersWithoutPatterns() { + public void testStraightLineTransfersWithNoPatterns() { + // There is no trip-patterns to transfer between; Hence empty. var repository = testData().withTransferRequests(REQUEST_WITH_WALK_TRANSFER).build(); assertEquals("", toString(repository.getAllPathTransfers())); } @Test - public void testDirectTransfersWithPatterns() { + public void testStraightLineTransfersWithoutPatternsPruning() { + OTPFeature.ConsiderPatternsForDirectTransfers.testOff(() -> { + var repository = testData().withTransferRequests(REQUEST_WITH_WALK_TRANSFER).build(); + // S0 <-> S23 is too fare, not found in neardby search + assertEquals( + """ + S0 - S11, 1668m + S0 - S12, 3892m + S0 - S21, 1829m + S0 - S22, 3964m + S11 - S0, 1668m + S11 - S12, 2224m + S11 - S13, 4448m + S11 - S21, 751m + S11 - S22, 2347m + S11 - S23, 4511m + S12 - S0, 3892m + S12 - S11, 2224m + S12 - S13, 2224m + S12 - S21, 2347m + S12 - S22, 751m + S12 - S23, 2347m + S13 - S11, 4448m + S13 - S12, 2224m + S13 - S21, 4511m + S13 - S22, 2347m + S13 - S23, 751m + S21 - S0, 1829m + S21 - S11, 751m + S21 - S12, 2347m + S21 - S13, 4511m + S21 - S22, 2224m + S21 - S23, 4448m + S22 - S0, 3964m + S22 - S11, 2347m + S22 - S12, 751m + S22 - S13, 2347m + S22 - S21, 2224m + S22 - S23, 2224m + S23 - S11, 4511m + S23 - S12, 2347m + S23 - S13, 751m + S23 - S21, 4448m + S23 - S22, 2224m""", + toString(repository.getAllPathTransfers()) + ); + }); + } + + @Test + public void testStraightLineTransfersWithPatternsPruning() { var repository = testData() .withPatterns() .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) .build(); + // Exactly one transfer is expected from each stop to the closest place to board all + // patterns, including the same pattern used by the stop (E.g. S12 - S11). assertEquals( """ - S0 - S11, 556m - S0 - S21, 935m + S0 - S11, 1668m + S0 - S21, 1829m + S11 - S0, 1668m S11 - S21, 751m + S12 - S0, 3892m + S12 - S11, 2224m S12 - S22, 751m - S13 - S12, 2224m + S13 - S11, 4448m S13 - S22, 2347m + S21 - S0, 1829m S21 - S11, 751m - S22 - S12, 751m - S23 - S12, 2347m + S22 - S0, 3964m + S22 - S11, 2347m + S23 - S11, 4511m S23 - S22, 2224m""", toString(repository.getAllPathTransfers()) ); } @Test - public void testDirectTransfersWithRestrictedPatterns() { + public void testStraightLineTransfersWithBoardingRestrictions() { var repository = testData() .withPatterns() - .withBoardingConstraint() + .withNoBoardingForR1AtStop11() .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) .build(); assertEquals( + // * - S11 is not allowed, because of boarding constraints """ - S0 - S12, 2780m - S0 - S21, 935m - S11 - S12, 2224m + S0 - S21, 1829m + S11 - S0, 1668m S11 - S21, 751m + S12 - S0, 3892m S12 - S22, 751m - S13 - S12, 2224m S13 - S22, 2347m - S21 - S12, 2347m - S22 - S12, 751m - S23 - S12, 2347m + S21 - S0, 1829m + S22 - S0, 3964m S23 - S22, 2224m""", toString(repository.getAllPathTransfers()) ); } @Test - public void testSingleRequestWithoutPatterns() { + public void testStreetTransfersWithNoPatterns() { + // There is no trip-patterns to transfer between; Hence empty. var repository = testData() - .withgraphHasStreets() + .withStreetGraph() .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) .build(); @@ -121,10 +174,40 @@ public void testSingleRequestWithoutPatterns() { } @Test - public void testSingleRequestWithPatterns() { + public void testStreetTransfersWithoutPatternsPruning() { + OTPFeature.ConsiderPatternsForDirectTransfers.testOff(() -> { + var repository = testData() + .withStreetGraph() + .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) + .build(); + + assertEquals( + """ + S0 - S11, 100m + S0 - S12, 200m + S0 - S21, 100m + S0 - S22, 200m + S0 - S23, 300m + S11 - S12, 100m + S11 - S21, 100m + S11 - S22, 110m + S11 - S23, 210m + S12 - S22, 110m + S12 - S23, 210m + S13 - S12, 100m + S13 - S22, 210m + S13 - S23, 310m + S22 - S23, 100m""", + toString(repository.getAllPathTransfers()) + ); + }); + } + + @Test + public void testStreetTransfersWithPatterns() { var repository = testData() .withPatterns() - .withgraphHasStreets() + .withStreetGraph() .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) .build(); @@ -132,26 +215,45 @@ public void testSingleRequestWithPatterns() { """ S0 - S11, 100m S0 - S21, 100m - S11 - S21, 100m""", + S11 - S21, 100m + S12 - S22, 110m + S13 - S22, 210m""", toString(repository.getAllPathTransfers()) ); } @Test - public void testMultipleRequestsWithoutPatterns() { - var repository = testData() - .withgraphHasStreets() - .withTransferRequests(REQUEST_WITH_WALK_TRANSFER, REQUEST_WITH_BIKE_TRANSFER) - .build(); + public void testStreetTransfersWithPatternsIncludeRealTimeUsedStops() { + OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { + var repository = testData() + .withPatterns() + .withStreetGraph() + .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) + .build(); - assertEquals("", toString(repository.getAllPathTransfers())); + assertEquals( + """ + S0 - S11, 100m + S0 - S21, 100m + S0 - S23, 300m + S11 - S21, 100m + S11 - S23, 210m + S12 - S22, 110m + S12 - S23, 210m + S13 - S22, 210m + S13 - S23, 310m + S22 - S23, 100m""", + toString(repository.getAllPathTransfers()) + ); + }); } + @Test - public void testMultipleRequestsWithPatterns() { + public void testStreetTransfersWithMultipleRequestsWithPatterns() { var repository = testData() .withPatterns() - .withgraphHasStreets() + .withStreetGraph() .withTransferRequests(REQUEST_WITH_WALK_TRANSFER, REQUEST_WITH_BIKE_TRANSFER) .build(); @@ -163,28 +265,43 @@ public void testMultipleRequestsWithPatterns() { """ S0 - S11, 100m S0 - S21, 100m - S11 - S21, 100m""", + S11 - S21, 100m + S12 - S22, 110m + S13 - S22, 210m""", toString(walkTransfers) ); + + // Transfer S11-S21 is dominated by the S11-S22 with lower cost; Hence missing. Some of the + // edges used are not allowed for bicycles, but you can walk the bike so they are included here + // with a higher cost. assertEquals( """ S0 - S11, 100m S0 - S21, 100m - S11 - S22, 110m""", + S11 - S22, 110m + S12 - S22, 110m + S13 - S22, 210m""", toString(bikeTransfers) ); assertEquals("", toString(carTransfers)); } @Test - public void testTransferOnIsolatedStations() { + public void testStreetTransfersWithStationWithTransfersNotAllowed() { var repository = testData() .withPatterns() - .withNoTransfersOnStations() + .withStreetGraph() + .withNoTransfersOnStationA() .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) .build(); - assertEquals("", toString(repository.getAllPathTransfers())); + assertEquals( + // TODO Fix: "S0 - S22, 200m" is missing, it is the best transfer after S11 - S21 is droped. + """ + S12 - S22, 110m + S13 - S22, 210m""", + toString(repository.getAllPathTransfers()) + ); } @Test @@ -192,13 +309,19 @@ public void testBikeRequestWithBikesAllowedTransfersWithIncludeEmptyRailStopsInT OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { var repository = testData() .withPatterns() - .withgraphHasStreets() + .withStreetGraph() .withTransferRequests(REQUEST_WITH_BIKE_TRANSFER) .addTransferParameters(StreetMode.BIKE, TX_BIKES_ALLOWED_1H) .build(); var bikeTransfers = repository.findTransfers(StreetMode.BIKE); - assertEquals(" S0 - S21, 100m", toString(bikeTransfers)); + assertEquals( + """ + S13 - S22, 210m + S13 - S23, 310m + S22 - S23, 100m""", + toString(bikeTransfers) + ); }); } @@ -207,20 +330,16 @@ public void testBikeRequestWithBikesAllowedTransfersWithConsiderPatternsForDirec OTPFeature.ConsiderPatternsForDirectTransfers.testOff(() -> { var repository = testData() .withPatterns() - .withgraphHasStreets() + .withStreetGraph() .withTransferRequests(REQUEST_WITH_BIKE_TRANSFER) .addTransferParameters(StreetMode.BIKE, TX_BIKES_ALLOWED_1H) .build(); var bikeTransfers = repository.findTransfers(StreetMode.BIKE); - // no transfers involving S11, S12 and S13 assertEquals( """ - S0 - S21, 100m - S0 - S22, 200m - S0 - S23, 300m - S21 - S22, 100m - S21 - S23, 200m + S13 - S22, 210m + S13 - S23, 310m S22 - S23, 100m""", toString(bikeTransfers) ); @@ -252,7 +371,7 @@ private static class TestData extends GraphRoutingTest { private boolean addPatterns = false; private boolean withBoardingConstraint = false; - private boolean withNoTransfersOnStations = false; + private boolean noTransfersOnStationA = false; private boolean graphHasStreets = false; private final List transferRequests = new ArrayList<>(); private final Map transferParametersForMode = new HashMap<>(); @@ -262,17 +381,17 @@ public TestData withPatterns() { return this; } - public TestData withBoardingConstraint() { + public TestData withNoBoardingForR1AtStop11() { this.withBoardingConstraint = true; return this; } - public TestData withNoTransfersOnStations() { - this.withNoTransfersOnStations = true; + public TestData withNoTransfersOnStationA() { + this.noTransfersOnStationA = true; return this; } - public TestData withgraphHasStreets() { + public TestData withStreetGraph() { this.graphHasStreets = true; return this; } @@ -307,36 +426,31 @@ private class Builder extends GraphRoutingTest.Builder { @Override public void build() { - var station = stationEntity("1", s -> s.withTransfersNotAllowed(withNoTransfersOnStations)); - TransitStopVertex S0, S11, S12, S13, S21, S22, S23; + var stationA = stationEntity("1", s -> s.withTransfersNotAllowed(noTransfersOnStationA)); + TransitStopVertex S0, S_FAR_AWAY, S11, S12, S13, S21, S22, S23; StreetVertex V0, V11, V12, V13, V21, V22, V23; - S0 = stop("S0", b -> - b - .withCoordinate(47.495, 19.001) - .withParentStation(station) - .withVehicleType(TransitMode.RAIL) - ); - S11 = stop("S11", 47.500, 19.001, station); - S12 = stop("S12", 47.520, 19.001, station); - S13 = stop("S13", 47.540, 19.001, station); - S21 = stop("S21", 47.500, 19.011, station); + S0 = stop("S0", b -> b.withCoordinate(47.485, 19.001).withVehicleType(TransitMode.RAIL)); + S_FAR_AWAY = stop("FarAway", 55.0, 30.0); + S11 = stop("S11", 47.500, 19.001, stationA); + S12 = stop("S12", 47.520, 19.001); + S13 = stop("S13", b -> b.withCoordinate(47.540, 19.001).withSometimesUsedRealtime(true)); + S21 = stop("S21", 47.500, 19.011, stationA); S22 = stop("S22", b -> b .withCoordinate(47.520, 19.011) - .withParentStation(station) .withVehicleType(TransitMode.BUS) .withSometimesUsedRealtime(true) ); - S23 = stop("S23", 47.540, 19.011, station); + S23 = stop("S23", b -> b.withCoordinate(47.540, 19.011).withSometimesUsedRealtime(true)); - V0 = intersection("V0", 47.495, 19.000); + V0 = intersection("V0", 47.485, 19.000); V11 = intersection("V11", 47.500, 19.000); - V12 = intersection("V12", 47.510, 19.000); - V13 = intersection("V13", 47.520, 19.000); + V12 = intersection("V12", 47.520, 19.000); + V13 = intersection("V13", 47.540, 19.000); V21 = intersection("V21", 47.500, 19.010); - V22 = intersection("V22", 47.510, 19.010); - V23 = intersection("V23", 47.520, 19.010); + V22 = intersection("V22", 47.520, 19.010); + V23 = intersection("V23", 47.540, 19.010); biLink(V0, S0); biLink(V11, S11); @@ -347,33 +461,37 @@ public void build() { biLink(V23, S23); street(V0, V11, 100, StreetTraversalPermission.ALL); - street(V0, V12, 200, StreetTraversalPermission.ALL); street(V0, V21, 100, StreetTraversalPermission.ALL); street(V0, V22, 200, StreetTraversalPermission.ALL); street(V11, V12, 100, StreetTraversalPermission.PEDESTRIAN); - street(V12, V13, 100, StreetTraversalPermission.PEDESTRIAN); - street(V21, V22, 100, StreetTraversalPermission.PEDESTRIAN); - street(V22, V23, 100, StreetTraversalPermission.PEDESTRIAN); street(V11, V21, 100, StreetTraversalPermission.PEDESTRIAN); street(V11, V22, 110, StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE); + street(V12, V22, 110, StreetTraversalPermission.PEDESTRIAN); + street(V13, V12, 100, StreetTraversalPermission.PEDESTRIAN); + street(V22, V23, 100, StreetTraversalPermission.PEDESTRIAN); if (addPatterns) { var agency = TimetableRepositoryForTest.agency("Agency"); + tripPattern( + TripPattern.of(TimetableRepositoryForTest.id("TP0")) + .withRoute(route("R0", TransitMode.RAIL, agency)) + .withStopPattern(new StopPattern(List.of(st(S0), st(S_FAR_AWAY)))) + .build() + ); tripPattern( TripPattern.of(TimetableRepositoryForTest.id("TP1")) .withRoute(route("R1", TransitMode.BUS, agency)) .withStopPattern( - new StopPattern(List.of(st(S11, !withBoardingConstraint, true), st(S12), st(S13))) + new StopPattern(List.of(st(S11, !withBoardingConstraint, true), st(S12))) ) .build() ); - tripPattern( TripPattern.of(TimetableRepositoryForTest.id("TP2")) .withRoute(route("R2", TransitMode.BUS, agency)) - .withStopPattern(new StopPattern(List.of(st(S21), st(S22), st(S23)))) + .withStopPattern(new StopPattern(List.of(st(S21), st(S22), st(S_FAR_AWAY)))) .withScheduledTimeTableBuilder(builder -> builder.addTripTimes( ScheduledTripTimes.of() From 397ecc656960d3fdd9d1a8887c3dd4c781f3c6f3 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 11 Nov 2025 14:56:32 +0100 Subject: [PATCH 08/22] refactor: Split PatternConsideringNearbyStopFinder into classes with smaller responsibilities --- .../module/DirectTransferGenerator.java | 2 +- .../PatternConsideringNearbyStopFinder.java | 105 ------------------ .../CompositNearbyStopFilter.java | 51 +++++++++ .../FlexTripNearbyStopFilter.java | 34 ++++++ .../{ => transferfilter}/MinMap.java | 2 +- .../transferfilter/NearbyStopFilter.java | 18 +++ .../PatternConsideringNearbyStopFinder.java | 48 ++++++++ .../PatternNearbyStopFilter.java | 70 ++++++++++++ .../module/DirectTransferGeneratorTest.java | 1 - 9 files changed, 223 insertions(+), 108 deletions(-) delete mode 100644 application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java create mode 100644 application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/CompositNearbyStopFilter.java create mode 100644 application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/FlexTripNearbyStopFilter.java rename application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/{ => transferfilter}/MinMap.java (94%) create mode 100644 application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/NearbyStopFilter.java create mode 100644 application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/PatternConsideringNearbyStopFinder.java create mode 100644 application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/PatternNearbyStopFilter.java diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java b/application/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java index 564adbb0bae..9bb5b22fbd8 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java @@ -16,11 +16,11 @@ import org.opentripplanner.graph_builder.issues.StopNotLinkedForTransfers; import org.opentripplanner.graph_builder.model.GraphBuilderModule; import org.opentripplanner.graph_builder.module.nearbystops.NearbyStopFinder; -import org.opentripplanner.graph_builder.module.nearbystops.PatternConsideringNearbyStopFinder; import org.opentripplanner.graph_builder.module.nearbystops.SiteRepositoryResolver; import org.opentripplanner.graph_builder.module.nearbystops.StopResolver; import org.opentripplanner.graph_builder.module.nearbystops.StraightLineNearbyStopFinder; import org.opentripplanner.graph_builder.module.nearbystops.StreetNearbyStopFinder; +import org.opentripplanner.graph_builder.module.nearbystops.transferfilter.PatternConsideringNearbyStopFinder; import org.opentripplanner.model.PathTransfer; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java deleted file mode 100644 index d94f903c93a..00000000000 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/PatternConsideringNearbyStopFinder.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.opentripplanner.graph_builder.module.nearbystops; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import org.opentripplanner.ext.flex.trip.FlexTrip; -import org.opentripplanner.framework.application.OTPFeature; -import org.opentripplanner.routing.api.request.RouteRequest; -import org.opentripplanner.routing.api.request.request.StreetRequest; -import org.opentripplanner.routing.graphfinder.NearbyStop; -import org.opentripplanner.street.model.vertex.Vertex; -import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.site.RegularStop; -import org.opentripplanner.transit.model.site.StopLocation; -import org.opentripplanner.transit.service.TransitService; - -public class PatternConsideringNearbyStopFinder implements NearbyStopFinder { - - private final NearbyStopFinder delegateNearbyStopFinder; - private final TransitService transitService; - - public PatternConsideringNearbyStopFinder( - TransitService transitService, - NearbyStopFinder delegateNearbyStopFinder - ) { - this.transitService = transitService; - this.delegateNearbyStopFinder = delegateNearbyStopFinder; - } - - /** - * Find all unique nearby stops that are the closest stop on some trip pattern or flex trip. Note - * that the result will include the origin vertex if it is an instance of StopVertex. This is - * intentional: we don't want to return the next stop down the line for trip patterns that pass - * through the origin vertex. Taking the patterns into account reduces the number of transfers - * significantly compared to simple traverse-duration-constrained all-to-all stop linkage. - */ - @Override - public List findNearbyStops( - Vertex vertex, - RouteRequest routingRequest, - StreetRequest streetRequest, - boolean reverseDirection - ) { - // Track the closest stop on each pattern passing nearby. - MinMap closestStopForPattern = new MinMap<>(); - - // Track the closest stop on each flex trip nearby. - MinMap, NearbyStop> closestStopForFlexTrip = new MinMap<>(); - - // The end result - Set uniqueStopsResult = new HashSet<>(); - - // fetch nearby stops via the street network or using straight-line distance. - var nearbyStops = delegateNearbyStopFinder.findNearbyStops( - vertex, - routingRequest, - streetRequest, - reverseDirection - ); - - for (NearbyStop nearbyStop : nearbyStops) { - StopLocation stop = nearbyStop.stop; - - if (stop instanceof RegularStop regularStop) { - var patternsForStop = findPatternsForStop(regularStop, reverseDirection); - - if (OTPFeature.IncludeStopsUsedRealtimeInTransfers.isOn()) { - if (patternsForStop.isEmpty() && regularStop.isSometimesUsedRealtime()) { - uniqueStopsResult.add(nearbyStop); - } - } - - for (var pattern : patternsForStop) { - closestStopForPattern.putMin(pattern, nearbyStop); - } - } - - if (OTPFeature.FlexRouting.isOn()) { - for (FlexTrip trip : transitService.getFlexIndex().getFlexTripsByStop(stop)) { - if (reverseDirection ? trip.isAlightingPossible(stop) : trip.isBoardingPossible(stop)) { - closestStopForFlexTrip.putMin(trip, nearbyStop); - } - } - } - } - - /* Make a transfer from the origin stop to each destination stop that was the closest stop on any pattern. */ - uniqueStopsResult.addAll(closestStopForFlexTrip.values()); - uniqueStopsResult.addAll(closestStopForPattern.values()); - // TODO: don't convert to list - return uniqueStopsResult.stream().toList(); - } - - /** - * Find all candidate patterns for the given destination {@code stop}. Only return patterns - * where we can board(forward direction) or alight(reverse direction) at the given stop. - */ - private List findPatternsForStop(RegularStop stop, boolean reverseDirection) { - return transitService - .findPatterns(stop) - .stream() - .filter(reverseDirection ? p -> p.canAlight(stop) : p -> p.canBoard(stop)) - .toList(); - } -} diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/CompositNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/CompositNearbyStopFilter.java new file mode 100644 index 00000000000..fa542f5bdbd --- /dev/null +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/CompositNearbyStopFilter.java @@ -0,0 +1,51 @@ +package org.opentripplanner.graph_builder.module.nearbystops.transferfilter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.opentripplanner.routing.graphfinder.NearbyStop; + +class CompositNearbyStopFilter implements NearbyStopFilter { + + private final List filters; + + private CompositNearbyStopFilter(List filters) { + this.filters = filters; + } + + static Builder of() { + return new Builder(); + } + + @Override + public Collection filterToStops( + Collection nearbyStops, + boolean reverseDirection + ) { + Set result = new HashSet<>(); + + for (NearbyStopFilter it : filters) { + result.addAll(it.filterToStops(nearbyStops, reverseDirection)); + } + return result; + } + + static class Builder { + + List filters = new ArrayList<>(); + + Builder add(NearbyStopFilter filter) { + filters.add(filter); + return this; + } + + NearbyStopFilter build() { + if (filters.size() == 1) { + return filters.getFirst(); + } + return new CompositNearbyStopFilter(filters); + } + } +} diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/FlexTripNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/FlexTripNearbyStopFilter.java new file mode 100644 index 00000000000..4aaeb4774a3 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/FlexTripNearbyStopFilter.java @@ -0,0 +1,34 @@ +package org.opentripplanner.graph_builder.module.nearbystops.transferfilter; + +import java.util.Collection; +import org.opentripplanner.ext.flex.trip.FlexTrip; +import org.opentripplanner.routing.graphfinder.NearbyStop; +import org.opentripplanner.transit.service.TransitService; + +class FlexTripNearbyStopFilter implements NearbyStopFilter { + + private final TransitService transitService; + + FlexTripNearbyStopFilter(TransitService transitService) { + this.transitService = transitService; + } + + @Override + public Collection filterToStops( + Collection nearbyStops, + boolean reverseDirection + ) { + MinMap, NearbyStop> closestStopForFlexTrip = new MinMap<>(); + for (var it : nearbyStops) { + var stop = it.stop; + var flexTrips = transitService.getFlexIndex().getFlexTripsByStop(stop); + + for (FlexTrip trip : flexTrips) { + if (reverseDirection ? trip.isAlightingPossible(stop) : trip.isBoardingPossible(stop)) { + closestStopForFlexTrip.putMin(trip, it); + } + } + } + return closestStopForFlexTrip.values(); + } +} diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/MinMap.java b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/MinMap.java similarity index 94% rename from application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/MinMap.java rename to application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/MinMap.java index 6492a134474..50c479a4025 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/MinMap.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/MinMap.java @@ -1,4 +1,4 @@ -package org.opentripplanner.graph_builder.module.nearbystops; +package org.opentripplanner.graph_builder.module.nearbystops.transferfilter; import java.util.HashMap; diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/NearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/NearbyStopFilter.java new file mode 100644 index 00000000000..532da55819d --- /dev/null +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/NearbyStopFilter.java @@ -0,0 +1,18 @@ +package org.opentripplanner.graph_builder.module.nearbystops.transferfilter; + +import java.util.Collection; +import org.opentripplanner.routing.graphfinder.NearbyStop; + +interface NearbyStopFilter { + /** + * Find all unique nearby stops that are the closest stop on some trip pattern or flex trip. Note + * that the result will include the origin vertex if it is an instance of StopVertex. This is + * intentional: we don't want to return the next stop down the line for trip patterns that pass + * through the origin vertex. Taking the patterns into account reduces the number of transfers + * significantly compared to simple traverse-duration-constrained all-to-all stop linkage. + */ + Collection filterToStops( + Collection nearbyStops, + boolean reverseDirection + ); +} diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/PatternConsideringNearbyStopFinder.java b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/PatternConsideringNearbyStopFinder.java new file mode 100644 index 00000000000..60afe6880d2 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/PatternConsideringNearbyStopFinder.java @@ -0,0 +1,48 @@ +package org.opentripplanner.graph_builder.module.nearbystops.transferfilter; + +import java.util.List; +import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.graph_builder.module.nearbystops.NearbyStopFinder; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.routing.api.request.request.StreetRequest; +import org.opentripplanner.routing.graphfinder.NearbyStop; +import org.opentripplanner.street.model.vertex.Vertex; +import org.opentripplanner.transit.service.TransitService; + +public class PatternConsideringNearbyStopFinder implements NearbyStopFinder { + + private final NearbyStopFilter filter; + + private final NearbyStopFinder delegateNearbyStopFinder; + + public PatternConsideringNearbyStopFinder( + TransitService transitService, + NearbyStopFinder delegateNearbyStopFinder + ) { + var builder = CompositNearbyStopFilter.of().add(new PatternNearbyStopFilter(transitService)); + + if (OTPFeature.FlexRouting.isOn()) { + builder.add(new FlexTripNearbyStopFilter(transitService)); + } + this.filter = builder.build(); + this.delegateNearbyStopFinder = delegateNearbyStopFinder; + } + + @Override + public List findNearbyStops( + Vertex vertex, + RouteRequest routingRequest, + StreetRequest streetRequest, + boolean reverseDirection + ) { + // fetch nearby stops via the street network or using straight-line distance. + var nearbyStops = delegateNearbyStopFinder.findNearbyStops( + vertex, + routingRequest, + streetRequest, + reverseDirection + ); + var result = filter.filterToStops(nearbyStops, reverseDirection); + return List.copyOf(result); + } +} diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/PatternNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/PatternNearbyStopFilter.java new file mode 100644 index 00000000000..445fac0dbc5 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/PatternNearbyStopFilter.java @@ -0,0 +1,70 @@ +package org.opentripplanner.graph_builder.module.nearbystops.transferfilter; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.routing.graphfinder.NearbyStop; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.service.TransitService; + +class PatternNearbyStopFilter implements NearbyStopFilter { + + private final TransitService transitService; + + PatternNearbyStopFilter(TransitService transitService) { + this.transitService = transitService; + } + + @Override + public Collection filterToStops( + Collection nearbyStops, + boolean reverseDirection + ) { + // Track the closest stop on each pattern passing nearby. + MinMap closestStopForPattern = new MinMap<>(); + + // The end result + Set uniqueStopsResult = new HashSet<>(); + + for (var it : nearbyStops) { + StopLocation stop = it.stop; + + if (stop instanceof RegularStop regularStop) { + var patternsForStop = findPatternsForStop(regularStop, reverseDirection); + + if (patternsForStop.isEmpty() && includeStopUsedRealtime(regularStop)) { + uniqueStopsResult.add(it); + } + + for (var pattern : patternsForStop) { + closestStopForPattern.putMin(pattern, it); + } + } + } + uniqueStopsResult.addAll(closestStopForPattern.values()); + + return uniqueStopsResult; + } + + /** + * Find all candidate patterns for the given destination {@code stop}. Only return patterns + * where we can board(forward direction) or alight(reverse direction) at the given stop. + */ + private List findPatternsForStop(RegularStop stop, boolean reverseDirection) { + return transitService + .findPatterns(stop) + .stream() + .filter(reverseDirection ? p -> p.canAlight(stop) : p -> p.canBoard(stop)) + .map(TripPattern::getId) + .toList(); + } + + private boolean includeStopUsedRealtime(RegularStop stop) { + return OTPFeature.IncludeStopsUsedRealtimeInTransfers.isOn() && stop.isSometimesUsedRealtime(); + } +} diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java index e0c69d4cb09..0d4bc24af0d 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java @@ -248,7 +248,6 @@ public void testStreetTransfersWithPatternsIncludeRealTimeUsedStops() { }); } - @Test public void testStreetTransfersWithMultipleRequestsWithPatterns() { var repository = testData() From b3cb1d78674a75fdbcfa18c578035c42eff1c748 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 11 Nov 2025 15:03:13 +0100 Subject: [PATCH 09/22] refactor: Move DirectTransferGenerator(++) into o.o.graph_builder.module.transfer --- .../ext/flex/FlexIntegrationTest.java | 2 +- .../module/configure/GraphBuilderFactory.java | 2 +- .../module/configure/GraphBuilderModules.java | 2 +- .../{ => transfer}/DirectTransferGenerator.java | 5 +++-- .../filter}/CompositNearbyStopFilter.java | 2 +- .../filter}/FlexTripNearbyStopFilter.java | 2 +- .../transferfilter => transfer/filter}/MinMap.java | 2 +- .../filter}/NearbyStopFilter.java | 2 +- .../filter}/PatternConsideringNearbyStopFinder.java | 2 +- .../filter}/PatternNearbyStopFilter.java | 2 +- application/src/main/resources/logback.xml | 2 +- .../java/org/opentripplanner/ConstantsForTests.java | 2 +- .../DirectTransferGeneratorCarTest.java | 3 ++- .../DirectTransferGeneratorTest.drawio.png | Bin .../{ => transfer}/DirectTransferGeneratorTest.java | 3 ++- application/src/test/resources/logback.xml | 2 +- 16 files changed, 19 insertions(+), 16 deletions(-) rename application/src/main/java/org/opentripplanner/graph_builder/module/{ => transfer}/DirectTransferGenerator.java (98%) rename application/src/main/java/org/opentripplanner/graph_builder/module/{nearbystops/transferfilter => transfer/filter}/CompositNearbyStopFilter.java (93%) rename application/src/main/java/org/opentripplanner/graph_builder/module/{nearbystops/transferfilter => transfer/filter}/FlexTripNearbyStopFilter.java (93%) rename application/src/main/java/org/opentripplanner/graph_builder/module/{nearbystops/transferfilter => transfer/filter}/MinMap.java (94%) rename application/src/main/java/org/opentripplanner/graph_builder/module/{nearbystops/transferfilter => transfer/filter}/NearbyStopFilter.java (90%) rename application/src/main/java/org/opentripplanner/graph_builder/module/{nearbystops/transferfilter => transfer/filter}/PatternConsideringNearbyStopFinder.java (95%) rename application/src/main/java/org/opentripplanner/graph_builder/module/{nearbystops/transferfilter => transfer/filter}/PatternNearbyStopFilter.java (96%) rename application/src/test/java/org/opentripplanner/graph_builder/module/{ => transfer}/DirectTransferGeneratorCarTest.java (99%) rename application/src/test/java/org/opentripplanner/graph_builder/module/{ => transfer}/DirectTransferGeneratorTest.drawio.png (100%) rename application/src/test/java/org/opentripplanner/graph_builder/module/{ => transfer}/DirectTransferGeneratorTest.java (99%) diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java index ff1c88ca223..c0f076f1c13 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java @@ -20,8 +20,8 @@ import org.opentripplanner.TestServerContext; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; -import org.opentripplanner.graph_builder.module.DirectTransferGenerator; import org.opentripplanner.graph_builder.module.TestStreetLinkerModule; +import org.opentripplanner.graph_builder.module.transfer.DirectTransferGenerator; import org.opentripplanner.gtfs.graphbuilder.GtfsBundleTestFactory; import org.opentripplanner.gtfs.graphbuilder.GtfsModule; import org.opentripplanner.model.GenericLocation; diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderFactory.java b/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderFactory.java index bbacbdb06e4..d9f52c255b9 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderFactory.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderFactory.java @@ -21,7 +21,6 @@ import org.opentripplanner.graph_builder.GraphBuilderDataSources; import org.opentripplanner.graph_builder.configure.GraphBuilderModule; import org.opentripplanner.graph_builder.issue.report.DataImportIssueReporter; -import org.opentripplanner.graph_builder.module.DirectTransferGenerator; import org.opentripplanner.graph_builder.module.GraphCoherencyCheckerModule; import org.opentripplanner.graph_builder.module.OsmBoardingLocationsModule; import org.opentripplanner.graph_builder.module.RouteToCentroidStationIdsValidator; @@ -33,6 +32,7 @@ import org.opentripplanner.graph_builder.module.islandpruning.PruneIslands; import org.opentripplanner.graph_builder.module.ned.ElevationModule; import org.opentripplanner.graph_builder.module.osm.OsmModule; +import org.opentripplanner.graph_builder.module.transfer.DirectTransferGenerator; import org.opentripplanner.gtfs.graphbuilder.GtfsModule; import org.opentripplanner.netex.NetexModule; import org.opentripplanner.routing.fares.FareServiceFactory; diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java b/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java index 572d04f18a8..30ceeb3fc00 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java @@ -21,7 +21,6 @@ import org.opentripplanner.graph_builder.issue.report.DataImportIssueReporter; import org.opentripplanner.graph_builder.issue.service.DefaultDataImportIssueStore; import org.opentripplanner.graph_builder.model.ConfiguredDataSource; -import org.opentripplanner.graph_builder.module.DirectTransferGenerator; import org.opentripplanner.graph_builder.module.RouteToCentroidStationIdsValidator; import org.opentripplanner.graph_builder.module.StreetLinkerModule; import org.opentripplanner.graph_builder.module.TurnRestrictionModule; @@ -33,6 +32,7 @@ import org.opentripplanner.graph_builder.module.ned.parameter.DemExtractParameters; import org.opentripplanner.graph_builder.module.osm.OsmModule; import org.opentripplanner.graph_builder.module.osm.parameters.OsmExtractParameters; +import org.opentripplanner.graph_builder.module.transfer.DirectTransferGenerator; import org.opentripplanner.graph_builder.services.ned.ElevationGridCoverageFactory; import org.opentripplanner.gtfs.graphbuilder.GtfsBundle; import org.opentripplanner.gtfs.graphbuilder.GtfsModule; diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGenerator.java similarity index 98% rename from application/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java rename to application/src/main/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGenerator.java index 9bb5b22fbd8..7559c770846 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGenerator.java @@ -1,4 +1,4 @@ -package org.opentripplanner.graph_builder.module; +package org.opentripplanner.graph_builder.module.transfer; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimaps; @@ -15,12 +15,13 @@ import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.graph_builder.issues.StopNotLinkedForTransfers; import org.opentripplanner.graph_builder.model.GraphBuilderModule; +import org.opentripplanner.graph_builder.module.TransferParameters; import org.opentripplanner.graph_builder.module.nearbystops.NearbyStopFinder; import org.opentripplanner.graph_builder.module.nearbystops.SiteRepositoryResolver; import org.opentripplanner.graph_builder.module.nearbystops.StopResolver; import org.opentripplanner.graph_builder.module.nearbystops.StraightLineNearbyStopFinder; import org.opentripplanner.graph_builder.module.nearbystops.StreetNearbyStopFinder; -import org.opentripplanner.graph_builder.module.nearbystops.transferfilter.PatternConsideringNearbyStopFinder; +import org.opentripplanner.graph_builder.module.transfer.filter.PatternConsideringNearbyStopFinder; import org.opentripplanner.model.PathTransfer; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/CompositNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositNearbyStopFilter.java similarity index 93% rename from application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/CompositNearbyStopFilter.java rename to application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositNearbyStopFilter.java index fa542f5bdbd..f90fdb9f656 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/CompositNearbyStopFilter.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositNearbyStopFilter.java @@ -1,4 +1,4 @@ -package org.opentripplanner.graph_builder.module.nearbystops.transferfilter; +package org.opentripplanner.graph_builder.module.transfer.filter; import java.util.ArrayList; import java.util.Collection; diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/FlexTripNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/FlexTripNearbyStopFilter.java similarity index 93% rename from application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/FlexTripNearbyStopFilter.java rename to application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/FlexTripNearbyStopFilter.java index 4aaeb4774a3..c6722f15d67 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/FlexTripNearbyStopFilter.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/FlexTripNearbyStopFilter.java @@ -1,4 +1,4 @@ -package org.opentripplanner.graph_builder.module.nearbystops.transferfilter; +package org.opentripplanner.graph_builder.module.transfer.filter; import java.util.Collection; import org.opentripplanner.ext.flex.trip.FlexTrip; diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/MinMap.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMap.java similarity index 94% rename from application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/MinMap.java rename to application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMap.java index 50c479a4025..4866da2d162 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/MinMap.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMap.java @@ -1,4 +1,4 @@ -package org.opentripplanner.graph_builder.module.nearbystops.transferfilter; +package org.opentripplanner.graph_builder.module.transfer.filter; import java.util.HashMap; diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/NearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/NearbyStopFilter.java similarity index 90% rename from application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/NearbyStopFilter.java rename to application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/NearbyStopFilter.java index 532da55819d..562ca938687 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/NearbyStopFilter.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/NearbyStopFilter.java @@ -1,4 +1,4 @@ -package org.opentripplanner.graph_builder.module.nearbystops.transferfilter; +package org.opentripplanner.graph_builder.module.transfer.filter; import java.util.Collection; import org.opentripplanner.routing.graphfinder.NearbyStop; diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/PatternConsideringNearbyStopFinder.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java similarity index 95% rename from application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/PatternConsideringNearbyStopFinder.java rename to application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java index 60afe6880d2..a658cbc0441 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/PatternConsideringNearbyStopFinder.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java @@ -1,4 +1,4 @@ -package org.opentripplanner.graph_builder.module.nearbystops.transferfilter; +package org.opentripplanner.graph_builder.module.transfer.filter; import java.util.List; import org.opentripplanner.framework.application.OTPFeature; diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/PatternNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java similarity index 96% rename from application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/PatternNearbyStopFilter.java rename to application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java index 445fac0dbc5..19c1ecdd747 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/nearbystops/transferfilter/PatternNearbyStopFilter.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java @@ -1,4 +1,4 @@ -package org.opentripplanner.graph_builder.module.nearbystops.transferfilter; +package org.opentripplanner.graph_builder.module.transfer.filter; import java.util.Collection; import java.util.HashSet; diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index fcd82609b14..f2c4f20c078 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -54,7 +54,7 @@ - + diff --git a/application/src/test/java/org/opentripplanner/ConstantsForTests.java b/application/src/test/java/org/opentripplanner/ConstantsForTests.java index 17251bbb9ab..7735b29bb0a 100644 --- a/application/src/test/java/org/opentripplanner/ConstantsForTests.java +++ b/application/src/test/java/org/opentripplanner/ConstantsForTests.java @@ -16,12 +16,12 @@ import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.graph_builder.model.ConfiguredCompositeDataSource; -import org.opentripplanner.graph_builder.module.DirectTransferGenerator; import org.opentripplanner.graph_builder.module.TestStreetLinkerModule; import org.opentripplanner.graph_builder.module.TurnRestrictionModule; import org.opentripplanner.graph_builder.module.ned.ElevationModule; import org.opentripplanner.graph_builder.module.ned.GeotiffGridCoverageFactoryImpl; import org.opentripplanner.graph_builder.module.osm.OsmModuleTestFactory; +import org.opentripplanner.graph_builder.module.transfer.DirectTransferGenerator; import org.opentripplanner.gtfs.graphbuilder.GtfsBundleTestFactory; import org.opentripplanner.gtfs.graphbuilder.GtfsModule; import org.opentripplanner.model.calendar.ServiceDateInterval; diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorCarTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java similarity index 99% rename from application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorCarTest.java rename to application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java index 427ed52655d..bdde8ae3960 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorCarTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java @@ -1,4 +1,4 @@ -package org.opentripplanner.graph_builder.module; +package org.opentripplanner.graph_builder.module.transfer; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -19,6 +19,7 @@ import org.junit.jupiter.api.function.Executable; import org.opentripplanner.TestOtpModel; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.graph_builder.module.TransferParameters; import org.opentripplanner.model.PathTransfer; import org.opentripplanner.model.StopTime; import org.opentripplanner.routing.algorithm.GraphRoutingTest; diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.drawio.png b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.drawio.png similarity index 100% rename from application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.drawio.png rename to application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.drawio.png diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java similarity index 99% rename from application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java rename to application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java index 0d4bc24af0d..84f24c79ced 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/DirectTransferGeneratorTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java @@ -1,4 +1,4 @@ -package org.opentripplanner.graph_builder.module; +package org.opentripplanner.graph_builder.module.transfer; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.graph_builder.module.TransferParameters; import org.opentripplanner.model.PathTransfer; import org.opentripplanner.routing.algorithm.GraphRoutingTest; import org.opentripplanner.routing.api.request.RouteRequest; diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml index effedc8b242..b3fbc105ac3 100644 --- a/application/src/test/resources/logback.xml +++ b/application/src/test/resources/logback.xml @@ -27,7 +27,7 @@ - + From 626b07c66ff68c4bdbd0ad3cd8fb8b8ae70df99e Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 11 Nov 2025 15:29:23 +0100 Subject: [PATCH 10/22] fix: Nearby-stops with transfers-not-allowed are removed after filtering, causing the the best transfer alternative to be dropped. --- .../module/transfer/DirectTransferGenerator.java | 4 ---- .../filter/PatternConsideringNearbyStopFinder.java | 13 +++++++++++++ .../transfer/DirectTransferGeneratorTest.java | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGenerator.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGenerator.java index 7559c770846..44cceca5901 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGenerator.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGenerator.java @@ -389,10 +389,6 @@ private void calculateDefaultTransfers( if (sd.stop == stop) { continue; } - // TODO FIX THIS - This is wrong! This will prune other options, because the test is done too late. - if (sd.stop.transfersNotAllowed()) { - continue; - } createPathTransfer(stop, sd.stop, sd, distinctTransfers, mode); } } diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java index a658cbc0441..ef0a18cd425 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java @@ -1,5 +1,6 @@ package org.opentripplanner.graph_builder.module.transfer.filter; +import java.util.Collection; import java.util.List; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.graph_builder.module.nearbystops.NearbyStopFinder; @@ -42,7 +43,19 @@ public List findNearbyStops( streetRequest, reverseDirection ); + + // Remove transfersNotAllowed stops BEFORE we filter in Pattern and Flex Trips + nearbyStops = removeTransferNotAllowedStops(nearbyStops); + + // Run TripPattern and FlexTrip filters var result = filter.filterToStops(nearbyStops, reverseDirection); + return List.copyOf(result); } + + private static Collection removeTransferNotAllowedStops( + Collection nearbyStops + ) { + return nearbyStops.stream().filter(s -> !s.stop.transfersNotAllowed()).toList(); + } } diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java index 84f24c79ced..ae0d7dd15f5 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java @@ -296,8 +296,8 @@ public void testStreetTransfersWithStationWithTransfersNotAllowed() { .build(); assertEquals( - // TODO Fix: "S0 - S22, 200m" is missing, it is the best transfer after S11 - S21 is droped. """ + S0 - S22, 200m S12 - S22, 110m S13 - S22, 210m""", toString(repository.getAllPathTransfers()) From 37cf8b9c4d92a518af4371067129b3d775cb393b Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 11 Nov 2025 19:04:04 +0100 Subject: [PATCH 11/22] fix: Critical error in StopPattern#canAlight(StopLocation), allways retuned false for the last stop. + Improve documentation and deprecate canAlight(StopLocation) and canBoard(StopLocation) methods --- .../transit/model/network/StopPattern.java | 32 +++++++++++++++---- .../model/network/StopPatternTest.java | 26 +++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java b/application/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java index 116cec822d8..af154c9fbe4 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java +++ b/application/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java @@ -202,12 +202,22 @@ boolean canAlight(int stopPosInPattern) { } /** - * Returns whether passengers can alight at a given stop. This is an inefficient method iterating - * over the stops, do not use it in routing. + * Returns whether passengers can alight at a given stop. + *

+ * WARNING! This is an inefficient method iterating over the stops, do not use it in routing. + *

+ * WARNING! This does not support ring patterns. + *

+ * WARNING! This does not produce the same result as the {@link #canAlight(int)}, + * this method ALWAYS returns {@code false} for the first stop, while the + * other method returns whatever is in the data. This method is probably the + * correct way - but this is not a clear decision. + * @deprecated Avoid using this method! */ + @Deprecated boolean canAlight(StopLocation stop) { - // We skip the last stop, not allowed for boarding - for (int i = 0; i < stops.length - 1; ++i) { + // We skip the first stop, not allowed for alighting + for (int i = 1; i < stops.length; ++i) { if (stop == stops[i] && canAlight(i)) { return true; } @@ -221,9 +231,19 @@ boolean canBoard(int stopPosInPattern) { } /** - * Returns whether passengers can board at a given stop. This is an inefficient method iterating - * over the stops, do not use it in routing. + * Returns whether passengers can board at a given stop. + *

+ * WARNING! This is an inefficient method iterating over the stops, do not use it in routing. + *

+ * WARNING! This does not support ring patterns. + *

+ * WARNING! This does not produce the same result as the {@link #canBoard(int)}, + * this method ALWAYS returns {@code false} for the last stop, while the + * other method returns whatever is in the data. This method is probably the + * correct way - but this is not a clear decision. + * @deprecated Avoid using this method! */ + @Deprecated boolean canBoard(StopLocation stop) { // We skip the last stop, not allowed for boarding for (int i = 0; i < stops.length - 1; ++i) { diff --git a/application/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java b/application/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java index 5b59ba88508..af6403f0052 100644 --- a/application/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java +++ b/application/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java @@ -49,6 +49,32 @@ void boardingAlightingConditions() { assertFalse(stopPattern.canBoard(3), "Forbidden at AreaStop"); } + @Test + void boardingAlightingUsingStopInstance() { + // We have different types of stops, of which only regular stops should allow boarding/alighting + var s1 = testModel.stop("1", 60.0, 11.0).build(); + var s2 = testModel.stop("2", 61.0, 11.0).build(); + var s3 = testModel.stop("3", 62.0, 11.0).build(); + + Trip t = TimetableRepositoryForTest.trip("trip").build(); + + StopPattern stopPattern = new StopPattern( + List.of( + testModel.stopTime(t, 0, s1), + testModel.stopTime(t, 1, s2), + testModel.stopTime(t, 2, s3) + ) + ); + + assertFalse(stopPattern.canAlight(s1), "Alight is not allowed on the first stop!"); + assertTrue(stopPattern.canAlight(s2)); + assertTrue(stopPattern.canAlight(s3)); + + assertTrue(stopPattern.canBoard(s1)); + assertTrue(stopPattern.canBoard(s2)); + assertFalse(stopPattern.canBoard(s3), "Boarding is not allowed on the last stop!"); + } + @Test void replaceStop() { var s1 = testModel.stop("1").build(); From e7be5cb05784511a6cb1f4f589593ddf6f56cf9e Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Wed, 12 Nov 2025 18:32:56 +0100 Subject: [PATCH 12/22] refactor: Simplify DirectTransferGenerator tests and introduce PathTransferToString utility --- .../DirectTransferGeneratorCarTest.java | 696 +++++------------- .../DirectTransferGeneratorTest.drawio.png | Bin 65798 -> 45166 bytes .../transfer/DirectTransferGeneratorTest.java | 265 +------ .../DirectTransferGeneratorTestData.java | 230 ++++++ .../module/transfer/PathTransferToString.java | 24 + 5 files changed, 451 insertions(+), 764 deletions(-) create mode 100644 application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTestData.java create mode 100644 application/src/test/java/org/opentripplanner/graph_builder/module/transfer/PathTransferToString.java diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java index bdde8ae3960..9dd52a83a2a 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java @@ -1,65 +1,19 @@ package org.opentripplanner.graph_builder.module.transfer; -import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import static org.opentripplanner.graph_builder.module.transfer.PathTransferToString.pathToString; import java.time.Duration; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; -import org.opentripplanner.TestOtpModel; -import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.graph_builder.module.TransferParameters; -import org.opentripplanner.model.PathTransfer; -import org.opentripplanner.model.StopTime; import org.opentripplanner.routing.algorithm.GraphRoutingTest; 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.graphfinder.StopResolver; -import org.opentripplanner.street.model.StreetTraversalPermission; -import org.opentripplanner.street.model.edge.Edge; -import org.opentripplanner.street.model.vertex.StreetVertex; -import org.opentripplanner.street.model.vertex.TransitStopVertex; -import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; -import org.opentripplanner.transit.model.basic.TransitMode; -import org.opentripplanner.transit.model.network.BikeAccess; -import org.opentripplanner.transit.model.network.CarAccess; -import org.opentripplanner.transit.model.network.Route; -import org.opentripplanner.transit.model.network.StopPattern; -import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.site.RegularStop; -import org.opentripplanner.transit.model.site.Station; -import org.opentripplanner.transit.model.site.StopLocation; -import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; -import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.utils.tostring.ToStringBuilder; -/** - * This creates a graph with trip patterns -

-  S0 -  V0 ------------
-        |     \       |
- S11 - V11 --------> V21 - S21
-        |      \      |
- S12 - V12 --------> V22 - V22
-        |             |
- S13 - V13 --------> V23 - V23
- 
- */ class DirectTransferGeneratorCarTest extends GraphRoutingTest { - private static final Duration MAX_TRANSFER_DURATION = Duration.ofHours(1); private static final RouteRequest REQUEST_WITH_WALK_TRANSFER = RouteRequest.defaultValue(); private static final RouteRequest REQUEST_WITH_BIKE_TRANSFER = RouteRequest.of() .withJourney(jb -> jb.withTransfer(new StreetRequest(StreetMode.BIKE))) @@ -68,542 +22,220 @@ class DirectTransferGeneratorCarTest extends GraphRoutingTest { .withJourney(jb -> jb.withTransfer(new StreetRequest(StreetMode.CAR))) .buildDefault(); - private TransitStopVertex S0, S11, S12, S13, S21, S22, S23; - private StreetVertex V0, V11, V12, V13, V21, V22, V23; - @Test public void testRequestWithCarsAllowedPatterns() { - var transferRequests = List.of(REQUEST_WITH_CAR_TRANSFER); - - var otpModel = model(false); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - var transferParametersBuilder = new TransferParameters.Builder(); - transferParametersBuilder.withCarsAllowedStopMaxTransferDuration(Duration.ofMinutes(60)); - transferParametersBuilder.withDisableDefaultTransfers(true); - Map transferParametersForMode = new HashMap<>(); - transferParametersForMode.put(StreetMode.CAR, transferParametersBuilder.build()); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests, - transferParametersForMode - ).buildGraph(); - - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - timetableRepository.getAllPathTransfers(), - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 200, List.of(V0, V12), S12) - ); + OTPFeature.ConsiderPatternsForDirectTransfers.testOff(() -> { + var transferParameters = new TransferParameters.Builder() + .withCarsAllowedStopMaxTransferDuration(Duration.ofMinutes(60)) + .withDisableDefaultTransfers(true) + .build(); + + var repository = testDataWithStreetFraphAndPatterns() + .withCarFerrys_FARAWAY_S0_S12_and_S22_S23() + .withTransferRequests(REQUEST_WITH_CAR_TRANSFER) + .addTransferParameters(StreetMode.CAR, transferParameters) + .build(); + + assertEquals( + """ + S0 - S12, 200m + S0 - S22, 200m + S0 - S23, 300m + S12 - S22, 110m + S12 - S23, 210m + S22 - S23, 100m""", + pathToString(repository.getAllPathTransfers()) + ); + }); } @Test public void testRequestWithCarsAllowedPatternsWithDurationLimit() { - var transferRequests = List.of(REQUEST_WITH_CAR_TRANSFER); - - var otpModel = model(false); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - TransferParameters.Builder transferParametersBuilder = new TransferParameters.Builder(); - transferParametersBuilder.withCarsAllowedStopMaxTransferDuration(Duration.ofSeconds(10)); - transferParametersBuilder.withDisableDefaultTransfers(true); - Map transferParametersForMode = new HashMap<>(); - transferParametersForMode.put(StreetMode.CAR, transferParametersBuilder.build()); + var transferParameters = new TransferParameters.Builder() + .withCarsAllowedStopMaxTransferDuration(Duration.ofSeconds(10)) + .withDisableDefaultTransfers(true) + .build(); - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests, - transferParametersForMode - ).buildGraph(); + var repository = testDataWithStreetFraphAndPatterns() + .withCarFerrys_FARAWAY_S0_S12_and_S22_S23() + .withTransferRequests(REQUEST_WITH_CAR_TRANSFER) + .addTransferParameters(StreetMode.CAR, transferParameters) + .build(); - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - timetableRepository.getAllPathTransfers(), - tr(resolver, S0, 100, List.of(V0, V11), S11) + assertEquals( + """ + S12 - S22, 110m""", + pathToString(repository.getAllPathTransfers()) ); } @Test public void testMultipleRequestsWithPatternsAndWithCarsAllowedPatterns() { - var transferRequests = List.of( - REQUEST_WITH_WALK_TRANSFER, - REQUEST_WITH_BIKE_TRANSFER, - REQUEST_WITH_CAR_TRANSFER - ); - - var otpModel = model(true); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - TransferParameters.Builder transferParametersBuilder = new TransferParameters.Builder(); - transferParametersBuilder.withCarsAllowedStopMaxTransferDuration(Duration.ofMinutes(60)); - transferParametersBuilder.withDisableDefaultTransfers(true); - Map transferParametersForMode = new HashMap<>(); - transferParametersForMode.put(StreetMode.CAR, transferParametersBuilder.build()); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests, - transferParametersForMode - ).buildGraph(); + var transferParameters = new TransferParameters.Builder() + .withCarsAllowedStopMaxTransferDuration(Duration.ofMinutes(60)) + .withDisableDefaultTransfers(true) + .build(); - var walkTransfers = timetableRepository.findTransfers(StreetMode.WALK); - var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); - var carTransfers = timetableRepository.findTransfers(StreetMode.CAR); + var repository = testDataWithStreetFraphAndPatterns() + .withCarFerrys_FARAWAY_S0_S12_and_S22_S23() + .withTransferRequests( + REQUEST_WITH_WALK_TRANSFER, + REQUEST_WITH_BIKE_TRANSFER, + REQUEST_WITH_CAR_TRANSFER + ) + .addTransferParameters(StreetMode.CAR, transferParameters) + .build(); - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - walkTransfers, - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S11, 100, List.of(V11, V21), S21), - tr(resolver, S0, 200, List.of(V0, V12), S12), - tr(resolver, S11, 100, List.of(V11, V12), S12) + String expected_walk_bike_results = + """ + S0 - S11, 100m + S0 - S21, 100m + S0 - S22, 200m + S11 - S21, 100m + S11 - S22, 110m + S12 - S22, 110m + S13 - S22, 210m"""; + assertEquals( + expected_walk_bike_results, + pathToString(repository.findTransfers(StreetMode.WALK)) ); - assertTransfers( - bikeTransfers, - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S11, 110, List.of(V11, V22), S22), - tr(resolver, S0, 200, List.of(V0, V12), S12), - tr(resolver, S11, 100, List.of(V11, V12), S12) + assertEquals( + expected_walk_bike_results, + pathToString(repository.findTransfers(StreetMode.BIKE)) ); - assertTransfers( - carTransfers, - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 200, List.of(V0, V12), S12), - tr(resolver, S0, 100, List.of(V0, V21), S21) + assertEquals( + """ + S0 - S22, 200m + S12 - S22, 110m""", + pathToString(repository.findTransfers(StreetMode.CAR)) ); } @Test public void testBikeRequestWithPatternsAndWithCarsAllowedPatterns() { - var transferRequests = List.of(REQUEST_WITH_BIKE_TRANSFER); - - var otpModel = model(true); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - TransferParameters.Builder transferParametersBuilder = new TransferParameters.Builder(); - transferParametersBuilder.withCarsAllowedStopMaxTransferDuration(Duration.ofMinutes(120)); - Map transferParametersForMode = new HashMap<>(); - transferParametersForMode.put(StreetMode.BIKE, transferParametersBuilder.build()); + var transferParameters = new TransferParameters.Builder() + .withCarsAllowedStopMaxTransferDuration(Duration.ofMinutes(120)) + .build(); - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - Duration.ofSeconds(30), - transferRequests, - transferParametersForMode - ).buildGraph(); + var repository = testDataWithStreetFraphAndPatterns() + .withCarFerrys_FARAWAY_S0_S12_and_S22_S23() + .withTransferRequests(REQUEST_WITH_BIKE_TRANSFER) + .addTransferParameters(StreetMode.BIKE, transferParameters) + .build(); - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - timetableRepository.getAllPathTransfers(), - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S0, 200, List.of(V0, V12), S12), - tr(resolver, S11, 110, List.of(V11, V22), S22), - tr(resolver, S11, 100, List.of(V11, V12), S12) + assertEquals( + """ + S0 - S11, 100m + S0 - S21, 100m + S0 - S22, 200m + S11 - S21, 100m + S11 - S22, 110m + S12 - S22, 110m + S13 - S22, 210m""", + pathToString(repository.getAllPathTransfers()) ); } @Test public void testBikeRequestWithPatternsAndWithCarsAllowedPatternsWithoutCarInTransferRequests() { - var transferRequests = List.of(REQUEST_WITH_BIKE_TRANSFER); - - var otpModel = model(true); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - Duration.ofSeconds(30), - transferRequests - ).buildGraph(); + var repository = testDataWithStreetFraphAndPatterns() + .withCarFerrys_FARAWAY_S0_S12_and_S22_S23() + .withMaxTransferDuration(Duration.ofSeconds(30)) + .withTransferRequests(REQUEST_WITH_BIKE_TRANSFER) + .build(); - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - timetableRepository.getAllPathTransfers(), - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S11, 110, List.of(V11, V22), S22) + assertEquals( + """ + S0 - S11, 100m + S0 - S21, 100m + S11 - S21, 100m + S11 - S22, 110m + S12 - S22, 110m""", + pathToString(repository.getAllPathTransfers()) ); } @Test public void testDisableDefaultTransfersForMode() { - var transferRequests = List.of( - REQUEST_WITH_WALK_TRANSFER, - REQUEST_WITH_BIKE_TRANSFER, - REQUEST_WITH_CAR_TRANSFER - ); + var transferParametersBuilderBike = new TransferParameters.Builder() + .withDisableDefaultTransfers(true) + .build(); - var otpModel = model(true); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); + var transferParametersBuilderCar = new TransferParameters.Builder() + .withDisableDefaultTransfers(true) + .build(); - TransferParameters.Builder transferParametersBuilderBike = new TransferParameters.Builder(); - transferParametersBuilderBike.withDisableDefaultTransfers(true); - TransferParameters.Builder transferParametersBuilderCar = new TransferParameters.Builder(); - transferParametersBuilderCar.withDisableDefaultTransfers(true); - Map transferParametersForMode = new HashMap<>(); - transferParametersForMode.put(StreetMode.BIKE, transferParametersBuilderBike.build()); - transferParametersForMode.put(StreetMode.CAR, transferParametersBuilderCar.build()); + var repository = testDataWithStreetFraphAndPatterns() + .withCarFerrys_FARAWAY_S0_S12_and_S22_S23() + .withTransferRequests( + REQUEST_WITH_WALK_TRANSFER, + REQUEST_WITH_BIKE_TRANSFER, + REQUEST_WITH_CAR_TRANSFER + ) + .addTransferParameters(StreetMode.BIKE, transferParametersBuilderBike) + .addTransferParameters(StreetMode.CAR, transferParametersBuilderCar) + .build(); - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests, - transferParametersForMode - ).buildGraph(); + assertEquals( + """ + S0 - S11, 100m + S0 - S21, 100m + S0 - S22, 200m + S11 - S21, 100m + S11 - S22, 110m + S12 - S22, 110m + S13 - S22, 210m""", + pathToString(repository.findTransfers(StreetMode.WALK)) + ); - var walkTransfers = timetableRepository.findTransfers(StreetMode.WALK); - var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); - var carTransfers = timetableRepository.findTransfers(StreetMode.CAR); + assertEquals("", pathToString(repository.findTransfers(StreetMode.BIKE))); - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - walkTransfers, - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S11, 100, List.of(V11, V21), S21), - tr(resolver, S0, 200, List.of(V0, V12), S12), - tr(resolver, S11, 100, List.of(V11, V12), S12) - ); - assertTransfers(bikeTransfers); - assertTransfers(carTransfers); + assertEquals("", pathToString(repository.findTransfers(StreetMode.CAR))); } @Test public void testMaxTransferDurationForMode() { - var transferRequests = List.of(REQUEST_WITH_WALK_TRANSFER, REQUEST_WITH_BIKE_TRANSFER); - - var otpModel = model(true); - var graph = otpModel.graph(); - graph.hasStreets = true; - var timetableRepository = otpModel.timetableRepository(); - - TransferParameters.Builder transferParametersBuilderWalk = new TransferParameters.Builder(); - transferParametersBuilderWalk.withMaxTransferDuration(Duration.ofSeconds(100)); - TransferParameters.Builder transferParametersBuilderBike = new TransferParameters.Builder(); - transferParametersBuilderBike.withMaxTransferDuration(Duration.ofSeconds(21)); - Map transferParametersForMode = new HashMap<>(); - transferParametersForMode.put(StreetMode.WALK, transferParametersBuilderWalk.build()); - transferParametersForMode.put(StreetMode.BIKE, transferParametersBuilderBike.build()); - - new DirectTransferGenerator( - graph, - timetableRepository, - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests, - transferParametersForMode - ).buildGraph(); - - var walkTransfers = timetableRepository.findTransfers(StreetMode.WALK); - var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); - var carTransfers = timetableRepository.findTransfers(StreetMode.CAR); - - StopResolver resolver = timetableRepository.getSiteRepository()::getRegularStop; - assertTransfers( - walkTransfers, - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21), - tr(resolver, S11, 100, List.of(V11, V21), S21), - tr(resolver, S11, 100, List.of(V11, V12), S12) - ); - assertTransfers( - bikeTransfers, - tr(resolver, S0, 100, List.of(V0, V11), S11), - tr(resolver, S0, 100, List.of(V0, V21), S21) - ); - assertTransfers(carTransfers); - } - - private TestOtpModel model(boolean addPatterns) { - return modelOf( - new Builder() { - @Override - public void build() { - var station = stationEntity("1", s -> {}); - - S0 = stop("S0", b -> - b - .withCoordinate(47.495, 19.001) - .withParentStation(station) - .withVehicleType(TransitMode.RAIL) - ); - S11 = stop("S11", 47.500, 19.001, station); - S12 = stop("S12", 47.520, 19.001, station); - S13 = stop("S13", 47.540, 19.001, station); - S21 = stop("S21", 47.500, 19.011, station); - S22 = stop("S22", 47.520, 19.011, station); - S23 = stop("S23", 47.540, 19.011, station); - - V0 = intersection("V0", 47.495, 19.000); - V11 = intersection("V11", 47.500, 19.000); - V12 = intersection("V12", 47.510, 19.000); - V13 = intersection("V13", 47.520, 19.000); - V21 = intersection("V21", 47.500, 19.010); - V22 = intersection("V22", 47.510, 19.010); - V23 = intersection("V23", 47.520, 19.010); - - biLink(V0, S0); - biLink(V11, S11); - biLink(V12, S12); - biLink(V13, S13); - biLink(V21, S21); - biLink(V22, S22); - biLink(V23, S23); - - street(V0, V11, 100, StreetTraversalPermission.ALL); - street(V0, V12, 200, StreetTraversalPermission.ALL); - street(V0, V21, 100, StreetTraversalPermission.ALL); - street(V0, V22, 200, StreetTraversalPermission.ALL); - - street(V11, V12, 100, StreetTraversalPermission.PEDESTRIAN); - street(V12, V13, 100, StreetTraversalPermission.PEDESTRIAN); - street(V21, V22, 100, StreetTraversalPermission.PEDESTRIAN); - street(V22, V23, 100, StreetTraversalPermission.PEDESTRIAN); - street(V11, V21, 100, StreetTraversalPermission.PEDESTRIAN); - street(V11, V22, 110, StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE); - - if (addPatterns) { - var agency = TimetableRepositoryForTest.agency("Agency"); - - tripPattern( - TripPattern.of(TimetableRepositoryForTest.id("TP1")) - .withRoute(route("R1", TransitMode.BUS, agency)) - .withStopPattern(new StopPattern(List.of(st(S11, true, true), st(S12), st(S13)))) - .build() - ); - - tripPattern( - createTripPattern( - "TP2", - route("R2", TransitMode.BUS, agency), - List.of(st(S21), st(S22), st(S23)), - createBikesAllowedTrip(), - "00:00 01:00 02:00" - ) - ); - } - - var agency = TimetableRepositoryForTest.agency("FerryAgency"); - - tripPattern( - createTripPattern( - "TP3", - route("R3", TransitMode.FERRY, agency), - List.of(st(S11), st(S21)), - createCarsAllowedTrip(), - "00:00 01:00" - ) - ); - - tripPattern( - createTripPattern( - "TP4", - route("R4", TransitMode.FERRY, agency), - List.of(st(S0), st(S13)), - createCarsAllowedTrip(), - "00:00 01:00" - ) - ); - - tripPattern( - createTripPattern( - "TP5", - route("R5", TransitMode.FERRY, agency), - List.of(st(S12), st(S22)), - createCarsAllowedTrip(), - "00:00 01:00" - ) - ); - } - - private TransitStopVertex stop(String id, double lat, double lon, Station parentStation) { - return stop(id, b -> b.withCoordinate(lat, lon).withParentStation(parentStation)); - } - } - ); - } - - private static TripPattern createTripPattern( - String id, - Route route, - List times, - Trip trip, - String schedule - ) { - return TripPattern.of(TimetableRepositoryForTest.id(id)) - .withRoute(route) - .withStopPattern(new StopPattern(times)) - .withScheduledTimeTableBuilder(builder -> - builder.addTripTimes( - ScheduledTripTimes.of().withTrip(trip).withDepartureTimes(schedule).build() - ) - ) + var transferParametersBuilderWalk = new TransferParameters.Builder() + .withMaxTransferDuration(Duration.ofSeconds(100)) .build(); - } - - private static Trip createBikesAllowedTrip() { - return TimetableRepositoryForTest.trip("bikesAllowedTrip") - .withBikesAllowed(BikeAccess.ALLOWED) + var transferParametersBuilderBike = new TransferParameters.Builder() + .withMaxTransferDuration(Duration.ofSeconds(21)) .build(); - } - private static Trip createCarsAllowedTrip() { - return TimetableRepositoryForTest.trip("carsAllowedTrip") - .withCarsAllowed(CarAccess.ALLOWED) + var repository = DirectTransferGeneratorTestData.of() + .withPatterns() + .withStreetGraph() + .withCarFerrys_FARAWAY_S0_S12_and_S22_S23() + .withTransferRequests(REQUEST_WITH_WALK_TRANSFER, REQUEST_WITH_BIKE_TRANSFER) + .addTransferParameters(StreetMode.WALK, transferParametersBuilderWalk) + .addTransferParameters(StreetMode.BIKE, transferParametersBuilderBike) .build(); - } - private void assertTransfers( - Collection allPathTransfers, - TransferDescriptor... transfers - ) { - var matchedTransfers = new HashSet(); - var assertions = Stream.concat( - Arrays.stream(transfers).map(td -> td.matcher(allPathTransfers, matchedTransfers)), - Stream.of(allTransfersMatched(allPathTransfers, matchedTransfers)) + assertEquals( + """ + S0 - S11, 100m + S0 - S21, 100m + S11 - S21, 100m + S11 - S22, 110m + S12 - S22, 110m""", + pathToString(repository.findTransfers(StreetMode.WALK)) ); - - assertAll(assertions); - } - - private Executable allTransfersMatched( - Collection transfersByStop, - Set matchedTransfers - ) { - return () -> { - var missingTransfers = new HashSet<>(transfersByStop); - missingTransfers.removeAll(matchedTransfers); - - assertEquals(Set.of(), missingTransfers, "All transfers matched"); - }; - } - - private TransferDescriptor tr( - StopResolver resolver, - TransitStopVertex from, - double distance, - TransitStopVertex to - ) { - return new TransferDescriptor( - resolver.getStop(from.getId()), - distance, - resolver.getStop(to.getId()) + assertEquals( + """ + S0 - S11, 100m + S0 - S21, 100m + S11 - S21, 100m""", + pathToString(repository.findTransfers(StreetMode.BIKE)) ); + assertEquals("", pathToString(repository.findTransfers(StreetMode.CAR))); } - private TransferDescriptor tr( - StopResolver resolver, - TransitStopVertex from, - double distance, - List vertices, - TransitStopVertex to - ) { - return new TransferDescriptor(resolver, from, distance, vertices, to); - } - - private static class TransferDescriptor { - - private final StopLocation from; - private final StopLocation to; - private final Double distanceMeters; - private final List vertices; - - public TransferDescriptor(RegularStop from, Double distanceMeters, RegularStop to) { - this.from = from; - this.distanceMeters = distanceMeters; - this.vertices = null; - this.to = to; - } - - public TransferDescriptor( - StopResolver resolver, - TransitStopVertex from, - Double distanceMeters, - List vertices, - TransitStopVertex to - ) { - this.from = resolver.getStop(from.getId()); - this.distanceMeters = distanceMeters; - this.vertices = vertices; - this.to = resolver.getStop(to.getId()); - } - - @Override - public String toString() { - return ToStringBuilder.of(getClass()) - .addObj("from", from) - .addObj("to", to) - .addNum("distanceMeters", distanceMeters) - .addCol("vertices", vertices) - .toString(); - } - - boolean matches(PathTransfer transfer) { - if (!Objects.equals(from, transfer.from) || !Objects.equals(to, transfer.to)) { - return false; - } - - if (vertices == null) { - return distanceMeters == transfer.getDistanceMeters() && transfer.getEdges() == null; - } else { - var transferVertices = transfer - .getEdges() - .stream() - .map(Edge::getToVertex) - .filter(StreetVertex.class::isInstance) - .toList(); - - return ( - distanceMeters == transfer.getDistanceMeters() && - Objects.equals(vertices, transferVertices) - ); - } - } - - private Executable matcher( - Collection transfersByStop, - Set matchedTransfers - ) { - return () -> { - var matched = transfersByStop.stream().filter(this::matches).findFirst(); - - if (matched.isPresent()) { - assertTrue(true, "Found transfer for " + this); - matchedTransfers.add(matched.get()); - } else { - fail("Missing transfer for " + this); - } - }; - } + /** + * Testing Car Transfer Generation should be done with the street-graph and using the patterns + * for "tagging" stops with CAR_ALLOWED. Using "line-of-sight" street routing does not add much + * to this test. It is covered by the {@link DirectTransferGeneratorTest}. + */ + private static DirectTransferGeneratorTestData testDataWithStreetFraphAndPatterns() { + return DirectTransferGeneratorTestData.of().withPatterns().withStreetGraph(); } } diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.drawio.png b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.drawio.png index f921b0148d557cd30822b18f00b7b728e7402c1e..d1ce61f0d262f8b1d8f793ab797db64da45355f0 100644 GIT binary patch literal 45166 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

2Y$$0$6!f3|7 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 ZEiUg5J{I_v0DgjXOG)!ap@LcP{{lvxY`Fjc literal 65798 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 MVlprDpKE#le_(2eD*ylh diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java index ae0d7dd15f5..08789cbd6a8 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java @@ -1,35 +1,15 @@ package org.opentripplanner.graph_builder.module.transfer; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.graph_builder.module.transfer.PathTransferToString.pathToString; import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.opentripplanner.framework.application.OTPFeature; -import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.graph_builder.module.TransferParameters; -import org.opentripplanner.model.PathTransfer; -import org.opentripplanner.routing.algorithm.GraphRoutingTest; 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.street.model.StreetTraversalPermission; -import org.opentripplanner.street.model.vertex.StreetVertex; -import org.opentripplanner.street.model.vertex.TransitStopVertex; -import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; -import org.opentripplanner.transit.model.basic.TransitMode; -import org.opentripplanner.transit.model.network.BikeAccess; -import org.opentripplanner.transit.model.network.StopPattern; -import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.site.Station; -import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; -import org.opentripplanner.transit.service.TimetableRepository; /** * This test uses the following graph/network for testing the DirectTransfer generation. The @@ -40,7 +20,6 @@ */ class DirectTransferGeneratorTest { - private static final Duration MAX_TRANSFER_DURATION = Duration.ofHours(1); private static final RouteRequest REQUEST_WITH_WALK_TRANSFER = RouteRequest.defaultValue(); private static final RouteRequest REQUEST_WITH_BIKE_TRANSFER = RouteRequest.of() .withJourney(jb -> jb.withTransfer(new StreetRequest(StreetMode.BIKE))) @@ -55,14 +34,18 @@ class DirectTransferGeneratorTest { @Test public void testStraightLineTransfersWithNoPatterns() { // There is no trip-patterns to transfer between; Hence empty. - var repository = testData().withTransferRequests(REQUEST_WITH_WALK_TRANSFER).build(); - assertEquals("", toString(repository.getAllPathTransfers())); + var repository = DirectTransferGeneratorTestData.of() + .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) + .build(); + assertEquals("", pathToString(repository.getAllPathTransfers())); } @Test public void testStraightLineTransfersWithoutPatternsPruning() { OTPFeature.ConsiderPatternsForDirectTransfers.testOff(() -> { - var repository = testData().withTransferRequests(REQUEST_WITH_WALK_TRANSFER).build(); + var repository = DirectTransferGeneratorTestData.of() + .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) + .build(); // S0 <-> S23 is too fare, not found in neardby search assertEquals( """ @@ -104,14 +87,14 @@ public void testStraightLineTransfersWithoutPatternsPruning() { S23 - S13, 751m S23 - S21, 4448m S23 - S22, 2224m""", - toString(repository.getAllPathTransfers()) + pathToString(repository.getAllPathTransfers()) ); }); } @Test public void testStraightLineTransfersWithPatternsPruning() { - var repository = testData() + var repository = DirectTransferGeneratorTestData.of() .withPatterns() .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) .build(); @@ -135,13 +118,13 @@ public void testStraightLineTransfersWithPatternsPruning() { S22 - S11, 2347m S23 - S11, 4511m S23 - S22, 2224m""", - toString(repository.getAllPathTransfers()) + pathToString(repository.getAllPathTransfers()) ); } @Test public void testStraightLineTransfersWithBoardingRestrictions() { - var repository = testData() + var repository = DirectTransferGeneratorTestData.of() .withPatterns() .withNoBoardingForR1AtStop11() .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) @@ -159,25 +142,25 @@ public void testStraightLineTransfersWithBoardingRestrictions() { S21 - S0, 1829m S22 - S0, 3964m S23 - S22, 2224m""", - toString(repository.getAllPathTransfers()) + pathToString(repository.getAllPathTransfers()) ); } @Test public void testStreetTransfersWithNoPatterns() { // There is no trip-patterns to transfer between; Hence empty. - var repository = testData() + var repository = DirectTransferGeneratorTestData.of() .withStreetGraph() .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) .build(); - assertEquals("", toString(repository.getAllPathTransfers())); + assertEquals("", pathToString(repository.getAllPathTransfers())); } @Test public void testStreetTransfersWithoutPatternsPruning() { OTPFeature.ConsiderPatternsForDirectTransfers.testOff(() -> { - var repository = testData() + var repository = DirectTransferGeneratorTestData.of() .withStreetGraph() .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) .build(); @@ -199,14 +182,14 @@ public void testStreetTransfersWithoutPatternsPruning() { S13 - S22, 210m S13 - S23, 310m S22 - S23, 100m""", - toString(repository.getAllPathTransfers()) + pathToString(repository.getAllPathTransfers()) ); }); } @Test public void testStreetTransfersWithPatterns() { - var repository = testData() + var repository = DirectTransferGeneratorTestData.of() .withPatterns() .withStreetGraph() .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) @@ -219,14 +202,14 @@ public void testStreetTransfersWithPatterns() { S11 - S21, 100m S12 - S22, 110m S13 - S22, 210m""", - toString(repository.getAllPathTransfers()) + pathToString(repository.getAllPathTransfers()) ); } @Test public void testStreetTransfersWithPatternsIncludeRealTimeUsedStops() { OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { - var repository = testData() + var repository = DirectTransferGeneratorTestData.of() .withPatterns() .withStreetGraph() .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) @@ -244,14 +227,14 @@ public void testStreetTransfersWithPatternsIncludeRealTimeUsedStops() { S13 - S22, 210m S13 - S23, 310m S22 - S23, 100m""", - toString(repository.getAllPathTransfers()) + pathToString(repository.getAllPathTransfers()) ); }); } @Test public void testStreetTransfersWithMultipleRequestsWithPatterns() { - var repository = testData() + var repository = DirectTransferGeneratorTestData.of() .withPatterns() .withStreetGraph() .withTransferRequests(REQUEST_WITH_WALK_TRANSFER, REQUEST_WITH_BIKE_TRANSFER) @@ -261,34 +244,21 @@ public void testStreetTransfersWithMultipleRequestsWithPatterns() { var bikeTransfers = repository.findTransfers(StreetMode.BIKE); var carTransfers = repository.findTransfers(StreetMode.CAR); - assertEquals( + String expected = """ S0 - S11, 100m S0 - S21, 100m S11 - S21, 100m S12 - S22, 110m - S13 - S22, 210m""", - toString(walkTransfers) - ); - - // Transfer S11-S21 is dominated by the S11-S22 with lower cost; Hence missing. Some of the - // edges used are not allowed for bicycles, but you can walk the bike so they are included here - // with a higher cost. - assertEquals( - """ - S0 - S11, 100m - S0 - S21, 100m - S11 - S22, 110m - S12 - S22, 110m - S13 - S22, 210m""", - toString(bikeTransfers) - ); - assertEquals("", toString(carTransfers)); + S13 - S22, 210m"""; + assertEquals(expected, pathToString(walkTransfers)); + assertEquals(expected, pathToString(bikeTransfers)); + assertEquals("", pathToString(carTransfers)); } @Test public void testStreetTransfersWithStationWithTransfersNotAllowed() { - var repository = testData() + var repository = DirectTransferGeneratorTestData.of() .withPatterns() .withStreetGraph() .withNoTransfersOnStationA() @@ -300,14 +270,14 @@ public void testStreetTransfersWithStationWithTransfersNotAllowed() { S0 - S22, 200m S12 - S22, 110m S13 - S22, 210m""", - toString(repository.getAllPathTransfers()) + pathToString(repository.getAllPathTransfers()) ); } @Test public void testBikeRequestWithBikesAllowedTransfersWithIncludeEmptyRailStopsInTransfersOn() { OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { - var repository = testData() + var repository = DirectTransferGeneratorTestData.of() .withPatterns() .withStreetGraph() .withTransferRequests(REQUEST_WITH_BIKE_TRANSFER) @@ -320,7 +290,7 @@ public void testBikeRequestWithBikesAllowedTransfersWithIncludeEmptyRailStopsInT S13 - S22, 210m S13 - S23, 310m S22 - S23, 100m""", - toString(bikeTransfers) + pathToString(bikeTransfers) ); }); } @@ -328,7 +298,7 @@ public void testBikeRequestWithBikesAllowedTransfersWithIncludeEmptyRailStopsInT @Test public void testBikeRequestWithBikesAllowedTransfersWithConsiderPatternsForDirectTransfersOff() { OTPFeature.ConsiderPatternsForDirectTransfers.testOff(() -> { - var repository = testData() + var repository = DirectTransferGeneratorTestData.of() .withPatterns() .withStreetGraph() .withTransferRequests(REQUEST_WITH_BIKE_TRANSFER) @@ -341,177 +311,8 @@ public void testBikeRequestWithBikesAllowedTransfersWithConsiderPatternsForDirec S13 - S22, 210m S13 - S23, 310m S22 - S23, 100m""", - toString(bikeTransfers) + pathToString(bikeTransfers) ); }); } - - private String toString(Collection transfers) { - if (transfers.isEmpty()) { - return ""; - } - return transfers - .stream() - .map(tx -> - "%3s - %3s, %dm".formatted( - tx.from.getName(), - tx.to.getName(), - Math.round(tx.getDistanceMeters()) - ) - ) - .sorted() - .collect(Collectors.joining("\n")); - } - - private TestData testData() { - return new TestData(); - } - - private static class TestData extends GraphRoutingTest { - - private boolean addPatterns = false; - private boolean withBoardingConstraint = false; - private boolean noTransfersOnStationA = false; - private boolean graphHasStreets = false; - private final List transferRequests = new ArrayList<>(); - private final Map transferParametersForMode = new HashMap<>(); - - public TestData withPatterns() { - this.addPatterns = true; - return this; - } - - public TestData withNoBoardingForR1AtStop11() { - this.withBoardingConstraint = true; - return this; - } - - public TestData withNoTransfersOnStationA() { - this.noTransfersOnStationA = true; - return this; - } - - public TestData withStreetGraph() { - this.graphHasStreets = true; - return this; - } - - public TestData withTransferRequests(RouteRequest... request) { - this.transferRequests.addAll(Arrays.asList(request)); - return this; - } - - public TestData addTransferParameters(StreetMode mode, TransferParameters value) { - this.transferParametersForMode.put(mode, value); - return this; - } - - public TimetableRepository build() { - var model = modelOf(new Builder()); - model.graph().hasStreets = graphHasStreets; - - new DirectTransferGenerator( - model.graph(), - model.timetableRepository(), - DataImportIssueStore.NOOP, - MAX_TRANSFER_DURATION, - transferRequests, - transferParametersForMode - ).buildGraph(); - - return model.timetableRepository(); - } - - private class Builder extends GraphRoutingTest.Builder { - - @Override - public void build() { - var stationA = stationEntity("1", s -> s.withTransfersNotAllowed(noTransfersOnStationA)); - TransitStopVertex S0, S_FAR_AWAY, S11, S12, S13, S21, S22, S23; - StreetVertex V0, V11, V12, V13, V21, V22, V23; - - S0 = stop("S0", b -> b.withCoordinate(47.485, 19.001).withVehicleType(TransitMode.RAIL)); - S_FAR_AWAY = stop("FarAway", 55.0, 30.0); - S11 = stop("S11", 47.500, 19.001, stationA); - S12 = stop("S12", 47.520, 19.001); - S13 = stop("S13", b -> b.withCoordinate(47.540, 19.001).withSometimesUsedRealtime(true)); - S21 = stop("S21", 47.500, 19.011, stationA); - S22 = stop("S22", b -> - b - .withCoordinate(47.520, 19.011) - .withVehicleType(TransitMode.BUS) - .withSometimesUsedRealtime(true) - ); - S23 = stop("S23", b -> b.withCoordinate(47.540, 19.011).withSometimesUsedRealtime(true)); - - V0 = intersection("V0", 47.485, 19.000); - V11 = intersection("V11", 47.500, 19.000); - V12 = intersection("V12", 47.520, 19.000); - V13 = intersection("V13", 47.540, 19.000); - V21 = intersection("V21", 47.500, 19.010); - V22 = intersection("V22", 47.520, 19.010); - V23 = intersection("V23", 47.540, 19.010); - - biLink(V0, S0); - biLink(V11, S11); - biLink(V12, S12); - biLink(V13, S13); - biLink(V21, S21); - biLink(V22, S22); - biLink(V23, S23); - - street(V0, V11, 100, StreetTraversalPermission.ALL); - street(V0, V21, 100, StreetTraversalPermission.ALL); - street(V0, V22, 200, StreetTraversalPermission.ALL); - - street(V11, V12, 100, StreetTraversalPermission.PEDESTRIAN); - street(V11, V21, 100, StreetTraversalPermission.PEDESTRIAN); - street(V11, V22, 110, StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE); - street(V12, V22, 110, StreetTraversalPermission.PEDESTRIAN); - street(V13, V12, 100, StreetTraversalPermission.PEDESTRIAN); - street(V22, V23, 100, StreetTraversalPermission.PEDESTRIAN); - - if (addPatterns) { - var agency = TimetableRepositoryForTest.agency("Agency"); - - tripPattern( - TripPattern.of(TimetableRepositoryForTest.id("TP0")) - .withRoute(route("R0", TransitMode.RAIL, agency)) - .withStopPattern(new StopPattern(List.of(st(S0), st(S_FAR_AWAY)))) - .build() - ); - tripPattern( - TripPattern.of(TimetableRepositoryForTest.id("TP1")) - .withRoute(route("R1", TransitMode.BUS, agency)) - .withStopPattern( - new StopPattern(List.of(st(S11, !withBoardingConstraint, true), st(S12))) - ) - .build() - ); - tripPattern( - TripPattern.of(TimetableRepositoryForTest.id("TP2")) - .withRoute(route("R2", TransitMode.BUS, agency)) - .withStopPattern(new StopPattern(List.of(st(S21), st(S22), st(S_FAR_AWAY)))) - .withScheduledTimeTableBuilder(builder -> - builder.addTripTimes( - ScheduledTripTimes.of() - .withTrip( - TimetableRepositoryForTest.trip("bikesAllowedTrip") - .withBikesAllowed(BikeAccess.ALLOWED) - .build() - ) - .withDepartureTimes("00:00 01:00 02:00") - .build() - ) - ) - .build() - ); - } - } - - private TransitStopVertex stop(String id, double lat, double lon, Station parentStation) { - return stop(id, b -> b.withCoordinate(lat, lon).withParentStation(parentStation)); - } - } - } } diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTestData.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTestData.java new file mode 100644 index 00000000000..559393b6311 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTestData.java @@ -0,0 +1,230 @@ +package org.opentripplanner.graph_builder.module.transfer; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.graph_builder.module.TransferParameters; +import org.opentripplanner.routing.algorithm.GraphRoutingTest; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.routing.api.request.StreetMode; +import org.opentripplanner.street.model.StreetTraversalPermission; +import org.opentripplanner.street.model.vertex.StreetVertex; +import org.opentripplanner.street.model.vertex.TransitStopVertex; +import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.network.BikeAccess; +import org.opentripplanner.transit.model.network.CarAccess; +import org.opentripplanner.transit.model.network.StopPattern; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.Station; +import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; +import org.opentripplanner.transit.service.TimetableRepository; + +/** + * + */ +class DirectTransferGeneratorTestData extends GraphRoutingTest { + + private static final Duration MAX_TRANSFER_DURATION = Duration.ofHours(1); + + private boolean addPatterns = false; + private boolean withBoardingConstraint = false; + private boolean noTransfersOnStationA = false; + private boolean graphHasStreets = false; + private boolean includeCarFerryTrips = false; + private Duration maxTransferDuration = MAX_TRANSFER_DURATION; + private final List transferRequests = new ArrayList<>(); + private final Map transferParametersForMode = new HashMap<>(); + + public DirectTransferGeneratorTestData withPatterns() { + this.addPatterns = true; + return this; + } + + public DirectTransferGeneratorTestData withNoBoardingForR1AtStop11() { + this.withBoardingConstraint = true; + return this; + } + + public DirectTransferGeneratorTestData withNoTransfersOnStationA() { + this.noTransfersOnStationA = true; + return this; + } + + public DirectTransferGeneratorTestData withStreetGraph() { + this.graphHasStreets = true; + return this; + } + + public DirectTransferGeneratorTestData withCarFerrys_FARAWAY_S0_S12_and_S22_S23() { + this.includeCarFerryTrips = true; + return this; + } + + public DirectTransferGeneratorTestData withMaxTransferDuration(Duration value) { + this.maxTransferDuration = value; + return this; + } + + public DirectTransferGeneratorTestData withTransferRequests(RouteRequest... request) { + this.transferRequests.addAll(Arrays.asList(request)); + return this; + } + + public DirectTransferGeneratorTestData addTransferParameters( + StreetMode mode, + TransferParameters value + ) { + this.transferParametersForMode.put(mode, value); + return this; + } + + public TimetableRepository build() { + var model = modelOf(new Builder()); + model.graph().hasStreets = graphHasStreets; + + new DirectTransferGenerator( + model.graph(), + model.timetableRepository(), + DataImportIssueStore.NOOP, + maxTransferDuration, + transferRequests, + transferParametersForMode + ).buildGraph(); + + return model.timetableRepository(); + } + + static DirectTransferGeneratorTestData of() { + return new DirectTransferGeneratorTestData(); + } + + private class Builder extends GraphRoutingTest.Builder { + + @Override + public void build() { + var stationA = stationEntity("1", s -> s.withTransfersNotAllowed(noTransfersOnStationA)); + TransitStopVertex S0, S_FAR_AWAY, S11, S12, S13, S21, S22, S23; + StreetVertex V0, V11, V12, V13, V21, V22, V23; + + S0 = stop("S0", b -> b.withCoordinate(47.485, 19.001).withVehicleType(TransitMode.RAIL)); + S_FAR_AWAY = stop("FarAway", 55.0, 30.0); + S11 = stop("S11", 47.500, 19.001, stationA); + S12 = stop("S12", 47.520, 19.001); + S13 = stop("S13", b -> b.withCoordinate(47.540, 19.001).withSometimesUsedRealtime(true)); + S21 = stop("S21", 47.500, 19.011, stationA); + S22 = stop("S22", b -> + b + .withCoordinate(47.520, 19.011) + .withVehicleType(TransitMode.BUS) + .withSometimesUsedRealtime(true) + ); + S23 = stop("S23", b -> b.withCoordinate(47.540, 19.011).withSometimesUsedRealtime(true)); + + V0 = intersection("V0", 47.485, 19.000); + V11 = intersection("V11", 47.500, 19.000); + V12 = intersection("V12", 47.520, 19.000); + V13 = intersection("V13", 47.540, 19.000); + V21 = intersection("V21", 47.500, 19.010); + V22 = intersection("V22", 47.520, 19.010); + V23 = intersection("V23", 47.540, 19.010); + + biLink(V0, S0); + biLink(V11, S11); + biLink(V12, S12); + biLink(V13, S13); + biLink(V21, S21); + biLink(V22, S22); + biLink(V23, S23); + + // The street routing is not under test, so no need to restrict + // StreetTraversalPermission - this only complicates the tests. + street(V0, V11, 100, StreetTraversalPermission.ALL); + street(V0, V21, 100, StreetTraversalPermission.ALL); + street(V0, V22, 200, StreetTraversalPermission.ALL); + + street(V11, V12, 100, StreetTraversalPermission.ALL); + street(V11, V21, 100, StreetTraversalPermission.ALL); + street(V11, V22, 110, StreetTraversalPermission.ALL); + street(V12, V22, 110, StreetTraversalPermission.ALL); + street(V13, V12, 100, StreetTraversalPermission.ALL); + street(V22, V23, 100, StreetTraversalPermission.ALL); + + if (addPatterns) { + var agency = TimetableRepositoryForTest.agency("Agency"); + + tripPattern( + TripPattern.of(TimetableRepositoryForTest.id("TP0")) + .withRoute(route("R0", TransitMode.RAIL, agency)) + .withStopPattern(new StopPattern(List.of(st(S0), st(S_FAR_AWAY)))) + .build() + ); + tripPattern( + TripPattern.of(TimetableRepositoryForTest.id("TP1")) + .withRoute(route("R1", TransitMode.BUS, agency)) + .withStopPattern( + new StopPattern(List.of(st(S11, !withBoardingConstraint, true), st(S12))) + ) + .build() + ); + tripPattern( + TripPattern.of(TimetableRepositoryForTest.id("TP2")) + .withRoute(route("R2", TransitMode.BUS, agency)) + .withStopPattern(new StopPattern(List.of(st(S21), st(S22), st(S_FAR_AWAY)))) + .withScheduledTimeTableBuilder(builder -> + builder.addTripTimes( + ScheduledTripTimes.of() + .withTrip( + TimetableRepositoryForTest.trip("bikesAllowedTrip") + .withBikesAllowed(BikeAccess.ALLOWED) + .build() + ) + .withDepartureTimes("00:00 01:00 02:00") + .build() + ) + ) + .build() + ); + + if (includeCarFerryTrips) { + tripPattern( + TripPattern.of(TimetableRepositoryForTest.id("TP4")) + .withRoute(route("R4", TransitMode.FERRY, agency)) + .withStopPattern( + new StopPattern( List.of(st(S_FAR_AWAY),st(S0), st(S12))) + ) + .withScheduledTimeTableBuilder(b -> b.addTripTimes(createCarsAllowedTripTimesWithTwoStops())) + .build() + ); + tripPattern( + TripPattern.of(TimetableRepositoryForTest.id("TP5")) + .withRoute(route("R5", TransitMode.FERRY, agency)) + .withStopPattern(new StopPattern(List.of(st(S22), st(S23)))) + .withScheduledTimeTableBuilder(b -> b.addTripTimes(createCarsAllowedTripTimesWithTwoStops()) + ) + .build() + ); + } + } + } + + private static ScheduledTripTimes createCarsAllowedTripTimesWithTwoStops() { + return ScheduledTripTimes.of() + .withTrip( + TimetableRepositoryForTest.trip("carsAllowed") + .withCarsAllowed(CarAccess.ALLOWED) + .build() + ) + .withDepartureTimes("00:00 01:00") + .build(); + } + + private TransitStopVertex stop(String id, double lat, double lon, Station parentStation) { + return stop(id, b -> b.withCoordinate(lat, lon).withParentStation(parentStation)); + } + } +} diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/PathTransferToString.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/PathTransferToString.java new file mode 100644 index 00000000000..0d712029a66 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/PathTransferToString.java @@ -0,0 +1,24 @@ +package org.opentripplanner.graph_builder.module.transfer; + +import java.util.Collection; +import java.util.stream.Collectors; +import org.opentripplanner.model.PathTransfer; + +public class PathTransferToString { + static String pathToString(Collection transfers) { + if (transfers.isEmpty()) { + return ""; + } + return transfers + .stream() + .map(tx -> + "%3s - %3s, %dm".formatted( + tx.from.getName(), + tx.to.getName(), + Math.round(tx.getDistanceMeters()) + ) + ) + .sorted() + .collect(Collectors.joining("\n")); + } +} From 97007895f1e143f0196abb59f5194a88970d24ac Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 11 Nov 2025 20:26:23 +0100 Subject: [PATCH 13/22] refactor: Sort OTPFeature alphabetically --- .../framework/application/OTPFeature.java | 32 +++++++++---------- doc/user/Configuration.md | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java b/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java index 2c0536e3cd3..b4123f442c1 100644 --- a/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java +++ b/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java @@ -22,22 +22,6 @@ public enum OTPFeature { ), APIServerInfo(true, false, "Enable the server info endpoint."), APIUpdaterStatus(true, false, "Enable endpoint for graph updaters status."), - IncludeStopsUsedRealtimeInTransfers( - false, - false, - """ - When generating transfers, stops without any patterns are excluded to improve performance if - `ConsiderPatternsForDirectTransfers` is enabled. However, some stops are only used by trips - changed or added by real-time updates. Since transfer generation happens before real-time - updates are applied, OTP cannot know which stops will be needed. Instead, OTP will attempt to - identify stops likely to be used by real-time updates at import time. Common cases include rail - stops (which often have late platform assignments) and stops reserved for replacement services - (which can be detected in NeTEx by examining the stop sub-mode). This feature has no effect if - `ConsiderPatternsForDirectTransfers` is disabled. - - This feature is only supported for NeTEx feeds, not for GTFS feeds. - """ - ), ConsiderPatternsForDirectTransfers( true, false, @@ -59,6 +43,22 @@ public enum OTPFeature { ), FloatingBike(true, false, "Enable floating bike routing."), GtfsGraphQlApi(true, false, "Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md)."), + IncludeStopsUsedRealtimeInTransfers( + false, + false, + """ + When generating transfers, stops without any patterns are excluded to improve performance if + `ConsiderPatternsForDirectTransfers` is enabled. However, some stops are only used by trips + changed or added by real-time updates. Since transfer generation happens before real-time + updates are applied, OTP cannot know which stops will be needed. Instead, OTP will attempt to + identify stops likely to be used by real-time updates at import time. Common cases include rail + stops (which often have late platform assignments) and stops reserved for replacement services + (which can be detected in NeTEx by examining the stop sub-mode). This feature has no effect if + `ConsiderPatternsForDirectTransfers` is disabled. + + This feature is only supported for NeTEx feeds, not for GTFS feeds. + """ + ), /** * If this feature flag is switched on, then the minimum transfer time is not the minimum transfer * time, but the definitive transfer time. Use this to override what we think the transfer will diff --git a/doc/user/Configuration.md b/doc/user/Configuration.md index 13c7e684fe3..76aaa529e8f 100644 --- a/doc/user/Configuration.md +++ b/doc/user/Configuration.md @@ -227,12 +227,12 @@ Here is a list of all features which can be toggled on/off and their default val | `AlertMetrics` | Starts a background thread to continuously publish metrics about alerts. Needs to be enabled together with `ActuatorAPI`. | | | | `APIServerInfo` | Enable the server info endpoint. | ✓️ | | | `APIUpdaterStatus` | Enable endpoint for graph updaters status. | ✓️ | | -| `IncludeStopsUsedRealtimeInTransfers` | When generating transfers, stops without any patterns are excluded to improve performance if `ConsiderPatternsForDirectTransfers` is enabled. However, some stops are only used by trips changed or added by real-time updates. Since transfer generation happens before real-time updates are applied, OTP cannot know which stops will be needed. Instead, OTP will attempt to identify stops likely to be used by real-time updates at import time. Common cases include rail stops (which often have late platform assignments) and stops reserved for replacement services (which can be detected in NeTEx by examining the stop sub-mode). This feature has no effect if `ConsiderPatternsForDirectTransfers` is disabled. This feature is only supported for NeTEx feeds, not for GTFS feeds. | | | | `ConsiderPatternsForDirectTransfers` | Enable limiting transfers so that there is only a single transfer to each pattern. | ✓️ | | | `DebugUi` | Enable the debug GraphQL client and web UI and located at the root of the web server as well as the debug map tiles it uses. Be aware that the map tiles are not a stable API and can change without notice. Use the [vector tiles feature](sandbox/MapboxVectorTilesApi.md) if you want a stable map tiles API. | ✓️ | | | `ExtraTransferLegOnSameStop` | Should there be a transfer leg when transferring on the very same stop. Note that for in-seat/interlined transfers no transfer leg will be generated. | | | | `FloatingBike` | Enable floating bike routing. | ✓️ | | | `GtfsGraphQlApi` | Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md). | ✓️ | | +| `IncludeStopsUsedRealtimeInTransfers` | When generating transfers, stops without any patterns are excluded to improve performance if `ConsiderPatternsForDirectTransfers` is enabled. However, some stops are only used by trips changed or added by real-time updates. Since transfer generation happens before real-time updates are applied, OTP cannot know which stops will be needed. Instead, OTP will attempt to identify stops likely to be used by real-time updates at import time. Common cases include rail stops (which often have late platform assignments) and stops reserved for replacement services (which can be detected in NeTEx by examining the stop sub-mode). This feature has no effect if `ConsiderPatternsForDirectTransfers` is disabled. This feature is only supported for NeTEx feeds, not for GTFS feeds. | | | | `MinimumTransferTimeIsDefinitive` | If the minimum transfer time is a lower bound (default) or the definitive time for the transfer. Set this to `true` if you want to set a transfer time lower than what OTP derives from OSM data. | | | | `OnDemandRaptorTransfer` | Calculate transfers only when accessed by Raptor, instead of calculating and caching all transfers for the whole graph, for runtime requests which are not pre-cached in `transferCacheRequests` in router-config.json. This may help performance when doing local journey planning in a large graph. Requests which are specified in `transferCacheRequests` in router-config.json are not affected and are always pre-cached for the whole graph. | | | | `OptimizeTransfers` | OTP will inspect all itineraries found and optimize where (which stops) the transfer will happen. Waiting time, priority and guaranteed transfers are taken into account. | ✓️ | | From f80117b4ae246f701fab42055e4c3af1e19bff0a Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 11 Nov 2025 20:38:56 +0100 Subject: [PATCH 14/22] refactor: Correct spelling of CompositeNearbyStopFilter --- ...opFilter.java => CompositeNearbyStopFilter.java} | 6 +++--- .../filter/PatternConsideringNearbyStopFinder.java | 2 +- .../transfer/DirectTransferGeneratorCarTest.java | 6 +++--- .../transfer/DirectTransferGeneratorTestData.java | 13 ++++++------- .../module/transfer/PathTransferToString.java | 1 + 5 files changed, 14 insertions(+), 14 deletions(-) rename application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/{CompositNearbyStopFilter.java => CompositeNearbyStopFilter.java} (84%) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositeNearbyStopFilter.java similarity index 84% rename from application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositNearbyStopFilter.java rename to application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositeNearbyStopFilter.java index f90fdb9f656..673374da375 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositNearbyStopFilter.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositeNearbyStopFilter.java @@ -7,11 +7,11 @@ import java.util.Set; import org.opentripplanner.routing.graphfinder.NearbyStop; -class CompositNearbyStopFilter implements NearbyStopFilter { +class CompositeNearbyStopFilter implements NearbyStopFilter { private final List filters; - private CompositNearbyStopFilter(List filters) { + private CompositeNearbyStopFilter(List filters) { this.filters = filters; } @@ -45,7 +45,7 @@ NearbyStopFilter build() { if (filters.size() == 1) { return filters.getFirst(); } - return new CompositNearbyStopFilter(filters); + return new CompositeNearbyStopFilter(filters); } } } diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java index ef0a18cd425..2a1509b55f6 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java @@ -20,7 +20,7 @@ public PatternConsideringNearbyStopFinder( TransitService transitService, NearbyStopFinder delegateNearbyStopFinder ) { - var builder = CompositNearbyStopFilter.of().add(new PatternNearbyStopFilter(transitService)); + var builder = CompositeNearbyStopFilter.of().add(new PatternNearbyStopFilter(transitService)); if (OTPFeature.FlexRouting.isOn()) { builder.add(new FlexTripNearbyStopFilter(transitService)); diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java index 9dd52a83a2a..6c3a85a7f2f 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java @@ -222,9 +222,9 @@ public void testMaxTransferDurationForMode() { ); assertEquals( """ - S0 - S11, 100m - S0 - S21, 100m - S11 - S21, 100m""", + S0 - S11, 100m + S0 - S21, 100m + S11 - S21, 100m""", pathToString(repository.findTransfers(StreetMode.BIKE)) ); assertEquals("", pathToString(repository.findTransfers(StreetMode.CAR))); diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTestData.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTestData.java index 559393b6311..abe831622e5 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTestData.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTestData.java @@ -194,17 +194,18 @@ public void build() { tripPattern( TripPattern.of(TimetableRepositoryForTest.id("TP4")) .withRoute(route("R4", TransitMode.FERRY, agency)) - .withStopPattern( - new StopPattern( List.of(st(S_FAR_AWAY),st(S0), st(S12))) + .withStopPattern(new StopPattern(List.of(st(S_FAR_AWAY), st(S0), st(S12)))) + .withScheduledTimeTableBuilder(b -> + b.addTripTimes(createCarsAllowedTripTimesWithTwoStops()) ) - .withScheduledTimeTableBuilder(b -> b.addTripTimes(createCarsAllowedTripTimesWithTwoStops())) .build() ); tripPattern( TripPattern.of(TimetableRepositoryForTest.id("TP5")) .withRoute(route("R5", TransitMode.FERRY, agency)) .withStopPattern(new StopPattern(List.of(st(S22), st(S23)))) - .withScheduledTimeTableBuilder(b -> b.addTripTimes(createCarsAllowedTripTimesWithTwoStops()) + .withScheduledTimeTableBuilder(b -> + b.addTripTimes(createCarsAllowedTripTimesWithTwoStops()) ) .build() ); @@ -215,9 +216,7 @@ public void build() { private static ScheduledTripTimes createCarsAllowedTripTimesWithTwoStops() { return ScheduledTripTimes.of() .withTrip( - TimetableRepositoryForTest.trip("carsAllowed") - .withCarsAllowed(CarAccess.ALLOWED) - .build() + TimetableRepositoryForTest.trip("carsAllowed").withCarsAllowed(CarAccess.ALLOWED).build() ) .withDepartureTimes("00:00 01:00") .build(); diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/PathTransferToString.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/PathTransferToString.java index 0d712029a66..a059473c058 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/PathTransferToString.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/PathTransferToString.java @@ -5,6 +5,7 @@ import org.opentripplanner.model.PathTransfer; public class PathTransferToString { + static String pathToString(Collection transfers) { if (transfers.isEmpty()) { return ""; From 4a4c7853bfec26e95d8b0a02058af48a79410306 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Wed, 12 Nov 2025 19:12:30 +0100 Subject: [PATCH 15/22] refactor: Cleanup MinMap and add unit test --- .../module/transfer/filter/MinMap.java | 31 ++++++++------- .../module/transfer/filter/MinMapTest.java | 39 +++++++++++++++++++ 2 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 application/src/test/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMapTest.java diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMap.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMap.java index 4866da2d162..5b11f83b537 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMap.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMap.java @@ -1,13 +1,17 @@ package org.opentripplanner.graph_builder.module.transfer.filter; +import java.util.Collection; import java.util.HashMap; +import java.util.Map; /** * A HashMap that has been extended to track the greatest or smallest value for each key. Note that * this does not change the meaning of the 'put' method. It adds two new methods that add the * min/max behavior. This class used to be inside SimpleIsochrone. */ -class MinMap> extends HashMap { +class MinMap> { + + private final Map map = new HashMap<>(); /** * Put the given key-value pair in the map if the map does not yet contain the key, or if the @@ -16,26 +20,25 @@ class MinMap> extends HashMap { * @return whether the key-value pair was inserted in the map. */ boolean putMin(K key, V value) { - V oldValue = this.get(key); + V oldValue = map.get(key); if (oldValue == null || value.compareTo(oldValue) < 0) { - this.put(key, value); + map.put(key, value); return true; } return false; } /** - * Put the given key-value pair in the map if the map does not yet contain the key, or if the - * value is greater than the existing value for the same key. - * - * @return whether the key-value pair was inserted in the map. + * @see Map#get(Object) */ - boolean putMax(K key, V value) { - V oldValue = this.get(key); - if (oldValue == null || value.compareTo(oldValue) > 0) { - this.put(key, value); - return true; - } - return false; + public V get(K key) { + return map.get(key); + } + + /** + * @see Map#values() + */ + public Collection values() { + return map.values(); } } diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMapTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMapTest.java new file mode 100644 index 00000000000..52b7238ba27 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMapTest.java @@ -0,0 +1,39 @@ +package org.opentripplanner.graph_builder.module.transfer.filter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.google.common.truth.Truth; +import org.junit.jupiter.api.Test; + +class MinMapTest { + + private static final String KEY = "key"; + private final MinMap subject = new MinMap<>(); + + @Test + void putMinAndGet() { + assertNull(subject.get(KEY)); + + subject.putMin(KEY, "orange"); + assertEquals("orange", subject.get(KEY)); + + subject.putMin(KEY, "apple"); + assertEquals("apple", subject.get(KEY)); + + subject.putMin(KEY, "banana"); + assertEquals("apple", subject.get(KEY)); + } + + @Test + void values() { + subject.putMin("key1", "orange"); + Truth.assertThat(subject.values()).containsExactly("orange"); + + subject.putMin("key2", "apple"); + Truth.assertThat(subject.values()).containsExactly("apple", "orange"); + + subject.putMin("key3", "banana"); + Truth.assertThat(subject.values()).containsExactly("apple", "banana", "orange"); + } +} From 694710f7f219b622df5729104705a3affff1e4a6 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Wed, 12 Nov 2025 21:08:49 +0100 Subject: [PATCH 16/22] feat: Filter unused from stops in Transfers generation --- .../filter/CompositeNearbyStopFilter.java | 11 +++ .../filter/FlexTripNearbyStopFilter.java | 7 ++ .../transfer/filter/NearbyStopFilter.java | 31 +++++- .../PatternConsideringNearbyStopFinder.java | 12 +++ .../filter/PatternNearbyStopFilter.java | 7 ++ .../DirectTransferGeneratorCarTest.java | 24 +---- .../DirectTransferGeneratorTest.drawio.png | Bin 45166 -> 45114 bytes .../transfer/DirectTransferGeneratorTest.java | 91 +++++++++--------- .../DirectTransferGeneratorTestData.java | 2 +- .../module/transfer/PathTransferToString.java | 2 +- 10 files changed, 117 insertions(+), 70 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositeNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositeNearbyStopFilter.java index 673374da375..c6851c63dee 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositeNearbyStopFilter.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositeNearbyStopFilter.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Set; import org.opentripplanner.routing.graphfinder.NearbyStop; +import org.opentripplanner.transit.model.framework.FeedScopedId; class CompositeNearbyStopFilter implements NearbyStopFilter { @@ -19,6 +20,16 @@ static Builder of() { return new Builder(); } + @Override + public boolean includeFromStop(FeedScopedId id, boolean reverseDirection) { + for (NearbyStopFilter filter : filters) { + if (filter.includeFromStop(id, reverseDirection)) { + return true; + } + } + return false; + } + @Override public Collection filterToStops( Collection nearbyStops, diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/FlexTripNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/FlexTripNearbyStopFilter.java index c6722f15d67..666c7794819 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/FlexTripNearbyStopFilter.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/FlexTripNearbyStopFilter.java @@ -3,6 +3,7 @@ import java.util.Collection; import org.opentripplanner.ext.flex.trip.FlexTrip; import org.opentripplanner.routing.graphfinder.NearbyStop; +import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.service.TransitService; class FlexTripNearbyStopFilter implements NearbyStopFilter { @@ -13,6 +14,12 @@ class FlexTripNearbyStopFilter implements NearbyStopFilter { this.transitService = transitService; } + @Override + public boolean includeFromStop(FeedScopedId id, boolean reverseDirection) { + var stop = transitService.getStopLocation(id); + return !transitService.getFlexIndex().getFlexTripsByStop(stop).isEmpty(); + } + @Override public Collection filterToStops( Collection nearbyStops, diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/NearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/NearbyStopFilter.java index 562ca938687..4540ca346b5 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/NearbyStopFilter.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/NearbyStopFilter.java @@ -2,14 +2,35 @@ import java.util.Collection; import org.opentripplanner.routing.graphfinder.NearbyStop; +import org.opentripplanner.transit.model.framework.FeedScopedId; +/** + * Filters nearby stops for transfer generation based on trip patterns and flex trips. + * Returns only stops where boarding or alighting is possible, and for each pattern/trip, + * returns only the closest stop to minimize transfer generation. + */ interface NearbyStopFilter { /** - * Find all unique nearby stops that are the closest stop on some trip pattern or flex trip. Note - * that the result will include the origin vertex if it is an instance of StopVertex. This is - * intentional: we don't want to return the next stop down the line for trip patterns that pass - * through the origin vertex. Taking the patterns into account reduces the number of transfers - * significantly compared to simple traverse-duration-constrained all-to-all stop linkage. + * Checks if a stop should be included as a transfer origin/destination. + * + * @param id the stop-location to check + * @param reverseDirection true for arrival searches (checks boarding), false for departure + * searches (checks alighting) + * @return true if the stop has relevant trip patterns or flex trips + */ + boolean includeFromStop(FeedScopedId id, boolean reverseDirection); + + /** + * Filters nearby stops to find optimal transfer points. For each trip pattern or flex trip, + * returns only the closest stop. + *

+ * Note: The result will include the origin stop if it is a StopVertex. This is intentional - we + * don't want to return the next stop down the line for patterns passing through the origin. + * + * @param nearbyStops the stops to filter, typically from a street search + * @param reverseDirection true for arrival searches (filters by boarding), false for departure + * searches (filters by alighting) + * @return filtered stops, one per relevant trip pattern/flex trip */ Collection filterToStops( Collection nearbyStops, diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java index 2a1509b55f6..f1d2f56dddc 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java @@ -7,6 +7,7 @@ import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.request.StreetRequest; import org.opentripplanner.routing.graphfinder.NearbyStop; +import org.opentripplanner.street.model.vertex.TransitStopVertex; import org.opentripplanner.street.model.vertex.Vertex; import org.opentripplanner.transit.service.TransitService; @@ -36,6 +37,17 @@ public List findNearbyStops( StreetRequest streetRequest, boolean reverseDirection ) { + if (!(vertex instanceof TransitStopVertex stopVertex)) { + throw new IllegalArgumentException( + "Transfers can only be created between stops. Vertex: " + vertex + ); + } + + // Check if the from stop can be used in a transfer + if (!filter.includeFromStop(stopVertex.getId(), reverseDirection)) { + return List.of(); + } + // fetch nearby stops via the street network or using straight-line distance. var nearbyStops = delegateNearbyStopFinder.findNearbyStops( vertex, diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java index 19c1ecdd747..78b6f1dc633 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java @@ -20,6 +20,13 @@ class PatternNearbyStopFilter implements NearbyStopFilter { this.transitService = transitService; } + @Override + public boolean includeFromStop(FeedScopedId id, boolean reverseDirection) { + var stop = transitService.getRegularStop(id); + boolean hasPatterns = !findPatternsForStop(stop, !reverseDirection).isEmpty(); + return hasPatterns || includeStopUsedRealtime(stop); + } + @Override public Collection filterToStops( Collection nearbyStops, diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java index 6c3a85a7f2f..68681c77af4 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorCarTest.java @@ -91,10 +91,7 @@ public void testMultipleRequestsWithPatternsAndWithCarsAllowedPatterns() { S0 - S11, 100m S0 - S21, 100m S0 - S22, 200m - S11 - S21, 100m - S11 - S22, 110m - S12 - S22, 110m - S13 - S22, 210m"""; + S12 - S22, 110m"""; assertEquals( expected_walk_bike_results, pathToString(repository.findTransfers(StreetMode.WALK)) @@ -128,10 +125,7 @@ public void testBikeRequestWithPatternsAndWithCarsAllowedPatterns() { S0 - S11, 100m S0 - S21, 100m S0 - S22, 200m - S11 - S21, 100m - S11 - S22, 110m - S12 - S22, 110m - S13 - S22, 210m""", + S12 - S22, 110m""", pathToString(repository.getAllPathTransfers()) ); } @@ -148,8 +142,6 @@ public void testBikeRequestWithPatternsAndWithCarsAllowedPatternsWithoutCarInTra """ S0 - S11, 100m S0 - S21, 100m - S11 - S21, 100m - S11 - S22, 110m S12 - S22, 110m""", pathToString(repository.getAllPathTransfers()) ); @@ -181,10 +173,7 @@ public void testDisableDefaultTransfersForMode() { S0 - S11, 100m S0 - S21, 100m S0 - S22, 200m - S11 - S21, 100m - S11 - S22, 110m - S12 - S22, 110m - S13 - S22, 210m""", + S12 - S22, 110m""", pathToString(repository.findTransfers(StreetMode.WALK)) ); @@ -215,16 +204,13 @@ public void testMaxTransferDurationForMode() { """ S0 - S11, 100m S0 - S21, 100m - S11 - S21, 100m - S11 - S22, 110m S12 - S22, 110m""", pathToString(repository.findTransfers(StreetMode.WALK)) ); assertEquals( """ - S0 - S11, 100m - S0 - S21, 100m - S11 - S21, 100m""", + S0 - S11, 100m + S0 - S21, 100m""".indent(1).stripTrailing(), pathToString(repository.findTransfers(StreetMode.BIKE)) ); assertEquals("", pathToString(repository.findTransfers(StreetMode.CAR))); diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.drawio.png b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.drawio.png index d1ce61f0d262f8b1d8f793ab797db64da45355f0..7eba7ab944e16d8fb4269c709c9791b5e8ffd885 100644 GIT binary patch literal 45114 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 literal 45166 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

2Y$$0$6!f3|7 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 ZEiUg5J{I_v0DgjXOG)!ap@LcP{{lvxY`Fjc diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java index 08789cbd6a8..bb7ba4abe8d 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java @@ -101,23 +101,15 @@ public void testStraightLineTransfersWithPatternsPruning() { // Exactly one transfer is expected from each stop to the closest place to board all // patterns, including the same pattern used by the stop (E.g. S12 - S11). + // S11, S13, S21, S23 -> * No patterns alight here + // * -> S0, S12, S13, S23 No patterns board here assertEquals( """ S0 - S11, 1668m S0 - S21, 1829m - S11 - S0, 1668m - S11 - S21, 751m - S12 - S0, 3892m S12 - S11, 2224m S12 - S22, 751m - S13 - S11, 4448m - S13 - S22, 2347m - S21 - S0, 1829m - S21 - S11, 751m - S22 - S0, 3964m - S22 - S11, 2347m - S23 - S11, 4511m - S23 - S22, 2224m""", + S22 - S11, 2347m""", pathToString(repository.getAllPathTransfers()) ); } @@ -131,17 +123,12 @@ public void testStraightLineTransfersWithBoardingRestrictions() { .build(); assertEquals( - // * - S11 is not allowed, because of boarding constraints + // * -> S11 is not allowed, because of boarding constraints + // S11, S13, S21 -> * No patterns alight here + // * -> S0, S12, S23 No patterns board here """ S0 - S21, 1829m - S11 - S0, 1668m - S11 - S21, 751m - S12 - S0, 3892m - S12 - S22, 751m - S13 - S22, 2347m - S21 - S0, 1829m - S22 - S0, 3964m - S23 - S22, 2224m""", + S12 - S22, 751m""", pathToString(repository.getAllPathTransfers()) ); } @@ -165,6 +152,7 @@ public void testStreetTransfersWithoutPatternsPruning() { .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) .build(); + // All transfers reachable in the street graph is included. assertEquals( """ S0 - S11, 100m @@ -195,13 +183,14 @@ public void testStreetTransfersWithPatterns() { .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) .build(); + // Best transfers between patterns; Hence S0 - S22 removed + // S11, S13, S21 -> * No patterns alight here + // * -> S0, S12, S23 No patterns board here assertEquals( """ S0 - S11, 100m S0 - S21, 100m - S11 - S21, 100m - S12 - S22, 110m - S13 - S22, 210m""", + S12 - S22, 110m""", pathToString(repository.getAllPathTransfers()) ); } @@ -215,13 +204,15 @@ public void testStreetTransfersWithPatternsIncludeRealTimeUsedStops() { .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) .build(); + // Best transfers between patterns; Hence S0 - S22 removed + // S11, S21 -> * No patterns alight here + // * -> S0, S12 No patterns board here + // S13, S23 Included, used real-time assertEquals( """ S0 - S11, 100m S0 - S21, 100m S0 - S23, 300m - S11 - S21, 100m - S11 - S23, 210m S12 - S22, 110m S12 - S23, 210m S13 - S22, 210m @@ -244,34 +235,46 @@ public void testStreetTransfersWithMultipleRequestsWithPatterns() { var bikeTransfers = repository.findTransfers(StreetMode.BIKE); var carTransfers = repository.findTransfers(StreetMode.CAR); - String expected = + // Best transfers between patterns; Hence S0 - S22 removed + // S11, S13, S21 -> * No patterns alight here + // * -> S0, S12, S23 No patterns board here + String expectedWalkAndBike = """ S0 - S11, 100m S0 - S21, 100m - S11 - S21, 100m - S12 - S22, 110m - S13 - S22, 210m"""; - assertEquals(expected, pathToString(walkTransfers)); - assertEquals(expected, pathToString(bikeTransfers)); + S12 - S22, 110m"""; + assertEquals(expectedWalkAndBike, pathToString(walkTransfers)); + assertEquals(expectedWalkAndBike, pathToString(bikeTransfers)); assertEquals("", pathToString(carTransfers)); } @Test public void testStreetTransfersWithStationWithTransfersNotAllowed() { - var repository = DirectTransferGeneratorTestData.of() - .withPatterns() - .withStreetGraph() - .withNoTransfersOnStationA() - .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) - .build(); + OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { + var repository = DirectTransferGeneratorTestData.of() + .withPatterns() + .withStreetGraph() + .withNoTransfersOnStationA() + .withTransferRequests(REQUEST_WITH_WALK_TRANSFER) + .build(); - assertEquals( - """ - S0 - S22, 200m - S12 - S22, 110m - S13 - S22, 210m""", - pathToString(repository.getAllPathTransfers()) - ); + // Best transfers between patterns; Hence S0 - S22 removed + // S11, S13, S21 -> * No patterns alight here + // * -> S0, S12, S23 No patterns board here + // Not: S11, S21 Transfers NOT_ALLOWED for station + // Allow: S13, S23 Included, used real-time + assertEquals( + """ + S0 - S22, 200m + S0 - S23, 300m + S12 - S22, 110m + S12 - S23, 210m + S13 - S22, 210m + S13 - S23, 310m + S22 - S23, 100m""", + pathToString(repository.getAllPathTransfers()) + ); + }); } @Test diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTestData.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTestData.java index abe831622e5..c3f5de05bb0 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTestData.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTestData.java @@ -160,7 +160,7 @@ public void build() { tripPattern( TripPattern.of(TimetableRepositoryForTest.id("TP0")) .withRoute(route("R0", TransitMode.RAIL, agency)) - .withStopPattern(new StopPattern(List.of(st(S0), st(S_FAR_AWAY)))) + .withStopPattern(new StopPattern(List.of(st(S_FAR_AWAY), st(S0)))) .build() ); tripPattern( diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/PathTransferToString.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/PathTransferToString.java index a059473c058..15cc2c3f814 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/PathTransferToString.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/PathTransferToString.java @@ -4,7 +4,7 @@ import java.util.stream.Collectors; import org.opentripplanner.model.PathTransfer; -public class PathTransferToString { +class PathTransferToString { static String pathToString(Collection transfers) { if (transfers.isEmpty()) { From 32d09bccb8577b1e7edafa3c4259c205272e634d Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Wed, 12 Nov 2025 21:46:57 +0100 Subject: [PATCH 17/22] refactor: Rename canBoard/Alight(StopLocation) to boarding/alightingExist --- .../filter/PatternNearbyStopFilter.java | 2 +- .../PlaceFinderTraverseVisitor.java | 2 +- .../transit/model/network/StopPattern.java | 30 ++++++++------- .../transit/model/network/TripPattern.java | 38 +++++++++++++++---- .../model/network/StopPatternTest.java | 12 +++--- 5 files changed, 54 insertions(+), 30 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java index 78b6f1dc633..2797d7bba74 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java @@ -66,7 +66,7 @@ private List findPatternsForStop(RegularStop stop, boolean reverse return transitService .findPatterns(stop) .stream() - .filter(reverseDirection ? p -> p.canAlight(stop) : p -> p.canBoard(stop)) + .filter(reverseDirection ? p -> p.alightingExist(stop) : p -> p.boardingExist(stop)) .map(TripPattern::getId) .toList(); } diff --git a/application/src/main/java/org/opentripplanner/routing/graphfinder/PlaceFinderTraverseVisitor.java b/application/src/main/java/org/opentripplanner/routing/graphfinder/PlaceFinderTraverseVisitor.java index 10cd4323c11..e4bdf9c8c88 100644 --- a/application/src/main/java/org/opentripplanner/routing/graphfinder/PlaceFinderTraverseVisitor.java +++ b/application/src/main/java/org/opentripplanner/routing/graphfinder/PlaceFinderTraverseVisitor.java @@ -241,7 +241,7 @@ private void handlePatternsAtStop(RegularStop stop, double distance) { .filter( pattern -> filterByRoutes.isEmpty() || filterByRoutes.contains(pattern.getRoute().getId()) ) - .filter(pattern -> pattern.canBoard(stop)) + .filter(pattern -> pattern.boardingExist(stop)) .toList(); for (TripPattern pattern : patterns) { diff --git a/application/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java b/application/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java index af154c9fbe4..c4bd9b45742 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java +++ b/application/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java @@ -202,20 +202,21 @@ boolean canAlight(int stopPosInPattern) { } /** - * Returns whether passengers can alight at a given stop. + * Use {@link #canAlight(int)} if you want to check if a stop can be alighted at a given + * stop position, ONLY use this method if you would like to search the stop-pattern for a + * alighting. *

- * WARNING! This is an inefficient method iterating over the stops, do not use it in routing. + * Returns whether passengers can alight at a given stop SOMEWHERE in the pattern, + * considering all stops in case the pattern visit the same stop twice. *

- * WARNING! This does not support ring patterns. + * WARNING! This is an inefficient method iterating over the stops, do not use it in routing. *

* WARNING! This does not produce the same result as the {@link #canAlight(int)}, * this method ALWAYS returns {@code false} for the first stop, while the * other method returns whatever is in the data. This method is probably the * correct way - but this is not a clear decision. - * @deprecated Avoid using this method! */ - @Deprecated - boolean canAlight(StopLocation stop) { + boolean alightingExist(StopLocation stop) { // We skip the first stop, not allowed for alighting for (int i = 1; i < stops.length; ++i) { if (stop == stops[i] && canAlight(i)) { @@ -231,20 +232,21 @@ boolean canBoard(int stopPosInPattern) { } /** - * Returns whether passengers can board at a given stop. + * Use {@link #canBoard(int)} if you want to check if a stop can be boarded at a given + * stop position, ONLY use this method if you would like to search the stop-pattern for a + * bording. *

- * WARNING! This is an inefficient method iterating over the stops, do not use it in routing. + * Returns whether passengers can board at a given stop SOMEWHERE in the pattern, + * considering all stops in case the pattern visit the same stop twice. *

- * WARNING! This does not support ring patterns. + * WARNING! This is an inefficient method iterating over the stops, do not use it in routing. *

* WARNING! This does not produce the same result as the {@link #canBoard(int)}, * this method ALWAYS returns {@code false} for the last stop, while the * other method returns whatever is in the data. This method is probably the * correct way - but this is not a clear decision. - * @deprecated Avoid using this method! */ - @Deprecated - boolean canBoard(StopLocation stop) { + boolean boardingExist(StopLocation stop) { // We skip the last stop, not allowed for boarding for (int i = 0; i < stops.length - 1; ++i) { if (stop == stops[i] && canBoard(i)) { @@ -311,13 +313,13 @@ boolean sameStations(StopPattern other, int index) { var otherOrigin = other.getStop(index).getParentStation(); var otherDestination = other.getStop(index + 1).getParentStation(); var origin = getStop(index).getParentStation(); - var destionation = getStop(index + 1).getParentStation(); + var destination = getStop(index + 1).getParentStation(); var sameOrigin = Optional.ofNullable(origin) .map(o -> o.equals(otherOrigin)) .orElse(getStop(index).equals(other.getStop(index))); - var sameDestination = Optional.ofNullable(destionation) + var sameDestination = Optional.ofNullable(destination) .map(o -> o.equals(otherDestination)) .orElse(getStop(index + 1).equals(other.getStop(index + 1))); diff --git a/application/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java b/application/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java index 52be144e09d..a563df58d7f 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java +++ b/application/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java @@ -297,19 +297,41 @@ public boolean canBoard(int stopPos) { } /** - * Returns whether passengers can board at a given stop. This is an inefficient method iterating - * over the stops, do not use it in routing. + * Use {@link #canBoard(int)} if you want to check if a stop can be boarded at a given + * stop position, ONLY use this method if you would like to search the stop-pattern for + * if it contains a bording for the given stop. + *

+ * Returns whether passengers can board at a given stop SOMEWHERE in the pattern, + * considering all stops in case the pattern visit the same stop twice. + *

+ * WARNING! This is an inefficient method iterating over the stops, do not use it in routing. + *

+ * WARNING! This does not produce the same result as the {@link #canBoard(int)}, + * this method ALWAYS returns {@code false} for the last stop, while the + * other method returns whatever is in the data. This method is probably the + * correct way - but this is not a clear decision. */ - public boolean canBoard(StopLocation stop) { - return stopPattern.canBoard(stop); + public boolean boardingExist(StopLocation stop) { + return stopPattern.boardingExist(stop); } /** - * Returns whether passengers can alight at a given stop. This is an inefficient method iterating - * over the stops, do not use it in routing. + * Use {@link #canAlight(int)} if you want to check if a stop can be alighted at a given + * stop position, ONLY use this method if you would like to search the stop-pattern for a + * alighting. + *

+ * Returns whether passengers can alight at a given stop SOMEWHERE in the pattern, + * considering all stops in case the pattern visit the same stop twice. + *

+ * WARNING! This is an inefficient method iterating over the stops, do not use it in routing. + *

+ * WARNING! This does not produce the same result as the {@link #canAlight(int)}, + * this method ALWAYS returns {@code false} for the first stop, while the + * other method returns whatever is in the data. This method is probably the + * correct way - but this is not a clear decision. */ - public boolean canAlight(StopLocation stop) { - return stopPattern.canAlight(stop); + public boolean alightingExist(StopLocation stop) { + return stopPattern.alightingExist(stop); } /** Returns whether a given stop is wheelchair-accessible. */ diff --git a/application/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java b/application/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java index af6403f0052..71ec37f7a37 100644 --- a/application/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java +++ b/application/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java @@ -66,13 +66,13 @@ void boardingAlightingUsingStopInstance() { ) ); - assertFalse(stopPattern.canAlight(s1), "Alight is not allowed on the first stop!"); - assertTrue(stopPattern.canAlight(s2)); - assertTrue(stopPattern.canAlight(s3)); + assertFalse(stopPattern.alightingExist(s1), "Alight is not allowed on the first stop!"); + assertTrue(stopPattern.alightingExist(s2)); + assertTrue(stopPattern.alightingExist(s3)); - assertTrue(stopPattern.canBoard(s1)); - assertTrue(stopPattern.canBoard(s2)); - assertFalse(stopPattern.canBoard(s3), "Boarding is not allowed on the last stop!"); + assertTrue(stopPattern.boardingExist(s1)); + assertTrue(stopPattern.boardingExist(s2)); + assertFalse(stopPattern.boardingExist(s3), "Boarding is not allowed on the last stop!"); } @Test From 3df0b60e6249c6b68af681e1c358022c2879b618 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 14 Nov 2025 13:30:17 +0100 Subject: [PATCH 18/22] review: Fix spelling and other documentation issues --- .../graph_builder/module/transfer/filter/MinMap.java | 7 ++++--- .../module/transfer/DirectTransferGeneratorTest.java | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMap.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMap.java index 5b11f83b537..e9d900a4982 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMap.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/MinMap.java @@ -3,11 +3,10 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import javax.annotation.Nullable; /** - * A HashMap that has been extended to track the greatest or smallest value for each key. Note that - * this does not change the meaning of the 'put' method. It adds two new methods that add the - * min/max behavior. This class used to be inside SimpleIsochrone. + * Decorate a HashMap that to track the smallest value for each key. */ class MinMap> { @@ -17,6 +16,7 @@ class MinMap> { * Put the given key-value pair in the map if the map does not yet contain the key, or if the * value is less than the existing value for the same key. * + * @see Map#put(Object, Object) * @return whether the key-value pair was inserted in the map. */ boolean putMin(K key, V value) { @@ -31,6 +31,7 @@ boolean putMin(K key, V value) { /** * @see Map#get(Object) */ + @Nullable public V get(K key) { return map.get(key); } diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java index bb7ba4abe8d..e2eeb35fcc8 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java @@ -13,7 +13,7 @@ /** * This test uses the following graph/network for testing the DirectTransfer generation. The - * fokus is on the filtering of the transfers, not on testing that the NearBySearch return the + * focus is on the filtering of the transfers, not on testing that the NearBySearch return the * correct set of nearby stops. *

* From 8dadd0b7eca122581cb10f92f03ca8d3601e2da0 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Mon, 17 Nov 2025 16:58:26 +0100 Subject: [PATCH 19/22] test: Add unit tests for StopMapper functionality --- .../mapping/StopAndStationMapperTest.java | 138 ---------- .../gtfs/mapping/StopMapperTest.java | 239 ++++++++++++++++++ 2 files changed, 239 insertions(+), 138 deletions(-) delete mode 100644 application/src/test/java/org/opentripplanner/gtfs/mapping/StopAndStationMapperTest.java create mode 100644 application/src/test/java/org/opentripplanner/gtfs/mapping/StopMapperTest.java diff --git a/application/src/test/java/org/opentripplanner/gtfs/mapping/StopAndStationMapperTest.java b/application/src/test/java/org/opentripplanner/gtfs/mapping/StopAndStationMapperTest.java deleted file mode 100644 index fa11f2d08b7..00000000000 --- a/application/src/test/java/org/opentripplanner/gtfs/mapping/StopAndStationMapperTest.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.opentripplanner.gtfs.mapping; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Collection; -import java.util.Collections; -import org.junit.jupiter.api.Test; -import org.onebusaway.gtfs.model.AgencyAndId; -import org.onebusaway.gtfs.model.Stop; -import org.opentripplanner.transit.model.basic.Accessibility; -import org.opentripplanner.transit.model.site.RegularStop; -import org.opentripplanner.transit.service.SiteRepository; - -public class StopAndStationMapperTest { - - private static final AgencyAndId AGENCY_AND_ID = new AgencyAndId("A", "1"); - - private static final String CODE = "Code"; - - private static final String DESC = "Desc"; - - private static final String DIRECTION = "Direction"; - - private static final double LAT = 60.0d; - - private static final double LON = 45.0d; - - private static final String NAME = "Name"; - - private static final String PARENT = "Parent"; - - private static final String PLATFORM_CODE = "Platform Code"; - - private static final String TIMEZONE = "GMT"; - - private static final String URL = "www.url.me"; - - private static final int VEHICLE_TYPE = 5; - - private static final int WHEELCHAIR_BOARDING = 1; - - private static final String ZONE_ID = "Zone Id"; - - private static final Stop STOP = new Stop(); - - private final StopMapper subject = new StopMapper( - new IdFactory("A"), - new TranslationHelper(), - stationId -> null, - new SiteRepository().withContext() - ); - - static { - STOP.setId(AGENCY_AND_ID); - STOP.setCode(CODE); - STOP.setDesc(DESC); - STOP.setDirection(DIRECTION); - STOP.setLat(LAT); - STOP.setLon(LON); - STOP.setName(NAME); - STOP.setParentStation(PARENT); - STOP.setPlatformCode(PLATFORM_CODE); - STOP.setTimezone(TIMEZONE); - STOP.setUrl(URL); - STOP.setVehicleType(VEHICLE_TYPE); - STOP.setWheelchairBoarding(WHEELCHAIR_BOARDING); - STOP.setZoneId(ZONE_ID); - } - - @Test - void testMapCollection() { - assertNull(subject.map((Collection) null)); - assertTrue(subject.map(Collections.emptyList()).isEmpty()); - assertEquals(1, subject.map(Collections.singleton(STOP)).size()); - } - - @Test - void testMap() { - RegularStop result = subject.map(STOP); - - assertEquals("A:1", result.getId().toString()); - assertEquals(CODE, result.getCode()); - assertEquals(DESC, result.getDescription().toString()); - assertEquals(LAT, result.getLat(), 0.0001d); - assertEquals(LON, result.getLon(), 0.0001d); - assertEquals(NAME, result.getName().toString()); - assertEquals(URL, result.getUrl().toString()); - assertEquals(Accessibility.POSSIBLE, result.getWheelchairAccessibility()); - assertEquals(ZONE_ID, result.getFirstZoneAsString()); - } - - @Test - void testMapWithNulls() { - Stop input = new Stop(); - input.setId(AGENCY_AND_ID); - input.setName(NAME); - - RegularStop result = subject.map(input); - - assertNotNull(result.getId()); - assertNull(result.getCode()); - assertNull(result.getDescription()); - assertEquals(NAME, result.getName().toString()); - assertNull(result.getParentStation()); - assertNull(result.getCode()); - assertNull(result.getUrl()); - // Skip getting coordinate, it will throw an exception - assertEquals(Accessibility.NO_INFORMATION, result.getWheelchairAccessibility()); - assertNull(result.getFirstZoneAsString()); - } - - @Test - void verifyMissingCoordinateThrowsException() { - Stop input = new Stop(); - input.setId(AGENCY_AND_ID); - input.setName(NAME); - - RegularStop result = subject.map(input); - - // Getting the coordinate will throw an IllegalArgumentException if not set, - // this is considered to be a implementation error - assertThrows(IllegalStateException.class, result::getCoordinate); - } - - /** Mapping the same object twice, should return the the same instance. */ - @Test - void testMapCache() { - RegularStop result1 = subject.map(STOP); - RegularStop result2 = subject.map(STOP); - - assertSame(result1, result2); - } -} diff --git a/application/src/test/java/org/opentripplanner/gtfs/mapping/StopMapperTest.java b/application/src/test/java/org/opentripplanner/gtfs/mapping/StopMapperTest.java new file mode 100644 index 00000000000..20e118aec6f --- /dev/null +++ b/application/src/test/java/org/opentripplanner/gtfs/mapping/StopMapperTest.java @@ -0,0 +1,239 @@ +package org.opentripplanner.gtfs.mapping; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.ZoneId; +import java.util.Collection; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.onebusaway.gtfs.model.AgencyAndId; +import org.onebusaway.gtfs.model.Stop; +import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; +import org.opentripplanner.transit.model.basic.Accessibility; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.Station; +import org.opentripplanner.transit.service.SiteRepository; + +public class StopMapperTest { + + private static final AgencyAndId AGENCY_AND_ID = new AgencyAndId("A", "1"); + + private static final String CODE = "Code"; + + private static final String DESC = "Desc"; + + private static final double LAT = 60.0d; + + private static final double LON = 45.0d; + + private static final String NAME = "Name"; + + private static final String PLATFORM_CODE = "Platform Code"; + + private static final String TIMEZONE = "GMT"; + + private static final String URL = "www.url.me"; + + // GTFS vehicle type constants + private static final int VEHICLE_TYPE_RAIL = 2; + private static final int VEHICLE_TYPE_BUS = 3; + private static final int VEHICLE_TYPE_CABLE_CAR = 5; + + private static final int VEHICLE_TYPE = VEHICLE_TYPE_CABLE_CAR; + + private static final int WHEELCHAIR_BOARDING = 1; + + private static final String ZONE_ID = "Zone Id"; + + private final StopMapper subject = new StopMapper( + new IdFactory("A"), + new TranslationHelper(), + stationId -> null, + new SiteRepository().withContext() + ); + + @Test + void testMapCollection() { + assertNull(subject.map((Collection) null)); + assertTrue(subject.map(Collections.emptyList()).isEmpty()); + assertEquals(1, subject.map(Collections.singleton(createTestStop())).size()); + } + + @Test + void testMap() { + RegularStop result = subject.map(createTestStop()); + + assertEquals("A:1", result.getId().toString()); + assertEquals(CODE, result.getCode()); + assertEquals(DESC, result.getDescription().toString()); + assertEquals(LAT, result.getLat(), 0.0001d); + assertEquals(LON, result.getLon(), 0.0001d); + assertEquals(NAME, result.getName().toString()); + assertEquals(URL, result.getUrl().toString()); + assertEquals(Accessibility.POSSIBLE, result.getWheelchairAccessibility()); + assertEquals(ZONE_ID, result.getFirstZoneAsString()); + } + + @Test + void testMapWithNulls() { + Stop input = createMinimalStop(); + + RegularStop result = subject.map(input); + + assertNotNull(result.getId()); + assertNull(result.getCode()); + assertNull(result.getDescription()); + assertEquals(NAME, result.getName().toString()); + assertNull(result.getParentStation()); + assertNull(result.getCode()); + assertNull(result.getUrl()); + // Skip getting coordinate, it will throw an exception + assertEquals(Accessibility.NO_INFORMATION, result.getWheelchairAccessibility()); + assertNull(result.getFirstZoneAsString()); + } + + @Test + void verifyMissingCoordinateThrowsException() { + Stop input = createMinimalStop(); + + RegularStop result = subject.map(input); + + // Getting the coordinate will throw an IllegalStateException if not set, + // this is considered to be a implementation error + var ex = assertThrows(IllegalStateException.class, result::getCoordinate); + assertEquals("Coordinate not set for: RegularStop{A:1 Name}", ex.getMessage()); + } + + /** Mapping the same object twice, should return the the same instance. */ + @Test + void testMapCache() { + Stop stop = createTestStop(); + + RegularStop result1 = subject.map(stop); + RegularStop result2 = subject.map(stop); + + assertSame(result1, result2); + } + + @Test + void testMapNull() { + assertNull(subject.map((Stop) null)); + } + + @Test + void testMapWithInvalidLocationType() { + Stop input = createBasicStop(); + input.setLocationType(Stop.LOCATION_TYPE_STATION); // Invalid - should be STOP + + var ex = assertThrows(IllegalArgumentException.class, () -> subject.map(input)); + assertEquals( + "Expected location_type 0, but got 1 for stops.txt entry ", + ex.getMessage() + ); + } + + @Test + void testMapWithParentStation() { + TimetableRepositoryForTest testModel = TimetableRepositoryForTest.of(); + Station parentStation = testModel.station("Parent").build(); + + StopMapper mapperWithStation = new StopMapper( + new IdFactory("A"), + new TranslationHelper(), + id -> parentStation, + new SiteRepository().withContext() + ); + + Stop input = createBasicStop(); + input.setParentStation("Parent"); + + RegularStop result = mapperWithStation.map(input); + + assertNotNull(result.getParentStation()); + assertEquals(parentStation, result.getParentStation()); + } + + @Test + void testMapWithTimezone() { + Stop input = createBasicStop(); + input.setTimezone(TIMEZONE); + + RegularStop result = subject.map(input); + + assertNotNull(result.getTimeZone()); + assertEquals(ZoneId.of(TIMEZONE), result.getTimeZone()); + } + + @Test + void testMapWithVehicleType() { + Stop input = createBasicStop(); + input.setVehicleType(VEHICLE_TYPE_RAIL); + + RegularStop result = subject.map(input); + + assertNotNull(result.getVehicleType()); + assertEquals(TransitMode.RAIL, result.getVehicleType()); + } + + @Test + void testMapWithPlatformCode() { + Stop input = createBasicStop(); + input.setPlatformCode(PLATFORM_CODE); + + RegularStop result = subject.map(input); + + assertEquals(PLATFORM_CODE, result.getPlatformCode()); + } + + /** + * Creates a minimal Stop with only required fields (ID and name). + */ + private static Stop createMinimalStop() { + Stop stop = new Stop(); + stop.setId(AGENCY_AND_ID); + stop.setName(NAME); + return stop; + } + + /** + * Creates a basic Stop with standard fields needed for most tests + * (ID, name, latitude, longitude, and correct location type). + */ + private static Stop createBasicStop() { + Stop stop = new Stop(); + stop.setId(AGENCY_AND_ID); + stop.setName(NAME); + stop.setLat(LAT); + stop.setLon(LON); + stop.setLocationType(Stop.LOCATION_TYPE_STOP); + return stop; + } + + /** + * Creates a fully populated Stop for comprehensive testing. + */ + private static Stop createTestStop() { + Stop stop = new Stop(); + stop.setId(AGENCY_AND_ID); + stop.setCode(CODE); + stop.setDesc(DESC); + stop.setLat(LAT); + stop.setLon(LON); + stop.setName(NAME); + stop.setPlatformCode(PLATFORM_CODE); + stop.setParentStation("Parent"); + stop.setTimezone(TIMEZONE); + stop.setUrl(URL); + stop.setVehicleType(VEHICLE_TYPE); + stop.setWheelchairBoarding(WHEELCHAIR_BOARDING); + stop.setZoneId(ZONE_ID); + stop.setLocationType(Stop.LOCATION_TYPE_STOP); + return stop; + } +} From 5822cddd48cf7a2107af96502ef21ad39e5bcb61 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Mon, 17 Nov 2025 16:53:23 +0100 Subject: [PATCH 20/22] feat: Enhance StopMapper to include sometimes-used-realtime-stops in transfer generation --- .../gtfs/mapping/StopMapper.java | 29 ++++++++++- .../gtfs/mapping/TransitModeMapper.java | 6 +++ .../gtfs/mapping/StopMapperTest.java | 52 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/opentripplanner/gtfs/mapping/StopMapper.java b/application/src/main/java/org/opentripplanner/gtfs/mapping/StopMapper.java index ce77ab5f55e..a0897ec40d9 100644 --- a/application/src/main/java/org/opentripplanner/gtfs/mapping/StopMapper.java +++ b/application/src/main/java/org/opentripplanner/gtfs/mapping/StopMapper.java @@ -6,6 +6,8 @@ import java.util.Map; import java.util.function.Function; import org.onebusaway.gtfs.model.Stop; +import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.site.FareZone; import org.opentripplanner.transit.model.site.RegularStop; @@ -45,6 +47,8 @@ RegularStop map(org.onebusaway.gtfs.model.Stop original) { } private RegularStop doMap(org.onebusaway.gtfs.model.Stop gtfsStop) { + TransitMode mode = TransitModeMapper.mapMode(gtfsStop.getVehicleType()); + assertLocationTypeIsStop(gtfsStop); StopMappingWrapper base = new StopMappingWrapper(idFactory, gtfsStop); RegularStopBuilder builder = siteRepositoryBuilder @@ -54,7 +58,8 @@ private RegularStop doMap(org.onebusaway.gtfs.model.Stop gtfsStop) { .withWheelchairAccessibility(base.getWheelchairAccessibility()) .withLevel(base.getLevel()) .withPlatformCode(gtfsStop.getPlatformCode()) - .withVehicleType(TransitModeMapper.mapMode(gtfsStop.getVehicleType())); + .withVehicleType(mode) + .withSometimesUsedRealtime(mapSometimesUsedRealtime(mode, gtfsStop)); builder.withName( translationHelper.getTranslation( @@ -100,6 +105,28 @@ private RegularStop doMap(org.onebusaway.gtfs.model.Stop gtfsStop) { return builder.build(); } + /** + * If a stop is sometimes used by realtime, then we must include it in transfer generation, even + * when it does not have any trips/routes visiting it. + */ + private static boolean mapSometimesUsedRealtime(TransitMode mode, Stop gtfsStop) { + if (OTPFeature.IncludeStopsUsedRealtimeInTransfers.isOn()) { + if (mode == TransitMode.RAIL) { + return true; + } + // We only consider rail and rail-replacement-bus stops when generating transfers. The reason + // we do not include all stops is due to perfomance reasons. This should be replaced by + // generating transfers as needed for realtime updates. + else if ( + mode == TransitMode.BUS && + TransitModeMapper.isRailReplacementBusService(gtfsStop.getVehicleType()) + ) { + return true; + } + } + return false; + } + private void assertLocationTypeIsStop(Stop gtfsStop) { if (gtfsStop.getLocationType() != Stop.LOCATION_TYPE_STOP) { throw new IllegalArgumentException( diff --git a/application/src/main/java/org/opentripplanner/gtfs/mapping/TransitModeMapper.java b/application/src/main/java/org/opentripplanner/gtfs/mapping/TransitModeMapper.java index 919d71455a2..b1d080dd721 100644 --- a/application/src/main/java/org/opentripplanner/gtfs/mapping/TransitModeMapper.java +++ b/application/src/main/java/org/opentripplanner/gtfs/mapping/TransitModeMapper.java @@ -4,6 +4,8 @@ public class TransitModeMapper { + private static final int RAIL_REPLACEMENT_BUS_SERVICE = 714; + /** * Return an OTP TransitMode matching a routeType. If no good match is found, it returns null. * @@ -86,4 +88,8 @@ public static TransitMode mapMode(int routeType) { default -> throw new IllegalArgumentException("unknown gtfs route type " + routeType); }; } + + public static boolean isRailReplacementBusService(int routeType) { + return routeType == RAIL_REPLACEMENT_BUS_SERVICE; + } } diff --git a/application/src/test/java/org/opentripplanner/gtfs/mapping/StopMapperTest.java b/application/src/test/java/org/opentripplanner/gtfs/mapping/StopMapperTest.java index 20e118aec6f..9b283621bb8 100644 --- a/application/src/test/java/org/opentripplanner/gtfs/mapping/StopMapperTest.java +++ b/application/src/test/java/org/opentripplanner/gtfs/mapping/StopMapperTest.java @@ -1,6 +1,7 @@ package org.opentripplanner.gtfs.mapping; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; @@ -13,6 +14,7 @@ import org.junit.jupiter.api.Test; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.gtfs.model.Stop; +import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; import org.opentripplanner.transit.model.basic.Accessibility; import org.opentripplanner.transit.model.basic.TransitMode; @@ -44,6 +46,7 @@ public class StopMapperTest { private static final int VEHICLE_TYPE_RAIL = 2; private static final int VEHICLE_TYPE_BUS = 3; private static final int VEHICLE_TYPE_CABLE_CAR = 5; + private static final int VEHICLE_TYPE_RAIL_REPLACEMENT_BUS = 714; private static final int VEHICLE_TYPE = VEHICLE_TYPE_CABLE_CAR; @@ -78,6 +81,7 @@ void testMap() { assertEquals(URL, result.getUrl().toString()); assertEquals(Accessibility.POSSIBLE, result.getWheelchairAccessibility()); assertEquals(ZONE_ID, result.getFirstZoneAsString()); + assertFalse(result.isSometimesUsedRealtime()); } @Test @@ -191,6 +195,54 @@ void testMapWithPlatformCode() { assertEquals(PLATFORM_CODE, result.getPlatformCode()); } + @Test + void testMapSometimesUsedRealtimeForRailWithFeatureOn() { + OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { + Stop input = createBasicStop(); + input.setVehicleType(VEHICLE_TYPE_RAIL); + + RegularStop result = subject.map(input); + + assertTrue(result.isSometimesUsedRealtime()); + }); + } + + @Test + void testMapSometimesUsedRealtimeForRailReplacementBusWithFeatureOn() { + OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { + Stop input = createBasicStop(); + input.setVehicleType(VEHICLE_TYPE_RAIL_REPLACEMENT_BUS); + + RegularStop result = subject.map(input); + + assertTrue(result.isSometimesUsedRealtime()); + }); + } + + @Test + void testMapSometimesUsedRealtimeForBusWithFeatureOn() { + OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { + Stop input = createBasicStop(); + input.setVehicleType(VEHICLE_TYPE_BUS); + + RegularStop result = subject.map(input); + + assertFalse(result.isSometimesUsedRealtime()); + }); + } + + @Test + void testMapSometimesUsedRealtimeWithFeatureOff() { + OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOff(() -> { + Stop input = createBasicStop(); + input.setVehicleType(VEHICLE_TYPE_RAIL); + + RegularStop result = subject.map(input); + + assertFalse(result.isSometimesUsedRealtime()); + }); + } + /** * Creates a minimal Stop with only required fields (ID and name). */ From bbadd277ba24078dbd60d3ef76fd2432f9deb609 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Mon, 17 Nov 2025 17:22:09 +0100 Subject: [PATCH 21/22] review: Fix spelling errors and add JavaDoc comments for NearbyStopFilter implementations --- .../filter/CompositeNearbyStopFilter.java | 7 +++++++ .../transfer/filter/FlexTripNearbyStopFilter.java | 7 +++++++ .../PatternConsideringNearbyStopFinder.java | 15 +++++++++++++++ .../transfer/filter/PatternNearbyStopFilter.java | 10 ++++++++++ .../transit/model/network/StopPattern.java | 2 +- .../transit/model/site/RegularStop.java | 2 +- 6 files changed, 41 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositeNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositeNearbyStopFilter.java index c6851c63dee..76c25d27d0e 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositeNearbyStopFilter.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/CompositeNearbyStopFilter.java @@ -8,6 +8,13 @@ import org.opentripplanner.routing.graphfinder.NearbyStop; import org.opentripplanner.transit.model.framework.FeedScopedId; +/** + * Combines multiple {@link NearbyStopFilter}s into a single filter. + *

+ * This filter applies all configured filters and returns the union of their results. A stop is + * included if ANY of the component filters include it (OR logic for from-stops, union for + * to-stops). + */ class CompositeNearbyStopFilter implements NearbyStopFilter { private final List filters; diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/FlexTripNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/FlexTripNearbyStopFilter.java index 666c7794819..be3ba1214bf 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/FlexTripNearbyStopFilter.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/FlexTripNearbyStopFilter.java @@ -6,6 +6,13 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.service.TransitService; +/** + * Filters nearby stops based on flex trip availability. + *

+ * This filter ensures that transfers are generated for stops used by flex trips. For each flex + * trip, it keeps only the closest stop where the flex trip can board or alight (depending on + * direction). + */ class FlexTripNearbyStopFilter implements NearbyStopFilter { private final TransitService transitService; diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java index f1d2f56dddc..183620aa2c1 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternConsideringNearbyStopFinder.java @@ -11,6 +11,21 @@ import org.opentripplanner.street.model.vertex.Vertex; import org.opentripplanner.transit.service.TransitService; +/** + * A {@link NearbyStopFinder} that filters nearby stops based on trip patterns and flex trips. + *

+ * This finder delegates to another NearbyStopFinder to find physically nearby stops, then filters + * them to include only stops that: + *

    + *
  • Are served by trip patterns where boarding/alighting is possible, OR
  • + *
  • Are served by flex trips (if FlexRouting is enabled), OR
  • + *
  • Are sometimes used by real-time trips (if IncludeStopsUsedRealTimeInTransfers is enabled)
  • + *
+ *

+ * For each trip pattern, only the closest stop is included to reduce the number of transfers + * generated. This significantly improves transfer generation performance by limiting transfers to + * the most relevant stops. + */ public class PatternConsideringNearbyStopFinder implements NearbyStopFinder { private final NearbyStopFilter filter; diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java index 2797d7bba74..60ab29b2073 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java @@ -12,6 +12,16 @@ import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.service.TransitService; +/** + * Filters nearby stops based on trip pattern availability. + *

+ * This filter ensures that transfers are only generated between stops that are served by trip + * patterns. For each trip pattern passing nearby, it keeps only the closest stop where boarding + * or alighting is possible (depending on direction). + *

+ * Stops without patterns may still be included if they are marked as sometimes-used by real-time + * updates (when the IncludeStopsUsedRealTimeInTransfers feature is enabled). + */ class PatternNearbyStopFilter implements NearbyStopFilter { private final TransitService transitService; diff --git a/application/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java b/application/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java index c4bd9b45742..20f327cd290 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java +++ b/application/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java @@ -234,7 +234,7 @@ boolean canBoard(int stopPosInPattern) { /** * Use {@link #canBoard(int)} if you want to check if a stop can be boarded at a given * stop position, ONLY use this method if you would like to search the stop-pattern for a - * bording. + * boarding. *

* Returns whether passengers can board at a given stop SOMEWHERE in the pattern, * considering all stops in case the pattern visit the same stop twice. diff --git a/application/src/main/java/org/opentripplanner/transit/model/site/RegularStop.java b/application/src/main/java/org/opentripplanner/transit/model/site/RegularStop.java index c40c750e2dc..e6c17c7bfbf 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/site/RegularStop.java +++ b/application/src/main/java/org/opentripplanner/transit/model/site/RegularStop.java @@ -134,7 +134,7 @@ public SubMode getNetexVehicleSubmode() { * DO NOT EXPOSE THIS PARAMETER ON ANY API. Business logic using this feature should only use it * to improve routing by including these stops when stops with no trip patterns would otherwise be * excluded for performance reasons. Incorrectly tagging stops with this flag is not critical, it - * will only degrade perfomance. + * will only degrade performance. * * @return {@code true} if this stop may be used by real-time trips despite having no scheduled * patterns, {@code false} otherwise From 73a22ad19e1ae4e9206efc714431369f16f3f0d6 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 18 Nov 2025 01:03:59 +0100 Subject: [PATCH 22/22] refactor: Correct spelling of 'Realtime' in IncludeStopsUsedRealTimeInTransfers --- .../framework/application/OTPFeature.java | 10 ++- .../filter/PatternNearbyStopFilter.java | 2 +- .../gtfs/mapping/StopMapper.java | 2 +- .../netex/mapping/QuayMapper.java | 2 +- .../transfer/DirectTransferGeneratorTest.java | 6 +- .../gtfs/mapping/StopMapperTest.java | 8 +-- doc/user/Configuration.md | 72 +++++++++---------- 7 files changed, 50 insertions(+), 52 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java b/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java index b4123f442c1..a1b303fc65e 100644 --- a/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java +++ b/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java @@ -43,7 +43,7 @@ public enum OTPFeature { ), FloatingBike(true, false, "Enable floating bike routing."), GtfsGraphQlApi(true, false, "Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md)."), - IncludeStopsUsedRealtimeInTransfers( + IncludeStopsUsedRealTimeInTransfers( false, false, """ @@ -52,11 +52,9 @@ public enum OTPFeature { changed or added by real-time updates. Since transfer generation happens before real-time updates are applied, OTP cannot know which stops will be needed. Instead, OTP will attempt to identify stops likely to be used by real-time updates at import time. Common cases include rail - stops (which often have late platform assignments) and stops reserved for replacement services - (which can be detected in NeTEx by examining the stop sub-mode). This feature has no effect if - `ConsiderPatternsForDirectTransfers` is disabled. - - This feature is only supported for NeTEx feeds, not for GTFS feeds. + stops (which often have late platform assignments) and stops reserved for replacement services. + This is detected examining the stop `subMode`(NeTEx) and `vehicleType`(GTFS). This feature has + no effect if `ConsiderPatternsForDirectTransfers` is disabled. """ ), /** diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java index 60ab29b2073..3e9f83191ae 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/transfer/filter/PatternNearbyStopFilter.java @@ -82,6 +82,6 @@ private List findPatternsForStop(RegularStop stop, boolean reverse } private boolean includeStopUsedRealtime(RegularStop stop) { - return OTPFeature.IncludeStopsUsedRealtimeInTransfers.isOn() && stop.isSometimesUsedRealtime(); + return OTPFeature.IncludeStopsUsedRealTimeInTransfers.isOn() && stop.isSometimesUsedRealtime(); } } diff --git a/application/src/main/java/org/opentripplanner/gtfs/mapping/StopMapper.java b/application/src/main/java/org/opentripplanner/gtfs/mapping/StopMapper.java index a0897ec40d9..33b61cb1c5f 100644 --- a/application/src/main/java/org/opentripplanner/gtfs/mapping/StopMapper.java +++ b/application/src/main/java/org/opentripplanner/gtfs/mapping/StopMapper.java @@ -110,7 +110,7 @@ private RegularStop doMap(org.onebusaway.gtfs.model.Stop gtfsStop) { * when it does not have any trips/routes visiting it. */ private static boolean mapSometimesUsedRealtime(TransitMode mode, Stop gtfsStop) { - if (OTPFeature.IncludeStopsUsedRealtimeInTransfers.isOn()) { + if (OTPFeature.IncludeStopsUsedRealTimeInTransfers.isOn()) { if (mode == TransitMode.RAIL) { return true; } diff --git a/application/src/main/java/org/opentripplanner/netex/mapping/QuayMapper.java b/application/src/main/java/org/opentripplanner/netex/mapping/QuayMapper.java index 22eff398616..0966280754c 100644 --- a/application/src/main/java/org/opentripplanner/netex/mapping/QuayMapper.java +++ b/application/src/main/java/org/opentripplanner/netex/mapping/QuayMapper.java @@ -79,7 +79,7 @@ private RegularStop map( String subMode = transitMode.subMode(); boolean sometimesUsedRealtime = false; - if (OTPFeature.IncludeStopsUsedRealtimeInTransfers.isOn()) { + if (OTPFeature.IncludeStopsUsedRealTimeInTransfers.isOn()) { if (transitMode.mainMode() == RAIL) { sometimesUsedRealtime = true; } diff --git a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java index e2eeb35fcc8..055bd0bfcb6 100644 --- a/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java +++ b/application/src/test/java/org/opentripplanner/graph_builder/module/transfer/DirectTransferGeneratorTest.java @@ -197,7 +197,7 @@ public void testStreetTransfersWithPatterns() { @Test public void testStreetTransfersWithPatternsIncludeRealTimeUsedStops() { - OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { + OTPFeature.IncludeStopsUsedRealTimeInTransfers.testOn(() -> { var repository = DirectTransferGeneratorTestData.of() .withPatterns() .withStreetGraph() @@ -250,7 +250,7 @@ public void testStreetTransfersWithMultipleRequestsWithPatterns() { @Test public void testStreetTransfersWithStationWithTransfersNotAllowed() { - OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { + OTPFeature.IncludeStopsUsedRealTimeInTransfers.testOn(() -> { var repository = DirectTransferGeneratorTestData.of() .withPatterns() .withStreetGraph() @@ -279,7 +279,7 @@ public void testStreetTransfersWithStationWithTransfersNotAllowed() { @Test public void testBikeRequestWithBikesAllowedTransfersWithIncludeEmptyRailStopsInTransfersOn() { - OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { + OTPFeature.IncludeStopsUsedRealTimeInTransfers.testOn(() -> { var repository = DirectTransferGeneratorTestData.of() .withPatterns() .withStreetGraph() diff --git a/application/src/test/java/org/opentripplanner/gtfs/mapping/StopMapperTest.java b/application/src/test/java/org/opentripplanner/gtfs/mapping/StopMapperTest.java index 9b283621bb8..149c45f8489 100644 --- a/application/src/test/java/org/opentripplanner/gtfs/mapping/StopMapperTest.java +++ b/application/src/test/java/org/opentripplanner/gtfs/mapping/StopMapperTest.java @@ -197,7 +197,7 @@ void testMapWithPlatformCode() { @Test void testMapSometimesUsedRealtimeForRailWithFeatureOn() { - OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { + OTPFeature.IncludeStopsUsedRealTimeInTransfers.testOn(() -> { Stop input = createBasicStop(); input.setVehicleType(VEHICLE_TYPE_RAIL); @@ -209,7 +209,7 @@ void testMapSometimesUsedRealtimeForRailWithFeatureOn() { @Test void testMapSometimesUsedRealtimeForRailReplacementBusWithFeatureOn() { - OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { + OTPFeature.IncludeStopsUsedRealTimeInTransfers.testOn(() -> { Stop input = createBasicStop(); input.setVehicleType(VEHICLE_TYPE_RAIL_REPLACEMENT_BUS); @@ -221,7 +221,7 @@ void testMapSometimesUsedRealtimeForRailReplacementBusWithFeatureOn() { @Test void testMapSometimesUsedRealtimeForBusWithFeatureOn() { - OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOn(() -> { + OTPFeature.IncludeStopsUsedRealTimeInTransfers.testOn(() -> { Stop input = createBasicStop(); input.setVehicleType(VEHICLE_TYPE_BUS); @@ -233,7 +233,7 @@ void testMapSometimesUsedRealtimeForBusWithFeatureOn() { @Test void testMapSometimesUsedRealtimeWithFeatureOff() { - OTPFeature.IncludeStopsUsedRealtimeInTransfers.testOff(() -> { + OTPFeature.IncludeStopsUsedRealTimeInTransfers.testOff(() -> { Stop input = createBasicStop(); input.setVehicleType(VEHICLE_TYPE_RAIL); diff --git a/doc/user/Configuration.md b/doc/user/Configuration.md index 76aaa529e8f..3bf80d7def3 100644 --- a/doc/user/Configuration.md +++ b/doc/user/Configuration.md @@ -222,42 +222,42 @@ Here is a list of all features which can be toggled on/off and their default val -| Feature | Description | Enabled by default | Sandbox | -|---------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------:|:-------:| -| `AlertMetrics` | Starts a background thread to continuously publish metrics about alerts. Needs to be enabled together with `ActuatorAPI`. | | | -| `APIServerInfo` | Enable the server info endpoint. | ✓️ | | -| `APIUpdaterStatus` | Enable endpoint for graph updaters status. | ✓️ | | -| `ConsiderPatternsForDirectTransfers` | Enable limiting transfers so that there is only a single transfer to each pattern. | ✓️ | | -| `DebugUi` | Enable the debug GraphQL client and web UI and located at the root of the web server as well as the debug map tiles it uses. Be aware that the map tiles are not a stable API and can change without notice. Use the [vector tiles feature](sandbox/MapboxVectorTilesApi.md) if you want a stable map tiles API. | ✓️ | | -| `ExtraTransferLegOnSameStop` | Should there be a transfer leg when transferring on the very same stop. Note that for in-seat/interlined transfers no transfer leg will be generated. | | | -| `FloatingBike` | Enable floating bike routing. | ✓️ | | -| `GtfsGraphQlApi` | Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md). | ✓️ | | -| `IncludeStopsUsedRealtimeInTransfers` | When generating transfers, stops without any patterns are excluded to improve performance if `ConsiderPatternsForDirectTransfers` is enabled. However, some stops are only used by trips changed or added by real-time updates. Since transfer generation happens before real-time updates are applied, OTP cannot know which stops will be needed. Instead, OTP will attempt to identify stops likely to be used by real-time updates at import time. Common cases include rail stops (which often have late platform assignments) and stops reserved for replacement services (which can be detected in NeTEx by examining the stop sub-mode). This feature has no effect if `ConsiderPatternsForDirectTransfers` is disabled. This feature is only supported for NeTEx feeds, not for GTFS feeds. | | | -| `MinimumTransferTimeIsDefinitive` | If the minimum transfer time is a lower bound (default) or the definitive time for the transfer. Set this to `true` if you want to set a transfer time lower than what OTP derives from OSM data. | | | -| `OnDemandRaptorTransfer` | Calculate transfers only when accessed by Raptor, instead of calculating and caching all transfers for the whole graph, for runtime requests which are not pre-cached in `transferCacheRequests` in router-config.json. This may help performance when doing local journey planning in a large graph. Requests which are specified in `transferCacheRequests` in router-config.json are not affected and are always pre-cached for the whole graph. | | | -| `OptimizeTransfers` | OTP will inspect all itineraries found and optimize where (which stops) the transfer will happen. Waiting time, priority and guaranteed transfers are taken into account. | ✓️ | | -| `ParallelRouting` | Enable performing parts of the trip planning in parallel. | | | -| `TransferConstraints` | Enforce transfers to happen according to the _transfers.txt_ (GTFS) and Interchanges (NeTEx). Turning this _off_ will increase the routing performance a little. | ✓️ | | -| `TransmodelGraphQlApi` | Enable the [Transmodel (NeTEx) GraphQL API](apis/TransmodelApi.md). | ✓️ | ✓️ | -| `ActuatorAPI` | Endpoint for actuators (service health status). | | ✓️ | -| `AsyncGraphQLFetchers` | Whether the @async annotation in the GraphQL schema should lead to the fetch being executed asynchronously. This allows batch or alias queries to run in parallel at the cost of consuming extra threads. | | | -| `WaitForGraphUpdateInPollingUpdaters` | Make all polling updaters wait for graph updates to complete before finishing. If this is not enabled, the updaters will finish after submitting the task to update the graph. | ✓️ | | -| `Emission` | Enable the emission sandbox module. | | ✓️ | -| `EmpiricalDelay` | Enable empirical delay sandbox module. | | ✓️ | -| `DataOverlay` | Enable usage of data overlay when calculating costs for the street network. | | ✓️ | -| `DebugRasterTiles` | Enable debug raster tile API. | | ✓️ | -| `FaresV2` | Enable import of GTFS-Fares v2 data. | | ✓️ | -| `FlexRouting` | Enable FLEX routing. | | ✓️ | -| `GoogleCloudStorage` | Enable Google Cloud Storage integration. | | ✓️ | -| `MultiCriteriaGroupMaxFilter` | Keep the best itinerary with respect to each criteria used in the transit-routing search. For example the itinerary with the lowest cost, fewest transfers, and each unique transit-group (transit-group-priority) is kept, even if the max-limit is exceeded. This is turned off by default for now, until this feature is well tested. | | | -| `RealtimeResolver` | When routing with ignoreRealtimeUpdates=true, add an extra step which populates results with real-time data | | ✓️ | -| `ReportApi` | Enable the report API. | | ✓️ | -| `SandboxAPIGeocoder` | Enable the Geocoder API. | | ✓️ | -| `SandboxAPIMapboxVectorTilesApi` | Enable Mapbox vector tiles API. | | ✓️ | -| `SandboxAPIParkAndRideApi` | Enable park-and-ride endpoint. | | ✓️ | -| `Sorlandsbanen` | Include train Sørlandsbanen in results when searching in south of Norway. Only relevant in Norway. | | ✓️ | -| `TransferAnalyzer` | Analyze transfers during graph build. | | ✓️ | -| `TriasApi` | TRIAS API. | | ✓️ | +| Feature | Description | Enabled by default | Sandbox | +|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------:|:-------:| +| `AlertMetrics` | Starts a background thread to continuously publish metrics about alerts. Needs to be enabled together with `ActuatorAPI`. | | | +| `APIServerInfo` | Enable the server info endpoint. | ✓️ | | +| `APIUpdaterStatus` | Enable endpoint for graph updaters status. | ✓️ | | +| `ConsiderPatternsForDirectTransfers` | Enable limiting transfers so that there is only a single transfer to each pattern. | ✓️ | | +| `DebugUi` | Enable the debug GraphQL client and web UI and located at the root of the web server as well as the debug map tiles it uses. Be aware that the map tiles are not a stable API and can change without notice. Use the [vector tiles feature](sandbox/MapboxVectorTilesApi.md) if you want a stable map tiles API. | ✓️ | | +| `ExtraTransferLegOnSameStop` | Should there be a transfer leg when transferring on the very same stop. Note that for in-seat/interlined transfers no transfer leg will be generated. | | | +| `FloatingBike` | Enable floating bike routing. | ✓️ | | +| `GtfsGraphQlApi` | Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md). | ✓️ | | +| `IncludeStopsUsedRealTimeInTransfers` | When generating transfers, stops without any patterns are excluded to improve performance if `ConsiderPatternsForDirectTransfers` is enabled. However, some stops are only used by trips changed or added by real-time updates. Since transfer generation happens before real-time updates are applied, OTP cannot know which stops will be needed. Instead, OTP will attempt to identify stops likely to be used by real-time updates at import time. Common cases include rail stops (which often have late platform assignments) and stops reserved for replacement services. This is detected examining the stop `subMode`(NeTEx) and `vehicleType`(GTFS). This feature has no effect if `ConsiderPatternsForDirectTransfers` is disabled. | | | +| `MinimumTransferTimeIsDefinitive` | If the minimum transfer time is a lower bound (default) or the definitive time for the transfer. Set this to `true` if you want to set a transfer time lower than what OTP derives from OSM data. | | | +| `OnDemandRaptorTransfer` | Calculate transfers only when accessed by Raptor, instead of calculating and caching all transfers for the whole graph, for runtime requests which are not pre-cached in `transferCacheRequests` in router-config.json. This may help performance when doing local journey planning in a large graph. Requests which are specified in `transferCacheRequests` in router-config.json are not affected and are always pre-cached for the whole graph. | | | +| `OptimizeTransfers` | OTP will inspect all itineraries found and optimize where (which stops) the transfer will happen. Waiting time, priority and guaranteed transfers are taken into account. | ✓️ | | +| `ParallelRouting` | Enable performing parts of the trip planning in parallel. | | | +| `TransferConstraints` | Enforce transfers to happen according to the _transfers.txt_ (GTFS) and Interchanges (NeTEx). Turning this _off_ will increase the routing performance a little. | ✓️ | | +| `TransmodelGraphQlApi` | Enable the [Transmodel (NeTEx) GraphQL API](apis/TransmodelApi.md). | ✓️ | ✓️ | +| `ActuatorAPI` | Endpoint for actuators (service health status). | | ✓️ | +| `AsyncGraphQLFetchers` | Whether the @async annotation in the GraphQL schema should lead to the fetch being executed asynchronously. This allows batch or alias queries to run in parallel at the cost of consuming extra threads. | | | +| `WaitForGraphUpdateInPollingUpdaters` | Make all polling updaters wait for graph updates to complete before finishing. If this is not enabled, the updaters will finish after submitting the task to update the graph. | ✓️ | | +| `Emission` | Enable the emission sandbox module. | | ✓️ | +| `EmpiricalDelay` | Enable empirical delay sandbox module. | | ✓️ | +| `DataOverlay` | Enable usage of data overlay when calculating costs for the street network. | | ✓️ | +| `DebugRasterTiles` | Enable debug raster tile API. | | ✓️ | +| `FaresV2` | Enable import of GTFS-Fares v2 data. | | ✓️ | +| `FlexRouting` | Enable FLEX routing. | | ✓️ | +| `GoogleCloudStorage` | Enable Google Cloud Storage integration. | | ✓️ | +| `MultiCriteriaGroupMaxFilter` | Keep the best itinerary with respect to each criteria used in the transit-routing search. For example the itinerary with the lowest cost, fewest transfers, and each unique transit-group (transit-group-priority) is kept, even if the max-limit is exceeded. This is turned off by default for now, until this feature is well tested. | | | +| `RealtimeResolver` | When routing with ignoreRealtimeUpdates=true, add an extra step which populates results with real-time data | | ✓️ | +| `ReportApi` | Enable the report API. | | ✓️ | +| `SandboxAPIGeocoder` | Enable the Geocoder API. | | ✓️ | +| `SandboxAPIMapboxVectorTilesApi` | Enable Mapbox vector tiles API. | | ✓️ | +| `SandboxAPIParkAndRideApi` | Enable park-and-ride endpoint. | | ✓️ | +| `Sorlandsbanen` | Include train Sørlandsbanen in results when searching in south of Norway. Only relevant in Norway. | | ✓️ | +| `TransferAnalyzer` | Analyze transfers during graph build. | | ✓️ | +| `TriasApi` | TRIAS API. | | ✓️ |