diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index c232ca6e3d3..dbaccd6e799 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -15,3 +15,6 @@ ead33ffe280dd7caf72cae5ff7a41542e8427636 # json file reformatting with prettier c287575df6798810a69fafc54c8c4e1867b71367 + +# prettier formatting for sandbox code +14f2d6bc7622649c817e9f45e4d1a9e6dd94847d \ No newline at end of file diff --git a/.github/workflows/debug-client.yml b/.github/workflows/debug-client.yml index 3c87e1c1b5c..e5f1c4516d1 100644 --- a/.github/workflows/debug-client.yml +++ b/.github/workflows/debug-client.yml @@ -2,9 +2,13 @@ name: Debug client on: push: + branches: + - dev-2.x paths: - 'client/**' pull_request: + branches: + - dev-2.x paths: - 'client/**' @@ -41,11 +45,11 @@ jobs: working-directory: client run: | npm install - npm run build -- --base https://cdn.jsdelivr.net/gh/opentripplanner/debug-client-assets@main/${VERSION}/ + npm run build -- --base https://www.opentripplanner.org/debug-client-assets/${VERSION}/ npm run coverage - name: Deploy compiled assets to repo - if: github.event_name == 'push' && github.ref == 'refs/heads/dev-2.x' + if: github.event_name == 'push' env: REMOTE: debug-client LOCAL_BRANCH: local-assets diff --git a/.github/workflows/schema-validation.yml b/.github/workflows/schema-validation.yml new file mode 100644 index 00000000000..8e2a3631df9 --- /dev/null +++ b/.github/workflows/schema-validation.yml @@ -0,0 +1,37 @@ +name: Validate schema changes + +on: + pull_request: + branches: + - dev-2.x + +jobs: + validate-gtfs: + if: github.repository_owner == 'opentripplanner' + name: Validate GraphQL schema changes + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: 'Fetch dev.2.x for diffing' + run: | + git fetch origin dev-2.x --depth 1 + + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install GraphQL Inspector + run: | + npm i --global @graphql-inspector/ci graphql @graphql-inspector/diff-command @graphql-inspector/graphql-loader @graphql-inspector/git-loader + + - name: Validate GTFS GraphQL schema changes + run: | + graphql-inspector diff 'git:origin/dev-2.x:application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls' 'application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls' + + - name: Validate Transmodel GraphQL schema changes + run: | + graphql-inspector diff 'git:origin/dev-2.x:application/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql' 'application/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql' diff --git a/.gitignore b/.gitignore index 70213c7ef70..397da64acba 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ graph.obj # IntelliJ creates these pid files when you attach the debugger to tests .attach_pid* +# draw.io backup files +*.svg.bkp + smoke-tests/*.jar smoke-tests/**/*.obj smoke-tests/**/*.pbf diff --git a/application/pom.xml b/application/pom.xml index 62eb761213f..035a4478688 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -302,7 +302,7 @@ org.onebusaway onebusaway-gtfs - 5.0.0 + 5.0.2 @@ -485,6 +485,20 @@ + + com.hubspot.maven.plugins + prettier-maven-plugin + + + src/main/java/**/*.java + src/ext/java/**/*.java + src/test/java/**/*.java + src/ext-test/java/**/*.java + src/**/*.json + src/test/resources/org/opentripplanner/apis/**/*.graphql + + + diff --git a/application/src/client/index.html b/application/src/client/index.html index d105b0aab08..0e8f974de9e 100644 --- a/application/src/client/index.html +++ b/application/src/client/index.html @@ -5,10 +5,10 @@ OTP Debug - - + +
- + \ No newline at end of file diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/impl/GtfsFaresV2ServiceTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/impl/GtfsFaresV2ServiceTest.java index 19b55489778..47875c65801 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/fares/impl/GtfsFaresV2ServiceTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/impl/GtfsFaresV2ServiceTest.java @@ -12,7 +12,6 @@ import java.util.Set; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.opentripplanner.ext.fares.model.Distance; import org.opentripplanner.ext.fares.model.FareDistance; import org.opentripplanner.ext.fares.model.FareDistance.LinearDistance; import org.opentripplanner.ext.fares.model.FareLegRule; @@ -23,6 +22,7 @@ import org.opentripplanner.model.plan.Place; import org.opentripplanner.model.plan.PlanTestConstants; import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; +import org.opentripplanner.transit.model.basic.Distance; import org.opentripplanner.transit.model.basic.Money; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -331,15 +331,30 @@ class DistanceFares { List distanceRules = List.of( FareLegRule .of(DISTANCE_ID, tenKmProduct) - .withFareDistance(new LinearDistance(Distance.ofKilometers(7), Distance.ofKilometers(10))) + .withFareDistance( + new LinearDistance( + Distance.ofKilometersBoxed(7d, ignore -> {}).orElse(null), + Distance.ofKilometersBoxed(10d, ignore -> {}).orElse(null) + ) + ) .build(), FareLegRule .of(DISTANCE_ID, threeKmProduct) - .withFareDistance(new LinearDistance(Distance.ofKilometers(3), Distance.ofKilometers(6))) + .withFareDistance( + new LinearDistance( + Distance.ofKilometersBoxed(3d, ignore -> {}).orElse(null), + Distance.ofKilometersBoxed(6d, ignore -> {}).orElse(null) + ) + ) .build(), FareLegRule .of(DISTANCE_ID, twoKmProduct) - .withFareDistance(new LinearDistance(Distance.ofMeters(0), Distance.ofMeters(2000))) + .withFareDistance( + new LinearDistance( + Distance.ofMetersBoxed(0d, ignore -> {}).orElse(null), + Distance.ofMetersBoxed(2000d, ignore -> {}).orElse(null) + ) + ) .build() ); diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java index c819d4abc2e..89ea6b22dbc 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java @@ -226,12 +226,12 @@ void calculateFareThatIncludesNoFreeTransfers() { ); calculateFare(rides, FareType.youth, Money.ZERO_USD); // We don't get any fares for the skagit transit leg below here because they don't accept ORCA (electronic) - calculateFare(rides, FareType.electronicSpecial, ONE_DOLLAR.plus(ONE_DOLLAR).plus(DEFAULT_TEST_RIDE_PRICE.times(2))); calculateFare( rides, - FareType.electronicRegular, - DEFAULT_TEST_RIDE_PRICE.times(4) + FareType.electronicSpecial, + ONE_DOLLAR.plus(ONE_DOLLAR).plus(DEFAULT_TEST_RIDE_PRICE.times(2)) ); + calculateFare(rides, FareType.electronicRegular, DEFAULT_TEST_RIDE_PRICE.times(4)); calculateFare( rides, FareType.electronicSenior, 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 fb19f1dff36..99bd9ea5cbf 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 @@ -30,6 +30,7 @@ import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.routing.api.RoutingService; import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.api.request.framework.TimeAndCostPenalty; import org.opentripplanner.routing.api.request.request.filter.AllowAllTransitFilter; import org.opentripplanner.routing.graph.Graph; @@ -237,12 +238,16 @@ private static List getItineraries( ); var modes = request.journey().modes().copyOf(); - modes.withEgressMode(FLEXIBLE); if (onlyDirect) { - modes.withDirectMode(FLEXIBLE); - request.journey().transit().setFilters(List.of(ExcludeAllTransitFilter.of())); + modes + .withDirectMode(FLEXIBLE) + .withAccessMode(StreetMode.WALK) + .withEgressMode(StreetMode.WALK); + request.journey().transit().setFilters(List.of(AllowAllTransitFilter.of())); + request.journey().transit().disable(); } else { + modes.withEgressMode(FLEXIBLE); request.journey().transit().setFilters(List.of(AllowAllTransitFilter.of())); } diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexStopTimesForTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexStopTimesForTest.java index b67ae85a434..9cff9177e2e 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexStopTimesForTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexStopTimesForTest.java @@ -96,7 +96,4 @@ public static StopTime regularStop(int arrivalTime, int departureTime) { stopTime.setTrip(TRIP); return stopTime; } - - - } diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/filter/FilterMapperTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/filter/FilterMapperTest.java new file mode 100644 index 00000000000..b8ab9f27084 --- /dev/null +++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/filter/FilterMapperTest.java @@ -0,0 +1,55 @@ +package org.opentripplanner.ext.flex.filter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.opentripplanner.ext.flex.filter.FlexTripFilterRequest.Filter; +import org.opentripplanner.routing.api.request.request.filter.AllowAllTransitFilter; +import org.opentripplanner.routing.api.request.request.filter.SelectRequest; +import org.opentripplanner.routing.api.request.request.filter.TransitFilterRequest; + +class FilterMapperTest { + + @Test + void allowAll() { + var filter = FilterMapper.mapFilters(List.of(AllowAllTransitFilter.of())); + assertEquals(FlexTripFilter.ALLOW_ALL, filter); + } + + @Test + void distinct() { + var filter = FilterMapper.mapFilters( + List.of(AllowAllTransitFilter.of(), AllowAllTransitFilter.of()) + ); + assertEquals(FlexTripFilter.ALLOW_ALL, filter); + } + + @Test + void routes() { + var select = SelectRequest.of().withRoutes(List.of(id("r1"))).build(); + var transitFilter = TransitFilterRequest.of().addSelect(select).addNot(select).build(); + var filter = FilterMapper.mapFilters(List.of(transitFilter)); + assertEquals( + new FlexTripFilter( + List.of(new Filter(Set.of(), Set.of(), Set.of(id("r1")), Set.of(id("r1")))) + ), + filter + ); + } + + @Test + void agencies() { + var select = SelectRequest.of().withAgencies(List.of(id("a1"))).build(); + var transitFilter = TransitFilterRequest.of().addSelect(select).addNot(select).build(); + var filter = FilterMapper.mapFilters(List.of(transitFilter)); + assertEquals( + new FlexTripFilter( + List.of(new Filter(Set.of(id("a1")), Set.of(id("a1")), Set.of(), Set.of())) + ), + filter + ); + } +} diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/filter/FlexTripFilterTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/filter/FlexTripFilterTest.java new file mode 100644 index 00000000000..a719c4a7b36 --- /dev/null +++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/filter/FlexTripFilterTest.java @@ -0,0 +1,177 @@ +package org.opentripplanner.ext.flex.filter; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.of; +import static org.opentripplanner.ext.flex.filter.FlexTripFilterTest.FilterResult.EXCLUDED; +import static org.opentripplanner.ext.flex.filter.FlexTripFilterTest.FilterResult.SELECTED; +import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.agency; +import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.ext.flex.filter.FlexTripFilterRequest.Filter; +import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.organization.Agency; +import org.opentripplanner.transit.model.timetable.Trip; + +class FlexTripFilterTest { + + private static List allowAllCases() { + return List.of( + of(route("r1"), agency("a1")), + of(route("r1"), agency("a2")), + of(route("r2"), agency("a2")), + of(route("r3"), agency("a3")) + ); + } + + @ParameterizedTest + @MethodSource("allowAllCases") + void allowAll(Route route, Agency agency) { + var filter = FlexTripFilter.ALLOW_ALL; + + var trip = trip(route, agency); + + assertTrue(filter.allowsTrip(trip)); + } + + private static List selectedAgencyCases() { + return List.of( + of(route("r1"), agency("a1"), EXCLUDED), + of(route("r1"), agency("a2"), EXCLUDED), + of(route("selected"), agency("a2"), EXCLUDED), + of(route("selected"), agency("selected"), SELECTED), + of(route("r1"), agency("selected"), SELECTED) + ); + } + + @ParameterizedTest + @MethodSource("selectedAgencyCases") + void selectedAgency(Route route, Agency agency, FilterResult expectation) { + var filter = new FlexTripFilter( + List.of(new Filter(Set.of(id("selected")), Set.of(), Set.of(), Set.of())) + ); + + var trip = trip(route, agency); + + expectation.assertFilter(trip, filter); + } + + private static List selectedRouteCases() { + return List.of( + of(route("r1"), agency("a1"), EXCLUDED), + of(route("r1"), agency("a2"), EXCLUDED), + of(route("selected"), agency("a2"), SELECTED), + of(route("selected"), agency("selected"), SELECTED), + of(route("r1"), agency("selected"), EXCLUDED) + ); + } + + @ParameterizedTest + @MethodSource("selectedRouteCases") + void selectedRoute(Route route, Agency agency, FilterResult expectation) { + var filter = new FlexTripFilter( + List.of(new Filter(Set.of(), Set.of(), Set.of(id("selected")), Set.of())) + ); + + var trip = trip(route, agency); + + expectation.assertFilter(trip, filter); + } + + private static List excludedAgencyCases() { + return List.of( + of(route("r1"), agency("a1"), SELECTED), + of(route("r1"), agency("a2"), SELECTED), + of(route("selected"), agency("a2"), SELECTED), + of(route("excluded"), agency("excluded"), EXCLUDED), + of(route("r1"), agency("excluded"), EXCLUDED) + ); + } + + @ParameterizedTest + @MethodSource("excludedAgencyCases") + void excludedAgency(Route route, Agency agency, FilterResult expectation) { + var filter = new FlexTripFilter( + List.of(new Filter(Set.of(), Set.of(id("excluded")), Set.of(), Set.of())) + ); + + var trip = trip(route, agency); + + expectation.assertFilter(trip, filter); + } + + private static List excludedRouteCases() { + return List.of( + of(route("r1"), agency("a1"), SELECTED), + of(route("r1"), agency("a2"), SELECTED), + of(route("selected"), agency("a2"), SELECTED), + of(route("excluded"), agency("selected"), EXCLUDED), + of(route("r1"), agency("excluded"), SELECTED) + ); + } + + @ParameterizedTest + @MethodSource("excludedRouteCases") + void excludedRoute(Route route, Agency agency, FilterResult expectation) { + var filter = new FlexTripFilter( + List.of(new Filter(Set.of(), Set.of(), Set.of(), Set.of(id("excluded")))) + ); + + var trip = trip(route, agency); + + expectation.assertFilter(trip, filter); + } + + private static List excludedCases() { + return List.of( + of(route("r1"), agency("a1"), SELECTED), + of(route("r1"), agency("a2"), SELECTED), + of(route("selected"), agency("a2"), SELECTED), + of(route("excluded-route"), agency("a2"), EXCLUDED), + of(route("excluded-route"), agency("excluded-agency"), EXCLUDED), + of(route("r2"), agency("excluded-agency"), EXCLUDED) + ); + } + + @ParameterizedTest + @MethodSource("excludedCases") + void excluded(Route route, Agency agency, FilterResult expectation) { + var filter = new FlexTripFilter( + List.of( + new Filter(Set.of(), Set.of(id("excluded-agency")), Set.of(), Set.of(id("excluded-route"))) + ) + ); + + var trip = trip(route, agency); + + expectation.assertFilter(trip, filter); + } + + private static Trip trip(Route route, Agency agency) { + var r = route.copy().withAgency(agency).build(); + return TimetableRepositoryForTest.trip("1").withRoute(r).build(); + } + + private static Route route(String routeId) { + return TimetableRepositoryForTest.route(routeId).build(); + } + + enum FilterResult { + SELECTED, + EXCLUDED; + + void assertFilter(Trip trip, FlexTripFilter filter) { + if (this == EXCLUDED) { + Assertions.assertFalse(filter.allowsTrip(trip)); + } else if (this == SELECTED) { + assertTrue(filter.allowsTrip(trip)); + } + } + } +} diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripIntegrationTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripIntegrationTest.java index 3a8e33cfbf2..055a6a815d5 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripIntegrationTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripIntegrationTest.java @@ -21,6 +21,7 @@ import org.opentripplanner.ext.flex.FlexIntegrationTestData; import org.opentripplanner.ext.flex.FlexParameters; import org.opentripplanner.ext.flex.FlexRouter; +import org.opentripplanner.ext.flex.filter.FlexTripFilter; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.geometry.EncodedPolyline; import org.opentripplanner.framework.i18n.I18NString; @@ -104,6 +105,7 @@ void calculateDirectFare() { graph, new DefaultTransitService(timetableRepository), FlexParameters.defaultValues(), + FlexTripFilter.ALLOW_ALL, OffsetDateTime.parse("2021-11-12T10:15:24-05:00").toInstant(), null, 1, diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java index 4beefeb271e..5adf8c7264a 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java @@ -45,10 +45,7 @@ private static List> isNotScheduledDeviatedTripCases() { areaWithContinuousStopping("10:40"), regularStop("10:50", "11:00") ), - List.of( - regularStop("10:10"), - regularStop("10:20") - ) + List.of(regularStop("10:10"), regularStop("10:20")) ); } @@ -57,6 +54,4 @@ private static List> isNotScheduledDeviatedTripCases() { void isNotScheduledDeviatedTrip(List stopTimes) { assertFalse(ScheduledDeviatedTrip.isScheduledDeviatedFlexTrip(stopTimes)); } - - -} \ No newline at end of file +} diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java index b7f2706d25a..6ce964045ab 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java @@ -48,12 +48,20 @@ class IsUnscheduledTrip { private static final StopTime SCHEDULED_STOP = FlexStopTimesForTest.regularStop("10:00"); private static final StopTime UNSCHEDULED_STOP = FlexStopTimesForTest.area("10:10", "10:20"); - private static final StopTime CONTINUOUS_PICKUP_STOP = FlexStopTimesForTest.regularStopWithContinuousPickup("10:30"); - private static final StopTime CONTINUOUS_DROP_OFF_STOP = FlexStopTimesForTest.regularStopWithContinuousDropOff("10:40"); + private static final StopTime CONTINUOUS_PICKUP_STOP = FlexStopTimesForTest.regularStopWithContinuousPickup( + "10:30" + ); + private static final StopTime CONTINUOUS_DROP_OFF_STOP = FlexStopTimesForTest.regularStopWithContinuousDropOff( + "10:40" + ); // disallowed by the GTFS spec - private static final StopTime FLEX_AND_CONTINUOUS_PICKUP_STOP = FlexStopTimesForTest.areaWithContinuousPickup("10:50"); - private static final StopTime FLEX_AND_CONTINUOUS_DROP_OFF_STOP = FlexStopTimesForTest.areaWithContinuousDropOff("11:00"); + private static final StopTime FLEX_AND_CONTINUOUS_PICKUP_STOP = FlexStopTimesForTest.areaWithContinuousPickup( + "10:50" + ); + private static final StopTime FLEX_AND_CONTINUOUS_DROP_OFF_STOP = FlexStopTimesForTest.areaWithContinuousDropOff( + "11:00" + ); static List> notUnscheduled() { return List.of( diff --git a/application/src/ext-test/java/org/opentripplanner/ext/geocoder/StopClusterMapperTest.java b/application/src/ext-test/java/org/opentripplanner/ext/geocoder/StopClusterMapperTest.java index fdff7787fb5..6af0eab2e18 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/geocoder/StopClusterMapperTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/geocoder/StopClusterMapperTest.java @@ -26,7 +26,7 @@ class StopClusterMapperTest { .siteRepositoryBuilder() .withRegularStops(STOPS) .build(); - private static final TimetableRepository TRANSIT_MODEL = new TimetableRepository( + private static final TimetableRepository TIMETABLE_REPOSITORY = new TimetableRepository( SITE_REPOSITORY, new Deduplicator() ); @@ -40,8 +40,8 @@ void clusterConsolidatedStops() { var repo = new DefaultStopConsolidationRepository(); repo.addGroups(List.of(new ConsolidatedStopGroup(STOP_A.getId(), List.of(STOP_B.getId())))); - var service = new DefaultStopConsolidationService(repo, TRANSIT_MODEL); - var mapper = new StopClusterMapper(new DefaultTransitService(TRANSIT_MODEL), service); + var service = new DefaultStopConsolidationService(repo, TIMETABLE_REPOSITORY); + var mapper = new StopClusterMapper(new DefaultTransitService(TIMETABLE_REPOSITORY), service); var clusters = mapper.generateStopClusters(LOCATIONS, List.of()); diff --git a/application/src/ext-test/java/org/opentripplanner/ext/restapi/mapping/EnumMapperTest.java b/application/src/ext-test/java/org/opentripplanner/ext/restapi/mapping/EnumMapperTest.java index 5b03ada1c1f..151c1759a25 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/restapi/mapping/EnumMapperTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/restapi/mapping/EnumMapperTest.java @@ -46,5 +46,4 @@ private , A extends Enum> void verifyExplicitMatch( } assertTrue(rest.isEmpty()); } - } diff --git a/application/src/ext-test/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdaterTest.java b/application/src/ext-test/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdaterTest.java index f4cfa8ceb80..c0336bb6d7d 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdaterTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdaterTest.java @@ -1,9 +1,9 @@ package org.opentripplanner.ext.siri.updater.azure; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.Mockito.*; import com.azure.core.util.ExpandableStringEnum; @@ -56,21 +56,22 @@ public void setUp() throws Exception { when(mockConfig.isFuzzyTripMatching()).thenReturn(true); // Create a spy on AbstractAzureSiriUpdater with the mock configuration - updater = spy(new AbstractAzureSiriUpdater(mockConfig) { - @Override - protected void messageConsumer(ServiceBusReceivedMessageContext messageContext) { - } - - @Override - protected void errorConsumer(ServiceBusErrorContext errorContext) { - } - - @Override - protected void initializeData(String url, - Consumer consumer - ) throws URISyntaxException { - } - }); + updater = + spy( + new AbstractAzureSiriUpdater(mockConfig) { + @Override + protected void messageConsumer(ServiceBusReceivedMessageContext messageContext) {} + + @Override + protected void errorConsumer(ServiceBusErrorContext errorContext) {} + + @Override + protected void initializeData( + String url, + Consumer consumer + ) throws URISyntaxException {} + } + ); task = mock(AbstractAzureSiriUpdater.CheckedRunnable.class); } @@ -81,8 +82,8 @@ protected void initializeData(String url, */ @Test void testExecuteWithRetry_FullBackoffSequence() throws Throwable { - final int totalRunCalls = 10; // 9 failures + 1 success - final int totalSleepCalls = 9; // 9 retries + final int totalRunCalls = 10; // 9 failures + 1 success + final int totalSleepCalls = 9; // 9 retries doNothing().when(updater).sleep(anyInt()); @@ -97,7 +98,8 @@ void testExecuteWithRetry_FullBackoffSequence() throws Throwable { .doThrow(createServiceBusException(ServiceBusFailureReason.SERVICE_BUSY)) .doThrow(createServiceBusException(ServiceBusFailureReason.SERVICE_BUSY)) .doNothing() // Succeed on the 10th attempt - .when(task).run(); + .when(task) + .run(); updater.executeWithRetry(task, "Test Task"); @@ -126,14 +128,20 @@ void testExecuteWithRetry_FullBackoffSequence() throws Throwable { public void testExecuteWithRetry_NonRetryableException() throws Throwable { doNothing().when(updater).sleep(anyInt()); - ServiceBusException serviceBusException = createServiceBusException(ServiceBusFailureReason.MESSAGE_SIZE_EXCEEDED); + ServiceBusException serviceBusException = createServiceBusException( + ServiceBusFailureReason.MESSAGE_SIZE_EXCEEDED + ); doThrow(serviceBusException).when(task).run(); try { updater.executeWithRetry(task, "Test Task"); } catch (ServiceBusException e) { - assertEquals(ServiceBusFailureReason.MESSAGE_SIZE_EXCEEDED, e.getReason(), "Exception should have reason MESSAGE_SIZE_EXCEEDED"); + assertEquals( + ServiceBusFailureReason.MESSAGE_SIZE_EXCEEDED, + e.getReason(), + "Exception should have reason MESSAGE_SIZE_EXCEEDED" + ); } verify(updater, never()).sleep(anyInt()); @@ -153,12 +161,15 @@ public void testExecuteWithRetry_MultipleRetriesThenSuccess() throws Throwable { .doThrow(createServiceBusException(ServiceBusFailureReason.SERVICE_BUSY)) .doThrow(createServiceBusException(ServiceBusFailureReason.SERVICE_BUSY)) .doNothing() - .when(task).run(); + .when(task) + .run(); doAnswer(invocation -> { - latch.countDown(); - return null; - }).when(updater).sleep(anyInt()); + latch.countDown(); + return null; + }) + .when(updater) + .sleep(anyInt()); updater.executeWithRetry(task, "Test Task"); @@ -169,11 +180,14 @@ public void testExecuteWithRetry_MultipleRetriesThenSuccess() throws Throwable { verify(updater, times(retriesBeforeSuccess)).sleep(sleepCaptor.capture()); var sleepDurations = sleepCaptor.getAllValues(); - long[] expectedBackoffSequence = {1000, 2000, 4000}; + long[] expectedBackoffSequence = { 1000, 2000, 4000 }; for (int i = 0; i < expectedBackoffSequence.length; i++) { - assertEquals(expectedBackoffSequence[i], Long.valueOf(sleepDurations.get(i)), - "Backoff duration mismatch at retry " + (i + 1)); + assertEquals( + expectedBackoffSequence[i], + Long.valueOf(sleepDurations.get(i)), + "Backoff duration mismatch at retry " + (i + 1) + ); } verify(task, times(retriesBeforeSuccess + 1)).run(); @@ -205,14 +219,17 @@ public void testExecuteWithRetry_OneRetryThenSuccess() throws Throwable { doThrow(createServiceBusException(ServiceBusFailureReason.SERVICE_BUSY)) .doNothing() - .when(task).run(); + .when(task) + .run(); doAnswer(invocation -> { - if (invocation.getArgument(0).equals(1000)) { - latch.countDown(); - } - return null; - }).when(updater).sleep(anyInt()); + if (invocation.getArgument(0).equals(1000)) { + latch.countDown(); + } + return null; + }) + .when(updater) + .sleep(anyInt()); updater.executeWithRetry(task, "Test Task"); @@ -232,10 +249,17 @@ public void testExecuteWithRetry_OneRetryThenSuccess() throws Throwable { @ParameterizedTest(name = "shouldRetry with reason {0} should return {1}") @MethodSource("provideServiceBusFailureReasons") @DisplayName("Test shouldRetry for all ServiceBusFailureReason values") - void testShouldRetry_ServiceBusFailureReasons(ServiceBusFailureReason reason, boolean expectedRetry) throws Exception { + void testShouldRetry_ServiceBusFailureReasons( + ServiceBusFailureReason reason, + boolean expectedRetry + ) throws Exception { ServiceBusException serviceBusException = createServiceBusException(reason); boolean result = updater.shouldRetry(serviceBusException); - assertEquals(expectedRetry, result, "shouldRetry should return " + expectedRetry + " for reason " + reason); + assertEquals( + expectedRetry, + result, + "shouldRetry should return " + expectedRetry + " for reason " + reason + ); } /** @@ -258,7 +282,11 @@ public void testShouldRetry_NonServiceBusException() { public void testShouldRetry_CoversAllReasons() { long enumCount = getExpandableStringEnumValues(ServiceBusFailureReason.class).size(); long testCaseCount = provideServiceBusFailureReasons().count(); - assertEquals(enumCount, testCaseCount, "All ServiceBusFailureReason values should be covered by tests."); + assertEquals( + enumCount, + testCaseCount, + "All ServiceBusFailureReason values should be covered by tests." + ); } @Test @@ -268,15 +296,24 @@ void testExecuteWithRetry_InterruptedException() throws Throwable { doThrow(createServiceBusException(ServiceBusFailureReason.SERVICE_BUSY)) .doThrow(new InterruptedException("Sleep interrupted")) - .when(task).run(); + .when(task) + .run(); doNothing().when(updater).sleep(1000); - InterruptedException thrownException = assertThrows(InterruptedException.class, () -> { - updater.executeWithRetry(task, "Test Task"); - }, "Expected executeWithRetry to throw InterruptedException"); + InterruptedException thrownException = assertThrows( + InterruptedException.class, + () -> { + updater.executeWithRetry(task, "Test Task"); + }, + "Expected executeWithRetry to throw InterruptedException" + ); - assertEquals("Sleep interrupted", thrownException.getMessage(), "Exception message should match"); + assertEquals( + "Sleep interrupted", + thrownException.getMessage(), + "Exception message should match" + ); verify(updater, times(expectedSleepCalls)).sleep(1000); verify(task, times(expectedRunCalls)).run(); assertTrue(Thread.currentThread().isInterrupted(), "Thread should be interrupted"); @@ -291,7 +328,8 @@ void testExecuteWithRetry_OtpHttpClientException() throws Throwable { .doThrow(new OtpHttpClientException("could not get historical data")) .doThrow(new OtpHttpClientException("could not get historical data")) .doNothing() - .when(task).run(); + .when(task) + .run(); doNothing().when(updater).sleep(anyInt()); @@ -304,8 +342,11 @@ void testExecuteWithRetry_OtpHttpClientException() throws Throwable { List expectedBackoffSequence = Arrays.asList(1000, 2000, 4000); for (int i = 0; i < retryAttempts; i++) { - assertEquals(expectedBackoffSequence.get(i), sleepDurations.get(i), - "Backoff duration mismatch at retry " + (i + 1)); + assertEquals( + expectedBackoffSequence.get(i), + sleepDurations.get(i), + "Backoff duration mismatch at retry " + (i + 1) + ); } verify(task, times(retryAttempts + 1)).run(); @@ -318,9 +359,13 @@ void testExecuteWithRetry_UnexpectedException() throws Throwable { Exception unexpectedException = new NullPointerException("Unexpected null value"); doThrow(unexpectedException).when(task).run(); - Exception thrown = assertThrows(NullPointerException.class, () -> { - updater.executeWithRetry(task, "Test Task"); - }, "Expected executeWithRetry to throw NullPointerException"); + Exception thrown = assertThrows( + NullPointerException.class, + () -> { + updater.executeWithRetry(task, "Test Task"); + }, + "Expected executeWithRetry to throw NullPointerException" + ); assertEquals("Unexpected null value", thrown.getMessage(), "Exception message should match"); verify(updater, never()).sleep(anyInt()); @@ -344,7 +389,6 @@ private static Stream provideServiceBusFailureReasons() { Arguments.of(ServiceBusFailureReason.QUOTA_EXCEEDED, true), Arguments.of(ServiceBusFailureReason.GENERAL_ERROR, true), Arguments.of(ServiceBusFailureReason.UNAUTHORIZED, true), - // Non-Retryable Errors Arguments.of(ServiceBusFailureReason.MESSAGING_ENTITY_NOT_FOUND, false), Arguments.of(ServiceBusFailureReason.MESSAGING_ENTITY_DISABLED, false), @@ -361,7 +405,10 @@ private static Stream provideServiceBusFailureReasons() { * @return A ServiceBusException instance with the specified reason. */ private ServiceBusException createServiceBusException(ServiceBusFailureReason reason) { - ServiceBusException exception = new ServiceBusException(new Throwable(), ServiceBusErrorSource.RECEIVE); + ServiceBusException exception = new ServiceBusException( + new Throwable(), + ServiceBusErrorSource.RECEIVE + ); try { Field reasonField = ServiceBusException.class.getDeclaredField("reason"); reasonField.setAccessible(true); @@ -379,7 +426,9 @@ private ServiceBusException createServiceBusException(ServiceBusFailureReason re * @param The type parameter extending ExpandableStringEnum. * @return A Collection of all registered instances. */ - private static > Collection getExpandableStringEnumValues(Class clazz) { + private static > Collection getExpandableStringEnumValues( + Class clazz + ) { try { Method valuesMethod = ExpandableStringEnum.class.getDeclaredMethod("values", Class.class); valuesMethod.setAccessible(true); @@ -390,4 +439,4 @@ private static > Collection getExpandableSt throw new RuntimeException("Failed to retrieve values from ExpandableStringEnum.", e); } } -} \ No newline at end of file +} diff --git a/application/src/ext-test/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSourceTest.java b/application/src/ext-test/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSourceTest.java index 84161c00484..d8f06ef50c5 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSourceTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSourceTest.java @@ -7,8 +7,8 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; -import org.opentripplanner.updater.vehicle_rental.datasources.params.RentalPickupType; import org.opentripplanner.updater.spi.HttpHeaders; +import org.opentripplanner.updater.vehicle_rental.datasources.params.RentalPickupType; class SmooveBikeRentalDataSourceTest { diff --git a/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java b/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java index ae4386e9dd6..cad94672576 100644 --- a/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java +++ b/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java @@ -110,7 +110,11 @@ public void vehicleParkingGeometryTest() { var nodeAdapter = newNodeAdapterForTest(config); var tiles = VectorTileConfig.mapVectorTilesParameters(nodeAdapter, "vectorTiles"); assertEquals(1, tiles.layers().size()); - var builder = new VehicleParkingsLayerBuilder(new DefaultVehicleParkingService(repo), tiles.layers().getFirst(), Locale.US); + var builder = new VehicleParkingsLayerBuilder( + new DefaultVehicleParkingService(repo), + tiles.layers().getFirst(), + Locale.US + ); List geometries = builder.getGeometries(new Envelope(0.99, 1.01, 1.99, 2.01)); diff --git a/application/src/ext/java/org/opentripplanner/ext/fares/model/Distance.java b/application/src/ext/java/org/opentripplanner/ext/fares/model/Distance.java deleted file mode 100644 index f30712d4cad..00000000000 --- a/application/src/ext/java/org/opentripplanner/ext/fares/model/Distance.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.opentripplanner.ext.fares.model; - -import org.opentripplanner.utils.tostring.ValueObjectToStringBuilder; - -public class Distance { - - private static final int METERS_PER_KM = 1000; - private final double meters; - - /** Returns a Distance object representing the given number of meters */ - public Distance(double value) { - this.meters = value; - } - - /** Returns a Distance object representing the given number of meters */ - public static Distance ofMeters(double value) { - return new Distance(value); - } - - /** Returns a Distance object representing the given number of kilometers */ - public static Distance ofKilometers(double value) { - return new Distance(value * METERS_PER_KM); - } - - /** Returns the distance in meters */ - public double toMeters() { - return this.meters; - } - - @Override - public boolean equals(Object other) { - if (other instanceof Distance distance) { - return distance.meters == this.meters; - } else { - return false; - } - } - - @Override - public String toString() { - if (meters < METERS_PER_KM) { - return ValueObjectToStringBuilder.of().addNum(meters, "m").toString(); - } else { - return ValueObjectToStringBuilder.of().addNum(meters / 1000, "km").toString(); - } - } -} diff --git a/application/src/ext/java/org/opentripplanner/ext/fares/model/FareDistance.java b/application/src/ext/java/org/opentripplanner/ext/fares/model/FareDistance.java index 9c18e3947b3..bbbc5e64426 100644 --- a/application/src/ext/java/org/opentripplanner/ext/fares/model/FareDistance.java +++ b/application/src/ext/java/org/opentripplanner/ext/fares/model/FareDistance.java @@ -1,5 +1,7 @@ package org.opentripplanner.ext.fares.model; +import org.opentripplanner.transit.model.basic.Distance; + /** Represents a distance metric used in distance-based fare computation*/ public sealed interface FareDistance { /** Represents the number of stops as a distance metric in fare computation */ diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/FlexIndex.java b/application/src/ext/java/org/opentripplanner/ext/flex/FlexIndex.java index 8097bd05c6e..852447c1135 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/FlexIndex.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/FlexIndex.java @@ -60,8 +60,8 @@ public Collection getTransfersFromStop(StopLocation stopLocation) return flexTripsByStop.get(stopLocation); } - public Route getRouteById(FeedScopedId id) { - return routeById.get(id); + public boolean contains(Route route) { + return routeById.containsKey(route.getId()); } public Collection getAllFlexRoutes() { diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java b/application/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java index e7f3d3544ab..a8b3e18c727 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java @@ -9,6 +9,7 @@ import java.util.List; import javax.annotation.Nullable; import org.opentripplanner.astar.model.GraphPath; +import org.opentripplanner.ext.flex.filter.FlexTripFilter; import org.opentripplanner.ext.flex.flexpathcalculator.DirectFlexPathCalculator; import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; import org.opentripplanner.ext.flex.flexpathcalculator.StreetFlexPathCalculator; @@ -52,11 +53,13 @@ public class FlexRouter { private final int requestedTime; private final int requestedBookingTime; private final List dates; + private final FlexTripFilter flexTripFilter; public FlexRouter( Graph graph, TransitService transitService, FlexParameters flexParameters, + FlexTripFilter filters, Instant requestedTime, @Nullable Instant requestedBookingTime, int additionalPastSearchDays, @@ -70,6 +73,7 @@ public FlexRouter( this.streetAccesses = streetAccesses; this.streetEgresses = egressTransfers; this.flexIndex = transitService.getFlexIndex(); + this.flexTripFilter = filters; this.callbackService = new CallbackAdapter(); this.graphPathToItineraryMapper = new GraphPathToItineraryMapper( @@ -114,7 +118,8 @@ public List createFlexOnlyItineraries(boolean arriveBy) { callbackService, accessFlexPathCalculator, egressFlexPathCalculator, - flexParameters.maxTransferDuration() + flexParameters.maxTransferDuration(), + flexTripFilter ) .calculateDirectFlexPaths(streetAccesses, streetEgresses, dates, requestedTime, arriveBy); @@ -139,7 +144,8 @@ public Collection createFlexAccesses() { return new FlexAccessFactory( callbackService, accessFlexPathCalculator, - flexParameters.maxTransferDuration() + flexParameters.maxTransferDuration(), + flexTripFilter ) .createFlexAccesses(streetAccesses, dates); } @@ -149,7 +155,8 @@ public Collection createFlexEgresses() { return new FlexEgressFactory( callbackService, egressFlexPathCalculator, - flexParameters.maxTransferDuration() + flexParameters.maxTransferDuration(), + flexTripFilter ) .createFlexEgresses(streetEgresses, dates); } diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/filter/FilterMapper.java b/application/src/ext/java/org/opentripplanner/ext/flex/filter/FilterMapper.java new file mode 100644 index 00000000000..386fafb1188 --- /dev/null +++ b/application/src/ext/java/org/opentripplanner/ext/flex/filter/FilterMapper.java @@ -0,0 +1,61 @@ +package org.opentripplanner.ext.flex.filter; + +import java.util.HashSet; +import java.util.List; +import org.opentripplanner.ext.flex.filter.FlexTripFilterRequest.AllowAll; +import org.opentripplanner.model.modes.ExcludeAllTransitFilter; +import org.opentripplanner.routing.api.request.request.filter.AllowAllTransitFilter; +import org.opentripplanner.routing.api.request.request.filter.TransitFilter; +import org.opentripplanner.routing.api.request.request.filter.TransitFilterRequest; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +/** + * Map the internal OTP filter API into the reduced, flex-specific version of it. + */ +public class FilterMapper { + + public static FlexTripFilter mapFilters(List filters) { + var flexFilters = filters + .stream() + .map(s -> + switch (s) { + case TransitFilterRequest sr -> mapFilters(sr); + case AllowAllTransitFilter sr -> AllowAll.of(); + // excluding all transit means all fixed schedule transit but flex can still be use for + // direct routes, therefore it means to allow all trips in the context of flex + case ExcludeAllTransitFilter f -> AllowAll.of(); + default -> throw new IllegalStateException("Unexpected value: " + s); + } + ) + .distinct() + .toList(); + return new FlexTripFilter(flexFilters); + } + + private static FlexTripFilterRequest.Filter mapFilters(TransitFilterRequest sr) { + var bannedAgencies = new HashSet(); + var bannedRoutes = new HashSet(); + var selectedAgencies = new HashSet(); + var selectedRoutes = new HashSet(); + + sr + .not() + .forEach(s -> { + bannedRoutes.addAll(s.routes()); + bannedAgencies.addAll(s.agencies()); + }); + sr + .select() + .forEach(s -> { + selectedRoutes.addAll(s.routes()); + selectedAgencies.addAll(s.agencies()); + }); + + return new FlexTripFilterRequest.Filter( + selectedAgencies, + bannedAgencies, + selectedRoutes, + bannedRoutes + ); + } +} diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/filter/FlexTripFilter.java b/application/src/ext/java/org/opentripplanner/ext/flex/filter/FlexTripFilter.java new file mode 100644 index 00000000000..ce2bc48736f --- /dev/null +++ b/application/src/ext/java/org/opentripplanner/ext/flex/filter/FlexTripFilter.java @@ -0,0 +1,49 @@ +package org.opentripplanner.ext.flex.filter; + +import java.util.List; +import org.opentripplanner.ext.flex.filter.FlexTripFilterRequest.AllowAll; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.utils.tostring.ToStringBuilder; + +/** + * Filters trips based on filter criteria. This is a much reduced version of the general + * filtering that applies to fixed-schedule transit. It only supports the following features: + * + * - selecting (whitelisting) agencies and routes + * - banning (blacklisting) agencies and routes + */ +public class FlexTripFilter { + + public static FlexTripFilter ALLOW_ALL = new FlexTripFilter(List.of(AllowAll.of())); + private final List filters; + + public FlexTripFilter(List filters) { + this.filters = filters; + } + + /** + * Should the trip be used in the routing according to the filter criteria. + */ + public boolean allowsTrip(Trip trip) { + for (var filter : filters) { + if (!filter.allowsTrip(trip)) { + return false; + } + } + return true; + } + + @Override + public boolean equals(Object other) { + if (other instanceof FlexTripFilter filter) { + return this.filters.equals(filter.filters); + } else { + return false; + } + } + + @Override + public String toString() { + return ToStringBuilder.of(FlexTripFilter.class).addCol("filters", filters).toString(); + } +} diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/filter/FlexTripFilterRequest.java b/application/src/ext/java/org/opentripplanner/ext/flex/filter/FlexTripFilterRequest.java new file mode 100644 index 00000000000..e2193fbb1f0 --- /dev/null +++ b/application/src/ext/java/org/opentripplanner/ext/flex/filter/FlexTripFilterRequest.java @@ -0,0 +1,119 @@ +package org.opentripplanner.ext.flex.filter; + +import com.google.common.base.MoreObjects; +import java.util.Objects; +import java.util.Set; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.utils.tostring.ToStringBuilder; + +public sealed interface FlexTripFilterRequest { + boolean allowsTrip(Trip trip); + + /** + * A filter that allows you to select (whitelist) or exclude (blacklist) + */ + final class Filter implements FlexTripFilterRequest { + + private final Set selectedAgencies; + private final Set excludedAgencies; + private final Set selectedRoutes; + private final Set excludedRoutes; + + /** + * + */ + public Filter( + Set selectedAgencies, + Set excludedAgencies, + Set selectedRoutes, + Set excludedRoutes + ) { + this.selectedAgencies = selectedAgencies; + this.excludedAgencies = excludedAgencies; + this.selectedRoutes = selectedRoutes; + this.excludedRoutes = excludedRoutes; + } + + @Override + public boolean allowsTrip(Trip trip) { + var agencyId = trip.getRoute().getAgency().getId(); + var routeId = trip.getRoute().getId(); + if (containsSelect()) { + return selectedRoutes.contains(routeId) || selectedAgencies.contains(agencyId); + } else if (containsBan()) { + if (excludedRoutes.contains(routeId)) { + return false; + } else return !excludedAgencies.contains(agencyId); + } else { + return true; + } + } + + private boolean containsBan() { + return !excludedAgencies.isEmpty() || !excludedRoutes.isEmpty(); + } + + boolean containsSelect() { + return !selectedAgencies.isEmpty() || !selectedRoutes.isEmpty(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (Filter) obj; + return ( + Objects.equals(this.selectedAgencies, that.selectedAgencies) && + Objects.equals(this.excludedAgencies, that.excludedAgencies) && + Objects.equals(this.selectedRoutes, that.selectedRoutes) && + Objects.equals(this.excludedRoutes, that.excludedRoutes) + ); + } + + @Override + public int hashCode() { + return Objects.hash(selectedAgencies, excludedAgencies, selectedRoutes, excludedRoutes); + } + + @Override + public String toString() { + return ToStringBuilder + .of(Filter.class) + .addCol("selectedAgencies", selectedAgencies) + .addCol("excludedAgencies", excludedAgencies) + .addCol("selectedRoutes", selectedRoutes) + .addCol("excludedRoutes", excludedRoutes) + .toString(); + } + } + + /** + * The default filter which allows all flex trips to be used for routing. + */ + final class AllowAll implements FlexTripFilterRequest { + + private static final AllowAll INSTANCE = new AllowAll(); + + private AllowAll() {} + + public static AllowAll of() { + return INSTANCE; + } + + @Override + public boolean allowsTrip(Trip trip) { + return true; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof AllowAll; + } + + @Override + public String toString() { + return AllowAll.class.getSimpleName(); + } + } +} diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/template/ClosestTrip.java b/application/src/ext/java/org/opentripplanner/ext/flex/template/ClosestTrip.java index 553c8aee6c0..6f844d6444f 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/template/ClosestTrip.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/template/ClosestTrip.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.Objects; import org.opentripplanner.ext.flex.trip.FlexTrip; +import org.opentripplanner.routing.api.request.request.filter.TransitFilter; import org.opentripplanner.routing.graphfinder.NearbyStop; import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo; import org.opentripplanner.utils.lang.IntUtils; diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessFactory.java b/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessFactory.java index a4d348e6603..ee0816d34a0 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessFactory.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessFactory.java @@ -4,20 +4,24 @@ import java.util.Collection; import java.util.List; import org.opentripplanner.ext.flex.FlexAccessEgress; +import org.opentripplanner.ext.flex.filter.FlexTripFilter; import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; import org.opentripplanner.routing.graphfinder.NearbyStop; public class FlexAccessFactory { private final FlexAccessEgressCallbackAdapter callbackService; + private final FlexTripFilter filter; private final FlexTemplateFactory templateFactory; public FlexAccessFactory( FlexAccessEgressCallbackAdapter callbackService, FlexPathCalculator pathCalculator, - Duration maxTransferDuration + Duration maxTransferDuration, + FlexTripFilter filter ) { this.callbackService = callbackService; + this.filter = filter; this.templateFactory = FlexTemplateFactory.of(pathCalculator, maxTransferDuration); } @@ -40,6 +44,7 @@ List calculateFlexAccessTemplates( var closestFlexTrips = ClosestTrip.of(callbackService, streetAccesses, dates, true); return closestFlexTrips .stream() + .filter(ct -> filter.allowsTrip(ct.flexTrip().getTrip())) .flatMap(it -> templateFactory.createAccessTemplates(it).stream()) .toList(); } diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java b/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java index f27a502911f..c7ef450ff1b 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java @@ -9,6 +9,7 @@ import java.util.Collection; import java.util.List; import java.util.Optional; +import org.opentripplanner.ext.flex.filter.FlexTripFilter; import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; import org.opentripplanner.routing.graphfinder.NearbyStop; import org.opentripplanner.street.model.vertex.Vertex; @@ -23,17 +24,20 @@ public class FlexDirectPathFactory { private final FlexPathCalculator accessPathCalculator; private final FlexPathCalculator egressPathCalculator; private final Duration maxTransferDuration; + private final FlexTripFilter filter; public FlexDirectPathFactory( FlexAccessEgressCallbackAdapter callbackService, FlexPathCalculator accessPathCalculator, FlexPathCalculator egressPathCalculator, - Duration maxTransferDuration + Duration maxTransferDuration, + FlexTripFilter filter ) { this.callbackService = callbackService; this.accessPathCalculator = accessPathCalculator; this.egressPathCalculator = egressPathCalculator; this.maxTransferDuration = maxTransferDuration; + this.filter = filter; } public Collection calculateDirectFlexPaths( @@ -48,14 +52,16 @@ public Collection calculateDirectFlexPaths( var flexAccessTemplates = new FlexAccessFactory( callbackService, accessPathCalculator, - maxTransferDuration + maxTransferDuration, + filter ) .calculateFlexAccessTemplates(streetAccesses, dates); var flexEgressTemplates = new FlexEgressFactory( callbackService, egressPathCalculator, - maxTransferDuration + maxTransferDuration, + filter ) .calculateFlexEgressTemplates(streetEgresses, dates); diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressFactory.java b/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressFactory.java index 28908cb7e7b..f0b6a8c56bd 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressFactory.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressFactory.java @@ -4,20 +4,24 @@ import java.util.Collection; import java.util.List; import org.opentripplanner.ext.flex.FlexAccessEgress; +import org.opentripplanner.ext.flex.filter.FlexTripFilter; import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; import org.opentripplanner.routing.graphfinder.NearbyStop; public class FlexEgressFactory { private final FlexAccessEgressCallbackAdapter callbackService; + private final FlexTripFilter filter; private final FlexTemplateFactory templateFactory; public FlexEgressFactory( FlexAccessEgressCallbackAdapter callbackService, FlexPathCalculator pathCalculator, - Duration maxTransferDuration + Duration maxTransferDuration, + FlexTripFilter filter ) { this.callbackService = callbackService; + this.filter = filter; this.templateFactory = FlexTemplateFactory.of(pathCalculator, maxTransferDuration); } @@ -40,6 +44,7 @@ List calculateFlexEgressTemplates( var closestFlexTrips = ClosestTrip.of(callbackService, streetEgresses, dates, false); return closestFlexTrips .stream() + .filter(ct -> filter.allowsTrip(ct.flexTrip().getTrip())) .flatMap(it -> templateFactory.createEgressTemplates(it).stream()) .toList(); } diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTrip.java b/application/src/ext/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTrip.java index 25eac71ebb6..ce6404a975b 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTrip.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTrip.java @@ -57,7 +57,8 @@ public static ScheduledDeviatedTripBuilder of(FeedScopedId id) { public static boolean isScheduledDeviatedFlexTrip(List stopTimes) { Predicate notFixedStop = Predicate.not(st -> st.getStop() instanceof RegularStop); return ( - stopTimes.stream().anyMatch(notFixedStop) && stopTimes.stream().noneMatch(StopTime::combinesContinuousStoppingWithFlexWindow) + stopTimes.stream().anyMatch(notFixedStop) && + stopTimes.stream().noneMatch(StopTime::combinesContinuousStoppingWithFlexWindow) ); } diff --git a/application/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java b/application/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java index d420d0e8784..df9531d4b8b 100644 --- a/application/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java +++ b/application/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java @@ -208,11 +208,7 @@ StopCluster.Location toLocation(FeedScopedId id) { } else { var group = transitService.getStopLocationsGroup(id); var feedPublisher = toFeedPublisher(transitService.getFeedInfo(id.getFeedId())); - var modes = transitService - .findTransitModes(group) - .stream() - .map(Enum::name) - .toList(); + var modes = transitService.findTransitModes(group).stream().map(Enum::name).toList(); var agencies = agenciesForStopLocationsGroup(group) .stream() .map(StopClusterMapper::toAgency) diff --git a/application/src/ext/java/org/opentripplanner/ext/parkAndRideApi/ParkAndRideResource.java b/application/src/ext/java/org/opentripplanner/ext/parkAndRideApi/ParkAndRideResource.java index 611f4d46420..5fbae4b75d6 100644 --- a/application/src/ext/java/org/opentripplanner/ext/parkAndRideApi/ParkAndRideResource.java +++ b/application/src/ext/java/org/opentripplanner/ext/parkAndRideApi/ParkAndRideResource.java @@ -42,7 +42,8 @@ public ParkAndRideResource( // - serverContext.graphFinder(). This needs at least a comment! // - This can be replaced with a search done with the SiteRepository // - if we have a radius search there. - this.graphFinder = new DirectGraphFinder(serverContext.transitService()::findRegularStopsByBoundingBox); + this.graphFinder = + new DirectGraphFinder(serverContext.transitService()::findRegularStopsByBoundingBox); } /** Envelopes are in latitude, longitude format */ diff --git a/application/src/ext/java/org/opentripplanner/ext/restapi/model/ApiRouterInfo.java b/application/src/ext/java/org/opentripplanner/ext/restapi/model/ApiRouterInfo.java index 95317d40057..87ac08a7d8b 100644 --- a/application/src/ext/java/org/opentripplanner/ext/restapi/model/ApiRouterInfo.java +++ b/application/src/ext/java/org/opentripplanner/ext/restapi/model/ApiRouterInfo.java @@ -50,7 +50,12 @@ public ApiRouterInfo( this.hasParkRide = this.hasCarPark; this.hasVehicleParking = mapHasVehicleParking(vehicleParkingService); this.travelOptions = - ApiTravelOptionsMaker.makeOptions(graph, vehicleRentalService, vehicleParkingService, transitService); + ApiTravelOptionsMaker.makeOptions( + graph, + vehicleRentalService, + vehicleParkingService, + transitService + ); } public boolean mapHasBikeSharing(VehicleRentalService service) { diff --git a/application/src/ext/java/org/opentripplanner/ext/restapi/resources/IndexAPI.java b/application/src/ext/java/org/opentripplanner/ext/restapi/resources/IndexAPI.java index b9c049f547e..37330d85f05 100644 --- a/application/src/ext/java/org/opentripplanner/ext/restapi/resources/IndexAPI.java +++ b/application/src/ext/java/org/opentripplanner/ext/restapi/resources/IndexAPI.java @@ -222,10 +222,7 @@ public List getStopsInRadius( ); var stops = transitService().findRegularStopsByBoundingBox(envelope); - return stops - .stream() - .map(StopMapper::mapToApiShort) - .toList(); + return stops.stream().map(StopMapper::mapToApiShort).toList(); } } diff --git a/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java b/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java index 545b216dd56..bb9b23cc0f3 100644 --- a/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java +++ b/application/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java @@ -1,6 +1,5 @@ package org.opentripplanner.ext.siri.updater.azure; - import com.azure.identity.DefaultAzureCredentialBuilder; import com.azure.messaging.servicebus.ServiceBusClientBuilder; import com.azure.messaging.servicebus.ServiceBusErrorContext; @@ -98,9 +97,14 @@ interface CheckedRunnable { public AbstractAzureSiriUpdater(SiriAzureUpdaterParameters config) { this.configRef = Objects.requireNonNull(config.configRef(), "configRef must not be null"); - this.authenticationType = Objects.requireNonNull(config.getAuthenticationType(), "authenticationType must not be null"); + this.authenticationType = + Objects.requireNonNull(config.getAuthenticationType(), "authenticationType must not be null"); this.topicName = Objects.requireNonNull(config.getTopicName(), "topicName must not be null"); - this.dataInitializationUrl = Objects.requireNonNull(config.getDataInitializationUrl(), "dataInitializationUrl must not be null"); + this.dataInitializationUrl = + Objects.requireNonNull( + config.getDataInitializationUrl(), + "dataInitializationUrl must not be null" + ); this.timeout = config.getTimeout(); this.feedId = Objects.requireNonNull(config.feedId(), "feedId must not be null"); this.autoDeleteOnIdle = config.getAutoDeleteOnIdle(); @@ -108,16 +112,18 @@ public AbstractAzureSiriUpdater(SiriAzureUpdaterParameters config) { this.fuzzyTripMatching = config.isFuzzyTripMatching(); if (authenticationType == AuthenticationType.FederatedIdentity) { - this.fullyQualifiedNamespace = Objects.requireNonNull( - config.getFullyQualifiedNamespace(), - "fullyQualifiedNamespace must not be null when using FederatedIdentity authentication" - ); + this.fullyQualifiedNamespace = + Objects.requireNonNull( + config.getFullyQualifiedNamespace(), + "fullyQualifiedNamespace must not be null when using FederatedIdentity authentication" + ); this.serviceBusUrl = null; } else if (authenticationType == AuthenticationType.SharedAccessKey) { - this.serviceBusUrl = Objects.requireNonNull( - config.getServiceBusUrl(), - "serviceBusUrl must not be null when using SharedAccessKey authentication" - ); + this.serviceBusUrl = + Objects.requireNonNull( + config.getServiceBusUrl(), + "serviceBusUrl must not be null when using SharedAccessKey authentication" + ); this.fullyQualifiedNamespace = null; } else { throw new IllegalArgumentException("Unsupported authentication type: " + authenticationType); @@ -143,7 +149,6 @@ public void setup(WriteToGraphCallback writeToGraphCallback) { @Override public void run() { - // In Kubernetes this should be the POD identifier subscriptionName = System.getenv("HOSTNAME"); if (subscriptionName == null || subscriptionName.isBlank()) { @@ -151,20 +156,14 @@ public void run() { } try { - executeWithRetry( - this::setupSubscription, - "Setting up Service Bus subscription to topic" - ); + executeWithRetry(this::setupSubscription, "Setting up Service Bus subscription to topic"); executeWithRetry( () -> initializeData(dataInitializationUrl, messageConsumer), "Initializing historical Siri data" ); - executeWithRetry( - this::startEventProcessor, - "Starting Service Bus event processor" - ); + executeWithRetry(this::startEventProcessor, "Starting Service Bus event processor"); setPrimed(); @@ -181,7 +180,6 @@ public void run() { } } ); - } catch (ServiceBusException e) { LOG.error("Service Bus encountered an error during setup: {}", e.getMessage(), e); } catch (URISyntaxException e) { @@ -235,7 +233,7 @@ protected void executeWithRetry(CheckedRunnable task, String description) throws attemptCounter++; try { sleep(sleepPeriod); - } catch (InterruptedException ie){ + } catch (InterruptedException ie) { LOG.warn("{} was interrupted during sleep.", description); Thread.currentThread().interrupt(); // Restore interrupted status throw ie; @@ -250,21 +248,16 @@ protected boolean shouldRetry(Exception e) { ServiceBusFailureReason reason = sbException.getReason(); if (RETRYABLE_REASONS.contains(reason)) { - LOG.warn("Transient error encountered: {}. Retrying...", reason); return true; - } else if (NON_RETRYABLE_REASONS.contains(reason)) { - LOG.error("Non-recoverable error encountered: {}. Not retrying.", reason); return false; - } else { LOG.warn("Unhandled ServiceBusFailureReason: {}. Retrying by default.", reason); return true; } - } - else if (ExceptionUtils.hasCause(e, OtpHttpClientException.class)){ + } else if (ExceptionUtils.hasCause(e, OtpHttpClientException.class)) { // retry for OtpHttpClientException as it is thrown if historical data can't be read at the moment return true; } @@ -297,8 +290,15 @@ private void setupSubscription() throws ServiceBusException, URISyntaxException .setAutoDeleteOnIdle(autoDeleteOnIdle); // Make sure there is no old subscription on serviceBus - if ( Boolean.TRUE.equals( serviceBusAdmin.getSubscriptionExists(topicName, subscriptionName).block())) { - LOG.info("Subscription '{}' already exists. Deleting existing subscription.", subscriptionName); + if ( + Boolean.TRUE.equals( + serviceBusAdmin.getSubscriptionExists(topicName, subscriptionName).block() + ) + ) { + LOG.info( + "Subscription '{}' already exists. Deleting existing subscription.", + subscriptionName + ); serviceBusAdmin.deleteSubscription(topicName, subscriptionName).block(); LOG.info("Service Bus deleted subscription {}.", subscriptionName); } @@ -314,28 +314,34 @@ private void startEventProcessor() throws ServiceBusException { ServiceBusClientBuilder clientBuilder = new ServiceBusClientBuilder(); if (authenticationType == AuthenticationType.FederatedIdentity) { - Preconditions.checkNotNull(fullyQualifiedNamespace, "fullyQualifiedNamespace must be set for FederatedIdentity authentication"); + Preconditions.checkNotNull( + fullyQualifiedNamespace, + "fullyQualifiedNamespace must be set for FederatedIdentity authentication" + ); clientBuilder .fullyQualifiedNamespace(fullyQualifiedNamespace) .credential(new DefaultAzureCredentialBuilder().build()); } else if (authenticationType == AuthenticationType.SharedAccessKey) { - Preconditions.checkNotNull(serviceBusUrl, "serviceBusUrl must be set for SharedAccessKey authentication"); - clientBuilder - .connectionString(serviceBusUrl); + Preconditions.checkNotNull( + serviceBusUrl, + "serviceBusUrl must be set for SharedAccessKey authentication" + ); + clientBuilder.connectionString(serviceBusUrl); } else { throw new IllegalArgumentException("Unsupported authentication type: " + authenticationType); } - eventProcessor = clientBuilder - .processor() - .topicName(topicName) - .subscriptionName(subscriptionName) - .receiveMode(ServiceBusReceiveMode.RECEIVE_AND_DELETE) - .disableAutoComplete() // Receive and delete does not need autocomplete - .prefetchCount(prefetchCount) - .processError(errorConsumer) - .processMessage(messageConsumer) - .buildProcessorClient(); + eventProcessor = + clientBuilder + .processor() + .topicName(topicName) + .subscriptionName(subscriptionName) + .receiveMode(ServiceBusReceiveMode.RECEIVE_AND_DELETE) + .disableAutoComplete() // Receive and delete does not need autocomplete + .prefetchCount(prefetchCount) + .processError(errorConsumer) + .processMessage(messageConsumer) + .buildProcessorClient(); eventProcessor.start(); LOG.info( @@ -346,7 +352,6 @@ private void startEventProcessor() throws ServiceBusException { ); } - @Override public boolean isPrimed() { return this.isPrimed; @@ -391,7 +396,6 @@ boolean fuzzyTripMatching() { return fuzzyTripMatching; } - protected abstract void initializeData( String url, Consumer consumer @@ -416,8 +420,10 @@ protected void defaultErrorConsumer(ServiceBusErrorContext errorContext) { var reason = e.getReason(); - if (reason == ServiceBusFailureReason.MESSAGING_ENTITY_DISABLED || - reason == ServiceBusFailureReason.MESSAGING_ENTITY_NOT_FOUND) { + if ( + reason == ServiceBusFailureReason.MESSAGING_ENTITY_DISABLED || + reason == ServiceBusFailureReason.MESSAGING_ENTITY_NOT_FOUND + ) { LOG.error( "An unrecoverable error occurred. Stopping processing with reason {} {}", reason, @@ -425,8 +431,10 @@ protected void defaultErrorConsumer(ServiceBusErrorContext errorContext) { ); } else if (reason == ServiceBusFailureReason.MESSAGE_LOCK_LOST) { LOG.error("Message lock lost for message", e); - } else if (reason == ServiceBusFailureReason.SERVICE_BUSY || - reason == ServiceBusFailureReason.UNAUTHORIZED) { + } else if ( + reason == ServiceBusFailureReason.SERVICE_BUSY || + reason == ServiceBusFailureReason.UNAUTHORIZED + ) { LOG.error("Service Bus is busy or unauthorized, wait and try again"); try { // Choosing an arbitrary amount of time to wait until trying again. diff --git a/application/src/ext/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSourceParameters.java b/application/src/ext/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSourceParameters.java index a04ea004bf0..fc2d6700816 100644 --- a/application/src/ext/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSourceParameters.java +++ b/application/src/ext/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSourceParameters.java @@ -2,9 +2,9 @@ import java.util.Set; import javax.annotation.Nullable; -import org.opentripplanner.updater.vehicle_rental.datasources.params.RentalPickupType; import org.opentripplanner.updater.spi.HttpHeaders; import org.opentripplanner.updater.vehicle_rental.VehicleRentalSourceType; +import org.opentripplanner.updater.vehicle_rental.datasources.params.RentalPickupType; import org.opentripplanner.updater.vehicle_rental.datasources.params.VehicleRentalDataSourceParameters; /** diff --git a/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/CoachCostCalculator.java b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/CoachCostCalculator.java index ba4830f29cf..0862f5bd201 100644 --- a/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/CoachCostCalculator.java +++ b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/CoachCostCalculator.java @@ -7,76 +7,74 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; import org.opentripplanner.transit.model.basic.TransitMode; - /** * This cost calculator increases the cost on mode coach by adding an extra reluctance. The * reluctance is hardcoded in this class and cannot be configured. */ -class CoachCostCalculator implements RaptorCostCalculator { +class CoachCostCalculator implements RaptorCostCalculator { private static final int EXTRA_RELUCTANCE_ON_COACH = RaptorCostConverter.toRaptorCost(0.6); private final RaptorCostCalculator delegate; CoachCostCalculator(RaptorCostCalculator delegate) { - this.delegate = delegate; - } - - @Override - public int boardingCost( - boolean firstBoarding, - int prevArrivalTime, - int boardStop, - int boardTime, - T trip, - RaptorTransferConstraint transferConstraints - ) { - return delegate.boardingCost( - firstBoarding, - prevArrivalTime, - boardStop, - boardTime, - trip, - transferConstraints - ); - } + this.delegate = delegate; + } - @Override - public int onTripRelativeRidingCost(int boardTime, T tripScheduledBoarded) { - return delegate.onTripRelativeRidingCost(boardTime, tripScheduledBoarded); - } + @Override + public int boardingCost( + boolean firstBoarding, + int prevArrivalTime, + int boardStop, + int boardTime, + T trip, + RaptorTransferConstraint transferConstraints + ) { + return delegate.boardingCost( + firstBoarding, + prevArrivalTime, + boardStop, + boardTime, + trip, + transferConstraints + ); + } - @Override - public int transitArrivalCost( - int boardCost, - int alightSlack, - int transitTime, - T trip, - int toStop - ) { - int cost = delegate.transitArrivalCost(boardCost, alightSlack, transitTime, trip, toStop); + @Override + public int onTripRelativeRidingCost(int boardTime, T tripScheduledBoarded) { + return delegate.onTripRelativeRidingCost(boardTime, tripScheduledBoarded); + } - // This is a bit ugly, since it relies on the fact that the 'transitReluctanceFactorIndex' - // returns the 'route.getMode().ordinal()' - if(trip.transitReluctanceFactorIndex() == TransitMode.COACH.ordinal()) { - cost += transitTime * EXTRA_RELUCTANCE_ON_COACH; - } - return cost; - } + @Override + public int transitArrivalCost( + int boardCost, + int alightSlack, + int transitTime, + T trip, + int toStop + ) { + int cost = delegate.transitArrivalCost(boardCost, alightSlack, transitTime, trip, toStop); - @Override - public int waitCost(int waitTimeInSeconds) { - return delegate.waitCost(waitTimeInSeconds); + // This is a bit ugly, since it relies on the fact that the 'transitReluctanceFactorIndex' + // returns the 'route.getMode().ordinal()' + if (trip.transitReluctanceFactorIndex() == TransitMode.COACH.ordinal()) { + cost += transitTime * EXTRA_RELUCTANCE_ON_COACH; } + return cost; + } - @Override - public int calculateRemainingMinCost(int minTravelTime, int minNumTransfers, int fromStop) { - return delegate.calculateRemainingMinCost(minTravelTime, minNumTransfers, fromStop); - } + @Override + public int waitCost(int waitTimeInSeconds) { + return delegate.waitCost(waitTimeInSeconds); + } - @Override - public int costEgress(RaptorAccessEgress egress) { - return delegate.costEgress(egress); - } + @Override + public int calculateRemainingMinCost(int minTravelTime, int minNumTransfers, int fromStop) { + return delegate.calculateRemainingMinCost(minTravelTime, minNumTransfers, fromStop); + } + @Override + public int costEgress(RaptorAccessEgress egress) { + return delegate.costEgress(egress); + } } diff --git a/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/MergePaths.java b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/MergePaths.java index 2f7b38a7a08..392023c3beb 100644 --- a/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/MergePaths.java +++ b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/MergePaths.java @@ -15,10 +15,15 @@ * Everything from the main result is kept, and any additional rail results from the alternative * search are added. */ -class MergePaths implements BiFunction>, Collection>, Collection>> { +class MergePaths + implements + BiFunction>, Collection>, Collection>> { @Override - public Collection> apply(Collection> main, Collection> alternatives) { + public Collection> apply( + Collection> main, + Collection> alternatives + ) { Map> result = new HashMap<>(); addAllToMap(result, main); addRailToMap(result, alternatives); @@ -27,7 +32,7 @@ public Collection> apply(Collection> main, Collectio private void addAllToMap(Map> map, Collection> paths) { for (var it : paths) { - map.put(new PathKey(it), it); + map.put(new PathKey(it), it); } } diff --git a/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java index e4504b3ed14..27e1c9de4ea 100644 --- a/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java +++ b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java @@ -3,7 +3,6 @@ import org.opentripplanner.raptor.api.path.PathLeg; import org.opentripplanner.raptor.api.path.RaptorPath; - /** * The purpose of this class is to create a key to be able to compare paths so duplicate results * can be ignored. diff --git a/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/SorlandsbanenNorwayService.java b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/SorlandsbanenNorwayService.java index ecccb5d2370..aad43712437 100644 --- a/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/SorlandsbanenNorwayService.java +++ b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/SorlandsbanenNorwayService.java @@ -32,9 +32,12 @@ public class SorlandsbanenNorwayService { private static final double SOUTH_BORDER_LIMIT = 59.1; private static final int MIN_DISTANCE_LIMIT = 120_000; - @Nullable - public ExtraMcRouterSearch createExtraMcRouterSearch(RouteRequest request, AccessEgresses accessEgresses, TransitLayer transitLayer) { + public ExtraMcRouterSearch createExtraMcRouterSearch( + RouteRequest request, + AccessEgresses accessEgresses, + TransitLayer transitLayer + ) { WgsCoordinate from = findStopCoordinate( request.from(), accessEgresses.getAccesses(), @@ -53,9 +56,11 @@ public ExtraMcRouterSearch createExtraMcRouterSearch(RouteRequest return new ExtraMcRouterSearch<>() { @Override - public RaptorTransitDataProvider createTransitDataAlternativeSearch(RaptorTransitDataProvider transitDataMainSearch) { + public RaptorTransitDataProvider createTransitDataAlternativeSearch( + RaptorTransitDataProvider transitDataMainSearch + ) { return new RaptorRoutingRequestTransitData( - (RaptorRoutingRequestTransitData)transitDataMainSearch, + (RaptorRoutingRequestTransitData) transitDataMainSearch, new CoachCostCalculator<>(transitDataMainSearch.multiCriteriaCostCalculator()) ); } diff --git a/application/src/ext/java/org/opentripplanner/ext/stopconsolidation/DecorateConsolidatedStopNames.java b/application/src/ext/java/org/opentripplanner/ext/stopconsolidation/DecorateConsolidatedStopNames.java index 6def5c85fa1..150ad5ebece 100644 --- a/application/src/ext/java/org/opentripplanner/ext/stopconsolidation/DecorateConsolidatedStopNames.java +++ b/application/src/ext/java/org/opentripplanner/ext/stopconsolidation/DecorateConsolidatedStopNames.java @@ -62,17 +62,11 @@ private void replaceConsolidatedStops(Itinerary i) { private void removeShortWalkLegs(Itinerary itinerary) { var legs = new ArrayList<>(itinerary.getLegs()); var first = legs.getFirst(); - if ( - service.isPartOfConsolidatedStop(first.getTo().stop) && - isShortWalkLeg(first) - ) { + if (service.isPartOfConsolidatedStop(first.getTo().stop) && isShortWalkLeg(first)) { legs.removeFirst(); } var last = legs.getLast(); - if ( - service.isPartOfConsolidatedStop(last.getFrom().stop) && - isShortWalkLeg(last) - ) { + if (service.isPartOfConsolidatedStop(last.getFrom().stop) && isShortWalkLeg(last)) { legs.removeLast(); } @@ -82,14 +76,15 @@ private void removeShortWalkLegs(Itinerary itinerary) { } private boolean isTransferWithinConsolidatedStop(Leg l) { - return isShortWalkLeg(l) && + return ( + isShortWalkLeg(l) && service.isPartOfConsolidatedStop(l.getFrom().stop) && - service.isPartOfConsolidatedStop(l.getTo().stop); + service.isPartOfConsolidatedStop(l.getTo().stop) + ); } private static boolean isShortWalkLeg(Leg leg) { - return leg.isWalkingLeg() && - leg.getDistanceMeters() < MAX_INTRA_STOP_WALK_DISTANCE_METERS; + return leg.isWalkingLeg() && leg.getDistanceMeters() < MAX_INTRA_STOP_WALK_DISTANCE_METERS; } /** diff --git a/application/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java b/application/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java index 8e6f31fb6fe..aa84caf4f4d 100644 --- a/application/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java +++ b/application/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java @@ -4,8 +4,8 @@ import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; -import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; import javax.annotation.Nullable; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; import org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup; import org.opentripplanner.ext.stopconsolidation.model.StopReplacement; diff --git a/application/src/ext/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerBuilder.java b/application/src/ext/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerBuilder.java index 22fb75f7b49..ea569c7a295 100644 --- a/application/src/ext/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerBuilder.java +++ b/application/src/ext/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerBuilder.java @@ -46,7 +46,8 @@ public VehicleParkingsLayerBuilder( @Override protected List getGeometries(Envelope query) { return service - .listVehicleParkings().stream() + .listVehicleParkings() + .stream() .map(vehicleParking -> { Coordinate coordinate = vehicleParking.getCoordinate().asJtsCoordinate(); Point point = GeometryUtils.getGeometryFactory().createPoint(coordinate); diff --git a/application/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java b/application/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java index dc0fa2868ec..179a158a2a9 100644 --- a/application/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java +++ b/application/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java @@ -18,8 +18,8 @@ import org.opentripplanner.updater.spi.GraphUpdater; import org.opentripplanner.updater.vehicle_rental.VehicleRentalUpdater; import org.opentripplanner.updater.vehicle_rental.datasources.VehicleRentalDataSourceFactory; -import org.opentripplanner.updater.vehicle_rental.datasources.params.RentalPickupType; import org.opentripplanner.updater.vehicle_rental.datasources.params.GbfsVehicleRentalDataSourceParameters; +import org.opentripplanner.updater.vehicle_rental.datasources.params.RentalPickupType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java b/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java index d3f64288417..721ad3e3ee7 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java @@ -59,6 +59,7 @@ import org.opentripplanner.apis.gtfs.datafetchers.QueryTypeImpl; import org.opentripplanner.apis.gtfs.datafetchers.RealTimeEstimateImpl; import org.opentripplanner.apis.gtfs.datafetchers.RentalPlaceTypeResolver; +import org.opentripplanner.apis.gtfs.datafetchers.RentalVehicleFuelImpl; import org.opentripplanner.apis.gtfs.datafetchers.RentalVehicleImpl; import org.opentripplanner.apis.gtfs.datafetchers.RentalVehicleTypeImpl; import org.opentripplanner.apis.gtfs.datafetchers.RideHailingEstimateImpl; @@ -199,6 +200,7 @@ protected static GraphQLSchema buildSchema() { .type(typeWiring.build(RealTimeEstimateImpl.class)) .type(typeWiring.build(EstimatedTimeImpl.class)) .type(typeWiring.build(EntranceImpl.class)) + .type(typeWiring.build(RentalVehicleFuelImpl.class)) .build(); SchemaGenerator schemaGenerator = new SchemaGenerator(); return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring); diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RentalVehicleFuelImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RentalVehicleFuelImpl.java new file mode 100644 index 00000000000..aca43154e02 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RentalVehicleFuelImpl.java @@ -0,0 +1,25 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; +import org.opentripplanner.service.vehiclerental.model.RentalVehicleFuel; + +public class RentalVehicleFuelImpl implements GraphQLDataFetchers.GraphQLRentalVehicleFuel { + + @Override + public DataFetcher percent() { + return environment -> + getSource(environment).percent() != null ? getSource(environment).percent().asDouble() : null; + } + + @Override + public DataFetcher range() { + return environment -> + getSource(environment).range() != null ? getSource(environment).range().toMeters() : null; + } + + private RentalVehicleFuel getSource(DataFetchingEnvironment environment) { + return environment.getSource(); + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RentalVehicleImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RentalVehicleImpl.java index c4fb92c0ef4..5697aa15a5f 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RentalVehicleImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RentalVehicleImpl.java @@ -4,6 +4,7 @@ import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; +import org.opentripplanner.service.vehiclerental.model.RentalVehicleFuel; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris; import org.opentripplanner.service.vehiclerental.model.VehicleRentalSystem; @@ -16,6 +17,11 @@ public DataFetcher allowPickupNow() { return environment -> getSource(environment).allowPickupNow(); } + @Override + public DataFetcher fuel() { + return environment -> getSource(environment).getFuel(); + } + @Override public DataFetcher id() { return environment -> diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java index 26ace8fc66a..4a29f62e84d 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java @@ -58,6 +58,7 @@ import org.opentripplanner.service.vehicleparking.model.VehicleParkingSpaces; import org.opentripplanner.service.vehicleparking.model.VehicleParkingState; import org.opentripplanner.service.vehiclerental.model.RentalVehicleEntityCounts; +import org.opentripplanner.service.vehiclerental.model.RentalVehicleFuel; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; import org.opentripplanner.service.vehiclerental.model.RentalVehicleTypeCount; import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; @@ -908,6 +909,8 @@ public interface GraphQLRentalPlace extends TypeResolver {} public interface GraphQLRentalVehicle { public DataFetcher allowPickupNow(); + public DataFetcher fuel(); + public DataFetcher id(); public DataFetcher lat(); @@ -935,6 +938,13 @@ public interface GraphQLRentalVehicleEntityCounts { public DataFetcher total(); } + /** Rental vehicle fuel represent the current status of the battery or fuel of a rental vehicle */ + public interface GraphQLRentalVehicleFuel { + public DataFetcher percent(); + + public DataFetcher range(); + } + public interface GraphQLRentalVehicleType { public DataFetcher formFactor(); diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java index fc20625e18e..c12ccb82888 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java @@ -555,12 +555,14 @@ public void setGraphQLUnpreferredCost( public static class GraphQLCarPreferencesInput { + private org.opentripplanner.framework.model.Cost boardCost; private GraphQLCarParkingPreferencesInput parking; private Double reluctance; private GraphQLCarRentalPreferencesInput rental; public GraphQLCarPreferencesInput(Map args) { if (args != null) { + this.boardCost = (org.opentripplanner.framework.model.Cost) args.get("boardCost"); this.parking = new GraphQLCarParkingPreferencesInput((Map) args.get("parking")); this.reluctance = (Double) args.get("reluctance"); @@ -569,6 +571,10 @@ public GraphQLCarPreferencesInput(Map args) { } } + public org.opentripplanner.framework.model.Cost getGraphQLBoardCost() { + return this.boardCost; + } + public GraphQLCarParkingPreferencesInput getGraphQLParking() { return this.parking; } @@ -581,6 +587,10 @@ public GraphQLCarRentalPreferencesInput getGraphQLRental() { return this.rental; } + public void setGraphQLBoardCost(org.opentripplanner.framework.model.Cost boardCost) { + this.boardCost = boardCost; + } + public void setGraphQLParking(GraphQLCarParkingPreferencesInput parking) { this.parking = parking; } diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml index a9bb87a6ea5..3272efa894f 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml @@ -134,4 +134,5 @@ config: CallRealTime: org.opentripplanner.apis.gtfs.model.CallRealTime#CallRealTime RentalPlace: org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace#VehicleRentalPlace CallSchedule: org.opentripplanner.apis.gtfs.model.CallSchedule#CallSchedule + RentalVehicleFuel: org.opentripplanner.service.vehiclerental.model.RentalVehicleFuel#RentalVehicleFuel diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/CarPreferencesMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/CarPreferencesMapper.java index 01a78153c9b..4ae2ac97b51 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/CarPreferencesMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/CarPreferencesMapper.java @@ -27,6 +27,10 @@ static void setCarPreferences( if (reluctance != null) { preferences.withReluctance(reluctance); } + var boardCost = args.getGraphQLBoardCost(); + if (boardCost != null) { + preferences.withBoardCost(boardCost.toSeconds()); + } preferences.withParking(parking -> setCarParkingPreferences(parking, args.getGraphQLParking(), environment) ); diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java b/application/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java index 0eed3d3fb84..e9056081b12 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java @@ -123,10 +123,10 @@ import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; import org.opentripplanner.transit.api.model.FilterValues; import org.opentripplanner.transit.api.request.FindRegularStopsByBoundingBoxRequest; +import org.opentripplanner.transit.api.request.FindRoutesRequest; import org.opentripplanner.transit.api.request.TripRequest; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.service.TransitService; import org.slf4j.Logger; @@ -1098,24 +1098,42 @@ private GraphQLSchema create() { GraphQLFieldDefinition .newFieldDefinition() .name("lines") - .description("Get all lines") + .description("Get all _lines_") .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(lineType))) .argument( GraphQLArgument .newArgument() .name("ids") + .description( + "Set of ids of _lines_ to fetch. If this is set, no other filters can be set." + ) .type(new GraphQLList(Scalars.GraphQLID)) .build() ) - .argument(GraphQLArgument.newArgument().name("name").type(Scalars.GraphQLString).build()) .argument( - GraphQLArgument.newArgument().name("publicCode").type(Scalars.GraphQLString).build() + GraphQLArgument + .newArgument() + .name("name") + .description( + "Prefix of the _name_ of the _line_ to fetch. This filter is case insensitive." + ) + .type(Scalars.GraphQLString) + .build() + ) + .argument( + GraphQLArgument + .newArgument() + .name("publicCode") + .description("_Public code_ of the _line_ to fetch.") + .type(Scalars.GraphQLString) + .build() ) .argument( GraphQLArgument .newArgument() .name("publicCodes") + .description("Set of _public codes_ to fetch _lines_ for.") .type(new GraphQLList(Scalars.GraphQLString)) .build() ) @@ -1123,6 +1141,7 @@ private GraphQLSchema create() { GraphQLArgument .newArgument() .name("transportModes") + .description("Set of _transport modes_ to fetch _lines_ for.") .type(new GraphQLList(TRANSPORT_MODE)) .build() ) @@ -1130,7 +1149,7 @@ private GraphQLSchema create() { GraphQLArgument .newArgument() .name("authorities") - .description("Set of ids of authorities to fetch lines for.") + .description("Set of ids of _authorities_ to fetch _lines_ for.") .type(new GraphQLList(Scalars.GraphQLString)) .build() ) @@ -1138,83 +1157,57 @@ private GraphQLSchema create() { GraphQLArgument .newArgument() .name("flexibleOnly") - .description("Filter by lines containing flexible / on demand serviceJourneys only.") + .description( + "Filter by _lines_ containing flexible / on demand _service journey_ only." + ) .type(Scalars.GraphQLBoolean) - .defaultValue(false) + .defaultValueProgrammatic(false) .build() ) .dataFetcher(environment -> { - if ((environment.getArgument("ids") instanceof List)) { + if (environment.containsArgument("ids")) { + var ids = mapIDsToDomainNullSafe(environment.getArgument("ids")); + + // flexibleLines gets special treatment because it has a default value. if ( - environment - .getArguments() - .entrySet() - .stream() - .filter(it -> - it.getValue() != null && - !(it.getKey().equals("flexibleOnly") && it.getValue().equals(false)) - ) - .count() != - 1 + Stream + .of("name", "publicCode", "publicCodes", "transportModes", "authorities") + .anyMatch(environment::containsArgument) || + Boolean.TRUE.equals(environment.getArgument("flexibleOnly")) ) { throw new IllegalArgumentException("Unable to combine other filters with ids"); } - return ((List) environment.getArgument("ids")).stream() - .map(TransitIdMapper::mapIDToDomain) - .map(id -> { - return GqlUtil.getTransitService(environment).getRoute(id); - }) - .collect(Collectors.toList()); - } - Stream stream = GqlUtil.getTransitService(environment).listRoutes().stream(); - if ((boolean) environment.getArgument("flexibleOnly")) { - Collection flexRoutes = GqlUtil - .getTransitService(environment) - .getFlexIndex() - .getAllFlexRoutes(); - stream = stream.filter(flexRoutes::contains); - } - if (environment.getArgument("name") != null) { - stream = - stream - .filter(route -> route.getLongName() != null) - .filter(route -> - route - .getLongName() - .toString() - .toLowerCase() - .startsWith(((String) environment.getArgument("name")).toLowerCase()) - ); - } - if (environment.getArgument("publicCode") != null) { - stream = - stream - .filter(route -> route.getShortName() != null) - .filter(route -> - route.getShortName().equals(environment.getArgument("publicCode")) - ); - } - if (environment.getArgument("publicCodes") instanceof List) { - Set publicCodes = Set.copyOf(environment.getArgument("publicCodes")); - stream = - stream - .filter(route -> route.getShortName() != null) - .filter(route -> publicCodes.contains(route.getShortName())); - } - if (environment.getArgument("transportModes") != null) { - Set modes = Set.copyOf(environment.getArgument("transportModes")); - stream = stream.filter(route -> modes.contains(route.getMode())); - } - if ((environment.getArgument("authorities") instanceof Collection)) { - Collection authorityIds = environment.getArgument("authorities"); - stream = - stream.filter(route -> - route.getAgency() != null && - authorityIds.contains(route.getAgency().getId().getId()) - ); + return GqlUtil.getTransitService(environment).getRoutes(ids); } - return stream.collect(Collectors.toList()); + + var name = environment.getArgument("name"); + var publicCode = environment.getArgument("publicCode"); + var publicCodes = FilterValues.ofEmptyIsEverything( + "publicCodes", + environment.>getArgument("publicCodes") + ); + var transportModes = FilterValues.ofEmptyIsEverything( + "transportModes", + environment.>getArgument("transportModes") + ); + var authorities = FilterValues.ofEmptyIsEverything( + "authorities", + environment.>getArgument("authorities") + ); + boolean flexibleOnly = Boolean.TRUE.equals(environment.getArgument("flexibleOnly")); + + FindRoutesRequest findRoutesRequest = FindRoutesRequest + .of() + .withLongName(name) + .withShortName(publicCode) + .withShortNames(publicCodes) + .withTransitModes(transportModes) + .withAgencies(authorities) + .withFlexibleOnly(flexibleOnly) + .build(); + + return GqlUtil.getTransitService(environment).findRoutes(findRoutesRequest); }) .build() ) diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/BookingInfoMapper.java b/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/BookingInfoMapper.java new file mode 100644 index 00000000000..de59794ea18 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/BookingInfoMapper.java @@ -0,0 +1,38 @@ +package org.opentripplanner.apis.transmodel.mapping; + +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; +import org.opentripplanner.transit.model.timetable.booking.BookingTime; + +/** + * Maps the {@link BookingInfo} to enum value (as a string) returned by the API. + */ +public class BookingInfoMapper { + + public static String mapToBookWhen(BookingInfo bookingInfo) { + if (bookingInfo.getMinimumBookingNotice().isPresent()) { + return null; + } + BookingTime latestBookingTime = bookingInfo.getLatestBookingTime(); + BookingTime earliestBookingTime = bookingInfo.getEarliestBookingTime(); + + // Try to deduce the original enum from stored values + if (earliestBookingTime == null) { + if (latestBookingTime == null) { + return "timeOfTravelOnly"; + } else if (latestBookingTime.getDaysPrior() == 1) { + return "untilPreviousDay"; + } else if (latestBookingTime.getDaysPrior() == 0) { + return "advanceAndDayOfTravel"; + } else { + return "other"; + } + } else if ( + earliestBookingTime.getDaysPrior() == 0 && + (latestBookingTime == null || latestBookingTime.getDaysPrior() == 0) + ) { + return "dayOfTravelOnly"; + } else { + return "other"; + } + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/RelativeDirectionMapper.java b/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/RelativeDirectionMapper.java index 3228cb914df..1787515e0c7 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/RelativeDirectionMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/mapping/RelativeDirectionMapper.java @@ -22,12 +22,12 @@ public static RelativeDirection map(RelativeDirection relativeDirection) { CIRCLE_COUNTERCLOCKWISE, ELEVATOR, UTURN_LEFT, - UTURN_RIGHT -> relativeDirection; - // for these the Transmodel API doesn't have a mapping. should it? - case ENTER_STATION, + UTURN_RIGHT, + ENTER_STATION, EXIT_STATION, - ENTER_OR_EXIT_STATION, - FOLLOW_SIGNS -> RelativeDirection.CONTINUE; + FOLLOW_SIGNS -> relativeDirection; + // this type should never be exposed by an API + case ENTER_OR_EXIT_STATION -> RelativeDirection.CONTINUE; }; } } diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java b/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java index 2f8e69cc593..fba1a8f637a 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java @@ -289,6 +289,9 @@ public class EnumTypes { .value("elevator", RelativeDirection.ELEVATOR) .value("uturnLeft", RelativeDirection.UTURN_LEFT) .value("uturnRight", RelativeDirection.UTURN_RIGHT) + .value("enterStation", RelativeDirection.ENTER_STATION) + .value("exitStation", RelativeDirection.EXIT_STATION) + .value("followSigns", RelativeDirection.FOLLOW_SIGNS) .build(); public static final GraphQLEnumType REPORT_TYPE = GraphQLEnumType diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/model/stop/RentalVehicleType.java b/application/src/main/java/org/opentripplanner/apis/transmodel/model/stop/RentalVehicleType.java index e44639e09e7..a142873cd97 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/model/stop/RentalVehicleType.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/model/stop/RentalVehicleType.java @@ -71,7 +71,7 @@ public static GraphQLObjectType create( .name("currentRangeMeters") .type(Scalars.GraphQLFloat) .dataFetcher(environment -> - ((VehicleRentalVehicle) environment.getSource()).currentRangeMeters + ((VehicleRentalVehicle) environment.getSource()).getFuel().range() ) .build() ) diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/BookingArrangementType.java b/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/BookingArrangementType.java index 911ec8d9b0c..097fa92baca 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/BookingArrangementType.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/BookingArrangementType.java @@ -6,6 +6,7 @@ import graphql.schema.GraphQLList; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLOutputType; +import org.opentripplanner.apis.transmodel.mapping.BookingInfoMapper; import org.opentripplanner.apis.transmodel.model.EnumTypes; import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.transit.model.organization.ContactInfo; @@ -107,34 +108,7 @@ public static GraphQLObjectType create() { .name("bookWhen") .description("Time constraints for booking") .type(EnumTypes.PURCHASE_WHEN) - .dataFetcher(environment -> { - BookingInfo bookingInfo = bookingInfo(environment); - if (bookingInfo.getMinimumBookingNotice().isPresent()) { - return null; - } - BookingTime latestBookingTime = bookingInfo.getLatestBookingTime(); - BookingTime earliestBookingTime = bookingInfo.getEarliestBookingTime(); - - // Try to deduce the original enum from stored values - if (earliestBookingTime == null) { - if (latestBookingTime == null) { - return "timeOfTravelOnly"; - } else if (latestBookingTime.getDaysPrior() == 1) { - return "untilPreviousDay"; - } else if (latestBookingTime.getDaysPrior() == 0) { - return "advanceAndDayOfTravel"; - } else { - return "other"; - } - } else if ( - earliestBookingTime.getDaysPrior() == 0 && - (latestBookingTime == null || latestBookingTime.getDaysPrior() == 0) - ) { - return "dayOfTravelOnly"; - } else { - return "other"; - } - }) + .dataFetcher(environment -> BookingInfoMapper.mapToBookWhen(bookingInfo(environment))) .build() ) .field( 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 37ac69d88ff..f6e6ce1b238 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 @@ -8,8 +8,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.graph_builder.issues.StopNotLinkedForTransfers; @@ -45,36 +45,69 @@ public class DirectTransferGenerator implements GraphBuilderModule { private static final Logger LOG = LoggerFactory.getLogger(DirectTransferGenerator.class); - private final Duration radiusByDuration; + private final Duration defaultMaxTransferDuration; private final List transferRequests; + private final Map transferParametersForMode; private final Graph graph; private final TimetableRepository timetableRepository; private final DataImportIssueStore issueStore; + /** + * Constructor used in tests. This initializes transferParametersForMode as an empty map. + */ public DirectTransferGenerator( Graph graph, TimetableRepository timetableRepository, DataImportIssueStore issueStore, - Duration radiusByDuration, + Duration defaultMaxTransferDuration, List transferRequests ) { this.graph = graph; this.timetableRepository = timetableRepository; this.issueStore = issueStore; - this.radiusByDuration = radiusByDuration; + this.defaultMaxTransferDuration = defaultMaxTransferDuration; + this.transferRequests = transferRequests; + this.transferParametersForMode = Map.of(); + } + + public DirectTransferGenerator( + Graph graph, + TimetableRepository timetableRepository, + DataImportIssueStore issueStore, + Duration defaultMaxTransferDuration, + List transferRequests, + Map transferParametersForMode + ) { + this.graph = graph; + this.timetableRepository = timetableRepository; + this.issueStore = issueStore; + this.defaultMaxTransferDuration = defaultMaxTransferDuration; this.transferRequests = transferRequests; + this.transferParametersForMode = transferParametersForMode; } @Override public void buildGraph() { - /* Initialize transit model index which is needed by the nearby stop finder. */ + // Initialize transit model index which is needed by the nearby stop finder. timetableRepository.index(); - /* The linker will use streets if they are available, or straight-line distance otherwise. */ - NearbyStopFinder nearbyStopFinder = createNearbyStopFinder(); + // The linker will use streets if they are available, or straight-line distance otherwise. + NearbyStopFinder nearbyStopFinder = createNearbyStopFinder(defaultMaxTransferDuration); List stops = graph.getVerticesOfType(TransitStopVertex.class); + Set carsAllowedStops = timetableRepository.getStopLocationsUsedForCarsAllowedTrips(); + + LOG.info("Creating transfers based on requests:"); + transferRequests.forEach(transferProfile -> LOG.info(transferProfile.toString())); + if (transferParametersForMode.isEmpty()) { + LOG.info("No mode-specific transfer configurations provided."); + } else { + LOG.info("Using transfer configurations for modes:"); + transferParametersForMode.forEach((mode, transferParameters) -> + LOG.info(mode + ": " + transferParameters) + ); + } ProgressTracker progress = ProgressTracker.track( "Create transfer edges for stops", @@ -90,16 +123,8 @@ public void buildGraph() { HashMultimap.create() ); - List flexTransferRequests = new ArrayList<>(); - // Flex transfer requests only use the WALK mode. - if (OTPFeature.FlexRouting.isOn()) { - flexTransferRequests.addAll( - transferRequests - .stream() - .filter(transferProfile -> transferProfile.journey().transfer().mode() == StreetMode.WALK) - .toList() - ); - } + // Parse the transfer configuration from the parameters given in the build config. + TransferConfiguration transferConfiguration = parseTransferParameters(nearbyStopFinder); stops .stream() @@ -116,70 +141,15 @@ public void buildGraph() { LOG.debug("Linking stop '{}' {}", stop, ts0); - // Calculate default transfers. - for (RouteRequest transferProfile : transferRequests) { - StreetMode mode = transferProfile.journey().transfer().mode(); - for (NearbyStop sd : nearbyStopFinder.findNearbyStops( - ts0, - transferProfile, - transferProfile.journey().transfer(), - false - )) { - // Skip the origin stop, loop transfers are not needed. - if (sd.stop == stop) { - continue; - } - if (sd.stop.transfersNotAllowed()) { - continue; - } - TransferKey transferKey = new TransferKey(stop, sd.stop, sd.edges); - PathTransfer pathTransfer = distinctTransfers.get(transferKey); - if (pathTransfer == null) { - // If the PathTransfer can't be found, it is created. - distinctTransfers.put( - transferKey, - new PathTransfer(stop, sd.stop, sd.distance, sd.edges, EnumSet.of(mode)) - ); - } else { - // If the PathTransfer is found, a new PathTransfer with the added mode is created. - distinctTransfers.put(transferKey, pathTransfer.withAddedMode(mode)); - } - } - } - // Calculate flex transfers if flex routing is enabled. - for (RouteRequest transferProfile : flexTransferRequests) { - // Flex transfer requests only use the WALK mode. - StreetMode mode = StreetMode.WALK; - // This code is for finding transfers from AreaStops to Stops, transfers - // from Stops to AreaStops and between Stops are already covered above. - for (NearbyStop sd : nearbyStopFinder.findNearbyStops( - ts0, - transferProfile, - transferProfile.journey().transfer(), - true - )) { - // Skip the origin stop, loop transfers are not needed. - if (sd.stop == stop) { - continue; - } - if (sd.stop instanceof RegularStop) { - continue; - } - // The TransferKey and PathTransfer are created differently for flex routing. - TransferKey transferKey = new TransferKey(sd.stop, stop, sd.edges); - PathTransfer pathTransfer = distinctTransfers.get(transferKey); - if (pathTransfer == null) { - // If the PathTransfer can't be found, it is created. - distinctTransfers.put( - transferKey, - new PathTransfer(sd.stop, stop, sd.distance, sd.edges, EnumSet.of(mode)) - ); - } else { - // If the PathTransfer is found, a new PathTransfer with the added mode is created. - distinctTransfers.put(transferKey, pathTransfer.withAddedMode(mode)); - } - } - } + calculateDefaultTransfers(transferConfiguration, ts0, stop, distinctTransfers); + calculateFlexTransfers(transferConfiguration, ts0, stop, distinctTransfers); + calculateCarsAllowedTransfers( + transferConfiguration, + ts0, + stop, + distinctTransfers, + carsAllowedStops + ); LOG.debug( "Linked stop {} with {} transfers to stops with different patterns.", @@ -227,7 +197,7 @@ public void buildGraph() { * whether the graph has a street network and if ConsiderPatternsForDirectTransfers feature is * enabled. */ - private NearbyStopFinder createNearbyStopFinder() { + private NearbyStopFinder createNearbyStopFinder(Duration radiusByDuration) { var transitService = new DefaultTransitService(timetableRepository); NearbyStopFinder finder; if (!graph.hasStreets) { @@ -247,5 +217,209 @@ private NearbyStopFinder createNearbyStopFinder() { } } + private void createPathTransfer( + StopLocation from, + StopLocation to, + NearbyStop sd, + Map distinctTransfers, + StreetMode mode + ) { + TransferKey transferKey = new TransferKey(from, to, sd.edges); + PathTransfer pathTransfer = distinctTransfers.get(transferKey); + if (pathTransfer == null) { + // If the PathTransfer can't be found, it is created. + distinctTransfers.put( + transferKey, + new PathTransfer(from, to, sd.distance, sd.edges, EnumSet.of(mode)) + ); + } else { + // If the PathTransfer is found, a new PathTransfer with the added mode is created. + distinctTransfers.put(transferKey, pathTransfer.withAddedMode(mode)); + } + } + + /** + * This method parses the given transfer parameters into a transfer configuration and checks for invalid input. + */ + private TransferConfiguration parseTransferParameters(NearbyStopFinder nearbyStopFinder) { + List defaultTransferRequests = new ArrayList<>(); + List carsAllowedStopTransferRequests = new ArrayList<>(); + List flexTransferRequests = new ArrayList<>(); + HashMap defaultNearbyStopFinderForMode = new HashMap<>(); + // These are used for calculating transfers only between carsAllowedStops. + HashMap carsAllowedStopNearbyStopFinderForMode = new HashMap<>(); + + // Check that the mode specified in transferParametersForMode can also be found in transferRequests. + for (StreetMode mode : transferParametersForMode.keySet()) { + if ( + !transferRequests + .stream() + .anyMatch(transferProfile -> transferProfile.journey().transfer().mode() == mode) + ) { + throw new IllegalArgumentException( + String.format( + "Mode %s is used in transferParametersForMode but not in transferRequests", + mode + ) + ); + } + } + + for (RouteRequest transferProfile : transferRequests) { + StreetMode mode = transferProfile.journey().transfer().mode(); + TransferParameters transferParameters = transferParametersForMode.get(mode); + if (transferParameters != null) { + // WALK mode transfers can not be disabled. For example, flex transfers need them. + if (transferParameters.disableDefaultTransfers() && mode == StreetMode.WALK) { + throw new IllegalArgumentException("WALK mode transfers can not be disabled"); + } + // Disable normal transfer calculations for the specific mode, if disableDefaultTransfers is set in the build config. + if (!transferParameters.disableDefaultTransfers()) { + defaultTransferRequests.add(transferProfile); + // Set mode-specific maxTransferDuration, if it is set in the build config. + Duration maxTransferDuration = transferParameters.maxTransferDuration(); + if (maxTransferDuration != null) { + defaultNearbyStopFinderForMode.put(mode, createNearbyStopFinder(maxTransferDuration)); + } else { + defaultNearbyStopFinderForMode.put(mode, nearbyStopFinder); + } + } + // Create transfers between carsAllowedStops for the specific mode if carsAllowedStopMaxTransferDuration is set in the build config. + Duration carsAllowedStopMaxTransferDuration = transferParameters.carsAllowedStopMaxTransferDuration(); + if (carsAllowedStopMaxTransferDuration != null) { + carsAllowedStopTransferRequests.add(transferProfile); + carsAllowedStopNearbyStopFinderForMode.put( + mode, + createNearbyStopFinder(carsAllowedStopMaxTransferDuration) + ); + } + } else { + defaultTransferRequests.add(transferProfile); + defaultNearbyStopFinderForMode.put(mode, nearbyStopFinder); + } + } + + // Flex transfer requests only use the WALK mode. + if (OTPFeature.FlexRouting.isOn()) { + flexTransferRequests.addAll( + transferRequests + .stream() + .filter(transferProfile -> transferProfile.journey().transfer().mode() == StreetMode.WALK) + .toList() + ); + } + + return new TransferConfiguration( + defaultTransferRequests, + carsAllowedStopTransferRequests, + flexTransferRequests, + defaultNearbyStopFinderForMode, + carsAllowedStopNearbyStopFinderForMode + ); + } + + /** + * This method calculates default transfers. + */ + private void calculateDefaultTransfers( + TransferConfiguration transferConfiguration, + TransitStopVertex ts0, + RegularStop stop, + Map distinctTransfers + ) { + for (RouteRequest transferProfile : transferConfiguration.defaultTransferRequests()) { + StreetMode mode = transferProfile.journey().transfer().mode(); + var nearbyStops = transferConfiguration + .defaultNearbyStopFinderForMode() + .get(mode) + .findNearbyStops(ts0, transferProfile, transferProfile.journey().transfer(), false); + for (NearbyStop sd : nearbyStops) { + // Skip the origin stop, loop transfers are not needed. + if (sd.stop == stop) { + continue; + } + if (sd.stop.transfersNotAllowed()) { + continue; + } + createPathTransfer(stop, sd.stop, sd, distinctTransfers, mode); + } + } + } + + /** + * This method calculates flex transfers if flex routing is enabled. + */ + private void calculateFlexTransfers( + TransferConfiguration transferConfiguration, + TransitStopVertex ts0, + RegularStop stop, + Map distinctTransfers + ) { + for (RouteRequest transferProfile : transferConfiguration.flexTransferRequests()) { + // Flex transfer requests only use the WALK mode. + StreetMode mode = StreetMode.WALK; + var nearbyStops = transferConfiguration + .defaultNearbyStopFinderForMode() + .get(mode) + .findNearbyStops(ts0, transferProfile, transferProfile.journey().transfer(), true); + // This code is for finding transfers from AreaStops to Stops, transfers + // from Stops to AreaStops and between Stops are already covered above. + for (NearbyStop sd : nearbyStops) { + // Skip the origin stop, loop transfers are not needed. + if (sd.stop == stop) { + continue; + } + if (sd.stop instanceof RegularStop) { + continue; + } + // The TransferKey and PathTransfer are created differently for flex routing. + createPathTransfer(sd.stop, stop, sd, distinctTransfers, mode); + } + } + } + + /** + * This method calculates transfers between stops that are visited by trips that allow cars, if configured. + */ + private void calculateCarsAllowedTransfers( + TransferConfiguration transferConfiguration, + TransitStopVertex ts0, + RegularStop stop, + Map distinctTransfers, + Set carsAllowedStops + ) { + if (carsAllowedStops.contains(stop)) { + for (RouteRequest transferProfile : transferConfiguration.carsAllowedStopTransferRequests()) { + StreetMode mode = transferProfile.journey().transfer().mode(); + var nearbyStops = transferConfiguration + .carsAllowedStopNearbyStopFinderForMode() + .get(mode) + .findNearbyStops(ts0, transferProfile, transferProfile.journey().transfer(), false); + for (NearbyStop sd : nearbyStops) { + // Skip the origin stop, loop transfers are not needed. + if (sd.stop == stop) { + continue; + } + if (sd.stop.transfersNotAllowed()) { + continue; + } + // Only calculate transfers between carsAllowedStops. + if (!carsAllowedStops.contains(sd.stop)) { + continue; + } + createPathTransfer(stop, sd.stop, sd, distinctTransfers, mode); + } + } + } + } + + private record TransferConfiguration( + List defaultTransferRequests, + List carsAllowedStopTransferRequests, + List flexTransferRequests, + HashMap defaultNearbyStopFinderForMode, + HashMap carsAllowedStopNearbyStopFinderForMode + ) {} + private record TransferKey(StopLocation source, StopLocation target, List edges) {} } diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/TransferParameters.java b/application/src/main/java/org/opentripplanner/graph_builder/module/TransferParameters.java new file mode 100644 index 00000000000..0d7b31b4a81 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/TransferParameters.java @@ -0,0 +1,68 @@ +package org.opentripplanner.graph_builder.module; + +import java.time.Duration; +import org.opentripplanner.utils.tostring.ToStringBuilder; + +/** + * Mode-specific parameters for transfers. + */ +public record TransferParameters( + Duration maxTransferDuration, + Duration carsAllowedStopMaxTransferDuration, + boolean disableDefaultTransfers +) { + public static final Duration DEFAULT_MAX_TRANSFER_DURATION = null; + public static final Duration DEFAULT_CARS_ALLOWED_STOP_MAX_TRANSFER_DURATION = null; + public static final boolean DEFAULT_DISABLE_DEFAULT_TRANSFERS = false; + + TransferParameters(Builder builder) { + this( + builder.maxTransferDuration, + builder.carsAllowedStopMaxTransferDuration, + builder.disableDefaultTransfers + ); + } + + public String toString() { + return ToStringBuilder + .of(getClass()) + .addDuration("maxTransferDuration", maxTransferDuration) + .addDuration("carsAllowedStopMaxTransferDuration", carsAllowedStopMaxTransferDuration) + .addBool("disableDefaultTransfers", disableDefaultTransfers) + .toString(); + } + + public static class Builder { + + private Duration maxTransferDuration; + private Duration carsAllowedStopMaxTransferDuration; + private boolean disableDefaultTransfers; + + public Builder() { + this.maxTransferDuration = DEFAULT_MAX_TRANSFER_DURATION; + this.carsAllowedStopMaxTransferDuration = DEFAULT_CARS_ALLOWED_STOP_MAX_TRANSFER_DURATION; + this.disableDefaultTransfers = DEFAULT_DISABLE_DEFAULT_TRANSFERS; + } + + public Builder withMaxTransferDuration(Duration maxTransferDuration) { + this.maxTransferDuration = maxTransferDuration; + return this; + } + + public Builder withCarsAllowedStopMaxTransferDuration( + Duration carsAllowedStopMaxTransferDuration + ) { + this.carsAllowedStopMaxTransferDuration = carsAllowedStopMaxTransferDuration; + return this; + } + + public Builder withDisableDefaultTransfers(boolean disableDefaultTransfers) { + this.disableDefaultTransfers = disableDefaultTransfers; + return this; + } + + public TransferParameters build() { + return new TransferParameters(this); + } + } +} 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 d464523a61a..6cf3e593d93 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 @@ -259,7 +259,8 @@ static DirectTransferGenerator provideDirectTransferGenerator( timetableRepository, issueStore, config.maxTransferDuration, - config.transferRequests + config.transferRequests, + config.transferParametersForMode ); } diff --git a/application/src/main/java/org/opentripplanner/gtfs/mapping/FareLegRuleMapper.java b/application/src/main/java/org/opentripplanner/gtfs/mapping/FareLegRuleMapper.java index 2218be9cf30..31fcf95d3cb 100644 --- a/application/src/main/java/org/opentripplanner/gtfs/mapping/FareLegRuleMapper.java +++ b/application/src/main/java/org/opentripplanner/gtfs/mapping/FareLegRuleMapper.java @@ -4,14 +4,18 @@ import java.util.Collection; import java.util.Objects; -import org.opentripplanner.ext.fares.model.Distance; import org.opentripplanner.ext.fares.model.FareDistance; import org.opentripplanner.ext.fares.model.FareLegRule; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.transit.model.basic.Distance; import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public final class FareLegRuleMapper { + private static final Logger LOG = LoggerFactory.getLogger(FareLegRuleMapper.class); + private final FareProductMapper fareProductMapper; private final DataImportIssueStore issueStore; @@ -75,8 +79,28 @@ private static FareDistance createFareDistance( fareLegRule.getMaxDistance().intValue() ); case 1 -> new FareDistance.LinearDistance( - Distance.ofMeters(fareLegRule.getMinDistance()), - Distance.ofMeters(fareLegRule.getMaxDistance()) + Distance + .ofMetersBoxed( + fareLegRule.getMinDistance(), + error -> + LOG.warn( + "Fare leg rule min distance not valid: {} - {}", + fareLegRule.getMinDistance(), + error + ) + ) + .orElse(null), + Distance + .ofMetersBoxed( + fareLegRule.getMaxDistance(), + error -> + LOG.warn( + "Fare leg rule max distance not valid: {} - {}", + fareLegRule.getMaxDistance(), + error + ) + ) + .orElse(null) ); default -> null; }; diff --git a/application/src/main/java/org/opentripplanner/model/TimetableSnapshotProvider.java b/application/src/main/java/org/opentripplanner/model/TimetableSnapshotProvider.java deleted file mode 100644 index 047f0301141..00000000000 --- a/application/src/main/java/org/opentripplanner/model/TimetableSnapshotProvider.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.opentripplanner.model; - -/** - * This interface is used to retrieve the latest available instance of TimetableSnapshot - * that is ready for use in routing. Slightly newer TimetableSnapshots may be available, but still - * in the process of accumulating updates or being indexed and finalized for read-only routing use. - *

- * Any provider implementing this interface is responsible for ensuring access to the latest - * {@code TimetableSnapshot} is handled in a thread-safe manner, as this method can be called by - * any number of concurrent routing requests at once. - *

- * Note that in the long run we don't necessarily want multiple snapshot providers. Ideally we'll - * just have one way of handling these concurrency concerns, so no need for an interface and - * multiple implementations. But in the short term, handling both GTFS-RT and SIRI has led to two - * different providers. - */ -public interface TimetableSnapshotProvider { - TimetableSnapshot getTimetableSnapshot(); -} diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java index d332013d5ac..23293e6d200 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java @@ -36,6 +36,7 @@ import org.opentripplanner.routing.algorithm.transferoptimization.configure.TransferOptimizationServiceConfigurator; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; +import org.opentripplanner.routing.api.request.preference.AccessEgressPreferences; import org.opentripplanner.routing.api.request.request.StreetRequest; import org.opentripplanner.routing.api.response.InputField; import org.opentripplanner.routing.api.response.RoutingError; @@ -239,6 +240,7 @@ private Collection fetchEgress() { private Collection fetchAccessEgresses(AccessEgressType type) { var streetRequest = type.isAccess() ? request.journey().access() : request.journey().egress(); + StreetMode mode = streetRequest.mode(); // Prepare access/egress lists RouteRequest accessRequest = request.clone(); @@ -252,13 +254,15 @@ private Collection fetchAccessEgresses(AccessEgre }); } - Duration durationLimit = accessRequest + AccessEgressPreferences accessEgressPreferences = accessRequest .preferences() .street() - .accessEgress() - .maxDuration() - .valueOf(streetRequest.mode()); - int stopCountLimit = accessRequest.preferences().street().accessEgress().maxStopCount(); + .accessEgress(); + + Duration durationLimit = accessEgressPreferences.maxDuration().valueOf(mode); + int stopCountLimit = accessEgressPreferences + .maxStopCountForMode() + .getOrDefault(mode, accessEgressPreferences.defaultMaxStopCount()); var nearbyStops = AccessEgressRouter.findAccessEgresses( accessRequest, @@ -275,7 +279,7 @@ private Collection fetchAccessEgresses(AccessEgre var results = new ArrayList<>(accessEgresses); // Special handling of flex accesses - if (OTPFeature.FlexRouting.isOn() && streetRequest.mode() == StreetMode.FLEXIBLE) { + if (OTPFeature.FlexRouting.isOn() && mode == StreetMode.FLEXIBLE) { var flexAccessList = FlexAccessEgressRouter.routeAccessEgress( accessRequest, temporaryVerticesContainer, diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java index 6d54973bff9..3f6b0096fd3 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.List; import org.opentripplanner.ext.flex.FlexRouter; +import org.opentripplanner.ext.flex.filter.FilterMapper; import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.routing.algorithm.raptoradapter.router.AdditionalSearchDays; @@ -58,6 +59,7 @@ public static List route( serverContext.graph(), serverContext.transitService(), serverContext.flexParameters(), + FilterMapper.mapFilters(request.journey().transit().filters()), request.dateTime(), request.bookingTime(), additionalSearchDays.additionalSearchDaysInPast(), diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java index f5aa9142fac..847da80f274 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java @@ -6,6 +6,7 @@ import org.opentripplanner.ext.flex.FlexAccessEgress; import org.opentripplanner.ext.flex.FlexParameters; import org.opentripplanner.ext.flex.FlexRouter; +import org.opentripplanner.ext.flex.filter.FilterMapper; import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.routing.algorithm.raptoradapter.router.AdditionalSearchDays; import org.opentripplanner.routing.api.request.RouteRequest; @@ -61,6 +62,7 @@ public static Collection routeAccessEgress( serverContext.graph(), transitService, config, + FilterMapper.mapFilters(request.journey().transit().filters()), request.dateTime(), request.bookingTime(), searchDays.additionalSearchDaysInPast(), diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/GeneralizedCostParametersMapper.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/GeneralizedCostParametersMapper.java index fdaa7e3b4fd..49f348fb753 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/GeneralizedCostParametersMapper.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/GeneralizedCostParametersMapper.java @@ -25,8 +25,11 @@ public static GeneralizedCostParameters map( .transferCost(preferences.transfer().cost()) .waitReluctanceFactor(preferences.transfer().waitReluctance()); - if (request.journey().transfer().mode() == StreetMode.BIKE) { + StreetMode mode = request.journey().transfer().mode(); + if (mode == StreetMode.BIKE) { builder.boardCost(preferences.bike().boardCost()); + } else if (mode == StreetMode.CAR) { + builder.boardCost(preferences.car().boardCost()); } else { builder.boardCost(preferences.walk().boardCost()); } diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java index 8b307321ca8..5cd9fe3fd95 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java @@ -19,7 +19,7 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; -import org.opentripplanner.transit.service.TransitEditorService; +import org.opentripplanner.transit.service.TimetableRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,7 +39,7 @@ public class TransitLayerUpdater { private static final Logger LOG = LoggerFactory.getLogger(TransitLayerUpdater.class); - private final TransitEditorService transitService; + private final TimetableRepository timetableRepository; /** * Cache the TripPatternForDates indexed on the original TripPatterns in order to avoid this @@ -55,15 +55,15 @@ public class TransitLayerUpdater { private final Map> tripPatternsRunningOnDateMapCache = new HashMap<>(); - public TransitLayerUpdater(TransitEditorService transitService) { - this.transitService = transitService; + public TransitLayerUpdater(TimetableRepository timetableRepository) { + this.timetableRepository = timetableRepository; } public void update( Collection updatedTimetables, Map> timetables ) { - if (!transitService.hasRealtimeTransitLayer()) { + if (!timetableRepository.hasRealtimeTransitLayer()) { return; } @@ -71,11 +71,13 @@ public void update( // Make a shallow copy of the realtime transit layer. Only the objects that are copied will be // changed during this update process. - TransitLayer realtimeTransitLayer = new TransitLayer(transitService.getRealtimeTransitLayer()); + TransitLayer realtimeTransitLayer = new TransitLayer( + timetableRepository.getRealtimeTransitLayer() + ); // Instantiate a TripPatternForDateMapper with the new TripPattern mappings TripPatternForDateMapper tripPatternForDateMapper = new TripPatternForDateMapper( - transitService.getServiceCodesRunningForDate() + timetableRepository.getServiceCodesRunningForDate() ); Set datesToBeUpdated = new HashSet<>(); @@ -222,7 +224,7 @@ public void update( // Switch out the reference with the updated realtimeTransitLayer. This is synchronized to // guarantee that the reference is set after all the fields have been updated. - transitService.setRealtimeTransitLayer(realtimeTransitLayer); + timetableRepository.setRealtimeTransitLayer(realtimeTransitLayer); LOG.debug( "UPDATING {} tripPatterns took {} ms", diff --git a/application/src/main/java/org/opentripplanner/routing/api/request/preference/AccessEgressPreferences.java b/application/src/main/java/org/opentripplanner/routing/api/request/preference/AccessEgressPreferences.java index 289e06e6e02..4a8c342275f 100644 --- a/application/src/main/java/org/opentripplanner/routing/api/request/preference/AccessEgressPreferences.java +++ b/application/src/main/java/org/opentripplanner/routing/api/request/preference/AccessEgressPreferences.java @@ -4,6 +4,7 @@ import java.io.Serializable; import java.time.Duration; +import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; @@ -27,18 +28,21 @@ public final class AccessEgressPreferences implements Serializable { private final TimeAndCostPenaltyForEnum penalty; private final DurationForEnum maxDuration; - private final int maxStopCount; + private final int defaultMaxStopCount; + private final Map maxStopCountForMode; private AccessEgressPreferences() { this.maxDuration = durationForStreetModeOf(ofMinutes(45)); this.penalty = DEFAULT_TIME_AND_COST; - this.maxStopCount = 500; + this.defaultMaxStopCount = 500; + this.maxStopCountForMode = Map.of(); } private AccessEgressPreferences(Builder builder) { this.maxDuration = builder.maxDuration; this.penalty = builder.penalty; - this.maxStopCount = builder.maxStopCount; + this.defaultMaxStopCount = builder.defaultMaxStopCount; + this.maxStopCountForMode = Collections.unmodifiableMap(builder.maxStopCountForMode); } public static Builder of() { @@ -57,8 +61,12 @@ public DurationForEnum maxDuration() { return maxDuration; } - public int maxStopCount() { - return maxStopCount; + public int defaultMaxStopCount() { + return defaultMaxStopCount; + } + + public Map maxStopCountForMode() { + return maxStopCountForMode; } @Override @@ -69,13 +77,14 @@ public boolean equals(Object o) { return ( penalty.equals(that.penalty) && maxDuration.equals(that.maxDuration) && - maxStopCount == that.maxStopCount + defaultMaxStopCount == that.defaultMaxStopCount && + maxStopCountForMode.equals(that.maxStopCountForMode) ); } @Override public int hashCode() { - return Objects.hash(penalty, maxDuration, maxStopCount); + return Objects.hash(penalty, maxDuration, defaultMaxStopCount, maxStopCountForMode); } @Override @@ -84,7 +93,8 @@ public String toString() { .of(AccessEgressPreferences.class) .addObj("penalty", penalty, DEFAULT.penalty) .addObj("maxDuration", maxDuration, DEFAULT.maxDuration) - .addObj("maxStopCount", maxStopCount, DEFAULT.maxStopCount) + .addObj("defaultMaxStopCount", defaultMaxStopCount, DEFAULT.defaultMaxStopCount) + .addObj("maxStopCountForMode", maxStopCountForMode, DEFAULT.maxStopCountForMode) .toString(); } @@ -93,13 +103,15 @@ public static class Builder { private final AccessEgressPreferences original; private TimeAndCostPenaltyForEnum penalty; private DurationForEnum maxDuration; - private int maxStopCount; + private Map maxStopCountForMode; + private int defaultMaxStopCount; public Builder(AccessEgressPreferences original) { this.original = original; this.maxDuration = original.maxDuration; this.penalty = original.penalty; - this.maxStopCount = original.maxStopCount; + this.defaultMaxStopCount = original.defaultMaxStopCount; + this.maxStopCountForMode = original.maxStopCountForMode; } public Builder withMaxDuration(Consumer> body) { @@ -112,8 +124,12 @@ public Builder withMaxDuration(Duration defaultValue, Map return withMaxDuration(b -> b.withDefault(defaultValue).withValues(values)); } - public Builder withMaxStopCount(int maxCount) { - this.maxStopCount = maxCount; + public Builder withMaxStopCount( + int defaultMaxStopCount, + Map maxStopCountForMode + ) { + this.defaultMaxStopCount = defaultMaxStopCount; + this.maxStopCountForMode = maxStopCountForMode; return this; } diff --git a/application/src/main/java/org/opentripplanner/routing/api/request/preference/CarPreferences.java b/application/src/main/java/org/opentripplanner/routing/api/request/preference/CarPreferences.java index 3ea3634c623..2e2c3bd9a23 100644 --- a/application/src/main/java/org/opentripplanner/routing/api/request/preference/CarPreferences.java +++ b/application/src/main/java/org/opentripplanner/routing/api/request/preference/CarPreferences.java @@ -23,6 +23,7 @@ public final class CarPreferences implements Serializable { public static final CarPreferences DEFAULT = new CarPreferences(); private final double reluctance; + private final Cost boardCost; private final VehicleParkingPreferences parking; private final VehicleRentalPreferences rental; private final Duration pickupTime; @@ -33,6 +34,7 @@ public final class CarPreferences implements Serializable { /** Create a new instance with default values. */ private CarPreferences() { this.reluctance = 2.0; + this.boardCost = Cost.costOfMinutes(10); this.parking = VehicleParkingPreferences.DEFAULT; this.rental = VehicleRentalPreferences.DEFAULT; this.pickupTime = Duration.ofMinutes(1); @@ -43,6 +45,7 @@ private CarPreferences() { private CarPreferences(Builder builder) { this.reluctance = Units.reluctance(builder.reluctance); + this.boardCost = builder.boardCost; this.parking = builder.parking; this.rental = builder.rental; this.pickupTime = Duration.ofSeconds(Units.duration(builder.pickupTime)); @@ -63,6 +66,15 @@ public double reluctance() { return reluctance; } + /** + * Separate cost for boarding a vehicle with a car, which is different compared to on foot or with a bicycle. This + * is in addition to the cost of the transfer and waiting-time. It is also in addition to + * the {@link TransferPreferences#cost()}. + */ + public int boardCost() { + return boardCost.toSeconds(); + } + /** Parking preferences that can be different per request */ public VehicleParkingPreferences parking() { return parking; @@ -106,6 +118,7 @@ public boolean equals(Object o) { CarPreferences that = (CarPreferences) o; return ( DoubleUtils.doubleEquals(that.reluctance, reluctance) && + boardCost.equals(that.boardCost) && parking.equals(that.parking) && rental.equals(that.rental) && Objects.equals(pickupTime, that.pickupTime) && @@ -119,6 +132,7 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash( reluctance, + boardCost, parking, rental, pickupTime, @@ -133,6 +147,7 @@ public String toString() { return ToStringBuilder .of(CarPreferences.class) .addNum("reluctance", reluctance, DEFAULT.reluctance) + .addObj("boardCost", boardCost, DEFAULT.boardCost) .addObj("parking", parking, DEFAULT.parking) .addObj("rental", rental, DEFAULT.rental) .addObj("pickupTime", pickupTime, DEFAULT.pickupTime) @@ -147,6 +162,7 @@ public static class Builder { private final CarPreferences original; private double reluctance; + private Cost boardCost; private VehicleParkingPreferences parking; private VehicleRentalPreferences rental; private int pickupTime; @@ -157,6 +173,7 @@ public static class Builder { public Builder(CarPreferences original) { this.original = original; this.reluctance = original.reluctance; + this.boardCost = original.boardCost; this.parking = original.parking; this.rental = original.rental; this.pickupTime = (int) original.pickupTime.toSeconds(); @@ -174,6 +191,15 @@ public Builder withReluctance(double reluctance) { return this; } + public Cost boardCost() { + return boardCost; + } + + public Builder withBoardCost(int boardCost) { + this.boardCost = Cost.costOfSeconds(boardCost); + return this; + } + public Builder withParking(Consumer body) { this.parking = ifNotNull(this.parking, original.parking).copyOf().apply(body).build(); return this; diff --git a/application/src/main/java/org/opentripplanner/routing/api/request/request/filter/TransitFilterRequest.java b/application/src/main/java/org/opentripplanner/routing/api/request/request/filter/TransitFilterRequest.java index 6380ace7c83..d3b19b664ff 100644 --- a/application/src/main/java/org/opentripplanner/routing/api/request/request/filter/TransitFilterRequest.java +++ b/application/src/main/java/org/opentripplanner/routing/api/request/request/filter/TransitFilterRequest.java @@ -31,6 +31,10 @@ public List select() { return Collections.unmodifiableList(Arrays.asList(select)); } + public List not() { + return Collections.unmodifiableList(Arrays.asList(not)); + } + public static Builder of() { return new Builder(); } diff --git a/application/src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleFuel.java b/application/src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleFuel.java new file mode 100644 index 00000000000..0cc8e489b49 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleFuel.java @@ -0,0 +1,40 @@ +package org.opentripplanner.service.vehiclerental.model; + +import javax.annotation.Nullable; +import org.opentripplanner.transit.model.basic.Distance; +import org.opentripplanner.transit.model.basic.Ratio; + +/** + * Contains information about the current battery or fuel status. + * See the GBFS + * vehicle_status specification for more details. + */ +public class RentalVehicleFuel { + + /** + * Current fuel percentage, expressed from 0 to 1. + */ + @Nullable + private final Ratio percent; + + /** + * Distance that the vehicle can travel with the current fuel. + */ + @Nullable + private final Distance range; + + public RentalVehicleFuel(@Nullable Ratio fuelPercent, @Nullable Distance range) { + this.percent = fuelPercent; + this.range = range; + } + + @Nullable + public Ratio percent() { + return this.percent; + } + + @Nullable + public Distance range() { + return range; + } +} diff --git a/application/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalVehicle.java b/application/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalVehicle.java index 042e608c88f..446711bad36 100644 --- a/application/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalVehicle.java +++ b/application/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalVehicle.java @@ -22,9 +22,9 @@ public class VehicleRentalVehicle implements VehicleRentalPlace { public boolean isReserved = false; public boolean isDisabled = false; public Instant lastReported; - public Double currentRangeMeters; public VehicleRentalStation station; public String pricingPlanId; + public RentalVehicleFuel fuel; @Override public FeedScopedId getId() { @@ -133,4 +133,8 @@ public VehicleRentalStationUris getRentalUris() { public VehicleRentalSystem getVehicleRentalSystem() { return system; } + + public RentalVehicleFuel getFuel() { + return fuel; + } } diff --git a/application/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java b/application/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java index 16c6f1e722c..f8533bd75ca 100644 --- a/application/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java +++ b/application/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java @@ -15,6 +15,7 @@ import java.time.LocalDate; import java.time.ZoneId; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -23,6 +24,7 @@ import org.opentripplanner.ext.emissions.EmissionsConfig; import org.opentripplanner.ext.fares.FaresConfiguration; import org.opentripplanner.framework.geometry.CompactElevationProfile; +import org.opentripplanner.graph_builder.module.TransferParameters; import org.opentripplanner.graph_builder.module.ned.parameter.DemExtractParameters; import org.opentripplanner.graph_builder.module.ned.parameter.DemExtractParametersList; import org.opentripplanner.graph_builder.module.osm.parameters.OsmExtractParameters; @@ -32,6 +34,8 @@ import org.opentripplanner.model.calendar.ServiceDateInterval; import org.opentripplanner.netex.config.NetexFeedParameters; import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.routing.api.request.StreetMode; +import org.opentripplanner.routing.api.request.framework.DurationForEnum; import org.opentripplanner.routing.fares.FareServiceFactory; import org.opentripplanner.standalone.config.buildconfig.DemConfig; import org.opentripplanner.standalone.config.buildconfig.GtfsConfig; @@ -39,6 +43,7 @@ import org.opentripplanner.standalone.config.buildconfig.NetexConfig; import org.opentripplanner.standalone.config.buildconfig.OsmConfig; import org.opentripplanner.standalone.config.buildconfig.S3BucketConfig; +import org.opentripplanner.standalone.config.buildconfig.TransferConfig; import org.opentripplanner.standalone.config.buildconfig.TransferRequestConfig; import org.opentripplanner.standalone.config.buildconfig.TransitFeedConfig; import org.opentripplanner.standalone.config.buildconfig.TransitFeeds; @@ -151,6 +156,7 @@ public class BuildConfig implements OtpDataStoreConfig { public final IslandPruningConfig islandPruning; public final Duration maxTransferDuration; + public final Map transferParametersForMode; public final NetexFeedParameters netexDefaults; public final GtfsFeedParameters gtfsDefaults; @@ -284,9 +290,10 @@ When set to true (it is false by default), the elevation module will include the .of("maxTransferDuration") .since(V2_1) .summary( - "Transfers up to this duration with the default walk speed value will be pre-calculated and included in the Graph." + "Transfers up to this duration with a mode-specific speed value will be pre-calculated and included in the Graph." ) .asDuration(Duration.ofMinutes(30)); + transferParametersForMode = TransferConfig.map(root, "transferParametersForMode"); maxStopToShapeSnapDistance = root .of("maxStopToShapeSnapDistance") diff --git a/application/src/main/java/org/opentripplanner/standalone/config/buildconfig/TransferConfig.java b/application/src/main/java/org/opentripplanner/standalone/config/buildconfig/TransferConfig.java new file mode 100644 index 00000000000..5549e009c48 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/standalone/config/buildconfig/TransferConfig.java @@ -0,0 +1,44 @@ +package org.opentripplanner.standalone.config.buildconfig; + +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_7; + +import java.util.EnumMap; +import java.util.Map; +import org.opentripplanner.graph_builder.module.TransferParameters; +import org.opentripplanner.routing.api.request.StreetMode; +import org.opentripplanner.standalone.config.framework.json.NodeAdapter; + +public class TransferConfig { + + public static Map map(NodeAdapter root, String parameterName) { + return root + .of(parameterName) + .since(V2_7) + .summary("Configures mode-specific properties for transfer calculations.") + .description( + """ +This field enables configuring mode-specific parameters for transfer calculations. +To configure mode-specific parameters, the modes should also be used in the `transferRequests` field in the build config. + +**Example** + +```JSON +// build-config.json +{ + "transferParametersForMode": { + "CAR": { + "disableDefaultTransfers": true, + "carsAllowedStopMaxTransferDuration": "3h" + }, + "BIKE": { + "maxTransferDuration": "30m", + "carsAllowedStopMaxTransferDuration": "3h" + } + } +} +``` +""" + ) + .asEnumMap(StreetMode.class, TransferParametersMapper::map, new EnumMap<>(StreetMode.class)); + } +} diff --git a/application/src/main/java/org/opentripplanner/standalone/config/buildconfig/TransferParametersMapper.java b/application/src/main/java/org/opentripplanner/standalone/config/buildconfig/TransferParametersMapper.java new file mode 100644 index 00000000000..e9cce1a367d --- /dev/null +++ b/application/src/main/java/org/opentripplanner/standalone/config/buildconfig/TransferParametersMapper.java @@ -0,0 +1,67 @@ +package org.opentripplanner.standalone.config.buildconfig; + +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_7; + +import org.opentripplanner.graph_builder.module.TransferParameters; +import org.opentripplanner.standalone.config.framework.json.NodeAdapter; + +public class TransferParametersMapper { + + public static TransferParameters map(NodeAdapter c) { + TransferParameters.Builder builder = new TransferParameters.Builder(); + builder.withMaxTransferDuration( + c + .of("maxTransferDuration") + .summary("This overwrites the default `maxTransferDuration` for the given mode.") + .since(V2_7) + .asDuration(TransferParameters.DEFAULT_MAX_TRANSFER_DURATION) + ); + builder.withCarsAllowedStopMaxTransferDuration( + c + .of("carsAllowedStopMaxTransferDuration") + .summary( + "This is used for specifying a `maxTransferDuration` value to use with transfers between stops which are visited by trips that allow cars." + ) + .description( + """ +This parameter configures additional transfers to be calculated for the specified mode only between stops that have trips with cars. +The transfers are calculated for the mode in a range based on the given duration. +By default, these transfers are not calculated unless specified for a mode with this field. + +Calculating transfers only between stops that have trips with cars can be useful with car ferries, for example. +Using transit with cars can only occur between certain stops. +These kinds of stops require support for loading cars into ferries, for example. +The default transfers are calculated based on a configurable range (configurable by using the `maxTransferDuration` field) +which limits transfers from stops to only be calculated to other stops that are in range. +When compared to walking, using a car can cover larger distances within the same duration specified in the `maxTransferDuration` field. +This can lead to large amounts of transfers calculated between stops that do not require car transfers between them. +This in turn can lead to a large increase in memory for the stored graph, depending on the data used in the graph. + +For cars, using this parameter in conjunction with `disableDefaultTransfers` allows calculating transfers only between relevant stops. +For bikes, using this parameter can enable transfers between ferry stops that would normally not be in range. +In Finland this is useful for bike routes that use ferries near the Turku archipelago, for example. +""" + ) + .since(V2_7) + .asDuration(TransferParameters.DEFAULT_CARS_ALLOWED_STOP_MAX_TRANSFER_DURATION) + ); + builder.withDisableDefaultTransfers( + c + .of("disableDefaultTransfers") + .summary("This disables default transfer calculations.") + .description( + """ +The default transfers are calculated based on a configurable range (configurable by using the `maxTransferDuration` field) +which limits transfers from stops to only be calculated to other stops that are in range. +This parameter disables these transfers. +A motivation to disable default transfers could be related to using the `carsAllowedStopMaxTransferDuration` field which only +calculates transfers between stops that have trips with cars. +For example, when using the `carsAllowedStopMaxTransferDuration` field with cars, the default transfers can be redundant. +""" + ) + .since(V2_7) + .asBoolean(TransferParameters.DEFAULT_DISABLE_DEFAULT_TRANSFERS) + ); + return builder.build(); + } +} diff --git a/application/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java b/application/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java index 454ab29a68c..4a623f02c3b 100644 --- a/application/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java +++ b/application/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java @@ -542,7 +542,20 @@ duration can be set per mode(`maxDurationForMode`), because some street modes se Safety limit to prevent access to and egress from too many stops. """ ) - .asInt(dftAccessEgress.maxStopCount()) + .asInt(dftAccessEgress.defaultMaxStopCount()), + cae + .of("maxStopCountForMode") + .since(V2_7) + .summary( + "Maximal number of stops collected in access/egress routing for the given mode" + ) + .description( + """ + Safety limit to prevent access to and egress from too many stops. + Mode-specific version of `maxStopCount`. + """ + ) + .asEnumMap(StreetMode.class, Integer.class) ); }) .withMaxDirectDuration( @@ -623,6 +636,19 @@ private static void mapCarPreferences(NodeAdapter root, CarPreferences.Builder b ) .asDouble(dft.reluctance()) ) + .withBoardCost( + c + .of("boardCost") + .since(V2_7) + .summary( + "Prevents unnecessary transfers by adding a cost for boarding a transit vehicle." + ) + .description( + "This is the cost that is used when boarding while driving. " + + "This can be different compared to the boardCost while walking or cycling." + ) + .asInt(dft.boardCost()) + ) .withPickupCost( c .of("pickupCost") diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java index eeaaf6427cb..f2edde933e9 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java @@ -16,7 +16,6 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitTuningParameters; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerMapper; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.service.osminfo.OsmInfoGraphBuildRepository; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; @@ -35,9 +34,9 @@ import org.opentripplanner.standalone.server.OTPWebApplication; import org.opentripplanner.street.model.StreetLimitationParameters; import org.opentripplanner.street.model.elevation.ElevationUtils; -import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.updater.configure.UpdaterConfigurator; +import org.opentripplanner.updater.trip.TimetableSnapshotManager; import org.opentripplanner.utils.logging.ProgressTracker; import org.opentripplanner.visualizer.GraphVisualizer; import org.slf4j.Logger; @@ -171,7 +170,7 @@ private void setupTransitRoutingServer() { enableRequestTraceLogging(); createMetricsLogging(); - creatTransitLayerForRaptor(timetableRepository(), routerConfig().transitTuningConfig()); + createTransitLayerForRaptor(timetableRepository(), routerConfig().transitTuningConfig()); /* Create updater modules from JSON config. */ UpdaterConfigurator.configure( @@ -180,6 +179,7 @@ private void setupTransitRoutingServer() { vehicleRentalRepository(), vehicleParkingRepository(), timetableRepository(), + snapshotManager(), routerConfig().updaterConfig() ); @@ -217,7 +217,7 @@ private void initEllipsoidToGeoidDifference() { /** * Create transit layer for Raptor routing. Here we map the scheduled timetables. */ - public static void creatTransitLayerForRaptor( + public static void createTransitLayerForRaptor( TimetableRepository timetableRepository, TransitTuningParameters tuningParameters ) { @@ -233,9 +233,6 @@ public static void creatTransitLayerForRaptor( timetableRepository.setRealtimeTransitLayer( new TransitLayer(timetableRepository.getTransitLayer()) ); - timetableRepository.setTransitLayerUpdater( - new TransitLayerUpdater(new DefaultTransitService(timetableRepository)) - ); } public static void initializeTransferCache( @@ -287,6 +284,10 @@ public VehicleRentalRepository vehicleRentalRepository() { return factory.vehicleRentalRepository(); } + private TimetableSnapshotManager snapshotManager() { + return factory.timetableSnapshotManager(); + } + public VehicleParkingService vehicleParkingService() { return factory.vehicleParkingService(); } diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java index 3d479f0fa63..6b2fdff0947 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java @@ -41,6 +41,7 @@ import org.opentripplanner.transit.configure.TransitModule; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.transit.service.TransitService; +import org.opentripplanner.updater.trip.TimetableSnapshotManager; import org.opentripplanner.visualizer.GraphVisualizer; /** @@ -81,6 +82,7 @@ public interface ConstructApplicationFactory { VehicleRentalService vehicleRentalService(); VehicleParkingRepository vehicleParkingRepository(); VehicleParkingService vehicleParkingService(); + TimetableSnapshotManager timetableSnapshotManager(); DataImportIssueSummary dataImportIssueSummary(); @Nullable diff --git a/application/src/main/java/org/opentripplanner/transit/api/request/FindRoutesRequest.java b/application/src/main/java/org/opentripplanner/transit/api/request/FindRoutesRequest.java new file mode 100644 index 00000000000..d0f2153921d --- /dev/null +++ b/application/src/main/java/org/opentripplanner/transit/api/request/FindRoutesRequest.java @@ -0,0 +1,65 @@ +package org.opentripplanner.transit.api.request; + +import org.opentripplanner.transit.api.model.FilterValues; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.network.Route; + +/** + * A request for finding {@link Route}s. + *

+ * This request is used to retrieve Routes that match the provided filter values. + * At least one filter value must be provided. + */ +public class FindRoutesRequest { + + private final boolean flexibleOnly; + private final String longName; + private final String shortName; + private final FilterValues shortNames; + private final FilterValues transitModes; + private final FilterValues agencyIds; + + protected FindRoutesRequest( + boolean flexibleOnly, + String longName, + String shortName, + FilterValues shortNames, + FilterValues transitModes, + FilterValues agencyIds + ) { + this.flexibleOnly = flexibleOnly; + this.longName = longName; + this.shortName = shortName; + this.shortNames = shortNames; + this.transitModes = transitModes; + this.agencyIds = agencyIds; + } + + public static FindRoutesRequestBuilder of() { + return new FindRoutesRequestBuilder(); + } + + public boolean flexibleOnly() { + return flexibleOnly; + } + + public String longName() { + return longName; + } + + public String shortName() { + return shortName; + } + + public FilterValues shortNames() { + return shortNames; + } + + public FilterValues transitModes() { + return transitModes; + } + + public FilterValues agencies() { + return agencyIds; + } +} diff --git a/application/src/main/java/org/opentripplanner/transit/api/request/FindRoutesRequestBuilder.java b/application/src/main/java/org/opentripplanner/transit/api/request/FindRoutesRequestBuilder.java new file mode 100644 index 00000000000..66b03904abe --- /dev/null +++ b/application/src/main/java/org/opentripplanner/transit/api/request/FindRoutesRequestBuilder.java @@ -0,0 +1,65 @@ +package org.opentripplanner.transit.api.request; + +import java.util.List; +import javax.annotation.Nullable; +import org.opentripplanner.transit.api.model.FilterValues; +import org.opentripplanner.transit.model.basic.TransitMode; + +public class FindRoutesRequestBuilder { + + private boolean flexibleOnly; + private String longName; + private String shortName; + private FilterValues shortNames = FilterValues.ofEmptyIsEverything( + "shortNames", + List.of() + ); + private FilterValues transitModes = FilterValues.ofEmptyIsEverything( + "transitModes", + List.of() + ); + private FilterValues agencies = FilterValues.ofEmptyIsEverything("agencies", List.of()); + + protected FindRoutesRequestBuilder() {} + + public FindRoutesRequestBuilder withAgencies(FilterValues agencies) { + this.agencies = agencies; + return this; + } + + public FindRoutesRequestBuilder withFlexibleOnly(boolean flexibleOnly) { + this.flexibleOnly = flexibleOnly; + return this; + } + + public FindRoutesRequestBuilder withLongName(@Nullable String longName) { + this.longName = longName; + return this; + } + + public FindRoutesRequestBuilder withShortName(@Nullable String shortName) { + this.shortName = shortName; + return this; + } + + public FindRoutesRequestBuilder withShortNames(FilterValues shortNames) { + this.shortNames = shortNames; + return this; + } + + public FindRoutesRequestBuilder withTransitModes(FilterValues transitModes) { + this.transitModes = transitModes; + return this; + } + + public FindRoutesRequest build() { + return new FindRoutesRequest( + flexibleOnly, + longName, + shortName, + shortNames, + transitModes, + agencies + ); + } +} diff --git a/application/src/main/java/org/opentripplanner/transit/configure/TransitModule.java b/application/src/main/java/org/opentripplanner/transit/configure/TransitModule.java index 37e142d7e0f..0f246ad7cec 100644 --- a/application/src/main/java/org/opentripplanner/transit/configure/TransitModule.java +++ b/application/src/main/java/org/opentripplanner/transit/configure/TransitModule.java @@ -2,9 +2,18 @@ import dagger.Binds; import dagger.Module; +import dagger.Provides; +import jakarta.inject.Singleton; +import java.time.LocalDate; +import java.time.ZoneId; +import org.opentripplanner.model.TimetableSnapshot; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; import org.opentripplanner.standalone.api.HttpRequestScoped; +import org.opentripplanner.standalone.config.ConfigModel; import org.opentripplanner.transit.service.DefaultTransitService; +import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.transit.service.TransitService; +import org.opentripplanner.updater.trip.TimetableSnapshotManager; @Module public abstract class TransitModule { @@ -12,4 +21,36 @@ public abstract class TransitModule { @Binds @HttpRequestScoped abstract TransitService bind(DefaultTransitService service); + + @Provides + @Singleton + public static TimetableSnapshotManager timetableSnapshotManager( + TransitLayerUpdater transitLayerUpdater, + ConfigModel config, + TimetableRepository timetableRepository + ) { + return new TimetableSnapshotManager( + transitLayerUpdater, + config.routerConfig().updaterConfig().timetableSnapshotParameters(), + () -> LocalDate.now(timetableRepository.getTimeZone()) + ); + } + + /** + * Create a single instance of the transit layer updater which holds the incremental caches for + * the updates that need to applied to the {@link org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer}. + */ + @Provides + @Singleton + public static TransitLayerUpdater transitLayerUpdater(TimetableRepository timetableRepository) { + return new TransitLayerUpdater(timetableRepository); + } + + /** + * Provides the currently published, immutable {@link TimetableSnapshot}. + */ + @Provides + public static TimetableSnapshot timetableSnapshot(TimetableSnapshotManager manager) { + return manager.getTimetableSnapshot(); + } } diff --git a/application/src/main/java/org/opentripplanner/transit/model/basic/Distance.java b/application/src/main/java/org/opentripplanner/transit/model/basic/Distance.java new file mode 100644 index 00000000000..04c6c1bbf6f --- /dev/null +++ b/application/src/main/java/org/opentripplanner/transit/model/basic/Distance.java @@ -0,0 +1,112 @@ +package org.opentripplanner.transit.model.basic; + +import java.util.Optional; +import java.util.function.Consumer; +import javax.annotation.Nullable; +import org.opentripplanner.utils.tostring.ValueObjectToStringBuilder; + +public class Distance { + + private static final int MILLIMETERS_PER_M = 1000; + private static final int MILLIMETERS_PER_KM = 1000 * MILLIMETERS_PER_M; + private final int millimeters; + + /** + * Represents a distance. + * The class ensures that the distance, saved as an integer + * representing the millimeters, is not negative. + */ + private Distance(int distanceInMillimeters) { + this.millimeters = distanceInMillimeters; + } + + /** + * This method is similar to {@link #of(double, Consumer)}, but throws an + * {@link IllegalArgumentException} if the distance is negative. + */ + private static Distance of(int distanceInMillimeters) { + return of( + distanceInMillimeters, + errMsg -> { + throw new IllegalArgumentException(errMsg); + } + ) + .orElseThrow(); + } + + private static Optional of( + int distanceInMillimeters, + Consumer validationErrorHandler + ) { + if (distanceInMillimeters >= 0) { + return Optional.of(new Distance(distanceInMillimeters)); + } else { + validationErrorHandler.accept( + "Distance must be greater or equal than 0, but was: " + distanceInMillimeters + ); + return Optional.empty(); + } + } + + private static Optional ofBoxed( + @Nullable Double value, + Consumer validationErrorHandler, + int multiplier + ) { + if (value == null) { + return Optional.empty(); + } + return of((int) (value * multiplier), validationErrorHandler); + } + + /** Returns a Distance object representing the given number of meters */ + public static Optional ofMetersBoxed( + @Nullable Double value, + Consumer validationErrorHandler + ) { + return ofBoxed(value, validationErrorHandler, MILLIMETERS_PER_M); + } + + /** Returns a Distance object representing the given number of kilometers */ + public static Optional ofKilometersBoxed( + @Nullable Double value, + Consumer validationErrorHandler + ) { + return ofBoxed(value, validationErrorHandler, MILLIMETERS_PER_KM); + } + + /** Returns the distance in meters */ + public int toMeters() { + double meters = (double) this.millimeters / (double) MILLIMETERS_PER_M; + return (int) Math.round(meters); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + var other = (Distance) o; + return this.millimeters == other.millimeters; + } + + @Override + public int hashCode() { + return Integer.hashCode(this.millimeters); + } + + @Override + public String toString() { + if (millimeters > MILLIMETERS_PER_KM) { + return ValueObjectToStringBuilder + .of() + .addNum((double) this.millimeters / (double) MILLIMETERS_PER_KM, "km") + .toString(); + } else { + return ValueObjectToStringBuilder + .of() + .addNum((double) this.millimeters / (double) MILLIMETERS_PER_M, "m") + .toString(); + } + } +} diff --git a/application/src/main/java/org/opentripplanner/transit/model/basic/Ratio.java b/application/src/main/java/org/opentripplanner/transit/model/basic/Ratio.java new file mode 100644 index 00000000000..0ca16391475 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/transit/model/basic/Ratio.java @@ -0,0 +1,76 @@ +package org.opentripplanner.transit.model.basic; + +import java.util.Optional; +import java.util.function.Consumer; +import javax.annotation.Nullable; +import org.opentripplanner.utils.lang.DoubleUtils; + +/** + * Represents a ratio within the range [0, 1]. + * The class ensures that the ratio value, represented as a double, + * falls withing the specified range. + */ +public class Ratio { + + private final double ratio; + + private Ratio(double ratio) { + this.ratio = DoubleUtils.roundTo3Decimals(ratio); + } + + /** + * This method is similar to {@link #of(double, Consumer)}, but throws an + * {@link IllegalArgumentException} if the ratio is not valid. + */ + public static Ratio of(double ratio) { + return of( + ratio, + errMsg -> { + throw new IllegalArgumentException(errMsg); + } + ) + .orElseThrow(); + } + + public static Optional of(double ratio, Consumer validationErrorHandler) { + if (ratio >= 0d && ratio <= 1d) { + return Optional.of(new Ratio(ratio)); + } else { + validationErrorHandler.accept("Ratio must be in range [0,1], but was: " + ratio); + return Optional.empty(); + } + } + + public static Optional ofBoxed( + @Nullable Double ratio, + Consumer validationErrorHandler + ) { + if (ratio == null) { + return Optional.empty(); + } + return of(ratio, validationErrorHandler); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + var other = (Ratio) o; + return Double.compare(ratio, other.ratio) == 0; + } + + @Override + public int hashCode() { + return Double.hashCode(ratio); + } + + @Override + public String toString() { + return Double.toString(ratio); + } + + public double asDouble() { + return ratio; + } +} diff --git a/application/src/main/java/org/opentripplanner/transit/model/filter/expr/CaseInsensitiveStringPrefixMatcher.java b/application/src/main/java/org/opentripplanner/transit/model/filter/expr/CaseInsensitiveStringPrefixMatcher.java new file mode 100644 index 00000000000..befa4517426 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/transit/model/filter/expr/CaseInsensitiveStringPrefixMatcher.java @@ -0,0 +1,41 @@ +package org.opentripplanner.transit.model.filter.expr; + +import java.util.function.Function; + +/** + * A matcher that checks if a string field starts with a given value. + *

+ * @param The type of the entity being matched. + */ +public class CaseInsensitiveStringPrefixMatcher implements Matcher { + + private final String typeName; + private final String value; + private final Function valueProvider; + + /** + * @param typeName The typeName appears in the toString for easier debugging. + * @param value - The String that may be a prefix. + * @param valueProvider - A function that maps the entity being matched to the String being + * checked for a prefix match. + */ + public CaseInsensitiveStringPrefixMatcher( + String typeName, + String value, + Function valueProvider + ) { + this.typeName = typeName; + this.value = value; + this.valueProvider = valueProvider; + } + + @Override + public boolean match(T entity) { + return valueProvider.apply(entity).toLowerCase().startsWith(value.toLowerCase()); + } + + @Override + public String toString() { + return typeName + " has prefix: " + value; + } +} diff --git a/application/src/main/java/org/opentripplanner/transit/model/filter/expr/NullSafeWrapperMatcher.java b/application/src/main/java/org/opentripplanner/transit/model/filter/expr/NullSafeWrapperMatcher.java new file mode 100644 index 00000000000..4e972ac69c6 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/transit/model/filter/expr/NullSafeWrapperMatcher.java @@ -0,0 +1,42 @@ +package org.opentripplanner.transit.model.filter.expr; + +import java.util.function.Function; + +/** + * A matcher that validates that a value is not null before applying another matcher. A useful case + * is when you want to check that a String field is not null before applying a {@link CaseInsensitiveStringPrefixMatcher}. + *

+ * @param The type of the entity being matched. + * @param The type of the value that the matcher will test for not null. + */ +public class NullSafeWrapperMatcher implements Matcher { + + private final String typeName; + private final Function valueProvider; + private final Matcher valueMatcher; + + /** + * @param typeName The typeName appears in the toString for easier debugging. + * @param valueProvider The function that maps the entity being matched by this matcher (T) to + * the value being checked for non-null. + */ + public NullSafeWrapperMatcher( + String typeName, + Function valueProvider, + Matcher valueMatcher + ) { + this.typeName = typeName; + this.valueProvider = valueProvider; + this.valueMatcher = valueMatcher; + } + + @Override + public boolean match(T entity) { + return valueProvider.apply(entity) != null && valueMatcher.match(entity); + } + + @Override + public String toString() { + return typeName + " is not null"; + } +} diff --git a/application/src/main/java/org/opentripplanner/transit/model/filter/transit/RouteMatcherFactory.java b/application/src/main/java/org/opentripplanner/transit/model/filter/transit/RouteMatcherFactory.java new file mode 100644 index 00000000000..c673a193a50 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/transit/model/filter/transit/RouteMatcherFactory.java @@ -0,0 +1,81 @@ +package org.opentripplanner.transit.model.filter.transit; + +import java.util.function.Predicate; +import org.opentripplanner.transit.api.request.FindRoutesRequest; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.filter.expr.CaseInsensitiveStringPrefixMatcher; +import org.opentripplanner.transit.model.filter.expr.EqualityMatcher; +import org.opentripplanner.transit.model.filter.expr.ExpressionBuilder; +import org.opentripplanner.transit.model.filter.expr.GenericUnaryMatcher; +import org.opentripplanner.transit.model.filter.expr.Matcher; +import org.opentripplanner.transit.model.filter.expr.NullSafeWrapperMatcher; +import org.opentripplanner.transit.model.network.Route; + +public class RouteMatcherFactory { + + public static Matcher of( + FindRoutesRequest request, + Predicate isFlexRoutePredicate + ) { + ExpressionBuilder expr = ExpressionBuilder.of(); + + if (request.flexibleOnly()) { + expr.matches(isFlexRoute(isFlexRoutePredicate)); + } + expr.atLeastOneMatch(request.agencies(), RouteMatcherFactory::agencies); + expr.atLeastOneMatch(request.transitModes(), RouteMatcherFactory::transitModes); + if (request.shortName() != null) { + expr.matches(shortName(request.shortName())); + } + expr.atLeastOneMatch(request.shortNames(), RouteMatcherFactory::shortNames); + if (request.longName() != null) { + expr.matches(longName(request.longName())); + } + + return expr.build(); + } + + static Matcher agencies(String agencyId) { + return new NullSafeWrapperMatcher<>( + "agency", + Route::getAgency, + new EqualityMatcher<>("agencyId", agencyId, route -> route.getAgency().getId().getId()) + ); + } + + static Matcher transitModes(TransitMode transitMode) { + return new EqualityMatcher<>("transitMode", transitMode, Route::getMode); + } + + static Matcher shortName(String publicCode) { + return new NullSafeWrapperMatcher<>( + "shortName", + Route::getShortName, + new EqualityMatcher<>("shortName", publicCode, Route::getShortName) + ); + } + + static Matcher shortNames(String publicCode) { + return new NullSafeWrapperMatcher<>( + "shortNames", + Route::getShortName, + new EqualityMatcher<>("shortNames", publicCode, Route::getShortName) + ); + } + + static Matcher isFlexRoute(Predicate isFlexRoute) { + return new GenericUnaryMatcher<>("isFlexRoute", isFlexRoute); + } + + static Matcher longName(String name) { + return new NullSafeWrapperMatcher<>( + "longName", + Route::getLongName, + new CaseInsensitiveStringPrefixMatcher<>( + "name", + name, + route -> route.getLongName().toString() + ) + ); + } +} diff --git a/application/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java b/application/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java index d27fe138ef4..cd86f157e16 100644 --- a/application/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java +++ b/application/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java @@ -14,6 +14,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -36,12 +37,14 @@ import org.opentripplanner.routing.stoptimes.ArrivalDeparture; import org.opentripplanner.routing.stoptimes.StopTimesHelper; import org.opentripplanner.transit.api.request.FindRegularStopsByBoundingBoxRequest; +import org.opentripplanner.transit.api.request.FindRoutesRequest; import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; import org.opentripplanner.transit.api.request.TripRequest; import org.opentripplanner.transit.model.basic.Notice; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.filter.expr.Matcher; import org.opentripplanner.transit.model.filter.transit.RegularStopMatcherFactory; +import org.opentripplanner.transit.model.filter.transit.RouteMatcherFactory; import org.opentripplanner.transit.model.filter.transit.TripMatcherFactory; import org.opentripplanner.transit.model.filter.transit.TripOnServiceDateMatcherFactory; import org.opentripplanner.transit.model.framework.AbstractTransitEntity; @@ -78,22 +81,27 @@ public class DefaultTransitService implements TransitEditorService { private final TimetableRepositoryIndex timetableRepositoryIndex; /** - * This should only be accessed through the getTimetableSnapshot method. + * A nullable timetable snapshot containing real-time updates. If {@code null} then this + * instance does not contain any real-time information. */ - private TimetableSnapshot timetableSnapshot; + @Nullable + private final TimetableSnapshot timetableSnapshot; - @Inject + /** + * Create a service without a real-time snapshot (and therefore without any real-time data). + */ public DefaultTransitService(TimetableRepository timetableRepository) { - this.timetableRepository = timetableRepository; - this.timetableRepositoryIndex = timetableRepository.getTimetableRepositoryIndex(); + this(timetableRepository, null); } + @Inject public DefaultTransitService( TimetableRepository timetableRepository, - TimetableSnapshot timetableSnapshotBuffer + @Nullable TimetableSnapshot timetableSnapshot ) { - this(timetableRepository); - this.timetableSnapshot = timetableSnapshotBuffer; + this.timetableRepository = timetableRepository; + this.timetableRepositoryIndex = timetableRepository.getTimetableRepositoryIndex(); + this.timetableSnapshot = timetableSnapshot; } @Override @@ -183,9 +191,8 @@ public RegularStop getRegularStop(FeedScopedId id) { @Override public Route getRoute(FeedScopedId id) { - TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); - if (currentSnapshot != null) { - Route realtimeAddedRoute = currentSnapshot.getRealtimeAddedRoute(id); + if (timetableSnapshot != null) { + Route realtimeAddedRoute = timetableSnapshot.getRealtimeAddedRoute(id); if (realtimeAddedRoute != null) { return realtimeAddedRoute; } @@ -193,6 +200,17 @@ public Route getRoute(FeedScopedId id) { return timetableRepositoryIndex.getRouteForId(id); } + @Override + public Collection getRoutes(Collection ids) { + return ids.stream().map(this::getRoute).filter(Objects::nonNull).toList(); + } + + @Override + public Collection findRoutes(FindRoutesRequest request) { + Matcher matcher = RouteMatcherFactory.of(request, this.getFlexIndex()::contains); + return listRoutes().stream().filter(matcher::match).toList(); + } + /** * Add a route to the transit model. * Used only in unit tests. @@ -260,9 +278,8 @@ public StopLocationsGroup getStopLocationsGroup(FeedScopedId id) { @Override public Trip getTrip(FeedScopedId id) { - TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); - if (currentSnapshot != null) { - Trip trip = currentSnapshot.getRealTimeAddedTrip(id); + if (timetableSnapshot != null) { + Trip trip = timetableSnapshot.getRealTimeAddedTrip(id); if (trip != null) { return trip; } @@ -282,7 +299,6 @@ public Trip getScheduledTrip(FeedScopedId id) { @Override public List listCanceledTrips() { OTPRequestTimeoutException.checkForTimeout(); - var timetableSnapshot = lazyGetTimeTableSnapShot(); if (timetableSnapshot == null) { return List.of(); } @@ -294,11 +310,10 @@ public List listCanceledTrips() { @Override public Collection listTrips() { OTPRequestTimeoutException.checkForTimeout(); - TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); - if (currentSnapshot != null) { + if (timetableSnapshot != null) { return new CollectionsView<>( timetableRepositoryIndex.getAllTrips(), - currentSnapshot.listRealTimeAddedTrips() + timetableSnapshot.listRealTimeAddedTrips() ); } return Collections.unmodifiableCollection(timetableRepositoryIndex.getAllTrips()); @@ -307,11 +322,10 @@ public Collection listTrips() { @Override public Collection listRoutes() { OTPRequestTimeoutException.checkForTimeout(); - TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); - if (currentSnapshot != null) { + if (timetableSnapshot != null) { return new CollectionsView<>( timetableRepositoryIndex.getAllRoutes(), - currentSnapshot.listRealTimeAddedRoutes() + timetableSnapshot.listRealTimeAddedRoutes() ); } return timetableRepositoryIndex.getAllRoutes(); @@ -319,9 +333,8 @@ public Collection listRoutes() { @Override public TripPattern findPattern(Trip trip) { - TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); - if (currentSnapshot != null) { - TripPattern realtimeAddedTripPattern = currentSnapshot.getRealTimeAddedPatternForTrip(trip); + if (timetableSnapshot != null) { + TripPattern realtimeAddedTripPattern = timetableSnapshot.getRealTimeAddedPatternForTrip(trip); if (realtimeAddedTripPattern != null) { return realtimeAddedTripPattern; } @@ -344,9 +357,8 @@ public Collection findPatterns(Route route) { Collection tripPatterns = new HashSet<>( timetableRepositoryIndex.getPatternsForRoute(route) ); - TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); - if (currentSnapshot != null) { - Collection realTimeAddedPatternForRoute = currentSnapshot.getRealTimeAddedPatternForRoute( + if (timetableSnapshot != null) { + Collection realTimeAddedPatternForRoute = timetableSnapshot.getRealTimeAddedPatternForRoute( route ); tripPatterns.addAll(realTimeAddedPatternForRoute); @@ -469,9 +481,8 @@ public Collection findPatterns(StopLocation stop, boolean includeRe Set tripPatterns = new HashSet<>(findPatterns(stop)); if (includeRealtimeUpdates) { - TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); - if (currentSnapshot != null) { - tripPatterns.addAll(currentSnapshot.getPatternsForStop(stop)); + if (timetableSnapshot != null) { + tripPatterns.addAll(timetableSnapshot.getPatternsForStop(stop)); } } return tripPatterns; @@ -502,48 +513,31 @@ public GroupOfRoutes getGroupOfRoutes(FeedScopedId id) { @Override public Timetable findTimetable(TripPattern tripPattern, LocalDate serviceDate) { OTPRequestTimeoutException.checkForTimeout(); - TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); - return currentSnapshot != null - ? currentSnapshot.resolve(tripPattern, serviceDate) + return timetableSnapshot != null + ? timetableSnapshot.resolve(tripPattern, serviceDate) : tripPattern.getScheduledTimetable(); } @Override public TripPattern findNewTripPatternForModifiedTrip(FeedScopedId tripId, LocalDate serviceDate) { - TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); - if (currentSnapshot == null) { + if (timetableSnapshot == null) { return null; } - return currentSnapshot.getNewTripPatternForModifiedTrip(tripId, serviceDate); + return timetableSnapshot.getNewTripPatternForModifiedTrip(tripId, serviceDate); } @Override public boolean hasNewTripPatternsForModifiedTrips() { - TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); - if (currentSnapshot == null) { + if (timetableSnapshot == null) { return false; } - return currentSnapshot.hasNewTripPatternsForModifiedTrips(); - } - - /** - * Lazy-initialization of TimetableSnapshot - * - * @return The same TimetableSnapshot is returned throughout the lifecycle of this object. - */ - @Nullable - private TimetableSnapshot lazyGetTimeTableSnapShot() { - if (this.timetableSnapshot == null) { - timetableSnapshot = timetableRepository.getTimetableSnapshot(); - } - return this.timetableSnapshot; + return timetableSnapshot.hasNewTripPatternsForModifiedTrips(); } @Override public TripOnServiceDate getTripOnServiceDate(FeedScopedId id) { - TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); - if (currentSnapshot != null) { - TripOnServiceDate tripOnServiceDate = currentSnapshot.getRealTimeAddedTripOnServiceDateById( + if (timetableSnapshot != null) { + TripOnServiceDate tripOnServiceDate = timetableSnapshot.getRealTimeAddedTripOnServiceDateById( id ); if (tripOnServiceDate != null) { @@ -555,11 +549,10 @@ public TripOnServiceDate getTripOnServiceDate(FeedScopedId id) { @Override public Collection listTripsOnServiceDate() { - TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); - if (currentSnapshot != null) { + if (timetableSnapshot != null) { return new CollectionsView<>( timetableRepository.getAllTripsOnServiceDates(), - currentSnapshot.listRealTimeAddedTripOnServiceDate() + timetableSnapshot.listRealTimeAddedTripOnServiceDate() ); } return timetableRepository.getAllTripsOnServiceDates(); @@ -567,9 +560,8 @@ public Collection listTripsOnServiceDate() { @Override public TripOnServiceDate getTripOnServiceDate(TripIdAndServiceDate tripIdAndServiceDate) { - TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); - if (currentSnapshot != null) { - TripOnServiceDate tripOnServiceDate = currentSnapshot.getRealTimeAddedTripOnServiceDateForTripAndDay( + if (timetableSnapshot != null) { + TripOnServiceDate tripOnServiceDate = timetableSnapshot.getRealTimeAddedTripOnServiceDateForTripAndDay( tripIdAndServiceDate ); if (tripOnServiceDate != null) { @@ -593,9 +585,8 @@ public List findTripsOnServiceDate(TripOnServiceDateRequest r @Override public boolean containsTrip(FeedScopedId id) { - TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); - if (currentSnapshot != null) { - Trip trip = currentSnapshot.getRealTimeAddedTrip(id); + if (timetableSnapshot != null) { + Trip trip = timetableSnapshot.getRealTimeAddedTrip(id); if (trip != null) { return true; } @@ -654,21 +645,6 @@ public TransitLayer getRealtimeTransitLayer() { return this.timetableRepository.getRealtimeTransitLayer(); } - @Override - public void setTransitLayer(TransitLayer transitLayer) { - this.timetableRepository.setTransitLayer(transitLayer); - } - - @Override - public void setRealtimeTransitLayer(TransitLayer realtimeTransitLayer) { - timetableRepository.setRealtimeTransitLayer(realtimeTransitLayer); - } - - @Override - public boolean hasRealtimeTransitLayer() { - return timetableRepository.hasRealtimeTransitLayer(); - } - @Override public CalendarService getCalendarService() { return this.timetableRepository.getCalendarService(); diff --git a/application/src/main/java/org/opentripplanner/transit/service/TimetableRepository.java b/application/src/main/java/org/opentripplanner/transit/service/TimetableRepository.java index ff8607f3818..9dea6323f7b 100644 --- a/application/src/main/java/org/opentripplanner/transit/service/TimetableRepository.java +++ b/application/src/main/java/org/opentripplanner/transit/service/TimetableRepository.java @@ -4,6 +4,7 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; +import gnu.trove.set.TIntSet; import gnu.trove.set.hash.TIntHashSet; import jakarta.inject.Inject; import java.io.Serializable; @@ -28,14 +29,11 @@ import org.opentripplanner.graph_builder.issues.NoFutureDates; import org.opentripplanner.model.FeedInfo; import org.opentripplanner.model.PathTransfer; -import org.opentripplanner.model.TimetableSnapshot; -import org.opentripplanner.model.TimetableSnapshotProvider; import org.opentripplanner.model.calendar.CalendarService; import org.opentripplanner.model.calendar.CalendarServiceData; import org.opentripplanner.model.calendar.impl.CalendarServiceImpl; import org.opentripplanner.model.transfer.DefaultTransferService; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.impl.DelegatingTransitAlertServiceImpl; import org.opentripplanner.routing.services.TransitAlertService; @@ -79,10 +77,6 @@ * At this point the TimetableRepository is not often read directly. Many requests will look at the * TransitLayer rather than the TimetableRepository it's derived from. Both are often accessed via the * TransitService rather than directly reading the fields of TimetableRepository or TransitLayer. - * - * TODO RT_AB: consider renaming. By some definitions this is not really the model, but a top-level - * object grouping together instances of model classes with things that operate on and map those - * instances. */ public class TimetableRepository implements Serializable { @@ -112,15 +106,6 @@ public class TimetableRepository implements Serializable { */ private transient TransitLayer transitLayer; - /** - * This updater applies realtime changes queued up for the next TimetableSnapshot such that - * this TimetableRepository.realtimeSnapshot remains aligned with the service represented in - * (this TimetableRepository instance + that next TimetableSnapshot). This is a way of keeping the - * TransitLayer up to date without repeatedly deriving it from scratch every few seconds. The - * same incremental changes are applied to both sets of data and they are published together. - */ - private transient TransitLayerUpdater transitLayerUpdater; - /** * An optionally present second TransitLayer representing the contents of this TimetableRepository plus * the results of realtime updates in the latest TimetableSnapshot. @@ -132,7 +117,6 @@ public class TimetableRepository implements Serializable { private final CalendarServiceData calendarServiceData = new CalendarServiceData(); private transient TimetableRepositoryIndex index; - private transient TimetableSnapshotProvider timetableSnapshotProvider = null; private ZoneId timeZone = null; private boolean timeZoneExplicitlySet = false; @@ -174,24 +158,6 @@ public void index() { } } - @Nullable - public TimetableSnapshot getTimetableSnapshot() { - return timetableSnapshotProvider == null - ? null - : timetableSnapshotProvider.getTimetableSnapshot(); - } - - public void initTimetableSnapshotProvider(TimetableSnapshotProvider timetableSnapshotProvider) { - if (this.timetableSnapshotProvider != null) { - throw new IllegalArgumentException( - "We support only one timetableSnapshotSource, there are two implementation; one for " + - "GTFS and one for Netex/Siri. They need to be refactored to work together. This cast " + - "will fail if updaters try setup both." - ); - } - this.timetableSnapshotProvider = timetableSnapshotProvider; - } - /** Data model for Raptor routing, with realtime updates applied (if any). */ public TransitLayer getTransitLayer() { return transitLayer; @@ -202,14 +168,24 @@ public void setTransitLayer(TransitLayer transitLayer) { } /** Data model for Raptor routing, with realtime updates applied (if any). */ + @Nullable public TransitLayer getRealtimeTransitLayer() { return realtimeTransitLayer.get(); } + /** + * Publish the latest snapshot of the real-time transit layer. + * Should be called only when creating a new TransitLayer, from the graph writer thread. + */ public void setRealtimeTransitLayer(TransitLayer realtimeTransitLayer) { this.realtimeTransitLayer.publish(realtimeTransitLayer); } + /** + * Return true if a real-time transit layer is present. + * The real-time transit layer is optional, + * it is present only when real-time updaters are configured. + */ public boolean hasRealtimeTransitLayer() { return realtimeTransitLayer != null; } @@ -218,7 +194,9 @@ public DefaultTransferService getTransferService() { return transferService; } - // Check to see if we have transit information for a given date + /** + * Returns true if this repository contains any transit data at the given instant. + */ public boolean transitFeedCovers(Instant time) { return ( !time.isBefore(this.transitServiceStarts.toInstant()) && @@ -234,7 +212,7 @@ public void addTransitMode(TransitMode mode) { transitModes.add(mode); } - /** List of transit modes that are availible in GTFS data used in this graph **/ + /** List of transit modes that are available in GTFS data used in this graph **/ public HashSet getTransitModes() { return transitModes; } @@ -258,11 +236,11 @@ public void updateCalendarServiceData( /** * Get or create a serviceId for a given date. This method is used when a new trip is added from a - * realtime data update. It make sure the date is in the existing transit service period. + * realtime data update. It makes sure the date is in the existing transit service period. *

* * @param serviceDate service date for the added service id - * @return service-id for date if it exist or is created. If the given service date is outside the + * @return service-id for date if it exists or is created. If the given service date is outside the * service period {@code null} is returned. */ @Nullable @@ -479,10 +457,6 @@ public GraphUpdaterManager getUpdaterManager() { return updaterManager; } - public TransitLayerUpdater getTransitLayerUpdater() { - return transitLayerUpdater; - } - public Deduplicator getDeduplicator() { return deduplicator; } @@ -508,10 +482,6 @@ private void updateHasTransit(boolean hasTransit) { this.hasTransit = this.hasTransit || hasTransit; } - public void setTransitLayerUpdater(TransitLayerUpdater transitLayerUpdater) { - this.transitLayerUpdater = transitLayerUpdater; - } - /** * Updating the site repository is only allowed during graph build */ @@ -566,6 +536,13 @@ TimetableRepositoryIndex getTimetableRepositoryIndex() { return index; } + /** + * For all dates in the system get the service codes that run on it. + */ + public Map getServiceCodesRunningForDate() { + return Collections.unmodifiableMap(index.getServiceCodesRunningForDate()); + } + public boolean isIndexed() { return index != null; } diff --git a/application/src/main/java/org/opentripplanner/transit/service/TransitEditorService.java b/application/src/main/java/org/opentripplanner/transit/service/TransitEditorService.java index 411ab3d652b..2d076d328ac 100644 --- a/application/src/main/java/org/opentripplanner/transit/service/TransitEditorService.java +++ b/application/src/main/java/org/opentripplanner/transit/service/TransitEditorService.java @@ -32,23 +32,4 @@ public interface TransitEditorService extends TransitService { */ @Nullable Trip getScheduledTrip(FeedScopedId id); - - /** - * Set the original, immutable, transit layer, - * based on scheduled data (not real-time data). - */ - void setTransitLayer(TransitLayer transitLayer); - - /** - * Return true if a real-time transit layer is present. - * The real-time transit layer is optional, - * it is present only when real-time updaters are configured. - */ - boolean hasRealtimeTransitLayer(); - - /** - * Publish the latest snapshot of the real-time transit layer. - * Should be called only when creating a new TransitLayer, from the graph writer thread. - */ - void setRealtimeTransitLayer(TransitLayer realtimeTransitLayer); } diff --git a/application/src/main/java/org/opentripplanner/transit/service/TransitService.java b/application/src/main/java/org/opentripplanner/transit/service/TransitService.java index 6e005b355d1..00cfcc6673d 100644 --- a/application/src/main/java/org/opentripplanner/transit/service/TransitService.java +++ b/application/src/main/java/org/opentripplanner/transit/service/TransitService.java @@ -25,6 +25,7 @@ import org.opentripplanner.routing.services.TransitAlertService; import org.opentripplanner.routing.stoptimes.ArrivalDeparture; import org.opentripplanner.transit.api.request.FindRegularStopsByBoundingBoxRequest; +import org.opentripplanner.transit.api.request.FindRoutesRequest; import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; import org.opentripplanner.transit.api.request.TripRequest; import org.opentripplanner.transit.model.basic.Notice; @@ -105,6 +106,11 @@ public interface TransitService { */ Route getRoute(FeedScopedId id); + /** + * Return all routes for a given set of ids, including routes created by real-time updates. + */ + Collection getRoutes(Collection ids); + /** * Return the routes using the given stop, not including real-time updates. */ @@ -334,4 +340,9 @@ List findTripTimeOnDate( Collection findRegularStopsByBoundingBox( FindRegularStopsByBoundingBoxRequest request ); + + /** + * Returns a list of {@link Route}s that match the filtering defined in the request. + */ + Collection findRoutes(FindRoutesRequest request); } diff --git a/application/src/main/java/org/opentripplanner/updater/TimetableSnapshotSourceParameters.java b/application/src/main/java/org/opentripplanner/updater/TimetableSnapshotSourceParameters.java index 8b9882ae3aa..4a0654fe61e 100644 --- a/application/src/main/java/org/opentripplanner/updater/TimetableSnapshotSourceParameters.java +++ b/application/src/main/java/org/opentripplanner/updater/TimetableSnapshotSourceParameters.java @@ -15,6 +15,11 @@ public record TimetableSnapshotSourceParameters( true ); + public static final TimetableSnapshotSourceParameters PUBLISH_IMMEDIATELY = new TimetableSnapshotSourceParameters( + Duration.ZERO, + false + ); + /* Factory functions, used instead of a builder - useful in tests. */ public TimetableSnapshotSourceParameters withMaxSnapshotFrequency(Duration maxSnapshotFrequency) { diff --git a/application/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java b/application/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java index 11be185fa2a..8ddb2883c7d 100644 --- a/application/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java +++ b/application/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java @@ -1,5 +1,6 @@ package org.opentripplanner.updater.configure; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -29,6 +30,7 @@ import org.opentripplanner.updater.spi.TimetableSnapshotFlush; import org.opentripplanner.updater.trip.MqttGtfsRealtimeUpdater; import org.opentripplanner.updater.trip.PollingTripUpdater; +import org.opentripplanner.updater.trip.TimetableSnapshotManager; import org.opentripplanner.updater.trip.TimetableSnapshotSource; import org.opentripplanner.updater.vehicle_parking.AvailabilityDatasourceFactory; import org.opentripplanner.updater.vehicle_parking.VehicleParkingAvailabilityUpdater; @@ -53,8 +55,7 @@ public class UpdaterConfigurator { private final RealtimeVehicleRepository realtimeVehicleRepository; private final VehicleRentalRepository vehicleRentalRepository; private final VehicleParkingRepository parkingRepository; - private SiriTimetableSnapshotSource siriTimetableSnapshotSource = null; - private TimetableSnapshotSource gtfsTimetableSnapshotSource = null; + private final TimetableSnapshotManager snapshotManager; private UpdaterConfigurator( Graph graph, @@ -62,6 +63,7 @@ private UpdaterConfigurator( VehicleRentalRepository vehicleRentalRepository, VehicleParkingRepository parkingRepository, TimetableRepository timetableRepository, + TimetableSnapshotManager snapshotManager, UpdatersParameters updatersParameters ) { this.graph = graph; @@ -70,6 +72,7 @@ private UpdaterConfigurator( this.timetableRepository = timetableRepository; this.updatersParameters = updatersParameters; this.parkingRepository = parkingRepository; + this.snapshotManager = snapshotManager; } public static void configure( @@ -78,6 +81,7 @@ public static void configure( VehicleRentalRepository vehicleRentalRepository, VehicleParkingRepository parkingRepository, TimetableRepository timetableRepository, + TimetableSnapshotManager snapshotManager, UpdatersParameters updatersParameters ) { new UpdaterConfigurator( @@ -86,6 +90,7 @@ public static void configure( vehicleRentalRepository, parkingRepository, timetableRepository, + snapshotManager, updatersParameters ) .configure(); @@ -103,18 +108,13 @@ private void configure() { ) ); - TimetableSnapshot timetableSnapshotBuffer = null; - if (siriTimetableSnapshotSource != null) { - timetableSnapshotBuffer = siriTimetableSnapshotSource.getTimetableSnapshotBuffer(); - } else if (gtfsTimetableSnapshotSource != null) { - timetableSnapshotBuffer = gtfsTimetableSnapshotSource.getTimetableSnapshotBuffer(); - } + TimetableSnapshot timetableSnapshotBuffer = snapshotManager.getTimetableSnapshotBuffer(); GraphUpdaterManager updaterManager = new GraphUpdaterManager( new DefaultRealTimeUpdateContext(graph, timetableRepository, timetableSnapshotBuffer), updaters ); - configureTimetableSnapshotFlush(updaterManager); + configureTimetableSnapshotFlush(updaterManager, snapshotManager); updaterManager.startUpdaters(); @@ -235,42 +235,32 @@ private List createUpdatersFromConfig() { } private SiriTimetableSnapshotSource provideSiriTimetableSnapshot() { - if (siriTimetableSnapshotSource == null) { - this.siriTimetableSnapshotSource = - new SiriTimetableSnapshotSource( - updatersParameters.timetableSnapshotParameters(), - timetableRepository - ); - } - - return siriTimetableSnapshotSource; + return new SiriTimetableSnapshotSource(timetableRepository, snapshotManager); } private TimetableSnapshotSource provideGtfsTimetableSnapshot() { - if (gtfsTimetableSnapshotSource == null) { - this.gtfsTimetableSnapshotSource = - new TimetableSnapshotSource( - updatersParameters.timetableSnapshotParameters(), - timetableRepository - ); - } - return gtfsTimetableSnapshotSource; + return new TimetableSnapshotSource( + timetableRepository, + snapshotManager, + () -> LocalDate.now(timetableRepository.getTimeZone()) + ); } /** * If SIRI or GTFS real-time updaters are in use, configure a periodic flush of the timetable * snapshot. */ - private void configureTimetableSnapshotFlush(GraphUpdaterManager updaterManager) { - if (siriTimetableSnapshotSource != null || gtfsTimetableSnapshotSource != null) { - updaterManager - .getScheduler() - .scheduleWithFixedDelay( - new TimetableSnapshotFlush(siriTimetableSnapshotSource, gtfsTimetableSnapshotSource), - 0, - updatersParameters.timetableSnapshotParameters().maxSnapshotFrequency().toSeconds(), - TimeUnit.SECONDS - ); - } + private void configureTimetableSnapshotFlush( + GraphUpdaterManager updaterManager, + TimetableSnapshotManager snapshotManager + ) { + updaterManager + .getScheduler() + .scheduleWithFixedDelay( + new TimetableSnapshotFlush(snapshotManager), + 0, + updatersParameters.timetableSnapshotParameters().maxSnapshotFrequency().toSeconds(), + TimeUnit.SECONDS + ); } } diff --git a/application/src/main/java/org/opentripplanner/updater/siri/SiriTimetableSnapshotSource.java b/application/src/main/java/org/opentripplanner/updater/siri/SiriTimetableSnapshotSource.java index 73e4c711269..2fa4a5e0209 100644 --- a/application/src/main/java/org/opentripplanner/updater/siri/SiriTimetableSnapshotSource.java +++ b/application/src/main/java/org/opentripplanner/updater/siri/SiriTimetableSnapshotSource.java @@ -16,8 +16,6 @@ import javax.annotation.Nullable; import org.opentripplanner.model.RealTimeTripUpdate; import org.opentripplanner.model.Timetable; -import org.opentripplanner.model.TimetableSnapshot; -import org.opentripplanner.model.TimetableSnapshotProvider; import org.opentripplanner.transit.model.framework.DataValidationException; import org.opentripplanner.transit.model.framework.Result; import org.opentripplanner.transit.model.network.TripPattern; @@ -27,7 +25,6 @@ import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.transit.service.TransitEditorService; -import org.opentripplanner.updater.TimetableSnapshotSourceParameters; import org.opentripplanner.updater.spi.DataValidationExceptionMapper; import org.opentripplanner.updater.spi.UpdateError; import org.opentripplanner.updater.spi.UpdateResult; @@ -45,12 +42,12 @@ * necessary to provide planning threads a consistent constant view of a graph with real-time data at * a specific point in time. */ -public class SiriTimetableSnapshotSource implements TimetableSnapshotProvider { +public class SiriTimetableSnapshotSource { private static final Logger LOG = LoggerFactory.getLogger(SiriTimetableSnapshotSource.class); /** - * Use a id generator to generate TripPattern ids for new TripPatterns created by RealTime + * Use an id generator to generate TripPattern ids for new TripPatterns created by RealTime * updates. */ private final SiriTripPatternIdGenerator tripPatternIdGenerator = new SiriTripPatternIdGenerator(); @@ -70,27 +67,18 @@ public class SiriTimetableSnapshotSource implements TimetableSnapshotProvider { private final TimetableSnapshotManager snapshotManager; public SiriTimetableSnapshotSource( - TimetableSnapshotSourceParameters parameters, - TimetableRepository timetableRepository + TimetableRepository timetableRepository, + TimetableSnapshotManager snapshotManager ) { - this.snapshotManager = - new TimetableSnapshotManager( - timetableRepository.getTransitLayerUpdater(), - parameters, - () -> LocalDate.now(timetableRepository.getTimeZone()) - ); + this.snapshotManager = snapshotManager; this.transitEditorService = - new DefaultTransitService(timetableRepository, getTimetableSnapshotBuffer()); + new DefaultTransitService(timetableRepository, snapshotManager.getTimetableSnapshotBuffer()); this.tripPatternCache = new SiriTripPatternCache(tripPatternIdGenerator, transitEditorService::findPattern); - - timetableRepository.initTimetableSnapshotProvider(this); } /** - * Method to apply a trip update list to the most recent version of the timetable snapshot. - * FIXME RT_AB: TripUpdate is the GTFS term, and these SIRI ETs are never converted into that - * same internal model. + * Method to apply estimated timetables to the most recent version of the timetable snapshot. * * @param incrementality the incrementality of the update, for example if updates represent all * updates that are active right now, i.e. all previous updates should be @@ -131,21 +119,6 @@ public UpdateResult applyEstimatedTimetable( return UpdateResult.ofResults(results); } - @Override - public TimetableSnapshot getTimetableSnapshot() { - return snapshotManager.getTimetableSnapshot(); - } - - /** - * @return the current timetable snapshot buffer that contains pending changes (not yet published - * in a snapshot). - * This should be used in the context of an updater to build a TransitEditorService that sees all - * the changes applied so far by real-time updates. - */ - public TimetableSnapshot getTimetableSnapshotBuffer() { - return snapshotManager.getTimetableSnapshotBuffer(); - } - private Result apply( EstimatedVehicleJourney journey, TransitEditorService transitService, @@ -215,7 +188,7 @@ private boolean shouldAddNewTrip( * Snapshot timetable is used as source if initialised, trip patterns scheduled timetable if not. */ private Timetable getCurrentTimetable(TripPattern tripPattern, LocalDate serviceDate) { - return getTimetableSnapshotBuffer().resolve(tripPattern, serviceDate); + return snapshotManager.getTimetableSnapshotBuffer().resolve(tripPattern, serviceDate); } private Result handleModifiedTrip( @@ -358,11 +331,4 @@ private boolean markScheduledTripAsDeleted(Trip trip, final LocalDate serviceDat return success; } - - /** - * Flush pending changes in the timetable snapshot buffer and publish a new snapshot. - */ - public void flushBuffer() { - snapshotManager.purgeAndCommit(); - } } diff --git a/application/src/main/java/org/opentripplanner/updater/spi/TimetableSnapshotFlush.java b/application/src/main/java/org/opentripplanner/updater/spi/TimetableSnapshotFlush.java index 89dbeb23302..1d802ff0e9e 100644 --- a/application/src/main/java/org/opentripplanner/updater/spi/TimetableSnapshotFlush.java +++ b/application/src/main/java/org/opentripplanner/updater/spi/TimetableSnapshotFlush.java @@ -1,7 +1,6 @@ package org.opentripplanner.updater.spi; -import org.opentripplanner.updater.siri.SiriTimetableSnapshotSource; -import org.opentripplanner.updater.trip.TimetableSnapshotSource; +import org.opentripplanner.updater.trip.TimetableSnapshotManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,27 +13,17 @@ public class TimetableSnapshotFlush implements Runnable { private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshotFlush.class); - private final SiriTimetableSnapshotSource siriTimetableSnapshotSource; - private final TimetableSnapshotSource gtfsTimetableSnapshotSource; + private final TimetableSnapshotManager snapshotManager; - public TimetableSnapshotFlush( - SiriTimetableSnapshotSource siriTimetableSnapshotSource, - TimetableSnapshotSource gtfsTimetableSnapshotSource - ) { - this.siriTimetableSnapshotSource = siriTimetableSnapshotSource; - this.gtfsTimetableSnapshotSource = gtfsTimetableSnapshotSource; + public TimetableSnapshotFlush(TimetableSnapshotManager snapshotManager) { + this.snapshotManager = snapshotManager; } @Override public void run() { try { LOG.debug("Flushing timetable snapshot buffer"); - if (siriTimetableSnapshotSource != null) { - siriTimetableSnapshotSource.flushBuffer(); - } - if (gtfsTimetableSnapshotSource != null) { - gtfsTimetableSnapshotSource.flushBuffer(); - } + snapshotManager.purgeAndCommit(); LOG.debug("Flushed timetable snapshot buffer"); } catch (Throwable t) { LOG.error("Error flushing timetable snapshot buffer", t); diff --git a/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java b/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java index e95afe399ab..b0b49937810 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java @@ -20,10 +20,6 @@ /** * A class which abstracts away locking, updating, committing and purging of the timetable snapshot. - * In order to keep code reviews easier this is an intermediate stage and will be refactored further. - * In particular the following refactorings are planned: - *

- * - create only one "snapshot manager" per transit model that is shared between Siri/GTFS-RT updaters */ public final class TimetableSnapshotManager { @@ -61,7 +57,7 @@ public final class TimetableSnapshotManager { * considered 'today'. This is useful for unit testing. */ public TimetableSnapshotManager( - TransitLayerUpdater transitLayerUpdater, + @Nullable TransitLayerUpdater transitLayerUpdater, TimetableSnapshotSourceParameters parameters, Supplier localDateNow ) { @@ -73,8 +69,8 @@ public TimetableSnapshotManager( } /** - * @return an up-to-date snapshot mapping TripPatterns to Timetables. This snapshot and the - * timetable objects it references are guaranteed to never change, so the requesting thread is + * @return an up-to-date snapshot of real-time data. This snapshot and the timetable objects it + * references are guaranteed to never change, so the requesting thread is * provided a consistent view of all TripTimes. The routing thread need only release its reference * to the snapshot to release resources. */ diff --git a/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java b/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java index e3ec690237e..54518b0e28d 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java @@ -43,7 +43,6 @@ import org.opentripplanner.model.StopTime; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TimetableSnapshot; -import org.opentripplanner.model.TimetableSnapshotProvider; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.DataValidationException; import org.opentripplanner.transit.model.framework.Deduplicator; @@ -63,7 +62,6 @@ import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.updater.GtfsRealtimeFuzzyTripMatcher; import org.opentripplanner.updater.GtfsRealtimeMapper; -import org.opentripplanner.updater.TimetableSnapshotSourceParameters; import org.opentripplanner.updater.spi.DataValidationExceptionMapper; import org.opentripplanner.updater.spi.ResultLogger; import org.opentripplanner.updater.spi.UpdateError; @@ -80,7 +78,7 @@ * necessary to provide planning threads a consistent constant view of a graph with realtime data at * a specific point in time. */ -public class TimetableSnapshotSource implements TimetableSnapshotProvider { +public class TimetableSnapshotSource { private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshotSource.class); @@ -108,37 +106,22 @@ public class TimetableSnapshotSource implements TimetableSnapshotProvider { private final TimetableSnapshotManager snapshotManager; private final Supplier localDateNow; - public TimetableSnapshotSource( - TimetableSnapshotSourceParameters parameters, - TimetableRepository timetableRepository - ) { - this(parameters, timetableRepository, () -> LocalDate.now(timetableRepository.getTimeZone())); - } - /** * Constructor is package local to allow unit-tests to provide their own clock, not using system * time. */ - TimetableSnapshotSource( - TimetableSnapshotSourceParameters parameters, + public TimetableSnapshotSource( TimetableRepository timetableRepository, + TimetableSnapshotManager snapshotManager, Supplier localDateNow ) { - this.snapshotManager = - new TimetableSnapshotManager( - timetableRepository.getTransitLayerUpdater(), - parameters, - localDateNow - ); + this.snapshotManager = snapshotManager; this.timeZone = timetableRepository.getTimeZone(); + this.localDateNow = localDateNow; this.transitEditorService = new DefaultTransitService(timetableRepository, snapshotManager.getTimetableSnapshotBuffer()); this.deduplicator = timetableRepository.getDeduplicator(); this.serviceCodes = timetableRepository.getServiceCodes(); - this.localDateNow = localDateNow; - - // Inject this into the transit model - timetableRepository.initTimetableSnapshotProvider(this); } /** @@ -348,20 +331,6 @@ private boolean isPreviouslyAddedTrip( return tripTimes.getRealTimeState() == RealTimeState.ADDED; } - @Override - public TimetableSnapshot getTimetableSnapshot() { - return snapshotManager.getTimetableSnapshot(); - } - - /** - * @return the current timetable snapshot buffer that contains pending changes (not yet published - * in a snapshot). This should be used in the context of an updater to build a TransitEditorService - * that sees all the changes applied so far by real-time updates. - */ - public TimetableSnapshot getTimetableSnapshotBuffer() { - return snapshotManager.getTimetableSnapshotBuffer(); - } - private static void logUpdateResult( String feedId, Map failuresByRelationship, @@ -1241,8 +1210,4 @@ private enum CancelationType { CANCEL, DELETE, } - - public void flushBuffer() { - snapshotManager.purgeAndCommit(); - } } diff --git a/application/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapper.java b/application/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapper.java index 959521e017e..7c242e85bbe 100644 --- a/application/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapper.java +++ b/application/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapper.java @@ -9,14 +9,23 @@ import org.mobilitydata.gbfs.v2_3.free_bike_status.GBFSBike; import org.mobilitydata.gbfs.v2_3.free_bike_status.GBFSRentalUris; import org.opentripplanner.framework.i18n.NonLocalizedString; +import org.opentripplanner.service.vehiclerental.model.RentalVehicleFuel; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris; import org.opentripplanner.service.vehiclerental.model.VehicleRentalSystem; import org.opentripplanner.service.vehiclerental.model.VehicleRentalVehicle; +import org.opentripplanner.transit.model.basic.Distance; +import org.opentripplanner.transit.model.basic.Ratio; import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.utils.logging.Throttle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class GbfsFreeVehicleStatusMapper { + private static final Logger LOG = LoggerFactory.getLogger(GbfsFreeVehicleStatusMapper.class); + private static final Throttle LOG_THROTTLE = Throttle.ofOneMinute(); + private final VehicleRentalSystem system; private final Map vehicleTypes; @@ -52,7 +61,41 @@ public VehicleRentalVehicle mapFreeVehicleStatus(GBFSBike vehicle) { vehicle.getLastReported() != null ? Instant.ofEpochSecond((long) (double) vehicle.getLastReported()) : null; - rentalVehicle.currentRangeMeters = vehicle.getCurrentRangeMeters(); + + var fuelRatio = Ratio + .ofBoxed( + vehicle.getCurrentFuelPercent(), + validationErrorMessage -> + LOG_THROTTLE.throttle(() -> + LOG.warn("'currentFuelPercent' is not valid. Details: {}", validationErrorMessage) + ) + ) + .orElse(null); + var rangeMeters = Distance + .ofMetersBoxed( + vehicle.getCurrentRangeMeters(), + error -> { + LOG_THROTTLE.throttle(() -> + LOG.warn( + "Current range meter value not valid: {} - {}", + vehicle.getCurrentRangeMeters(), + error + ) + ); + } + ) + .orElse(null); + // if the propulsion type has an engine current_range_meters is required + if ( + vehicle.getVehicleTypeId() != null && + vehicleTypes.get(vehicle.getVehicleTypeId()) != null && + vehicleTypes.get(vehicle.getVehicleTypeId()).propulsionType != + RentalVehicleType.PropulsionType.HUMAN && + rangeMeters == null + ) { + return null; + } + rentalVehicle.fuel = new RentalVehicleFuel(fuelRatio, rangeMeters); rentalVehicle.pricingPlanId = vehicle.getPricingPlanId(); GBFSRentalUris rentalUris = vehicle.getRentalUris(); if (rentalUris != null) { diff --git a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index ce808e546d1..183ad23d43d 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -1889,6 +1889,8 @@ type RealTimeEstimate { type RentalVehicle implements Node & PlaceInterface { "If true, vehicle is currently available for renting." allowPickupNow: Boolean + "Fuel or battery status of the rental vehicle" + fuel: RentalVehicleFuel "Global object ID provided by Relay. This value can be used to refetch this object using **node** query." id: ID! "Latitude of the vehicle (WGS 84)" @@ -1918,6 +1920,14 @@ type RentalVehicleEntityCounts { total: Int! } +"Rental vehicle fuel represent the current status of the battery or fuel of a rental vehicle" +type RentalVehicleFuel { + "Fuel or battery power remaining in the vehicle. Expressed from 0 to 1." + percent: Ratio + "Range in meters that the vehicle can travel with the current charge or fuel." + range: Int +} + type RentalVehicleType { "The vehicle's general form factor" formFactor: FormFactor @@ -3984,6 +3994,8 @@ input CarParkingPreferencesInput { "Preferences related to traveling on a car (excluding car travel on transit services such as taxi)." input CarPreferencesInput { + "Cost of boarding a vehicle with a car." + boardCost: Cost "Car parking related preferences." parking: CarParkingPreferencesInput "A multiplier for how bad travelling on car is compared to being in transit for equal lengths of time." diff --git a/application/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/application/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index 6834d375bf1..611f6e38156 100644 --- a/application/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/application/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -651,16 +651,21 @@ type QueryType { leg(id: ID!): Leg @timingData "Get a single line based on its id" line(id: ID!): Line @timingData - "Get all lines" + "Get all _lines_" lines( - "Set of ids of authorities to fetch lines for." + "Set of ids of _authorities_ to fetch _lines_ for." authorities: [String], - "Filter by lines containing flexible / on demand serviceJourneys only." + "Filter by _lines_ containing flexible / on demand _service journey_ only." flexibleOnly: Boolean = false, + "Set of ids of _lines_ to fetch. If this is set, no other filters can be set." ids: [ID], + "Prefix of the _name_ of the _line_ to fetch. This filter is case insensitive." name: String, + "_Public code_ of the _line_ to fetch." publicCode: String, + "Set of _public codes_ to fetch _lines_ for." publicCodes: [String], + "Set of _transport modes_ to fetch _lines_ for." transportModes: [TransportMode] ): [Line]! @timingData "Get all places (quays, stop places, car parks etc. with coordinates) within the specified radius from a location. The returned type has two fields place and distance. The search is done by walking so the distance is according to the network of walkables." @@ -1719,6 +1724,9 @@ enum RelativeDirection { continue depart elevator + enterStation + exitStation + followSigns hardLeft hardRight left diff --git a/application/src/test/java/org/opentripplanner/GtfsTest.java b/application/src/test/java/org/opentripplanner/GtfsTest.java index 05b7bfbf4f6..d7e9a6e7e7e 100644 --- a/application/src/test/java/org/opentripplanner/GtfsTest.java +++ b/application/src/test/java/org/opentripplanner/GtfsTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.opentripplanner.routing.api.request.StreetMode.NOT_SET; import static org.opentripplanner.routing.api.request.StreetMode.WALK; +import static org.opentripplanner.standalone.configure.ConstructApplication.createTransitLayerForRaptor; import static org.opentripplanner.updater.trip.BackwardsDelayPropagationType.REQUIRED_NO_DATA; import com.google.transit.realtime.GtfsRealtime.FeedEntity; @@ -13,9 +14,11 @@ import com.google.transit.realtime.GtfsRealtime.TripUpdate; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.InputStream; import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -27,6 +30,7 @@ import org.opentripplanner.model.calendar.ServiceDateInterval; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.model.plan.Leg; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; import org.opentripplanner.routing.api.request.RequestModes; import org.opentripplanner.routing.api.request.RequestModesBuilder; import org.opentripplanner.routing.api.request.RouteRequest; @@ -36,6 +40,7 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.impl.TransitAlertServiceImpl; import org.opentripplanner.standalone.api.OtpServerRequestContext; +import org.opentripplanner.standalone.config.RouterConfig; import org.opentripplanner.transit.model.basic.MainAndSubMode; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.Deduplicator; @@ -44,6 +49,7 @@ import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.updater.TimetableSnapshotSourceParameters; import org.opentripplanner.updater.alert.AlertsUpdateHandler; +import org.opentripplanner.updater.trip.TimetableSnapshotManager; import org.opentripplanner.updater.trip.TimetableSnapshotSource; import org.opentripplanner.updater.trip.UpdateIncrementality; @@ -207,14 +213,16 @@ protected void setUp() throws Exception { gtfsGraphBuilderImpl.buildGraph(); timetableRepository.index(); graph.index(timetableRepository.getSiteRepository()); - serverContext = TestServerContext.createServerContext(graph, timetableRepository); + + createTransitLayerForRaptor(timetableRepository, RouterConfig.DEFAULT.transitTuningConfig()); + + var snapshotManager = new TimetableSnapshotManager( + new TransitLayerUpdater(timetableRepository), + TimetableSnapshotSourceParameters.PUBLISH_IMMEDIATELY, + LocalDate::now + ); timetableSnapshotSource = - new TimetableSnapshotSource( - TimetableSnapshotSourceParameters.DEFAULT - .withPurgeExpiredData(true) - .withMaxSnapshotFrequency(Duration.ZERO), - timetableRepository - ); + new TimetableSnapshotSource(timetableRepository, snapshotManager, LocalDate::now); alertPatchServiceImpl = new TransitAlertServiceImpl(timetableRepository); alertsUpdateHandler.setTransitAlertService(alertPatchServiceImpl); alertsUpdateHandler.setFeedId(feedId.getId()); @@ -234,8 +242,9 @@ protected void setUp() throws Exception { updates, feedId.getId() ); - timetableSnapshotSource.flushBuffer(); alertsUpdateHandler.update(feedMessage, null); - } catch (Exception exception) {} + } catch (FileNotFoundException exception) {} + serverContext = + TestServerContext.createServerContext(graph, timetableRepository, snapshotManager); } } diff --git a/application/src/test/java/org/opentripplanner/TestServerContext.java b/application/src/test/java/org/opentripplanner/TestServerContext.java index ca818a64a58..9620dc982eb 100644 --- a/application/src/test/java/org/opentripplanner/TestServerContext.java +++ b/application/src/test/java/org/opentripplanner/TestServerContext.java @@ -1,8 +1,9 @@ package org.opentripplanner; -import static org.opentripplanner.standalone.configure.ConstructApplication.creatTransitLayerForRaptor; +import static org.opentripplanner.standalone.configure.ConstructApplication.createTransitLayerForRaptor; import io.micrometer.core.instrument.Metrics; +import java.time.LocalDate; import java.util.List; import org.opentripplanner.ext.emissions.DefaultEmissionsService; import org.opentripplanner.ext.emissions.EmissionsDataModel; @@ -31,19 +32,40 @@ import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.transit.service.TransitService; +import org.opentripplanner.updater.TimetableSnapshotSourceParameters; +import org.opentripplanner.updater.trip.TimetableSnapshotManager; public class TestServerContext { private TestServerContext() {} - /** Create a context for unit testing, using the default RouteRequest. */ public static OtpServerRequestContext createServerContext( Graph graph, TimetableRepository timetableRepository ) { timetableRepository.index(); - final RouterConfig routerConfig = RouterConfig.DEFAULT; - var transitService = new DefaultTransitService(timetableRepository); + createTransitLayerForRaptor(timetableRepository, RouterConfig.DEFAULT.transitTuningConfig()); + return createServerContext( + graph, + timetableRepository, + new TimetableSnapshotManager(null, TimetableSnapshotSourceParameters.DEFAULT, LocalDate::now) + ); + } + + /** Create a context for unit testing, using the default RouteRequest. */ + public static OtpServerRequestContext createServerContext( + Graph graph, + TimetableRepository timetableRepository, + TimetableSnapshotManager snapshotManager + ) { + timetableRepository.index(); + var routerConfig = RouterConfig.DEFAULT; + //createTransitLayerForRaptor(timetableRepository, routerConfig.transitTuningConfig()); + snapshotManager.purgeAndCommit(); + var transitService = new DefaultTransitService( + timetableRepository, + snapshotManager.getTimetableSnapshot() + ); DefaultServerRequestContext context = DefaultServerRequestContext.create( routerConfig.transitTuningConfig(), routerConfig.routingRequestDefaults(), @@ -52,7 +74,7 @@ public static OtpServerRequestContext createServerContext( RaptorEnvironmentFactory.create(routerConfig.transitTuningConfig().searchThreadPoolSize()) ), graph, - new DefaultTransitService(timetableRepository), + transitService, Metrics.globalRegistry, routerConfig.vectorTileConfig(), createWorldEnvelopeService(), @@ -69,7 +91,6 @@ public static OtpServerRequestContext createServerContext( null, DebugUiConfig.DEFAULT ); - creatTransitLayerForRaptor(timetableRepository, routerConfig.transitTuningConfig()); return context; } diff --git a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index 2f190502ccc..1d7f5b73abc 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -66,6 +66,7 @@ import org.opentripplanner.routing.alertpatch.TimePeriod; import org.opentripplanner.routing.alertpatch.TransitAlert; import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graphfinder.GraphFinder; import org.opentripplanner.routing.graphfinder.NearbyStop; import org.opentripplanner.routing.graphfinder.PlaceAtDistance; @@ -134,10 +135,19 @@ class GraphQLIntegrationTest { .withSystem("Network-1", "https://foo.bar") .build(); - private static final VehicleRentalVehicle RENTAL_VEHICLE = new TestFreeFloatingRentalVehicleBuilder() + private static final VehicleRentalVehicle RENTAL_VEHICLE_1 = new TestFreeFloatingRentalVehicleBuilder() .withSystem("Network-1", "https://foo.bar") .build(); + private static final VehicleRentalVehicle RENTAL_VEHICLE_2 = new TestFreeFloatingRentalVehicleBuilder() + .withSystem("Network-2", "https://foo.bar.baz") + .withNetwork("Network-2") + .withCurrentRangeMeters(null) + .withCurrentFuelPercent(null) + .build(); + + static final Graph GRAPH = new Graph(); + static final Instant ALERT_START_TIME = OffsetDateTime .parse("2023-02-15T12:03:28+01:00") .toInstant(); @@ -205,7 +215,27 @@ static void setup() { timetableRepository.addAgency(agency); timetableRepository.initTimeZone(BERLIN); + + // Create a calendar (needed for testing cancelled trips) + CalendarServiceData calendarServiceData = new CalendarServiceData(); + var firstDate = LocalDate.of(2024, 8, 8); + var secondDate = LocalDate.of(2024, 8, 9); + calendarServiceData.putServiceDatesForServiceId(cal_id, List.of(firstDate, secondDate)); + timetableRepository.getServiceCodes().put(cal_id, 0); + timetableRepository.updateCalendarServiceData( + true, + calendarServiceData, + DataImportIssueStore.NOOP + ); + timetableRepository.index(); + + TimetableSnapshot timetableSnapshot = new TimetableSnapshot(); + tripTimes2.cancelTrip(); + timetableSnapshot.update(new RealTimeTripUpdate(pattern, tripTimes2, secondDate)); + + var snapshot = timetableSnapshot.commit(); + var routes = Arrays .stream(TransitMode.values()) .sorted(Comparator.comparing(Enum::name)) @@ -221,7 +251,7 @@ static void setup() { .toList(); var busRoute = routes.stream().filter(r -> r.getMode().equals(BUS)).findFirst().get(); - TransitEditorService transitService = new DefaultTransitService(timetableRepository) { + TransitEditorService transitService = new DefaultTransitService(timetableRepository, snapshot) { private final TransitAlertService alertService = new TransitAlertServiceImpl( timetableRepository ); @@ -241,25 +271,8 @@ public Set findRoutes(StopLocation stop) { return Set.of(ROUTE); } }; - routes.forEach(transitService::addRoutes); - // Crate a calendar (needed for testing cancelled trips) - CalendarServiceData calendarServiceData = new CalendarServiceData(); - var firstDate = LocalDate.of(2024, 8, 8); - var secondDate = LocalDate.of(2024, 8, 9); - calendarServiceData.putServiceDatesForServiceId(cal_id, List.of(firstDate, secondDate)); - timetableRepository.getServiceCodes().put(cal_id, 0); - timetableRepository.updateCalendarServiceData( - true, - calendarServiceData, - DataImportIssueStore.NOOP - ); - TimetableSnapshot timetableSnapshot = new TimetableSnapshot(); - tripTimes2.cancelTrip(); - timetableSnapshot.update(new RealTimeTripUpdate(pattern, tripTimes2, secondDate)); - - var snapshot = timetableSnapshot.commit(); - timetableRepository.initTimetableSnapshotProvider(() -> snapshot); + routes.forEach(transitService::addRoutes); var step1 = walkStep("street") .withRelativeDirection(RelativeDirection.DEPART) @@ -354,7 +367,8 @@ public Set findRoutes(StopLocation stop) { DefaultVehicleRentalService defaultVehicleRentalService = new DefaultVehicleRentalService(); defaultVehicleRentalService.addVehicleRentalStation(VEHICLE_RENTAL_STATION); - defaultVehicleRentalService.addVehicleRentalStation(RENTAL_VEHICLE); + defaultVehicleRentalService.addVehicleRentalStation(RENTAL_VEHICLE_1); + defaultVehicleRentalService.addVehicleRentalStation(RENTAL_VEHICLE_2); context = new GraphQLRequestContext( @@ -521,7 +535,7 @@ public List findClosestPlaces( return List.of( new PlaceAtDistance(stop, 0), new PlaceAtDistance(VEHICLE_RENTAL_STATION, 30), - new PlaceAtDistance(RENTAL_VEHICLE, 50) + new PlaceAtDistance(RENTAL_VEHICLE_1, 50) ); } }; diff --git a/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperCarTest.java b/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperCarTest.java index 6c8109676f0..f1912ee57f2 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperCarTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperCarTest.java @@ -19,14 +19,26 @@ class RouteRequestMapperCarTest { void testBasicCarPreferences() { var carArgs = createArgsCopy(RouteRequestMapperTest.ARGS); var reluctance = 7.5; + var boardCost = Cost.costOfSeconds(500); carArgs.put( "preferences", - Map.ofEntries(entry("street", Map.ofEntries(entry("car", Map.of("reluctance", reluctance))))) + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "car", + Map.ofEntries(entry("reluctance", reluctance), entry("boardCost", boardCost)) + ) + ) + ) + ) ); var env = executionContext(carArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); var carPreferences = routeRequest.preferences().car(); assertEquals(reluctance, carPreferences.reluctance()); + assertEquals(boardCost.toSeconds(), carPreferences.boardCost()); } @Test diff --git a/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/BookingInfoMapperTest.java b/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/BookingInfoMapperTest.java new file mode 100644 index 00000000000..76af5f76d37 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/BookingInfoMapperTest.java @@ -0,0 +1,83 @@ +package org.opentripplanner.apis.transmodel.mapping; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.opentripplanner.apis.transmodel.mapping.BookingInfoMapper.mapToBookWhen; + +import java.time.Duration; +import java.time.LocalTime; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; +import org.opentripplanner.transit.model.timetable.booking.BookingTime; + +class BookingInfoMapperTest { + + private static final Duration TEN_MINUTES = Duration.ofMinutes(10); + private static final BookingTime BOOKING_TIME_ZERO_DAYS_PRIOR = new BookingTime( + LocalTime.of(10, 0), + 0 + ); + + @Test + void bookingNotice() { + assertNull(mapToBookWhen(BookingInfo.of().withMinimumBookingNotice(TEN_MINUTES).build())); + } + + @Test + void timeOfTravelOnly() { + assertEquals("timeOfTravelOnly", mapToBookWhen(BookingInfo.of().build())); + } + + @Test + void untilPreviousDay() { + var info = daysPrior(1); + assertEquals("untilPreviousDay", mapToBookWhen(info)); + } + + @Test + void advanceAndDayOfTravel() { + var info = daysPrior(0); + assertEquals("advanceAndDayOfTravel", mapToBookWhen(info)); + } + + @ParameterizedTest + @ValueSource(ints = { 2, 3, 4, 14, 28 }) + void other(int days) { + var info = daysPrior(days); + assertEquals("other", mapToBookWhen(info)); + } + + @Test + void dayOfTravelOnly() { + var info = BookingInfo.of().withEarliestBookingTime(BOOKING_TIME_ZERO_DAYS_PRIOR).build(); + assertEquals("dayOfTravelOnly", mapToBookWhen(info)); + } + + @Test + void latestBookingTime() { + var info = BookingInfo + .of() + .withEarliestBookingTime(BOOKING_TIME_ZERO_DAYS_PRIOR) + .withLatestBookingTime(BOOKING_TIME_ZERO_DAYS_PRIOR) + .build(); + assertEquals("dayOfTravelOnly", mapToBookWhen(info)); + } + + @Test + void earliestBookingTimeZero() { + var info = BookingInfo + .of() + .withEarliestBookingTime(new BookingTime(LocalTime.of(10, 0), 10)) + .build(); + assertEquals("other", mapToBookWhen(info)); + } + + private static BookingInfo daysPrior(int daysPrior) { + return BookingInfo + .of() + .withLatestBookingTime(new BookingTime(LocalTime.of(10, 0), daysPrior)) + .build(); + } +} diff --git a/application/src/test/java/org/opentripplanner/apis/transmodel/model/EnumTypesTest.java b/application/src/test/java/org/opentripplanner/apis/transmodel/model/EnumTypesTest.java index 9090cd1bdc5..5a83ec66bb8 100644 --- a/application/src/test/java/org/opentripplanner/apis/transmodel/model/EnumTypesTest.java +++ b/application/src/test/java/org/opentripplanner/apis/transmodel/model/EnumTypesTest.java @@ -1,6 +1,7 @@ package org.opentripplanner.apis.transmodel.model; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -93,7 +94,7 @@ void serializeRelativeDirection(RelativeDirection direction) { Locale.ENGLISH ); assertInstanceOf(String.class, value); - assertNotNull(value); + assertFalse(((String) value).isEmpty()); } @Test diff --git a/application/src/test/java/org/opentripplanner/generate/doc/framework/NodeAdapterHelper.java b/application/src/test/java/org/opentripplanner/generate/doc/framework/NodeAdapterHelper.java index 7df4429da15..a0eb9180c97 100644 --- a/application/src/test/java/org/opentripplanner/generate/doc/framework/NodeAdapterHelper.java +++ b/application/src/test/java/org/opentripplanner/generate/doc/framework/NodeAdapterHelper.java @@ -15,6 +15,7 @@ public class NodeAdapterHelper { new AnchorAbbreviation("od.", "osmDefaults."), new AnchorAbbreviation("lfp.", "localFileNamePatterns."), new AnchorAbbreviation("u.", "updaters."), + new AnchorAbbreviation("tpfm.", "transferParametersForMode."), new AnchorAbbreviation("0.", "[0]."), new AnchorAbbreviation("1.", "[1].") ); 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 39d2f4b5684..c18200793df 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,8 +8,11 @@ import java.time.Duration; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +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; @@ -21,15 +24,18 @@ 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.framework.DurationForEnum; 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.CarAccess; import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; import org.opentripplanner.utils.tostring.ToStringBuilder; /** @@ -241,36 +247,145 @@ public void testMultipleRequestsWithPatterns() { ) .buildGraph(); + var walkTransfers = timetableRepository.findTransfers(StreetMode.WALK); + var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); + var carTransfers = timetableRepository.findTransfers(StreetMode.CAR); + assertTransfers( - timetableRepository.getAllPathTransfers(), + walkTransfers, + tr(S0, 100, List.of(V0, V11), S11), + tr(S0, 100, List.of(V0, V21), S21), + tr(S11, 100, List.of(V11, V21), S21) + ); + assertTransfers( + bikeTransfers, tr(S0, 100, List.of(V0, V11), S11), tr(S0, 100, List.of(V0, V21), S21), - tr(S11, 100, List.of(V11, V21), S21), tr(S11, 110, List.of(V11, V22), S22) ); + assertTransfers(carTransfers); } @Test - public void testPathTransfersWithModesForMultipleRequestsWithPatterns() { + public void testTransferOnIsolatedStations() { + var otpModel = model(true, false, true, false); + var graph = otpModel.graph(); + graph.hasStreets = false; + + var timetableRepository = otpModel.timetableRepository(); + var req = new RouteRequest(); + req.journey().transfer().setMode(StreetMode.WALK); + var transferRequests = List.of(req); + + new DirectTransferGenerator( + graph, + timetableRepository, + DataImportIssueStore.NOOP, + MAX_TRANSFER_DURATION, + transferRequests + ) + .buildGraph(); + + assertTrue(timetableRepository.getAllPathTransfers().isEmpty()); + } + + @Test + public void testRequestWithCarsAllowedPatterns() { + var reqCar = new RouteRequest(); + reqCar.journey().transfer().setMode(StreetMode.CAR); + + var transferRequests = List.of(reqCar); + + 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.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(); + + assertTransfers( + timetableRepository.getAllPathTransfers(), + tr(S0, 100, List.of(V0, V11), S11), + tr(S0, 200, List.of(V0, V12), S12) + ); + } + + @Test + public void testRequestWithCarsAllowedPatternsWithDurationLimit() { + var reqCar = new RouteRequest(); + reqCar.journey().transfer().setMode(StreetMode.CAR); + + var transferRequests = List.of(reqCar); + + 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(); + + assertTransfers(timetableRepository.getAllPathTransfers(), tr(S0, 100, List.of(V0, V11), S11)); + } + + @Test + public void testMultipleRequestsWithPatternsAndWithCarsAllowedPatterns() { var reqWalk = new RouteRequest(); reqWalk.journey().transfer().setMode(StreetMode.WALK); var reqBike = new RouteRequest(); reqBike.journey().transfer().setMode(StreetMode.BIKE); - var transferRequests = List.of(reqWalk, reqBike); + var reqCar = new RouteRequest(); + reqCar.journey().transfer().setMode(StreetMode.CAR); - TestOtpModel model = model(true); - var graph = model.graph(); + var transferRequests = List.of(reqWalk, reqBike, reqCar); + + var otpModel = model(true, false, false, true); + var graph = otpModel.graph(); graph.hasStreets = true; - var timetableRepository = model.timetableRepository(); + 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 + transferRequests, + transferParametersForMode ) .buildGraph(); @@ -282,38 +397,194 @@ public void testPathTransfersWithModesForMultipleRequestsWithPatterns() { walkTransfers, tr(S0, 100, List.of(V0, V11), S11), tr(S0, 100, List.of(V0, V21), S21), - tr(S11, 100, List.of(V11, V21), S21) + tr(S11, 100, List.of(V11, V21), S21), + tr(S0, 200, List.of(V0, V12), S12), + tr(S11, 100, List.of(V11, V12), S12) ); assertTransfers( bikeTransfers, tr(S0, 100, List.of(V0, V11), S11), tr(S0, 100, List.of(V0, V21), S21), + tr(S11, 110, List.of(V11, V22), S22), + tr(S0, 200, List.of(V0, V12), S12), + tr(S11, 100, List.of(V11, V12), S12) + ); + assertTransfers( + carTransfers, + tr(S0, 100, List.of(V0, V11), S11), + tr(S0, 200, List.of(V0, V12), S12), + tr(S0, 100, List.of(V0, V21), S21) + ); + } + + @Test + public void testBikeRequestWithPatternsAndWithCarsAllowedPatterns() { + var reqBike = new RouteRequest(); + reqBike.journey().transfer().setMode(StreetMode.BIKE); + + var transferRequests = List.of(reqBike); + + 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(); + + assertTransfers( + timetableRepository.getAllPathTransfers(), + tr(S0, 100, List.of(V0, V11), S11), + tr(S0, 100, List.of(V0, V21), S21), + tr(S0, 200, List.of(V0, V12), S12), + tr(S11, 110, List.of(V11, V22), S22), + tr(S11, 100, List.of(V11, V12), S12) + ); + } + + @Test + public void testBikeRequestWithPatternsAndWithCarsAllowedPatternsWithoutCarInTransferRequests() { + var reqBike = new RouteRequest(); + reqBike.journey().transfer().setMode(StreetMode.BIKE); + + var transferRequests = List.of(reqBike); + + 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(); + + assertTransfers( + timetableRepository.getAllPathTransfers(), + tr(S0, 100, List.of(V0, V11), S11), + tr(S0, 100, List.of(V0, V21), S21), tr(S11, 110, List.of(V11, V22), S22) ); - assertTransfers(carTransfers); } @Test - public void testTransferOnIsolatedStations() { - var otpModel = model(true, false, true); + public void testDisableDefaultTransfersForMode() { + var reqWalk = new RouteRequest(); + reqWalk.journey().transfer().setMode(StreetMode.WALK); + + var reqBike = new RouteRequest(); + reqBike.journey().transfer().setMode(StreetMode.BIKE); + + var reqCar = new RouteRequest(); + reqCar.journey().transfer().setMode(StreetMode.CAR); + + var transferRequests = List.of(reqWalk, reqBike, reqCar); + + var otpModel = model(true, false, false, true); var graph = otpModel.graph(); - graph.hasStreets = false; + 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); + + assertTransfers( + walkTransfers, + tr(S0, 100, List.of(V0, V11), S11), + tr(S0, 100, List.of(V0, V21), S21), + tr(S11, 100, List.of(V11, V21), S21), + tr(S0, 200, List.of(V0, V12), S12), + tr(S11, 100, List.of(V11, V12), S12) + ); + assertTransfers(bikeTransfers); + assertTransfers(carTransfers); + } + + @Test + public void testMaxTransferDurationForMode() { + var reqWalk = new RouteRequest(); + reqWalk.journey().transfer().setMode(StreetMode.WALK); + + var reqBike = new RouteRequest(); + reqBike.journey().transfer().setMode(StreetMode.BIKE); + var transferRequests = List.of(reqWalk, reqBike); + + var otpModel = model(true, false, false, true); + var graph = otpModel.graph(); + graph.hasStreets = true; var timetableRepository = otpModel.timetableRepository(); - var req = new RouteRequest(); - req.journey().transfer().setMode(StreetMode.WALK); - var transferRequests = List.of(req); + + 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 + transferRequests, + transferParametersForMode ) .buildGraph(); - assertTrue(timetableRepository.getAllPathTransfers().isEmpty()); + var walkTransfers = timetableRepository.findTransfers(StreetMode.WALK); + var bikeTransfers = timetableRepository.findTransfers(StreetMode.BIKE); + var carTransfers = timetableRepository.findTransfers(StreetMode.CAR); + + assertTransfers( + walkTransfers, + tr(S0, 100, List.of(V0, V11), S11), + tr(S0, 100, List.of(V0, V21), S21), + tr(S11, 100, List.of(V11, V21), S21), + tr(S11, 100, List.of(V11, V12), S12) + ); + assertTransfers( + bikeTransfers, + tr(S0, 100, List.of(V0, V11), S11), + tr(S0, 100, List.of(V0, V21), S21) + ); + assertTransfers(carTransfers); } private TestOtpModel model(boolean addPatterns) { @@ -321,13 +592,14 @@ private TestOtpModel model(boolean addPatterns) { } private TestOtpModel model(boolean addPatterns, boolean withBoardingConstraint) { - return model(addPatterns, withBoardingConstraint, false); + return model(addPatterns, withBoardingConstraint, false, false); } private TestOtpModel model( boolean addPatterns, boolean withBoardingConstraint, - boolean withNoTransfersOnStations + boolean withNoTransfersOnStations, + boolean addCarsAllowedPatterns ) { return modelOf( new Builder() { @@ -395,6 +667,76 @@ 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() + ); + } } } ); diff --git a/application/src/test/java/org/opentripplanner/gtfs/mapping/FareLegRuleMapperTest.java b/application/src/test/java/org/opentripplanner/gtfs/mapping/FareLegRuleMapperTest.java index dbe833a7aaa..334d30fa94f 100644 --- a/application/src/test/java/org/opentripplanner/gtfs/mapping/FareLegRuleMapperTest.java +++ b/application/src/test/java/org/opentripplanner/gtfs/mapping/FareLegRuleMapperTest.java @@ -12,11 +12,11 @@ import org.onebusaway.gtfs.model.FareLegRule; import org.onebusaway.gtfs.model.FareMedium; import org.onebusaway.gtfs.model.FareProduct; -import org.opentripplanner.ext.fares.model.Distance; import org.opentripplanner.ext.fares.model.FareDistance; import org.opentripplanner.ext.fares.model.FareDistance.LinearDistance; import org.opentripplanner.ext.fares.model.FareDistance.Stops; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.transit.model.basic.Distance; class FareLegRuleMapperTest { @@ -33,7 +33,10 @@ private record TestCase( 1, 5000d, 10000d, - new LinearDistance(Distance.ofKilometers(5), Distance.ofKilometers(10)) + new LinearDistance( + Distance.ofKilometersBoxed(5d, ignore -> {}).orElse(null), + Distance.ofKilometersBoxed(10d, ignore -> {}).orElse(null) + ) ), new TestCase(null, null, null, null) ); diff --git a/application/src/test/java/org/opentripplanner/routing/api/request/preference/CarPreferencesTest.java b/application/src/test/java/org/opentripplanner/routing/api/request/preference/CarPreferencesTest.java index 6b03873b80d..564baa0330d 100644 --- a/application/src/test/java/org/opentripplanner/routing/api/request/preference/CarPreferencesTest.java +++ b/application/src/test/java/org/opentripplanner/routing/api/request/preference/CarPreferencesTest.java @@ -11,6 +11,7 @@ class CarPreferencesTest { private static final double RELUCTANCE = 5.111; + public static final int BOARD_COST = 550; private static final double EXPECTED_RELUCTANCE = 5.1; private static final int PICKUP_TIME = 600; private static final int PICKUP_COST = 500; @@ -22,6 +23,7 @@ class CarPreferencesTest { private final CarPreferences subject = CarPreferences .of() .withReluctance(RELUCTANCE) + .withBoardCost(BOARD_COST) .withPickupTime(Duration.ofSeconds(PICKUP_TIME)) .withPickupCost(PICKUP_COST) .withAccelerationSpeed(ACCELERATION_SPEED) @@ -35,6 +37,11 @@ void reluctance() { assertEquals(EXPECTED_RELUCTANCE, subject.reluctance()); } + @Test + void boardCost() { + assertEquals(BOARD_COST, subject.boardCost()); + } + @Test void pickupTime() { assertEquals(Duration.ofSeconds(PICKUP_TIME), subject.pickupTime()); @@ -85,6 +92,7 @@ void testToString() { assertEquals( "CarPreferences{" + "reluctance: 5.1, " + + "boardCost: $550, " + "parking: VehicleParkingPreferences{cost: $30}, " + "rental: VehicleRentalPreferences{pickupTime: 30s}, " + "pickupTime: PT10M, " + diff --git a/application/src/test/java/org/opentripplanner/routing/core/DistanceTest.java b/application/src/test/java/org/opentripplanner/routing/core/DistanceTest.java new file mode 100644 index 00000000000..5eb1ce8bc6a --- /dev/null +++ b/application/src/test/java/org/opentripplanner/routing/core/DistanceTest.java @@ -0,0 +1,45 @@ +package org.opentripplanner.routing.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.model.basic.Distance; + +public class DistanceTest { + + private static final Distance ONE_THOUSAND_FIVE_HUNDRED_METERS = Distance + .ofMetersBoxed(1500d, ignore -> {}) + .orElse(null); + private static final Distance ONE_POINT_FIVE_KILOMETERS = Distance + .ofKilometersBoxed(1.5d, ignore -> {}) + .orElse(null); + private static final Distance TWO_KILOMETERS = Distance + .ofKilometersBoxed(2d, ignore -> {}) + .orElse(null); + private static final Distance ONE_HUNDRED_METERS = Distance + .ofMetersBoxed(100d, ignore -> {}) + .orElse(null); + private static final Distance POINT_ONE_KILOMETER = Distance + .ofKilometersBoxed(0.1d, ignore -> {}) + .orElse(null); + private static final Distance ONE_HUNDRED_POINT_FIVE_METERS = Distance + .ofMetersBoxed(100.5d, ignore -> {}) + .orElse(null); + + @Test + void equals() { + assertEquals(ONE_THOUSAND_FIVE_HUNDRED_METERS, ONE_POINT_FIVE_KILOMETERS); + assertEquals(POINT_ONE_KILOMETER, ONE_HUNDRED_METERS); + assertNotEquals(ONE_HUNDRED_POINT_FIVE_METERS, ONE_HUNDRED_METERS); + assertNotEquals(TWO_KILOMETERS, ONE_POINT_FIVE_KILOMETERS); + } + + @Test + void testHashCode() { + assertEquals( + Distance.ofMetersBoxed(5d, ignore -> {}).hashCode(), + Distance.ofMetersBoxed(5d, ignore -> {}).hashCode() + ); + } +} diff --git a/application/src/test/java/org/opentripplanner/routing/core/RatioTest.java b/application/src/test/java/org/opentripplanner/routing/core/RatioTest.java new file mode 100644 index 00000000000..74a8fe6060a --- /dev/null +++ b/application/src/test/java/org/opentripplanner/routing/core/RatioTest.java @@ -0,0 +1,42 @@ +package org.opentripplanner.routing.core; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.model.basic.Ratio; + +public class RatioTest { + + private static final Double HALF = 0.5d; + private static final Double ZERO = 0d; + private static final Double ONE = 1d; + private static final Double TOO_HIGH = 1.1d; + private static final Double TOO_LOW = -1.1d; + + @Test + void validRatios() { + assertDoesNotThrow(() -> Ratio.of(HALF)); + assertDoesNotThrow(() -> Ratio.of(ZERO)); + assertDoesNotThrow(() -> Ratio.of(ONE)); + } + + @Test + void invalidRatios() { + assertThrows(IllegalArgumentException.class, () -> Ratio.of(TOO_HIGH)); + assertThrows(IllegalArgumentException.class, () -> Ratio.of(TOO_LOW)); + } + + @Test + void testHashCode() { + Ratio half = Ratio.of(HALF); + + Ratio half2 = Ratio.of(HALF); + assertEquals(half.hashCode(), half2.hashCode()); + + Double halfDouble = 2d; + assertNotEquals(half.hashCode(), halfDouble); + } +} diff --git a/application/src/test/java/org/opentripplanner/service/vehiclerental/model/TestFreeFloatingRentalVehicleBuilder.java b/application/src/test/java/org/opentripplanner/service/vehiclerental/model/TestFreeFloatingRentalVehicleBuilder.java index a9b2398f686..2b8881715a9 100644 --- a/application/src/test/java/org/opentripplanner/service/vehiclerental/model/TestFreeFloatingRentalVehicleBuilder.java +++ b/application/src/test/java/org/opentripplanner/service/vehiclerental/model/TestFreeFloatingRentalVehicleBuilder.java @@ -1,7 +1,10 @@ package org.opentripplanner.service.vehiclerental.model; +import javax.annotation.Nullable; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.street.model.RentalFormFactor; +import org.opentripplanner.transit.model.basic.Distance; +import org.opentripplanner.transit.model.basic.Ratio; import org.opentripplanner.transit.model.framework.FeedScopedId; public class TestFreeFloatingRentalVehicleBuilder { @@ -9,10 +12,15 @@ public class TestFreeFloatingRentalVehicleBuilder { public static final String NETWORK_1 = "Network-1"; public static final double DEFAULT_LATITUDE = 47.520; public static final double DEFAULT_LONGITUDE = 19.01; + public static final double DEFAULT_CURRENT_FUEL_PERCENT = 0.5; + public static final double DEFAULT_CURRENT_RANGE_METERS = 5500.7; private double latitude = DEFAULT_LATITUDE; private double longitude = DEFAULT_LONGITUDE; + private Ratio currentFuelPercent = Ratio.of(DEFAULT_CURRENT_FUEL_PERCENT); + private Double currentRangeMeters = DEFAULT_CURRENT_RANGE_METERS; private VehicleRentalSystem system = null; + private String network = NETWORK_1; private RentalVehicleType vehicleType = RentalVehicleType.getDefaultType(NETWORK_1); @@ -30,6 +38,27 @@ public TestFreeFloatingRentalVehicleBuilder withLongitude(double longitude) { return this; } + public TestFreeFloatingRentalVehicleBuilder withCurrentFuelPercent( + @Nullable Double currentFuelPercent + ) { + if (currentFuelPercent == null) { + this.currentFuelPercent = null; + } else { + this.currentFuelPercent = Ratio.ofBoxed(currentFuelPercent, ignore -> {}).orElse(null); + } + return this; + } + + public TestFreeFloatingRentalVehicleBuilder withCurrentRangeMeters(Double currentRangeMeters) { + this.currentRangeMeters = currentRangeMeters; + return this; + } + + public TestFreeFloatingRentalVehicleBuilder withNetwork(String network) { + this.network = network; + return this; + } + public TestFreeFloatingRentalVehicleBuilder withSystem(String id, String url) { this.system = new VehicleRentalSystem( @@ -64,6 +93,23 @@ public TestFreeFloatingRentalVehicleBuilder withVehicleCar() { return buildVehicleType(RentalFormFactor.CAR); } + public VehicleRentalVehicle build() { + var vehicle = new VehicleRentalVehicle(); + var stationName = "free-floating-" + vehicleType.formFactor.name().toLowerCase(); + vehicle.id = new FeedScopedId(this.network, stationName); + vehicle.name = new NonLocalizedString(stationName); + vehicle.latitude = latitude; + vehicle.longitude = longitude; + vehicle.vehicleType = vehicleType; + vehicle.system = system; + vehicle.fuel = + new RentalVehicleFuel( + currentFuelPercent, + Distance.ofMetersBoxed(currentRangeMeters, ignore -> {}).orElse(null) + ); + return vehicle; + } + private TestFreeFloatingRentalVehicleBuilder buildVehicleType(RentalFormFactor rentalFormFactor) { this.vehicleType = new RentalVehicleType( @@ -75,16 +121,4 @@ private TestFreeFloatingRentalVehicleBuilder buildVehicleType(RentalFormFactor r ); return this; } - - public VehicleRentalVehicle build() { - var vehicle = new VehicleRentalVehicle(); - var stationName = "free-floating-" + vehicleType.formFactor.name().toLowerCase(); - vehicle.id = new FeedScopedId(NETWORK_1, stationName); - vehicle.name = new NonLocalizedString(stationName); - vehicle.latitude = latitude; - vehicle.longitude = longitude; - vehicle.vehicleType = vehicleType; - vehicle.system = system; - return vehicle; - } } diff --git a/application/src/test/java/org/opentripplanner/transit/model/filter/expr/CaseInsensitiveStringPrefixMatcherTest.java b/application/src/test/java/org/opentripplanner/transit/model/filter/expr/CaseInsensitiveStringPrefixMatcherTest.java new file mode 100644 index 00000000000..112fbaa7002 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/transit/model/filter/expr/CaseInsensitiveStringPrefixMatcherTest.java @@ -0,0 +1,16 @@ +package org.opentripplanner.transit.model.filter.expr; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class CaseInsensitiveStringPrefixMatcherTest { + + @Test + void testMatches() { + var matcher = new CaseInsensitiveStringPrefixMatcher<>("prefix", "foo", s -> s.toString()); + assertTrue(matcher.match("foo")); + assertTrue(matcher.match("foobar")); + assertFalse(matcher.match("bar")); + } +} diff --git a/application/src/test/java/org/opentripplanner/transit/model/filter/expr/NullSafeWrapperMatcherTest.java b/application/src/test/java/org/opentripplanner/transit/model/filter/expr/NullSafeWrapperMatcherTest.java new file mode 100644 index 00000000000..0b2f008a2fc --- /dev/null +++ b/application/src/test/java/org/opentripplanner/transit/model/filter/expr/NullSafeWrapperMatcherTest.java @@ -0,0 +1,30 @@ +package org.opentripplanner.transit.model.filter.expr; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class NullSafeWrapperMatcherTest { + + @Test + void testMatches() { + var matcher = new NullSafeWrapperMatcher<>( + "string", + s -> s, + new CaseInsensitiveStringPrefixMatcher<>("string", "namePrefix", s -> s.toString()) + ); + assertTrue(matcher.match("namePrefix and more")); + assertFalse(matcher.match("not namePrefix")); + assertFalse(matcher.match(null)); + } + + @Test + void testFailsWithoutNullSafeWrapperMatcher() { + var matcher = new CaseInsensitiveStringPrefixMatcher<>( + "string", + "here's a string", + s -> s.toString() + ); + assertThrows(NullPointerException.class, () -> matcher.match(null)); + } +} diff --git a/application/src/test/java/org/opentripplanner/transit/model/filter/transit/RouteMatcherFactoryTest.java b/application/src/test/java/org/opentripplanner/transit/model/filter/transit/RouteMatcherFactoryTest.java new file mode 100644 index 00000000000..ce9230ac0a6 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/transit/model/filter/transit/RouteMatcherFactoryTest.java @@ -0,0 +1,146 @@ +package org.opentripplanner.transit.model.filter.transit; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Locale; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.transit.api.model.FilterValues; +import org.opentripplanner.transit.api.request.FindRoutesRequest; +import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.filter.expr.Matcher; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.organization.Agency; + +class RouteMatcherFactoryTest { + + private Route route1; + private Route route2; + + @BeforeEach + void setUp() { + route1 = + Route + .of(new FeedScopedId("feedId", "routeId")) + .withAgency(TimetableRepositoryForTest.agency("AGENCY")) + .withMode(TransitMode.BUS) + .withShortName("ROUTE1") + .withLongName(I18NString.of("ROUTE1LONG")) + .build(); + route2 = + Route + .of(new FeedScopedId("otherFeedId", "otherRouteId")) + .withAgency(TimetableRepositoryForTest.agency("OTHER_AGENCY")) + .withMode(TransitMode.RAIL) + .withShortName("ROUTE2") + .withLongName(I18NString.of("ROUTE2LONG")) + .build(); + } + + @Test + void testAgencies() { + FindRoutesRequest request = FindRoutesRequest + .of() + .withAgencies(FilterValues.ofEmptyIsEverything("agencies", List.of("AGENCY"))) + .build(); + + Matcher matcher = RouteMatcherFactory.of(request, r -> false); + assertTrue(matcher.match(route1)); + assertFalse(matcher.match(route2)); + } + + @Test + void testTransitModes() { + FindRoutesRequest request = FindRoutesRequest + .of() + .withTransitModes(FilterValues.ofEmptyIsEverything("transitModes", List.of(TransitMode.BUS))) + .build(); + + Matcher matcher = RouteMatcherFactory.of(request, r -> false); + assertTrue(matcher.match(route1)); + assertFalse(matcher.match(route2)); + } + + @Test + void testShortLongName() { + FindRoutesRequest request = FindRoutesRequest.of().withShortName("ROUTE1").build(); + + Matcher matcher = RouteMatcherFactory.of(request, r -> false); + assertTrue(matcher.match(route1)); + assertFalse(matcher.match(route2)); + } + + @Test + void testShortNames() { + FindRoutesRequest request = FindRoutesRequest + .of() + .withShortNames(FilterValues.ofEmptyIsEverything("publicCodes", List.of("ROUTE1", "ROUTE3"))) + .build(); + + Matcher matcher = RouteMatcherFactory.of(request, r -> false); + assertTrue(matcher.match(route1)); + assertFalse(matcher.match(route2)); + } + + @Test + void testIsFlexRoute() { + FindRoutesRequest request = FindRoutesRequest.of().withFlexibleOnly(true).build(); + + Set flexRoutes = Set.of(route1); + + Matcher matcher = RouteMatcherFactory.of(request, flexRoutes::contains); + assertTrue(matcher.match(route1)); + assertFalse(matcher.match(route2)); + } + + @Test + void testLongNameExactMatch() { + FindRoutesRequest request = FindRoutesRequest.of().withLongName("ROUTE1LONG").build(); + + Matcher matcher = RouteMatcherFactory.of(request, r -> false); + assertTrue(matcher.match(route1)); + assertFalse(matcher.match(route2)); + } + + @Test + void testLongNamePrefixMatch() { + FindRoutesRequest request = FindRoutesRequest.of().withLongName("ROUTE1").build(); + + Matcher matcher = RouteMatcherFactory.of(request, r -> false); + assertTrue(matcher.match(route1)); + assertFalse(matcher.match(route2)); + } + + @Test + void testLongNameCaseInsensitivePrefixMatch() { + FindRoutesRequest request = FindRoutesRequest.of().withLongName("route1").build(); + + Matcher matcher = RouteMatcherFactory.of(request, r -> false); + assertTrue(matcher.match(route1)); + assertFalse(matcher.match(route2)); + } + + @Test + void testAll() { + FindRoutesRequest request = FindRoutesRequest + .of() + .withAgencies(FilterValues.ofEmptyIsEverything("agencies", List.of("AGENCY"))) + .withTransitModes(FilterValues.ofEmptyIsEverything("transitModes", List.of(TransitMode.BUS))) + .withShortName("ROUTE1") + .withShortNames(FilterValues.ofEmptyIsEverything("publicCodes", List.of("ROUTE1", "ROUTE3"))) + .withFlexibleOnly(true) + .withLongName("ROUTE1") + .build(); + + Set flexRoutes = Set.of(route1); + + Matcher matcher = RouteMatcherFactory.of(request, flexRoutes::contains); + assertTrue(matcher.match(route1)); + assertFalse(matcher.match(route2)); + } +} diff --git a/application/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java b/application/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java index ab644227cf2..45626a42714 100644 --- a/application/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java +++ b/application/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java @@ -106,10 +106,9 @@ static void setup() { timetableSnapshot.update(new RealTimeTripUpdate(RAIL_PATTERN, canceledTripTimes, secondDate)); var snapshot = timetableSnapshot.commit(); - timetableRepository.initTimetableSnapshotProvider(() -> snapshot); service = - new DefaultTransitService(timetableRepository) { + new DefaultTransitService(timetableRepository, snapshot) { @Override public Collection findPatterns(StopLocation stop) { if (stop.equals(STOP_B)) { diff --git a/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java b/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java index 2a3add223e8..3f28a1f2811 100644 --- a/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java +++ b/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java @@ -1,13 +1,14 @@ package org.opentripplanner.transit.speed_test; import static org.opentripplanner.model.projectinfo.OtpProjectInfo.projectInfo; -import static org.opentripplanner.standalone.configure.ConstructApplication.creatTransitLayerForRaptor; +import static org.opentripplanner.standalone.configure.ConstructApplication.createTransitLayerForRaptor; import static org.opentripplanner.standalone.configure.ConstructApplication.initializeTransferCache; import static org.opentripplanner.transit.speed_test.support.AssertSpeedTestSetup.assertTestDateHasData; import java.io.File; import java.lang.ref.WeakReference; import java.net.URI; +import java.time.LocalDate; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -18,6 +19,7 @@ import org.opentripplanner.framework.application.OtpAppException; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.raptor.configure.RaptorConfig; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; import org.opentripplanner.routing.api.response.RoutingResponse; import org.opentripplanner.routing.framework.DebugTimingAggregator; import org.opentripplanner.routing.graph.Graph; @@ -45,7 +47,9 @@ import org.opentripplanner.transit.speed_test.model.timer.SpeedTestTimer; import org.opentripplanner.transit.speed_test.options.SpeedTestCmdLineOpts; import org.opentripplanner.transit.speed_test.options.SpeedTestConfig; +import org.opentripplanner.updater.TimetableSnapshotSourceParameters; import org.opentripplanner.updater.configure.UpdaterConfigurator; +import org.opentripplanner.updater.trip.TimetableSnapshotManager; /** * Test response times for a large batch of origin/destination points. Also demonstrates how to run @@ -101,6 +105,7 @@ public SpeedTest( new DefaultVehicleRentalService(), new DefaultVehicleParkingRepository(), timetableRepository, + new TimetableSnapshotManager(null, TimetableSnapshotSourceParameters.DEFAULT, LocalDate::now), config.updatersConfig ); if (timetableRepository.getUpdaterManager() != null) { @@ -135,7 +140,7 @@ public SpeedTest( ); // Creating transitLayerForRaptor should be integrated into the TimetableRepository, but for now // we do it manually here - creatTransitLayerForRaptor(timetableRepository, config.transitRoutingParams); + createTransitLayerForRaptor(timetableRepository, config.transitRoutingParams); initializeTransferCache(config.transitRoutingParams, timetableRepository); diff --git a/application/src/test/java/org/opentripplanner/transit/speed_test/TransferCacheTest.java b/application/src/test/java/org/opentripplanner/transit/speed_test/TransferCacheTest.java index e6c8de67688..fd3a3676ebe 100644 --- a/application/src/test/java/org/opentripplanner/transit/speed_test/TransferCacheTest.java +++ b/application/src/test/java/org/opentripplanner/transit/speed_test/TransferCacheTest.java @@ -1,10 +1,9 @@ package org.opentripplanner.transit.speed_test; -import static org.opentripplanner.standalone.configure.ConstructApplication.creatTransitLayerForRaptor; +import static org.opentripplanner.standalone.configure.ConstructApplication.createTransitLayerForRaptor; import static org.opentripplanner.transit.speed_test.support.AssertSpeedTestSetup.assertTestDateHasData; import java.util.stream.IntStream; -import org.opentripplanner.framework.application.OtpAppException; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.standalone.OtpStartupInfo; import org.opentripplanner.transit.service.TimetableRepository; @@ -33,7 +32,7 @@ public static void main(String[] args) { // Creating transitLayerForRaptor should be integrated into the TimetableRepository, but for now // we do it manually here - creatTransitLayerForRaptor(timetableRepository, config.transitRoutingParams); + createTransitLayerForRaptor(timetableRepository, config.transitRoutingParams); assertTestDateHasData(timetableRepository, config, buildConfig); diff --git a/application/src/test/java/org/opentripplanner/updater/siri/SiriTimetableSnapshotSourceTest.java b/application/src/test/java/org/opentripplanner/updater/siri/SiriTimetableSnapshotSourceTest.java index fea39f912db..106347baeac 100644 --- a/application/src/test/java/org/opentripplanner/updater/siri/SiriTimetableSnapshotSourceTest.java +++ b/application/src/test/java/org/opentripplanner/updater/siri/SiriTimetableSnapshotSourceTest.java @@ -36,7 +36,7 @@ class SiriTimetableSnapshotSourceTest implements RealtimeTestConstants { @Test void testCancelTrip() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); assertEquals(RealTimeState.SCHEDULED, env.getTripTimesForTrip(TRIP_1_ID).getRealTimeState()); @@ -53,7 +53,7 @@ void testCancelTrip() { @Test void testAddJourneyWithExistingRoute() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); Route route = ROUTE_1; int numPatternForRoute = env.getTransitService().findPatterns(route).size(); @@ -88,7 +88,7 @@ void testAddJourneyWithExistingRoute() { @Test void testAddJourneyWithNewRoute() { // we actually don't need the trip, but it's the only way to add a route to the index - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); String newRouteRef = "new route ref"; var updates = createValidAddedJourney(env) @@ -115,7 +115,7 @@ void testAddJourneyWithNewRoute() { @Test void testAddJourneyMultipleTimes() { // we actually don't need the trip, but it's the only way to add a route to the index - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); var updates = createValidAddedJourney(env).buildEstimatedTimetableDeliveries(); int numTrips = env.getTransitService().listTrips().size(); @@ -130,7 +130,7 @@ void testAddJourneyMultipleTimes() { @Test void testAddedJourneyWithInvalidScheduledData() { // we actually don't need the trip, but it's the only way to add a route to the index - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); // Create an extra journey with invalid planned data (travel back in time) // and valid real time data @@ -155,7 +155,7 @@ void testAddedJourneyWithInvalidScheduledData() { @Test void testAddedJourneyWithUnresolvableAgency() { - var env = RealtimeTestEnvironment.siri().build(); + var env = RealtimeTestEnvironment.of().build(); // Create an extra journey with unknown line and operator var createExtraJourney = new SiriEtBuilder(env.getDateTimeHelper()) @@ -179,7 +179,7 @@ void testAddedJourneyWithUnresolvableAgency() { @Test void testReplaceJourney() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) .withEstimatedVehicleJourneyCode("newJourney") @@ -212,7 +212,7 @@ void testReplaceJourney() { */ @Test void testUpdateJourneyWithDatedVehicleJourneyRef() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); var updates = updatedJourneyBuilder(env) .withDatedVehicleJourneyRef(TRIP_1_ID) @@ -231,7 +231,7 @@ void testUpdateJourneyWithDatedVehicleJourneyRef() { */ @Test void testUpdateJourneyWithFramedVehicleJourneyRef() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); var updates = updatedJourneyBuilder(env) .withFramedVehicleJourneyRef(builder -> @@ -248,7 +248,7 @@ void testUpdateJourneyWithFramedVehicleJourneyRef() { */ @Test void testUpdateJourneyWithoutJourneyRef() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); var updates = updatedJourneyBuilder(env).buildEstimatedTimetableDeliveries(); var result = env.applyEstimatedTimetable(updates); @@ -261,7 +261,7 @@ void testUpdateJourneyWithoutJourneyRef() { */ @Test void testUpdateJourneyWithFuzzyMatching() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); var updates = updatedJourneyBuilder(env).buildEstimatedTimetableDeliveries(); var result = env.applyEstimatedTimetableWithFuzzyMatcher(updates); @@ -275,7 +275,7 @@ void testUpdateJourneyWithFuzzyMatching() { */ @Test void testUpdateJourneyWithFuzzyMatchingAndMissingAimedDepartureTime() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) .withFramedVehicleJourneyRef(builder -> @@ -300,7 +300,7 @@ void testUpdateJourneyWithFuzzyMatchingAndMissingAimedDepartureTime() { */ @Test void testChangeQuay() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) .withDatedVehicleJourneyRef(TRIP_1_ID) @@ -321,7 +321,7 @@ void testChangeQuay() { @Test void testCancelStop() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_2_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_2_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) .withDatedVehicleJourneyRef(TRIP_2_ID) @@ -349,7 +349,7 @@ void testCancelStop() { @Test @Disabled("Not supported yet") void testAddStop() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) .withDatedVehicleJourneyRef(TRIP_1_ID) @@ -380,7 +380,7 @@ void testAddStop() { @Test void testNotMonitored() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) .withMonitored(false) @@ -393,7 +393,7 @@ void testNotMonitored() { @Test void testReplaceJourneyWithoutEstimatedVehicleJourneyCode() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) .withDatedVehicleJourneyRef("newJourney") @@ -418,7 +418,7 @@ void testReplaceJourneyWithoutEstimatedVehicleJourneyCode() { @Test void testNegativeHopTime() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) .withDatedVehicleJourneyRef(TRIP_1_ID) @@ -438,7 +438,7 @@ void testNegativeHopTime() { @Test void testNegativeDwellTime() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_2_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_2_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) .withDatedVehicleJourneyRef(TRIP_2_ID) @@ -463,7 +463,7 @@ void testNegativeDwellTime() { @Test @Disabled("Not supported yet") void testExtraUnknownStop() { - var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) .withDatedVehicleJourneyRef(TRIP_1_ID) diff --git a/application/src/test/java/org/opentripplanner/updater/siri/moduletests/rejection/InvalidStopPointRefTest.java b/application/src/test/java/org/opentripplanner/updater/siri/moduletests/rejection/InvalidStopPointRefTest.java index 820f44e3a86..b7129584c31 100644 --- a/application/src/test/java/org/opentripplanner/updater/siri/moduletests/rejection/InvalidStopPointRefTest.java +++ b/application/src/test/java/org/opentripplanner/updater/siri/moduletests/rejection/InvalidStopPointRefTest.java @@ -23,7 +23,7 @@ private static Stream cases() { @ParameterizedTest(name = "invalid id of ''{0}'', extraJourney={1}") @MethodSource("cases") void rejectEmptyStopPointRef(String invalidRef, boolean extraJourney) { - var env = RealtimeTestEnvironment.siri().build(); + var env = RealtimeTestEnvironment.of().build(); // journey contains empty stop point ref elements // happens in the South Tyrolian feed: https://github.com/noi-techpark/odh-mentor-otp/issues/213 diff --git a/application/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java b/application/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java index f20e58a7b62..d890e962c36 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java @@ -5,10 +5,8 @@ import static org.opentripplanner.updater.trip.UpdateIncrementality.FULL_DATASET; import com.google.transit.realtime.GtfsRealtime; -import java.time.Duration; import java.time.LocalDate; import java.util.List; -import java.util.Objects; import org.opentripplanner.DateTimeHelper; import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.routing.graph.Graph; @@ -29,58 +27,32 @@ /** * This class exists so that you can share the data building logic for GTFS and Siri tests. - * Since it's not possible to add a Siri and GTFS updater to the transit model at the same time, - * they each have their own test environment. - *

- * It is however a goal to change that and then these two can be combined. */ public final class RealtimeTestEnvironment implements RealtimeTestConstants { - // static constants - private static final TimetableSnapshotSourceParameters PARAMETERS = new TimetableSnapshotSourceParameters( - Duration.ZERO, - false - ); - public final TimetableRepository timetableRepository; + public final TimetableSnapshotManager snapshotManager; private final SiriTimetableSnapshotSource siriSource; private final TimetableSnapshotSource gtfsSource; private final DateTimeHelper dateTimeHelper; - enum SourceType { - GTFS_RT, - SIRI, - } - - /** - * Siri and GTFS-RT cannot be run at the same time, so you need to decide. - */ - public static RealtimeTestEnvironmentBuilder siri() { - return new RealtimeTestEnvironmentBuilder().withSourceType(SourceType.SIRI); - } - - /** - * Siri and GTFS-RT cannot be run at the same time, so you need to decide. - */ - public static RealtimeTestEnvironmentBuilder gtfs() { - return new RealtimeTestEnvironmentBuilder().withSourceType(SourceType.GTFS_RT); + public static RealtimeTestEnvironmentBuilder of() { + return new RealtimeTestEnvironmentBuilder(); } - RealtimeTestEnvironment(SourceType sourceType, TimetableRepository timetableRepository) { - Objects.requireNonNull(sourceType); + RealtimeTestEnvironment(TimetableRepository timetableRepository) { this.timetableRepository = timetableRepository; this.timetableRepository.index(); - // SIRI and GTFS-RT cannot be registered with the transit model at the same time - // we are actively refactoring to remove this restriction - // for the time being you cannot run a SIRI and GTFS-RT test at the same time - if (sourceType == SourceType.SIRI) { - siriSource = new SiriTimetableSnapshotSource(PARAMETERS, timetableRepository); - gtfsSource = null; - } else { - gtfsSource = new TimetableSnapshotSource(PARAMETERS, timetableRepository); - siriSource = null; - } + this.snapshotManager = + new TimetableSnapshotManager( + null, + TimetableSnapshotSourceParameters.PUBLISH_IMMEDIATELY, + () -> SERVICE_DATE + ); + siriSource = new SiriTimetableSnapshotSource(timetableRepository, snapshotManager); + gtfsSource = + new TimetableSnapshotSource(timetableRepository, snapshotManager, () -> SERVICE_DATE); dateTimeHelper = new DateTimeHelper(TIME_ZONE, SERVICE_DATE); } @@ -88,7 +60,7 @@ public static RealtimeTestEnvironmentBuilder gtfs() { * Returns a new fresh TransitService */ public TransitService getTransitService() { - return new DefaultTransitService(timetableRepository); + return new DefaultTransitService(timetableRepository, snapshotManager.getTimetableSnapshot()); } /** @@ -136,11 +108,7 @@ public DateTimeHelper getDateTimeHelper() { } public TimetableSnapshot getTimetableSnapshot() { - if (siriSource != null) { - return siriSource.getTimetableSnapshot(); - } else { - return gtfsSource.getTimetableSnapshot(); - } + return snapshotManager.getTimetableSnapshot(); } public String getRealtimeTimetable(String tripId) { @@ -194,7 +162,6 @@ public UpdateResult applyTripUpdates( List updates, UpdateIncrementality incrementality ) { - Objects.requireNonNull(gtfsSource, "Test environment is configured for SIRI only"); UpdateResult updateResult = gtfsSource.applyTripUpdates( null, BackwardsDelayPropagationType.REQUIRED_NO_DATA, @@ -212,7 +179,6 @@ private UpdateResult applyEstimatedTimetable( List updates, boolean fuzzyMatching ) { - Objects.requireNonNull(siriSource, "Test environment is configured for GTFS-RT only"); UpdateResult updateResult = getEstimatedTimetableHandler(fuzzyMatching) .applyUpdate( updates, @@ -220,7 +186,7 @@ private UpdateResult applyEstimatedTimetable( new DefaultRealTimeUpdateContext( new Graph(), timetableRepository, - siriSource.getTimetableSnapshotBuffer() + snapshotManager.getTimetableSnapshotBuffer() ) ); commitTimetableSnapshot(); @@ -228,11 +194,6 @@ private UpdateResult applyEstimatedTimetable( } private void commitTimetableSnapshot() { - if (siriSource != null) { - siriSource.flushBuffer(); - } - if (gtfsSource != null) { - gtfsSource.flushBuffer(); - } + snapshotManager.purgeAndCommit(); } } diff --git a/application/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironmentBuilder.java b/application/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironmentBuilder.java index eb31a555e1d..09eac9bbb4b 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironmentBuilder.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironmentBuilder.java @@ -21,17 +21,11 @@ public class RealtimeTestEnvironmentBuilder implements RealtimeTestConstants { - private RealtimeTestEnvironment.SourceType sourceType; private final TimetableRepository timetableRepository = new TimetableRepository( SITE_REPOSITORY, new Deduplicator() ); - RealtimeTestEnvironmentBuilder withSourceType(RealtimeTestEnvironment.SourceType sourceType) { - this.sourceType = sourceType; - return this; - } - public RealtimeTestEnvironmentBuilder addTrip(TripInput trip) { createTrip(trip); timetableRepository.index(); @@ -39,7 +33,6 @@ public RealtimeTestEnvironmentBuilder addTrip(TripInput trip) { } public RealtimeTestEnvironment build() { - Objects.requireNonNull(sourceType, "sourceType cannot be null"); timetableRepository.initTimeZone(TIME_ZONE); timetableRepository.addAgency(TimetableRepositoryForTest.AGENCY); @@ -55,7 +48,7 @@ public RealtimeTestEnvironment build() { DataImportIssueStore.NOOP ); - return new RealtimeTestEnvironment(sourceType, timetableRepository); + return new RealtimeTestEnvironment(timetableRepository); } private Trip createTrip(TripInput tripInput) { diff --git a/application/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java b/application/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java index d0e6f19a156..294e0300ebe 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java @@ -1,12 +1,10 @@ package org.opentripplanner.updater.trip; -import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.CANCELED; import static com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.SKIPPED; 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.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.opentripplanner.updater.trip.BackwardsDelayPropagationType.REQUIRED_NO_DATA; import static org.opentripplanner.updater.trip.UpdateIncrementality.DIFFERENTIAL; @@ -16,16 +14,15 @@ import com.google.transit.realtime.GtfsRealtime.TripUpdate; import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent; import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate; -import java.time.Duration; import java.time.LocalDate; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.opentripplanner.ConstantsForTests; import org.opentripplanner.TestOtpModel; -import org.opentripplanner._support.time.ZoneIds; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TimetableSnapshot; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.RealTimeState; @@ -41,19 +38,13 @@ public class TimetableSnapshotSourceTest { private static final LocalDate SERVICE_DATE = LocalDate.parse("2009-02-01"); - private static final TripUpdate CANCELLATION = new TripUpdateBuilder( - "1.1", - SERVICE_DATE, - CANCELED, - ZoneIds.NEW_YORK - ) - .build(); private TimetableRepository timetableRepository; private TransitService transitService; private final GtfsRealtimeFuzzyTripMatcher TRIP_MATCHER_NOOP = null; private String feedId; + private TimetableSnapshotManager snapshotManager; @BeforeEach public void setUp() { @@ -62,23 +53,12 @@ public void setUp() { transitService = new DefaultTransitService(timetableRepository); feedId = transitService.listFeedIds().stream().findFirst().get(); - } - - @Test - public void testGetSnapshot() { - var updater = defaultUpdater(); - - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(CANCELLATION), - feedId - ); - - final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); - assertNotNull(snapshot); - assertSame(snapshot, updater.getTimetableSnapshot()); + snapshotManager = + new TimetableSnapshotManager( + null, + TimetableSnapshotSourceParameters.DEFAULT, + () -> SERVICE_DATE + ); } @Test @@ -198,10 +178,10 @@ public void testHandleModifiedTrip() { List.of(tripUpdate), feedId ); - updater.flushBuffer(); + snapshotManager.purgeAndCommit(); // THEN - final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); + final TimetableSnapshot snapshot = snapshotManager.getTimetableSnapshot(); // Original trip pattern { @@ -282,10 +262,6 @@ public void testHandleModifiedTrip() { } private TimetableSnapshotSource defaultUpdater() { - return new TimetableSnapshotSource( - new TimetableSnapshotSourceParameters(Duration.ZERO, true), - timetableRepository, - () -> SERVICE_DATE - ); + return new TimetableSnapshotSource(timetableRepository, snapshotManager, () -> SERVICE_DATE); } } diff --git a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/addition/AddedTest.java b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/addition/AddedTest.java index e926361f4ea..501e5c80ac1 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/addition/AddedTest.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/addition/AddedTest.java @@ -29,7 +29,7 @@ class AddedTest implements RealtimeTestConstants { @Test void addedTrip() { - var env = RealtimeTestEnvironment.gtfs().build(); + var env = RealtimeTestEnvironment.of().build(); var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, TIME_ZONE) .addStopTime(STOP_A1_ID, 30) @@ -43,7 +43,7 @@ void addedTrip() { @Test void addedTripWithNewRoute() { - var env = RealtimeTestEnvironment.gtfs().build(); + var env = RealtimeTestEnvironment.of().build(); var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, TIME_ZONE) .addTripExtension() .addStopTime(STOP_A1_ID, 30, DropOffPickupType.PHONE_AGENCY) @@ -78,7 +78,7 @@ void addedTripWithNewRoute() { @Test void addedWithUnknownStop() { - var env = RealtimeTestEnvironment.gtfs().build(); + var env = RealtimeTestEnvironment.of().build(); var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, TIME_ZONE) // add extension to set route name, url, mode .addTripExtension() @@ -102,7 +102,7 @@ void addedWithUnknownStop() { @Test void repeatedlyAddedTripWithNewRoute() { - var env = RealtimeTestEnvironment.gtfs().build(); + var env = RealtimeTestEnvironment.of().build(); var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, TIME_ZONE) // add extension to set route name, url, mode .addTripExtension() diff --git a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CanceledTripTest.java b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CanceledTripTest.java index 75c8d5caa46..fbe00005d1f 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CanceledTripTest.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CanceledTripTest.java @@ -17,7 +17,7 @@ public class CanceledTripTest implements RealtimeTestConstants { @Test void listCanceledTrips() { var env = RealtimeTestEnvironment - .gtfs() + .of() .addTrip( TripInput .of(TRIP_1_ID) diff --git a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java index e79ca7f6ce5..9c9d5e5f962 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java @@ -36,7 +36,7 @@ static List cases() { @MethodSource("cases") void cancelledTrip(ScheduleRelationship relationship, RealTimeState state) { var env = RealtimeTestEnvironment - .gtfs() + .of() .addTrip( TripInput .of(TRIP_1_ID) @@ -77,7 +77,7 @@ void cancelledTrip(ScheduleRelationship relationship, RealTimeState state) { @ParameterizedTest @MethodSource("cases") void cancelingAddedTrip(ScheduleRelationship relationship, RealTimeState state) { - var env = RealtimeTestEnvironment.gtfs().build(); + var env = RealtimeTestEnvironment.of().build(); var addedTripId = "added-trip"; // First add ADDED trip var update = new TripUpdateBuilder( diff --git a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java index 665c79d193c..f17ad90f560 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java @@ -30,7 +30,7 @@ void singleStopDelay() { .addStop(STOP_A1, "0:00:10", "0:00:11") .addStop(STOP_B1, "0:00:20", "0:00:21") .build(); - var env = RealtimeTestEnvironment.gtfs().addTrip(TRIP_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_INPUT).build(); var tripUpdate = new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) .addDelayedStopTime(STOP_SEQUENCE, DELAY) @@ -78,7 +78,7 @@ void complexDelay() { .addStop(STOP_B1, "0:01:10", "0:01:11") .addStop(STOP_C1, "0:01:20", "0:01:21") .build(); - var env = RealtimeTestEnvironment.gtfs().addTrip(tripInput).build(); + var env = RealtimeTestEnvironment.of().addTrip(tripInput).build(); var tripUpdate = new TripUpdateBuilder(TRIP_2_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) .addDelayedStopTime(0, 0) diff --git a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java index e10b86797b8..680d6f4f3e2 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java @@ -32,7 +32,7 @@ class SkippedTest implements RealtimeTestConstants { @Test void scheduledTripWithSkippedAndScheduled() { - var env = RealtimeTestEnvironment.gtfs().addTrip(TRIP_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_INPUT).build(); var tripUpdate = new TripUpdateBuilder(TRIP_2_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) .addDelayedStopTime(0, 0) @@ -63,7 +63,7 @@ void scheduledTripWithSkippedAndScheduled() { */ @Test void scheduledTripWithPreviouslySkipped() { - var env = RealtimeTestEnvironment.gtfs().addTrip(TRIP_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_INPUT).build(); var tripUpdate = new TripUpdateBuilder(TRIP_2_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) .addDelayedStopTime(0, 0) @@ -107,7 +107,7 @@ void scheduledTripWithPreviouslySkipped() { */ @Test void skippedNoData() { - var env = RealtimeTestEnvironment.gtfs().addTrip(TRIP_INPUT).build(); + var env = RealtimeTestEnvironment.of().addTrip(TRIP_INPUT).build(); String tripId = TRIP_2_ID; diff --git a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java index 2ba6749b4b0..f4f385ff74f 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java @@ -32,7 +32,7 @@ void invalidTripDate(LocalDate date) { .addStop(STOP_A1, "0:00:10", "0:00:11") .addStop(STOP_B1, "0:00:20", "0:00:21") .build(); - var env = RealtimeTestEnvironment.gtfs().addTrip(tripInput).build(); + var env = RealtimeTestEnvironment.of().addTrip(tripInput).build(); var update = new TripUpdateBuilder(TRIP_1_ID, date, SCHEDULED, TIME_ZONE) .addDelayedStopTime(2, 60, 80) diff --git a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidTripIdTest.java b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidTripIdTest.java index 699e8fe865c..60723b2aa65 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidTripIdTest.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidTripIdTest.java @@ -22,7 +22,7 @@ static Stream invalidCases() { @ParameterizedTest(name = "tripId=\"{0}\"") @MethodSource("invalidCases") void invalidTripId(String tripId) { - var env = RealtimeTestEnvironment.gtfs().build(); + var env = RealtimeTestEnvironment.of().build(); var tripDescriptorBuilder = GtfsRealtime.TripDescriptor.newBuilder(); if (tripId != null) { tripDescriptorBuilder.setTripId(tripId); diff --git a/application/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapperTest.java b/application/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapperTest.java index 02295ce09e9..79322d325fc 100644 --- a/application/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapperTest.java +++ b/application/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapperTest.java @@ -37,7 +37,7 @@ class GbfsFreeVehicleStatusMapperTest { new FeedScopedId("1", "scooter"), "Scooter", RentalFormFactor.SCOOTER, - null, + RentalVehicleType.PropulsionType.COMBUSTION, null ) ) @@ -62,7 +62,6 @@ void withDefaultType() { bike.setLon(1d); bike.setVehicleTypeId("bike"); var mapped = MAPPER.mapFreeVehicleStatus(bike); - assertEquals("Default vehicle type", mapped.name.toString()); } @@ -73,6 +72,7 @@ void withType() { bike.setLat(1d); bike.setLon(1d); bike.setVehicleTypeId("scooter"); + bike.setCurrentRangeMeters(2000d); var mapped = MAPPER.mapFreeVehicleStatus(bike); assertEquals("Scooter", mapped.name.toString()); diff --git a/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/rental-vehicle.json b/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/rental-vehicle.json index bcff74d0413..7d739570a9d 100644 --- a/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/rental-vehicle.json +++ b/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/rental-vehicle.json @@ -15,6 +15,10 @@ "rentalNetwork": { "networkId": "Network-1", "url": "https://foo.bar" + }, + "fuel": { + "percent": 0.5, + "range": 5501 } } } diff --git a/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rentals-bybbox.json b/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rentals-bybbox.json index d28e62f8d93..10b7924d745 100644 --- a/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rentals-bybbox.json +++ b/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rentals-bybbox.json @@ -76,6 +76,32 @@ "rentalNetwork": { "networkId": "Network-1", "url": "https://foo.bar" + }, + "fuel": { + "percent": 0.5, + "range": 5501 + } + }, + { + "__typename": "RentalVehicle", + "vehicleId": "Network-2:free-floating-bicycle", + "name": "free-floating-bicycle", + "allowPickupNow": true, + "lon": 19.01, + "lat": 47.52, + "rentalUris": null, + "operative": true, + "vehicleType": { + "formFactor": "BICYCLE", + "propulsionType": "HUMAN" + }, + "rentalNetwork": { + "networkId": "Network-2", + "url": "https://foo.bar.baz" + }, + "fuel": { + "percent": null, + "range": null } } ] diff --git a/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/rental-vehicle.graphql b/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/rental-vehicle.graphql index 9a912781c56..8f32632abc0 100644 --- a/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/rental-vehicle.graphql +++ b/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/rental-vehicle.graphql @@ -19,5 +19,9 @@ networkId url } + fuel { + percent + range + } } } diff --git a/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rentals-bybbox.graphql b/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rentals-bybbox.graphql index 26209f427f9..55c71954692 100644 --- a/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rentals-bybbox.graphql +++ b/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rentals-bybbox.graphql @@ -26,6 +26,10 @@ networkId url } + fuel { + percent + range + } } ... on VehicleRentalStation { stationId diff --git a/application/src/test/resources/standalone/config/build-config.json b/application/src/test/resources/standalone/config/build-config.json index 11ea4a36b2e..9a32b3bd892 100644 --- a/application/src/test/resources/standalone/config/build-config.json +++ b/application/src/test/resources/standalone/config/build-config.json @@ -82,5 +82,15 @@ "emissions": { "carAvgCo2PerKm": 170, "carAvgOccupancy": 1.3 + }, + "transferParametersForMode": { + "CAR": { + "disableDefaultTransfers": true, + "carsAllowedStopMaxTransferDuration": "3h" + }, + "BIKE": { + "maxTransferDuration": "30m", + "carsAllowedStopMaxTransferDuration": "3h" + } } } diff --git a/application/src/test/resources/standalone/config/router-config.json b/application/src/test/resources/standalone/config/router-config.json index 77c67d85742..76896298707 100644 --- a/application/src/test/resources/standalone/config/router-config.json +++ b/application/src/test/resources/standalone/config/router-config.json @@ -44,6 +44,7 @@ }, "car": { "reluctance": 10, + "boardCost": 600, "decelerationSpeed": 2.9, "accelerationSpeed": 2.9, "rental": { @@ -99,6 +100,9 @@ "BIKE_RENTAL": "20m" }, "maxStopCount": 500, + "maxStopCountForMode": { + "CAR": 0 + }, "penalty": { "FLEXIBLE": { "timePenalty": "2m + 1.1t", diff --git a/client/.prettierignore b/client/.prettierignore index a96d61e932a..ba27ff090d8 100644 --- a/client/.prettierignore +++ b/client/.prettierignore @@ -1,3 +1,4 @@ node_modules/ output/ src/gql/ +src/static/query/tripQuery.tsx diff --git a/client/codegen-preprocess.ts b/client/codegen-preprocess.ts new file mode 100644 index 00000000000..ec1b1dfce0d --- /dev/null +++ b/client/codegen-preprocess.ts @@ -0,0 +1,16 @@ +import type { CodegenConfig } from '@graphql-codegen/cli'; + +import * as path from 'node:path'; + +const config: CodegenConfig = { + overwrite: true, + schema: '../application/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql', + documents: 'src/**/*.{ts,tsx}', + generates: { + 'src/static/query/tripQuery.tsx': { + plugins: [path.resolve(__dirname, './src/util/generate-queries.cjs')], + }, + }, +}; + +export default config; diff --git a/client/index.html b/client/index.html index f09832636f0..d73361cce9f 100644 --- a/client/index.html +++ b/client/index.html @@ -10,4 +10,4 @@

- + \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index b82002a5b50..71c57f7262a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -17,7 +17,8 @@ "react": "19.0.0", "react-bootstrap": "2.10.7", "react-dom": "19.0.0", - "react-map-gl": "7.1.8" + "react-map-gl": "7.1.8", + "react-select": "5.9.0" }, "devDependencies": { "@eslint/compat": "1.2.5", @@ -43,7 +44,7 @@ "prettier": "3.4.2", "typescript": "5.7.3", "typescript-eslint": "8.19.1", - "vite": "6.0.7", + "vite": "6.0.9", "vitest": "3.0.2" } }, @@ -235,7 +236,6 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", @@ -288,7 +288,6 @@ "version": "7.26.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", - "dev": true, "dependencies": { "@babel/parser": "^7.26.3", "@babel/types": "^7.26.3", @@ -366,7 +365,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dev": true, "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" @@ -447,7 +445,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -456,7 +453,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -487,7 +483,6 @@ "version": "7.26.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", - "dev": true, "dependencies": { "@babel/types": "^7.26.3" }, @@ -975,7 +970,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.25.9", "@babel/parser": "^7.25.9", @@ -989,7 +983,6 @@ "version": "7.26.4", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.3", @@ -1007,7 +1000,6 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "engines": { "node": ">=4" } @@ -1016,7 +1008,6 @@ "version": "7.26.3", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" @@ -1150,6 +1141,114 @@ "node": ">=18" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, "node_modules/@envelop/core": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.0.2.tgz", @@ -1727,6 +1826,28 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" + }, "node_modules/@googlemaps/polyline-codec": { "version": "1.0.28", "resolved": "https://registry.npmjs.org/@googlemaps/polyline-codec/-/polyline-codec-1.0.28.tgz", @@ -2892,7 +3013,6 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -2906,7 +3026,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -2915,7 +3034,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -2923,14 +3041,12 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -3873,6 +3989,11 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, "node_modules/@types/pbf": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", @@ -4724,6 +4845,43 @@ "node": ">= 0.4" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/babel-plugin-macros/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-plugin-syntax-trailing-function-commas": { "version": "7.0.0-beta.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz", @@ -4999,7 +5157,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -5476,7 +5633,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -5736,7 +5892,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -5961,7 +6116,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -6528,6 +6682,11 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6638,7 +6797,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7096,7 +7254,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -7114,6 +7271,19 @@ "tslib": "^2.0.3" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -7211,7 +7381,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -7227,7 +7396,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -7368,8 +7536,7 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-async-function": { "version": "2.0.0", @@ -7433,7 +7600,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "dependencies": { "hasown": "^2.0.2" }, @@ -8003,7 +8169,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -8020,8 +8185,7 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -8136,8 +8300,7 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/listr2": { "version": "4.0.5", @@ -8426,6 +8589,11 @@ "node": ">= 0.4" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -8527,8 +8695,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/murmurhash-js": { "version": "1.0.0", @@ -8936,7 +9103,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -8962,7 +9128,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -9038,8 +9203,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-root": { "version": "0.1.1", @@ -9088,7 +9252,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -9124,8 +9287,7 @@ "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -9432,6 +9594,26 @@ "node": ">=0.10.0" } }, + "node_modules/react-select": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.9.0.tgz", + "integrity": "sha512-nwRKGanVHGjdccsnzhFte/PULziueZxGD8LL2WojON78Mvnq7LdAMEtu2frrwld1fr3geixg3iiMBIc/LLAZpw==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -9557,7 +9739,6 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", @@ -10077,6 +10258,14 @@ "node": ">=0.10.0" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -10359,6 +10548,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/supercluster": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", @@ -10383,7 +10577,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -10949,6 +11142,19 @@ "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", "dev": true }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz", + "integrity": "sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -10965,9 +11171,9 @@ } }, "node_modules/vite": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", - "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.9.tgz", + "integrity": "sha512-MSgUxHcaXLtnBPktkbUSoQUANApKYuxZ6DrbVENlIorbhL2dZydTLaZ01tjUoE3szeFzlFk9ANOKk0xurh4MKA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/client/package.json b/client/package.json index f34ae361197..2322dcf60af 100644 --- a/client/package.json +++ b/client/package.json @@ -14,6 +14,8 @@ "preview": "vite preview", "prebuild": "npm run codegen && npm run lint && npm run check-format", "predev": "npm run codegen", + "codegen-preprocess": "graphql-codegen --config codegen-preprocess.ts", + "precodegen": "npm run codegen-preprocess", "codegen": "graphql-codegen --config codegen.ts" }, "dependencies": { @@ -26,7 +28,8 @@ "react": "19.0.0", "react-bootstrap": "2.10.7", "react-dom": "19.0.0", - "react-map-gl": "7.1.8" + "react-map-gl": "7.1.8", + "react-select": "5.9.0" }, "devDependencies": { "@eslint/compat": "1.2.5", @@ -52,7 +55,7 @@ "prettier": "3.4.2", "typescript": "5.7.3", "typescript-eslint": "8.19.1", - "vite": "6.0.7", + "vite": "6.0.9", "vitest": "3.0.2" } } diff --git a/client/src/components/ItineraryList/ItineraryListContainer.tsx b/client/src/components/ItineraryList/ItineraryListContainer.tsx index b474d2eb5ec..affff253388 100644 --- a/client/src/components/ItineraryList/ItineraryListContainer.tsx +++ b/client/src/components/ItineraryList/ItineraryListContainer.tsx @@ -26,39 +26,46 @@ export function ItineraryListContainer({ const timeZone = useContext(TimeZoneContext); return ( -
- - setSelectedTripPatternIndex(parseInt(eventKey as string))} - > - {tripQueryResult && - tripQueryResult.trip.tripPatterns.map((tripPattern, itineraryIndex) => ( - - - - - - - - - ))} - +
+ <> +
Itinerary results
+
+ +
+ setSelectedTripPatternIndex(parseInt(eventKey as string))} + > + {tripQueryResult && + tripQueryResult.trip.tripPatterns.map((tripPattern, itineraryIndex) => ( + + + + + + + + + ))} + + + + {/* Time Zone Info */}
All times in {timeZone}
diff --git a/client/src/components/ItineraryList/ItineraryPaginationControl.tsx b/client/src/components/ItineraryList/ItineraryPaginationControl.tsx index dc197a2451e..2e3e335cee0 100644 --- a/client/src/components/ItineraryList/ItineraryPaginationControl.tsx +++ b/client/src/components/ItineraryList/ItineraryPaginationControl.tsx @@ -12,7 +12,7 @@ export function ItineraryPaginationControl({ loading: boolean; }) { return ( -
+
+ + {/* Sidebar */} +
+ {isSidebarOpen && activeContent === 'debugLayer' && ( + + )} +
+
+ ); + } +} + +export default RightMenu; diff --git a/client/src/components/SearchBar/DepartureArrivalSelect.tsx b/client/src/components/SearchBar/DepartureArrivalSelect.tsx index b6a92cdd495..a94516dfc3b 100644 --- a/client/src/components/SearchBar/DepartureArrivalSelect.tsx +++ b/client/src/components/SearchBar/DepartureArrivalSelect.tsx @@ -24,6 +24,7 @@ export function DepartureArrivalSelect({ size="sm" onChange={(e) => (e.target.value === 'arrival' ? onChange(true) : onChange(false))} value={tripQueryVariables.arriveBy ? 'arrival' : 'departure'} + style={{ verticalAlign: 'bottom' }} > diff --git a/client/src/components/SearchBar/InputFieldsSection.tsx b/client/src/components/SearchBar/InputFieldsSection.tsx new file mode 100644 index 00000000000..234626d0c76 --- /dev/null +++ b/client/src/components/SearchBar/InputFieldsSection.tsx @@ -0,0 +1,82 @@ +import { Button, ButtonGroup, Spinner } from 'react-bootstrap'; +import { TripQueryVariables } from '../../gql/graphql.ts'; +import { LocationInputField } from './LocationInputField.tsx'; +import { SwapLocationsButton } from './SwapLocationsButton.tsx'; +import { DepartureArrivalSelect } from './DepartureArrivalSelect.tsx'; +import { DateTimeInputField } from './DateTimeInputField.tsx'; +import { SearchWindowInput } from './SearchWindowInput.tsx'; +import { AccessSelect } from './AccessSelect.tsx'; +import { EgressSelect } from './EgressSelect.tsx'; +import { DirectModeSelect } from './DirectModeSelect.tsx'; +import { TransitModeSelect } from './TransitModeSelect.tsx'; +import { NumTripPatternsInput } from './NumTripPatternsInput.tsx'; +import { ItineraryFilterDebugSelect } from './ItineraryFilterDebugSelect.tsx'; +import GraphiQLRouteButton from './GraphiQLRouteButton.tsx'; + +type InputFieldsSectionProps = { + tripQueryVariables: TripQueryVariables; + setTripQueryVariables: (tripQueryVariables: TripQueryVariables) => void; + onRoute: () => void; + loading: boolean; +}; + +export function InputFieldsSection({ + tripQueryVariables, + setTripQueryVariables, + onRoute, + loading, +}: InputFieldsSectionProps) { + return ( +
+
+ + + +
+
+ + +
+
+ + +
+
+ + + + +
+ + +
+ + + + +
+
+ ); +} diff --git a/client/src/components/SearchBar/ItineraryFilterDebugSelect.tsx b/client/src/components/SearchBar/ItineraryFilterDebugSelect.tsx index 6f479290947..5c781d93e7d 100644 --- a/client/src/components/SearchBar/ItineraryFilterDebugSelect.tsx +++ b/client/src/components/SearchBar/ItineraryFilterDebugSelect.tsx @@ -20,10 +20,10 @@ export function ItineraryFilterDebugSelect({ onChange={(e) => { setTripQueryVariables({ ...tripQueryVariables, - itineraryFiltersDebug: e.target.value as ItineraryFilterDebugProfile, + itineraryFilters: { debug: e.target.value as ItineraryFilterDebugProfile }, }); }} - value={tripQueryVariables.itineraryFiltersDebug || 'not_selected'} + value={tripQueryVariables.itineraryFilters?.debug || 'not_selected'} > {Object.values(ItineraryFilterDebugProfile).map((debugProfile) => ( diff --git a/client/src/components/SearchBar/LogoSection.tsx b/client/src/components/SearchBar/LogoSection.tsx new file mode 100644 index 00000000000..087263e8167 --- /dev/null +++ b/client/src/components/SearchBar/LogoSection.tsx @@ -0,0 +1,30 @@ +import { useState, useRef } from 'react'; +import Navbar from 'react-bootstrap/Navbar'; +import { ServerInfo } from '../../gql/graphql.ts'; +import { ServerInfoTooltip } from './ServerInfoTooltip.tsx'; +import logo from '../../static/img/otp-logo.svg'; + +type LogoSectionProps = { + serverInfo?: ServerInfo; +}; + +export function LogoSection({ serverInfo }: LogoSectionProps) { + const [showServerInfo, setShowServerInfo] = useState(false); + const target = useRef(null); + + return ( +
+ setShowServerInfo((v) => !v)}> +
+ + OTP Debug + {showServerInfo && } +
+
+
+
Version: {serverInfo?.version}
+
Time zone: {serverInfo?.internalTransitModelTimeZone}
+
+
+ ); +} diff --git a/client/src/components/SearchBar/NumTripPatternsInput.tsx b/client/src/components/SearchBar/NumTripPatternsInput.tsx index 360ce1c2c73..ae33e2f4e19 100644 --- a/client/src/components/SearchBar/NumTripPatternsInput.tsx +++ b/client/src/components/SearchBar/NumTripPatternsInput.tsx @@ -11,7 +11,7 @@ export function NumTripPatternsInput({ return ( - Num. results + # setTripQueryVariables({ diff --git a/client/src/components/SearchInput/ArgumentTooltip.tsx b/client/src/components/SearchInput/ArgumentTooltip.tsx new file mode 100644 index 00000000000..efb7a11dc19 --- /dev/null +++ b/client/src/components/SearchInput/ArgumentTooltip.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import infoIcon from '../../static/img/help-info-solid.svg'; +import inputIcon from '../../static/img/input.svg'; +import durationIcon from '../../static/img/lap-timer.svg'; +import { ResolvedType } from './useTripArgs.ts'; + +interface ArgumentTooltipProps { + defaultValue?: string | number | boolean | object | null | undefined; + type?: ResolvedType; +} + +const ArgumentTooltip: React.FC = ({ defaultValue, type }) => { + return ( + + {defaultValue !== undefined && defaultValue !== null && ( + + {'Info'} + + )} + {type !== undefined && type !== null && type.subtype === 'DoubleFunction' && ( + + {'Info'} + + )} + {type !== undefined && type !== null && type.subtype === 'Duration' && ( + + {'Info'} + + )} + + ); +}; + +export default ArgumentTooltip; diff --git a/client/src/components/SearchInput/ResetButton.tsx b/client/src/components/SearchInput/ResetButton.tsx new file mode 100644 index 00000000000..42e9d9e3d6b --- /dev/null +++ b/client/src/components/SearchInput/ResetButton.tsx @@ -0,0 +1,34 @@ +import { TripQueryVariables } from '../../gql/graphql.ts'; +import { excludedArguments } from './excluded-arguments.ts'; +import { getNestedValue, setNestedValue } from './nestedUtils.tsx'; +import React from 'react'; + +interface ResetButtonProps { + tripQueryVariables: TripQueryVariables; + setTripQueryVariables: (tripQueryVariables: TripQueryVariables) => void; +} + +const ResetButton: React.FC = ({ tripQueryVariables, setTripQueryVariables }) => { + function handleReset(): void { + // Start with an empty object (or partially typed) + let newVars: TripQueryVariables = {} as TripQueryVariables; + + // For each path in our excluded set, copy over that value (if any) + excludedArguments.forEach((excludedPath) => { + const value = getNestedValue(tripQueryVariables, excludedPath); + if (value !== undefined) { + newVars = setNestedValue(newVars, excludedPath, value) as TripQueryVariables; + } + }); + + setTripQueryVariables(newVars); + } + + return ( + + ); +}; + +export default ResetButton; diff --git a/client/src/components/SearchInput/Sidebar.tsx b/client/src/components/SearchInput/Sidebar.tsx new file mode 100644 index 00000000000..b362ad4720c --- /dev/null +++ b/client/src/components/SearchInput/Sidebar.tsx @@ -0,0 +1,53 @@ +import React, { useState, ReactNode } from 'react'; +import tripIcon from '../../static/img/route.svg'; +import filterIcon from '../../static/img/filter.svg'; +import jsonIcon from '../../static/img/json.svg'; + +interface SidebarProps { + children: ReactNode | ReactNode[]; +} + +const Sidebar: React.FC = ({ children }) => { + const [activeIndex, setActiveIndex] = useState(0); + + // Function to return the appropriate image based on the index + const getIconForIndex = (index: number) => { + switch (index) { + case 0: + return Itineray list; + case 1: + return Filters; + case 2: + return Filters; + default: + return null; + } + }; + + // Ensure children is always an array and filter out invalid children (null, undefined) + const childArray = React.Children.toArray(children).filter((child) => React.isValidElement(child)); + + return ( +
+ {/* Sidebar Navigation Buttons */} +
+ {childArray.map((_, index) => ( +
setActiveIndex(index)} + > + {getIconForIndex(index)} +
+ ))} +
+ + {/* Content Area */} +
+ {childArray.map((child, index) => (index === activeIndex ?
{child}
: null))} +
+
+ ); +}; + +export default Sidebar; diff --git a/client/src/components/SearchInput/TripArguments.ts b/client/src/components/SearchInput/TripArguments.ts new file mode 100644 index 00000000000..fbf31b5cbf8 --- /dev/null +++ b/client/src/components/SearchInput/TripArguments.ts @@ -0,0 +1,20 @@ +export interface TripArguments { + trip: { + arguments: { + [key: string]: Argument; + }; + }; +} + +export interface Argument { + type: TypeDescriptor; + defaultValue?: string; +} + +export type TypeDescriptor = ScalarType | NestedObject; + +export type ScalarType = 'ID' | 'String' | 'Int' | 'Float' | 'Boolean' | 'DateTime' | 'Duration'; + +export interface NestedObject { + [key: string]: Argument | string[]; // Allows for nested objects or enum arrays +} diff --git a/client/src/components/SearchInput/TripQueryArguments.tsx b/client/src/components/SearchInput/TripQueryArguments.tsx new file mode 100644 index 00000000000..3abcc19edc2 --- /dev/null +++ b/client/src/components/SearchInput/TripQueryArguments.tsx @@ -0,0 +1,409 @@ +import React, { JSX, useEffect, useState } from 'react'; +import { useTripSchema } from './useTripSchema.ts'; +import { TripQueryVariables } from '../../gql/graphql'; +import { getNestedValue, setNestedValue } from './nestedUtils'; +import ArgumentTooltip from './ArgumentTooltip.tsx'; +import { excludedArguments } from './excluded-arguments.ts'; +import { ResolvedType } from './useTripArgs.ts'; +import ResetButton from './ResetButton.tsx'; +import { DefaultValue, extractAllArgs, formatArgumentName, ProcessedArgument } from './extractArgs.ts'; + +interface TripQueryArgumentsProps { + tripQueryVariables: TripQueryVariables; + setTripQueryVariables: (tripQueryVariables: TripQueryVariables) => void; +} + +const TripQueryArguments: React.FC = ({ tripQueryVariables, setTripQueryVariables }) => { + const [argumentsList, setArgumentsList] = useState([]); + const [expandedArguments, setExpandedArguments] = useState>({}); + const [searchText] = useState(''); + + const { tripArgs, loading, error } = useTripSchema(); + + useEffect(() => { + if (!tripArgs) return; // Don't run if the data isn't loaded yet + if (loading || error) return; // Optionally handle error/loading + + const extractedArgs = extractAllArgs(tripArgs.trip.arguments); + setArgumentsList(extractedArgs); + }, [tripArgs, loading, error]); + + function normalizePathForList(path: string): string { + // Replace numeric segments with `*` + return path.replace(/\.\d+/g, '.*'); + } + + function handleInputChange(path: string, value: DefaultValue | undefined): void { + const normalizedPath = normalizePathForList(path); + const argumentConfig = argumentsList.find((arg) => arg.path === normalizedPath); + + if (!argumentConfig) { + console.error(`No matching argumentConfig found for path: ${path}`); + return; + } + + // Handle comma-separated input for string arrays + if ( + argumentConfig.type.subtype != null && + ['String', 'DoubleFunction', 'ID', 'Duration'].includes(argumentConfig.type.subtype) && + argumentConfig.isList + ) { + if (typeof value === 'string') { + // Convert comma-separated string into an array + const idsArray = value.split(',').map((id) => id.trim()); + + let updatedTripQueryVariables = setNestedValue(tripQueryVariables, path, idsArray) as TripQueryVariables; + updatedTripQueryVariables = cleanUpParentIfEmpty(updatedTripQueryVariables, path); + setTripQueryVariables(updatedTripQueryVariables); + return; + } + } + + // Default handling for other cases + let updatedTripQueryVariables = setNestedValue(tripQueryVariables, path, value) as TripQueryVariables; + updatedTripQueryVariables = cleanUpParentIfEmpty(updatedTripQueryVariables, path); + setTripQueryVariables(updatedTripQueryVariables); + } + + function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); + } + + /** + * Recursively removes empty arrays/objects from `variables` based on a path. + * Returns the updated variables. + */ + function cleanUpParentIfEmpty(variables: TripQueryVariables, path: string): TripQueryVariables { + if (!path.includes('.')) { + const topValue = getNestedValue(variables, path); + + if (Array.isArray(topValue) && topValue.length === 0) { + // Create a shallow copy as a flexible object: + const copy = { ...variables } as Record; + // Remove the property: + delete copy[path]; + return copy as TripQueryVariables; + } + + // If it's a plain object and all keys are undefined/null or empty, remove it + if (isPlainObject(topValue)) { + const allKeysEmpty = Object.keys(topValue).every((key) => { + const childVal = (topValue as Record)[key]; + return childVal === undefined || childVal === null || (Array.isArray(childVal) && childVal.length === 0); + }); + + if (allKeysEmpty) { + const copy = { ...variables } as Record; + delete copy[path]; + return copy as TripQueryVariables; + } + } + + return variables; // Otherwise leave it as is + } + + // For nested paths + const pathParts = path.split('.'); + for (let i = pathParts.length - 1; i > 0; i--) { + const parentPath = pathParts.slice(0, i).join('.'); + const parentValue = getNestedValue(variables, parentPath); + + if (parentValue == null) { + // Already null or undefined, nothing to do + continue; + } + + if (Array.isArray(parentValue)) { + // If the parent array is now empty, remove it + if (parentValue.length === 0) { + variables = setNestedValue(variables, parentPath, undefined) as TripQueryVariables; + } + } else if (isPlainObject(parentValue)) { + // If all child values are null/undefined or empty, remove the parent + const allKeysEmpty = Object.keys(parentValue).every((key) => { + const childPath = `${parentPath}.${key}`; + const childValue = getNestedValue(variables, childPath); + return ( + childValue === undefined || childValue === null || (Array.isArray(childValue) && childValue.length === 0) + ); + }); + + if (allKeysEmpty) { + variables = setNestedValue(variables, parentPath, undefined) as TripQueryVariables; + } + } + } + + return variables; + } + + function toggleExpand(path: string): void { + setExpandedArguments((prev) => ({ + ...prev, + [path]: !prev[path], + })); + } + + const filteredArgumentsList = argumentsList + .filter(({ path }) => formatArgumentName(path).toLowerCase().includes(searchText.toLowerCase())) + .filter(({ path }) => !excludedArguments.has(path)); + + /** + * Renders multiple InputObjects within an array. Each item in the array + * is shown with an expand/collapse toggle and a remove button. + */ + function renderListOfInputObjects( + listPath: string, + allArgs: ProcessedArgument[], + level: number, + type: ResolvedType, + ): React.JSX.Element { + // We assume getNestedValue returns unknown; cast to an array if needed + const arrayVal = (getNestedValue(tripQueryVariables, listPath) ?? []) as unknown[]; + + // You can customize this if you have a better naming scheme + const typeName = type.name; + + return ( +
+ {arrayVal.map((_, index) => { + const itemPath = `${listPath}.${index}`; + + // Replace the `.*` placeholder with the actual index + const itemNestedArgs = allArgs + .filter((arg) => arg.path.startsWith(`${listPath}.*.`) && arg.path !== `${listPath}.*`) + .map((arg) => ({ + ...arg, + path: arg.path.replace(`${listPath}.*`, itemPath), + })); + + const immediateNestedArgs = itemNestedArgs.filter( + (arg) => arg.path.split('.').length === itemPath.split('.').length + 1, + ); + + const isExpandedItem = expandedArguments[itemPath]; + + return ( +
+ toggleExpand(itemPath)}> + {isExpandedItem ? '▼ ' : '▶ '} [#{index + 1}] + + + + {isExpandedItem && ( +
+ {renderArgumentInputs(immediateNestedArgs, level + 1, itemNestedArgs)} +
+ )} +
+ ); + })} + +
+ ); + } + + function handleAddItem(listPath: string): void { + const currentValue = (getNestedValue(tripQueryVariables, listPath) ?? []) as unknown[]; + const newValue = [...currentValue, {}]; + const updatedTripQueryVariables = setNestedValue(tripQueryVariables, listPath, newValue) as TripQueryVariables; + setTripQueryVariables(updatedTripQueryVariables); + } + + function handleRemoveItem(listPath: string, index: number): void { + const currentValue = (getNestedValue(tripQueryVariables, listPath) ?? []) as unknown[]; + const newValue = currentValue.filter((_, i) => i !== index); + let updatedTripQueryVariables = setNestedValue(tripQueryVariables, listPath, newValue) as TripQueryVariables; + updatedTripQueryVariables = cleanUpParentIfEmpty(updatedTripQueryVariables, listPath); + setTripQueryVariables(updatedTripQueryVariables); + } + + function handleRemoveArgument(path: string): void { + let updatedTripQueryVariables = setNestedValue(tripQueryVariables, path, undefined) as TripQueryVariables; + updatedTripQueryVariables = cleanUpParentIfEmpty(updatedTripQueryVariables, path); + setTripQueryVariables(updatedTripQueryVariables); + } + + function renderArgumentInputs(args: ProcessedArgument[], level: number, allArgs: ProcessedArgument[]): JSX.Element[] { + return args.map(({ path, type, defaultValue, enumValues, isComplex, isList }) => { + const isExpanded = expandedArguments[path]; + const currentDepth = path.split('.').length; + const nestedArgs = allArgs.filter((arg) => { + const argDepth = arg.path.split('.').length; + return arg.path.startsWith(`${path}.`) && arg.path !== path && argDepth === currentDepth + 1; + }); + + const nestedLevel = level + 1; + + // Various input renderings depending on subtype + return ( +
+ {isComplex ? ( +
+ toggleExpand(path)}> + {isExpanded ? '▼ ' : '▶ '} {formatArgumentName(path)} + + {isExpanded && isList ? ( +
{renderListOfInputObjects(path, allArgs, nestedLevel, type)}
+ ) : isExpanded ? ( + renderArgumentInputs(nestedArgs, nestedLevel, allArgs) + ) : null} +
+ ) : ( +
+ + {type.subtype === 'Boolean' && + (() => { + const currentValue = getNestedValue(tripQueryVariables, path) as boolean | undefined; + const isInUse = currentValue !== undefined; + return ( + + handleInputChange(path, e.target.checked)} + /> + {isInUse && ( + handleRemoveArgument(path)} className="remove-argument"> + x + + )} + + ); + })()} + + {type.subtype != null && + ['String', 'DoubleFunction', 'ID', 'Duration'].includes(type.subtype) && + isList && ( + { + const currentValue = getNestedValue(tripQueryVariables, path); + return Array.isArray(currentValue) ? currentValue.join(', ') : ''; + })()} + onChange={(e) => handleInputChange(path, e.target.value)} + placeholder="Comma-separated list" + /> + )} + + {type.subtype != null && + ['String', 'DoubleFunction', 'ID', 'Duration'].includes(type.subtype) && + !isList && ( + handleInputChange(path, e.target.value || undefined)} + /> + )} + + {type.subtype === 'Int' && ( + { + const val = parseInt(e.target.value, 10); + handleInputChange(path, Number.isNaN(val) ? undefined : val); + }} + /> + )} + + {type.subtype === 'Float' && ( + { + const val = parseFloat(e.target.value); + handleInputChange(path, Number.isNaN(val) ? undefined : val); + }} + /> + )} + + {type.subtype === 'DateTime' && ( + { + const newValue = e.target.value ? new Date(e.target.value).toISOString() : undefined; + handleInputChange(path, newValue); + }} + /> + )} + + {type.type === 'Enum' && enumValues && isList && ( + + )} + + {type.type === 'Enum' && enumValues && !isList && ( + + )} +
+ )} +
+ ); + }); + } + + return ( +
+
+ Filters + +
+ {filteredArgumentsList.length === 0 ? ( +

No arguments found.

+ ) : ( +
+ {renderArgumentInputs( + // Top-level arguments have a path depth of 1 + filteredArgumentsList.filter((arg) => arg.path.split('.').length === 1), + 0, + filteredArgumentsList, + )} +
+ )} +
+ ); +}; + +export default TripQueryArguments; diff --git a/client/src/components/SearchInput/TripSchemaContext.tsx b/client/src/components/SearchInput/TripSchemaContext.tsx new file mode 100644 index 00000000000..f769b33855d --- /dev/null +++ b/client/src/components/SearchInput/TripSchemaContext.tsx @@ -0,0 +1,10 @@ +import { createContext } from 'react'; +import type { TripArgsRepresentation } from './useTripArgs'; + +export interface TripSchemaContextValue { + tripArgs: TripArgsRepresentation | null; + loading: boolean; + error: string | null; +} + +export const TripSchemaContext = createContext(undefined); diff --git a/client/src/components/SearchInput/TripSchemaProvider.tsx b/client/src/components/SearchInput/TripSchemaProvider.tsx new file mode 100644 index 00000000000..a8a3f9b30a8 --- /dev/null +++ b/client/src/components/SearchInput/TripSchemaProvider.tsx @@ -0,0 +1,47 @@ +import React, { useEffect, useState } from 'react'; +import { TripSchemaContext, TripSchemaContextValue } from './TripSchemaContext'; +import { fetchTripArgs, TripArgsRepresentation } from './useTripArgs'; + +interface TripSchemaProviderProps { + endpoint: string; + children: React.ReactNode; +} + +export function TripSchemaProvider({ endpoint, children }: TripSchemaProviderProps) { + const [tripArgs, setTripArgs] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let isMounted = true; + + async function loadSchema() { + setLoading(true); + setError(null); + try { + const result = await fetchTripArgs(endpoint); + if (isMounted) { + setTripArgs(result); + } + } catch (err) { + console.error('Error loading trip arguments:', err); + if (isMounted) { + setError('Failed to load trip schema'); + } + } finally { + if (isMounted) { + setLoading(false); + } + } + } + + loadSchema(); + return () => { + isMounted = false; + }; + }, [endpoint]); + + const value: TripSchemaContextValue = { tripArgs, loading, error }; + + return {children}; +} diff --git a/client/src/components/SearchInput/ViewArgumentsRaw.tsx b/client/src/components/SearchInput/ViewArgumentsRaw.tsx new file mode 100644 index 00000000000..c08fd833a65 --- /dev/null +++ b/client/src/components/SearchInput/ViewArgumentsRaw.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { TripQueryVariables } from '../../gql/graphql.ts'; +import ResetButton from './ResetButton.tsx'; + +interface ViewArgumentsRawProps { + tripQueryVariables: TripQueryVariables; + setTripQueryVariables: (tripQueryVariables: TripQueryVariables) => void; +} + +const ViewArgumentsRaw: React.FC = ({ tripQueryVariables, setTripQueryVariables }) => { + return ( +
+
+ Arguments raw + +
+ +
{JSON.stringify(tripQueryVariables, null, 2)}
+
+ ); +}; + +export default ViewArgumentsRaw; diff --git a/client/src/components/SearchInput/excluded-arguments.ts b/client/src/components/SearchInput/excluded-arguments.ts new file mode 100644 index 00000000000..bef4f1f6075 --- /dev/null +++ b/client/src/components/SearchInput/excluded-arguments.ts @@ -0,0 +1,12 @@ +export const excludedArguments = new Set([ + 'numTripPatterns', + 'arriveBy', + 'from', + 'to', + 'dateTime', + 'searchWindow', + 'modes.accessMode', + 'modes.directMode', + 'modes.egressMode', + // Add every full path you want to exclude - top level paths will remove all children! +]); diff --git a/client/src/components/SearchInput/extractArgs.ts b/client/src/components/SearchInput/extractArgs.ts new file mode 100644 index 00000000000..9bb9b6812b0 --- /dev/null +++ b/client/src/components/SearchInput/extractArgs.ts @@ -0,0 +1,123 @@ +import { ResolvedType } from './useTripArgs.ts'; + +export type DefaultValue = string | number | boolean | object | null; + +interface ArgData { + type: ResolvedType; + name?: string; + defaultValue?: DefaultValue; + enumValues?: string[]; + isComplex?: boolean; + isList?: boolean; + args?: Record; // Recursive for nested arguments +} + +export interface ProcessedArgument { + path: string; + type: ResolvedType; + name?: string; + defaultValue?: DefaultValue; + enumValues?: string[]; + isComplex?: boolean; + isList?: boolean; +} +/** + * Returns a human-readable name from a path like "someNestedArg.subArg". + */ +export function formatArgumentName(input: string): string { + if (!input) { + return ' '; + } + const parts = input.split('.'); + const formatted = parts[parts.length - 1].replace(/([A-Z])/g, ' $1').trim(); + return formatted.replace(/\b\w/g, (char) => char.toUpperCase()) + ' '; +} +/** + * Recursively extracts a flat list of arguments (ProcessedArgument[]). + */ +export function extractAllArgs( + args: Record | undefined, + parentPath: string[] = [], +): ProcessedArgument[] { + let allArgs: ProcessedArgument[] = []; + if (!args) return []; + + Object.entries(args).forEach(([argName, argData]) => { + const currentPath = [...parentPath, argName].join('.'); + allArgs = allArgs.concat(processArgument(argName, argData, currentPath, parentPath)); + }); + + return allArgs; +} + +/** + * Converts a single ArgData into one or more ProcessedArgument entries. + * If the argData is an InputObject with nested fields, we recurse. + */ +function processArgument( + argName: string, + argData: ArgData, + currentPath: string, + parentPath: string[], +): ProcessedArgument[] { + let allArgs: ProcessedArgument[] = []; + + if (typeof argData === 'object' && argData.type) { + if (argData.type.type === 'Enum') { + const enumValues = ['Not selected', ...(argData.type.values || [])]; + const defaultValue = argData.defaultValue !== undefined ? argData.defaultValue : 'Not selected'; + + allArgs.push({ + path: currentPath, + type: { type: 'Enum' }, + defaultValue, + enumValues, + isList: argData.isList, + }); + } else if (argData.type.type === 'InputObject' && argData.isList) { + // This is a list of InputObjects + allArgs.push({ + path: currentPath, + type: { type: 'Group', name: argData.type.name }, // We'll still call this 'Group' + defaultValue: argData.defaultValue, + isComplex: true, + isList: true, + }); + + allArgs = allArgs.concat(extractAllArgs(argData.type.fields, [...parentPath, `${argName}.*`])); + } else if (argData.type.type === 'InputObject') { + // Single InputObject + allArgs.push({ + path: currentPath, + type: { type: 'Group', name: argData.type.name }, + isComplex: true, + isList: false, + }); + allArgs = allArgs.concat(extractAllArgs(argData.type.fields, [...parentPath, argName])); + } else if (argData.type.type === 'Scalar') { + allArgs.push({ + path: currentPath, + type: { type: argData.type.type, subtype: argData.type.subtype }, + defaultValue: argData.defaultValue, + isList: argData.isList, + }); + } + } else if (typeof argData === 'object' && argData.type?.fields) { + // Possibly a nested object with fields + allArgs.push({ + path: currentPath, + type: { type: 'Group' }, + isComplex: true, + }); + allArgs = allArgs.concat(extractAllArgs(argData.type.fields, [...parentPath, argName])); + } else { + // Fallback case + allArgs.push({ + path: currentPath, + type: argData.type ?? (typeof argData as unknown), // <— If argData.type is missing, fallback + defaultValue: argData.defaultValue, + }); + } + + return allArgs; +} diff --git a/client/src/components/SearchInput/nestedUtils.tsx b/client/src/components/SearchInput/nestedUtils.tsx new file mode 100644 index 00000000000..cfcfcfc232d --- /dev/null +++ b/client/src/components/SearchInput/nestedUtils.tsx @@ -0,0 +1,129 @@ +/** + * Retrieves a nested value from an object or array based on a dot-separated path. + * @param obj - The object/array to traverse (can be anything). + * @param path - The dot-separated path string (e.g. "myList.0.fieldName"). + * @returns The value at the specified path or undefined if not found. + */ +export function getNestedValue(obj: unknown, path: string): unknown { + return path.split('.').reduce((acc, key) => { + if (acc == null) { + return undefined; + } + + if (Array.isArray(acc)) { + // If the current accumulator is an array, parse key as a numeric index + const idx = Number(key); + if (Number.isNaN(idx)) return undefined; // mismatch (path wanted array index but got non-numeric) + return acc[idx]; + } else if (typeof acc === 'object') { + // treat it like a dictionary + const record = acc as Record; + return record[key]; + } + // If acc is neither object nor array, we can't go deeper + return undefined; + }, obj); +} + +/** + * Sets a nested value in an object (or array) based on a dot-separated path, + * returning a new top-level object/array to ensure immutability. + * + * This version detects numeric path segments (like "0", "1") and uses arrays + * at those levels. Non-numeric segments use objects. If there's a mismatch, + * it will convert that level to the correct type. + * + * @param obj - The original object/array (could be anything). + * @param path - The dot-separated path (e.g. "myList.0.fieldName"). + * @param value - The value to set at that path. + * @returns A new object or array with the updated value. + */ +export function setNestedValue(obj: unknown, path: string, value: unknown): unknown { + const keys = path.split('.'); + + /** + * Recursively traverse `current` based on the path segments. + * At each level, create a shallow clone of the array/object + * and update the correct child. + */ + function cloneAndSet(current: unknown, index: number): unknown { + const key = keys[index]; + const isNumeric = !isNaN(Number(key)); + + // Base case: if we're at the final segment, just return `value`. + if (index === keys.length - 1) { + // If current is an array and key is numeric, place `value` at that index + if (Array.isArray(current) && isNumeric) { + const newArray = [...current]; + newArray[Number(key)] = value; + return newArray; + } + // If current is an object (Record) and key is non-numeric, place `value` in that object + if (isObject(current) && !isNumeric) { + return { ...current, [key]: value }; + } + // Otherwise there's a type mismatch, so we convert: + if (isNumeric) { + // We expected an array + const arr = Array.isArray(current) ? [...current] : []; + arr[Number(key)] = value; + return arr; + } else { + // We expected an object + const base = isObject(current) ? current : {}; + return { + ...base, + [key]: value, + }; + } + } + + // Not at the final segment => recurse deeper + const nextIndex = index + 1; + const nextKey = keys[nextIndex]; + const nextIsNumeric = !isNaN(Number(nextKey)); + + if (Array.isArray(current) && isNumeric) { + // current is an array, and we have a numeric key + const newArray = [...current]; + const childVal = current[Number(key)]; + newArray[Number(key)] = cloneAndSet(childVal !== undefined ? childVal : nextIsNumeric ? [] : {}, nextIndex); + return newArray; + } else if (isObject(current) && !isNumeric) { + // current is an object (Record), and we have a string key + const newObj = { ...current }; + const childVal = (current as Record)[key]; + newObj[key] = cloneAndSet(childVal !== undefined ? childVal : nextIsNumeric ? [] : {}, nextIndex); + return newObj; + } else { + // There's a mismatch at this level + // e.g. current is an object but key is numeric => we want an array, or vice versa. + if (isNumeric) { + // create a new array at this level + const arr: unknown[] = []; + arr[Number(key)] = cloneAndSet(nextIsNumeric ? [] : {}, nextIndex); + return arr; + } else { + // create a new object at this level + return { + [key]: cloneAndSet(nextIsNumeric ? [] : {}, nextIndex), + }; + } + } + } + + // If the root `obj` is undefined or null, base it on the first key + if (obj == null) { + const firstKeyIsNumeric = !isNaN(Number(keys[0])); + obj = firstKeyIsNumeric ? [] : {}; + } + + return cloneAndSet(obj, 0); +} + +/** + * A small helper type-guard to check if `value` is a non-null object (but not an array). + */ +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/client/src/components/SearchInput/useTripArgs.ts b/client/src/components/SearchInput/useTripArgs.ts new file mode 100644 index 00000000000..8f41f9f78d8 --- /dev/null +++ b/client/src/components/SearchInput/useTripArgs.ts @@ -0,0 +1,174 @@ +import { + buildClientSchema, + getIntrospectionQuery, + GraphQLSchema, + GraphQLType, + GraphQLNamedType, + isNonNullType, + isListType, + isScalarType, + isEnumType, + isInputObjectType, +} from 'graphql'; + +// +// Types +// +export interface ResolvedType { + type: 'Scalar' | 'Enum' | 'InputObject' | 'Group'; + // For scalars or fallback, e.g. "String", "Int", etc. + subtype?: string; + // For input objects + name?: string; + fields?: { + [fieldName: string]: { + type: ResolvedType; + defaultValue?: string | number | boolean | object | null; // Updated type + isList: boolean; + }; + }; + // For enums + values?: string[]; +} + +export interface ArgumentRepresentation { + [argName: string]: { + type: ResolvedType; + defaultValue?: string | number | boolean | object | null; // Updated type + isList: boolean; + }; +} + +export interface TripArgsRepresentation { + trip: { + arguments: ArgumentRepresentation; + }; +} + +/** + * Repeatedly unwraps NonNull and List wrappers until we get a named type. + */ +function getNamedType(type: GraphQLType): GraphQLNamedType { + let current: GraphQLType = type; + + while (true) { + if (isNonNullType(current)) { + current = current.ofType; + } else if (isListType(current)) { + current = current.ofType; + } else { + break; + } + } + + // At this point, current should be a GraphQLNamedType + return current as GraphQLNamedType; +} + +function resolveType(type: GraphQLType): ResolvedType { + const namedType = getNamedType(type); + + if (isScalarType(namedType)) { + return { type: 'Scalar', subtype: namedType.name }; + } + + if (isEnumType(namedType)) { + return { + type: 'Enum', + values: namedType.getValues().map((val) => val.name), + }; + } + + if (isInputObjectType(namedType)) { + const fields = namedType.getFields(); + const fieldTypes: Record< + string, + { type: ResolvedType; defaultValue?: string | number | boolean | object | null; isList: boolean } // Updated type + > = {}; + + for (const fieldName of Object.keys(fields)) { + const field = fields[fieldName]; + + // Exclude deprecated fields + if (field.deprecationReason) { + continue; + } + + const isList = isListType(field.type); + const defaultValue = field.defaultValue !== undefined ? field.defaultValue : null; + + fieldTypes[fieldName] = { + type: resolveType(field.type), + defaultValue: defaultValue, + isList, + }; + } + + return { + type: 'InputObject', + name: namedType.name, + fields: fieldTypes, + }; + } + + return { type: 'Scalar', subtype: 'String' }; +} + +function generateTripArgs(schema: GraphQLSchema): TripArgsRepresentation { + const queryType = schema.getQueryType(); + if (!queryType) { + throw new Error('No Query type found in the schema.'); + } + + const tripField = queryType.getFields()['trip']; + if (!tripField) { + throw new Error('No trip query found in the schema.'); + } + + const argsJson: ArgumentRepresentation = {}; + + tripField.args.forEach((arg) => { + if (arg.deprecationReason) { + // Skip deprecated arguments + return; + } + + const argName = arg.name; + const argType = resolveType(arg.type); + const argDefaultValue = arg.defaultValue !== null ? arg.defaultValue : null; + const isList = isListType(arg.type); + + argsJson[argName] = { + type: argType, + ...(argDefaultValue !== null && { defaultValue: argDefaultValue }), + isList, + }; + }); + + return { + trip: { + arguments: argsJson, + }, + }; +} + +//Fetch the remote GraphQL schema via introspection +export async function fetchTripArgs(graphqlEndpointUrl: string): Promise { + const introspectionQuery = getIntrospectionQuery(); + + const response = await fetch(graphqlEndpointUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: introspectionQuery }), + }); + + if (!response.ok) { + throw new Error(`Failed to fetch schema. HTTP error: ${response.status}`); + } + + const { data } = await response.json(); + + const schema = buildClientSchema(data); + + return generateTripArgs(schema); +} diff --git a/client/src/components/SearchInput/useTripSchema.ts b/client/src/components/SearchInput/useTripSchema.ts new file mode 100644 index 00000000000..b7cc210026a --- /dev/null +++ b/client/src/components/SearchInput/useTripSchema.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import { TripSchemaContext } from './TripSchemaContext'; + +export function useTripSchema() { + const context = useContext(TripSchemaContext); + if (!context) { + throw new Error('useTripSchema must be used within a TripSchemaProvider'); + } + return context; +} diff --git a/client/src/screens/App.tsx b/client/src/screens/App.tsx index 1b6b86b7a81..38cac431fb0 100644 --- a/client/src/screens/App.tsx +++ b/client/src/screens/App.tsx @@ -1,12 +1,17 @@ -import { Stack } from 'react-bootstrap'; import { MapView } from '../components/MapView/MapView.tsx'; -import { SearchBar } from '../components/SearchBar/SearchBar.tsx'; import { ItineraryListContainer } from '../components/ItineraryList/ItineraryListContainer.tsx'; import { useState } from 'react'; import { useTripQuery } from '../hooks/useTripQuery.ts'; import { useServerInfo } from '../hooks/useServerInfo.ts'; import { useTripQueryVariables } from '../hooks/useTripQueryVariables.ts'; import { TimeZoneContext } from '../hooks/TimeZoneContext.ts'; +import { LogoSection } from '../components/SearchBar/LogoSection.tsx'; +import { InputFieldsSection } from '../components/SearchBar/InputFieldsSection.tsx'; +import TripQueryArguments from '../components/SearchInput/TripQueryArguments.tsx'; +import Sidebar from '../components/SearchInput/Sidebar.tsx'; +import ViewArgumentsRaw from '../components/SearchInput/ViewArgumentsRaw.tsx'; +import { TripSchemaProvider } from '../components/SearchInput/TripSchemaProvider.tsx'; +import { getApiUrl } from '../util/getApiUrl.ts'; export function App() { const serverInfo = useServerInfo(); @@ -18,30 +23,49 @@ export function App() { return (
- - - - - - +
+
+ +
+
+ +
+
+ + + + + + + +
+
+ +
+
); diff --git a/client/src/static/img/code.svg b/client/src/static/img/code.svg new file mode 100644 index 00000000000..d303b8d18b5 --- /dev/null +++ b/client/src/static/img/code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/static/img/data-visualization.svg b/client/src/static/img/data-visualization.svg new file mode 100644 index 00000000000..043b9ee35a4 --- /dev/null +++ b/client/src/static/img/data-visualization.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/static/img/debug-layer.svg b/client/src/static/img/debug-layer.svg new file mode 100644 index 00000000000..ac614e639dc --- /dev/null +++ b/client/src/static/img/debug-layer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/static/img/filter.svg b/client/src/static/img/filter.svg new file mode 100644 index 00000000000..cbda5f955d5 --- /dev/null +++ b/client/src/static/img/filter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/static/img/graph.svg b/client/src/static/img/graph.svg new file mode 100644 index 00000000000..6eef9e5100a --- /dev/null +++ b/client/src/static/img/graph.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/static/img/graphic.svg b/client/src/static/img/graphic.svg new file mode 100644 index 00000000000..344e8f9d5d5 --- /dev/null +++ b/client/src/static/img/graphic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/static/img/help-info-solid.svg b/client/src/static/img/help-info-solid.svg new file mode 100644 index 00000000000..bd87cd69731 --- /dev/null +++ b/client/src/static/img/help-info-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/static/img/info-circle.svg b/client/src/static/img/info-circle.svg new file mode 100644 index 00000000000..0689c0044ec --- /dev/null +++ b/client/src/static/img/info-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/static/img/input.svg b/client/src/static/img/input.svg new file mode 100644 index 00000000000..4ed4605b2c6 --- /dev/null +++ b/client/src/static/img/input.svg @@ -0,0 +1,8 @@ + + + diff --git a/client/src/static/img/json.svg b/client/src/static/img/json.svg new file mode 100644 index 00000000000..a92f3eec55b --- /dev/null +++ b/client/src/static/img/json.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/static/img/lap-timer.svg b/client/src/static/img/lap-timer.svg new file mode 100644 index 00000000000..1de0b3be6ce --- /dev/null +++ b/client/src/static/img/lap-timer.svg @@ -0,0 +1,8 @@ + + + diff --git a/client/src/static/img/route.svg b/client/src/static/img/route.svg new file mode 100644 index 00000000000..6699f08361b --- /dev/null +++ b/client/src/static/img/route.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/client/src/static/query/selector.fragment.graphql b/client/src/static/query/selector.fragment.graphql new file mode 100644 index 00000000000..6f3bc847ee7 --- /dev/null +++ b/client/src/static/query/selector.fragment.graphql @@ -0,0 +1,63 @@ +{ + previousPageCursor + nextPageCursor + tripPatterns { + aimedStartTime + aimedEndTime + expectedEndTime + expectedStartTime + duration + distance + legs { + id + mode + aimedStartTime + aimedEndTime + expectedEndTime + expectedStartTime + realtime + distance + duration + fromPlace { + name + quay { + id + } + } + toPlace { + name + quay { + id + } + } + toEstimatedCall { + destinationDisplay { + frontText + } + } + line { + publicCode + name + id + presentation { + colour + } + } + authority { + name + id + } + pointsOnLink { + points + } + interchangeTo { + staySeated + } + interchangeFrom { + staySeated + } + } + systemNotices { + tag + } + } \ No newline at end of file diff --git a/client/src/static/query/tripQuery.tsx b/client/src/static/query/tripQuery.tsx index f435c56e4d6..14c5ed2ec26 100644 --- a/client/src/static/query/tripQuery.tsx +++ b/client/src/static/query/tripQuery.tsx @@ -1,97 +1,155 @@ import { graphql } from '../../gql'; import { print } from 'graphql/index'; +// Generated trip query based on schema.graphql + export const query = graphql(` - query trip( - $from: Location! - $to: Location! - $arriveBy: Boolean - $dateTime: DateTime - $numTripPatterns: Int - $searchWindow: Int - $modes: Modes - $itineraryFiltersDebug: ItineraryFilterDebugProfile - $wheelchairAccessible: Boolean - $pageCursor: String - ) { - trip( - from: $from - to: $to - arriveBy: $arriveBy - dateTime: $dateTime - numTripPatterns: $numTripPatterns - searchWindow: $searchWindow - modes: $modes - itineraryFilters: { debug: $itineraryFiltersDebug } - wheelchairAccessible: $wheelchairAccessible - pageCursor: $pageCursor - ) { - previousPageCursor - nextPageCursor - tripPatterns { +query trip( + $accessEgressPenalty: [PenaltyForStreetMode!] + $alightSlackDefault: Int + $alightSlackList: [TransportModeSlack] + $arriveBy: Boolean + $banned: InputBanned + $bicycleOptimisationMethod: BicycleOptimisationMethod + $bikeSpeed: Float + $boardSlackDefault: Int + $boardSlackList: [TransportModeSlack] + $bookingTime: DateTime + $dateTime: DateTime + $filters: [TripFilterInput!] + $from: Location! + $ignoreRealtimeUpdates: Boolean + $includePlannedCancellations: Boolean + $includeRealtimeCancellations: Boolean + $itineraryFilters: ItineraryFilters + $locale: Locale + $maxAccessEgressDurationForMode: [StreetModeDurationInput!] + $maxDirectDurationForMode: [StreetModeDurationInput!] + $maximumAdditionalTransfers: Int + $maximumTransfers: Int + $modes: Modes + $numTripPatterns: Int + $pageCursor: String + $relaxTransitGroupPriority: RelaxCostInput + $searchWindow: Int + $timetableView: Boolean + $to: Location! + $transferPenalty: Int + $transferSlack: Int + $triangleFactors: TriangleFactors + $useBikeRentalAvailabilityInformation: Boolean + $via: [TripViaLocationInput!] + $waitReluctance: Float + $walkReluctance: Float + $walkSpeed: Float + $wheelchairAccessible: Boolean + $whiteListed: InputWhiteListed +) { + trip( + accessEgressPenalty: $accessEgressPenalty + alightSlackDefault: $alightSlackDefault + alightSlackList: $alightSlackList + arriveBy: $arriveBy + banned: $banned + bicycleOptimisationMethod: $bicycleOptimisationMethod + bikeSpeed: $bikeSpeed + boardSlackDefault: $boardSlackDefault + boardSlackList: $boardSlackList + bookingTime: $bookingTime + dateTime: $dateTime + filters: $filters + from: $from + ignoreRealtimeUpdates: $ignoreRealtimeUpdates + includePlannedCancellations: $includePlannedCancellations + includeRealtimeCancellations: $includeRealtimeCancellations + itineraryFilters: $itineraryFilters + locale: $locale + maxAccessEgressDurationForMode: $maxAccessEgressDurationForMode + maxDirectDurationForMode: $maxDirectDurationForMode + maximumAdditionalTransfers: $maximumAdditionalTransfers + maximumTransfers: $maximumTransfers + modes: $modes + numTripPatterns: $numTripPatterns + pageCursor: $pageCursor + relaxTransitGroupPriority: $relaxTransitGroupPriority + searchWindow: $searchWindow + timetableView: $timetableView + to: $to + transferPenalty: $transferPenalty + transferSlack: $transferSlack + triangleFactors: $triangleFactors + useBikeRentalAvailabilityInformation: $useBikeRentalAvailabilityInformation + via: $via + waitReluctance: $waitReluctance + walkReluctance: $walkReluctance + walkSpeed: $walkSpeed + wheelchairAccessible: $wheelchairAccessible + whiteListed: $whiteListed + ) + { + previousPageCursor + nextPageCursor + tripPatterns { aimedStartTime aimedEndTime expectedEndTime expectedStartTime duration distance - generalizedCost legs { - id - mode - aimedStartTime - aimedEndTime - expectedEndTime - expectedStartTime - realtime - distance - duration - generalizedCost - fromPlace { - name - quay { - id + id + mode + aimedStartTime + aimedEndTime + expectedEndTime + expectedStartTime + realtime + distance + duration + fromPlace { + name + quay { + id + } } - } - toPlace { - name - quay { - id + toPlace { + name + quay { + id + } } - } - toEstimatedCall { - destinationDisplay { - frontText + toEstimatedCall { + destinationDisplay { + frontText + } } - } - line { - publicCode - name - id - presentation { - colour + line { + publicCode + name + id + presentation { + colour + } + } + authority { + name + id + } + pointsOnLink { + points + } + interchangeTo { + staySeated + } + interchangeFrom { + staySeated } - } - authority { - name - id - } - pointsOnLink { - points - } - interchangeTo { - staySeated - } - interchangeFrom { - staySeated - } } systemNotices { - tag + tag } - } } } -`); +}`); -export const queryAsString = print(query); +export const queryAsString = print(query); \ No newline at end of file diff --git a/client/src/style.css b/client/src/style.css index eb5cbadf93b..a3f8946b3ec 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -1,39 +1,43 @@ -.app { - min-width: 810px; +.layout { + display: grid; + grid-template-columns: 1fr 3fr; + grid-template-rows: 1fr 2fr; + height: 100vh; + gap: 0; } + +.box { + display: flex; + justify-content: center; + align-items: center; +} + .navbar-brand { color: #4078bc; - margin-top: 20px; - margin-right: 14px; + font-size: 2rem; } @media (min-width: 1895px) { - .top-content { - height: 75px; - } - .below-content { - height: calc(100vh - 75px); + height: calc(100vh - 175px); } } @media (max-width: 1896px) { - .top-content { - height: 150px; - } - .below-content { - height: calc(100vh - 150px); + height: calc(100vh - 175px); } } -@media (max-width: 1120px) { - .top-content { - height: 200px; +@media (max-width: 1250px) { + .below-content { + height: calc(100vh - 250px); } +} +@media (max-width: 900px) { .below-content { - height: calc(100vh - 200px); + height: calc(100vh - 325px); } } @@ -50,6 +54,10 @@ margin-right: 1rem; } +.search-bar input.input-tiny { + max-width: 50px; +} + .search-bar input.input-small { max-width: 100px; } @@ -73,16 +81,40 @@ margin: 30px 0 auto 0; } -.search-bar .swap-from-to img { +.input-family { + display: flex; + align-items: center; + gap: 2px; +} + +.swap-from-to img { width: 15px; } -.itinerary-list-container { - width: 36rem; +.logo-container { + display: flex; + flex-direction: column; +} + +.logo-container .details { + font-size: 0.8rem; + color: #666; + margin-top: 4px; + text-align: left; +} + +.logo-image { + margin-right: 2px; +} + +.left-pane-container { + font-size: 12px; + width: 100%; overflow-y: auto; + min-width: 300px; } -.itinerary-list-container .time-zone-info { +.left-pane-container .time-zone-info { margin: 10px 20px; font-size: 12px; text-align: right; @@ -207,3 +239,208 @@ .maplibregl-ctrl-group.layer-select div.layer { margin-left: 17px; } + +.right-menu-container { + position: absolute; + top: 0; + right: 0; + width: 0; + height: 100%; + background-color: #f4f4f4; + overflow-x: hidden; + transition: 0.3s; + padding-top: 60px; + box-shadow: none; +} + +.right-menu-container.open { + width: 250px; + box-shadow: -2px 0 5px rgba(0, 0, 0, 0.2); +} + +.sidebar-button.right { + position: absolute; + right: 0; /* Default position when sidebar is closed */ + background: #fff; + color: white; + border: none; + border-radius: 4px; + padding: 10px; + cursor: pointer; + transition: + right 0.3s, + background-color 0.2s; /* Smooth transitions */ +} + +.sidebar-button.right.open { + right: 270px; /* Shifted position when sidebar is open */ +} + +.sidebar-button.active { + background: #fff; +} + +.sidebar-button:hover { + background: #4078bc; /* Slightly darker when hovered */ +} + +.sidebar-button.active:hover { + background: #fff; +} + +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* For Firefox */ +input[type='number'] { + -moz-appearance: textfield; +} + +.default-tooltip-container { + position: relative; + cursor: pointer; +} + +.pagination-controls { + margin-top: 10px; + margin-bottom: 5px; +} + +.default-tooltip-icon { + width: 10px; + height: 10px; +} +.argument-label { + padding-right: 2px; +} + +/* Sidebar Container */ +.sidebar-container { + display: flex; + width: 100%; + height: 100%; + border-top: black 1px solid; +} + +/* Sidebar Navigation */ +.sidebar { + display: flex; + flex-direction: column; + align-items: center; + width: 40px; + background-color: #f7f7f7; + border-right: 1px solid #ccc; +} + +/* Sidebar Buttons */ +.sidebar-button { + cursor: pointer; + padding: 5px; + text-align: center; + border-radius: 8px; + margin: 5px 0; + background-color: transparent; + transition: background-color 0.3s ease; +} + +.sidebar-button:hover { + background-color: #e0e0e0; +} + +.sidebar-button.active { + background-color: #ddd; + font-weight: bold; +} + +/* Content Area */ +.sidebar-content { + flex: 1; + overflow-y: auto; + margin: 5px; +} + +.panel-header { + font-size: 24px; + text-align: center; + position: relative; + margin-bottom: 10px; +} + +.argument-list { + font-size: 12px; + line-height: 1; +} + +.argument-list button { + font-size: 13px; + padding: 5px 10px; + margin: 5px 0; + background-color: #007bff; + color: white; + border: none; + border-radius: 3px; + cursor: pointer; +} + +.argument-list button:hover { + background-color: #0056b3; /* Darker on hover */ +} + +.argument-list input[type='text'], +.argument-list input[type='number'], +.argument-list input[type='datetime-local'], +select { + font-size: 12px; + padding: 0; + margin: 0; + border: none; + border-bottom: 1px solid #ccc; /* Bottom border only */ + background: none; + box-sizing: border-box; +} + +.argument-list input[type='text'], +.argument-list input[type='number'] { + width: 50px; +} +.argument-list input[type='datetime-local'] { + width: 140px; +} + +input.comma-separated-input[type='text'], +input.comma-separated-input[type='number'] { + width: 140px; +} + +.remove-argument { + margin-left: 2px; + color: red; + cursor: pointer; +} + +.reset-button { + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + /* The transform ensures the button is vertically centered + if your header has a fixed height or if text is multiline. */ +} + +.panel-header button { + font-size: 13px; + padding: 5px 10px; + margin: 5px 0; + background-color: #007bff; + color: white; + border: none; + border-radius: 3px; + cursor: pointer; +} + +.panel-header button:hover { + background-color: #0056b3; /* Darker on hover */ +} diff --git a/client/src/util/generate-arguments.cjs b/client/src/util/generate-arguments.cjs new file mode 100644 index 00000000000..d2ff4b639b7 --- /dev/null +++ b/client/src/util/generate-arguments.cjs @@ -0,0 +1,130 @@ +const { + isScalarType, + isInputObjectType, + isNonNullType, + isListType, + isEnumType, +} = require('graphql'); + +/** + * Utility function to resolve the named type (unwrapping NonNull and List types) + */ +function getNamedType(type) { + let namedType = type; + while (isNonNullType(namedType) || isListType(namedType)) { + namedType = namedType.ofType; + } + return namedType; +} + +/** + * Recursively breaks down a GraphQL type into its primitive fields with default values + */ +function resolveType(type, schema = new Set()) { + const namedType = getNamedType(type); + + + if (isScalarType(namedType)) { + return { type: 'Scalar', subtype: namedType.name }; + } + + if (isEnumType(namedType)) { + return { type: 'Enum', values: namedType.getValues().map((val) => val.name) }; + } + + if (isInputObjectType(namedType)) { + const fields = namedType.getFields(); + const fieldTypes = {}; + + Object.keys(fields).forEach((fieldName) => { + const field = fields[fieldName]; + + // Exclude deprecated fields within input objects + if (field.deprecationReason) { + return; // Skip deprecated fields + } + + const fieldType = field.type; + const isList = isListType(fieldType); // Detect if the field is a list + const fieldDefaultValue = field.defaultValue !== undefined ? field.defaultValue : null; + + // Include defaultValue consistently, setting it to null if not defined + fieldTypes[fieldName] = { + type: resolveType(fieldType, schema), + defaultValue: fieldDefaultValue, + isList, // Explicitly indicate if it's a list + }; + }); + return { type: 'InputObject', name: namedType.name, fields: fieldTypes }; + } + + // Handle interfaces and unions if necessary + // For simplicity, treating them as strings + return { type: 'Scalar', subtype: 'String' }; +} + +/** + * Plugin to generate a JSON file with all arguments from a specified query, + * excluding deprecated arguments based on deprecationReason, + * and including their types, default values, + * and whether they support multiple selection. + */ +const generateTripArgsJsonPlugin = async (schema) => { + try { + const queryType = schema.getQueryType(); + if (!queryType) { + console.error('No Query type found in the schema.'); + return JSON.stringify({ error: 'No Query type found in the schema' }, null, 2); + } + + const tripField = queryType.getFields()['trip']; + if (!tripField) { + console.error('No trip query found in the schema.'); + return JSON.stringify({ error: 'No trip query found in the schema' }, null, 2); + } + + const args = tripField.args; + const argsJson = {}; + + args.forEach((arg) => { + if (arg.deprecationReason) { + return; // Skip deprecated arguments + } + + const argName = arg.name; + const argType = resolveType(arg.type, schema); + const argDefaultValue = arg.defaultValue !== undefined ? arg.defaultValue : null; + const isList = isListType(arg.type); // Detect if the argument is a list + + // Consistent representation for enum types + if (argDefaultValue !== null) { + argsJson[argName] = { + type: argType, + defaultValue: argDefaultValue, + isList, // Explicitly indicate if it's a list + }; + } else { + argsJson[argName] = { + type: argType, + isList, // Explicitly indicate if it's a list + }; + } + }); + + const output = { + trip: { + arguments: argsJson, + }, + }; + + // Stringify the JSON with indentation for readability + return JSON.stringify(output, null, 2); + } catch (error) { + console.error('Error generating tripArguments.json:', error); + return JSON.stringify({ error: 'Failed to generate trip arguments JSON' }, null, 2); + } +}; + +module.exports = { + plugin: generateTripArgsJsonPlugin, +}; diff --git a/client/src/util/generate-queries.cjs b/client/src/util/generate-queries.cjs new file mode 100644 index 00000000000..00366bc6a11 --- /dev/null +++ b/client/src/util/generate-queries.cjs @@ -0,0 +1,67 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Plugin to generate GraphQL queries dynamically from schema + */ +const generateQueriesPlugin = async (schema) => { + const queryType = schema.getQueryType(); + if (!queryType) { + return '// No Query type found in the schema'; + } + + // Read the content from the input file to replace "replacementContent" + const inputFilePath = path.join(__dirname, '../static/query/selector.fragment.graphql'); + let replacementContent = ''; + + try { + replacementContent = fs.readFileSync(inputFilePath, 'utf-8').trim(); + } catch (error) { + console.error(`Failed to read the input file at ${inputFilePath}`, error); + return '// Error: Failed to read the input file'; + } + + const queryFields = queryType.getFields(); + const queries = []; + + Object.keys(queryFields).forEach((fieldName) => { + if (fieldName === 'trip') { + // Only interested in the trip query + const field = queryFields[fieldName]; + + // Filter out deprecated arguments using deprecationReason - isDeprecated does not work + const validArgs = field.args.filter((arg) => !arg.deprecationReason); + + // Generate the arguments for the query with filtered arguments + const args = validArgs.map((arg) => ` $${arg.name}: ${arg.type}`).join('\n'); + + // Generate the arguments for the query variables with filtered arguments + const argsForQuery = validArgs.map((arg) => ` ${arg.name}: $${arg.name}`).join('\n'); + + const query = `import { graphql } from '../../gql'; +import { print } from 'graphql/index'; + +// Generated trip query based on schema.graphql + +export const query = graphql(\` +query ${fieldName}( +${args} +) { + ${fieldName}( +${argsForQuery} + ) + ${replacementContent} + } +}\`); + +export const queryAsString = print(query);`; + queries.push(query.trim()); // Trim unnecessary whitespace + } + }); + + return queries.join('\n\n'); // Separate queries with a blank line +}; + +module.exports = { + plugin: generateQueriesPlugin, +}; diff --git a/doc/dev/design/TimetableSnapshotManager.svg b/doc/dev/design/TimetableSnapshotManager.svg new file mode 100644 index 00000000000..156e00d0a82 --- /dev/null +++ b/doc/dev/design/TimetableSnapshotManager.svg @@ -0,0 +1,4 @@ + + + +
TimetableRepository
TimetableRepository
+ transitLayer
+ transitLayer
+ realtimeTransitLayer
+ realtimeTransitLayer
+ timetableIndex
+ timetableIndex
+ siteRepository
+ siteRepository
+ patterns
+ patterns
+ trips
+ trips
+ ...
+ ...
SiriTTSnapshotSource
SiriTTSnapshotSource
+ snapshotManager
+ snapshotManager
+ transitService
+ transitService
TimetableSnapshotSource
TimetableSnapshotSource
+ snapshotManager
+ snapshotManager
+ transitService
+ transitService
SnapshotManager
SnapshotManager
+ timetableSnapshot
+ timetableSnapshot
+ params
+ params
+ transitLayerUpdater
+ transitLayerUpdater
TimetableSnapshot
TimetableSnapshot
+ addedTripPatterns
+ addedTripPatterns
+ modifiedTripPatterns
+ modifiedTripPatterns
+ ...
+ ...
TransitLayerUpdater
TransitLayerUpdater
+ timetableRepository
+ timetableRepository
TimetableSnapshotSourceParams
TimetableSnapshotSourceParams
+ maxSnapshotFrequency
+ maxSnapshotFrequency
+ purgeExpiredData
+ purgeExpiredData
TransitLayer
TransitLayer
+ tripPatternsRunningOnDate
+ tripPatternsRunningOnDate
+ ...
+ ...
<<RequestScope>>
TransitService
<<RequestScope>>...
+ timetableSnapshot
+ timetableSnapshot
+ timetableRepository
+ timetableRepository
DaggerContext
DaggerContext
+ timetableSnapshotManager
+ timetableSnapshotManager
+ transitLayerUpdater
+ transitLayerUpdater
+ ...
+ ...
1
1
1
1
Cyclic dependency!

TransitLayer needs a TimetableRepository to be constructed but doesn't keep a reference to it.

Will be resolved later.
Cyclic dependency!...
TransitService is mostly request scoped 

However, the realtime updates receive a special long-lived TransitService that has a reference to the snapshot buffer that contains unpublished timetables.
TransitService is mostly...
GraphWriter
Thread
GraphWriter...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/doc/templates/DebugUiConfiguration.md b/doc/templates/DebugUiConfiguration.md index a54b1db8d2a..51491b36cef 100644 --- a/doc/templates/DebugUiConfiguration.md +++ b/doc/templates/DebugUiConfiguration.md @@ -8,7 +8,7 @@ # Debug UI configuration The Debug UI is the standard interface that is bundled with OTP and available by visiting -[`http://localhost:8080`](http://localhost:8080). This page list the configuration options available +[`http://localhost:8080`](http://localhost:8080). This page lists the configuration options available by placing a file `debug-ui-config.json` into OTP's working directory. diff --git a/doc/user/BuildConfiguration.md b/doc/user/BuildConfiguration.md index c5fdfa8095b..1ec7a7e4c7c 100644 --- a/doc/user/BuildConfiguration.md +++ b/doc/user/BuildConfiguration.md @@ -17,105 +17,114 @@ Sections follow that describe particular settings in more depth. -| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | -|--------------------------------------------------------------------------|:------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------:|-----------------------------------|:-----:| -| [areaVisibility](#areaVisibility) | `boolean` | Perform visibility calculations. | *Optional* | `false` | 1.5 | -| [buildReportDir](#buildReportDir) | `uri` | URI to the directory where the graph build report should be written to. | *Optional* | | 2.0 | -| [configVersion](#configVersion) | `string` | Deployment version of the *build-config.json*. | *Optional* | | 2.1 | -| [dataImportReport](#dataImportReport) | `boolean` | Generate nice HTML report of Graph errors/warnings | *Optional* | `false` | 2.0 | -| [distanceBetweenElevationSamples](#distanceBetweenElevationSamples) | `double` | The distance between elevation samples in meters. | *Optional* | `10.0` | 2.0 | -| embedRouterConfig | `boolean` | Embed the Router config in the graph, which allows it to be sent to a server fully configured over the wire. | *Optional* | `true` | 2.0 | -| [graph](#graph) | `uri` | URI to the graph object file for reading and writing. | *Optional* | | 2.0 | -| [gsCredentials](#gsCredentials) | `string` | Local file system path to Google Cloud Platform service accounts credentials file. | *Optional* | | 2.0 | -| [includeEllipsoidToGeoidDifference](#includeEllipsoidToGeoidDifference) | `boolean` | Include the Ellipsoid to Geoid difference in the calculations of every point along every StreetWithElevationEdge. | *Optional* | `false` | 2.0 | -| maxAreaNodes | `integer` | Visibility calculations for an area will not be done if there are more nodes than this limit. | *Optional* | `150` | 2.1 | -| [maxDataImportIssuesPerFile](#maxDataImportIssuesPerFile) | `integer` | When to split the import report. | *Optional* | `1000` | 2.0 | -| maxElevationPropagationMeters | `integer` | The maximum distance to propagate elevation to vertices which have no elevation. | *Optional* | `2000` | 1.5 | -| [maxStopToShapeSnapDistance](#maxStopToShapeSnapDistance) | `double` | Maximum distance between route shapes and their stops. | *Optional* | `150.0` | 2.1 | -| maxTransferDuration | `duration` | Transfers up to this duration with the default walk speed value will be pre-calculated and included in the Graph. | *Optional* | `"PT30M"` | 2.1 | -| [multiThreadElevationCalculations](#multiThreadElevationCalculations) | `boolean` | Configuring multi-threading during elevation calculations. | *Optional* | `false` | 2.0 | -| [osmCacheDataInMem](#osmCacheDataInMem) | `boolean` | If OSM data should be cached in memory during processing. | *Optional* | `false` | 2.0 | -| [osmNaming](#osmNaming) | `enum` | A custom OSM namer to use. | *Optional* | `"default"` | 1.5 | -| platformEntriesLinking | `boolean` | Link unconnected entries to public transport platforms. | *Optional* | `false` | 2.0 | -| [readCachedElevations](#readCachedElevations) | `boolean` | Whether to read cached elevation data. | *Optional* | `true` | 2.0 | -| staticBikeParkAndRide | `boolean` | Whether we should create bike P+R stations from OSM data. | *Optional* | `false` | 1.5 | -| staticParkAndRide | `boolean` | Whether we should create car P+R stations from OSM data. | *Optional* | `true` | 1.5 | -| stopConsolidationFile | `uri` | Name of the CSV-formatted file in the build directory which contains the configuration for stop consolidation. | *Optional* | | 2.5 | -| [streetGraph](#streetGraph) | `uri` | URI to the street graph object file for reading and writing. | *Optional* | | 2.0 | -| [subwayAccessTime](#subwayAccessTime) | `double` | Minutes necessary to reach stops served by trips on routes of route_type=1 (subway) from the street. | *Optional* | `2.0` | 1.5 | -| [transitModelTimeZone](#transitModelTimeZone) | `time-zone` | Time zone for the graph. | *Optional* | | 2.2 | -| [transitServiceEnd](#transitServiceEnd) | `duration` | Limit the import of transit services to the given end date. | *Optional* | `"P3Y"` | 2.0 | -| [transitServiceStart](#transitServiceStart) | `duration` | Limit the import of transit services to the given START date. | *Optional* | `"-P1Y"` | 2.0 | -| [writeCachedElevations](#writeCachedElevations) | `boolean` | Reusing elevation data from previous builds | *Optional* | `false` | 2.0 | -| [boardingLocationTags](#boardingLocationTags) | `string[]` | What OSM tags should be looked on for the source of matching stops to platforms and stops. | *Optional* | | 2.2 | -| [dataOverlay](sandbox/DataOverlay.md) | `object` | Config for the DataOverlay Sandbox module | *Optional* | | 2.2 | -| [dem](#dem) | `object[]` | Specify parameters for DEM extracts. | *Optional* | | 2.2 | -|       [elevationUnitMultiplier](#dem_0_elevationUnitMultiplier) | `double` | Specify a multiplier to convert elevation units from source to meters. Overrides the value specified in `demDefaults`. | *Optional* | `1.0` | 2.3 | -|       source | `uri` | The unique URI pointing to the data file. | *Required* | | 2.2 | -| demDefaults | `object` | Default properties for DEM extracts. | *Optional* | | 2.3 | -|    [elevationUnitMultiplier](#demDefaults_elevationUnitMultiplier) | `double` | Specify a multiplier to convert elevation units from source to meters. | *Optional* | `1.0` | 2.3 | -| [elevationBucket](#elevationBucket) | `object` | Used to download NED elevation tiles from the given AWS S3 bucket. | *Optional* | | na | -| [emissions](sandbox/Emissions.md) | `object` | Emissions configuration. | *Optional* | | 2.5 | -| [fares](sandbox/Fares.md) | `object` | Fare configuration. | *Optional* | | 2.0 | -| gtfsDefaults | `object` | The gtfsDefaults section allows you to specify default properties for GTFS files. | *Optional* | | 2.3 | -|    blockBasedInterlining | `boolean` | Whether to create stay-seated transfers in between two trips with the same block id. | *Optional* | `true` | 2.3 | -|    [discardMinTransferTimes](#gd_discardMinTransferTimes) | `boolean` | Should minimum transfer times in GTFS files be discarded. | *Optional* | `false` | 2.3 | -|    maxInterlineDistance | `integer` | Maximal distance between stops in meters that will connect consecutive trips that are made with same vehicle. | *Optional* | `200` | 2.3 | -|    removeRepeatedStops | `boolean` | Should consecutive identical stops be merged into one stop time entry. | *Optional* | `true` | 2.3 | -|    [stationTransferPreference](#gd_stationTransferPreference) | `enum` | Should there be some preference or aversion for transfers at stops that are part of a station. | *Optional* | `"allowed"` | 2.3 | -| islandPruning | `object` | Settings for fixing street graph connectivity errors | *Optional* | | 2.3 | -|    [adaptivePruningDistance](#islandPruning_adaptivePruningDistance) | `integer` | Search distance for analyzing islands in pruning. | *Optional* | `250` | 2.3 | -|    [adaptivePruningFactor](#islandPruning_adaptivePruningFactor) | `double` | Defines how much pruning thresholds grow maximally by distance. | *Optional* | `50.0` | 2.3 | -|    [islandWithStopsMaxSize](#islandPruning_islandWithStopsMaxSize) | `integer` | When a graph island with stops in it should be pruned. | *Optional* | `2` | 2.3 | -|    [islandWithoutStopsMaxSize](#islandPruning_islandWithoutStopsMaxSize) | `integer` | When a graph island without stops should be pruned. | *Optional* | `10` | 2.3 | -| [localFileNamePatterns](#localFileNamePatterns) | `object` | Patterns for matching OTP file types in the base directory | *Optional* | | 2.0 | -|    [dem](#lfp_dem) | `regexp` | Pattern for matching elevation DEM files. | *Optional* | `"(?i)\.tiff?$"` | 2.0 | -|    [gtfs](#lfp_gtfs) | `regexp` | Patterns for matching GTFS zip-files or directories. | *Optional* | `"(?i)gtfs"` | 2.0 | -|    [netex](#lfp_netex) | `regexp` | Patterns for matching NeTEx zip files or directories. | *Optional* | `"(?i)netex"` | 2.0 | -|    [osm](#lfp_osm) | `regexp` | Pattern for matching Open Street Map input files. | *Optional* | `"(?i)(\.pbf¦\.osm¦\.osm\.xml)$"` | 2.0 | -| netexDefaults | `object` | The netexDefaults section allows you to specify default properties for NeTEx files. | *Optional* | | 2.2 | -|    feedId | `string` | This field is used to identify the specific NeTEx feed. It is used instead of the feed_id field in GTFS file feed_info.txt. | *Optional* | `"NETEX"` | 2.2 | -|    [groupFilePattern](#nd_groupFilePattern) | `regexp` | Pattern for matching group NeTEx files. | *Optional* | `"(\w{3})-.*\.xml"` | 2.0 | -|    ignoreFareFrame | `boolean` | Ignore contents of the FareFrame | *Optional* | `false` | 2.3 | -|    [ignoreFilePattern](#nd_ignoreFilePattern) | `regexp` | Pattern for matching ignored files in a NeTEx bundle. | *Optional* | `"$^"` | 2.0 | -|    ignoreParking | `boolean` | Ignore Parking elements. | *Optional* | `true` | 2.6 | -|    noTransfersOnIsolatedStops | `boolean` | Whether we should allow transfers to and from StopPlaces marked with LimitedUse.ISOLATED | *Optional* | `false` | 2.2 | -|    [sharedFilePattern](#nd_sharedFilePattern) | `regexp` | Pattern for matching shared NeTEx files in a NeTEx bundle. | *Optional* | `"shared-data\.xml"` | 2.0 | -|    [sharedGroupFilePattern](#nd_sharedGroupFilePattern) | `regexp` | Pattern for matching shared group NeTEx files in a NeTEx bundle. | *Optional* | `"(\w{3})-.*-shared\.xml"` | 2.0 | -|    [ferryIdsNotAllowedForBicycle](#nd_ferryIdsNotAllowedForBicycle) | `string[]` | List ferries which do not allow bikes. | *Optional* | | 2.0 | -| [osm](#osm) | `object[]` | Configure properties for a given OpenStreetMap feed. | *Optional* | | 2.2 | -|       includeOsmSubwayEntrances | `boolean` | Whether to include subway entrances from the OSM data. Overrides the value specified in `osmDefaults`. | *Optional* | `false` | 2.7 | -|       [osmTagMapping](#osm_0_osmTagMapping) | `enum` | The named set of mapping rules applied when parsing OSM tags. Overrides the value specified in `osmDefaults`. | *Optional* | `"default"` | 2.2 | -|       source | `uri` | The unique URI pointing to the data file. | *Required* | | 2.2 | -|       timeZone | `time-zone` | The timezone used to resolve opening hours in OSM data. Overrides the value specified in `osmDefaults`. | *Optional* | | 2.2 | -| osmDefaults | `object` | Default properties for OpenStreetMap feeds. | *Optional* | | 2.2 | -|    includeOsmSubwayEntrances | `boolean` | Whether to include subway entrances from the OSM data. | *Optional* | `false` | 2.7 | -|    [osmTagMapping](#od_osmTagMapping) | `enum` | The named set of mapping rules applied when parsing OSM tags. | *Optional* | `"default"` | 2.2 | -|    timeZone | `time-zone` | The timezone used to resolve opening hours in OSM data. | *Optional* | | 2.2 | -| [transferRequests](RouteRequest.md) | `object[]` | Routing requests to use for pre-calculating stop-to-stop transfers. | *Optional* | | 2.1 | -| [transitFeeds](#transitFeeds) | `object[]` | Scan for transit data files | *Optional* | | 2.2 | -|    { object } | `object` | Nested object in array. The object type is determined by the parameters. | *Optional* | | 2.2 | -|       type = "gtfs" | `enum` | The feed input format. | *Required* | | 2.2 | -|       blockBasedInterlining | `boolean` | Whether to create stay-seated transfers in between two trips with the same block id. Overrides the value specified in `gtfsDefaults`. | *Optional* | `true` | 2.3 | -|       [discardMinTransferTimes](#tf_0_discardMinTransferTimes) | `boolean` | Should minimum transfer times in GTFS files be discarded. Overrides the value specified in `gtfsDefaults`. | *Optional* | `false` | 2.3 | -|       feedId | `string` | The unique ID for this feed. This overrides any feed ID defined within the feed itself. | *Optional* | | 2.2 | -|       maxInterlineDistance | `integer` | Maximal distance between stops in meters that will connect consecutive trips that are made with same vehicle. Overrides the value specified in `gtfsDefaults`. | *Optional* | `200` | 2.3 | -|       removeRepeatedStops | `boolean` | Should consecutive identical stops be merged into one stop time entry. Overrides the value specified in `gtfsDefaults`. | *Optional* | `true` | 2.3 | -|       source | `uri` | The unique URI pointing to the data file. | *Required* | | 2.2 | -|       [stationTransferPreference](#tf_0_stationTransferPreference) | `enum` | Should there be some preference or aversion for transfers at stops that are part of a station. Overrides the value specified in `gtfsDefaults`. | *Optional* | `"allowed"` | 2.3 | -|    { object } | `object` | Nested object in array. The object type is determined by the parameters. | *Optional* | | 2.2 | -|       type = "netex" | `enum` | The feed input format. | *Required* | | 2.2 | -|       feedId | `string` | This field is used to identify the specific NeTEx feed. It is used instead of the feed_id field in GTFS file feed_info.txt. | *Required* | | 2.2 | -|       [groupFilePattern](#tf_1_groupFilePattern) | `regexp` | Pattern for matching group NeTEx files. | *Optional* | `"(\w{3})-.*\.xml"` | 2.0 | -|       ignoreFareFrame | `boolean` | Ignore contents of the FareFrame | *Optional* | `false` | 2.3 | -|       [ignoreFilePattern](#tf_1_ignoreFilePattern) | `regexp` | Pattern for matching ignored files in a NeTEx bundle. | *Optional* | `"$^"` | 2.0 | -|       ignoreParking | `boolean` | Ignore Parking elements. | *Optional* | `true` | 2.6 | -|       noTransfersOnIsolatedStops | `boolean` | Whether we should allow transfers to and from StopPlaces marked with LimitedUse.ISOLATED | *Optional* | `false` | 2.2 | -|       [sharedFilePattern](#tf_1_sharedFilePattern) | `regexp` | Pattern for matching shared NeTEx files in a NeTEx bundle. | *Optional* | `"shared-data\.xml"` | 2.0 | -|       [sharedGroupFilePattern](#tf_1_sharedGroupFilePattern) | `regexp` | Pattern for matching shared group NeTEx files in a NeTEx bundle. | *Optional* | `"(\w{3})-.*-shared\.xml"` | 2.0 | -|       source | `uri` | The unique URI pointing to the data file. | *Required* | | 2.2 | -|       [ferryIdsNotAllowedForBicycle](#tf_1_ferryIdsNotAllowedForBicycle) | `string[]` | List ferries which do not allow bikes. | *Optional* | | 2.0 | -| [transitRouteToStationCentroid](#transitRouteToStationCentroid) | `feed-scoped-id[]` | List stations that should route to centroid. | *Optional* | | 2.7 | +| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | +|-------------------------------------------------------------------------------------------|:--------------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------:|-----------------------------------|:-----:| +| [areaVisibility](#areaVisibility) | `boolean` | Perform visibility calculations. | *Optional* | `false` | 1.5 | +| [buildReportDir](#buildReportDir) | `uri` | URI to the directory where the graph build report should be written to. | *Optional* | | 2.0 | +| [configVersion](#configVersion) | `string` | Deployment version of the *build-config.json*. | *Optional* | | 2.1 | +| [dataImportReport](#dataImportReport) | `boolean` | Generate nice HTML report of Graph errors/warnings | *Optional* | `false` | 2.0 | +| [distanceBetweenElevationSamples](#distanceBetweenElevationSamples) | `double` | The distance between elevation samples in meters. | *Optional* | `10.0` | 2.0 | +| embedRouterConfig | `boolean` | Embed the Router config in the graph, which allows it to be sent to a server fully configured over the wire. | *Optional* | `true` | 2.0 | +| [graph](#graph) | `uri` | URI to the graph object file for reading and writing. | *Optional* | | 2.0 | +| [gsCredentials](#gsCredentials) | `string` | Local file system path to Google Cloud Platform service accounts credentials file. | *Optional* | | 2.0 | +| [includeEllipsoidToGeoidDifference](#includeEllipsoidToGeoidDifference) | `boolean` | Include the Ellipsoid to Geoid difference in the calculations of every point along every StreetWithElevationEdge. | *Optional* | `false` | 2.0 | +| maxAreaNodes | `integer` | Visibility calculations for an area will not be done if there are more nodes than this limit. | *Optional* | `150` | 2.1 | +| [maxDataImportIssuesPerFile](#maxDataImportIssuesPerFile) | `integer` | When to split the import report. | *Optional* | `1000` | 2.0 | +| maxElevationPropagationMeters | `integer` | The maximum distance to propagate elevation to vertices which have no elevation. | *Optional* | `2000` | 1.5 | +| [maxStopToShapeSnapDistance](#maxStopToShapeSnapDistance) | `double` | Maximum distance between route shapes and their stops. | *Optional* | `150.0` | 2.1 | +| maxTransferDuration | `duration` | Transfers up to this duration with a mode-specific speed value will be pre-calculated and included in the Graph. | *Optional* | `"PT30M"` | 2.1 | +| [multiThreadElevationCalculations](#multiThreadElevationCalculations) | `boolean` | Configuring multi-threading during elevation calculations. | *Optional* | `false` | 2.0 | +| [osmCacheDataInMem](#osmCacheDataInMem) | `boolean` | If OSM data should be cached in memory during processing. | *Optional* | `false` | 2.0 | +| [osmNaming](#osmNaming) | `enum` | A custom OSM namer to use. | *Optional* | `"default"` | 1.5 | +| platformEntriesLinking | `boolean` | Link unconnected entries to public transport platforms. | *Optional* | `false` | 2.0 | +| [readCachedElevations](#readCachedElevations) | `boolean` | Whether to read cached elevation data. | *Optional* | `true` | 2.0 | +| staticBikeParkAndRide | `boolean` | Whether we should create bike P+R stations from OSM data. | *Optional* | `false` | 1.5 | +| staticParkAndRide | `boolean` | Whether we should create car P+R stations from OSM data. | *Optional* | `true` | 1.5 | +| stopConsolidationFile | `uri` | Name of the CSV-formatted file in the build directory which contains the configuration for stop consolidation. | *Optional* | | 2.5 | +| [streetGraph](#streetGraph) | `uri` | URI to the street graph object file for reading and writing. | *Optional* | | 2.0 | +| [subwayAccessTime](#subwayAccessTime) | `double` | Minutes necessary to reach stops served by trips on routes of route_type=1 (subway) from the street. | *Optional* | `2.0` | 1.5 | +| [transitModelTimeZone](#transitModelTimeZone) | `time-zone` | Time zone for the graph. | *Optional* | | 2.2 | +| [transitServiceEnd](#transitServiceEnd) | `duration` | Limit the import of transit services to the given end date. | *Optional* | `"P3Y"` | 2.0 | +| [transitServiceStart](#transitServiceStart) | `duration` | Limit the import of transit services to the given START date. | *Optional* | `"-P1Y"` | 2.0 | +| [writeCachedElevations](#writeCachedElevations) | `boolean` | Reusing elevation data from previous builds | *Optional* | `false` | 2.0 | +| [boardingLocationTags](#boardingLocationTags) | `string[]` | What OSM tags should be looked on for the source of matching stops to platforms and stops. | *Optional* | | 2.2 | +| [dataOverlay](sandbox/DataOverlay.md) | `object` | Config for the DataOverlay Sandbox module | *Optional* | | 2.2 | +| [dem](#dem) | `object[]` | Specify parameters for DEM extracts. | *Optional* | | 2.2 | +|       [elevationUnitMultiplier](#dem_0_elevationUnitMultiplier) | `double` | Specify a multiplier to convert elevation units from source to meters. Overrides the value specified in `demDefaults`. | *Optional* | `1.0` | 2.3 | +|       source | `uri` | The unique URI pointing to the data file. | *Required* | | 2.2 | +| demDefaults | `object` | Default properties for DEM extracts. | *Optional* | | 2.3 | +|    [elevationUnitMultiplier](#demDefaults_elevationUnitMultiplier) | `double` | Specify a multiplier to convert elevation units from source to meters. | *Optional* | `1.0` | 2.3 | +| [elevationBucket](#elevationBucket) | `object` | Used to download NED elevation tiles from the given AWS S3 bucket. | *Optional* | | na | +| [emissions](sandbox/Emissions.md) | `object` | Emissions configuration. | *Optional* | | 2.5 | +| [fares](sandbox/Fares.md) | `object` | Fare configuration. | *Optional* | | 2.0 | +| gtfsDefaults | `object` | The gtfsDefaults section allows you to specify default properties for GTFS files. | *Optional* | | 2.3 | +|    blockBasedInterlining | `boolean` | Whether to create stay-seated transfers in between two trips with the same block id. | *Optional* | `true` | 2.3 | +|    [discardMinTransferTimes](#gd_discardMinTransferTimes) | `boolean` | Should minimum transfer times in GTFS files be discarded. | *Optional* | `false` | 2.3 | +|    maxInterlineDistance | `integer` | Maximal distance between stops in meters that will connect consecutive trips that are made with same vehicle. | *Optional* | `200` | 2.3 | +|    removeRepeatedStops | `boolean` | Should consecutive identical stops be merged into one stop time entry. | *Optional* | `true` | 2.3 | +|    [stationTransferPreference](#gd_stationTransferPreference) | `enum` | Should there be some preference or aversion for transfers at stops that are part of a station. | *Optional* | `"allowed"` | 2.3 | +| islandPruning | `object` | Settings for fixing street graph connectivity errors | *Optional* | | 2.3 | +|    [adaptivePruningDistance](#islandPruning_adaptivePruningDistance) | `integer` | Search distance for analyzing islands in pruning. | *Optional* | `250` | 2.3 | +|    [adaptivePruningFactor](#islandPruning_adaptivePruningFactor) | `double` | Defines how much pruning thresholds grow maximally by distance. | *Optional* | `50.0` | 2.3 | +|    [islandWithStopsMaxSize](#islandPruning_islandWithStopsMaxSize) | `integer` | When a graph island with stops in it should be pruned. | *Optional* | `2` | 2.3 | +|    [islandWithoutStopsMaxSize](#islandPruning_islandWithoutStopsMaxSize) | `integer` | When a graph island without stops should be pruned. | *Optional* | `10` | 2.3 | +| [localFileNamePatterns](#localFileNamePatterns) | `object` | Patterns for matching OTP file types in the base directory | *Optional* | | 2.0 | +|    [dem](#lfp_dem) | `regexp` | Pattern for matching elevation DEM files. | *Optional* | `"(?i)\.tiff?$"` | 2.0 | +|    [gtfs](#lfp_gtfs) | `regexp` | Patterns for matching GTFS zip-files or directories. | *Optional* | `"(?i)gtfs"` | 2.0 | +|    [netex](#lfp_netex) | `regexp` | Patterns for matching NeTEx zip files or directories. | *Optional* | `"(?i)netex"` | 2.0 | +|    [osm](#lfp_osm) | `regexp` | Pattern for matching Open Street Map input files. | *Optional* | `"(?i)(\.pbf¦\.osm¦\.osm\.xml)$"` | 2.0 | +| netexDefaults | `object` | The netexDefaults section allows you to specify default properties for NeTEx files. | *Optional* | | 2.2 | +|    feedId | `string` | This field is used to identify the specific NeTEx feed. It is used instead of the feed_id field in GTFS file feed_info.txt. | *Optional* | `"NETEX"` | 2.2 | +|    [groupFilePattern](#nd_groupFilePattern) | `regexp` | Pattern for matching group NeTEx files. | *Optional* | `"(\w{3})-.*\.xml"` | 2.0 | +|    ignoreFareFrame | `boolean` | Ignore contents of the FareFrame | *Optional* | `false` | 2.3 | +|    [ignoreFilePattern](#nd_ignoreFilePattern) | `regexp` | Pattern for matching ignored files in a NeTEx bundle. | *Optional* | `"$^"` | 2.0 | +|    ignoreParking | `boolean` | Ignore Parking elements. | *Optional* | `true` | 2.6 | +|    noTransfersOnIsolatedStops | `boolean` | Whether we should allow transfers to and from StopPlaces marked with LimitedUse.ISOLATED | *Optional* | `false` | 2.2 | +|    [sharedFilePattern](#nd_sharedFilePattern) | `regexp` | Pattern for matching shared NeTEx files in a NeTEx bundle. | *Optional* | `"shared-data\.xml"` | 2.0 | +|    [sharedGroupFilePattern](#nd_sharedGroupFilePattern) | `regexp` | Pattern for matching shared group NeTEx files in a NeTEx bundle. | *Optional* | `"(\w{3})-.*-shared\.xml"` | 2.0 | +|    [ferryIdsNotAllowedForBicycle](#nd_ferryIdsNotAllowedForBicycle) | `string[]` | List ferries which do not allow bikes. | *Optional* | | 2.0 | +| [osm](#osm) | `object[]` | Configure properties for a given OpenStreetMap feed. | *Optional* | | 2.2 | +|       includeOsmSubwayEntrances | `boolean` | Whether to include subway entrances from the OSM data. Overrides the value specified in `osmDefaults`. | *Optional* | `false` | 2.7 | +|       [osmTagMapping](#osm_0_osmTagMapping) | `enum` | The named set of mapping rules applied when parsing OSM tags. Overrides the value specified in `osmDefaults`. | *Optional* | `"default"` | 2.2 | +|       source | `uri` | The unique URI pointing to the data file. | *Required* | | 2.2 | +|       timeZone | `time-zone` | The timezone used to resolve opening hours in OSM data. Overrides the value specified in `osmDefaults`. | *Optional* | | 2.2 | +| osmDefaults | `object` | Default properties for OpenStreetMap feeds. | *Optional* | | 2.2 | +|    includeOsmSubwayEntrances | `boolean` | Whether to include subway entrances from the OSM data. | *Optional* | `false` | 2.7 | +|    [osmTagMapping](#od_osmTagMapping) | `enum` | The named set of mapping rules applied when parsing OSM tags. | *Optional* | `"default"` | 2.2 | +|    timeZone | `time-zone` | The timezone used to resolve opening hours in OSM data. | *Optional* | | 2.2 | +| [transferParametersForMode](#transferParametersForMode) | `enum map of object` | Configures mode-specific properties for transfer calculations. | *Optional* | | 2.7 | +|    BIKE | `object` | NA | *Optional* | | 2.7 | +|       [carsAllowedStopMaxTransferDuration](#tpfm_BIKE_carsAllowedStopMaxTransferDuration) | `duration` | This is used for specifying a `maxTransferDuration` value to use with transfers between stops which are visited by trips that allow cars. | *Optional* | | 2.7 | +|       [disableDefaultTransfers](#tpfm_BIKE_disableDefaultTransfers) | `boolean` | This disables default transfer calculations. | *Optional* | `false` | 2.7 | +|       maxTransferDuration | `duration` | This overwrites the default `maxTransferDuration` for the given mode. | *Optional* | | 2.7 | +|    CAR | `object` | NA | *Optional* | | 2.7 | +|       [carsAllowedStopMaxTransferDuration](#tpfm_CAR_carsAllowedStopMaxTransferDuration) | `duration` | This is used for specifying a `maxTransferDuration` value to use with transfers between stops which are visited by trips that allow cars. | *Optional* | | 2.7 | +|       [disableDefaultTransfers](#tpfm_CAR_disableDefaultTransfers) | `boolean` | This disables default transfer calculations. | *Optional* | `false` | 2.7 | +|       maxTransferDuration | `duration` | This overwrites the default `maxTransferDuration` for the given mode. | *Optional* | | 2.7 | +| [transferRequests](RouteRequest.md) | `object[]` | Routing requests to use for pre-calculating stop-to-stop transfers. | *Optional* | | 2.1 | +| [transitFeeds](#transitFeeds) | `object[]` | Scan for transit data files | *Optional* | | 2.2 | +|    { object } | `object` | Nested object in array. The object type is determined by the parameters. | *Optional* | | 2.2 | +|       type = "gtfs" | `enum` | The feed input format. | *Required* | | 2.2 | +|       blockBasedInterlining | `boolean` | Whether to create stay-seated transfers in between two trips with the same block id. Overrides the value specified in `gtfsDefaults`. | *Optional* | `true` | 2.3 | +|       [discardMinTransferTimes](#tf_0_discardMinTransferTimes) | `boolean` | Should minimum transfer times in GTFS files be discarded. Overrides the value specified in `gtfsDefaults`. | *Optional* | `false` | 2.3 | +|       feedId | `string` | The unique ID for this feed. This overrides any feed ID defined within the feed itself. | *Optional* | | 2.2 | +|       maxInterlineDistance | `integer` | Maximal distance between stops in meters that will connect consecutive trips that are made with same vehicle. Overrides the value specified in `gtfsDefaults`. | *Optional* | `200` | 2.3 | +|       removeRepeatedStops | `boolean` | Should consecutive identical stops be merged into one stop time entry. Overrides the value specified in `gtfsDefaults`. | *Optional* | `true` | 2.3 | +|       source | `uri` | The unique URI pointing to the data file. | *Required* | | 2.2 | +|       [stationTransferPreference](#tf_0_stationTransferPreference) | `enum` | Should there be some preference or aversion for transfers at stops that are part of a station. Overrides the value specified in `gtfsDefaults`. | *Optional* | `"allowed"` | 2.3 | +|    { object } | `object` | Nested object in array. The object type is determined by the parameters. | *Optional* | | 2.2 | +|       type = "netex" | `enum` | The feed input format. | *Required* | | 2.2 | +|       feedId | `string` | This field is used to identify the specific NeTEx feed. It is used instead of the feed_id field in GTFS file feed_info.txt. | *Required* | | 2.2 | +|       [groupFilePattern](#tf_1_groupFilePattern) | `regexp` | Pattern for matching group NeTEx files. | *Optional* | `"(\w{3})-.*\.xml"` | 2.0 | +|       ignoreFareFrame | `boolean` | Ignore contents of the FareFrame | *Optional* | `false` | 2.3 | +|       [ignoreFilePattern](#tf_1_ignoreFilePattern) | `regexp` | Pattern for matching ignored files in a NeTEx bundle. | *Optional* | `"$^"` | 2.0 | +|       ignoreParking | `boolean` | Ignore Parking elements. | *Optional* | `true` | 2.6 | +|       noTransfersOnIsolatedStops | `boolean` | Whether we should allow transfers to and from StopPlaces marked with LimitedUse.ISOLATED | *Optional* | `false` | 2.2 | +|       [sharedFilePattern](#tf_1_sharedFilePattern) | `regexp` | Pattern for matching shared NeTEx files in a NeTEx bundle. | *Optional* | `"shared-data\.xml"` | 2.0 | +|       [sharedGroupFilePattern](#tf_1_sharedGroupFilePattern) | `regexp` | Pattern for matching shared group NeTEx files in a NeTEx bundle. | *Optional* | `"(\w{3})-.*-shared\.xml"` | 2.0 | +|       source | `uri` | The unique URI pointing to the data file. | *Required* | | 2.2 | +|       [ferryIdsNotAllowedForBicycle](#tf_1_ferryIdsNotAllowedForBicycle) | `string[]` | List ferries which do not allow bikes. | *Optional* | | 2.0 | +| [transitRouteToStationCentroid](#transitRouteToStationCentroid) | `feed-scoped-id[]` | List stations that should route to centroid. | *Optional* | | 2.7 | @@ -954,6 +963,116 @@ The named set of mapping rules applied when parsing OSM tags. Overrides the valu The named set of mapping rules applied when parsing OSM tags. +

transferParametersForMode

+ +**Since version:** `2.7` ∙ **Type:** `enum map of object` ∙ **Cardinality:** `Optional` +**Path:** / +**Enum keys:** `not-set` | `walk` | `bike` | `bike-to-park` | `bike-rental` | `scooter-rental` | `car` | `car-to-park` | `car-pickup` | `car-rental` | `car-hailing` | `flexible` + +Configures mode-specific properties for transfer calculations. + +This field enables configuring mode-specific parameters for transfer calculations. +To configure mode-specific parameters, the modes should also be used in the `transferRequests` field in the build config. + +**Example** + +```JSON +// build-config.json +{ + "transferParametersForMode": { + "CAR": { + "disableDefaultTransfers": true, + "carsAllowedStopMaxTransferDuration": "3h" + }, + "BIKE": { + "maxTransferDuration": "30m", + "carsAllowedStopMaxTransferDuration": "3h" + } + } +} +``` + + +

carsAllowedStopMaxTransferDuration

+ +**Since version:** `2.7` ∙ **Type:** `duration` ∙ **Cardinality:** `Optional` +**Path:** /transferParametersForMode/BIKE + +This is used for specifying a `maxTransferDuration` value to use with transfers between stops which are visited by trips that allow cars. + +This parameter configures additional transfers to be calculated for the specified mode only between stops that have trips with cars. +The transfers are calculated for the mode in a range based on the given duration. +By default, these transfers are not calculated unless specified for a mode with this field. + +Calculating transfers only between stops that have trips with cars can be useful with car ferries, for example. +Using transit with cars can only occur between certain stops. +These kinds of stops require support for loading cars into ferries, for example. +The default transfers are calculated based on a configurable range (configurable by using the `maxTransferDuration` field) +which limits transfers from stops to only be calculated to other stops that are in range. +When compared to walking, using a car can cover larger distances within the same duration specified in the `maxTransferDuration` field. +This can lead to large amounts of transfers calculated between stops that do not require car transfers between them. +This in turn can lead to a large increase in memory for the stored graph, depending on the data used in the graph. + +For cars, using this parameter in conjunction with `disableDefaultTransfers` allows calculating transfers only between relevant stops. +For bikes, using this parameter can enable transfers between ferry stops that would normally not be in range. +In Finland this is useful for bike routes that use ferries near the Turku archipelago, for example. + + +

disableDefaultTransfers

+ +**Since version:** `2.7` ∙ **Type:** `boolean` ∙ **Cardinality:** `Optional` ∙ **Default value:** `false` +**Path:** /transferParametersForMode/BIKE + +This disables default transfer calculations. + +The default transfers are calculated based on a configurable range (configurable by using the `maxTransferDuration` field) +which limits transfers from stops to only be calculated to other stops that are in range. +This parameter disables these transfers. +A motivation to disable default transfers could be related to using the `carsAllowedStopMaxTransferDuration` field which only +calculates transfers between stops that have trips with cars. +For example, when using the `carsAllowedStopMaxTransferDuration` field with cars, the default transfers can be redundant. + + +

carsAllowedStopMaxTransferDuration

+ +**Since version:** `2.7` ∙ **Type:** `duration` ∙ **Cardinality:** `Optional` +**Path:** /transferParametersForMode/CAR + +This is used for specifying a `maxTransferDuration` value to use with transfers between stops which are visited by trips that allow cars. + +This parameter configures additional transfers to be calculated for the specified mode only between stops that have trips with cars. +The transfers are calculated for the mode in a range based on the given duration. +By default, these transfers are not calculated unless specified for a mode with this field. + +Calculating transfers only between stops that have trips with cars can be useful with car ferries, for example. +Using transit with cars can only occur between certain stops. +These kinds of stops require support for loading cars into ferries, for example. +The default transfers are calculated based on a configurable range (configurable by using the `maxTransferDuration` field) +which limits transfers from stops to only be calculated to other stops that are in range. +When compared to walking, using a car can cover larger distances within the same duration specified in the `maxTransferDuration` field. +This can lead to large amounts of transfers calculated between stops that do not require car transfers between them. +This in turn can lead to a large increase in memory for the stored graph, depending on the data used in the graph. + +For cars, using this parameter in conjunction with `disableDefaultTransfers` allows calculating transfers only between relevant stops. +For bikes, using this parameter can enable transfers between ferry stops that would normally not be in range. +In Finland this is useful for bike routes that use ferries near the Turku archipelago, for example. + + +

disableDefaultTransfers

+ +**Since version:** `2.7` ∙ **Type:** `boolean` ∙ **Cardinality:** `Optional` ∙ **Default value:** `false` +**Path:** /transferParametersForMode/CAR + +This disables default transfer calculations. + +The default transfers are calculated based on a configurable range (configurable by using the `maxTransferDuration` field) +which limits transfers from stops to only be calculated to other stops that are in range. +This parameter disables these transfers. +A motivation to disable default transfers could be related to using the `carsAllowedStopMaxTransferDuration` field which only +calculates transfers between stops that have trips with cars. +For example, when using the `carsAllowedStopMaxTransferDuration` field with cars, the default transfers can be redundant. + +

transitFeeds

**Since version:** `2.2` ∙ **Type:** `object[]` ∙ **Cardinality:** `Optional` @@ -1186,6 +1305,16 @@ the centroid. "emissions" : { "carAvgCo2PerKm" : 170, "carAvgOccupancy" : 1.3 + }, + "transferParametersForMode" : { + "CAR" : { + "disableDefaultTransfers" : true, + "carsAllowedStopMaxTransferDuration" : "3h" + }, + "BIKE" : { + "maxTransferDuration" : "30m", + "carsAllowedStopMaxTransferDuration" : "3h" + } } } ``` diff --git a/doc/user/Changelog.md b/doc/user/Changelog.md index 9a5f2731733..2f376463182 100644 --- a/doc/user/Changelog.md +++ b/doc/user/Changelog.md @@ -78,6 +78,13 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - If configured, add subway station entrances from OSM to walk steps [#6343](https://github.com/opentripplanner/OpenTripPlanner/pull/6343) - Revert allow multiple states during transfer edge traversals [#6357](https://github.com/opentripplanner/OpenTripPlanner/pull/6357) - Generate Raptor transfer cache in parallel [#6326](https://github.com/opentripplanner/OpenTripPlanner/pull/6326) +- Add 'transferParametersForMode' build config field [#6215](https://github.com/opentripplanner/OpenTripPlanner/pull/6215) +- Add 'maxStopCountForMode' to the router config [#6383](https://github.com/opentripplanner/OpenTripPlanner/pull/6383) +- Add all routing parameters to debug UI [#6370](https://github.com/opentripplanner/OpenTripPlanner/pull/6370) +- Add currentFuelPercent and currentRangeMeters to RentalVehichle in the GTFS GraphQL API [#6272](https://github.com/opentripplanner/OpenTripPlanner/pull/6272) +- Add a matcher API for filters in the transit service used for route lookup [#6378](https://github.com/opentripplanner/OpenTripPlanner/pull/6378) +- Use SIRI-ET and GTFS-RT TripUpdates at the same time [#6363](https://github.com/opentripplanner/OpenTripPlanner/pull/6363) +- Add 'boardCost' parameter for cars [#6413](https://github.com/opentripplanner/OpenTripPlanner/pull/6413) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.6.0 (2024-09-18) diff --git a/doc/user/DebugUiConfiguration.md b/doc/user/DebugUiConfiguration.md index a1657796fe0..d0a9c65c9a0 100644 --- a/doc/user/DebugUiConfiguration.md +++ b/doc/user/DebugUiConfiguration.md @@ -8,7 +8,7 @@ # Debug UI configuration The Debug UI is the standard interface that is bundled with OTP and available by visiting -[`http://localhost:8080`](http://localhost:8080). This page list the configuration options available +[`http://localhost:8080`](http://localhost:8080). This page lists the configuration options available by placing a file `debug-ui-config.json` into OTP's working directory. diff --git a/doc/user/RouteRequest.md b/doc/user/RouteRequest.md index 332058b2a42..3e50dbd6fbd 100644 --- a/doc/user/RouteRequest.md +++ b/doc/user/RouteRequest.md @@ -46,6 +46,7 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe |    [maxDuration](#rd_accessEgress_maxDuration) | `duration` | This is the maximum duration for access/egress for street searches. | *Optional* | `"PT45M"` | 2.1 | |    [maxStopCount](#rd_accessEgress_maxStopCount) | `integer` | Maximal number of stops collected in access/egress routing | *Optional* | `500` | 2.4 | |    [maxDurationForMode](#rd_accessEgress_maxDurationForMode) | `enum map of duration` | Limit access/egress per street mode. | *Optional* | | 2.1 | +|    [maxStopCountForMode](#rd_accessEgress_maxStopCountForMode) | `enum map of integer` | Maximal number of stops collected in access/egress routing for the given mode | *Optional* | | 2.7 | |    [penalty](#rd_accessEgress_penalty) | `enum map of object` | Penalty for access/egress by street mode. | *Optional* | | 2.4 | |       FLEXIBLE | `object` | NA | *Optional* | | 2.4 | |          costFactor | `double` | A factor multiplied with the time-penalty to get the cost-penalty. | *Optional* | `0.0` | 2.4 | @@ -86,6 +87,7 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe | [boardSlackForMode](#rd_boardSlackForMode) | `enum map of duration` | How much extra time should be given when boarding a vehicle for each given mode. | *Optional* | | 2.0 | | car | `object` | Car preferences. | *Optional* | | 2.5 | |    accelerationSpeed | `double` | The acceleration speed of an automobile, in meters per second per second. | *Optional* | `2.9` | 2.0 | +|    [boardCost](#rd_car_boardCost) | `integer` | Prevents unnecessary transfers by adding a cost for boarding a transit vehicle. | *Optional* | `600` | 2.7 | |    decelerationSpeed | `double` | The deceleration speed of an automobile, in meters per second per second. | *Optional* | `2.9` | 2.0 | |    pickupCost | `integer` | Add a cost for car pickup changes when a pickup or drop off takes place | *Optional* | `120` | 2.1 | |    pickupTime | `duration` | Add a time for car pickup changes when a pickup or drop off takes place | *Optional* | `"PT1M"` | 2.1 | @@ -431,6 +433,18 @@ Override the settings in `maxDuration` for specific street modes. This is done because some street modes searches are much more resource intensive than others. +

maxStopCountForMode

+ +**Since version:** `2.7` ∙ **Type:** `enum map of integer` ∙ **Cardinality:** `Optional` +**Path:** /routingDefaults/accessEgress +**Enum keys:** `not-set` | `walk` | `bike` | `bike-to-park` | `bike-rental` | `scooter-rental` | `car` | `car-to-park` | `car-pickup` | `car-rental` | `car-hailing` | `flexible` + +Maximal number of stops collected in access/egress routing for the given mode + +Safety limit to prevent access to and egress from too many stops. +Mode-specific version of `maxStopCount`. + +

penalty

**Since version:** `2.4` ∙ **Type:** `enum map of object` ∙ **Cardinality:** `Optional` @@ -605,6 +619,15 @@ Sometimes there is a need to configure a board times for specific modes, such as ferries, where the check-in process needs to be done in good time before ride. +

boardCost

+ +**Since version:** `2.7` ∙ **Type:** `integer` ∙ **Cardinality:** `Optional` ∙ **Default value:** `600` +**Path:** /routingDefaults/car + +Prevents unnecessary transfers by adding a cost for boarding a transit vehicle. + +This is the cost that is used when boarding while driving. This can be different compared to the boardCost while walking or cycling. +

unpreferredVehicleParkingTagCost

**Since version:** `2.3` ∙ **Type:** `integer` ∙ **Cardinality:** `Optional` ∙ **Default value:** `300` @@ -1196,6 +1219,7 @@ include stairs as a last result. }, "car" : { "reluctance" : 10, + "boardCost" : 600, "decelerationSpeed" : 2.9, "accelerationSpeed" : 2.9, "rental" : { @@ -1250,6 +1274,9 @@ include stairs as a last result. "BIKE_RENTAL" : "20m" }, "maxStopCount" : 500, + "maxStopCountForMode" : { + "CAR" : 0 + }, "penalty" : { "FLEXIBLE" : { "timePenalty" : "2m + 1.1t", diff --git a/doc/user/RouterConfiguration.md b/doc/user/RouterConfiguration.md index b5cbf15a4a5..f646f0e2848 100644 --- a/doc/user/RouterConfiguration.md +++ b/doc/user/RouterConfiguration.md @@ -514,6 +514,7 @@ Used to group requests when monitoring OTP. }, "car" : { "reluctance" : 10, + "boardCost" : 600, "decelerationSpeed" : 2.9, "accelerationSpeed" : 2.9, "rental" : { @@ -568,6 +569,9 @@ Used to group requests when monitoring OTP. "BIKE_RENTAL" : "20m" }, "maxStopCount" : 500, + "maxStopCountForMode" : { + "CAR" : 0 + }, "penalty" : { "FLEXIBLE" : { "timePenalty" : "2m + 1.1t", diff --git a/pom.xml b/pom.xml index 3a7d7e3703e..262f520cec2 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,7 @@ - 177 + 179 32.1 @@ -71,7 +71,7 @@ 5.6.0 1.5.12 10.1.0 - 1.14.1 + 1.14.3 2.0.15 5.6.0 4.28.3 @@ -313,8 +313,6 @@ src/main/java/**/*.java src/test/java/**/*.java - src/**/*.json - src/test/resources/org/opentripplanner/apis/**/*.graphql diff --git a/renovate.json5 b/renovate.json5 index 30a5c6a99c1..14d025745bb 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -109,7 +109,7 @@ }, { "groupName": "Update GTFS API code generation in a single PR", - "matchFiles": ["src/main/java/org/opentripplanner/apis/gtfs/generated/package.json"], + "matchFiles": ["application/src/main/java/org/opentripplanner/apis/gtfs/generated/package.json"], "reviewers": ["optionsome", "leonardehrenfried"], "schedule": "on the 11th through 12th day of the month" }, @@ -158,7 +158,8 @@ "io.micrometer:micrometer-registry-influx", "com.fasterxml.jackson:{/,}**", "com.fasterxml.jackson.datatype::{/,}**" - ] + ], + "automerge": true }, { "description": "give some projects time to publish a changelog before opening the PR",