From 356cbfd842ba79f25dd8291d019f21fa2b260db6 Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Tue, 10 Oct 2023 16:47:03 +0300 Subject: [PATCH 01/60] Make vehicleRentalStation query optionally accept id without feed --- docs/Configuration.md | 61 ++++++++++--------- .../apis/gtfs/datafetchers/QueryTypeImpl.java | 30 ++++++++- .../framework/application/OTPFeature.java | 5 ++ .../apis/gtfs/GraphQLIntegrationTest.java | 18 +++++- .../expectations/vehicle-rental-station.json | 12 ++++ .../queries/vehicle-rental-station.graphql | 10 +++ 6 files changed, 104 insertions(+), 32 deletions(-) create mode 100644 src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json create mode 100644 src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql diff --git a/docs/Configuration.md b/docs/Configuration.md index 5073dafed31..5692b5f87d0 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -219,36 +219,37 @@ Here is a list of all features which can be toggled on/off and their default val -| Feature | Description | Enabled by default | Sandbox | -|--------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------:|:-------:| -| `APIBikeRental` | Enable the bike rental endpoint. | ✓️ | | -| `APIServerInfo` | Enable the server info endpoint. | ✓️ | | -| `APIGraphInspectorTile` | Enable the inspector endpoint for graph information for inspection/debugging purpose. | ✓️ | | -| `APIUpdaterStatus` | Enable endpoint for graph updaters status. | ✓️ | | -| `ConsiderPatternsForDirectTransfers` | Enable limiting transfers so that there is only a single transfer to each pattern. | ✓️ | | -| `DebugClient` | Enable the debug web client located at the root of the web server. | ✓️ | | -| `FloatingBike` | Enable floating bike routing. | ✓️ | | -| `GtfsGraphQlApi` | Enable GTFS GraphQL API. | ✓️ | | -| `MinimumTransferTimeIsDefinitive` | If the minimum transfer time is a lower bound (default) or the definitive time for the transfer. Set this to `true` if you want to set a transfer time lower than what OTP derives from OSM data. | | | -| `OptimizeTransfers` | OTP will inspect all itineraries found and optimize where (which stops) the transfer will happen. Waiting time, priority and guaranteed transfers are taken into account. | ✓️ | | -| `ParallelRouting` | Enable performing parts of the trip planning in parallel. | | | -| `TransferConstraints` | Enforce transfers to happen according to the _transfers.txt_(GTFS) and Interchanges(NeTEx). Turing this _off_ will increase the routing performance a little. | ✓️ | | -| `ActuatorAPI` | Endpoint for actuators (service health status). | | ✓️ | -| `AsyncGraphQLFetchers` | Whether the @async annotation in the GraphQL schema should lead to the fetch being executed asynchronously. This allows batch or alias queries to run in parallel at the cost of consuming extra threads. | | | -| `DataOverlay` | Enable usage of data overlay when calculating costs for the street network. | | ✓️ | -| `FaresV2` | Enable import of GTFS-Fares v2 data. | | ✓️ | -| `FlexRouting` | Enable FLEX routing. | | ✓️ | -| `GoogleCloudStorage` | Enable Google Cloud Storage integration. | | ✓️ | -| `RealtimeResolver` | When routing with ignoreRealtimeUpdates=true, add an extra step which populates results with realtime data | | ✓️ | -| `ReportApi` | Enable the report API. | | ✓️ | -| `RestAPIPassInDefaultConfigAsJson` | Enable a default RouteRequest to be passed in as JSON on the REST API - FOR DEBUGGING ONLY! | | | -| `SandboxAPIGeocoder` | Enable the Geocoder API. | | ✓️ | -| `SandboxAPIMapboxVectorTilesApi` | Enable Mapbox vector tiles API. | | ✓️ | -| `SandboxAPIParkAndRideApi` | Enable park-and-ride endpoint. | | ✓️ | -| `SandboxAPITransmodelApi` | Enable Entur Transmodel(NeTEx) GraphQL API. | | ✓️ | -| `SandboxAPITravelTime` | Enable the isochrone/travel time surface API. | | ✓️ | -| `TransferAnalyzer` | Analyze transfers during graph build. | | ✓️ | -| `VehicleToStopHeuristics` | Enable improved heuristic for park-and-ride queries. | | ✓️ | +| Feature | Description | Enabled by default | Sandbox | +|--------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------:|:-------:| +| `APIBikeRental` | Enable the bike rental endpoint. | ✓️ | | +| `APIServerInfo` | Enable the server info endpoint. | ✓️ | | +| `APIGraphInspectorTile` | Enable the inspector endpoint for graph information for inspection/debugging purpose. | ✓️ | | +| `APIUpdaterStatus` | Enable endpoint for graph updaters status. | ✓️ | | +| `ConsiderPatternsForDirectTransfers` | Enable limiting transfers so that there is only a single transfer to each pattern. | ✓️ | | +| `DebugClient` | Enable the debug web client located at the root of the web server. | ✓️ | | +| `FloatingBike` | Enable floating bike routing. | ✓️ | | +| `GtfsGraphQlApi` | Enable GTFS GraphQL API. | ✓️ | | +| `GtfsGraphQlApiRentalStationFuzzyMatching` | Does vehicleRentalStation query also allow ids that are not feed scoped. | | | +| `MinimumTransferTimeIsDefinitive` | If the minimum transfer time is a lower bound (default) or the definitive time for the transfer. Set this to `true` if you want to set a transfer time lower than what OTP derives from OSM data. | | | +| `OptimizeTransfers` | OTP will inspect all itineraries found and optimize where (which stops) the transfer will happen. Waiting time, priority and guaranteed transfers are taken into account. | ✓️ | | +| `ParallelRouting` | Enable performing parts of the trip planning in parallel. | | | +| `TransferConstraints` | Enforce transfers to happen according to the _transfers.txt_(GTFS) and Interchanges(NeTEx). Turing this _off_ will increase the routing performance a little. | ✓️ | | +| `ActuatorAPI` | Endpoint for actuators (service health status). | | ✓️ | +| `AsyncGraphQLFetchers` | Whether the @async annotation in the GraphQL schema should lead to the fetch being executed asynchronously. This allows batch or alias queries to run in parallel at the cost of consuming extra threads. | | | +| `DataOverlay` | Enable usage of data overlay when calculating costs for the street network. | | ✓️ | +| `FaresV2` | Enable import of GTFS-Fares v2 data. | | ✓️ | +| `FlexRouting` | Enable FLEX routing. | | ✓️ | +| `GoogleCloudStorage` | Enable Google Cloud Storage integration. | | ✓️ | +| `RealtimeResolver` | When routing with ignoreRealtimeUpdates=true, add an extra step which populates results with realtime data | | ✓️ | +| `ReportApi` | Enable the report API. | | ✓️ | +| `RestAPIPassInDefaultConfigAsJson` | Enable a default RouteRequest to be passed in as JSON on the REST API - FOR DEBUGGING ONLY! | | | +| `SandboxAPIGeocoder` | Enable the Geocoder API. | | ✓️ | +| `SandboxAPIMapboxVectorTilesApi` | Enable Mapbox vector tiles API. | | ✓️ | +| `SandboxAPIParkAndRideApi` | Enable park-and-ride endpoint. | | ✓️ | +| `SandboxAPITransmodelApi` | Enable Entur Transmodel(NeTEx) GraphQL API. | | ✓️ | +| `SandboxAPITravelTime` | Enable the isochrone/travel time surface API. | | ✓️ | +| `TransferAnalyzer` | Analyze transfers during graph build. | | ✓️ | +| `VehicleToStopHeuristics` | Enable improved heuristic for park-and-ride queries. | | ✓️ | diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java index dfa4a60ce1c..2e6e9fe219b 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java @@ -33,6 +33,7 @@ import org.opentripplanner.ext.fares.impl.DefaultFareService; import org.opentripplanner.ext.fares.impl.GtfsFaresService; import org.opentripplanner.ext.fares.model.FareRuleSet; +import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.time.ServiceDateUtils; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.gtfs.mapping.DirectionMapper; @@ -841,11 +842,16 @@ public DataFetcher vehicleRentalStation() { .getContext() .vehicleRentalService(); + var id = args.getGraphQLId(); + + // TODO the fuzzy matching can be potentially removed after a while. return vehicleRentalStationService .getVehicleRentalStations() .stream() .filter(vehicleRentalStation -> - vehicleRentalStation.getId().toString().equals(args.getGraphQLId()) + OTPFeature.GtfsGraphQlApiRentalStationFuzzyMatching.isOn() + ? isFuzzyMatchRentalStationIds(vehicleRentalStation, id) + : isMatchRentalStationIds(vehicleRentalStation, id) ) .findAny() .orElse(null); @@ -890,6 +896,28 @@ public DataFetcher viewer() { return environment -> new Object(); } + /** + * This matches station's feedScopedId to the given string. + */ + private boolean isMatchRentalStationIds(VehicleRentalStation station, String feedScopedId) { + return station.getId().toString().equals(feedScopedId); + } + + /** + * This matches station's feedScopedId to the given string if the string is feed scoped (i.e + * contains a `:` separator) or only matches the station's id without the feed to the given + * string. This approach can lead to a random station matching the criteria if there are multiple + * stations with the same id in different feeds. + *

+ * TODO this can be potentially removed after a while, only used by Digitransit as of now. + */ + private boolean isFuzzyMatchRentalStationIds(VehicleRentalStation station, String idWithoutFeed) { + if (idWithoutFeed != null && idWithoutFeed.contains(":")) { + return isMatchRentalStationIds(station, idWithoutFeed); + } + return station.getId().getId().equals(idWithoutFeed); + } + private TransitService getTransitService(DataFetchingEnvironment environment) { return environment.getContext().transitService(); } diff --git a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java index 0652fb667a8..a80bd26bad1 100644 --- a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java +++ b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java @@ -30,6 +30,11 @@ public enum OTPFeature { DebugClient(true, false, "Enable the debug web client located at the root of the web server."), FloatingBike(true, false, "Enable floating bike routing."), GtfsGraphQlApi(true, false, "Enable GTFS GraphQL API."), + GtfsGraphQlApiRentalStationFuzzyMatching( + false, + false, + "Does vehicleRentalStation query also allow ids that are not feed scoped." + ), /** * If this feature flag is switched on, then the minimum transfer time is not the minimum transfer * time, but the definitive transfer time. Use this to override what we think the transfer will diff --git a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index bd3c10f491e..6dd79460e8b 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -70,6 +70,7 @@ import org.opentripplanner.routing.vehicle_parking.VehicleParking; import org.opentripplanner.service.vehiclepositions.internal.DefaultVehiclePositionService; import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; +import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; import org.opentripplanner.standalone.config.framework.json.JsonSupport; import org.opentripplanner.test.support.FilePatternSource; import org.opentripplanner.transit.model._data.TransitModelForTest; @@ -216,13 +217,16 @@ public TransitAlertService getTransitAlertService() { var alerts = ListUtils.combine(List.of(alert), getTransitAlert(entitySelector)); transitService.getTransitAlertService().setAlerts(alerts); + var rentalService = new DefaultVehicleRentalService(); + rentalService.addVehicleRentalStation(vehicleRentalStation("abc")); + context = new GraphQLRequestContext( new TestRoutingService(List.of(i1)), transitService, new DefaultFareService(), graph.getVehicleParkingService(), - new DefaultVehicleRentalService(), + rentalService, new DefaultVehiclePositionService(), GraphFinder.getInstance(graph, transitService::findRegularStop), new RouteRequest() @@ -304,6 +308,18 @@ private static FareProduct fareProduct(String name) { ); } + @Nonnull + private static VehicleRentalStation vehicleRentalStation(String name) { + var station = new VehicleRentalStation(); + station.id = id(name); + station.name = I18NString.of(name); + station.longitude = 10; + station.latitude = 20; + station.vehiclesAvailable = 3; + station.spacesAvailable = 2; + return station; + } + /** * Locate 'expectations' relative to the given query input file. The 'expectations' and 'queries' * subdirectories are expected to be in the same directory. diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json new file mode 100644 index 00000000000..cf4ed49c538 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json @@ -0,0 +1,12 @@ +{ + "data" : { + "vehicleRentalStation" : { + "stationId": "F:abc", + "name" : "abc", + "lat" : 20.0, + "lon" : 10.0, + "spacesAvailable" : 2, + "vehiclesAvailable" : 3 + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql new file mode 100644 index 00000000000..836e12a9c04 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql @@ -0,0 +1,10 @@ +{ + vehicleRentalStation(id:"F:abc") { + stationId + name + lat + lon + spacesAvailable + vehiclesAvailable + } +} \ No newline at end of file From bce7526ce1e7263cb9cda66e7e38989e3f992e81 Mon Sep 17 00:00:00 2001 From: Joel Lappalainen Date: Tue, 10 Oct 2023 18:40:32 +0300 Subject: [PATCH 02/60] Apply review feedback --- .../apis/gtfs/datafetchers/QueryTypeImpl.java | 10 +++++----- .../apis/gtfs/GraphQLIntegrationTest.java | 20 +++++++++---------- .../expectations/vehicle-rental-station.json | 4 ++-- .../queries/vehicle-rental-station.graphql | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java index 2e6e9fe219b..750cb0a4929 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java @@ -850,8 +850,8 @@ public DataFetcher vehicleRentalStation() { .stream() .filter(vehicleRentalStation -> OTPFeature.GtfsGraphQlApiRentalStationFuzzyMatching.isOn() - ? isFuzzyMatchRentalStationIds(vehicleRentalStation, id) - : isMatchRentalStationIds(vehicleRentalStation, id) + ? stationIdFuzzyMatches(vehicleRentalStation, id) + : stationIdMatches(vehicleRentalStation, id) ) .findAny() .orElse(null); @@ -899,7 +899,7 @@ public DataFetcher viewer() { /** * This matches station's feedScopedId to the given string. */ - private boolean isMatchRentalStationIds(VehicleRentalStation station, String feedScopedId) { + private boolean stationIdMatches(VehicleRentalStation station, String feedScopedId) { return station.getId().toString().equals(feedScopedId); } @@ -911,9 +911,9 @@ private boolean isMatchRentalStationIds(VehicleRentalStation station, String fee *

* TODO this can be potentially removed after a while, only used by Digitransit as of now. */ - private boolean isFuzzyMatchRentalStationIds(VehicleRentalStation station, String idWithoutFeed) { + private boolean stationIdFuzzyMatches(VehicleRentalStation station, String idWithoutFeed) { if (idWithoutFeed != null && idWithoutFeed.contains(":")) { - return isMatchRentalStationIds(station, idWithoutFeed); + return stationIdMatches(station, idWithoutFeed); } return station.getId().getId().equals(idWithoutFeed); } diff --git a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index 6dd79460e8b..d961de61f57 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -70,6 +70,7 @@ import org.opentripplanner.routing.vehicle_parking.VehicleParking; import org.opentripplanner.service.vehiclepositions.internal.DefaultVehiclePositionService; import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; +import org.opentripplanner.service.vehiclerental.model.TestVehicleRentalStationBuilder; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; import org.opentripplanner.standalone.config.framework.json.JsonSupport; import org.opentripplanner.test.support.FilePatternSource; @@ -218,7 +219,7 @@ public TransitAlertService getTransitAlertService() { transitService.getTransitAlertService().setAlerts(alerts); var rentalService = new DefaultVehicleRentalService(); - rentalService.addVehicleRentalStation(vehicleRentalStation("abc")); + rentalService.addVehicleRentalStation(vehicleRentalStation()); context = new GraphQLRequestContext( @@ -309,15 +310,14 @@ private static FareProduct fareProduct(String name) { } @Nonnull - private static VehicleRentalStation vehicleRentalStation(String name) { - var station = new VehicleRentalStation(); - station.id = id(name); - station.name = I18NString.of(name); - station.longitude = 10; - station.latitude = 20; - station.vehiclesAvailable = 3; - station.spacesAvailable = 2; - return station; + private static VehicleRentalStation vehicleRentalStation() { + return TestVehicleRentalStationBuilder + .of() + .withLongitude(10) + .withLatitude(20) + .withVehicles(3) + .withSpaces(2) + .build(); } /** diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json index cf4ed49c538..892b401ab7b 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json @@ -1,8 +1,8 @@ { "data" : { "vehicleRentalStation" : { - "stationId": "F:abc", - "name" : "abc", + "stationId": "Network-1:FooStation", + "name" : "FooStation", "lat" : 20.0, "lon" : 10.0, "spacesAvailable" : 2, diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql index 836e12a9c04..fc40e65b63e 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql @@ -1,5 +1,5 @@ { - vehicleRentalStation(id:"F:abc") { + vehicleRentalStation(id:"Network-1:FooStation") { stationId name lat From dedbd23a40a0fb4695d1fbc0c595c589b0abaa89 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Fri, 13 Oct 2023 10:27:41 +0200 Subject: [PATCH 03/60] Filter out null, empty and blank elements when mapping feed-scoped ids --- .../mapping/TransitIdMapperTest.java | 72 +++++++++++++++++++ .../TransmodelGraphQLSchema.java | 4 +- .../transmodelapi/mapping/FilterMapper.java | 11 +-- .../mapping/SelectRequestMapper.java | 8 +-- .../mapping/TransitIdMapper.java | 24 +++---- .../mapping/TripRequestMapper.java | 14 ++-- .../model/plan/JourneyWhiteListed.java | 5 +- 7 files changed, 107 insertions(+), 31 deletions(-) create mode 100644 src/ext-test/java/org/opentripplanner/ext/transmodelapi/mapping/TransitIdMapperTest.java diff --git a/src/ext-test/java/org/opentripplanner/ext/transmodelapi/mapping/TransitIdMapperTest.java b/src/ext-test/java/org/opentripplanner/ext/transmodelapi/mapping/TransitIdMapperTest.java new file mode 100644 index 00000000000..df1a00230cb --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/transmodelapi/mapping/TransitIdMapperTest.java @@ -0,0 +1,72 @@ +package org.opentripplanner.ext.transmodelapi.mapping; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +class TransitIdMapperTest { + + private static final String FEED_ID = "xxx"; + private static final String ID = "yyy"; + + @Test + void testMapValidId() { + TransitIdMapper.clearFixedFeedId(); + FeedScopedId mappedID = TransitIdMapper.mapIDToDomain(FEED_ID + ":" + ID); + assertNotNull(mappedID); + assertEquals(FEED_ID, mappedID.getFeedId()); + assertEquals(ID, mappedID.getId()); + } + + @Test + void testMapInvalidId() { + TransitIdMapper.clearFixedFeedId(); + assertThrows(IllegalArgumentException.class, () -> TransitIdMapper.mapIDToDomain("invalid")); + } + + @Test + void testMapNullId() { + assertNull(TransitIdMapper.mapIDToDomain(null)); + } + + @Test + void testMapEmptyId() { + assertNull(TransitIdMapper.mapIDToDomain("")); + } + + @Test + void testMapBlankId() { + assertNull(TransitIdMapper.mapIDToDomain(" ")); + } + + @Test + void testMapNullCollectionOfIds() { + assertNotNull(TransitIdMapper.mapIDsToDomainNullSafe(null)); + } + + @Test + void testMapEmptyCollectionOfIds() { + assertNotNull(TransitIdMapper.mapIDsToDomainNullSafe(Set.of())); + } + + @Test + void testMapCollectionOfNullIds() { + List mappedIds = TransitIdMapper.mapIDsToDomainNullSafe( + Arrays.asList(new String[] { null }) + ); + assertNotNull(mappedIds); + assertTrue(mappedIds.isEmpty()); + } + + @Test + void testMapCollectionOfEmptyIds() { + List mappedIds = TransitIdMapper.mapIDsToDomainNullSafe(List.of("")); + assertNotNull(mappedIds); + assertTrue(mappedIds.isEmpty()); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/transmodelapi/TransmodelGraphQLSchema.java b/src/ext/java/org/opentripplanner/ext/transmodelapi/TransmodelGraphQLSchema.java index cec1dec7402..40a5d936885 100644 --- a/src/ext/java/org/opentripplanner/ext/transmodelapi/TransmodelGraphQLSchema.java +++ b/src/ext/java/org/opentripplanner/ext/transmodelapi/TransmodelGraphQLSchema.java @@ -3,7 +3,7 @@ import static java.lang.Boolean.TRUE; import static java.util.Collections.emptyList; import static org.opentripplanner.ext.transmodelapi.mapping.SeverityMapper.getTransmodelSeverity; -import static org.opentripplanner.ext.transmodelapi.mapping.TransitIdMapper.mapIDsToDomain; +import static org.opentripplanner.ext.transmodelapi.mapping.TransitIdMapper.mapIDsToDomainNullSafe; import static org.opentripplanner.ext.transmodelapi.model.EnumTypes.FILTER_PLACE_TYPE_ENUM; import static org.opentripplanner.ext.transmodelapi.model.EnumTypes.MULTI_MODAL_MODE; import static org.opentripplanner.ext.transmodelapi.model.EnumTypes.TRANSPORT_MODE; @@ -1297,7 +1297,7 @@ private GraphQLSchema create() { .build() ) .dataFetcher(environment -> { - List lineIds = mapIDsToDomain( + List lineIds = mapIDsToDomainNullSafe( environment.getArgumentOrDefault("lines", List.of()) ); List privateCodes = environment.getArgumentOrDefault("privateCodes", List.of()); diff --git a/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/FilterMapper.java b/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/FilterMapper.java index dcfdde7303f..c2282c46e4b 100644 --- a/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/FilterMapper.java +++ b/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/FilterMapper.java @@ -1,6 +1,6 @@ package org.opentripplanner.ext.transmodelapi.mapping; -import static org.opentripplanner.ext.transmodelapi.mapping.TransitIdMapper.mapIDsToDomain; +import static org.opentripplanner.ext.transmodelapi.mapping.TransitIdMapper.mapIDsToDomainNullSafe; import graphql.schema.DataFetchingEnvironment; import java.util.ArrayList; @@ -43,7 +43,7 @@ static void mapFilterOldWay( var bannedAgencies = new ArrayList(); callWith.argument( "banned.authorities", - (Collection authorities) -> bannedAgencies.addAll(mapIDsToDomain(authorities)) + (Collection authorities) -> bannedAgencies.addAll(mapIDsToDomainNullSafe(authorities)) ); if (!bannedAgencies.isEmpty()) { filterRequestBuilder.addNot(SelectRequest.of().withAgencies(bannedAgencies).build()); @@ -52,7 +52,7 @@ static void mapFilterOldWay( var bannedLines = new ArrayList(); callWith.argument( "banned.lines", - (List lines) -> bannedLines.addAll(mapIDsToDomain(lines)) + (List lines) -> bannedLines.addAll(mapIDsToDomainNullSafe(lines)) ); if (!bannedLines.isEmpty()) { filterRequestBuilder.addNot(SelectRequest.of().withRoutes(bannedLines).build()); @@ -63,7 +63,8 @@ static void mapFilterOldWay( var whiteListedAgencies = new ArrayList(); callWith.argument( "whiteListed.authorities", - (Collection authorities) -> whiteListedAgencies.addAll(mapIDsToDomain(authorities)) + (Collection authorities) -> + whiteListedAgencies.addAll(mapIDsToDomainNullSafe(authorities)) ); if (!whiteListedAgencies.isEmpty()) { selectors.add(SelectRequest.of().withAgencies(whiteListedAgencies)); @@ -72,7 +73,7 @@ static void mapFilterOldWay( var whiteListedLines = new ArrayList(); callWith.argument( "whiteListed.lines", - (List lines) -> whiteListedLines.addAll(mapIDsToDomain(lines)) + (List lines) -> whiteListedLines.addAll(mapIDsToDomainNullSafe(lines)) ); if (!whiteListedLines.isEmpty()) { selectors.add(SelectRequest.of().withRoutes(whiteListedLines)); diff --git a/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/SelectRequestMapper.java b/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/SelectRequestMapper.java index 131a2dad3dd..f4699ed7394 100644 --- a/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/SelectRequestMapper.java +++ b/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/SelectRequestMapper.java @@ -1,6 +1,6 @@ package org.opentripplanner.ext.transmodelapi.mapping; -import static org.opentripplanner.ext.transmodelapi.mapping.TransitIdMapper.mapIDsToDomain; +import static org.opentripplanner.ext.transmodelapi.mapping.TransitIdMapper.mapIDsToDomainNullSafe; import java.util.ArrayList; import java.util.List; @@ -19,17 +19,17 @@ static SelectRequest mapSelectRequest(Map> input) { if (input.containsKey("lines")) { var lines = (List) input.get("lines"); - selectRequestBuilder.withRoutes(mapIDsToDomain(lines)); + selectRequestBuilder.withRoutes(mapIDsToDomainNullSafe(lines)); } if (input.containsKey("authorities")) { var authorities = (List) input.get("authorities"); - selectRequestBuilder.withAgencies(mapIDsToDomain(authorities)); + selectRequestBuilder.withAgencies(mapIDsToDomainNullSafe(authorities)); } if (input.containsKey("groupOfLines")) { var groupOfLines = (List) input.get("groupOfLines"); - selectRequestBuilder.withGroupOfRoutes(mapIDsToDomain(groupOfLines)); + selectRequestBuilder.withGroupOfRoutes(mapIDsToDomainNullSafe(groupOfLines)); } if (input.containsKey("transportModes")) { diff --git a/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/TransitIdMapper.java b/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/TransitIdMapper.java index 615439e7957..52d7e0b0aba 100644 --- a/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/TransitIdMapper.java +++ b/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/TransitIdMapper.java @@ -1,10 +1,12 @@ package org.opentripplanner.ext.transmodelapi.mapping; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.opentripplanner.framework.lang.StringUtils; import org.opentripplanner.transit.model.framework.AbstractTransitEntity; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.slf4j.Logger; @@ -32,19 +34,17 @@ public static String mapIDToApi(FeedScopedId id) { return id.toString(); } - public static List mapIDsToDomainNullSafe(Collection ids) { - return (ids == null) ? List.of() : mapIDsToDomain(ids); - } - - public static List mapIDsToDomain(Collection ids) { + /** + * Maps ids to feed-scoped ids. + * Return an empty collection if the collection of ids is null. + * If the collection of ids contains null or blank elements, they are ignored. + */ + @Nonnull + public static List mapIDsToDomainNullSafe(@Nullable Collection ids) { if (ids == null) { - return null; - } - List list = new ArrayList<>(); - for (String id : ids) { - list.add(mapIDToDomain(id)); + return List.of(); } - return list; + return ids.stream().filter(StringUtils::hasValue).map(TransitIdMapper::mapIDToDomain).toList(); } public static FeedScopedId mapIDToDomain(String id) { diff --git a/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/TripRequestMapper.java b/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/TripRequestMapper.java index 2ee2d5b2eda..d3dd06bb945 100644 --- a/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/TripRequestMapper.java +++ b/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/TripRequestMapper.java @@ -1,6 +1,6 @@ package org.opentripplanner.ext.transmodelapi.mapping; -import static org.opentripplanner.ext.transmodelapi.mapping.TransitIdMapper.mapIDsToDomain; +import static org.opentripplanner.ext.transmodelapi.mapping.TransitIdMapper.mapIDsToDomainNullSafe; import graphql.schema.DataFetchingEnvironment; import java.time.Duration; @@ -66,22 +66,23 @@ public static RouteRequest createRequest(DataFetchingEnvironment environment) { callWith.argument( "preferred.authorities", (Collection authorities) -> - request.journey().transit().setPreferredAgencies(mapIDsToDomain(authorities)) + request.journey().transit().setPreferredAgencies(mapIDsToDomainNullSafe(authorities)) ); callWith.argument( "unpreferred.authorities", (Collection authorities) -> - request.journey().transit().setUnpreferredAgencies(mapIDsToDomain(authorities)) + request.journey().transit().setUnpreferredAgencies(mapIDsToDomainNullSafe(authorities)) ); callWith.argument( "preferred.lines", - (List lines) -> request.journey().transit().setPreferredRoutes(mapIDsToDomain(lines)) + (List lines) -> + request.journey().transit().setPreferredRoutes(mapIDsToDomainNullSafe(lines)) ); callWith.argument( "unpreferred.lines", (List lines) -> - request.journey().transit().setUnpreferredRoutes(mapIDsToDomain(lines)) + request.journey().transit().setUnpreferredRoutes(mapIDsToDomainNullSafe(lines)) ); callWith.argument( @@ -103,7 +104,8 @@ public static RouteRequest createRequest(DataFetchingEnvironment environment) { var bannedTrips = new ArrayList(); callWith.argument( "banned.serviceJourneys", - (Collection serviceJourneys) -> bannedTrips.addAll(mapIDsToDomain(serviceJourneys)) + (Collection serviceJourneys) -> + bannedTrips.addAll(mapIDsToDomainNullSafe(serviceJourneys)) ); if (!bannedTrips.isEmpty()) { request.journey().transit().setBannedTrips(bannedTrips); diff --git a/src/ext/java/org/opentripplanner/ext/transmodelapi/model/plan/JourneyWhiteListed.java b/src/ext/java/org/opentripplanner/ext/transmodelapi/model/plan/JourneyWhiteListed.java index daf4864e9c6..f7068060900 100644 --- a/src/ext/java/org/opentripplanner/ext/transmodelapi/model/plan/JourneyWhiteListed.java +++ b/src/ext/java/org/opentripplanner/ext/transmodelapi/model/plan/JourneyWhiteListed.java @@ -46,8 +46,9 @@ public JourneyWhiteListed(DataFetchingEnvironment environment) { this.authorityIds = Set.of(); this.lineIds = Set.of(); } else { - this.authorityIds = Set.copyOf(TransitIdMapper.mapIDsToDomain(whiteList.get("authorities"))); - this.lineIds = Set.copyOf(TransitIdMapper.mapIDsToDomain(whiteList.get("lines"))); + this.authorityIds = + Set.copyOf(TransitIdMapper.mapIDsToDomainNullSafe(whiteList.get("authorities"))); + this.lineIds = Set.copyOf(TransitIdMapper.mapIDsToDomainNullSafe(whiteList.get("lines"))); } } From 89a6b149ebac7c023853edf9e1e4156835a2f7a3 Mon Sep 17 00:00:00 2001 From: Samuel Cedarbaum Date: Mon, 16 Oct 2023 21:29:03 -0400 Subject: [PATCH 04/60] Expose rental station vehicle/space availability per vehicle type --- .../VehicleRentalStationImpl.java | 15 +++- .../gtfs/generated/GraphQLDataFetchers.java | 6 ++ .../apis/gtfs/generated/graphql-codegen.yml | 2 + .../model/RentalVehicleEntityCounts.java | 5 ++ .../model/RentalVehicleTypeCount.java | 3 + .../model/VehicleRentalStation.java | 32 ++++++++ .../opentripplanner/apis/gtfs/schema.graphqls | 30 ++++++- .../apis/gtfs/GraphQLIntegrationTest.java | 13 ++- .../DefaultVehicleRentalServiceTest.java | 3 +- .../TestVehicleRentalStationBuilder.java | 79 ++++++++++++++----- .../street/search/state/TestStateBuilder.java | 4 +- .../expectations/vehicle-rental-station.json | 59 ++++++++++++++ .../queries/vehicle-rental-station.graphql | 43 ++++++++++ 13 files changed, 264 insertions(+), 30 deletions(-) create mode 100644 src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleEntityCounts.java create mode 100644 src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleTypeCount.java create mode 100644 src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json create mode 100644 src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehicleRentalStationImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehicleRentalStationImpl.java index c1256f15c3f..dc60a7c76e8 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehicleRentalStationImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehicleRentalStationImpl.java @@ -4,7 +4,8 @@ import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; -import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; +import org.opentripplanner.service.vehiclerental.model.RentalVehicleEntityCounts; +import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris; public class VehicleRentalStationImpl implements GraphQLDataFetchers.GraphQLVehicleRentalStation { @@ -96,7 +97,17 @@ public DataFetcher vehiclesAvailable() { return environment -> getSource(environment).getVehiclesAvailable(); } - private VehicleRentalPlace getSource(DataFetchingEnvironment environment) { + @Override + public DataFetcher availableVehicles() { + return environment -> getSource(environment).getVehicleTypeCounts(); + } + + @Override + public DataFetcher availableSpaces() { + return environment -> getSource(environment).getVehicleSpaceCounts(); + } + + private VehicleRentalStation getSource(DataFetchingEnvironment environment) { return environment.getSource(); } } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java index 8f2013f75af..b31dac0ea8e 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java @@ -48,7 +48,9 @@ import org.opentripplanner.routing.vehicle_parking.VehicleParkingState; import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle.StopRelationship; +import org.opentripplanner.service.vehiclerental.model.RentalVehicleEntityCounts; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; +import org.opentripplanner.service.vehiclerental.model.RentalVehicleTypeCount; import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris; @@ -1181,6 +1183,10 @@ public interface GraphQLVehicleRentalStation { public DataFetcher stationId(); public DataFetcher vehiclesAvailable(); + + public DataFetcher availableVehicles(); + + public DataFetcher availableSpaces(); } public interface GraphQLVehicleRentalUris { diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml b/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml index 1f5341558c2..ed5726b9bb3 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml @@ -38,6 +38,8 @@ config: BikeRentalStation: org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace#VehicleRentalPlace BikeRentalStationUris: org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris#VehicleRentalStationUris VehicleRentalStation: org.opentripplanner.service.vehiclerental.model.VehicleRentalStation#VehicleRentalStation + RentalVehicleEntityCounts: org.opentripplanner.service.vehiclerental.model.RentalVehicleEntityCounts#RentalVehicleEntityCounts + RentalVehicleTypeCount: org.opentripplanner.service.vehiclerental.model.RentalVehicleTypeCount#RentalVehicleTypeCount RentalVehicle: org.opentripplanner.service.vehiclerental.model.VehicleRentalVehicle#VehicleRentalVehicle VehicleRentalUris: org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris#VehicleRentalStationUris BikesAllowed: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLBikesAllowed#GraphQLBikesAllowed diff --git a/src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleEntityCounts.java b/src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleEntityCounts.java new file mode 100644 index 00000000000..a2ca78b4145 --- /dev/null +++ b/src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleEntityCounts.java @@ -0,0 +1,5 @@ +package org.opentripplanner.service.vehiclerental.model; + +import java.util.List; + +public record RentalVehicleEntityCounts(int total, List byType) {} diff --git a/src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleTypeCount.java b/src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleTypeCount.java new file mode 100644 index 00000000000..9aff8079f2c --- /dev/null +++ b/src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleTypeCount.java @@ -0,0 +1,3 @@ +package org.opentripplanner.service.vehiclerental.model; + +public record RentalVehicleTypeCount(RentalVehicleType vehicleType, int count) {} diff --git a/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalStation.java b/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalStation.java index 2ced6b44909..a31e5e88a0e 100644 --- a/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalStation.java +++ b/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalStation.java @@ -3,6 +3,7 @@ import static java.util.Locale.ROOT; import java.time.Instant; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -188,4 +189,35 @@ public Set formFactors() { getAvailablePickupFormFactors(false) ); } + + /** + * @return Counts of available vehicles by type as well as the total number of available vehicles. + */ + public RentalVehicleEntityCounts getVehicleTypeCounts() { + return new RentalVehicleEntityCounts( + vehiclesAvailable, + vehicleRentalTypeMapToList(vehicleTypesAvailable) + ); + } + + /** + * @return Counts of available vehicle spaces by type as well as the total number of available + * vehicle spaces. + */ + public RentalVehicleEntityCounts getVehicleSpaceCounts() { + return new RentalVehicleEntityCounts( + spacesAvailable, + vehicleRentalTypeMapToList(vehicleSpacesAvailable) + ); + } + + private List vehicleRentalTypeMapToList( + Map vehicleTypeMap + ) { + return vehicleTypeMap + .entrySet() + .stream() + .map(vtc -> new RentalVehicleTypeCount(vtc.getKey(), vtc.getValue())) + .toList(); + } } diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 046e581af30..2dea139f6ea 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -558,7 +558,7 @@ type VehicleRentalStation implements Node & PlaceInterface { Number of vehicles currently available on the rental station. See field `allowPickupNow` to know if is currently possible to pick up a vehicle. """ - vehiclesAvailable: Int + vehiclesAvailable: Int @deprecated(reason: "Use availableVehicles instead, which also contains vehicle types") """ Number of free spaces currently available on the rental station. @@ -567,7 +567,17 @@ type VehicleRentalStation implements Node & PlaceInterface { the rental station, even if the vehicle racks don't have any spaces available. See field `allowDropoffNow` to know if is currently possible to return a vehicle. """ - spacesAvailable: Int + spacesAvailable: Int @deprecated(reason: "Use availableSpaces instead, which also contains the space vehicle types") + + """ + Number of vehicles currently available on the rental station, grouped by vehicle type. + """ + availableVehicles: RentalVehicleEntityCounts! + + """ + Number of free spaces currently available on the rental station, grouped by vehicle type. + """ + availableSpaces: RentalVehicleEntityCounts! """ If true, values of `vehiclesAvailable` and `spacesAvailable` are updated from a @@ -617,6 +627,22 @@ type VehicleRentalStation implements Node & PlaceInterface { operative: Boolean } +type RentalVehicleEntityCounts { + """The total number of entities (e.g. vehicles, spaces).""" + total: Int! + + """The number of entities by type""" + byType: [RentalVehicleTypeCount!]! +} + +type RentalVehicleTypeCount { + """The type of the rental vehicle (scooter, bicycle, car...)""" + vehicleType: RentalVehicleType! + + """The number of vehicles of this type""" + count: Int! +} + """ Rental vehicle represents a vehicle that belongs to a rental network. """ diff --git a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index c1e0d0bbccb..010fdc1adc6 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -73,6 +73,8 @@ import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; +import org.opentripplanner.service.vehiclerental.model.TestVehicleRentalStationBuilder; +import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; import org.opentripplanner.standalone.config.framework.json.JsonSupport; import org.opentripplanner.test.support.FilePatternSource; import org.opentripplanner.transit.model._data.TransitModelForTest; @@ -241,13 +243,22 @@ public TransitAlertService getTransitAlertService() { .build(); realtimeVehicleService.setRealtimeVehicles(pattern, List.of(occypancyVehicle, positionVehicle)); + DefaultVehicleRentalService defaultVehicleRentalService = new DefaultVehicleRentalService(); + VehicleRentalStation vehicleRentalStation = new TestVehicleRentalStationBuilder() + .withVehicles(10) + .withSpaces(10) + .withVehicleTypeBicycle(5, 7) + .withVehicleTypeElectricBicycle(5, 3) + .build(); + defaultVehicleRentalService.addVehicleRentalStation(vehicleRentalStation); + context = new GraphQLRequestContext( new TestRoutingService(List.of(i1)), transitService, new DefaultFareService(), graph.getVehicleParkingService(), - new DefaultVehicleRentalService(), + defaultVehicleRentalService, realtimeVehicleService, GraphFinder.getInstance(graph, transitService::findRegularStop), new RouteRequest() diff --git a/src/test/java/org/opentripplanner/service/vehiclerental/internal/DefaultVehicleRentalServiceTest.java b/src/test/java/org/opentripplanner/service/vehiclerental/internal/DefaultVehicleRentalServiceTest.java index f193b65d986..0a7cfd568aa 100644 --- a/src/test/java/org/opentripplanner/service/vehiclerental/internal/DefaultVehicleRentalServiceTest.java +++ b/src/test/java/org/opentripplanner/service/vehiclerental/internal/DefaultVehicleRentalServiceTest.java @@ -17,8 +17,7 @@ void getVehicleRentalStationForEnvelopeShouldExcludeVehicleRentalVehicle() { DefaultVehicleRentalService defaultVehicleRentalService = new DefaultVehicleRentalService(); VehicleRentalStation vehicleRentalStation = new TestVehicleRentalStationBuilder() - .withLatitude(1) - .withLongitude(1) + .withCoordinates(1, 1) .build(); defaultVehicleRentalService.addVehicleRentalStation(vehicleRentalStation); diff --git a/src/test/java/org/opentripplanner/service/vehiclerental/model/TestVehicleRentalStationBuilder.java b/src/test/java/org/opentripplanner/service/vehiclerental/model/TestVehicleRentalStationBuilder.java index 4557be813d5..33f922ff0b9 100644 --- a/src/test/java/org/opentripplanner/service/vehiclerental/model/TestVehicleRentalStationBuilder.java +++ b/src/test/java/org/opentripplanner/service/vehiclerental/model/TestVehicleRentalStationBuilder.java @@ -1,5 +1,6 @@ package org.opentripplanner.service.vehiclerental.model; +import java.util.HashMap; import java.util.Map; import javax.annotation.Nonnull; import org.opentripplanner.framework.i18n.NonLocalizedString; @@ -18,18 +19,15 @@ public class TestVehicleRentalStationBuilder { private int spaces = 10; private boolean overloadingAllowed = false; private boolean stationOn = false; - private RentalVehicleType vehicleType = RentalVehicleType.getDefaultType(NETWORK_1); + private final Map vehicleTypesAvailable = new HashMap<>(); + private final Map vehicleSpacesAvailable = new HashMap<>(); public static TestVehicleRentalStationBuilder of() { return new TestVehicleRentalStationBuilder(); } - public TestVehicleRentalStationBuilder withLatitude(double latitude) { + public TestVehicleRentalStationBuilder withCoordinates(double latitude, double longitude) { this.latitude = latitude; - return this; - } - - public TestVehicleRentalStationBuilder withLongitude(double longitude) { this.longitude = longitude; return this; } @@ -54,24 +52,55 @@ public TestVehicleRentalStationBuilder withStationOn(boolean stationOn) { return this; } - public TestVehicleRentalStationBuilder withVehicleTypeBicycle() { - return buildVehicleType(RentalFormFactor.BICYCLE); + public TestVehicleRentalStationBuilder withVehicleTypeBicycle(int numAvailable, int numSpaces) { + return buildVehicleType( + RentalFormFactor.BICYCLE, + RentalVehicleType.PropulsionType.HUMAN, + numAvailable, + numSpaces + ); } - public TestVehicleRentalStationBuilder withVehicleTypeCar() { - return buildVehicleType(RentalFormFactor.CAR); + public TestVehicleRentalStationBuilder withVehicleTypeElectricBicycle( + int numAvailable, + int numSpaces + ) { + return buildVehicleType( + RentalFormFactor.BICYCLE, + RentalVehicleType.PropulsionType.ELECTRIC, + numAvailable, + numSpaces + ); + } + + public TestVehicleRentalStationBuilder withVehicleTypeCar(int numAvailable, int numSpaces) { + return buildVehicleType( + RentalFormFactor.CAR, + RentalVehicleType.PropulsionType.ELECTRIC, + numAvailable, + numSpaces + ); } @Nonnull - private TestVehicleRentalStationBuilder buildVehicleType(RentalFormFactor rentalFormFactor) { - this.vehicleType = - new RentalVehicleType( - new FeedScopedId(TestVehicleRentalStationBuilder.NETWORK_1, rentalFormFactor.name()), - rentalFormFactor.name(), - rentalFormFactor, - RentalVehicleType.PropulsionType.ELECTRIC, - 100000d - ); + private TestVehicleRentalStationBuilder buildVehicleType( + RentalFormFactor rentalFormFactor, + RentalVehicleType.PropulsionType propulsionType, + int numAvailable, + int numSpaces + ) { + RentalVehicleType vehicleType = new RentalVehicleType( + new FeedScopedId( + TestVehicleRentalStationBuilder.NETWORK_1, + String.format("%s-%s", rentalFormFactor.name(), propulsionType.name()) + ), + rentalFormFactor.name(), + rentalFormFactor, + propulsionType, + 100000d + ); + this.vehicleTypesAvailable.put(vehicleType, numAvailable); + this.vehicleSpacesAvailable.put(vehicleType, numSpaces); return this; } @@ -84,8 +113,16 @@ public VehicleRentalStation build() { station.longitude = longitude; station.vehiclesAvailable = vehicles; station.spacesAvailable = spaces; - station.vehicleTypesAvailable = Map.of(vehicleType, vehicles); - station.vehicleSpacesAvailable = Map.of(vehicleType, spaces); + + // If no vehicle types are specified, use the default type + if (vehicleTypesAvailable.isEmpty() || vehicleSpacesAvailable.isEmpty()) { + station.vehicleTypesAvailable = Map.of(RentalVehicleType.getDefaultType(NETWORK_1), vehicles); + station.vehicleSpacesAvailable = Map.of(RentalVehicleType.getDefaultType(NETWORK_1), spaces); + } else { + station.vehicleTypesAvailable = vehicleTypesAvailable; + station.vehicleSpacesAvailable = vehicleSpacesAvailable; + } + station.overloadingAllowed = overloadingAllowed; station.isRenting = stationOn; station.isReturning = stationOn; diff --git a/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java b/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java index 9a65e670370..91c390932a8 100644 --- a/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java +++ b/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java @@ -117,7 +117,7 @@ public TestStateBuilder streetEdge() { public TestStateBuilder pickUpCarFromStation() { return pickUpRentalVehicle( RentalFormFactor.CAR, - TestVehicleRentalStationBuilder.of().withVehicleTypeCar().build() + TestVehicleRentalStationBuilder.of().withVehicleTypeCar(10, 10).build() ); } @@ -138,7 +138,7 @@ public TestStateBuilder pickUpFreeFloatingScooter() { public TestStateBuilder pickUpBikeFromStation() { return pickUpRentalVehicle( RentalFormFactor.BICYCLE, - TestVehicleRentalStationBuilder.of().withVehicleTypeBicycle().build() + TestVehicleRentalStationBuilder.of().withVehicleTypeElectricBicycle(10, 10).build() ); } diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json new file mode 100644 index 00000000000..ef1284c5c5e --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json @@ -0,0 +1,59 @@ +{ + "data" : { + "vehicleRentalStation" : { + "stationId" : "Network-1:FooStation", + "name" : "FooStation", + "vehiclesAvailable" : 10, + "availableVehicles" : { + "byType" : [ + { + "vehicleType" : { + "formFactor" : "BICYCLE", + "propulsionType" : "ELECTRIC" + }, + "count" : 5 + }, + { + "vehicleType" : { + "formFactor" : "BICYCLE", + "propulsionType" : "HUMAN" + }, + "count" : 5 + } + ], + "total" : 10 + }, + "spacesAvailable" : 10, + "availableSpaces" : { + "byType" : [ + { + "vehicleType" : { + "formFactor" : "BICYCLE", + "propulsionType" : "ELECTRIC" + }, + "count" : 3 + }, + { + "vehicleType" : { + "formFactor" : "BICYCLE", + "propulsionType" : "HUMAN" + }, + "count" : 7 + } + ], + "total" : 10 + }, + "allowDropoff" : false, + "allowPickup" : false, + "allowDropoffNow" : false, + "allowPickupNow" : false, + "network" : "Network-1", + "lon" : 18.99, + "lat" : 47.51, + "capacity" : null, + "allowOverloading" : false, + "rentalUris" : null, + "operative" : false + } + } +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql new file mode 100644 index 00000000000..8e555ffdbdd --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql @@ -0,0 +1,43 @@ +{ + vehicleRentalStation(id: "Network-1:FooStation") { + stationId + name + vehiclesAvailable + availableVehicles { + byType { + vehicleType { + formFactor + propulsionType + } + count + } + total + } + spacesAvailable + availableSpaces { + byType { + vehicleType { + formFactor + propulsionType + } + count + } + total + } + allowDropoff + allowPickup + allowDropoffNow + allowPickupNow + network + lon + lat + capacity + allowOverloading + rentalUris { + android + ios + web + } + operative + } +} From c2be864def127ce07d23bd5804c9feb215b19d90 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Thu, 19 Oct 2023 15:25:31 +0200 Subject: [PATCH 05/60] Validate stop id in Transit leg reference --- .../model/plan/ScheduledTransitLeg.java | 4 +- .../legreference/LegReferenceSerializer.java | 27 +++--- .../ScheduledTransitLegReference.java | 78 +++++++++++++++- .../LegReferenceSerializerTest.java | 23 +++-- .../ScheduledTransitLegReferenceTest.java | 90 ++++++++++++++++--- .../stoptimes/AlternativeLegsTest.java | 22 ++++- 6 files changed, 204 insertions(+), 40 deletions(-) diff --git a/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java index dc1843b4d52..10158cdc13b 100644 --- a/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java +++ b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java @@ -350,7 +350,9 @@ public LegReference getLegReference() { tripTimes.getTrip().getId(), serviceDate, boardStopPosInPattern, - alightStopPosInPattern + alightStopPosInPattern, + tripPattern.getStops().get(boardStopPosInPattern).getId(), + tripPattern.getStops().get(alightStopPosInPattern).getId() ); } diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java index eee47124f89..44e86f05eea 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java @@ -26,19 +26,6 @@ public class LegReferenceSerializer { private static final Logger LOG = LoggerFactory.getLogger(LegReferenceSerializer.class); - // TODO: This is for backwards compatibility. Change to use ISO_LOCAL_DATE after OTP v2.2 is released - private static final DateTimeFormatter LENIENT_ISO_LOCAL_DATE = new DateTimeFormatterBuilder() - .appendValue(YEAR, 4) - .optionalStart() - .appendLiteral('-') - .optionalEnd() - .appendValue(MONTH_OF_YEAR, 2) - .optionalStart() - .appendLiteral('-') - .optionalEnd() - .appendValue(DAY_OF_MONTH, 2) - .toFormatter(); - /** private constructor to prevent instantiating this utility class */ private LegReferenceSerializer() {} @@ -93,18 +80,28 @@ static void writeScheduledTransitLeg(LegReference ref, ObjectOutputStream out) out.writeUTF(s.serviceDate().toString()); out.writeInt(s.fromStopPositionInPattern()); out.writeInt(s.toStopPositionInPattern()); + out.writeUTF(s.fromStopId().toString()); + out.writeUTF(s.toStopId().toString()); } else { throw new IllegalArgumentException("Invalid LegReference type"); } } + /** + * Deserialize a leg reference. + * To remain backward-compatible, additional fields (stopId) are read optionally. + * TODO: Remove backward-compatible logic after OTP release 2.6 + * + */ static LegReference readScheduledTransitLeg(ObjectInputStream objectInputStream) throws IOException { return new ScheduledTransitLegReference( FeedScopedId.parse(objectInputStream.readUTF()), - LocalDate.parse(objectInputStream.readUTF(), LENIENT_ISO_LOCAL_DATE), + LocalDate.parse(objectInputStream.readUTF(), DateTimeFormatter.ISO_LOCAL_DATE), + objectInputStream.readInt(), objectInputStream.readInt(), - objectInputStream.readInt() + objectInputStream.available() > 0 ? FeedScopedId.parse(objectInputStream.readUTF()) : null, + objectInputStream.available() > 0 ? FeedScopedId.parse(objectInputStream.readUTF()) : null ); } diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java b/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java index e5e2ab695a4..41493416cad 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java @@ -9,6 +9,7 @@ import org.opentripplanner.routing.algorithm.mapping.AlertToLegMapper; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.transit.service.TransitService; @@ -23,7 +24,10 @@ public record ScheduledTransitLegReference( FeedScopedId tripId, LocalDate serviceDate, int fromStopPositionInPattern, - int toStopPositionInPattern + int toStopPositionInPattern, + FeedScopedId fromStopId, + + FeedScopedId toStopId ) implements LegReference { private static final Logger LOG = LoggerFactory.getLogger(ScheduledTransitLegReference.class); @@ -35,6 +39,10 @@ public record ScheduledTransitLegReference( * rolled out, or because a realtime update has modified a trip), * it may not be possible to reconstruct the leg. * In this case the method returns null. + * The method checks that the referenced stop positions still refer to the same stop ids. + * As an exception, the reference is still considered valid if the referenced stop is different + * but belongs to the same parent station: this covers for example the case of a last-minute + * platform change in a train station that typically does not affect the validity of the leg. */ @Override @Nullable @@ -69,6 +77,18 @@ public ScheduledTransitLeg getLeg(TransitService transitService) { return null; } + if ( + !matchReferencedStopInPattern( + tripPattern, + fromStopPositionInPattern, + fromStopId, + transitService + ) || + !matchReferencedStopInPattern(tripPattern, toStopPositionInPattern, toStopId, transitService) + ) { + return null; + } + Timetable timetable = transitService.getTimetableForTripPattern(tripPattern, serviceDate); TripTimes tripTimes = timetable.getTripTimes(trip); @@ -123,4 +143,60 @@ public ScheduledTransitLeg getLeg(TransitService transitService) { return leg; } + + /** + * Return false if the stop id in the reference does not match the actual stop id in the trip + * pattern. + * Return true in the specific case where the stop ids differ, but belong to the same parent + * station. + * + */ + private boolean matchReferencedStopInPattern( + TripPattern tripPattern, + int stopPosition, + FeedScopedId stopId, + TransitService transitService + ) { + if (stopId == null) { + // this is a legacy reference, skip validation + // TODO: remove backward-compatible logic after OTP release 2.6 + return true; + } + + StopLocation stopLocationInPattern = tripPattern.getStops().get(stopPosition); + if (stopId.equals(stopLocationInPattern.getId())) { + return true; + } + StopLocation stopLocationInLegReference = transitService.getStopLocation(stopId); + if ( + stopLocationInLegReference == null || + stopLocationInPattern.getParentStation() == null || + !stopLocationInPattern + .getParentStation() + .equals(stopLocationInLegReference.getParentStation()) + ) { + LOG.info( + "Invalid transit leg reference:" + + " The referenced stop at position {} with id '{}' does not match" + + " the stop id '{}' in trip {} and service date {}", + stopPosition, + stopId, + stopLocationInPattern.getId(), + tripId, + serviceDate + ); + return false; + } + LOG.info( + "Transit leg reference with modified stop id within the same station: " + + "The referenced stop at position {} with id '{}' does not match\" +\n" + + " \" the stop id '{}' in trip {} and service date {}", + stopPosition, + stopId, + stopLocationInPattern.getId(), + tripId, + serviceDate + ); + return true; + } } diff --git a/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java b/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java index 0ef6b240a56..563136432ea 100644 --- a/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java +++ b/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java @@ -1,25 +1,38 @@ package org.opentripplanner.model.plan.legreference; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.time.LocalDate; import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.framework.FeedScopedId; class LegReferenceSerializerTest { - private static final FeedScopedId TRIP_ID = new FeedScopedId("F", "Trip"); + private static final FeedScopedId TRIP_ID = TransitModelForTest.id("Trip"); private static final LocalDate SERVICE_DATE = LocalDate.of(2022, 1, 31); private static final int FROM_STOP_POS = 1; + + private static final FeedScopedId FROM_STOP_ID = TransitModelForTest.id("Boarding Stop"); private static final int TO_STOP_POS = 3; + private static final FeedScopedId TO_STOP_ID = TransitModelForTest.id("Alighting Stop"); private static final String ENCODED_TOKEN = - "rO0ABXc2ABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAAD"; + "rO0ABXdZABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAADAA9GOkJvYXJkaW5nIFN0b3AAEEY6QWxpZ2h0aW5nIFN0b3A="; + private static final String ENCODED_LEGACY_TOKEN = - "rO0ABXc0ABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAIMjAyMjAxMzEAAAABAAAAAw=="; + "rO0ABXc2ABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAAD"; @Test void testScheduledTransitLegReferenceRoundTrip() { - var ref = new ScheduledTransitLegReference(TRIP_ID, SERVICE_DATE, 1, 3); + var ref = new ScheduledTransitLegReference( + TRIP_ID, + SERVICE_DATE, + FROM_STOP_POS, + TO_STOP_POS, + FROM_STOP_ID, + TO_STOP_ID + ); var out = LegReferenceSerializer.encode(ref); @@ -33,7 +46,7 @@ void testScheduledTransitLegReferenceRoundTrip() { @Test void testScheduledTransitLegReferenceDeserialize() { var ref = (ScheduledTransitLegReference) LegReferenceSerializer.decode(ENCODED_TOKEN); - + assertNotNull(ref); assertEquals(TRIP_ID, ref.tripId()); assertEquals(SERVICE_DATE, ref.serviceDate()); assertEquals(FROM_STOP_POS, ref.fromStopPositionInPattern()); diff --git a/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java b/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java index ed54ac1bd44..d44b2eb4857 100644 --- a/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java +++ b/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java @@ -18,6 +18,8 @@ import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.transit.service.DefaultTransitService; @@ -30,19 +32,34 @@ class ScheduledTransitLegReferenceTest { private static final int SERVICE_CODE = 555; private static final LocalDate SERVICE_DATE = LocalDate.of(2023, 1, 1); private static final int NUMBER_OF_STOPS = 3; + public static FeedScopedId stopIdAtPosition0; + public static FeedScopedId stopIdAtPosition1; + + public static FeedScopedId stopIdAtPosition2; private static TransitService transitService; private static FeedScopedId tripId; + private static RegularStop stop4; @BeforeAll static void buildTransitService() { + Station parentStation = TransitModelForTest.station("PARENT_STATION").build(); + + RegularStop stop1 = TransitModelForTest.stopForTest("STOP1", 0, 0); + RegularStop stop2 = TransitModelForTest.stopForTest("STOP2", 0, 0); + RegularStop stop3 = TransitModelForTest.stopForTest("STOP3", 0, 0, parentStation); + stop4 = TransitModelForTest.stopForTest("STOP4", 0, 0, parentStation); + // build transit data TripPattern tripPattern = TransitModelForTest .tripPattern("1", TransitModelForTest.route(id("1")).build()) - .withStopPattern(TransitModelForTest.stopPattern(NUMBER_OF_STOPS)) + .withStopPattern(TransitModelForTest.stopPattern(stop1, stop2, stop3)) .build(); Timetable timetable = tripPattern.getScheduledTimetable(); Trip trip = TransitModelForTest.trip("1").build(); tripId = trip.getId(); + stopIdAtPosition0 = tripPattern.getStop(0).getId(); + stopIdAtPosition1 = tripPattern.getStop(1).getId(); + stopIdAtPosition2 = tripPattern.getStop(2).getId(); TripTimes tripTimes = new TripTimes( trip, TransitModelForTest.stopTimesEvery5Minutes(5, trip, PlanTestConstants.T11_00), @@ -52,7 +69,14 @@ static void buildTransitService() { timetable.addTripTimes(tripTimes); // build transit model - TransitModel transitModel = new TransitModel(StopModel.of().build(), new Deduplicator()); + StopModel stopModel = StopModel + .of() + .withRegularStop(stop1) + .withRegularStop(stop2) + .withRegularStop(stop3) + .withRegularStop(stop4) + .build(); + TransitModel transitModel = new TransitModel(stopModel, new Deduplicator()); transitModel.addTripPattern(tripPattern.getId(), tripPattern); transitModel.getServiceCodes().put(tripPattern.getId(), SERVICE_CODE); CalendarServiceData calendarServiceData = new CalendarServiceData(); @@ -66,20 +90,22 @@ static void buildTransitService() { @Test void getLegFromReference() { - int boardAtStop = 0; - int alightAtStop = 1; + int boardAtStopPos = 0; + int alightAtStopPos = 1; ScheduledTransitLegReference scheduledTransitLegReference = new ScheduledTransitLegReference( tripId, SERVICE_DATE, - boardAtStop, - alightAtStop + boardAtStopPos, + alightAtStopPos, + stopIdAtPosition0, + stopIdAtPosition1 ); ScheduledTransitLeg leg = scheduledTransitLegReference.getLeg(transitService); assertNotNull(leg); assertEquals(tripId, leg.getTrip().getId()); assertEquals(SERVICE_DATE, leg.getServiceDate()); - assertEquals(boardAtStop, leg.getBoardStopPosInPattern()); - assertEquals(alightAtStop, leg.getAlightStopPosInPattern()); + assertEquals(boardAtStopPos, leg.getBoardStopPosInPattern()); + assertEquals(alightAtStopPos, leg.getAlightStopPosInPattern()); } @Test @@ -88,7 +114,9 @@ void getLegFromReferenceUnknownTrip() { FeedScopedId.ofNullable("XXX", "YYY"), SERVICE_DATE, 0, - 1 + 1, + stopIdAtPosition0, + stopIdAtPosition1 ); assertNull(scheduledTransitLegReference.getLeg(transitService)); } @@ -99,29 +127,63 @@ void getLegFromReferenceInvalidServiceDate() { tripId, LocalDate.EPOCH, 0, - 1 + 1, + stopIdAtPosition0, + stopIdAtPosition1 ); assertNull(scheduledTransitLegReference.getLeg(transitService)); } @Test - void getLegFromReferenceInvalidBoardingStop() { + void getLegFromReferenceOutOfRangeBoardingStop() { ScheduledTransitLegReference scheduledTransitLegReference = new ScheduledTransitLegReference( tripId, SERVICE_DATE, NUMBER_OF_STOPS, - 1 + 1, + stopIdAtPosition0, + stopIdAtPosition1 + ); + assertNull(scheduledTransitLegReference.getLeg(transitService)); + } + + @Test + void getLegFromReferenceMismatchOnBoardingStop() { + ScheduledTransitLegReference scheduledTransitLegReference = new ScheduledTransitLegReference( + tripId, + SERVICE_DATE, + 0, + 1, + TransitModelForTest.id("invalid stop id"), + stopIdAtPosition1 ); assertNull(scheduledTransitLegReference.getLeg(transitService)); } @Test - void getLegFromReferenceInvalidAlightingStop() { + void getLegFromReferenceMismatchOnAlightingStopSameParentStation() { + // this tests substitutes the actual alighting stop (stop3) by another stop (stop4) that + // belongs to the same parent station ScheduledTransitLegReference scheduledTransitLegReference = new ScheduledTransitLegReference( tripId, SERVICE_DATE, 0, - NUMBER_OF_STOPS + 2, + stopIdAtPosition0, + stop4.getId() + ); + assertNotNull(scheduledTransitLegReference.getLeg(transitService)); + } + + @Test + void getLegFromReferenceOutOfRangeAlightingStop() { + ScheduledTransitLegReference scheduledTransitLegReference = new ScheduledTransitLegReference( + tripId, + SERVICE_DATE, + 0, + NUMBER_OF_STOPS, + stopIdAtPosition0, + stopIdAtPosition1 ); assertNull(scheduledTransitLegReference.getLeg(transitService)); } diff --git a/src/test/java/org/opentripplanner/routing/stoptimes/AlternativeLegsTest.java b/src/test/java/org/opentripplanner/routing/stoptimes/AlternativeLegsTest.java index db11e81dabc..a7369a98a8c 100644 --- a/src/test/java/org/opentripplanner/routing/stoptimes/AlternativeLegsTest.java +++ b/src/test/java/org/opentripplanner/routing/stoptimes/AlternativeLegsTest.java @@ -21,6 +21,12 @@ */ class AlternativeLegsTest extends GtfsTest { + private static final String FEED_ID = "FEED"; + private static final FeedScopedId STOP_ID_B = new FeedScopedId(FEED_ID, "B"); + private static final FeedScopedId STOP_ID_C = new FeedScopedId(FEED_ID, "C"); + private static final FeedScopedId STOP_ID_X = new FeedScopedId(FEED_ID, "X"); + private static final FeedScopedId STOP_ID_Y = new FeedScopedId(FEED_ID, "Y"); + @Override public String getFeedName() { return "gtfs/simple"; @@ -34,7 +40,9 @@ void testPreviousLegs() { new FeedScopedId(this.feedId.getId(), "1.2"), LocalDate.parse("2022-04-02"), 1, - 2 + 2, + STOP_ID_B, + STOP_ID_C ) .getLeg(transitService); @@ -70,7 +78,9 @@ void testNextLegs() { new FeedScopedId(this.feedId.getId(), "2.2"), LocalDate.parse("2022-04-02"), 0, - 1 + 1, + STOP_ID_B, + STOP_ID_C ) .getLeg(transitService); @@ -106,7 +116,9 @@ void testCircularRoutes() { new FeedScopedId(this.feedId.getId(), "19.1"), LocalDate.parse("2022-04-02"), 1, - 2 + 2, + STOP_ID_X, + STOP_ID_Y ) .getLeg(transitService); @@ -137,7 +149,9 @@ void testComplexCircularRoutes() { new FeedScopedId(this.feedId.getId(), "19.1"), LocalDate.parse("2022-04-02"), 1, - 7 + 7, + STOP_ID_X, + STOP_ID_B ) .getLeg(transitService); From 17937de169f5d766035a4c4c1f5a07a5819fe185 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Fri, 20 Oct 2023 12:41:47 +0200 Subject: [PATCH 06/60] Applied review suggestions --- .../model/plan/legreference/LegReferenceSerializer.java | 2 +- .../plan/legreference/ScheduledTransitLegReference.java | 2 +- .../plan/legreference/LegReferenceSerializerTest.java | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java index 44e86f05eea..8cfabeeb111 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java @@ -90,7 +90,7 @@ static void writeScheduledTransitLeg(LegReference ref, ObjectOutputStream out) /** * Deserialize a leg reference. * To remain backward-compatible, additional fields (stopId) are read optionally. - * TODO: Remove backward-compatible logic after OTP release 2.6 + * TODO: Remove backward-compatible logic after OTP release 2.5 * */ static LegReference readScheduledTransitLeg(ObjectInputStream objectInputStream) diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java b/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java index 41493416cad..ae81a906d12 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java @@ -159,7 +159,7 @@ private boolean matchReferencedStopInPattern( ) { if (stopId == null) { // this is a legacy reference, skip validation - // TODO: remove backward-compatible logic after OTP release 2.6 + // TODO: remove backward-compatible logic after OTP release 2.5 return true; } diff --git a/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java b/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java index 563136432ea..86d900913d5 100644 --- a/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java +++ b/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java @@ -17,9 +17,16 @@ class LegReferenceSerializerTest { private static final FeedScopedId FROM_STOP_ID = TransitModelForTest.id("Boarding Stop"); private static final int TO_STOP_POS = 3; private static final FeedScopedId TO_STOP_ID = TransitModelForTest.id("Alighting Stop"); + + /** + * Token based on the latest format, including stop ids. + */ private static final String ENCODED_TOKEN = "rO0ABXdZABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAADAA9GOkJvYXJkaW5nIFN0b3AAEEY6QWxpZ2h0aW5nIFN0b3A="; + /** + * Token based on the previous format, without stop ids. + */ private static final String ENCODED_LEGACY_TOKEN = "rO0ABXc2ABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAAD"; From 562609b560d952a8bf9ece8b1fcf1a8ec3f0b6da Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Fri, 20 Oct 2023 16:14:23 +0200 Subject: [PATCH 07/60] Add support for scheduled leg reference versioning --- .../legreference/LegReferenceSerializer.java | 46 ++++++++++++------- .../plan/legreference/LegReferenceType.java | 40 ++++++++++++---- .../LegReferenceSerializerTest.java | 14 +++--- 3 files changed, 66 insertions(+), 34 deletions(-) diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java index 8cfabeeb111..067037f54e1 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java @@ -1,18 +1,12 @@ package org.opentripplanner.model.plan.legreference; -import static java.time.temporal.ChronoField.DAY_OF_MONTH; -import static java.time.temporal.ChronoField.MONTH_OF_YEAR; -import static java.time.temporal.ChronoField.YEAR; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; -import java.text.ParseException; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; import java.util.Base64; import javax.annotation.Nullable; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -67,13 +61,25 @@ public static LegReference decode(String legReference) { var type = readEnum(in, LegReferenceType.class); return type.getDeserializer().read(in); - } catch (IOException | ParseException e) { + } catch (IOException e) { LOG.error("Unable to decode leg reference: '" + legReference + "'", e); return null; } } - static void writeScheduledTransitLeg(LegReference ref, ObjectOutputStream out) + static void writeScheduledTransitLegV1(LegReference ref, ObjectOutputStream out) + throws IOException { + if (ref instanceof ScheduledTransitLegReference s) { + out.writeUTF(s.tripId().toString()); + out.writeUTF(s.serviceDate().toString()); + out.writeInt(s.fromStopPositionInPattern()); + out.writeInt(s.toStopPositionInPattern()); + } else { + throw new IllegalArgumentException("Invalid LegReference type"); + } + } + + static void writeScheduledTransitLegV2(LegReference ref, ObjectOutputStream out) throws IOException { if (ref instanceof ScheduledTransitLegReference s) { out.writeUTF(s.tripId().toString()); @@ -87,21 +93,27 @@ static void writeScheduledTransitLeg(LegReference ref, ObjectOutputStream out) } } - /** - * Deserialize a leg reference. - * To remain backward-compatible, additional fields (stopId) are read optionally. - * TODO: Remove backward-compatible logic after OTP release 2.5 - * - */ - static LegReference readScheduledTransitLeg(ObjectInputStream objectInputStream) + static LegReference readScheduledTransitLegV1(ObjectInputStream objectInputStream) throws IOException { return new ScheduledTransitLegReference( FeedScopedId.parse(objectInputStream.readUTF()), LocalDate.parse(objectInputStream.readUTF(), DateTimeFormatter.ISO_LOCAL_DATE), objectInputStream.readInt(), objectInputStream.readInt(), - objectInputStream.available() > 0 ? FeedScopedId.parse(objectInputStream.readUTF()) : null, - objectInputStream.available() > 0 ? FeedScopedId.parse(objectInputStream.readUTF()) : null + null, + null + ); + } + + static LegReference readScheduledTransitLegV2(ObjectInputStream objectInputStream) + throws IOException { + return new ScheduledTransitLegReference( + FeedScopedId.parse(objectInputStream.readUTF()), + LocalDate.parse(objectInputStream.readUTF(), DateTimeFormatter.ISO_LOCAL_DATE), + objectInputStream.readInt(), + objectInputStream.readInt(), + FeedScopedId.parse(objectInputStream.readUTF()), + FeedScopedId.parse(objectInputStream.readUTF()) ); } diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceType.java b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceType.java index 488eb594aa1..44cea2226d3 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceType.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceType.java @@ -3,40 +3,60 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; -import java.text.ParseException; +import java.util.Arrays; +import java.util.Optional; /** * Enum for different types of LegReferences */ enum LegReferenceType { SCHEDULED_TRANSIT_LEG_V1( + 1, ScheduledTransitLegReference.class, - LegReferenceSerializer::writeScheduledTransitLeg, - LegReferenceSerializer::readScheduledTransitLeg + LegReferenceSerializer::writeScheduledTransitLegV1, + LegReferenceSerializer::readScheduledTransitLegV1 + ), + + SCHEDULED_TRANSIT_LEG_V2( + 2, + ScheduledTransitLegReference.class, + LegReferenceSerializer::writeScheduledTransitLegV2, + LegReferenceSerializer::readScheduledTransitLegV2 ); + private final int version; private final Class legReferenceClass; private final Writer serializer; private final Reader deserializer; LegReferenceType( + int version, Class legReferenceClass, Writer serializer, Reader deserializer ) { + this.version = version; this.legReferenceClass = legReferenceClass; this.serializer = serializer; this.deserializer = deserializer; } + /** + * Return the latest LegReferenceType version for a given leg reference class. + */ static LegReferenceType forClass(Class legReferenceClass) { - for (var type : LegReferenceType.values()) { - if (type.legReferenceClass.equals(legReferenceClass)) { - return type; - } - } - return null; + Optional latestVersion = Arrays + .stream(LegReferenceType.values()) + .filter(legReferenceType -> legReferenceType.legReferenceClass.equals(legReferenceClass)) + .reduce((legReferenceType, other) -> { + if (legReferenceType.version > other.version) { + return legReferenceType; + } + return other; + }); + + return latestVersion.orElse(null); } Writer getSerializer() { @@ -54,6 +74,6 @@ interface Writer { @FunctionalInterface interface Reader { - T read(ObjectInputStream in) throws IOException, ParseException; + T read(ObjectInputStream in) throws IOException; } } diff --git a/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java b/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java index 86d900913d5..655c2c24778 100644 --- a/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java +++ b/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java @@ -21,13 +21,13 @@ class LegReferenceSerializerTest { /** * Token based on the latest format, including stop ids. */ - private static final String ENCODED_TOKEN = - "rO0ABXdZABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAADAA9GOkJvYXJkaW5nIFN0b3AAEEY6QWxpZ2h0aW5nIFN0b3A="; + private static final String ENCODED_TOKEN_V2 = + "rO0ABXdZABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjIABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAADAA9GOkJvYXJkaW5nIFN0b3AAEEY6QWxpZ2h0aW5nIFN0b3A="; /** * Token based on the previous format, without stop ids. */ - private static final String ENCODED_LEGACY_TOKEN = + private static final String ENCODED_TOKEN_V1 = "rO0ABXc2ABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAAD"; @Test @@ -43,7 +43,7 @@ void testScheduledTransitLegReferenceRoundTrip() { var out = LegReferenceSerializer.encode(ref); - assertEquals(ENCODED_TOKEN, out); + assertEquals(ENCODED_TOKEN_V2, out); var ref2 = LegReferenceSerializer.decode(out); @@ -52,7 +52,7 @@ void testScheduledTransitLegReferenceRoundTrip() { @Test void testScheduledTransitLegReferenceDeserialize() { - var ref = (ScheduledTransitLegReference) LegReferenceSerializer.decode(ENCODED_TOKEN); + var ref = (ScheduledTransitLegReference) LegReferenceSerializer.decode(ENCODED_TOKEN_V2); assertNotNull(ref); assertEquals(TRIP_ID, ref.tripId()); assertEquals(SERVICE_DATE, ref.serviceDate()); @@ -62,8 +62,8 @@ void testScheduledTransitLegReferenceDeserialize() { @Test void testScheduledTransitLegReferenceLegacyDeserialize() { - var ref = (ScheduledTransitLegReference) LegReferenceSerializer.decode(ENCODED_LEGACY_TOKEN); - + var ref = (ScheduledTransitLegReference) LegReferenceSerializer.decode(ENCODED_TOKEN_V1); + assertNotNull(ref); assertEquals(TRIP_ID, ref.tripId()); assertEquals(SERVICE_DATE, ref.serviceDate()); assertEquals(FROM_STOP_POS, ref.fromStopPositionInPattern()); From 4eca183078e0b3bcb2ea8cd4b762c761c9930393 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:18:57 +0000 Subject: [PATCH 08/60] fix(deps): update dependency org.entur:siri-java-model to v1.25 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index eaf09e96082..6168c658509 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,7 @@ 9.8.0 2.0.9 2.0.14 - 1.22 + 1.25 3.0.2 UTF-8 From b0d49b64dd8b6fa473c9e8beccfd475e2991c60d Mon Sep 17 00:00:00 2001 From: Brede Date: Fri, 20 Oct 2023 16:21:01 -0400 Subject: [PATCH 09/60] Update roadmap_epic.md corrected typos --- .github/ISSUE_TEMPLATE/roadmap_epic.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/roadmap_epic.md b/.github/ISSUE_TEMPLATE/roadmap_epic.md index 0bc8f9c6485..2b38ba3191f 100644 --- a/.github/ISSUE_TEMPLATE/roadmap_epic.md +++ b/.github/ISSUE_TEMPLATE/roadmap_epic.md @@ -7,15 +7,15 @@ assignees: '' --- -### Describe expected behavior: +### Describe expected behavior - What: - Why: - When: ### Linked issue(s) - + -### OTP PO Discussion meeting details: +### OTP PO Discussion meeting details - Date: - Link(s): From cac1e5e296dd74d4081f9d9da782d15f67712603 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Sat, 21 Oct 2023 00:43:59 +0200 Subject: [PATCH 10/60] Fix sort order bug in optimized transfers --- .../raptoradapter/transit/Transfer.java | 10 ++++ .../costfilter/MinCostPathTailFilter.java | 9 +++ .../PassThroughPathTailFilter.java | 10 ++++ .../services/OptimizePathDomainService.java | 6 +- .../services/TransitPathLegSelector.java | 12 ++++ .../OptimizePathDomainServiceTest.java | 56 +++++++++++++++++++ 6 files changed, 100 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/Transfer.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/Transfer.java index 52c0514159f..169a44cb730 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/Transfer.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/Transfer.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Optional; import org.locationtech.jts.geom.Coordinate; +import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.raptor.api.model.RaptorTransfer; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter; import org.opentripplanner.routing.api.request.preference.WalkPreferences; @@ -86,4 +87,13 @@ public Optional asRaptorTransfer(StreetSearchRequest request) { ) ); } + + @Override + public String toString() { + return ToStringBuilder + .of(Transfer.class) + .addNum("toStop", toStop) + .addNum("distance", distanceMeters, "m") + .toString(); + } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/costfilter/MinCostPathTailFilter.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/costfilter/MinCostPathTailFilter.java index 3feb4cb5a1c..8de4c8fa847 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/costfilter/MinCostPathTailFilter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/costfilter/MinCostPathTailFilter.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Set; import java.util.function.ToIntFunction; +import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.routing.algorithm.transferoptimization.model.OptimizedPathTail; import org.opentripplanner.routing.algorithm.transferoptimization.model.PathTailFilter; @@ -64,4 +65,12 @@ private Set> filter( } return result; } + + @Override + public String toString() { + return ToStringBuilder + .of(MinCostPathTailFilter.class) + .addCol("costFunctions", costFunctions) + .toString(); + } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughPathTailFilter.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughPathTailFilter.java index f0e6ec8d24b..46b1a9020b0 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughPathTailFilter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughPathTailFilter.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.request.PassThroughPoint; import org.opentripplanner.routing.algorithm.transferoptimization.model.OptimizedPathTail; @@ -90,4 +91,13 @@ public Set> filterFinalResult(Set> ele return filterChain.filterFinalResult(result); } + + @Override + public String toString() { + return ToStringBuilder + .of(PassThroughPathTailFilter.class) + .addObj("c2Calculator", c2Calculator) + .addObj("filterChain", filterChain) + .toString(); + } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java index e587bff8e70..89669569f2c 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java @@ -106,7 +106,7 @@ public Set> findBestTransitPath(RaptorPath originalPath) { List> transitLegs = originalPath.transitLegs().collect(Collectors.toList()); // Find all possible transfers between each pair of transit legs, and sort on arrival time - var possibleTransfers = sortTransfersOnArrivalTimeInDecOrder( + var possibleTransfers = sortTransfersOnArrivalStopPosInDecOrder( transferGenerator.findAllPossibleTransfers(transitLegs) ); @@ -242,7 +242,7 @@ private OptimizedPathTail createNewTransitLegTail( return tail.mutate().addTransitAndTransferLeg(originalLeg, tx); } - private List>> sortTransfersOnArrivalTimeInDecOrder( + private List>> sortTransfersOnArrivalStopPosInDecOrder( List>> transfers ) { return transfers @@ -250,7 +250,7 @@ private List>> sortTransfersOnArrivalTimeInDecOrder( .map(it -> it .stream() - .sorted(Comparator.comparingInt(l -> -l.to().time())) + .sorted(Comparator.comparingInt(l -> -l.to().stopPosition())) .collect(Collectors.toList()) ) .collect(Collectors.toList()); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransitPathLegSelector.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransitPathLegSelector.java index 9c1af6fc1a4..9e0a4257f57 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransitPathLegSelector.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransitPathLegSelector.java @@ -2,6 +2,7 @@ import java.util.HashSet; import java.util.Set; +import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.routing.algorithm.transferoptimization.model.OptimizedPathTail; import org.opentripplanner.routing.algorithm.transferoptimization.model.PathTailFilter; @@ -75,4 +76,15 @@ Set> next(final int fromStopPosition) { return selectedLegs; } + + @Override + public String toString() { + return ToStringBuilder + .of(TransitPathLegSelector.class) + .addObj("filter", filter) + .addCol("remindingLegs", remindingLegs) + .addCol("selectedLegs", selectedLegs) + .addNum("prevStopPosition", prevStopPosition) + .toString(); + } } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceTest.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceTest.java index 8b7d589852e..222baded224 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceTest.java @@ -7,6 +7,7 @@ import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; import javax.annotation.Nullable; import org.junit.jupiter.api.Test; import org.opentripplanner.raptor._data.RaptorTestConstants; @@ -276,6 +277,61 @@ public void testConstrainedTransferIsPreferred() { ); } + /** + *

+   * DEPARTURE TIMES
+   * Stop        A      B      C      D
+   * Trip 1    10:10  10:10  10:15
+   * Trip 2           10:13  10:13  10:30
+   * 
+ * Case: A trip may have the exact same times for more than one stop. This is a regression test + * see https://github.com/opentripplanner/OpenTripPlanner/issues/5444. + * The following transfers are exist: A-B, A-C, B-B, B-C, C-B and C-C. + * Expect: Transfer B-B, the earliest transfer with the lowest transfer time and cost. + */ + @Test + public void testSameStopTimesInPattern() { + // Given + var trip1 = TestTripSchedule + .schedule() + .pattern("T1", STOP_A, STOP_B, STOP_C) + .times("10:10 10:10 10:15") + .build(); + + var trip2 = TestTripSchedule + .schedule() + .pattern("T2", STOP_B, STOP_C, STOP_D) + .times("10:13 10:13 10:30") + .build(); + + var transfers = dummyTransferGenerator( + List.of( + tx(trip1, STOP_A, trip2, STOP_B).walk(D10s).build(), + tx(trip1, STOP_A, trip2, STOP_C).walk(D10s).build(), + tx(trip1, STOP_B, trip2).build(), + tx(trip1, STOP_B, trip2, STOP_C).walk(D10s).build(), + tx(trip1, STOP_C, trip2, STOP_B).walk(D10s).build(), + tx(trip1, STOP_C, trip2).build() + ) + ); + + var original = pathBuilder() + .access(ITERATION_START_TIME, STOP_A) + .bus(trip1, STOP_B) + .bus(trip2, STOP_D) + .egress(D0s); + + var subject = subject(transfers, null); + + // Find the path with the lowest cost + var result = subject.findBestTransitPath(original); + + assertEquals( + "A ~ BUS T1 10:10 10:10 ~ B ~ BUS T2 10:13 10:30 ~ D [10:09:20 10:30:20 21m 1tx $1300 $33pri]", + result.stream().map(it -> it.toString(this::stopIndexToName)).collect(Collectors.joining()) + ); + } + static TestPathBuilder pathBuilder() { return new TestPathBuilder( new DefaultSlackProvider(TRANSFER_SLACK, BOARD_SLACK, ALIGHT_SLACK), From c608267061b46b46f7c120d20d8195b3b2c75fcd Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 20 Oct 2023 15:28:09 +0200 Subject: [PATCH 11/60] Ignore negative travel-times in Raptor - skip trip alight --- .../standard/ArrivalTimeRoutingStrategy.java | 27 ++++++++++++++++- .../MinTravelDurationRoutingStrategy.java | 29 +++++++++++++++---- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/ArrivalTimeRoutingStrategy.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/ArrivalTimeRoutingStrategy.java index 437dc44d79d..b8143fffef0 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/ArrivalTimeRoutingStrategy.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/ArrivalTimeRoutingStrategy.java @@ -11,6 +11,8 @@ import org.opentripplanner.raptor.spi.RaptorBoardOrAlightEvent; import org.opentripplanner.raptor.spi.RaptorConstrainedBoardingSearch; import org.opentripplanner.raptor.spi.RaptorRoute; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The purpose of this class is to implement a routing strategy for finding the best arrival-time. @@ -27,12 +29,15 @@ public final class ArrivalTimeRoutingStrategy implements RoutingStrategy { + private static final Logger LOG = LoggerFactory.getLogger(ArrivalTimeRoutingStrategy.class); + private static final int NOT_SET = -1; private final StdWorkerState state; private final TimeBasedBoardingSupport boardingSupport; private final TransitCalculator calculator; + private int logCount = 0; private int onTripIndex; private int onTripBoardTime; private int onTripBoardStop; @@ -66,7 +71,14 @@ public void prepareForTransitWith(RaptorRoute route) { public void alightOnlyRegularTransferExist(int stopIndex, int stopPos, int alightSlack) { if (onTripIndex != UNBOUNDED_TRIP_INDEX) { final int stopArrivalTime = calculator.stopArrivalTime(onTrip, stopPos, alightSlack); - state.transitToStop(stopIndex, stopArrivalTime, onTripBoardStop, onTripBoardTime, onTrip); + + // TODO: Make sure that the TimeTables can not have negative trip times, then + // this check can be removed. + if (calculator.isBefore(stopArrivalTime, onTripBoardTime)) { + logInvalidAlightTime(stopPos, stopArrivalTime); + } else { + state.transitToStop(stopIndex, stopArrivalTime, onTripBoardStop, onTripBoardTime, onTrip); + } } } @@ -125,4 +137,17 @@ private int prevArrivalTime(int stopIndex) { private TransitArrival previousTransitArrival(int boardStopIndex) { return state.previousTransit(boardStopIndex); } + + private void logInvalidAlightTime(int stopPos, int stopArrivalTime) { + if (logCount < 3) { + ++logCount; + LOG.error( + "Traveling back in time is not allowed. Board stop pos: {}, alight stop pos: {}, stop arrival time: {}, trip: {}.", + onTrip.findDepartureStopPosition(onTripBoardTime, onTripBoardStop), + stopPos, + stopArrivalTime, + onTrip + ); + } + } } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/MinTravelDurationRoutingStrategy.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/MinTravelDurationRoutingStrategy.java index f854465c51c..26a7708506f 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/MinTravelDurationRoutingStrategy.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/MinTravelDurationRoutingStrategy.java @@ -4,7 +4,6 @@ import org.opentripplanner.raptor.api.model.RaptorAccessEgress; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; -import org.opentripplanner.raptor.api.model.TransitArrival; import org.opentripplanner.raptor.rangeraptor.internalapi.RoutingStrategy; import org.opentripplanner.raptor.rangeraptor.internalapi.WorkerLifeCycle; import org.opentripplanner.raptor.rangeraptor.support.TimeBasedBoardingSupport; @@ -12,6 +11,8 @@ import org.opentripplanner.raptor.spi.RaptorBoardOrAlightEvent; import org.opentripplanner.raptor.spi.RaptorConstrainedBoardingSearch; import org.opentripplanner.raptor.spi.RaptorRoute; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The purpose of this class is to implement a routing strategy for finding the best minimum travel @@ -29,12 +30,15 @@ public final class MinTravelDurationRoutingStrategy implements RoutingStrategy { + private static final Logger LOG = LoggerFactory.getLogger(MinTravelDurationRoutingStrategy.class); + private static final int NOT_SET = -1; private final StdWorkerState state; private final TimeBasedBoardingSupport boardingSupport; private final TransitCalculator calculator; + private int logCount = 0; private int onTripIndex; private int onTripBoardTime; private int onTripBoardStop; @@ -113,10 +117,16 @@ private void alight(int stopIndex, int stopPos, int alightSlackApplied) { // Remove the wait time from the arrival-time. We don´t need to use the transit // calculator because of the way we compute the time-shift. It is positive in the case - // of a forward-search and negative int he case of a reverse-search. + // of a forward-search and negative in the case of a reverse-search. final int stopArrivalTime = stopArrivalTime0 - onTripTimeShift; - state.transitToStop(stopIndex, stopArrivalTime, onTripBoardStop, onTripBoardTime, onTrip); + // TODO: Make sure that the TimeTables can not have negative trip times, then + // this check can be removed. + if (calculator.isBefore(stopArrivalTime, onTripBoardTime)) { + logInvalidAlightTime(stopPos, stopArrivalTime); + } else { + state.transitToStop(stopIndex, stopArrivalTime, onTripBoardStop, onTripBoardTime, onTrip); + } } } @@ -168,7 +178,16 @@ private int prevArrivalTime(int stopIndex) { return state.bestTimePreviousRound(stopIndex); } - private TransitArrival previousTransitArrival(int boardStopIndex) { - return state.previousTransit(boardStopIndex); + private void logInvalidAlightTime(int stopPos, int stopArrivalTime) { + if (logCount < 3) { + ++logCount; + LOG.error( + "Traveling back in time is not allowed. Board stop pos: {}, alight stop pos: {}, stop arrival time: {}, trip: {}.", + onTrip.findDepartureStopPosition(onTripBoardTime, onTripBoardStop), + stopPos, + stopArrivalTime, + onTrip + ); + } } } From 3229c9f9ec43a6ec2f8192984593a79e722445a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 04:35:40 +0000 Subject: [PATCH 12/60] fix(deps): update dependency org.entur.gbfs:gbfs-java-model to v3.0.10 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index eaf09e96082..3b544578f1f 100644 --- a/pom.xml +++ b/pom.xml @@ -714,7 +714,7 @@ org.entur.gbfs gbfs-java-model - 3.0.9 + 3.0.10 From 3d15faa76959aab6294c05d0e2284edf60bc7ed2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 00:46:57 +0000 Subject: [PATCH 13/60] chore(deps): update dependency io.github.git-commit-id:git-commit-id-maven-plugin to v7 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index eaf09e96082..fd9e1e3b7e1 100644 --- a/pom.xml +++ b/pom.xml @@ -322,7 +322,7 @@ but we need the Maven project version as well, so we perform substitution. --> io.github.git-commit-id git-commit-id-maven-plugin - 6.0.0 + 7.0.0 From 65860869407efee637f77c294c965f03875243c5 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Tue, 10 Oct 2023 23:51:54 +0200 Subject: [PATCH 14/60] Clean up fare attribute, remove non-standard senior and youth prices --- .../ext/fares/impl/FareModelForTest.java | 9 ++-- .../ext/fares/impl/HSLFareServiceTest.java | 30 ++++++------- ...reInFreeTransferWindowFareServiceTest.java | 19 ++++---- .../ext/fares/impl/DefaultFareService.java | 22 +--------- .../ext/fares/impl/HSLFareService.java | 4 +- .../ext/fares/model/FareAttribute.java | 39 +++-------------- .../ext/fares/model/FareAttributeBuilder.java | 43 ++----------------- .../gtfs/datafetchers/TicketTypeImpl.java | 10 ++++- .../gtfs/mapping/FareAttributeMapper.java | 13 +++--- .../gtfs/mapping/FareAttributeMapperTest.java | 14 +++--- .../gtfs/mapping/FareRuleMapperTest.java | 3 +- 11 files changed, 60 insertions(+), 146 deletions(-) diff --git a/src/ext-test/java/org/opentripplanner/ext/fares/impl/FareModelForTest.java b/src/ext-test/java/org/opentripplanner/ext/fares/impl/FareModelForTest.java index 5c8b52548e7..8a6fd00f865 100644 --- a/src/ext-test/java/org/opentripplanner/ext/fares/impl/FareModelForTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/fares/impl/FareModelForTest.java @@ -1,7 +1,5 @@ package org.opentripplanner.ext.fares.impl; -import static org.opentripplanner.transit.model._data.TransitModelForTest.FEED_ID; -import static org.opentripplanner.transit.model._data.TransitModelForTest.OTHER_AGENCY; import static org.opentripplanner.transit.model._data.TransitModelForTest.OTHER_FEED_AGENCY; import static org.opentripplanner.transit.model._data.TransitModelForTest.id; @@ -9,6 +7,7 @@ import org.opentripplanner.ext.fares.model.FareRuleSet; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.NonLocalizedString; +import org.opentripplanner.transit.model.basic.Money; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.Route; @@ -57,15 +56,13 @@ public class FareModelForTest { .build(); static FareAttribute TEN_DOLLARS = FareAttribute .of(id("airport-to-city-center")) - .setCurrencyType("USD") - .setPrice(10) + .setPrice(Money.usDollars(10)) .setTransfers(0) .build(); static FareAttribute OTHER_FEED_ATTRIBUTE = FareAttribute .of(FeedScopedId.ofNullable("F2", "other-feed-attribute")) - .setCurrencyType("USD") - .setPrice(10) + .setPrice(Money.usDollars(10)) .setTransfers(1) .setAgency(OTHER_FEED_AGENCY.getId()) .build(); diff --git a/src/ext-test/java/org/opentripplanner/ext/fares/impl/HSLFareServiceTest.java b/src/ext-test/java/org/opentripplanner/ext/fares/impl/HSLFareServiceTest.java index 0a7ec4754e6..74c5b386630 100644 --- a/src/ext-test/java/org/opentripplanner/ext/fares/impl/HSLFareServiceTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/fares/impl/HSLFareServiceTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary; import static org.opentripplanner.transit.model._data.TransitModelForTest.FEED_ID; +import static org.opentripplanner.transit.model.basic.Money.euros; import java.util.LinkedList; import java.util.List; @@ -20,6 +21,7 @@ import org.opentripplanner.routing.core.FareComponent; import org.opentripplanner.routing.core.FareType; import org.opentripplanner.routing.fares.FareService; +import org.opentripplanner.transit.model.basic.Money; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.Route; @@ -84,13 +86,13 @@ private static List createTestCases() { Place D1 = PlanTestConstants.place("D1", 10.0, 12.0, D); Place D2 = PlanTestConstants.place("D2", 10.0, 12.0, D); - float AB_PRICE = 2.80f; - float BC_PRICE = 2.80f; - float CD_PRICE = 3.20f; - float ABC_PRICE = 4.10f; - float BCD_PRICE = 4.10f; - float ABCD_PRICE = 5.70f; - float D_PRICE = 2.80f; + var AB_PRICE = euros(2.80f); + var BC_PRICE = euros(2.80f); + var CD_PRICE = euros(3.20f); + var ABC_PRICE = euros(4.10f); + var BCD_PRICE = euros(4.10f); + var ABCD_PRICE = euros(5.70f); + var D_PRICE = euros(2.80f); HSLFareService hslFareService = new HSLFareService(); int fiveMinutes = 60 * 5; @@ -99,28 +101,24 @@ private static List createTestCases() { FareAttribute fareAttributeAB = FareAttribute .of(new FeedScopedId(FEED_ID, "AB")) - .setCurrencyType("EUR") .setPrice(AB_PRICE) .setTransferDuration(fiveMinutes) .build(); FareAttribute fareAttributeBC = FareAttribute .of(new FeedScopedId(FEED_ID, "BC")) - .setCurrencyType("EUR") .setPrice(BC_PRICE) .setTransferDuration(fiveMinutes) .build(); FareAttribute fareAttributeCD = FareAttribute .of(new FeedScopedId(FEED_ID, "CD")) - .setCurrencyType("EUR") .setPrice(CD_PRICE) .setTransferDuration(fiveMinutes) .build(); FareAttribute fareAttributeD = FareAttribute .of(new FeedScopedId(FEED_ID, "D")) - .setCurrencyType("EUR") .setPrice(D_PRICE) .setTransferDuration(fiveMinutes) //.setAgency(agency1.getId().getId()) @@ -128,34 +126,31 @@ private static List createTestCases() { FareAttribute fareAttributeABC = FareAttribute .of(new FeedScopedId(FEED_ID, "ABC")) - .setCurrencyType("EUR") .setPrice(ABC_PRICE) .setTransferDuration(fiveMinutes) .build(); FareAttribute fareAttributeBCD = FareAttribute .of(new FeedScopedId(FEED_ID, "BCD")) - .setCurrencyType("EUR") .setPrice(BCD_PRICE) .setTransferDuration(fiveMinutes) .build(); FareAttribute fareAttributeABCD = FareAttribute .of(new FeedScopedId(FEED_ID, "ABCD")) - .setCurrencyType("EUR") .setPrice(ABCD_PRICE) .setTransferDuration(fiveMinutes) .build(); FareAttribute fareAttributeD2 = FareAttribute .of(new FeedScopedId(FEED_ID, "D2")) - .setCurrencyType("EUR") + .setPrice(Money.euros(0)) .setAgency(agency2.getId()) .build(); FareAttribute fareAttributeAgency3 = FareAttribute .of(new FeedScopedId("FEED2", "attribute")) - .setCurrencyType("EUR") + .setPrice(Money.euros(0)) .setAgency(agency3.getId()) .build(); @@ -439,8 +434,7 @@ private static List createTestCases() { void unknownFare() { FareAttribute fareAttributeAB = FareAttribute .of(new FeedScopedId(FEED_ID, "AB")) - .setCurrencyType("EUR") - .setPrice(2.80f) + .setPrice(euros(2.80f)) .setTransferDuration(60 * 5) .build(); diff --git a/src/ext-test/java/org/opentripplanner/ext/fares/impl/HighestFareInFreeTransferWindowFareServiceTest.java b/src/ext-test/java/org/opentripplanner/ext/fares/impl/HighestFareInFreeTransferWindowFareServiceTest.java index 0edd54ce3a9..57621c9ab0f 100644 --- a/src/ext-test/java/org/opentripplanner/ext/fares/impl/HighestFareInFreeTransferWindowFareServiceTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/fares/impl/HighestFareInFreeTransferWindowFareServiceTest.java @@ -40,11 +40,10 @@ public void canCalculateFare( String testCaseName, // used to create parameterized test name FareService fareService, Itinerary i, - float expectedFare + Money expectedFare ) { var fares = fareService.calculateFares(i); - final Money expected = Money.usDollars(expectedFare); - assertEquals(expected, fares.getFare(FareType.regular)); + assertEquals(expectedFare, fares.getFare(FareType.regular)); for (var type : fares.getFareTypes()) { assertTrue(fares.getComponents(type).isEmpty()); @@ -55,7 +54,7 @@ public void canCalculateFare( .filter(fp -> fp.name().equals(type.name())) .map(FareProduct::price) .toList(); - assertEquals(List.of(expected), prices); + assertEquals(List.of(expectedFare), prices); } } @@ -71,10 +70,9 @@ private static List createTestCases() { List defaultFareRules = new LinkedList<>(); // $1 fares - float oneDollar = 1.0f; + var oneDollar = Money.usDollars(1.0f); FareAttribute oneDollarFareAttribute = FareAttribute .of(new FeedScopedId(FEED_ID, "oneDollarAttribute")) - .setCurrencyType("USD") .setPrice(oneDollar) .build(); FareRuleSet oneDollarRouteBasedFares = new FareRuleSet(oneDollarFareAttribute); @@ -83,10 +81,9 @@ private static List createTestCases() { defaultFareRules.add(oneDollarRouteBasedFares); // $2 fares - float twoDollars = 2.0f; + var twoDollars = Money.usDollars(2.0f); FareAttribute twoDollarFareAttribute = FareAttribute .of(new FeedScopedId(FEED_ID, "twoDollarAttribute")) - .setCurrencyType("USD") .setPrice(twoDollars) .build(); @@ -166,7 +163,7 @@ private static List createTestCases() { "Two transit legs, second leg starts outside free transfer window", defaultFareService, twoTransitLegSecondRouteHappensAfterFreeTransferWindowPath, - oneDollar + oneDollar + oneDollar.plus(oneDollar) ) ); @@ -182,7 +179,7 @@ private static List createTestCases() { "Three transit legs, second leg starts outside free transfer window, third leg within second free transfer window", defaultFareService, threeTransitLegSecondRouteHappensAfterFreeTransferWindowPath, - oneDollar + oneDollar + oneDollar.plus(oneDollar) ) ); @@ -198,7 +195,7 @@ private static List createTestCases() { "Three transit legs, all starting outside free transfer window", defaultFareService, threeTransitLegAllOutsideFreeTransferWindowPath, - oneDollar + oneDollar + oneDollar + oneDollar.plus(oneDollar).plus(oneDollar) ) ); diff --git a/src/ext/java/org/opentripplanner/ext/fares/impl/DefaultFareService.java b/src/ext/java/org/opentripplanner/ext/fares/impl/DefaultFareService.java index 19ff992f3c7..6b7081de5ec 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/impl/DefaultFareService.java +++ b/src/ext/java/org/opentripplanner/ext/fares/impl/DefaultFareService.java @@ -129,9 +129,7 @@ public ItineraryFares calculateFares(Itinerary itinerary) { // Get the currency from the first fareAttribute, assuming that all tickets use the same currency. if (fareRules != null && !fareRules.isEmpty()) { - Currency currency = Currency.getInstance( - fareRules.iterator().next().getFareAttribute().getCurrencyType() - ); + Currency currency = fareRules.iterator().next().getFareAttribute().getPrice().currency(); boolean feedHasFare = populateFare( currentFare, currency, @@ -359,7 +357,7 @@ protected Optional getBestFareAndId( journeyTime ) ) { - Money newFare = getFarePrice(attribute, fareType); + Money newFare = attribute.getPrice(); if (bestFare == null || newFare.lessThan(bestFare)) { bestAttribute = attribute; bestFare = newFare; @@ -373,22 +371,6 @@ protected Optional getBestFareAndId( .map(attribute -> new FareAndId(finalBestFare, attribute.getId())); } - protected Money getFarePrice(FareAttribute fare, FareType type) { - var currency = Currency.getInstance(fare.getCurrencyType()); - return switch (type) { - case senior: - if (fare.getSeniorPrice() >= 0) { - yield Money.ofFractionalAmount(currency, fare.getSeniorPrice()); - } - case youth: - if (fare.getYouthPrice() >= 0) { - yield Money.ofFractionalAmount(currency, fare.getYouthPrice()); - } - default: - yield Money.ofFractionalAmount(currency, fare.getPrice()); - }; - } - /** * Returns true if two interlined legs (those with a stay-seated transfer between them) should be * treated as a single leg for the purposes of fare calculation. diff --git a/src/ext/java/org/opentripplanner/ext/fares/impl/HSLFareService.java b/src/ext/java/org/opentripplanner/ext/fares/impl/HSLFareService.java index 159d3384dc4..410ca02c615 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/impl/HSLFareService.java +++ b/src/ext/java/org/opentripplanner/ext/fares/impl/HSLFareService.java @@ -118,7 +118,7 @@ but visit temporarily (maybe 1 stop only) an 'external' zone */ Duration.between(lastRideStartTime, startTime).getSeconds() < attribute.getTransferDuration() ) { - Money newFare = getFarePrice(attribute, fareType); + Money newFare = attribute.getPrice(); if (newFare.lessThan(bestSpecialFare)) { bestSpecialFare = newFare; ruleZones = ruleSet.getContains(); @@ -181,7 +181,7 @@ but visit temporarily (maybe 1 stop only) an 'external' zone */ ); } } - Money newFare = getFarePrice(attribute, fareType); + Money newFare = attribute.getPrice(); if (newFare.lessThan(bestFare)) { bestAttribute = attribute; bestFare = newFare; diff --git a/src/ext/java/org/opentripplanner/ext/fares/model/FareAttribute.java b/src/ext/java/org/opentripplanner/ext/fares/model/FareAttribute.java index c42670bbada..2c93fb016fa 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/model/FareAttribute.java +++ b/src/ext/java/org/opentripplanner/ext/fares/model/FareAttribute.java @@ -3,6 +3,7 @@ import java.util.Objects; import javax.annotation.Nonnull; +import org.opentripplanner.transit.model.basic.Money; import org.opentripplanner.transit.model.framework.AbstractTransitEntity; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -10,9 +11,7 @@ public final class FareAttribute extends AbstractTransitEntity { private FeedScopedId agency; - private final float price; - - private final String currencyType; + private final Money price; private final int paymentMethod; @@ -20,24 +19,15 @@ public final class FareAttribute private final Integer transferDuration; - /** youthPrice is an extension to the GTFS spec to support Seattle fare types. */ - private final float youthPrice; - - /** seniorPrice is an extension to the GTFS spec to support Seattle fare types. */ - private final float seniorPrice; - /** This is a proposed extension to the GTFS spec */ private final Integer journeyDuration; FareAttribute(FareAttributeBuilder builder) { super(builder.getId()); - this.price = builder.price(); - this.currencyType = builder.currencyType(); + this.price = Objects.requireNonNull(builder.price()); this.paymentMethod = builder.paymentMethod(); this.transfers = builder.transfers(); this.transferDuration = builder.transferDuration(); - this.youthPrice = builder.youthPrice(); - this.seniorPrice = builder.seniorPrice(); this.journeyDuration = builder.journeyDuration(); this.agency = builder.agency(); } @@ -46,10 +36,6 @@ public static FareAttributeBuilder of(FeedScopedId id) { return new FareAttributeBuilder(id); } - public boolean isAgencySet() { - return agency != null; - } - public FeedScopedId getAgency() { return agency; } @@ -58,14 +44,10 @@ public void setAgency(FeedScopedId agency) { this.agency = agency; } - public float getPrice() { + public Money getPrice() { return price; } - public String getCurrencyType() { - return currencyType; - } - public int getPaymentMethod() { return paymentMethod; } @@ -94,25 +76,14 @@ public Integer getJourneyDuration() { return journeyDuration; } - public float getYouthPrice() { - return youthPrice; - } - - public float getSeniorPrice() { - return seniorPrice; - } - @Override public boolean sameAs(@Nonnull FareAttribute other) { return ( getId().equals(other.getId()) && - price == other.getPrice() && - Objects.equals(currencyType, other.getCurrencyType()) && + getPrice().equals(other.getPrice()) && paymentMethod == other.getPaymentMethod() && Objects.equals(transfers, other.getTransfers()) && Objects.equals(transferDuration, other.getTransferDuration()) && - youthPrice == other.getYouthPrice() && - seniorPrice == other.getSeniorPrice() && Objects.equals(journeyDuration, other.getJourneyDuration()) ); } diff --git a/src/ext/java/org/opentripplanner/ext/fares/model/FareAttributeBuilder.java b/src/ext/java/org/opentripplanner/ext/fares/model/FareAttributeBuilder.java index 6e8e3725b13..c2c7ec407f4 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/model/FareAttributeBuilder.java +++ b/src/ext/java/org/opentripplanner/ext/fares/model/FareAttributeBuilder.java @@ -1,5 +1,6 @@ package org.opentripplanner.ext.fares.model; +import org.opentripplanner.transit.model.basic.Money; import org.opentripplanner.transit.model.framework.AbstractEntityBuilder; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -7,9 +8,7 @@ public class FareAttributeBuilder extends AbstractEntityBuilder { private FeedScopedId agency; - private float price; - - private String currencyType; + private Money price; private int paymentMethod; @@ -17,10 +16,6 @@ public class FareAttributeBuilder private Integer transferDuration; - private float youthPrice; - - private float seniorPrice; - private Integer journeyDuration; FareAttributeBuilder(FeedScopedId id) { @@ -31,12 +26,9 @@ public class FareAttributeBuilder super(original.getId()); this.agency = original.getAgency(); this.price = original.getPrice(); - this.currencyType = original.getCurrencyType(); this.paymentMethod = original.getPaymentMethod(); this.transfers = original.getTransfers(); this.transferDuration = original.getTransferDuration(); - this.youthPrice = original.getYouthPrice(); - this.seniorPrice = original.getSeniorPrice(); this.journeyDuration = original.getJourneyDuration(); } @@ -49,24 +41,15 @@ public FareAttributeBuilder setAgency(FeedScopedId agency) { return this; } - public float price() { + public Money price() { return price; } - public FareAttributeBuilder setPrice(float price) { + public FareAttributeBuilder setPrice(Money price) { this.price = price; return this; } - public String currencyType() { - return currencyType; - } - - public FareAttributeBuilder setCurrencyType(String currencyType) { - this.currencyType = currencyType; - return this; - } - public int paymentMethod() { return paymentMethod; } @@ -103,24 +86,6 @@ public FareAttributeBuilder setJourneyDuration(int journeyDuration) { return this; } - public float youthPrice() { - return youthPrice; - } - - public FareAttributeBuilder setYouthPrice(float youthPrice) { - this.youthPrice = youthPrice; - return this; - } - - public float seniorPrice() { - return seniorPrice; - } - - public FareAttributeBuilder setSeniorPrice(float seniorPrice) { - this.seniorPrice = seniorPrice; - return this; - } - @Override protected FareAttribute buildFromValues() { return new FareAttribute(this); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TicketTypeImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TicketTypeImpl.java index 1d1c0082126..e9da4b45207 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TicketTypeImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TicketTypeImpl.java @@ -12,7 +12,10 @@ public class TicketTypeImpl implements GraphQLDataFetchers.GraphQLTicketType { @Override public DataFetcher currency() { return environment -> - ((FareRuleSet) environment.getSource()).getFareAttribute().getCurrencyType(); + ((FareRuleSet) environment.getSource()).getFareAttribute() + .getPrice() + .currency() + .getCurrencyCode(); } @Override @@ -39,7 +42,10 @@ public DataFetcher price() { symbols.setDecimalSeparator('.'); format.setDecimalFormatSymbols(symbols); String price = format.format( - ((FareRuleSet) environment.getSource()).getFareAttribute().getPrice() + ((FareRuleSet) environment.getSource()).getFareAttribute() + .getPrice() + .fractionalAmount() + .floatValue() ); return Double.valueOf(price); }; diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/FareAttributeMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/FareAttributeMapper.java index 73cdafc6a0e..6b1a195b399 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/FareAttributeMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/FareAttributeMapper.java @@ -3,11 +3,13 @@ import static org.opentripplanner.gtfs.mapping.AgencyAndIdMapper.mapAgencyAndId; import java.util.Collection; +import java.util.Currency; import java.util.HashMap; import java.util.Map; import org.opentripplanner.ext.fares.model.FareAttribute; import org.opentripplanner.ext.fares.model.FareAttributeBuilder; import org.opentripplanner.framework.collection.MapUtils; +import org.opentripplanner.transit.model.basic.Money; import org.opentripplanner.transit.model.framework.FeedScopedId; /** Responsible for mapping GTFS FareAttribute into the OTP model. */ @@ -25,13 +27,14 @@ FareAttribute map(org.onebusaway.gtfs.model.FareAttribute orginal) { } private FareAttribute doMap(org.onebusaway.gtfs.model.FareAttribute rhs) { + var price = Money.ofFractionalAmount( + Currency.getInstance(rhs.getCurrencyType()), + rhs.getPrice() + ); FareAttributeBuilder builder = FareAttribute .of(mapAgencyAndId(rhs.getId())) - .setPrice(rhs.getPrice()) - .setCurrencyType(rhs.getCurrencyType()) - .setPaymentMethod(rhs.getPaymentMethod()) - .setYouthPrice(rhs.getYouthPrice()) - .setSeniorPrice(rhs.getSeniorPrice()); + .setPrice(price) + .setPaymentMethod(rhs.getPaymentMethod()); if (rhs.getId().getAgencyId() != null && rhs.getAgencyId() != null) { builder.setAgency(new FeedScopedId(rhs.getId().getAgencyId(), rhs.getAgencyId())); diff --git a/src/test/java/org/opentripplanner/gtfs/mapping/FareAttributeMapperTest.java b/src/test/java/org/opentripplanner/gtfs/mapping/FareAttributeMapperTest.java index 38ec41e288f..57c0986bd8a 100644 --- a/src/test/java/org/opentripplanner/gtfs/mapping/FareAttributeMapperTest.java +++ b/src/test/java/org/opentripplanner/gtfs/mapping/FareAttributeMapperTest.java @@ -10,9 +10,11 @@ import java.util.Collection; import java.util.Collections; +import java.util.Currency; import org.junit.jupiter.api.Test; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.gtfs.model.FareAttribute; +import org.opentripplanner.transit.model.basic.Money; public class FareAttributeMapperTest { @@ -35,6 +37,7 @@ public class FareAttributeMapperTest { private static final int TRANSFER_DURATION = 3; private static final int TRANSFERS = 2; + private static final Currency NOK = Currency.getInstance("NOK"); private final FareAttributeMapper subject = new FareAttributeMapper(); static { @@ -61,12 +64,9 @@ public void testMap() throws Exception { org.opentripplanner.ext.fares.model.FareAttribute result = subject.map(FARE_ATTRIBUTE); assertEquals("A:1", result.getId().toString()); - assertEquals(CURRENCY_TYPE, result.getCurrencyType()); assertEquals(JOURNEY_DURATION, result.getJourneyDuration()); assertEquals(PAY_MENTMETHOD, result.getPaymentMethod()); - assertEquals(PRICE, result.getPrice(), 0.00001f); - assertEquals(SENIOR_PRICE, result.getSeniorPrice(), 0.00001f); - assertEquals(YOUTH_PRICE, result.getYouthPrice(), 0.00001f); + assertEquals(Money.ofFractionalAmount(NOK, PRICE), result.getPrice()); assertEquals(TRANSFER_DURATION, result.getTransferDuration()); assertEquals(TRANSFERS, result.getTransfers()); } @@ -75,15 +75,13 @@ public void testMap() throws Exception { public void testMapWithNulls() throws Exception { FareAttribute orginal = new FareAttribute(); orginal.setId(ID); + orginal.setCurrencyType("EUR"); org.opentripplanner.ext.fares.model.FareAttribute result = subject.map(orginal); assertNotNull(result.getId()); - assertNull(result.getCurrencyType()); assertFalse(result.isJourneyDurationSet()); assertEquals(0, result.getPaymentMethod()); - assertEquals(0, result.getPrice(), 0.001d); - assertEquals(0, result.getSeniorPrice(), 0.001d); - assertEquals(0, result.getYouthPrice(), 0.001d); + assertEquals(Money.euros(0), result.getPrice()); assertFalse(result.isTransferDurationSet()); assertFalse(result.isTransfersSet()); } diff --git a/src/test/java/org/opentripplanner/gtfs/mapping/FareRuleMapperTest.java b/src/test/java/org/opentripplanner/gtfs/mapping/FareRuleMapperTest.java index 2b4383e9f45..985c7d63262 100644 --- a/src/test/java/org/opentripplanner/gtfs/mapping/FareRuleMapperTest.java +++ b/src/test/java/org/opentripplanner/gtfs/mapping/FareRuleMapperTest.java @@ -41,6 +41,7 @@ public class FareRuleMapperTest { var data = new GtfsTestData(); FARE_ATTRIBUTE.setId(AGENCY_AND_ID); + FARE_ATTRIBUTE.setCurrencyType("USD"); FARE_RULE.setId(ID); FARE_RULE.setContainsId(CONTAINS_ID); @@ -79,7 +80,7 @@ public void testMapWithNulls() throws Exception { assertNull(result.getRoute()); } - /** Mapping the same object twice, should return the the same instance. */ + /** Mapping the same object twice, should return the same instance. */ @Test public void testMapCache() throws Exception { org.opentripplanner.ext.fares.model.FareRule result1 = subject.map(FARE_RULE); From 027f3dd98de13058167be8abc11b6535b0f5494b Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 19 Oct 2023 16:40:32 +0200 Subject: [PATCH 15/60] Remove HTTPS handling and its documentation --- docs/Basic-Tutorial.md | 2 +- docs/Security.md | 38 ------------------ mkdocs.yml | 1 - .../config/CommandLineParameters.java | 9 ----- .../standalone/server/GrizzlyServer.java | 39 ++++--------------- .../config/CommandLineParametersTest.java | 1 - 6 files changed, 9 insertions(+), 81 deletions(-) delete mode 100644 docs/Security.md diff --git a/docs/Basic-Tutorial.md b/docs/Basic-Tutorial.md index 6373355d15f..51c164200e6 100644 --- a/docs/Basic-Tutorial.md +++ b/docs/Basic-Tutorial.md @@ -142,7 +142,7 @@ local machine. It can be informative to watch the HTTP requests and responses be the developer tools in your web browser. OTP's built-in web server will run by default on ports 8080 and 8081 for HTTP and HTTPS respectively. If by any chance some other software is already using one or both of those port numbers, you can specify different port numbers with switches -like `--port 8801 --securePort 8802`. +like `--port 8801`. ## Saving a Graph diff --git a/docs/Security.md b/docs/Security.md deleted file mode 100644 index a17a5a7dbda..00000000000 --- a/docs/Security.md +++ /dev/null @@ -1,38 +0,0 @@ -# Security - -OTP's built-in Grizzly web server is configured to accept HTTPS connections on port 8081 by default, -but the HTTPS listener needs an encryption key to establish a connection. The key is placed in a " -keystore", a format specific to Java server environments. - -## Creating a keystore - -By default, OTP will look for the keystore at `/var/otp/keystore`. To generate a self-signed key for -testing, use the command: - - keytool -genkey -keystore /var/otp/keystore -alias OTPServerKey - -The alias of the key is arbitrary, but it's best to supply one that indicates the purpose of the key -to override the default. `keytool` will ask you a series of questions about you and your -organization; again, any values will do when creating this self-signed test key. `keytool` will also -ask you for a password to protect your keystore and key. This password will eventually be -configurable, but for now it is hard-coded into the OTP server, so you must set the keystore and key -passwords both to `opentrip`. - -Of course with a self-signed key, most clients will (rightfully) refuse to connect without special -permission from the user. You'll need to add a security exception to most web browsers, or add -the `--insecure` switch when using CURL. You could theoretically buy and install a "real" trusted -SSL/TLS certificate it in the keystore using `keytool -gencert`, but since none of the functionality -protected by this encryption is public-facing a self-signed key should be sufficient for most use -cases. All connections to these API methods should be from trusted parties who can verify the -validity of the key with you directly as needed. - -## Testing - -Once you have created a key, start up the OTP server and test that HTTPS access and authentication -are possible. You should also be able to fetch any OTP resources over HTTPS. For example, you could -simply open `https://localhost:8081/index.html` in a browser, or open a raw TLS connection -using `openssl s_client -connect localhost:8081`, then issue the request `GET index.html HTTP/1.1`. - -## Other - -TODO explain CORS, explain adding TLS with reverse proxy e.g. nginx diff --git a/mkdocs.yml b/mkdocs.yml index 65e9c236be1..0de3c76803e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -60,7 +60,6 @@ nav: - System Requirements and Suggestions: 'System-Requirements.md' - Preparing OSM Data: 'Preparing-OSM.md' - Netex and SIRI: 'Netex-Norway.md' - - Security: 'Security.md' - Troubleshooting: 'Troubleshooting-Routing.md' - Comparing OTP2 to OTP1: 'Version-Comparison.md' - APIs: diff --git a/src/main/java/org/opentripplanner/standalone/config/CommandLineParameters.java b/src/main/java/org/opentripplanner/standalone/config/CommandLineParameters.java index cf02d127c52..fb26db4877d 100644 --- a/src/main/java/org/opentripplanner/standalone/config/CommandLineParameters.java +++ b/src/main/java/org/opentripplanner/standalone/config/CommandLineParameters.java @@ -29,7 +29,6 @@ public class CommandLineParameters { private static final String TIP = " Use --help to see available options."; private static final int DEFAULT_PORT = 8080; - private static final int DEFAULT_SECURE_PORT = 8081; private static final String DEFAULT_CACHE_PATH = "/var/otp/cache"; private static final String DEFAULT_BIND_ADDRESS = "0.0.0.0"; @@ -128,13 +127,6 @@ public class CommandLineParameters { ) public Integer port = DEFAULT_PORT; - @Parameter( - names = { "--securePort" }, - validateWith = PositiveInteger.class, - description = "Server port for HTTPS." - ) - public Integer securePort = DEFAULT_SECURE_PORT; - @Parameter( names = { "--visualize" }, description = "Open a graph visualizer window for debugging." @@ -240,7 +232,6 @@ private void validateOneDirectorySet() { private void validatePortsAvailable() { if (doServe()) { checkPortAvailable(port); - checkPortAvailable(securePort); } } diff --git a/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java b/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java index 19f1d72306f..53469bb1591 100644 --- a/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java +++ b/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java @@ -2,7 +2,6 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import jakarta.ws.rs.core.Application; -import java.io.File; import java.io.IOException; import java.net.BindException; import java.time.Duration; @@ -12,8 +11,6 @@ import org.glassfish.grizzly.http.server.HttpServer; import org.glassfish.grizzly.http.server.NetworkListener; import org.glassfish.grizzly.http.server.StaticHttpHandler; -import org.glassfish.grizzly.ssl.SSLContextConfigurator; -import org.glassfish.grizzly.ssl.SSLEngineConfigurator; import org.glassfish.grizzly.threadpool.ThreadPoolConfig; import org.glassfish.jersey.server.ContainerFactory; import org.opentripplanner.framework.application.ApplicationShutdownSupport; @@ -58,19 +55,13 @@ public GrizzlyServer( */ public void run() { LOG.info( - "Starting OTP Grizzly server on ports {} (HTTP) and {} (HTTPS) of interface {}", + "Starting OTP Grizzly server on ports {} of interface {}", params.port, - params.securePort, params.bindAddress ); LOG.info("OTP server base directory is: {}", params.baseDirectory); HttpServer httpServer = new HttpServer(); - /* Configure SSL FIXME OTP2 where will we store they keyfile? */ - SSLContextConfigurator sslConfig = new SSLContextConfigurator(); - sslConfig.setKeyStoreFile(new File(params.getBaseDirectory(), "keystore").getAbsolutePath()); - sslConfig.setKeyStorePass("opentrip"); - // Set up a pool of threads to handle incoming HTTP requests. // According to the Grizzly docs, setting the core and max pool size equal with no queue limit // will use a more efficient fixed-size thread pool implementation. @@ -92,28 +83,14 @@ public void run() { ); httpListener.setSecure(false); - /* HTTPS listener */ - NetworkListener httpsListener = new NetworkListener( - "otp_secure", - params.bindAddress, - params.securePort - ); - // Ideally we'd share the threads between HTTP and HTTPS. - httpsListener.setSecure(true); - httpsListener.setSSLEngineConfig( - new SSLEngineConfigurator(sslConfig).setClientMode(false).setNeedClientAuth(false) - ); - // For both HTTP and HTTPS listeners: enable gzip compression, set thread pool, add listener to httpServer. - for (NetworkListener listener : new NetworkListener[] { httpListener, httpsListener }) { - CompressionConfig cc = listener.getCompressionConfig(); - cc.setCompressionMode(CompressionConfig.CompressionMode.ON); - cc.setCompressionMinSize(50000); // the min number of bytes to compress - cc.setCompressableMimeTypes("application/json", "text/json"); // the mime types to compress - listener.getTransport().setWorkerThreadPoolConfig(threadPoolConfig); - listener.setTransactionTimeout((int) httpTransactionTimeout.toSeconds()); - httpServer.addListener(listener); - } + CompressionConfig cc = httpListener.getCompressionConfig(); + cc.setCompressionMode(CompressionConfig.CompressionMode.ON); + cc.setCompressionMinSize(50000); // the min number of bytes to compress + cc.setCompressableMimeTypes("application/json", "text/json"); // the mime types to compress + httpListener.getTransport().setWorkerThreadPoolConfig(threadPoolConfig); + httpListener.setTransactionTimeout((int) httpTransactionTimeout.toSeconds()); + httpServer.addListener(httpListener); /* Add a few handlers (~= servlets) to the Grizzly server. */ diff --git a/src/test/java/org/opentripplanner/standalone/config/CommandLineParametersTest.java b/src/test/java/org/opentripplanner/standalone/config/CommandLineParametersTest.java index fa2dadd8f95..82c3589202c 100644 --- a/src/test/java/org/opentripplanner/standalone/config/CommandLineParametersTest.java +++ b/src/test/java/org/opentripplanner/standalone/config/CommandLineParametersTest.java @@ -20,7 +20,6 @@ public class CommandLineParametersTest { public void setUp() { subject = CommandLineParameters.createCliForTest(BASE_DIR); subject.port = 13524; - subject.securePort = 13525; } @Test From 8517f893c9529a3e984bb395a53232917623a2f5 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Mon, 23 Oct 2023 11:34:03 +0200 Subject: [PATCH 16/60] Fix spelling --- .../org/opentripplanner/standalone/server/GrizzlyServer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java b/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java index 53469bb1591..09741bc475e 100644 --- a/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java +++ b/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java @@ -55,7 +55,7 @@ public GrizzlyServer( */ public void run() { LOG.info( - "Starting OTP Grizzly server on ports {} of interface {}", + "Starting OTP Grizzly server on port {} of interface {}", params.port, params.bindAddress ); From c4ae04a0744b4da2de116a6330d09ad0552b82ae Mon Sep 17 00:00:00 2001 From: eibakke Date: Mon, 23 Oct 2023 14:40:29 +0200 Subject: [PATCH 17/60] Creates the NumItinerariesFilter and the associated NumItinerariesFilterResults to limit itineraries based on the numItineraries parameter. Pulling this functionality into a separate filter from the MaxLimitFilter because of the need to keep more information about the filtered itineraries than just the first one that is removed. The NumItinerariesFilterResults contains information used in the page cursors currently as well as information that will be used in the future deduplication feature. --- .../opentripplanner/model/plan/SortOrder.java | 4 +- .../plan/pagecursor/PageCursorFactory.java | 103 ++++++---------- .../routing/algorithm/RoutingWorker.java | 14 +-- .../ItineraryListFilterChainBuilder.java | 34 +++--- .../deletionflagger/MaxLimitFilter.java | 32 +---- .../deletionflagger/NumItinerariesFilter.java | 65 ++++++++++ .../NumItinerariesFilterResults.java | 95 +++++++++++++++ .../RouteRequestToFilterChainMapper.java | 6 +- .../mapping/RoutingResponseMapper.java | 68 ++++++----- .../routing/api/request/RouteRequest.java | 4 +- .../pagecursor/PageCursorFactoryTest.java | 54 ++++++++- .../deletionflagger/MaxLimitFilterTest.java | 34 ------ .../NumItinerariesFilterTest.java | 111 ++++++++++++++++++ .../mapping/RoutingResponseMapperTest.java | 22 +++- 14 files changed, 446 insertions(+), 200 deletions(-) create mode 100644 src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilter.java create mode 100644 src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterResults.java create mode 100644 src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterTest.java diff --git a/src/main/java/org/opentripplanner/model/plan/SortOrder.java b/src/main/java/org/opentripplanner/model/plan/SortOrder.java index 1b5b3272a32..812973ebb4d 100644 --- a/src/main/java/org/opentripplanner/model/plan/SortOrder.java +++ b/src/main/java/org/opentripplanner/model/plan/SortOrder.java @@ -36,10 +36,10 @@ public enum SortOrder { * paging we need to know which end of the list of itineraries we should crop. This method is used * to decide that together with the current page type (next/previous). *

- * This return {@code true} for the default depart-after search, and {@code false} for an + * This returns {@code true} for the default depart-after search, and {@code false} for an * arrive-by search. */ - public boolean isSortedByArrivalTimeAcceding() { + public boolean isSortedByArrivalTimeAscending() { return this == STREET_AND_ARRIVAL_TIME; } } diff --git a/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactory.java b/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactory.java index afed96fd051..4a7bd71aa44 100644 --- a/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactory.java +++ b/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactory.java @@ -9,6 +9,7 @@ import javax.annotation.Nullable; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.model.plan.SortOrder; +import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.NumItinerariesFilterResults; public class PageCursorFactory { @@ -18,8 +19,7 @@ public class PageCursorFactory { private SearchTime current = null; private Duration currentSearchWindow = null; private boolean wholeSwUsed = true; - private Instant removedItineraryStartTime = null; - private Instant removedItineraryEndTime = null; + private NumItinerariesFilterResults numItinerariesFilterResults = null; private PageCursor nextCursor = null; private PageCursor prevCursor = null; @@ -48,27 +48,18 @@ public PageCursorFactory withOriginalSearch( } /** - * Set the start and end time for removed itineraries. The current implementation uses the FIRST - * removed itinerary, but this can in some cases lead to missed itineraries in the next search. - * So, we will document here what should be done. - *

- * For case {@code depart-after-crop-sw} and {@code arrive-by-crop-sw-reversed-filter} the {@code - * startTime} should be the EARLIEST departure time for all removed itineraries. - *

- * For case {@code depart-after-crop-sw-reversed-filter} and {@code arrive-by-crop-sw} the {@code - * startTime} should be the LATEST departure time for all removed itineraries. - *

- * The {@code endTime} should be replaced by removing duplicates between the to pages. This can - * for example be done by including a hash for each potential itinerary in the token, and make a - * filter to remove those in the following page response. + * If there were itineraries removed in the current search because the numItineraries parameter + * was used, then we want to allow the caller to move within some of the itineraries that were + * removed in the next and previous pages. This means we will use information from when we cropped + * the list of itineraries to create the new search encoded in the page cursors. * - * @param startTime is rounded down to the closest minute. - * @param endTime is round up to the closest minute. + * @param numItinerariesFilterResults the result from the {@code NumItinerariesFilter} */ - public PageCursorFactory withRemovedItineraries(Instant startTime, Instant endTime) { + public PageCursorFactory withRemovedItineraries( + NumItinerariesFilterResults numItinerariesFilterResults + ) { this.wholeSwUsed = false; - this.removedItineraryStartTime = startTime.truncatedTo(ChronoUnit.MINUTES); - this.removedItineraryEndTime = endTime.plusSeconds(59).truncatedTo(ChronoUnit.MINUTES); + this.numItinerariesFilterResults = numItinerariesFilterResults; return this; } @@ -94,8 +85,7 @@ public String toString() { .addDuration("currentSearchWindow", currentSearchWindow) .addDuration("newSearchWindow", newSearchWindow) .addBoolIfTrue("searchWindowCropped", !wholeSwUsed) - .addDateTime("removedItineraryStartTime", removedItineraryStartTime) - .addDateTime("removedItineraryEndTime", removedItineraryEndTime) + .addObj("numItinerariesFilterResults", numItinerariesFilterResults) .addObj("nextCursor", nextCursor) .addObj("prevCursor", prevCursor) .toString(); @@ -107,7 +97,7 @@ public String toString() { * equivalent when creating new cursors. */ private static PageType resolvePageTypeForTheFirstSearch(SortOrder sortOrder) { - return sortOrder.isSortedByArrivalTimeAcceding() ? NEXT_PAGE : PREVIOUS_PAGE; + return sortOrder.isSortedByArrivalTimeAscending() ? NEXT_PAGE : PREVIOUS_PAGE; } /** Create page cursor pair (next and previous) */ @@ -119,64 +109,41 @@ private void createPageCursors() { SearchTime prev = new SearchTime(null, null); SearchTime next = new SearchTime(null, null); - // Depart after, sort on arrival time with the earliest first - if (sortOrder.isSortedByArrivalTimeAcceding()) { - if (currentPageType == NEXT_PAGE) { - prev.edt = calcPrevSwStartRelativeToUsedSw(); - next.edt = wholeSwUsed ? calcNextSwStartRelativeToUsedSw() : removedItineraryStartTime; + if (wholeSwUsed) { + prev.edt = edtBeforeNewSw(); + next.edt = edtAfterUsedSw(); + if (!sortOrder.isSortedByArrivalTimeAscending()) { + prev.lat = current.lat; } - // current page type == PREV_PAGE - else { - if (wholeSwUsed) { - prev.edt = calcPrevSwStartRelativeToUsedSw(); + } else { // If the whole search window was not used (i.e. if there were removed itineraries) + if (currentPageType == NEXT_PAGE) { + prev.edt = edtBeforeNewSw(); + next.edt = numItinerariesFilterResults.earliestRemovedDeparture; + if (sortOrder.isSortedByArrivalTimeAscending()) { + prev.lat = + numItinerariesFilterResults.earliestKeptArrival.truncatedTo(ChronoUnit.MINUTES); } else { - //TODO: The start time for the removed itinerary is not the best thing to use - // here. We should take the LATEST start time of all removed itineraries - // instead. - prev.edt = calcPrevSwStartRelativeToRmItinerary(); - prev.lat = removedItineraryEndTime; - } - next.edt = calcNextSwStartRelativeToUsedSw(); - } - } - // Arrive-by, sort on departure time with the latest first - else { - if (currentPageType == PREVIOUS_PAGE) { - if (wholeSwUsed) { - prev.edt = calcPrevSwStartRelativeToUsedSw(); prev.lat = current.lat; - } else { - prev.edt = calcPrevSwStartRelativeToRmItinerary(); - // TODO: Replace this by hashing removed itineraries - prev.lat = removedItineraryEndTime; } - next.edt = calcNextSwStartRelativeToUsedSw(); - } - // Use normal sort and removal in ItineraryFilterChain - else { - prev.edt = calcPrevSwStartRelativeToUsedSw(); - prev.lat = current.lat; - next.edt = wholeSwUsed ? calcNextSwStartRelativeToUsedSw() : removedItineraryStartTime; + } else { + // The search-window start and end is [inclusive, exclusive], so to calculate the start of the + // search-window from the last time included in the search window we need to include one extra + // minute at the end. + prev.edt = + numItinerariesFilterResults.latestRemovedDeparture.minus(newSearchWindow).plusSeconds(60); + next.edt = edtAfterUsedSw(); + prev.lat = numItinerariesFilterResults.latestRemovedArrival; } } prevCursor = new PageCursor(PREVIOUS_PAGE, sortOrder, prev.edt, prev.lat, newSearchWindow); nextCursor = new PageCursor(NEXT_PAGE, sortOrder, next.edt, next.lat, newSearchWindow); } - /** - * The search-window start and end is [inclusive, exclusive], so to calculate the start of the - * search-window from the last time included in the search window we need to include one extra - * minute at the end. - */ - private Instant calcPrevSwStartRelativeToRmItinerary() { - return removedItineraryStartTime.minus(newSearchWindow).plusSeconds(60); - } - - private Instant calcPrevSwStartRelativeToUsedSw() { + private Instant edtBeforeNewSw() { return current.edt.minus(newSearchWindow); } - private Instant calcNextSwStartRelativeToUsedSw() { + private Instant edtAfterUsedSw() { return current.edt.plus(currentSearchWindow); } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java b/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java index f83069c1683..39eea303b10 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java @@ -11,7 +11,6 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; -import org.opentripplanner.ext.ridehailing.RideHailingService; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.framework.time.ServiceDateUtils; @@ -20,6 +19,7 @@ import org.opentripplanner.raptor.api.request.RaptorTuningParameters; import org.opentripplanner.raptor.api.request.SearchParams; import org.opentripplanner.routing.algorithm.filterchain.ItineraryListFilterChain; +import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.NumItinerariesFilterResults; import org.opentripplanner.routing.algorithm.mapping.RouteRequestToFilterChainMapper; import org.opentripplanner.routing.algorithm.mapping.RoutingResponseMapper; import org.opentripplanner.routing.algorithm.raptoradapter.router.AdditionalSearchDays; @@ -65,7 +65,7 @@ public class RoutingWorker { private final ZonedDateTime transitSearchTimeZero; private final AdditionalSearchDays additionalSearchDays; private SearchParams raptorSearchParamsUsed = null; - private Itinerary firstRemovedItinerary = null; + private NumItinerariesFilterResults numItinerariesFilterResults = null; public RoutingWorker(OtpServerRequestContext serverContext, RouteRequest request, ZoneId zoneId) { request.applyPageCursor(); @@ -103,7 +103,7 @@ public RoutingResponse route() { var routingErrors = Collections.synchronizedSet(new HashSet()); if (OTPFeature.ParallelRouting.isOn()) { - // TODO: This is not using {@link OtpRequestThreadFactory} witch mean we do not get + // TODO: This is not using {@link OtpRequestThreadFactory} which means we do not get // log-trace-parameters-propagation and graceful timeout handling here. try { CompletableFuture @@ -139,7 +139,7 @@ public RoutingResponse route() { filterOnLatestDepartureTime(), emptyDirectModeHandler.removeWalkAllTheWayResults() || removeWalkAllTheWayResultsFromDirectFlex, - it -> firstRemovedItinerary = it + res -> numItinerariesFilterResults = res ); List filteredItineraries = filterChain.filter(itineraries); @@ -167,7 +167,7 @@ public RoutingResponse route() { transitSearchTimeZero, raptorSearchParamsUsed, searchWindowNextSearch, - firstRemovedItinerary, + numItinerariesFilterResults, filteredItineraries, routingErrors, debugTimingAggregator, @@ -276,11 +276,11 @@ private Duration calculateSearchWindowNextSearch(List itineraries) { var sw = Duration.ofSeconds(raptorSearchParamsUsed.searchWindowInSeconds()); // SearchWindow cropped -> decrease search-window - if (firstRemovedItinerary != null) { + if (numItinerariesFilterResults != null) { Instant swStartTime = searchStartTime() .plusSeconds(raptorSearchParamsUsed.earliestDepartureTime()); boolean cropSWHead = request.doCropSearchWindowAtTail(); - Instant rmItineraryStartTime = firstRemovedItinerary.startTime().toInstant(); + Instant rmItineraryStartTime = numItinerariesFilterResults.firstRemovedDepartureTime; return pagingSearchWindowAdjuster.decreaseSearchWindow( sw, diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java index a0f03427e64..f66a713f9b1 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java @@ -17,6 +17,8 @@ import org.opentripplanner.routing.algorithm.filterchain.comparator.SortOrderComparator; import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.MaxLimitFilter; import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.NonTransitGeneralizedCostFilter; +import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.NumItinerariesFilter; +import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.NumItinerariesFilterResults; import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.OtherThanSameLegsMaxGeneralizedCostFilter; import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.OutsideSearchWindowFilter; import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveBikerentalWithMostlyWalkingFilter; @@ -65,7 +67,7 @@ public class ItineraryListFilterChainBuilder { private double parkAndRideDurationRatio; private CostLinearFunction nonTransitGeneralizedCostLimit; private Instant latestDepartureTimeLimit = null; - private Consumer maxLimitReachedSubscriber; + private Consumer numItinerariesFilterResultsConsumer; private boolean accessibilityScore; private double wheelchairMaxSlope; private FareService faresService; @@ -111,7 +113,7 @@ public ItineraryListFilterChainBuilder withMaxNumberOfItinerariesCrop(ListSectio /** * Group itineraries by the main legs and keeping approximately the given total number of - * itineraries. The itineraries are grouped by the legs that account for more then 'p' % for the + * itineraries. The itineraries are grouped by the legs that account for more than 'p' % for the * total distance. * * @see GroupBySimilarity for more details. @@ -186,7 +188,7 @@ public ItineraryListFilterChainBuilder withParkAndRideDurationRatio(double value /** * The direct street search(walk, bicycle, car) is not pruning the transit search, so in some * cases we get "silly" transit itineraries that is marginally better on travel-duration compared - * with a on-street-all-the-way itinerary. Use this method to filter worse enough itineraries. + * with an on-street-all-the-way itinerary. Use this method to filter worse enough itineraries. *

* The filter removes all itineraries with a generalized-cost that is higher than the best * on-street-all-the-way itinerary. @@ -205,7 +207,7 @@ public ItineraryListFilterChainBuilder withRemoveTransitWithHigherCostThanBestOn * A transit itinerary with higher generalized-cost than a walk-only itinerary is silly. This filter removes such * itineraries. *

- * This filter only have an effect, if an walk-all-the-way itinerary exist. + * This filter only have an effect, if a walk-all-the-way itinerary exist. */ public ItineraryListFilterChainBuilder withRemoveTransitIfWalkingIsBetter(boolean value) { this.removeTransitIfWalkingIsBetter = value; @@ -234,18 +236,17 @@ public ItineraryListFilterChainBuilder withLatestDepartureTimeLimit( /** * If the maximum number of itineraries is exceeded, then the excess itineraries are removed. To - * get notified about this a subscriber can be added. The first itinerary removed by the {@code - * maxLimit} is returned. The 'maxLimit' check is the last thing happening in the filter-chain - * after the final sort. So, if another filter remove an itinerary, the itinerary is not - * considered with the respect to this the {@link #withMaxNumberOfItineraries(int)} limit. + * get notified about this a consumer can be added. The 'maxLimit' check is the last thing + * happening in the filter-chain after the final sort. So, if another filter removes an itinerary, + * the itinerary is not considered with respect to the {@link #withMaxNumberOfItineraries(int)} + * limit. * - * @param maxLimitReachedSubscriber the subscriber to notify in case any elements are removed. - * Only the first element removed is passed to the subscriber. + * @param numItinerariesFilterResultsConsumer the consumer to notify when elements are removed. */ - public ItineraryListFilterChainBuilder withMaxLimitReachedSubscriber( - Consumer maxLimitReachedSubscriber + public ItineraryListFilterChainBuilder withNumItinerariesFilterResultsConsumer( + Consumer numItinerariesFilterResultsConsumer ) { - this.maxLimitReachedSubscriber = maxLimitReachedSubscriber; + this.numItinerariesFilterResultsConsumer = numItinerariesFilterResultsConsumer; return this; } @@ -410,11 +411,10 @@ public ItineraryListFilterChain build() { filters.add(new SortingFilter(SortOrderComparator.comparator(sortOrder))); filters.add( new DeletionFlaggingFilter( - new MaxLimitFilter( - MAX_NUMBER_OF_ITINERARIES_TAG, + new NumItinerariesFilter( maxNumberOfItineraries, maxNumberOfItinerariesCrop, - maxLimitReachedSubscriber + numItinerariesFilterResultsConsumer ) ) ); @@ -465,7 +465,7 @@ private List buildGroupBySameRoutesAndStopsFilter() { * of the total travel distance. *

* Each group is filtered using generalized-cost, keeping only the itineraries with the lowest - * cost. If there is a tie, the filter look at the number-of-transfers as a tie breaker. + * cost. If there is a tie, the filter look at the number-of-transfers as a tiebreaker. *

* The filter name is dynamically created: similar-legs-filter-68p-1 */ diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/MaxLimitFilter.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/MaxLimitFilter.java index 0de9ced8a3b..3a9d1422d34 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/MaxLimitFilter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/MaxLimitFilter.java @@ -1,41 +1,20 @@ package org.opentripplanner.routing.algorithm.filterchain.deletionflagger; import java.util.List; -import java.util.function.Consumer; import org.opentripplanner.model.plan.Itinerary; -import org.opentripplanner.routing.algorithm.filterchain.ListSection; /** - * Flag all itineraries after the provided limit. This flags the itineraries at the end of the list - * for removal, so the list should be sorted on the desired key before this filter is applied. - *

- * The filter can also report the first itinerary in the list it will flag. The subscriber is - * optional. + * Flags the itineraries at the end of the list for removal. The list should be sorted on the + * desired key before this filter is applied. */ public class MaxLimitFilter implements ItineraryDeletionFlagger { - private static final Consumer IGNORE_SUBSCRIBER = i -> {}; - private final String name; private final int maxLimit; - private final ListSection cropSection; - private final Consumer changedSubscriber; - /** Create filter with default crop(TAIL) and without any callback. */ public MaxLimitFilter(String name, int maxLimit) { - this(name, maxLimit, ListSection.TAIL, IGNORE_SUBSCRIBER); - } - - public MaxLimitFilter( - String name, - int maxLimit, - ListSection cropSection, - Consumer changedSubscriber - ) { this.name = name; this.maxLimit = maxLimit; - this.cropSection = cropSection; - this.changedSubscriber = changedSubscriber == null ? IGNORE_SUBSCRIBER : changedSubscriber; } @Override @@ -49,13 +28,6 @@ public List flagForRemoval(List itineraries) { return List.of(); } - if (cropSection == ListSection.HEAD) { - int limit = itineraries.size() - maxLimit; - changedSubscriber.accept(itineraries.get(limit - 1)); - return itineraries.subList(0, limit); - } - - changedSubscriber.accept(itineraries.get(maxLimit)); return itineraries.subList(maxLimit, itineraries.size()); } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilter.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilter.java new file mode 100644 index 00000000000..374dd9f4d93 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilter.java @@ -0,0 +1,65 @@ +package org.opentripplanner.routing.algorithm.filterchain.deletionflagger; + +import java.util.List; +import java.util.function.Consumer; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.routing.algorithm.filterchain.ListSection; + +/** + * Flag all itineraries after the provided limit. This flags the itineraries at the end of the list + * for removal, so the list should be sorted on the desired key before this filter is applied. + *

+ * This filter reports information about the removed itineraries in the results consumer. + */ +public class NumItinerariesFilter implements ItineraryDeletionFlagger { + + private static final Consumer IGNORE_SUBSCRIBER = i -> {}; + + private final int maxLimit; + private final ListSection cropSection; + private final Consumer numItinerariesFilterResultsConsumer; + + public NumItinerariesFilter( + int maxLimit, + ListSection cropSection, + Consumer numItinerariesFilterResultsConsumer + ) { + this.maxLimit = maxLimit; + this.cropSection = cropSection; + this.numItinerariesFilterResultsConsumer = + numItinerariesFilterResultsConsumer == null + ? IGNORE_SUBSCRIBER + : numItinerariesFilterResultsConsumer; + } + + @Override + public String name() { + return "number-of-itineraries-filter"; + } + + @Override + public List flagForRemoval(List itineraries) { + if (itineraries.size() <= maxLimit) { + return List.of(); + } + + List itinerariesToKeep; + List itinerariesToRemove; + + if (cropSection == ListSection.HEAD) { + int limit = itineraries.size() - maxLimit; + + itinerariesToRemove = itineraries.subList(0, limit); + itinerariesToKeep = itineraries.subList(limit, itineraries.size()); + } else { + itinerariesToRemove = itineraries.subList(maxLimit, itineraries.size()); + itinerariesToKeep = itineraries.subList(0, maxLimit); + } + + numItinerariesFilterResultsConsumer.accept( + new NumItinerariesFilterResults(itinerariesToKeep, itinerariesToRemove, cropSection) + ); + + return itinerariesToRemove; + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterResults.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterResults.java new file mode 100644 index 00000000000..ed03dd3e64c --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterResults.java @@ -0,0 +1,95 @@ +package org.opentripplanner.routing.algorithm.filterchain.deletionflagger; + +import java.time.Instant; +import java.util.List; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.routing.algorithm.filterchain.ListSection; + +public class NumItinerariesFilterResults { + + public final Instant earliestRemovedDeparture; + public final Instant latestRemovedDeparture; + public final Instant earliestRemovedArrival; + public final Instant latestRemovedArrival; + public final Instant earliestKeptArrival; + public final Instant firstRemovedArrivalTime; + public final int firstRemovedGeneralizedCost; + public final int firstRemovedNumOfTransfers; + public final Instant firstRemovedDepartureTime; + public final ListSection cropSection; + + /** + * The NumItinerariesFilter removes itineraries from a list of itineraries based on the number to + * keep and whether it should crop at the head or the tail of the list. The results class keeps + * the extreme endpoints of the sets of itineraries that were kept and removed, as well as more + * details about the first itinerary removed (bottom of the head, or top of the tail) and whether + * itineraries were cropped at the head or the tail. + */ + public NumItinerariesFilterResults( + List keptItineraries, + List removedItineraries, + ListSection cropSection + ) { + List removedDepartures = removedItineraries + .stream() + .map(it -> it.startTime().toInstant()) + .toList(); + List removedArrivals = removedItineraries + .stream() + .map(it -> it.endTime().toInstant()) + .toList(); + this.earliestRemovedDeparture = removedDepartures.stream().min(Instant::compareTo).orElse(null); + this.latestRemovedDeparture = removedDepartures.stream().max(Instant::compareTo).orElse(null); + this.earliestRemovedArrival = removedArrivals.stream().min(Instant::compareTo).orElse(null); + this.latestRemovedArrival = removedArrivals.stream().max(Instant::compareTo).orElse(null); + + this.earliestKeptArrival = + keptItineraries + .stream() + .map(it -> it.endTime().toInstant()) + .min(Instant::compareTo) + .orElse(null); + + Itinerary firstRemovedItinerary; + if (cropSection == ListSection.HEAD) { + firstRemovedItinerary = removedItineraries.get(removedItineraries.size() - 1); + } else { + firstRemovedItinerary = removedItineraries.get(0); + } + + this.firstRemovedArrivalTime = firstRemovedItinerary.endTime().toInstant(); + this.firstRemovedGeneralizedCost = firstRemovedItinerary.getGeneralizedCost(); + this.firstRemovedNumOfTransfers = firstRemovedItinerary.getNumberOfTransfers(); + this.firstRemovedDepartureTime = firstRemovedItinerary.startTime().toInstant(); + + this.cropSection = cropSection; + } + + @Override + public String toString() { + return ( + "NumItinerariesFilterResults{" + + "earliestRemovedDeparture=" + + earliestRemovedDeparture + + ", latestRemovedDeparture=" + + latestRemovedDeparture + + ", earliestRemovedArrival=" + + earliestRemovedArrival + + ", latestRemovedArrival=" + + latestRemovedArrival + + ", earliestKeptArrival=" + + earliestKeptArrival + + ", firstRemovedArrivalTime=" + + firstRemovedArrivalTime + + ", firstRemovedGeneralizedCost=" + + firstRemovedGeneralizedCost + + ", firstRemovedNumOfTransfers=" + + firstRemovedNumOfTransfers + + ", firstRemovedDepartureTime=" + + firstRemovedDepartureTime + + ", cropSection=" + + cropSection + + '}' + ); + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java index e8a3f226d8a..a7ca4989203 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java @@ -4,11 +4,11 @@ import java.util.List; import java.util.function.Consumer; import org.opentripplanner.ext.ridehailing.RideHailingFilter; -import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.routing.algorithm.filterchain.GroupBySimilarity; import org.opentripplanner.routing.algorithm.filterchain.ItineraryListFilterChain; import org.opentripplanner.routing.algorithm.filterchain.ItineraryListFilterChainBuilder; import org.opentripplanner.routing.algorithm.filterchain.ListSection; +import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.NumItinerariesFilterResults; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.api.request.preference.ItineraryFilterPreferences; @@ -27,7 +27,7 @@ public static ItineraryListFilterChain createFilterChain( OtpServerRequestContext context, Instant filterOnLatestDepartureTime, boolean removeWalkAllTheWayResults, - Consumer maxLimitReachedSubscriber + Consumer maxLimitFilterResultsSubscriber ) { var builder = new ItineraryListFilterChainBuilder(request.itinerariesSortOrder()); @@ -83,7 +83,7 @@ public static ItineraryListFilterChain createFilterChain( context.transitService()::getMultiModalStationForStation ) .withLatestDepartureTimeLimit(filterOnLatestDepartureTime) - .withMaxLimitReachedSubscriber(maxLimitReachedSubscriber) + .withNumItinerariesFilterResultsConsumer(maxLimitFilterResultsSubscriber) .withRemoveWalkAllTheWayResults(removeWalkAllTheWayResults) .withRemoveTransitIfWalkingIsBetter(true) .withDebugEnabled(params.debug()); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapper.java index 1b68143e03d..799ee431ac5 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapper.java @@ -15,6 +15,7 @@ import org.opentripplanner.model.plan.pagecursor.PageCursorFactory; import org.opentripplanner.model.plan.pagecursor.PageType; import org.opentripplanner.raptor.api.request.SearchParams; +import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.NumItinerariesFilterResults; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.response.RoutingError; import org.opentripplanner.routing.api.response.RoutingResponse; @@ -33,7 +34,7 @@ public static RoutingResponse map( ZonedDateTime transitSearchTimeZero, SearchParams searchParams, Duration searchWindowForNextSearch, - Itinerary firstRemovedItinerary, + NumItinerariesFilterResults numItinerariesFilterResults, List itineraries, Set routingErrors, DebugTimingAggregator debugTimingAggregator, @@ -55,7 +56,7 @@ public static RoutingResponse map( transitSearchTimeZero, searchParams, searchWindowForNextSearch, - firstRemovedItinerary, + numItinerariesFilterResults, request.pageCursor() == null ? null : request.pageCursor().type ); @@ -66,7 +67,16 @@ public static RoutingResponse map( logPagingInformation(request.pageCursor(), prevPageCursor, nextPageCursor, routingErrors); } - var metadata = createTripSearchMetadata(request, searchParams, firstRemovedItinerary); + var metadata = createTripSearchMetadata( + request, + searchParams, + numItinerariesFilterResults == null + ? null + : numItinerariesFilterResults.firstRemovedDepartureTime, + numItinerariesFilterResults == null + ? null + : numItinerariesFilterResults.firstRemovedArrivalTime + ); return new RoutingResponse( tripPlan, @@ -83,35 +93,34 @@ public static PageCursorFactory mapIntoPageCursorFactory( ZonedDateTime transitSearchTimeZero, SearchParams searchParams, Duration searchWindowNextSearch, - Itinerary firstRemovedItinerary, + NumItinerariesFilterResults numItinerariesFilterResults, @Nullable PageType currentPageType ) { var factory = new PageCursorFactory(sortOrder, searchWindowNextSearch); - if (searchParams != null) { - if (!searchParams.isSearchWindowSet()) { - LOG.debug("SearchWindow not set"); - return factory; - } - if (!searchParams.isEarliestDepartureTimeSet()) { - LOG.debug("Earliest departure time not set"); - return factory; - } - - long t0 = transitSearchTimeZero.toEpochSecond(); - var edt = Instant.ofEpochSecond(t0 + searchParams.earliestDepartureTime()); - var lat = searchParams.isLatestArrivalTimeSet() - ? Instant.ofEpochSecond(t0 + searchParams.latestArrivalTime()) - : null; - var searchWindow = Duration.ofSeconds(searchParams.searchWindowInSeconds()); - factory.withOriginalSearch(currentPageType, edt, lat, searchWindow); + if (searchParams == null) { + LOG.debug("SearchParams are null"); + return factory; + } + if (!searchParams.isSearchWindowSet()) { + LOG.debug("SearchWindow not set"); + return factory; + } + if (!searchParams.isEarliestDepartureTimeSet()) { + LOG.debug("Earliest departure time not set"); + return factory; } - if (firstRemovedItinerary != null) { - factory.withRemovedItineraries( - firstRemovedItinerary.startTime().toInstant(), - firstRemovedItinerary.endTime().toInstant() - ); + long t0 = transitSearchTimeZero.toEpochSecond(); + var edt = Instant.ofEpochSecond(t0 + searchParams.earliestDepartureTime()); + var lat = searchParams.isLatestArrivalTimeSet() + ? Instant.ofEpochSecond(t0 + searchParams.latestArrivalTime()) + : null; + var searchWindow = Duration.ofSeconds(searchParams.searchWindowInSeconds()); + factory = factory.withOriginalSearch(currentPageType, edt, lat, searchWindow); + + if (numItinerariesFilterResults != null) { + factory = factory.withRemovedItineraries(numItinerariesFilterResults); } return factory; @@ -121,7 +130,8 @@ public static PageCursorFactory mapIntoPageCursorFactory( private static TripSearchMetadata createTripSearchMetadata( RouteRequest request, SearchParams searchParams, - Itinerary firstRemovedItinerary + Instant firstRemovedDepartureTime, + Instant firstRemovedArrivalTime ) { if (searchParams == null) { return null; @@ -133,13 +143,13 @@ private static TripSearchMetadata createTripSearchMetadata( return TripSearchMetadata.createForArriveBy( reqTime, searchParams.searchWindowInSeconds(), - firstRemovedItinerary == null ? null : firstRemovedItinerary.endTime().toInstant() + firstRemovedArrivalTime ); } else { return TripSearchMetadata.createForDepartAfter( reqTime, searchParams.searchWindowInSeconds(), - firstRemovedItinerary == null ? null : firstRemovedItinerary.startTime().toInstant() + firstRemovedDepartureTime ); } } diff --git a/src/main/java/org/opentripplanner/routing/api/request/RouteRequest.java b/src/main/java/org/opentripplanner/routing/api/request/RouteRequest.java index 0a6b2b2a6c6..07e4d07237f 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/RouteRequest.java +++ b/src/main/java/org/opentripplanner/routing/api/request/RouteRequest.java @@ -195,7 +195,7 @@ public boolean maxNumberOfItinerariesCropHead() { } var previousPage = pageCursor.type == PageType.PREVIOUS_PAGE; - return pageCursor.originalSortOrder.isSortedByArrivalTimeAcceding() == previousPage; + return pageCursor.originalSortOrder.isSortedByArrivalTimeAscending() == previousPage; } /** @@ -207,7 +207,7 @@ public boolean maxNumberOfItinerariesCropHead() { */ public boolean doCropSearchWindowAtTail() { if (pageCursor == null) { - return itinerariesSortOrder().isSortedByArrivalTimeAcceding(); + return itinerariesSortOrder().isSortedByArrivalTimeAscending(); } return pageCursor.type == PageType.NEXT_PAGE; } diff --git a/src/test/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryTest.java b/src/test/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryTest.java index b72e0a5b015..4a3e45622f3 100644 --- a/src/test/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryTest.java +++ b/src/test/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryTest.java @@ -3,14 +3,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.opentripplanner.model.plan.SortOrder.STREET_AND_ARRIVAL_TIME; import static org.opentripplanner.model.plan.SortOrder.STREET_AND_DEPARTURE_TIME; +import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary; import static org.opentripplanner.model.plan.pagecursor.PageType.NEXT_PAGE; import static org.opentripplanner.model.plan.pagecursor.PageType.PREVIOUS_PAGE; import java.time.Duration; import java.time.Instant; +import java.util.List; import org.junit.jupiter.api.Test; import org.opentripplanner.framework.time.TimeUtils; import org.opentripplanner.model.plan.PlanTestConstants; +import org.opentripplanner.routing.algorithm.filterchain.ListSection; +import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.NumItinerariesFilterResults; @SuppressWarnings("ConstantConditions") class PageCursorFactoryTest implements PlanTestConstants { @@ -43,13 +47,23 @@ public void sortArrivalAscending() { public void sortArrivalAscendingCropSearchWindow() { var factory = new PageCursorFactory(STREET_AND_ARRIVAL_TIME, D90M) .withOriginalSearch(NEXT_PAGE, T12_00, null, D1H) - .withRemovedItineraries(T12_30, T13_30); + .withRemovedItineraries( + new NumItinerariesFilterResults( + List.of( + newItinerary(A).bus(22, TimeUtils.time("12:00"), TimeUtils.time("13:00"), B).build() + ), + List.of( + newItinerary(A).bus(21, TimeUtils.time("12:30"), TimeUtils.time("13:30"), B).build() + ), + ListSection.TAIL + ) + ); var nextPage = factory.nextPageCursor(); assetPageCursor(nextPage, T12_30, null, D90M, NEXT_PAGE); var prevPage = factory.previousPageCursor(); - assetPageCursor(prevPage, T10_30, null, D90M, PREVIOUS_PAGE); + assetPageCursor(prevPage, T10_30, T13_00, D90M, PREVIOUS_PAGE); } @Test @@ -68,7 +82,17 @@ public void sortArrivalAscendingPreviousPage() { public void sortArrivalAscendingCropSearchWindowPreviousPage() { var factory = new PageCursorFactory(STREET_AND_ARRIVAL_TIME, D90M) .withOriginalSearch(PREVIOUS_PAGE, T12_00, null, D1H) - .withRemovedItineraries(T12_30, T13_30); + .withRemovedItineraries( + new NumItinerariesFilterResults( + List.of( + newItinerary(A).bus(22, TimeUtils.time("12:00"), TimeUtils.time("13:00"), B).build() + ), + List.of( + newItinerary(A).bus(21, TimeUtils.time("12:30"), TimeUtils.time("13:30"), B).build() + ), + ListSection.HEAD + ) + ); var nextPage = factory.nextPageCursor(); assetPageCursor(nextPage, T13_00, null, D90M, NEXT_PAGE); @@ -93,7 +117,17 @@ public void sortDepartureDescending() { public void sortDepartureDescendingCropSearchWindow() { var factory = new PageCursorFactory(STREET_AND_DEPARTURE_TIME, D90M) .withOriginalSearch(PREVIOUS_PAGE, T12_00, T13_30, D1H) - .withRemovedItineraries(T12_30, T13_00); + .withRemovedItineraries( + new NumItinerariesFilterResults( + List.of( + newItinerary(A).bus(21, TimeUtils.time("12:30"), TimeUtils.time("13:30"), B).build() + ), + List.of( + newItinerary(A).bus(22, TimeUtils.time("12:30"), TimeUtils.time("13:00"), B).build() + ), + ListSection.TAIL + ) + ); var nextPage = factory.nextPageCursor(); assetPageCursor(nextPage, T13_00, null, D90M, NEXT_PAGE); @@ -118,7 +152,17 @@ public void sortDepartureDescendingNextPage() { public void sortDepartureDescendingCropSearchWindowNextPage() { var factory = new PageCursorFactory(STREET_AND_DEPARTURE_TIME, D90M) .withOriginalSearch(NEXT_PAGE, T12_00, T13_30, D1H) - .withRemovedItineraries(T12_30, T13_00); + .withRemovedItineraries( + new NumItinerariesFilterResults( + List.of( + newItinerary(A).bus(22, TimeUtils.time("12:00"), TimeUtils.time("13:00"), B).build() + ), + List.of( + newItinerary(A).bus(21, TimeUtils.time("12:30"), TimeUtils.time("13:30"), B).build() + ), + ListSection.HEAD + ) + ); var nextPage = factory.nextPageCursor(); assetPageCursor(nextPage, T12_30, null, D90M, NEXT_PAGE); diff --git a/src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/MaxLimitFilterTest.java b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/MaxLimitFilterTest.java index 34b848e3dc6..78c85b6bdb0 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/MaxLimitFilterTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/MaxLimitFilterTest.java @@ -4,12 +4,10 @@ import static org.opentripplanner.model.plan.Itinerary.toStr; import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary; -import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.model.plan.PlanTestConstants; -import org.opentripplanner.routing.algorithm.filterchain.ListSection; public class MaxLimitFilterTest implements PlanTestConstants { @@ -50,36 +48,4 @@ public void testNormalFilterMaxLimit0() { var result = DeletionFlaggerTestHelper.process(itineraries, subject); assertEquals(toStr(List.of()), toStr(result)); } - - @Test - public void testCropHead() { - MaxLimitFilter subject = new MaxLimitFilter("Test", 1, ListSection.HEAD, null); - List itineraries = List.of(i1, i2, i3); - var result = DeletionFlaggerTestHelper.process(itineraries, subject); - assertEquals(toStr(List.of(i3)), toStr(result)); - } - - @Test - public void testCropTailAndSubscribe() { - var subscribeResult = new ArrayList(); - var subject = new MaxLimitFilter("Test", 2, ListSection.TAIL, subscribeResult::add); - var itineraries = List.of(i1, i2, i3); - - var processedList = DeletionFlaggerTestHelper.process(itineraries, subject); - - assertEquals(toStr(List.of(i3)), toStr(subscribeResult)); - assertEquals(toStr(List.of(i1, i2)), toStr(processedList)); - } - - @Test - public void testCropHeadAndSubscribe() { - var subscribeResult = new ArrayList(); - var subject = new MaxLimitFilter("Test", 1, ListSection.HEAD, subscribeResult::add); - var itineraries = List.of(i1, i2, i3); - - var processedList = DeletionFlaggerTestHelper.process(itineraries, subject); - - assertEquals(toStr(List.of(i2)), toStr(subscribeResult)); - assertEquals(toStr(List.of(i3)), toStr(processedList)); - } } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterTest.java b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterTest.java new file mode 100644 index 00000000000..ead30cfe38c --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterTest.java @@ -0,0 +1,111 @@ +package org.opentripplanner.routing.algorithm.filterchain.deletionflagger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.model.plan.Itinerary.toStr; +import static org.opentripplanner.model.plan.PlanTestConstants.A; +import static org.opentripplanner.model.plan.PlanTestConstants.B; +import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.routing.algorithm.filterchain.ListSection; + +public class NumItinerariesFilterTest { + + private static final Itinerary i1 = newItinerary(A, 6).walk(1, B).build(); + private static final Itinerary i2 = newItinerary(A).bicycle(6, 8, B).build(); + private static final Itinerary i3 = newItinerary(A).bus(21, 7, 9, B).build(); + + private NumItinerariesFilterResults subscribeResult = null; + + @Test + public void name() { + NumItinerariesFilter subject = new NumItinerariesFilter(3, ListSection.TAIL, null); + assertEquals("number-of-itineraries-filter", subject.name()); + } + + @Test + public void testCropHead() { + NumItinerariesFilter subject = new NumItinerariesFilter(1, ListSection.HEAD, null); + List itineraries = List.of(i1, i2, i3); + var result = DeletionFlaggerTestHelper.process(itineraries, subject); + assertEquals(toStr(List.of(i3)), toStr(result)); + } + + @Test + public void testCropTailAndSubscribe() { + var subject = new NumItinerariesFilter(2, ListSection.TAIL, it -> subscribeResult = it); + var itineraries = List.of(i1, i2, i3); + + var processedList = DeletionFlaggerTestHelper.process(itineraries, subject); + + assertEquals( + i3.startTime().toInstant().toString(), + subscribeResult.earliestRemovedDeparture.toString() + ); + assertEquals( + i3.endTime().toInstant().toString(), + subscribeResult.earliestRemovedArrival.toString() + ); + + assertEquals( + i3.startTime().toInstant().toString(), + subscribeResult.latestRemovedDeparture.toString() + ); + assertEquals( + i3.endTime().toInstant().toString(), + subscribeResult.latestRemovedArrival.toString() + ); + + assertEquals( + i3.startTime().toInstant().toString(), + subscribeResult.firstRemovedDepartureTime.toString() + ); + + assertEquals( + i1.endTime().toInstant().toString(), + subscribeResult.earliestKeptArrival.toString() + ); + + assertEquals(toStr(List.of(i1, i2)), toStr(processedList)); + } + + @Test + public void testCropHeadAndSubscribe() { + var subject = new NumItinerariesFilter(1, ListSection.HEAD, it -> subscribeResult = it); + var itineraries = List.of(i1, i2, i3); + + var processedList = DeletionFlaggerTestHelper.process(itineraries, subject); + + assertEquals( + i2.startTime().toInstant().toString(), + subscribeResult.earliestRemovedDeparture.toString() + ); + assertEquals( + i1.endTime().toInstant().toString(), + subscribeResult.earliestRemovedArrival.toString() + ); + + assertEquals( + i2.startTime().toInstant().toString(), + subscribeResult.latestRemovedDeparture.toString() + ); + assertEquals( + i2.endTime().toInstant().toString(), + subscribeResult.latestRemovedArrival.toString() + ); + + assertEquals( + i2.startTime().toInstant().toString(), + subscribeResult.firstRemovedDepartureTime.toString() + ); + + assertEquals( + i3.endTime().toInstant().toString(), + subscribeResult.earliestKeptArrival.toString() + ); + + assertEquals(toStr(List.of(i3)), toStr(processedList)); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapperTest.java b/src/test/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapperTest.java index 48852bbc131..486a9ebf384 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapperTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapperTest.java @@ -8,6 +8,7 @@ import java.time.Duration; import java.time.ZonedDateTime; +import java.util.List; import org.junit.jupiter.api.Test; import org.opentripplanner.framework.time.TimeUtils; import org.opentripplanner.model.plan.Itinerary; @@ -17,6 +18,8 @@ import org.opentripplanner.raptor._data.transit.TestTripSchedule; import org.opentripplanner.raptor.api.request.RaptorRequestBuilder; import org.opentripplanner.raptor.api.request.SearchParams; +import org.opentripplanner.routing.algorithm.filterchain.ListSection; +import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.NumItinerariesFilterResults; public class RoutingResponseMapperTest { @@ -42,6 +45,11 @@ public class RoutingResponseMapperTest { .bus(1, T12_30, T13_00, B) .build(); + private static final Itinerary KEPT_ITINERARY = TestItineraryBuilder + .newItinerary(A, T12_00) + .bus(1, T12_00, T12_30, B) + .build(); + @Test public void mapIntoPageCursorFactoryNoTransitSearchParams() { var factory = RoutingResponseMapper.mapIntoPageCursorFactory( @@ -88,7 +96,11 @@ public void testArriveByReversedRemovedInsidePreviousPage() { TRANSIT_TIME_ZERO, SEARCH_PARAMS, D90M, - REMOVED_ITINERARY, + new NumItinerariesFilterResults( + List.of(KEPT_ITINERARY), + List.of(REMOVED_ITINERARY), + ListSection.TAIL + ), PageType.NEXT_PAGE ); @@ -101,8 +113,12 @@ public void testArriveByReversedRemovedInsidePreviousPage() { "currentSearchWindow: 1h, " + "newSearchWindow: 1h30m, " + "searchWindowCropped, " + - "removedItineraryStartTime: 2020-02-02T12:30:00Z, " + - "removedItineraryEndTime: 2020-02-02T13:00:00Z" + + "numItinerariesFilterResults: " + + new NumItinerariesFilterResults( + List.of(KEPT_ITINERARY), + List.of(REMOVED_ITINERARY), + ListSection.TAIL + ) + "}", factory.toString() ); From 2904afc7b8b8b3a57aa6e544fb671ce7a1fcd408 Mon Sep 17 00:00:00 2001 From: Daniel Heppner Date: Mon, 23 Oct 2023 15:41:39 -0700 Subject: [PATCH 18/60] remove redundant fares from ORCA fare model --- .../ext/fares/impl/OrcaFareServiceTest.java | 51 +--- .../ext/fares/impl/OrcaFareService.java | 37 --- .../ext/fares/impl/OrcaFaresData.java | 237 ------------------ 3 files changed, 13 insertions(+), 312 deletions(-) diff --git a/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java b/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java index 1f880dcab71..03594426c77 100644 --- a/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java @@ -332,27 +332,14 @@ void calculateFareForWSFPtToTahlequah() { */ @Test void calculateFareForLightRailLeg() { - var regularFare = usDollars(2.50f); List rides = List.of( getLeg(SOUND_TRANSIT_AGENCY_ID, "1-Line", 0, "Roosevelt Station", "Int'l Dist/Chinatown") ); - calculateFare(rides, regular, regularFare); - calculateFare(rides, FareType.senior, regularFare); - calculateFare(rides, FareType.youth, Money.ZERO_USD); - calculateFare(rides, FareType.electronicSpecial, ORCA_SPECIAL_FARE); - calculateFare(rides, FareType.electronicRegular, regularFare); - calculateFare(rides, FareType.electronicSenior, ONE_DOLLAR); - calculateFare(rides, FareType.electronicYouth, Money.ZERO_USD); - // Ensure that it works in reverse - rides = - List.of( - getLeg(SOUND_TRANSIT_AGENCY_ID, "1-Line", 0, "Int'l Dist/Chinatown", "Roosevelt Station") - ); - calculateFare(rides, regular, regularFare); - calculateFare(rides, FareType.senior, regularFare); + calculateFare(rides, regular, DEFAULT_TEST_RIDE_PRICE); + calculateFare(rides, FareType.senior, DEFAULT_TEST_RIDE_PRICE); calculateFare(rides, FareType.youth, Money.ZERO_USD); calculateFare(rides, FareType.electronicSpecial, ORCA_SPECIAL_FARE); - calculateFare(rides, FareType.electronicRegular, regularFare); + calculateFare(rides, FareType.electronicRegular, DEFAULT_TEST_RIDE_PRICE); calculateFare(rides, FareType.electronicSenior, ONE_DOLLAR); calculateFare(rides, FareType.electronicYouth, Money.ZERO_USD); } @@ -362,23 +349,11 @@ void calculateFareForSounderLeg() { List rides = List.of( getLeg(SOUND_TRANSIT_AGENCY_ID, "S Line", 0, "King Street Station", "Auburn Station") ); - calculateFare(rides, regular, usDollars(4.25f)); - calculateFare(rides, FareType.senior, usDollars(4.25f)); - calculateFare(rides, FareType.youth, Money.ZERO_USD); - calculateFare(rides, FareType.electronicSpecial, ORCA_SPECIAL_FARE); - calculateFare(rides, FareType.electronicRegular, usDollars(4.25f)); - calculateFare(rides, FareType.electronicSenior, ONE_DOLLAR); - calculateFare(rides, FareType.electronicYouth, Money.ZERO_USD); - // Ensure that it works in reverse - rides = - List.of( - getLeg(SOUND_TRANSIT_AGENCY_ID, "N Line", 0, "King Street Station", "Everett Station") - ); - calculateFare(rides, regular, usDollars(5.00f)); - calculateFare(rides, FareType.senior, usDollars(5.00f)); + calculateFare(rides, regular, DEFAULT_TEST_RIDE_PRICE); + calculateFare(rides, FareType.senior, DEFAULT_TEST_RIDE_PRICE); calculateFare(rides, FareType.youth, Money.ZERO_USD); calculateFare(rides, FareType.electronicSpecial, ORCA_SPECIAL_FARE); - calculateFare(rides, FareType.electronicRegular, usDollars(5.00f)); + calculateFare(rides, FareType.electronicRegular, DEFAULT_TEST_RIDE_PRICE); calculateFare(rides, FareType.electronicSenior, ONE_DOLLAR); calculateFare(rides, FareType.electronicYouth, Money.ZERO_USD); } @@ -440,17 +415,17 @@ void calculateCashFreeTransferKCMetro() { @Test void calculateTransferExtension() { List rides = List.of( - getLeg(SOUND_TRANSIT_AGENCY_ID, "1-Line", 0, "Int'l Dist/Chinatown", "Roosevelt Station"), // 2.50 - getLeg(SOUND_TRANSIT_AGENCY_ID, "1-Line", 60, "Roosevelt Station", "Angle Lake Station"), // 3.25, should extend transfer - getLeg(SOUND_TRANSIT_AGENCY_ID, "1-Line", 140, "Int'l Dist/Chinatown", "Angle Lake Station") // 3.00, should be free under extended transfer + getLeg(KITSAP_TRANSIT_AGENCY_ID, 0, 4,"Kitsap Fast Ferry", "east"), // 2.00 + getLeg(KC_METRO_AGENCY_ID, 100), // Default ride price, extends transfer + getLeg(KITSAP_TRANSIT_AGENCY_ID, 150, 4,"Kitsap Fast Ferry", "west") // 10.00 ); - var regularFare = usDollars(2.50f).plus(usDollars(3.25f)).plus(usDollars(3.00f)); + var regularFare = usDollars(2.00f).plus(DEFAULT_TEST_RIDE_PRICE).plus(usDollars(10f)); calculateFare(rides, regular, regularFare); calculateFare(rides, FareType.senior, regularFare); calculateFare(rides, FareType.youth, Money.ZERO_USD); - calculateFare(rides, FareType.electronicSpecial, ORCA_SPECIAL_FARE.times(2)); - calculateFare(rides, FareType.electronicRegular, usDollars(3.25f)); // transfer extended on second leg - calculateFare(rides, FareType.electronicSenior, TWO_DOLLARS); + calculateFare(rides, FareType.electronicSpecial, usDollars(6f)); + calculateFare(rides, FareType.electronicRegular, usDollars(10f)); // transfer extended on second leg + calculateFare(rides, FareType.electronicSenior, usDollars(6f)); calculateFare(rides, FareType.electronicYouth, Money.ZERO_USD); } diff --git a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java index 32aa8b75b45..90fd26dae94 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java +++ b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java @@ -13,7 +13,6 @@ import java.util.Set; import javax.annotation.Nullable; import org.opentripplanner.ext.fares.model.FareRuleSet; -import org.opentripplanner.ext.ridehailing.model.Ride; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.model.fare.FareMedium; import org.opentripplanner.model.fare.FareProduct; @@ -202,18 +201,6 @@ private static boolean checkShortName(Route route, String compareString) { } } - /** - * Cleans a station name by removing spaces and special phrases. - */ - private static String cleanStationName(String s) { - return s - .replaceAll(" ", "") - .replaceAll("(Northbound)", "") - .replaceAll("(Southbound)", "") - .replaceAll("Station", "") - .toLowerCase(); - } - /** * Classify the ride type based on the route information provided. In most cases the agency name * is sufficient. In some cases the route description and short name are needed to define inner @@ -297,9 +284,6 @@ private Optional getRegularFare( case WASHINGTON_STATE_FERRIES -> Optional.of( getWashingtonStateFerriesFare(route.getLongName(), fareType, defaultFare) ); - case SOUND_TRANSIT_LINK, SOUND_TRANSIT_SOUNDER -> Optional.of( - getSoundTransitFare(leg, defaultFare, rideType) - ); case SOUND_TRANSIT_BUS -> optionalUSD(3.25f); case WHATCOM_LOCAL, WHATCOM_CROSS_COUNTY, @@ -311,27 +295,6 @@ private Optional getRegularFare( }; } - /** - * Calculate the correct Link fare from a "ride" including start and end stations. - */ - private Money getSoundTransitFare(Leg leg, Money defaultFare, RideType rideType) { - String start = cleanStationName(leg.getFrom().name.toString()); - String end = cleanStationName(leg.getTo().name.toString()); - // Fares are the same no matter the order of the stations - // Therefore, the fares DB only contains each station pair once - // If no match is found, try the reversed order - String lookupKey = String.format("%s-%s", start, end); - String reverseLookupKey = String.format("%s-%s", end, start); - Map> fareModel = (rideType == RideType.SOUND_TRANSIT_LINK) - ? OrcaFaresData.linkFares - : OrcaFaresData.sounderFares; - Map fare = Optional - .ofNullable(fareModel.get(lookupKey)) - .orElseGet(() -> fareModel.get(reverseLookupKey)); - - return (fare != null) ? fare.get(FareType.regular) : defaultFare; - } - /** * Apply Orca lift discount fares based on the ride type. */ diff --git a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFaresData.java b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFaresData.java index 44d371590aa..4625a65d90c 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFaresData.java +++ b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFaresData.java @@ -13,227 +13,6 @@ * but a refactor is unneeded given the proximity of GTFS Fares V2 which will render this redundant. */ class OrcaFaresData { - - public static Map> sounderFares = Map.ofEntries( - entry("everett-mukilteo", 3.25f, 3.25f), - entry("everett-edmonds", 4.0f, 4.0f), - entry("everett-kingstreet", 5.0f, 5.0f), - entry("mukilteo-edmonds", 3.75f, 3.75f), - entry("mukilteo-kingstreet", 4.5f, 4.5f), - entry("edmonds-kingstreet", 4.0f, 4.0f), - entry("lakewood-southtacomadome", 3.25f, 3.25f), - entry("lakewood-tacomadome", 3.5f, 3.5f), - entry("lakewood-puyallup", 4.0f, 4.0f), - entry("lakewood-sumner", 4.0f, 4.0f), - entry("lakewood-auburn", 4.5f, 4.5f), - entry("lakewood-kent", 4.75f, 4.75f), - entry("lakewood-tukwila", 5.0f, 5.0f), - entry("lakewood-kingstreet", 5.75f, 5.75f), - entry("southtacomadome-tacomadome", 3.25f, 3.25f), - entry("southtacomadome-puyallup", 3.75f, 3.75f), - entry("southtacomadome-sumner", 4.0f, 4.0f), - entry("southtacomadome-auburn", 4.25f, 4.25f), - entry("southtacomadome-kent", 4.5f, 4.5f), - entry("southtacomadome-tukwila", 5.0f, 5.0f), - entry("southtacomadome-kingstreet", 5.5f, 5.5f), - entry("tacomadome-puyallup", 3.5f, 3.5f), - entry("tacomadome-sumner", 3.5f, 3.5f), - entry("tacomadome-auburn", 4.0f, 4.0f), - entry("tacomadome-kent", 4.25f, 4.25f), - entry("tacomadome-tukwila", 4.5f, 4.5f), - entry("tacomadome-kingstreet", 5.25f, 5.25f), - entry("puyallup-sumner", 3.25f, 3.25f), - entry("puyallup-auburn", 3.5f, 3.5f), - entry("puyallup-kent", 4.0f, 4.0f), - entry("puyallup-tukwila", 4.25f, 4.25f), - entry("puyallup-kingstreet", 4.75f, 4.75f), - entry("sumner-auburn", 3.5f, 3.5f), - entry("sumner-kent", 3.75f, 3.75f), - entry("sumner-tukwila", 4.0f, 4.0f), - entry("sumner-kingstreet", 4.75f, 4.75f), - entry("auburn-kent", 3.25f, 3.25f), - entry("auburn-tukwila", 3.75f, 3.75f), - entry("auburn-kingstreet", 4.25f, 4.25f), - entry("kent-tukwila", 3.25f, 3.25f), - entry("kent-kingstreet", 4.0f, 4.0f), - entry("tukwila-kingstreet", 3.75f, 3.75f) - ); - public static Map> linkFares = Map.ofEntries( - entry("northgate-roosevelt", 2.25f, 2.25f), - entry("northgate-udistrict", 2.5f, 2.5f), - entry("northgate-univofwashington", 2.5f, 2.5f), - entry("northgate-capitolhill", 2.5f, 2.5f), - entry("northgate-stadium", 2.75f, 2.75f), - entry("northgate-sodo", 2.75f, 2.75f), - entry("northgate-beaconhill", 2.75f, 2.75f), - entry("northgate-mountbaker", 2.75f, 2.75f), - entry("northgate-columbiacity", 3.0f, 3.0f), - entry("northgate-othello", 3.0f, 3.0f), - entry("northgate-rainierbeach", 3.0f, 3.0f), - entry("northgate-tukwilaint'lblvd", 3.25f, 3.25f), - entry("northgate-seatac/airport", 3.5f, 3.5f), - entry("northgate-anglelake", 3.5f, 3.5f), - entry("roosevelt-udistrict", 2.25f, 2.25f), - entry("roosevelt-univofwashington", 2.25f, 2.25f), - entry("roosevelt-capitolhill", 2.5f, 2.5f), - entry("roosevelt-stadium", 2.5f, 2.5f), - entry("roosevelt-sodo", 2.5f, 2.5f), - entry("roosevelt-beaconhill", 2.75f, 2.75f), - entry("roosevelt-mountbaker", 2.75f, 2.75f), - entry("roosevelt-columbiacity", 2.75f, 2.75f), - entry("roosevelt-othello", 2.75f, 2.75f), - entry("roosevelt-rainierbeach", 3.0f, 3.0f), - entry("roosevelt-tukwilaint'lblvd", 3.25f, 3.25f), - entry("roosevelt-seatac/airport", 3.25f, 3.25f), - entry("roosevelt-anglelake", 3.25f, 3.25f), - entry("udistrict-univofwashington", 2.25f, 2.25f), - entry("udistrict-capitolhill", 2.5f, 2.5f), - entry("udistrict-stadium", 2.5f, 2.5f), - entry("udistrict-sodo", 2.5f, 2.5f), - entry("udistrict-beaconhill", 2.75f, 2.75f), - entry("udistrict-mountbaker", 2.75f, 2.75f), - entry("udistrict-columbiacity", 2.75f, 2.75f), - entry("udistrict-othello", 2.75f, 2.75f), - entry("udistrict-rainierbeach", 2.75f, 2.75f), - entry("udistrict-tukwilaint'lblvd", 3.25f, 3.25f), - entry("udistrict-seatac/airport", 3.25f, 3.25f), - entry("udistrict-anglelake", 3.25f, 3.25f), - entry("univofwashington-capitolhill", 2.25f, 2.25f), - entry("univofwashington-stadium", 2.5f, 2.5f), - entry("univofwashington-sodo", 2.5f, 2.5f), - entry("univofwashington-beaconhill", 2.5f, 2.5f), - entry("univofwashington-mountbaker", 2.5f, 2.5f), - entry("univofwashington-columbiacity", 2.75f, 2.75f), - entry("univofwashington-othello", 2.75f, 2.75f), - entry("univofwashington-rainierbeach", 2.75f, 2.75f), - entry("univofwashington-tukwilaint'lblvd", 3.0f, 3.0f), - entry("univofwashington-seatac/airport", 3.25f, 3.25f), - entry("univofwashington-anglelake", 3.25f, 3.25f), - entry("capitolhill-stadium", 2.25f, 2.25f), - entry("capitolhill-sodo", 2.25f, 2.25f), - entry("capitolhill-beaconhill", 2.5f, 2.5f), - entry("capitolhill-mountbaker", 2.5f, 2.5f), - entry("capitolhill-columbiacity", 2.5f, 2.5f), - entry("capitolhill-othello", 2.5f, 2.5f), - entry("capitolhill-rainierbeach", 2.75f, 2.75f), - entry("capitolhill-tukwilaint'lblvd", 3.0f, 3.0f), - entry("capitolhill-seatac/airport", 3.0f, 3.0f), - entry("capitolhill-anglelake", 3.0f, 3.0f), - entry("stadium-beaconhill", 2.25f, 2.25f), - entry("stadium-mountbaker", 2.25f, 2.25f), - entry("stadium-columbiacity", 2.25f, 2.25f), - entry("stadium-othello", 2.5f, 2.5f), - entry("stadium-rainierbeach", 2.5f, 2.5f), - entry("stadium-tukwilaint'lblvd", 2.75f, 2.75f), - entry("stadium-seatac/airport", 3.0f, 3.0f), - entry("stadium-anglelake", 3.0f, 3.0f), - entry("sodo-roosevelt", 2.75f, 2.75f), - entry("sodo-stadium", 2.25f, 2.25f), - entry("sodo-mountbaker", 2.25f, 2.25f), - entry("sodo-columbiacity", 2.25f, 2.25f), - entry("sodo-othello", 2.5f, 2.5f), - entry("sodo-rainierbeach", 2.5f, 2.5f), - entry("sodo-tukwilaint'lblvd", 2.75f, 2.75f), - entry("sodo-seatac/airport", 2.75f, 2.75f), - entry("sodo-anglelake", 3.0f, 3.0f), - entry("beaconhill-sodo", 2.25f, 2.25f), - entry("beaconhill-columbiacity", 2.25f, 2.25f), - entry("beaconhill-othello", 2.5f, 2.5f), - entry("beaconhill-rainierbeach", 2.5f, 2.5f), - entry("beaconhill-tukwilaint'lblvd", 2.75f, 2.75f), - entry("beaconhill-seatac/airport", 2.75f, 2.75f), - entry("beaconhill-anglelake", 3.0f, 3.0f), - entry("mountbaker-beaconhill", 2.25f, 2.25f), - entry("mountbaker-othello", 2.25f, 2.25f), - entry("mountbaker-rainierbeach", 2.5f, 2.5f), - entry("mountbaker-tukwilaint'lblvd", 2.75f, 2.75f), - entry("mountbaker-seatac/airport", 2.75f, 2.75f), - entry("mountbaker-anglelake", 3.0f, 3.0f), - entry("columbiacity-mountbaker", 2.25f, 2.25f), - entry("columbiacity-rainierbeach", 2.25f, 2.25f), - entry("columbiacity-tukwilaint'lblvd", 2.5f, 2.5f), - entry("columbiacity-seatac/airport", 2.75f, 2.75f), - entry("columbiacity-anglelake", 2.75f, 2.75f), - entry("othello-columbiacity", 2.25f, 2.25f), - entry("othello-tukwilaint'lblvd", 2.5f, 2.5f), - entry("othello-seatac/airport", 2.75f, 2.75f), - entry("othello-anglelake", 2.75f, 2.75f), - entry("rainierbeach-othello", 2.25f, 2.25f), - entry("rainierbeach-seatac/airport", 2.5f, 2.5f), - entry("rainierbeach-anglelake", 2.75f, 2.75f), - entry("tukwilaint'lblvd-rainierbeach", 2.5f, 2.5f), - entry("tukwilaint'lblvd-anglelake", 2.5f, 2.5f), - entry("seatac/airport-tukwilaint'lblvd", 2.25f, 2.25f), - entry("anglelake-seatac/airport", 2.25f, 2.25f), - entry("northgate-universityst", 2.75f, 2.75f), - entry("northgate-pioneersquare", 2.75f, 2.75f), - entry("northgate-int'ldist/chinatown", 2.75f, 2.75f), - entry("roosevelt-westlake", 2.5f, 2.5f), - entry("roosevelt-universityst", 2.5f, 2.5f), - entry("roosevelt-pioneersquare", 2.5f, 2.5f), - entry("roosevelt-int'ldist/chinatown", 2.5f, 2.5f), - entry("udistrict-westlake", 2.5f, 2.5f), - entry("udistrict-universityst", 2.5f, 2.5f), - entry("udistrict-pioneersquare", 2.5f, 2.5f), - entry("udistrict-int'ldist/chinatown", 2.5f, 2.5f), - entry("univofwashington-westlake", 2.5f, 2.5f), - entry("univofwashington-universityst", 2.5f, 2.5f), - entry("univofwashington-pioneersquare", 2.5f, 2.5f), - entry("univofwashington-int'ldist/chinatown", 2.5f, 2.5f), - entry("capitolhill-westlake", 2.25f, 2.25f), - entry("capitolhill-universityst", 2.25f, 2.25f), - entry("capitolhill-pioneersquare", 2.25f, 2.25f), - entry("capitolhill-int'ldist/chinatown", 2.25f, 2.25f), - entry("westlake-northgate", 2.75f, 2.75f), - entry("westlake-westlake", 2.25f, 2.25f), - entry("westlake-universityst", 2.25f, 2.25f), - entry("westlake-pioneersquare", 2.25f, 2.25f), - entry("westlake-int'ldist/chinatown", 2.25f, 2.25f), - entry("universityst-pioneersquare", 2.25f, 2.25f), - entry("universityst-int'ldist/chinatown", 2.25f, 2.25f), - entry("pioneersquare-int'ldist/chinatown", 2.25f, 2.25f), - entry("universityst-stadium", 2.25f, 2.25f), - entry("pioneersquare-stadium", 2.25f, 2.25f), - entry("int'ldist/chinatown-stadium", 2.25f, 2.25f), - entry("westlake-sodo", 2.25f, 2.25f), - entry("universityst-sodo", 2.25f, 2.25f), - entry("pioneersquare-sodo", 2.25f, 2.25f), - entry("int'ldist/chinatown-sodo", 2.25f, 2.25f), - entry("westlake-beaconhill", 2.25f, 2.25f), - entry("universityst-beaconhill", 2.25f, 2.25f), - entry("pioneersquare-beaconhill", 2.25f, 2.25f), - entry("int'ldist/chinatown-beaconhill", 2.25f, 2.25f), - entry("westlake-mountbaker", 2.5f, 2.5f), - entry("universityst-mountbaker", 2.5f, 2.5f), - entry("pioneersquare-mountbaker", 2.5f, 2.5f), - entry("int'ldist/chinatown-mountbaker", 2.5f, 2.5f), - entry("westlake-columbiacity", 2.5f, 2.5f), - entry("universityst-columbiacity", 2.5f, 2.5f), - entry("pioneersquare-columbiacity", 2.5f, 2.5f), - entry("int'ldist/chinatown-columbiacity", 2.5f, 2.5f), - entry("westlake-othello", 2.5f, 2.5f), - entry("universityst-othello", 2.5f, 2.5f), - entry("pioneersquare-othello", 2.5f, 2.5f), - entry("int'ldist/chinatown-othello", 2.5f, 2.5f), - entry("westlake-rainierbeach", 2.5f, 2.5f), - entry("universityst-rainierbeach", 2.5f, 2.5f), - entry("pioneersquare-rainierbeach", 2.5f, 2.5f), - entry("int'ldist/chinatown-rainierbeach", 2.5f, 2.5f), - entry("westlake-tukwilaint'lblvd", 3.0f, 3.0f), - entry("universityst-tukwilaint'lblvd", 3.0f, 3.0f), - entry("pioneersquare-tukwilaint'lblvd", 3.0f, 3.0f), - entry("int'ldist/chinatown-tukwilaint'lblvd", 3.0f, 3.0f), - entry("westlake-seatac/airport", 3.0f, 3.0f), - entry("universityst-seatac/airport", 3.0f, 3.0f), - entry("pioneersquare-seatac/airport", 3.0f, 3.0f), - entry("int'ldist/chinatown-seatac/airport", 3.0f, 3.0f), - entry("westlake-anglelake", 3.0f, 3.0f), - entry("universityst-anglelake", 3.0f, 3.0f), - entry("pioneersquare-anglelake", 3.0f, 3.0f), - entry("int'ldist/chinatown-anglelake", 3.0f, 3.0f), - entry("stadium-westlake", 2.25f, 2.25f) - ); - // Spaces have been removed from the route name because of inconsistencies in the WSF GTFS route dataset. public static Map> washingtonStateFerriesFares = Map.ofEntries( sEntry("Seattle-BainbridgeIsland", 9.85f, 4.90f), @@ -252,22 +31,6 @@ class OrcaFaresData { sEntry("Southworth-VashonIsland", 6.50f, 3.25f) ); - private static Map.Entry> entry( - String name, - float regularFare, - float electronicRegularFare - ) { - return Map.entry( - name, - Map.of( - FareType.regular, - Money.usDollars(regularFare), - FareType.electronicRegular, - Money.usDollars(electronicRegularFare) - ) - ); - } - private static Map.Entry> sEntry( String name, float regularFare, From 16222e6a09d356004865a5bd9133cb09e2b9ed0d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 23:09:25 +0000 Subject: [PATCH 19/60] chore(deps): update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.2.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fd9e1e3b7e1..36b29192ad4 100644 --- a/pom.xml +++ b/pom.xml @@ -242,7 +242,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.1.2 + 3.2.1 me.fabriciorby From e4b42f282f9589437093247b5419ba3fac63d1b1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 06:15:55 +0000 Subject: [PATCH 20/60] fix(deps): update dependency com.graphql-java:graphql-java to v21.3 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 36b29192ad4..3bc91e79e5f 100644 --- a/pom.xml +++ b/pom.xml @@ -899,7 +899,7 @@ com.graphql-java graphql-java - 21.2 + 21.3 com.graphql-java From 52f68a230b1b62eb7dc2f8a0da7c107fdb46b570 Mon Sep 17 00:00:00 2001 From: OTP Changelog Bot Date: Tue, 24 Oct 2023 08:30:58 +0000 Subject: [PATCH 21/60] Add changelog entry for #5428 [ci skip] --- docs/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Changelog.md b/docs/Changelog.md index c0938adfcc2..db04106a952 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -23,6 +23,7 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Import Occupancy Status from GTFS-RT Vehicle Positions [#5372](https://github.com/opentripplanner/OpenTripPlanner/pull/5372) - Add Roadmap epic template [#5413](https://github.com/opentripplanner/OpenTripPlanner/pull/5413) - Allow multiple zones in an unscheduled flex trip [#5376](https://github.com/opentripplanner/OpenTripPlanner/pull/5376) +- Filter out null, empty and blank elements when mapping feed-scoped ids [#5428](https://github.com/opentripplanner/OpenTripPlanner/pull/5428) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.4.0 (2023-09-13) From 8571c08b242099f259aba7806ed6272f8a89616f Mon Sep 17 00:00:00 2001 From: OTP Changelog Bot Date: Tue, 24 Oct 2023 12:07:17 +0000 Subject: [PATCH 22/60] Add changelog entry for #5440 [ci skip] --- docs/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Changelog.md b/docs/Changelog.md index db04106a952..6d13e66cf57 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -24,6 +24,7 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Add Roadmap epic template [#5413](https://github.com/opentripplanner/OpenTripPlanner/pull/5413) - Allow multiple zones in an unscheduled flex trip [#5376](https://github.com/opentripplanner/OpenTripPlanner/pull/5376) - Filter out null, empty and blank elements when mapping feed-scoped ids [#5428](https://github.com/opentripplanner/OpenTripPlanner/pull/5428) +- Validate stop id in Transit leg reference [#5440](https://github.com/opentripplanner/OpenTripPlanner/pull/5440) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.4.0 (2023-09-13) From 8df18db87437a8f588b817843b08f6b9328c5386 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Tue, 24 Oct 2023 14:08:51 +0200 Subject: [PATCH 23/60] Use backticks to prettify generated documentation --- .../resources/org/opentripplanner/apis/gtfs/schema.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 2dea139f6ea..7f23b30914f 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -558,7 +558,7 @@ type VehicleRentalStation implements Node & PlaceInterface { Number of vehicles currently available on the rental station. See field `allowPickupNow` to know if is currently possible to pick up a vehicle. """ - vehiclesAvailable: Int @deprecated(reason: "Use availableVehicles instead, which also contains vehicle types") + vehiclesAvailable: Int @deprecated(reason: "Use `availableVehicles` instead, which also contains vehicle types") """ Number of free spaces currently available on the rental station. @@ -567,7 +567,7 @@ type VehicleRentalStation implements Node & PlaceInterface { the rental station, even if the vehicle racks don't have any spaces available. See field `allowDropoffNow` to know if is currently possible to return a vehicle. """ - spacesAvailable: Int @deprecated(reason: "Use availableSpaces instead, which also contains the space vehicle types") + spacesAvailable: Int @deprecated(reason: "Use `availableSpaces` instead, which also contains the space vehicle types") """ Number of vehicles currently available on the rental station, grouped by vehicle type. From 8ef960d41367699e7ffdcdae19ad3dbdb6f613c9 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 24 Oct 2023 14:28:30 +0200 Subject: [PATCH 24/60] Update src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceTest.java Co-authored-by: Johan Torin --- .../services/OptimizePathDomainServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceTest.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceTest.java index 222baded224..c15a7b8247a 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceTest.java @@ -286,7 +286,7 @@ public void testConstrainedTransferIsPreferred() { * * Case: A trip may have the exact same times for more than one stop. This is a regression test * see https://github.com/opentripplanner/OpenTripPlanner/issues/5444. - * The following transfers are exist: A-B, A-C, B-B, B-C, C-B and C-C. + * The following transfers exist: A-B, A-C, B-B, B-C, C-B and C-C. * Expect: Transfer B-B, the earliest transfer with the lowest transfer time and cost. */ @Test From 61ce86e808c89cea1eb8a5dda55f3d3a718f8788 Mon Sep 17 00:00:00 2001 From: Daniel Heppner Date: Tue, 24 Oct 2023 15:47:21 -0700 Subject: [PATCH 25/60] clean up getRideType function --- .../ext/fares/impl/OrcaFareService.java | 79 ++++++------------- 1 file changed, 25 insertions(+), 54 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java index 90fd26dae94..83929584bd5 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java +++ b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java @@ -97,7 +97,16 @@ public boolean agencyAcceptsOrca() { } } - static RideType getRideType(String agencyId, Route route) { + /** + * Categorizes a leg based on various parameters. + * The classifications determine the various rules and fares applied to the leg. + * @param leg Leg to be classified. + * @return RideType classification + */ + static RideType getRideType(Leg leg) { + var agencyId = leg.getAgency().getId().getId(); + var route = leg.getRoute(); + var tripId = leg.getTrip().getId().getId(); return switch (agencyId) { case COMM_TRANS_AGENCY_ID -> { try { @@ -158,7 +167,20 @@ static RideType getRideType(String agencyId, Route route) { case SEATTLE_STREET_CAR_AGENCY_ID -> RideType.SEATTLE_STREET_CAR; case WASHINGTON_STATE_FERRIES_AGENCY_ID -> RideType.WASHINGTON_STATE_FERRIES; case T_LINK_AGENCY_ID -> RideType.SOUND_TRANSIT_T_LINK; - case KITSAP_TRANSIT_AGENCY_ID -> RideType.KITSAP_TRANSIT; + case KITSAP_TRANSIT_AGENCY_ID -> { + if ( + route.getId().getId().equalsIgnoreCase("Kitsap Fast Ferry") && + route.getGtfsType() == ROUTE_TYPE_FERRY + ) { + // Additional trip id checks are required to distinguish Kitsap fast ferry routes. + if (tripId.contains("east")) { + yield RideType.KITSAP_TRANSIT_FAST_FERRY_EASTBOUND; + } else if (tripId.contains("west")) { + yield RideType.KITSAP_TRANSIT_FAST_FERRY_WESTBOUND; + } + } + yield RideType.KITSAP_TRANSIT; + } case WHATCOM_AGENCY_ID -> "80X".equals(route.getShortName()) ? RideType.WHATCOM_CROSS_COUNTY : RideType.WHATCOM_LOCAL; @@ -185,57 +207,6 @@ public OrcaFareService(Collection regularFareRules) { addFareRules(FareType.electronicSenior, regularFareRules); } - /** - * Checks a routeShortName against a given string after removing spaces - */ - private static boolean checkShortName(Route route, String compareString) { - String cleanCompareString = compareString.replaceAll("-", "").replaceAll(" ", ""); - if (route.getShortName() != null) { - return route - .getShortName() - .replaceAll("-", "") - .replaceAll(" ", "") - .equalsIgnoreCase(cleanCompareString); - } else { - return false; - } - } - - /** - * Classify the ride type based on the route information provided. In most cases the agency name - * is sufficient. In some cases the route description and short name are needed to define inner - * agency ride types. For Kitsap, the route data is enough to define the agency, but addition trip - * id checks are needed to define the fast ferry direction. - */ - private static RideType classify(Route route, String tripId) { - var rideType = getRideType(route.getAgency().getId().getId(), route); - if (rideType == null) { - return null; - } - if ( - rideType == RideType.KITSAP_TRANSIT && - route.getId().getId().equalsIgnoreCase("Kitsap Fast Ferry") && - route.getGtfsType() == ROUTE_TYPE_FERRY - ) { - // Additional trip id checks are required to distinguish Kitsap fast ferry routes. - if (tripId.contains("east")) { - rideType = RideType.KITSAP_TRANSIT_FAST_FERRY_EASTBOUND; - } else if (tripId.contains("west")) { - rideType = RideType.KITSAP_TRANSIT_FAST_FERRY_WESTBOUND; - } - } else if (rideType == RideType.SOUND_TRANSIT && checkShortName(route, "1 Line")) { - rideType = RideType.SOUND_TRANSIT_LINK; - } else if ( - rideType == RideType.SOUND_TRANSIT && - (checkShortName(route, "S Line") || checkShortName(route, "N Line")) - ) { - rideType = RideType.SOUND_TRANSIT_SOUNDER; - } else if (rideType == RideType.SOUND_TRANSIT) { //if it isn't Link or Sounder, then... - rideType = RideType.SOUND_TRANSIT_BUS; - } - return rideType; - } - /** * Define which discount fare should be applied based on the fare type. If the ride type is * unknown the discount fare can not be applied, use the default fare. @@ -444,7 +415,7 @@ public boolean populateFare( Money cost = Money.ZERO_USD; Money orcaFareDiscount = Money.ZERO_USD; for (Leg leg : legs) { - RideType rideType = classify(leg.getRoute(), leg.getTrip().getId().getId()); + RideType rideType = getRideType(leg); assert rideType != null; boolean ridePermitsFreeTransfers = rideType.permitsFreeTransfers(); if (freeTransferStartTime == null && ridePermitsFreeTransfers) { From 793895f475df5fff67feb96b8c9d19e404b25dee Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 25 Oct 2023 10:37:52 +0200 Subject: [PATCH 26/60] Format code --- .../opentripplanner/ext/fares/impl/OrcaFareServiceTest.java | 4 ++-- .../org/opentripplanner/ext/fares/impl/OrcaFaresData.java | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java b/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java index 03594426c77..f437677e4be 100644 --- a/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java @@ -415,9 +415,9 @@ void calculateCashFreeTransferKCMetro() { @Test void calculateTransferExtension() { List rides = List.of( - getLeg(KITSAP_TRANSIT_AGENCY_ID, 0, 4,"Kitsap Fast Ferry", "east"), // 2.00 + getLeg(KITSAP_TRANSIT_AGENCY_ID, 0, 4, "Kitsap Fast Ferry", "east"), // 2.00 getLeg(KC_METRO_AGENCY_ID, 100), // Default ride price, extends transfer - getLeg(KITSAP_TRANSIT_AGENCY_ID, 150, 4,"Kitsap Fast Ferry", "west") // 10.00 + getLeg(KITSAP_TRANSIT_AGENCY_ID, 150, 4, "Kitsap Fast Ferry", "west") // 10.00 ); var regularFare = usDollars(2.00f).plus(DEFAULT_TEST_RIDE_PRICE).plus(usDollars(10f)); calculateFare(rides, regular, regularFare); diff --git a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFaresData.java b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFaresData.java index 4625a65d90c..e970cb51718 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFaresData.java +++ b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFaresData.java @@ -13,6 +13,7 @@ * but a refactor is unneeded given the proximity of GTFS Fares V2 which will render this redundant. */ class OrcaFaresData { + // Spaces have been removed from the route name because of inconsistencies in the WSF GTFS route dataset. public static Map> washingtonStateFerriesFares = Map.ofEntries( sEntry("Seattle-BainbridgeIsland", 9.85f, 4.90f), From 73e945f79c13d80f511eface47c4185749a59be7 Mon Sep 17 00:00:00 2001 From: OTP Serialization Version Bot Date: Wed, 25 Oct 2023 09:24:10 +0000 Subject: [PATCH 27/60] Bump serialization version id for #5449 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 71e3db3c4f2..0a41eb308d1 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ - 122 + 123 30.0 2.48.1 From 0f2b89b86e5b777ff061b488a206de60d41cfee3 Mon Sep 17 00:00:00 2001 From: Henrik Abrahamsson Date: Thu, 26 Oct 2023 09:58:09 +0200 Subject: [PATCH 28/60] Fix race condition in StreetTransitEntitiyLinkTest --- .../edge/StreetTransitEntityLinkTest.java | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/test/java/org/opentripplanner/street/model/edge/StreetTransitEntityLinkTest.java b/src/test/java/org/opentripplanner/street/model/edge/StreetTransitEntityLinkTest.java index c996a6fc00a..f821d0e02e6 100644 --- a/src/test/java/org/opentripplanner/street/model/edge/StreetTransitEntityLinkTest.java +++ b/src/test/java/org/opentripplanner/street/model/edge/StreetTransitEntityLinkTest.java @@ -7,12 +7,13 @@ import static org.opentripplanner.transit.model.basic.Accessibility.NO_INFORMATION; import static org.opentripplanner.transit.model.basic.Accessibility.POSSIBLE; +import java.util.List; import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.api.request.preference.AccessibilityPreferences; import org.opentripplanner.routing.api.request.preference.WheelchairPreferences; @@ -22,7 +23,6 @@ import org.opentripplanner.street.search.request.StreetSearchRequest; import org.opentripplanner.street.search.state.State; import org.opentripplanner.street.search.state.TestStateBuilder; -import org.opentripplanner.test.support.VariableSource; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.site.RegularStop; @@ -119,38 +119,42 @@ private State[] traverse(RegularStop stop, boolean onlyAccessible) { @Nested class Rental { - static Stream allowedStates = Stream - .of( - TestStateBuilder.ofScooterRental().pickUpFreeFloatingScooter(), - TestStateBuilder.ofBikeRental().pickUpFreeFloatingBike(), - // allowing cars into stations is a bit questionable but the alternatives would be quite - // computationally expensive - TestStateBuilder.ofCarRental().pickUpFreeFloatingCar(), - TestStateBuilder.ofWalking(), - TestStateBuilder.ofCycling() - ) - .map(TestStateBuilder::build) - .map(Arguments::of); + static List allowedStates() { + return Stream + .of( + TestStateBuilder.ofScooterRental().pickUpFreeFloatingScooter(), + TestStateBuilder.ofBikeRental().pickUpFreeFloatingBike(), + // allowing cars into stations is a bit questionable but the alternatives would be quite + // computationally expensive + TestStateBuilder.ofCarRental().pickUpFreeFloatingCar(), + TestStateBuilder.ofWalking(), + TestStateBuilder.ofCycling() + ) + .map(TestStateBuilder::build) + .toList(); + } @ParameterizedTest - @VariableSource("allowedStates") + @MethodSource("allowedStates") void freeFloatingVehiclesAreAllowedIntoStops(State state) { testTraversalWithState(state, true); } - static Stream notAllowedStates = Stream - .of( - TestStateBuilder.ofBikeRental().pickUpBikeFromStation(), - TestStateBuilder.ofCarRental().pickUpCarFromStation(), - // for bike and ride you need to drop the bike at a parking facility first - TestStateBuilder.ofBikeAndRide().streetEdge(), - TestStateBuilder.parkAndRide().streetEdge() - ) - .map(TestStateBuilder::build) - .map(Arguments::of); + static List notAllowedStates() { + return Stream + .of( + TestStateBuilder.ofBikeRental().pickUpBikeFromStation(), + TestStateBuilder.ofCarRental().pickUpCarFromStation(), + // for bike and ride you need to drop the bike at a parking facility first + TestStateBuilder.ofBikeAndRide().streetEdge(), + TestStateBuilder.parkAndRide().streetEdge() + ) + .map(TestStateBuilder::build) + .toList(); + } @ParameterizedTest - @VariableSource("notAllowedStates") + @MethodSource("notAllowedStates") void stationBasedVehiclesAreNotAllowedIntoStops(State state) { testTraversalWithState(state, false); } From b0a9571d8a3e9b72aa0e7af3406ea26e61705802 Mon Sep 17 00:00:00 2001 From: eibakke Date: Mon, 23 Oct 2023 14:40:29 +0200 Subject: [PATCH 29/60] Creates the NumItinerariesFilter and the associated NumItinerariesFilterResults to limit itineraries based on the numItineraries parameter. Pulling this functionality into a separate filter from the MaxLimitFilter because of the need to keep more information about the filtered itineraries than just the first one that is removed. The NumItinerariesFilterResults contains information used in the page cursors currently as well as information that will be used in the future deduplication feature. --- .../routing/algorithm/mapping/RoutingResponseMapper.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapper.java index 799ee431ac5..33b90cab0b3 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapper.java @@ -111,12 +111,11 @@ public static PageCursorFactory mapIntoPageCursorFactory( return factory; } - long t0 = transitSearchTimeZero.toEpochSecond(); - var edt = Instant.ofEpochSecond(t0 + searchParams.earliestDepartureTime()); - var lat = searchParams.isLatestArrivalTimeSet() - ? Instant.ofEpochSecond(t0 + searchParams.latestArrivalTime()) + Instant edt = transitSearchTimeZero.plusSeconds(searchParams.earliestDepartureTime()).toInstant(); + Instant lat = searchParams.isLatestArrivalTimeSet() + ? transitSearchTimeZero.plusSeconds(searchParams.latestArrivalTime()).toInstant() : null; - var searchWindow = Duration.ofSeconds(searchParams.searchWindowInSeconds()); + Duration searchWindow = Duration.ofSeconds(searchParams.searchWindowInSeconds()); factory = factory.withOriginalSearch(currentPageType, edt, lat, searchWindow); if (numItinerariesFilterResults != null) { From 13aa4b2eb1d1a9cc835fa736c5e111ad555697f4 Mon Sep 17 00:00:00 2001 From: eibakke Date: Mon, 23 Oct 2023 14:40:29 +0200 Subject: [PATCH 30/60] Creates the NumItinerariesFilter and the associated NumItinerariesFilterResults to limit itineraries based on the numItineraries parameter. Pulling this functionality into a separate filter from the MaxLimitFilter because of the need to keep more information about the filtered itineraries than just the first one that is removed. The NumItinerariesFilterResults contains information used in the page cursors currently as well as information that will be used in the future deduplication feature. --- .../routing/algorithm/mapping/RoutingResponseMapper.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapper.java index 33b90cab0b3..864aa56dd39 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapper.java @@ -111,7 +111,9 @@ public static PageCursorFactory mapIntoPageCursorFactory( return factory; } - Instant edt = transitSearchTimeZero.plusSeconds(searchParams.earliestDepartureTime()).toInstant(); + Instant edt = transitSearchTimeZero + .plusSeconds(searchParams.earliestDepartureTime()) + .toInstant(); Instant lat = searchParams.isLatestArrivalTimeSet() ? transitSearchTimeZero.plusSeconds(searchParams.latestArrivalTime()).toInstant() : null; From 85753600ecce50a9fe0623d6e9631381215249fb Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 26 Oct 2023 16:41:39 +0200 Subject: [PATCH 31/60] Make fields nullable --- .../resources/org/opentripplanner/apis/gtfs/schema.graphqls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 7f23b30914f..82d297abe96 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -572,12 +572,12 @@ type VehicleRentalStation implements Node & PlaceInterface { """ Number of vehicles currently available on the rental station, grouped by vehicle type. """ - availableVehicles: RentalVehicleEntityCounts! + availableVehicles: RentalVehicleEntityCounts """ Number of free spaces currently available on the rental station, grouped by vehicle type. """ - availableSpaces: RentalVehicleEntityCounts! + availableSpaces: RentalVehicleEntityCounts """ If true, values of `vehiclesAvailable` and `spacesAvailable` are updated from a From ef5f54cb3cbdc2399355e9c439ae0a44c04b894a Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Mon, 23 Oct 2023 16:21:03 +0200 Subject: [PATCH 32/60] Add support for DSJ in transit leg reference --- .../realtimeresolver/RealtimeResolver.java | 21 +-- .../model/plan/FrequencyTransitLeg.java | 49 +---- .../plan/FrequencyTransitLegBuilder.java | 28 +++ .../org/opentripplanner/model/plan/Leg.java | 9 + .../model/plan/ScheduledTransitLeg.java | 81 ++++----- .../plan/ScheduledTransitLegBuilder.java | 169 ++++++++++++++++++ .../legreference/LegReferenceSerializer.java | 38 +++- .../plan/legreference/LegReferenceType.java | 13 +- .../ScheduledTransitLegReference.java | 92 +++++++--- .../mapping/RaptorPathToItineraryMapper.java | 81 +++++---- .../alternativelegs/AlternativeLegs.java | 44 +++-- .../model/plan/ScheduledTransitLegTest.java | 25 ++- .../model/plan/TestItineraryBuilder.java | 54 +++--- .../LegReferenceSerializerTest.java | 33 +++- .../ScheduledTransitLegReferenceTest.java | 98 +++++++++- .../stoptimes/AlternativeLegsTest.java | 12 +- 16 files changed, 602 insertions(+), 245 deletions(-) create mode 100644 src/main/java/org/opentripplanner/model/plan/FrequencyTransitLegBuilder.java create mode 100644 src/main/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilder.java diff --git a/src/ext/java/org/opentripplanner/ext/realtimeresolver/RealtimeResolver.java b/src/ext/java/org/opentripplanner/ext/realtimeresolver/RealtimeResolver.java index abbb0d7d1bc..a59d9fc589a 100644 --- a/src/ext/java/org/opentripplanner/ext/realtimeresolver/RealtimeResolver.java +++ b/src/ext/java/org/opentripplanner/ext/realtimeresolver/RealtimeResolver.java @@ -5,6 +5,7 @@ import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.model.plan.Leg; import org.opentripplanner.model.plan.ScheduledTransitLeg; +import org.opentripplanner.model.plan.ScheduledTransitLegBuilder; import org.opentripplanner.transit.service.TransitService; public class RealtimeResolver { @@ -53,20 +54,12 @@ private static Leg combineReferenceWithOriginal( ScheduledTransitLeg reference, ScheduledTransitLeg original ) { - var leg = new ScheduledTransitLeg( - reference.getTripTimes(), - reference.getTripPattern(), - reference.getBoardStopPosInPattern(), - reference.getAlightStopPosInPattern(), - reference.getStartTime(), - reference.getEndTime(), - reference.getServiceDate(), - reference.getZoneId(), - original.getTransferFromPrevLeg(), - original.getTransferToNextLeg(), - original.getGeneralizedCost(), - original.accessibilityScore() - ); + var leg = new ScheduledTransitLegBuilder<>(reference) + .withTransferFromPreviousLeg(original.getTransferFromPrevLeg()) + .withTransferToNextLeg(original.getTransferToNextLeg()) + .withGeneralizedCost(original.getGeneralizedCost()) + .withAccessibilityScore(original.accessibilityScore()) + .build(); reference.getTransitAlerts().forEach(leg::addAlert); return leg; } diff --git a/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLeg.java b/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLeg.java index bef50ddd2fd..d2188fb1b0b 100644 --- a/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLeg.java +++ b/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLeg.java @@ -21,36 +21,9 @@ public class FrequencyTransitLeg extends ScheduledTransitLeg { private final int frequencyHeadwayInSeconds; - public FrequencyTransitLeg( - TripTimes tripTimes, - TripPattern tripPattern, - int boardStopIndexInPattern, - int alightStopIndexInPattern, - ZonedDateTime startTime, - ZonedDateTime endTime, - LocalDate serviceDate, - ZoneId zoneId, - ConstrainedTransfer transferFromPreviousLeg, - ConstrainedTransfer transferToNextLeg, - int generalizedCost, - int frequencyHeadwayInSeconds, - @Nullable Float accessibilityScore - ) { - super( - tripTimes, - tripPattern, - boardStopIndexInPattern, - alightStopIndexInPattern, - startTime, - endTime, - serviceDate, - zoneId, - transferFromPreviousLeg, - transferToNextLeg, - generalizedCost, - accessibilityScore - ); - this.frequencyHeadwayInSeconds = frequencyHeadwayInSeconds; + FrequencyTransitLeg(FrequencyTransitLegBuilder builder) { + super(builder); + this.frequencyHeadwayInSeconds = builder.frequencyHeadwayInSeconds(); } @Override @@ -101,20 +74,6 @@ public List getIntermediateStops() { @Override public ScheduledTransitLeg withAccessibilityScore(Float score) { - return new FrequencyTransitLeg( - tripTimes, - tripPattern, - boardStopPosInPattern, - alightStopPosInPattern, - getStartTime(), - getEndTime(), - serviceDate, - zoneId, - getTransferFromPrevLeg(), - getTransferToNextLeg(), - getGeneralizedCost(), - frequencyHeadwayInSeconds, - score - ); + return new FrequencyTransitLegBuilder(this).withAccessibilityScore(score).build(); } } diff --git a/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLegBuilder.java b/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLegBuilder.java new file mode 100644 index 00000000000..bbbc89393f0 --- /dev/null +++ b/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLegBuilder.java @@ -0,0 +1,28 @@ +package org.opentripplanner.model.plan; + +public class FrequencyTransitLegBuilder + extends ScheduledTransitLegBuilder { + + private int frequencyHeadwayInSeconds; + + public FrequencyTransitLegBuilder() {} + + public FrequencyTransitLegBuilder(FrequencyTransitLeg original) { + super(original); + frequencyHeadwayInSeconds = original.getHeadway(); + } + + public FrequencyTransitLegBuilder withFrequencyHeadwayInSeconds(int frequencyHeadwayInSeconds) { + this.frequencyHeadwayInSeconds = frequencyHeadwayInSeconds; + return instance(); + } + + public int frequencyHeadwayInSeconds() { + return frequencyHeadwayInSeconds; + } + + @Override + public FrequencyTransitLeg build() { + return new FrequencyTransitLeg(this); + } +} diff --git a/src/main/java/org/opentripplanner/model/plan/Leg.java b/src/main/java/org/opentripplanner/model/plan/Leg.java index f561f2713e1..05213dbdf5e 100644 --- a/src/main/java/org/opentripplanner/model/plan/Leg.java +++ b/src/main/java/org/opentripplanner/model/plan/Leg.java @@ -24,6 +24,7 @@ import org.opentripplanner.transit.model.organization.Operator; import org.opentripplanner.transit.model.site.FareZone; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; /** * One leg of a trip -- that is, a temporally continuous piece of the journey that takes place on a @@ -187,6 +188,14 @@ default Trip getTrip() { return null; } + /** + * For transit legs, the trip on service date, if it exists. For non-transit legs, null. + */ + @Nullable + default TripOnServiceDate getTripOnServiceDate() { + return null; + } + default Accessibility getTripWheelchairAccessibility() { return null; } diff --git a/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java index 10158cdc13b..1c535f631b9 100644 --- a/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java +++ b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java @@ -35,6 +35,7 @@ import org.opentripplanner.transit.model.organization.Operator; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; /** @@ -57,47 +58,37 @@ public class ScheduledTransitLeg implements TransitLeg { private final int generalizedCost; protected final LocalDate serviceDate; protected final ZoneId zoneId; + private final TripOnServiceDate tripOnServiceDate; private double distanceMeters; private final double directDistanceMeters; private final Float accessibilityScore; private List fareProducts = List.of(); - public ScheduledTransitLeg( - TripTimes tripTimes, - TripPattern tripPattern, - int boardStopIndexInPattern, - int alightStopIndexInPattern, - ZonedDateTime startTime, - ZonedDateTime endTime, - LocalDate serviceDate, - ZoneId zoneId, - ConstrainedTransfer transferFromPreviousLeg, - ConstrainedTransfer transferToNextLeg, - int generalizedCost, - @Nullable Float accessibilityScore - ) { - this.tripTimes = tripTimes; - this.tripPattern = tripPattern; + ScheduledTransitLeg(ScheduledTransitLegBuilder builder) { + this.tripTimes = builder.tripTimes(); + this.tripPattern = builder.tripPattern(); - this.boardStopPosInPattern = boardStopIndexInPattern; - this.alightStopPosInPattern = alightStopIndexInPattern; + this.boardStopPosInPattern = builder.boardStopIndexInPattern(); + this.alightStopPosInPattern = builder.alightStopIndexInPattern(); - this.startTime = startTime; - this.endTime = endTime; + this.startTime = builder.startTime(); + this.endTime = builder.endTime(); - this.serviceDate = serviceDate; - this.zoneId = zoneId; + this.serviceDate = builder.serviceDate(); + this.zoneId = builder.zoneId(); - this.transferFromPrevLeg = transferFromPreviousLeg; - this.transferToNextLeg = transferToNextLeg; + this.tripOnServiceDate = builder.tripOnServiceDate(); - this.generalizedCost = generalizedCost; + this.transferFromPrevLeg = builder.transferFromPreviousLeg(); + this.transferToNextLeg = builder.transferToNextLeg(); - this.accessibilityScore = accessibilityScore; + this.generalizedCost = builder.generalizedCost(); + + this.accessibilityScore = builder.accessibilityScore(); List transitLegCoordinates = extractTransitLegCoordinates( tripPattern, - boardStopIndexInPattern, - alightStopIndexInPattern + builder.boardStopIndexInPattern(), + builder.alightStopIndexInPattern() ); this.legGeometry = GeometryUtils.makeLineString(transitLegCoordinates); @@ -127,10 +118,12 @@ public Instant getServiceDateMidnight() { return ServiceDateUtils.asStartOfService(serviceDate, zoneId).toInstant(); } + @Override public boolean isScheduledTransitLeg() { return true; } + @Override public ScheduledTransitLeg asScheduledTransitLeg() { return this; } @@ -244,6 +237,12 @@ public LocalDate getServiceDate() { return serviceDate; } + @Override + @Nullable + public TripOnServiceDate getTripOnServiceDate() { + return tripOnServiceDate; + } + @Override public Place getFrom() { return Place.forStop(tripPattern.getStop(boardStopPosInPattern)); @@ -344,18 +343,25 @@ public int getGeneralizedCost() { return generalizedCost; } + /** + * Construct a leg reference from this leg. + * If the trip is based on a TripOnServiceDate, the leg reference will contain the + * TripOnServiceDate id instead of the Trip id. + */ @Override public LegReference getLegReference() { return new ScheduledTransitLegReference( - tripTimes.getTrip().getId(), + tripOnServiceDate == null ? tripTimes.getTrip().getId() : null, serviceDate, boardStopPosInPattern, alightStopPosInPattern, tripPattern.getStops().get(boardStopPosInPattern).getId(), - tripPattern.getStops().get(alightStopPosInPattern).getId() + tripPattern.getStops().get(alightStopPosInPattern).getId(), + tripOnServiceDate == null ? null : tripOnServiceDate.getId() ); } + @Override public void addAlert(TransitAlert alert) { transitAlerts.add(alert); } @@ -377,20 +383,7 @@ public Float accessibilityScore() { } public ScheduledTransitLeg withAccessibilityScore(Float score) { - return new ScheduledTransitLeg( - tripTimes, - tripPattern, - boardStopPosInPattern, - alightStopPosInPattern, - startTime, - endTime, - serviceDate, - zoneId, - transferFromPrevLeg, - transferToNextLeg, - generalizedCost, - score - ); + return new ScheduledTransitLegBuilder<>(this).withAccessibilityScore(score).build(); } /** diff --git a/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilder.java b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilder.java new file mode 100644 index 00000000000..de775eab263 --- /dev/null +++ b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilder.java @@ -0,0 +1,169 @@ +package org.opentripplanner.model.plan; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import org.opentripplanner.model.transfer.ConstrainedTransfer; +import org.opentripplanner.street.model.edge.StreetEdgeBuilder; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; +import org.opentripplanner.transit.model.timetable.TripTimes; + +public class ScheduledTransitLegBuilder> { + + private TripTimes tripTimes; + private TripPattern tripPattern; + private int boardStopIndexInPattern; + private int alightStopIndexInPattern; + private ZonedDateTime startTime; + private ZonedDateTime endTime; + private LocalDate serviceDate; + private ZoneId zoneId; + private TripOnServiceDate tripOnServiceDate; + private ConstrainedTransfer transferFromPreviousLeg; + private ConstrainedTransfer transferToNextLeg; + private int generalizedCost; + private Float accessibilityScore; + + public ScheduledTransitLegBuilder() {} + + public ScheduledTransitLegBuilder(ScheduledTransitLeg original) { + tripTimes = original.getTripTimes(); + tripPattern = original.getTripPattern(); + boardStopIndexInPattern = original.getBoardStopPosInPattern(); + alightStopIndexInPattern = original.getAlightStopPosInPattern(); + startTime = original.getStartTime(); + endTime = original.getEndTime(); + serviceDate = original.getServiceDate(); + tripOnServiceDate = original.getTripOnServiceDate(); + transferFromPreviousLeg = original.getTransferFromPrevLeg(); + transferToNextLeg = original.getTransferToNextLeg(); + generalizedCost = original.getGeneralizedCost(); + accessibilityScore = original.accessibilityScore(); + } + + public B withTripTimes(TripTimes tripTimes) { + this.tripTimes = tripTimes; + return instance(); + } + + public TripTimes tripTimes() { + return tripTimes; + } + + public B withTripPattern(TripPattern tripPattern) { + this.tripPattern = tripPattern; + return instance(); + } + + public TripPattern tripPattern() { + return tripPattern; + } + + public B withBoardStopIndexInPattern(int boardStopIndexInPattern) { + this.boardStopIndexInPattern = boardStopIndexInPattern; + return instance(); + } + + public int boardStopIndexInPattern() { + return boardStopIndexInPattern; + } + + public B withAlightStopIndexInPattern(int alightStopIndexInPattern) { + this.alightStopIndexInPattern = alightStopIndexInPattern; + return instance(); + } + + public int alightStopIndexInPattern() { + return alightStopIndexInPattern; + } + + public B withStartTime(ZonedDateTime startTime) { + this.startTime = startTime; + return instance(); + } + + public ZonedDateTime startTime() { + return startTime; + } + + public B withEndTime(ZonedDateTime endTime) { + this.endTime = endTime; + return instance(); + } + + public ZonedDateTime endTime() { + return endTime; + } + + public B withServiceDate(LocalDate serviceDate) { + this.serviceDate = serviceDate; + return instance(); + } + + public LocalDate serviceDate() { + return serviceDate; + } + + public B withZoneId(ZoneId zoneId) { + this.zoneId = zoneId; + return instance(); + } + + public ZoneId zoneId() { + return zoneId; + } + + public B withTripOnServiceDate(TripOnServiceDate tripOnServiceDate) { + this.tripOnServiceDate = tripOnServiceDate; + return instance(); + } + + public TripOnServiceDate tripOnServiceDate() { + return tripOnServiceDate; + } + + public B withTransferFromPreviousLeg(ConstrainedTransfer transferFromPreviousLeg) { + this.transferFromPreviousLeg = transferFromPreviousLeg; + return instance(); + } + + public ConstrainedTransfer transferFromPreviousLeg() { + return transferFromPreviousLeg; + } + + public B withTransferToNextLeg(ConstrainedTransfer transferToNextLeg) { + this.transferToNextLeg = transferToNextLeg; + return instance(); + } + + public ConstrainedTransfer transferToNextLeg() { + return transferToNextLeg; + } + + public B withGeneralizedCost(int generalizedCost) { + this.generalizedCost = generalizedCost; + return instance(); + } + + public int generalizedCost() { + return generalizedCost; + } + + public B withAccessibilityScore(Float accessibilityScore) { + this.accessibilityScore = accessibilityScore; + return instance(); + } + + public Float accessibilityScore() { + return accessibilityScore; + } + + public ScheduledTransitLeg build() { + return new ScheduledTransitLeg(this); + } + + final B instance() { + return (B) this; + } +} diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java index 067037f54e1..4f090a440b6 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializer.java @@ -28,11 +28,9 @@ public static String encode(LegReference legReference) { if (legReference == null) { return null; } - LegReferenceType typeEnum = LegReferenceType.forClass(legReference.getClass()); - - if (typeEnum == null) { - throw new IllegalArgumentException("Unknown LegReference type"); - } + LegReferenceType typeEnum = LegReferenceType + .forClass(legReference.getClass()) + .orElseThrow(() -> new IllegalArgumentException("Unknown LegReference type")); var buf = new ByteArrayOutputStream(); try (var out = new ObjectOutputStream(buf)) { @@ -93,6 +91,21 @@ static void writeScheduledTransitLegV2(LegReference ref, ObjectOutputStream out) } } + static void writeScheduledTransitLegV3(LegReference ref, ObjectOutputStream out) + throws IOException { + if (ref instanceof ScheduledTransitLegReference s) { + out.writeUTF(s.tripOnServiceDateId() == null ? s.tripId().toString() : ""); + out.writeUTF(s.serviceDate().toString()); + out.writeInt(s.fromStopPositionInPattern()); + out.writeInt(s.toStopPositionInPattern()); + out.writeUTF(s.fromStopId().toString()); + out.writeUTF(s.toStopId().toString()); + out.writeUTF(s.tripOnServiceDateId() == null ? "" : s.tripOnServiceDateId().toString()); + } else { + throw new IllegalArgumentException("Invalid LegReference type"); + } + } + static LegReference readScheduledTransitLegV1(ObjectInputStream objectInputStream) throws IOException { return new ScheduledTransitLegReference( @@ -101,6 +114,7 @@ static LegReference readScheduledTransitLegV1(ObjectInputStream objectInputStrea objectInputStream.readInt(), objectInputStream.readInt(), null, + null, null ); } @@ -113,6 +127,20 @@ static LegReference readScheduledTransitLegV2(ObjectInputStream objectInputStrea objectInputStream.readInt(), objectInputStream.readInt(), FeedScopedId.parse(objectInputStream.readUTF()), + FeedScopedId.parse(objectInputStream.readUTF()), + null + ); + } + + static LegReference readScheduledTransitLegV3(ObjectInputStream objectInputStream) + throws IOException { + return new ScheduledTransitLegReference( + FeedScopedId.parse(objectInputStream.readUTF()), + LocalDate.parse(objectInputStream.readUTF(), DateTimeFormatter.ISO_LOCAL_DATE), + objectInputStream.readInt(), + objectInputStream.readInt(), + FeedScopedId.parse(objectInputStream.readUTF()), + FeedScopedId.parse(objectInputStream.readUTF()), FeedScopedId.parse(objectInputStream.readUTF()) ); } diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceType.java b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceType.java index 44cea2226d3..079f526676d 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceType.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/LegReferenceType.java @@ -22,6 +22,13 @@ enum LegReferenceType { ScheduledTransitLegReference.class, LegReferenceSerializer::writeScheduledTransitLegV2, LegReferenceSerializer::readScheduledTransitLegV2 + ), + + SCHEDULED_TRANSIT_LEG_V3( + 3, + ScheduledTransitLegReference.class, + LegReferenceSerializer::writeScheduledTransitLegV3, + LegReferenceSerializer::readScheduledTransitLegV3 ); private final int version; @@ -45,8 +52,8 @@ enum LegReferenceType { /** * Return the latest LegReferenceType version for a given leg reference class. */ - static LegReferenceType forClass(Class legReferenceClass) { - Optional latestVersion = Arrays + static Optional forClass(Class legReferenceClass) { + return Arrays .stream(LegReferenceType.values()) .filter(legReferenceType -> legReferenceType.legReferenceClass.equals(legReferenceClass)) .reduce((legReferenceType, other) -> { @@ -55,8 +62,6 @@ static LegReferenceType forClass(Class legReferenceClass } return other; }); - - return latestVersion.orElse(null); } Writer getSerializer() { diff --git a/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java b/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java index ae81a906d12..b008dea5737 100644 --- a/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java +++ b/src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java @@ -6,11 +6,13 @@ import org.opentripplanner.framework.time.ServiceDateUtils; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.plan.ScheduledTransitLeg; +import org.opentripplanner.model.plan.ScheduledTransitLegBuilder; import org.opentripplanner.routing.algorithm.mapping.AlertToLegMapper; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.transit.service.TransitService; import org.slf4j.Logger; @@ -27,11 +29,23 @@ public record ScheduledTransitLegReference( int toStopPositionInPattern, FeedScopedId fromStopId, - FeedScopedId toStopId + FeedScopedId toStopId, + FeedScopedId tripOnServiceDateId ) implements LegReference { private static final Logger LOG = LoggerFactory.getLogger(ScheduledTransitLegReference.class); + public ScheduledTransitLegReference { + if (tripId != null && tripOnServiceDateId != null) { + throw new IllegalArgumentException( + "ScheduledTransitLegReference cannot refer to both a Trip id " + + tripId + + " and a TripOnServiceDate id " + + tripOnServiceDateId + ); + } + } + /** * Reconstruct a scheduled transit leg from this scheduled transit leg reference. * Since the transit model could have been modified between the time the reference is created @@ -43,20 +57,47 @@ public record ScheduledTransitLegReference( * As an exception, the reference is still considered valid if the referenced stop is different * but belongs to the same parent station: this covers for example the case of a last-minute * platform change in a train station that typically does not affect the validity of the leg. + *

+ * If the referenced trip is based on a TripOnServiceDate (i.e. a TransModel dated service + * journey), the TripOnServiceDate id is stored in the leg reference instead of the Trip id: + * A TripOnServiceDate id is meant to be more stable than a Trip id across deliveries of planned + * data, using it gives a better guarantee to reconstruct correctly the original leg. */ @Override @Nullable public ScheduledTransitLeg getLeg(TransitService transitService) { - Trip trip = transitService.getTripForId(tripId); - if (trip == null) { - LOG.info("Invalid transit leg reference: trip {} not found", tripId); - return null; + Trip trip; + TripOnServiceDate tripOnServiceDate = null; + if (tripOnServiceDateId != null) { + tripOnServiceDate = transitService.getTripOnServiceDateById(tripOnServiceDateId); + if (tripOnServiceDate == null) { + LOG.info( + "Invalid transit leg reference: trip on service date '{}' not found", + tripOnServiceDateId + ); + return null; + } + if (!tripOnServiceDate.getServiceDate().equals(serviceDate)) { + LOG.info( + "Invalid transit leg reference: trip on service date '{}' does not run on service date {}", + tripOnServiceDateId, + serviceDate + ); + return null; + } + trip = tripOnServiceDate.getTrip(); + } else { + trip = transitService.getTripForId(tripId); + if (trip == null) { + LOG.info("Invalid transit leg reference: trip '{}' not found", tripId); + return null; + } } TripPattern tripPattern = transitService.getPatternForTrip(trip, serviceDate); if (tripPattern == null) { LOG.info( - "Invalid transit leg reference: trip pattern not found for trip {} and service date {} ", + "Invalid transit leg reference: trip pattern not found for trip '{}' and service date {} ", tripId, serviceDate ); @@ -67,7 +108,7 @@ public ScheduledTransitLeg getLeg(TransitService transitService) { if (fromStopPositionInPattern >= numStops || toStopPositionInPattern >= numStops) { LOG.info( "Invalid transit leg reference: boarding stop {} or alighting stop {} is out of range" + - " in trip {} and service date {} ({} stops in trip pattern) ", + " in trip '{}' and service date {} ({} stops in trip pattern) ", fromStopPositionInPattern, toStopPositionInPattern, tripId, @@ -94,7 +135,7 @@ public ScheduledTransitLeg getLeg(TransitService transitService) { if (tripTimes == null) { LOG.info( - "Invalid transit leg reference: trip times not found for trip {} and service date {} ", + "Invalid transit leg reference: trip times not found for trip '{}' and service date {}", tripId, serviceDate ); @@ -107,7 +148,7 @@ public ScheduledTransitLeg getLeg(TransitService transitService) { .contains(tripTimes.getServiceCode()) ) { LOG.info( - "Invalid transit leg reference: the trip {} does not run on service date {} ", + "Invalid transit leg reference: the trip '{}' does not run on service date {}", tripId, serviceDate ); @@ -120,20 +161,19 @@ public ScheduledTransitLeg getLeg(TransitService transitService) { int boardingTime = tripTimes.getDepartureTime(fromStopPositionInPattern); int alightingTime = tripTimes.getArrivalTime(toStopPositionInPattern); - ScheduledTransitLeg leg = new ScheduledTransitLeg( - tripTimes, - tripPattern, - fromStopPositionInPattern, - toStopPositionInPattern, - ServiceDateUtils.toZonedDateTime(serviceDate, timeZone, boardingTime), - ServiceDateUtils.toZonedDateTime(serviceDate, timeZone, alightingTime), - serviceDate, - timeZone, - null, - null, - 0, // TODO: What should we have here - null - ); + ScheduledTransitLeg leg = new ScheduledTransitLegBuilder<>() + .withTripTimes(tripTimes) + .withTripPattern(tripPattern) + .withBoardStopIndexInPattern(fromStopPositionInPattern) + .withAlightStopIndexInPattern(toStopPositionInPattern) + .withStartTime(ServiceDateUtils.toZonedDateTime(serviceDate, timeZone, boardingTime)) + .withEndTime(ServiceDateUtils.toZonedDateTime(serviceDate, timeZone, alightingTime)) + .withServiceDate(serviceDate) + .withTripOnServiceDate(tripOnServiceDate) + .withZoneId(timeZone) + // TODO: What should we have here + .withGeneralizedCost(0) + .build(); new AlertToLegMapper( transitService.getTransitAlertService(), @@ -178,7 +218,7 @@ private boolean matchReferencedStopInPattern( LOG.info( "Invalid transit leg reference:" + " The referenced stop at position {} with id '{}' does not match" + - " the stop id '{}' in trip {} and service date {}", + " the stop id '{}' in trip '{}' and service date {}", stopPosition, stopId, stopLocationInPattern.getId(), @@ -189,8 +229,8 @@ private boolean matchReferencedStopInPattern( } LOG.info( "Transit leg reference with modified stop id within the same station: " + - "The referenced stop at position {} with id '{}' does not match\" +\n" + - " \" the stop id '{}' in trip {} and service date {}", + "The referenced stop at position {} with id '{}' does not match" + + " the stop id '{}' in trip {} and service date {}", stopPosition, stopId, stopLocationInPattern.getId(), diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java index 5f946b1d775..61ac1dd5cc1 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java @@ -10,11 +10,11 @@ import org.opentripplanner.framework.geometry.GeometryUtils; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.model.GenericLocation; -import org.opentripplanner.model.plan.FrequencyTransitLeg; +import org.opentripplanner.model.plan.FrequencyTransitLegBuilder; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.model.plan.Leg; import org.opentripplanner.model.plan.Place; -import org.opentripplanner.model.plan.ScheduledTransitLeg; +import org.opentripplanner.model.plan.ScheduledTransitLegBuilder; import org.opentripplanner.model.plan.StreetLeg; import org.opentripplanner.model.plan.UnknownTransitPathLeg; import org.opentripplanner.model.transfer.ConstrainedTransfer; @@ -39,6 +39,8 @@ import org.opentripplanner.street.search.request.StreetSearchRequestMapper; import org.opentripplanner.street.search.state.State; import org.opentripplanner.street.search.state.StateEditor; +import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.service.TransitService; /** @@ -57,6 +59,7 @@ public class RaptorPathToItineraryMapper { private final ZonedDateTime transitSearchTimeZero; private final GraphPathToItineraryMapper graphPathToItineraryMapper; + private final TransitService transitService; /** * Constructs an itinerary mapper for a request and a set of results @@ -84,6 +87,7 @@ public RaptorPathToItineraryMapper( graph.streetNotesService, graph.ellipsoidToGeoidDifference ); + this.transitService = transitService; } public Itinerary createItinerary(RaptorPath path) { @@ -204,36 +208,53 @@ private Leg mapTransitLeg(Leg prevTransitLeg, TransitPathLeg pathLeg) { if (tripSchedule.isFrequencyBasedTrip()) { int frequencyHeadwayInSeconds = tripSchedule.frequencyHeadwayInSeconds(); - return new FrequencyTransitLeg( - tripSchedule.getOriginalTripTimes(), - tripSchedule.getOriginalTripPattern(), - boardStopIndexInPattern, - alightStopIndexInPattern, - createZonedDateTime(pathLeg.fromTime() + frequencyHeadwayInSeconds), - createZonedDateTime(pathLeg.toTime()), - tripSchedule.getServiceDate(), - transitSearchTimeZero.getZone().normalized(), - (prevTransitLeg == null ? null : prevTransitLeg.getTransferToNextLeg()), - (ConstrainedTransfer) pathLeg.getConstrainedTransferAfterLeg(), - toOtpDomainCost(pathLeg.generalizedCost() + lastLegCost), - frequencyHeadwayInSeconds, - null - ); + return new FrequencyTransitLegBuilder() + .withTripTimes(tripSchedule.getOriginalTripTimes()) + .withTripPattern(tripSchedule.getOriginalTripPattern()) + .withBoardStopIndexInPattern(boardStopIndexInPattern) + .withAlightStopIndexInPattern(alightStopIndexInPattern) + .withStartTime(createZonedDateTime(pathLeg.fromTime() + frequencyHeadwayInSeconds)) + .withEndTime(createZonedDateTime(pathLeg.toTime())) + .withServiceDate(tripSchedule.getServiceDate()) + .withZoneId(transitSearchTimeZero.getZone().normalized()) + .withTransferFromPreviousLeg( + (prevTransitLeg == null ? null : prevTransitLeg.getTransferToNextLeg()) + ) + .withTransferToNextLeg((ConstrainedTransfer) pathLeg.getConstrainedTransferAfterLeg()) + .withGeneralizedCost(toOtpDomainCost(pathLeg.generalizedCost() + lastLegCost)) + .withFrequencyHeadwayInSeconds(frequencyHeadwayInSeconds) + .build(); + } + + TripOnServiceDate tripOnServiceDate = getTripOnServiceDate(tripSchedule); + + return new ScheduledTransitLegBuilder<>() + .withTripTimes(tripSchedule.getOriginalTripTimes()) + .withTripPattern(tripSchedule.getOriginalTripPattern()) + .withBoardStopIndexInPattern(boardStopIndexInPattern) + .withAlightStopIndexInPattern(alightStopIndexInPattern) + .withStartTime(createZonedDateTime(pathLeg.fromTime())) + .withEndTime(createZonedDateTime(pathLeg.toTime())) + .withServiceDate(tripSchedule.getServiceDate()) + .withZoneId(transitSearchTimeZero.getZone().normalized()) + .withTripOnServiceDate(tripOnServiceDate) + .withTransferFromPreviousLeg( + (prevTransitLeg == null ? null : prevTransitLeg.getTransferToNextLeg()) + ) + .withTransferToNextLeg((ConstrainedTransfer) pathLeg.getConstrainedTransferAfterLeg()) + .withGeneralizedCost(toOtpDomainCost(pathLeg.generalizedCost() + lastLegCost)) + .build(); + } + + private TripOnServiceDate getTripOnServiceDate(T tripSchedule) { + if (tripSchedule.getOriginalTripTimes() == null) { + return null; } - return new ScheduledTransitLeg( - tripSchedule.getOriginalTripTimes(), - tripSchedule.getOriginalTripPattern(), - boardStopIndexInPattern, - alightStopIndexInPattern, - createZonedDateTime(pathLeg.fromTime()), - createZonedDateTime(pathLeg.toTime()), - tripSchedule.getServiceDate(), - transitSearchTimeZero.getZone().normalized(), - (prevTransitLeg == null ? null : prevTransitLeg.getTransferToNextLeg()), - (ConstrainedTransfer) pathLeg.getConstrainedTransferAfterLeg(), - toOtpDomainCost(pathLeg.generalizedCost() + lastLegCost), - null + TripIdAndServiceDate tripIdAndServiceDate = new TripIdAndServiceDate( + tripSchedule.getOriginalTripTimes().getTrip().getId(), + tripSchedule.getServiceDate() ); + return transitService.getTripOnServiceDateForTripAndDay(tripIdAndServiceDate); } private boolean isFree(EgressPathLeg egressPathLeg) { diff --git a/src/main/java/org/opentripplanner/routing/alternativelegs/AlternativeLegs.java b/src/main/java/org/opentripplanner/routing/alternativelegs/AlternativeLegs.java index cd4afd211af..6b51c0b1282 100644 --- a/src/main/java/org/opentripplanner/routing/alternativelegs/AlternativeLegs.java +++ b/src/main/java/org/opentripplanner/routing/alternativelegs/AlternativeLegs.java @@ -22,9 +22,12 @@ import org.opentripplanner.model.TripTimeOnDate; import org.opentripplanner.model.plan.Leg; import org.opentripplanner.model.plan.ScheduledTransitLeg; +import org.opentripplanner.model.plan.ScheduledTransitLegBuilder; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.transit.service.TransitService; @@ -206,7 +209,16 @@ private static Stream generateLegs( while (!pq.isEmpty()) { TripTimeOnDate tripTimeOnDate = pq.poll(); - res.add(mapToLeg(timeZone, pattern, boardingPosition, alightingPosition, tripTimeOnDate)); + res.add( + mapToLeg( + timeZone, + pattern, + boardingPosition, + alightingPosition, + tripTimeOnDate, + transitService + ) + ); } return res.stream(); @@ -218,7 +230,8 @@ private static ScheduledTransitLeg mapToLeg( TripPattern pattern, int boardingPosition, int alightingPosition, - TripTimeOnDate tripTimeOnDate + TripTimeOnDate tripTimeOnDate, + TransitService transitService ) { LocalDate serviceDay = tripTimeOnDate.getServiceDay(); TripTimes tripTimes = tripTimeOnDate.getTripTimes(); @@ -234,20 +247,21 @@ private static ScheduledTransitLeg mapToLeg( tripTimes.getArrivalTime(alightingPosition) ); - return new ScheduledTransitLeg( - tripTimes, - pattern, - boardingPosition, - alightingPosition, - boardingTime, - alightingTime, - serviceDay, - timeZone, - null, - null, - ZERO_COST, - null + TripOnServiceDate tripOnServiceDate = transitService.getTripOnServiceDateForTripAndDay( + new TripIdAndServiceDate(tripTimeOnDate.getTrip().getId(), tripTimeOnDate.getServiceDay()) ); + + return new ScheduledTransitLegBuilder<>() + .withTripTimes(tripTimes) + .withTripPattern(pattern) + .withBoardStopIndexInPattern(boardingPosition) + .withAlightStopIndexInPattern(alightingPosition) + .withStartTime(boardingTime) + .withEndTime(alightingTime) + .withServiceDate(serviceDay) + .withZoneId(timeZone) + .withTripOnServiceDate(tripOnServiceDate) + .build(); } @Nonnull diff --git a/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegTest.java b/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegTest.java index 71b110b0381..518fb528a22 100644 --- a/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegTest.java +++ b/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegTest.java @@ -23,20 +23,17 @@ void defaultFares() { .tripPattern("1", route) .withStopPattern(TransitModelForTest.stopPattern(3)) .build(); - var leg = new ScheduledTransitLeg( - null, - pattern, - 0, - 2, - TIME, - TIME.plusMinutes(10), - TIME.toLocalDate(), - ZoneIds.BERLIN, - null, - null, - 100, - null - ); + var leg = new ScheduledTransitLegBuilder() + .withTripTimes(null) + .withTripPattern(pattern) + .withBoardStopIndexInPattern(0) + .withAlightStopIndexInPattern(2) + .withStartTime(TIME) + .withEndTime(TIME.plusMinutes(10)) + .withServiceDate(TIME.toLocalDate()) + .withZoneId(ZoneIds.BERLIN) + .withGeneralizedCost(100) + .build(); assertEquals(List.of(), leg.fareProducts()); } diff --git a/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java b/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java index ec243fca0e6..3eccdaaabd1 100644 --- a/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java +++ b/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java @@ -482,37 +482,33 @@ public TestItineraryBuilder transit( if (headwaySecs != null) { leg = - new FrequencyTransitLeg( - tripTimes, - tripPattern, - fromStopIndex, - toStopIndex, - newTime(start), - newTime(end), - serviceDate != null ? serviceDate : SERVICE_DAY, - UTC, - transferFromPreviousLeg, - null, - legCost, - headwaySecs, - null - ); + new FrequencyTransitLegBuilder() + .withTripTimes(tripTimes) + .withTripPattern(tripPattern) + .withBoardStopIndexInPattern(fromStopIndex) + .withAlightStopIndexInPattern(toStopIndex) + .withStartTime(newTime(start)) + .withEndTime(newTime(end)) + .withServiceDate(serviceDate != null ? serviceDate : SERVICE_DAY) + .withZoneId(UTC) + .withTransferFromPreviousLeg(transferFromPreviousLeg) + .withGeneralizedCost(legCost) + .withFrequencyHeadwayInSeconds(headwaySecs) + .build(); } else { leg = - new ScheduledTransitLeg( - tripTimes, - tripPattern, - fromStopIndex, - toStopIndex, - newTime(start), - newTime(end), - serviceDate != null ? serviceDate : SERVICE_DAY, - UTC, - transferFromPreviousLeg, - null, - legCost, - null - ); + new ScheduledTransitLegBuilder() + .withTripTimes(tripTimes) + .withTripPattern(tripPattern) + .withBoardStopIndexInPattern(fromStopIndex) + .withAlightStopIndexInPattern(toStopIndex) + .withStartTime(newTime(start)) + .withEndTime(newTime(end)) + .withServiceDate(serviceDate != null ? serviceDate : SERVICE_DAY) + .withZoneId(UTC) + .withTransferFromPreviousLeg(transferFromPreviousLeg) + .withGeneralizedCost(legCost) + .build(); } leg.setDistanceMeters(speed(leg.getMode()) * (end - start)); diff --git a/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java b/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java index 655c2c24778..da295073e01 100644 --- a/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java +++ b/src/test/java/org/opentripplanner/model/plan/legreference/LegReferenceSerializerTest.java @@ -19,16 +19,22 @@ class LegReferenceSerializerTest { private static final FeedScopedId TO_STOP_ID = TransitModelForTest.id("Alighting Stop"); /** - * Token based on the latest format, including stop ids. + * Token based on the initial format, without stop ids. + */ + private static final String ENCODED_TOKEN_V1 = + "rO0ABXc2ABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAAD"; + + /** + * Token based on the second version of the format, including stop ids. */ private static final String ENCODED_TOKEN_V2 = "rO0ABXdZABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjIABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAADAA9GOkJvYXJkaW5nIFN0b3AAEEY6QWxpZ2h0aW5nIFN0b3A="; /** - * Token based on the previous format, without stop ids. + * Token based on the latest format, including stop ids and TripOnServiceDate id. */ - private static final String ENCODED_TOKEN_V1 = - "rO0ABXc2ABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjEABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAAD"; + private static final String ENCODED_TOKEN_V3 = + "rO0ABXdbABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjMABkY6VHJpcAAKMjAyMi0wMS0zMQAAAAEAAAADAA9GOkJvYXJkaW5nIFN0b3AAEEY6QWxpZ2h0aW5nIFN0b3AAAA=="; @Test void testScheduledTransitLegReferenceRoundTrip() { @@ -38,12 +44,13 @@ void testScheduledTransitLegReferenceRoundTrip() { FROM_STOP_POS, TO_STOP_POS, FROM_STOP_ID, - TO_STOP_ID + TO_STOP_ID, + null ); var out = LegReferenceSerializer.encode(ref); - assertEquals(ENCODED_TOKEN_V2, out); + assertEquals(ENCODED_TOKEN_V3, out); var ref2 = LegReferenceSerializer.decode(out); @@ -52,7 +59,7 @@ void testScheduledTransitLegReferenceRoundTrip() { @Test void testScheduledTransitLegReferenceDeserialize() { - var ref = (ScheduledTransitLegReference) LegReferenceSerializer.decode(ENCODED_TOKEN_V2); + var ref = (ScheduledTransitLegReference) LegReferenceSerializer.decode(ENCODED_TOKEN_V3); assertNotNull(ref); assertEquals(TRIP_ID, ref.tripId()); assertEquals(SERVICE_DATE, ref.serviceDate()); @@ -61,7 +68,7 @@ void testScheduledTransitLegReferenceDeserialize() { } @Test - void testScheduledTransitLegReferenceLegacyDeserialize() { + void testScheduledTransitLegReferenceLegacyV1Deserialize() { var ref = (ScheduledTransitLegReference) LegReferenceSerializer.decode(ENCODED_TOKEN_V1); assertNotNull(ref); assertEquals(TRIP_ID, ref.tripId()); @@ -69,4 +76,14 @@ void testScheduledTransitLegReferenceLegacyDeserialize() { assertEquals(FROM_STOP_POS, ref.fromStopPositionInPattern()); assertEquals(TO_STOP_POS, ref.toStopPositionInPattern()); } + + @Test + void testScheduledTransitLegReferenceLegacyV2Deserialize() { + var ref = (ScheduledTransitLegReference) LegReferenceSerializer.decode(ENCODED_TOKEN_V2); + assertNotNull(ref); + assertEquals(TRIP_ID, ref.tripId()); + assertEquals(SERVICE_DATE, ref.serviceDate()); + assertEquals(FROM_STOP_POS, ref.fromStopPositionInPattern()); + assertEquals(TO_STOP_POS, ref.toStopPositionInPattern()); + } } diff --git a/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java b/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java index d44b2eb4857..4f7a485ff83 100644 --- a/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java +++ b/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.opentripplanner.transit.model._data.TransitModelForTest.id; import java.time.LocalDate; @@ -21,6 +22,7 @@ import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.StopModel; @@ -32,6 +34,7 @@ class ScheduledTransitLegReferenceTest { private static final int SERVICE_CODE = 555; private static final LocalDate SERVICE_DATE = LocalDate.of(2023, 1, 1); private static final int NUMBER_OF_STOPS = 3; + private static final FeedScopedId TRIP_ON_SERVICE_DATE_ID = id("TRIP_ON_SERVICE_DATE_ID"); public static FeedScopedId stopIdAtPosition0; public static FeedScopedId stopIdAtPosition1; @@ -82,6 +85,18 @@ static void buildTransitService() { CalendarServiceData calendarServiceData = new CalendarServiceData(); calendarServiceData.putServiceDatesForServiceId(tripPattern.getId(), List.of(SERVICE_DATE)); transitModel.updateCalendarServiceData(true, calendarServiceData, DataImportIssueStore.NOOP); + + transitModel + .getTripOnServiceDates() + .put( + TRIP_ON_SERVICE_DATE_ID, + TripOnServiceDate + .of(TRIP_ON_SERVICE_DATE_ID) + .withTrip(trip) + .withServiceDate(SERVICE_DATE) + .build() + ); + transitModel.index(); // build transit service @@ -98,7 +113,8 @@ void getLegFromReference() { boardAtStopPos, alightAtStopPos, stopIdAtPosition0, - stopIdAtPosition1 + stopIdAtPosition1, + null ); ScheduledTransitLeg leg = scheduledTransitLegReference.getLeg(transitService); assertNotNull(leg); @@ -116,7 +132,8 @@ void getLegFromReferenceUnknownTrip() { 0, 1, stopIdAtPosition0, - stopIdAtPosition1 + stopIdAtPosition1, + null ); assertNull(scheduledTransitLegReference.getLeg(transitService)); } @@ -129,7 +146,8 @@ void getLegFromReferenceInvalidServiceDate() { 0, 1, stopIdAtPosition0, - stopIdAtPosition1 + stopIdAtPosition1, + null ); assertNull(scheduledTransitLegReference.getLeg(transitService)); } @@ -142,7 +160,8 @@ void getLegFromReferenceOutOfRangeBoardingStop() { NUMBER_OF_STOPS, 1, stopIdAtPosition0, - stopIdAtPosition1 + stopIdAtPosition1, + null ); assertNull(scheduledTransitLegReference.getLeg(transitService)); } @@ -155,7 +174,8 @@ void getLegFromReferenceMismatchOnBoardingStop() { 0, 1, TransitModelForTest.id("invalid stop id"), - stopIdAtPosition1 + stopIdAtPosition1, + null ); assertNull(scheduledTransitLegReference.getLeg(transitService)); } @@ -170,7 +190,8 @@ void getLegFromReferenceMismatchOnAlightingStopSameParentStation() { 0, 2, stopIdAtPosition0, - stop4.getId() + stop4.getId(), + null ); assertNotNull(scheduledTransitLegReference.getLeg(transitService)); } @@ -183,8 +204,71 @@ void getLegFromReferenceOutOfRangeAlightingStop() { 0, NUMBER_OF_STOPS, stopIdAtPosition0, - stopIdAtPosition1 + stopIdAtPosition1, + null + ); + assertNull(scheduledTransitLegReference.getLeg(transitService)); + } + + @Test + void legReferenceCannotReferToBothTripAndTripOnServiceDate() { + assertThrows( + IllegalArgumentException.class, + () -> + new ScheduledTransitLegReference( + tripId, + SERVICE_DATE, + 0, + NUMBER_OF_STOPS, + stopIdAtPosition0, + stopIdAtPosition1, + TransitModelForTest.id("trip on date id") + ) + ); + } + + @Test + void legReferenceCannotReferToInconsistentServiceDateAndTripOnServiceDate() { + ScheduledTransitLegReference scheduledTransitLegReference = new ScheduledTransitLegReference( + null, + SERVICE_DATE.plusDays(1), + 0, + 1, + stopIdAtPosition0, + stopIdAtPosition1, + TRIP_ON_SERVICE_DATE_ID + ); + assertNull(scheduledTransitLegReference.getLeg(transitService)); + } + + @Test + void getLegFromReferenceWithUnknownTripOnDate() { + ScheduledTransitLegReference scheduledTransitLegReference = new ScheduledTransitLegReference( + null, + SERVICE_DATE, + 0, + NUMBER_OF_STOPS, + stopIdAtPosition0, + stopIdAtPosition1, + TransitModelForTest.id("unknown trip on date id") ); assertNull(scheduledTransitLegReference.getLeg(transitService)); } + + @Test + void getLegFromReferenceWithValidTripOnDate() { + ScheduledTransitLegReference scheduledTransitLegReference = new ScheduledTransitLegReference( + null, + SERVICE_DATE, + 0, + 1, + stopIdAtPosition0, + stopIdAtPosition1, + TRIP_ON_SERVICE_DATE_ID + ); + ScheduledTransitLeg leg = scheduledTransitLegReference.getLeg(transitService); + assertNotNull(leg); + assertEquals(tripId, leg.getTrip().getId()); + assertEquals(SERVICE_DATE, leg.getServiceDate()); + } } diff --git a/src/test/java/org/opentripplanner/routing/stoptimes/AlternativeLegsTest.java b/src/test/java/org/opentripplanner/routing/stoptimes/AlternativeLegsTest.java index a7369a98a8c..96f49bb9c78 100644 --- a/src/test/java/org/opentripplanner/routing/stoptimes/AlternativeLegsTest.java +++ b/src/test/java/org/opentripplanner/routing/stoptimes/AlternativeLegsTest.java @@ -42,7 +42,8 @@ void testPreviousLegs() { 1, 2, STOP_ID_B, - STOP_ID_C + STOP_ID_C, + null ) .getLeg(transitService); @@ -80,7 +81,8 @@ void testNextLegs() { 0, 1, STOP_ID_B, - STOP_ID_C + STOP_ID_C, + null ) .getLeg(transitService); @@ -118,7 +120,8 @@ void testCircularRoutes() { 1, 2, STOP_ID_X, - STOP_ID_Y + STOP_ID_Y, + null ) .getLeg(transitService); @@ -151,7 +154,8 @@ void testComplexCircularRoutes() { 1, 7, STOP_ID_X, - STOP_ID_B + STOP_ID_B, + null ) .getLeg(transitService); From 64c48f9863ca87340db67fd83dae653f0b0e852b Mon Sep 17 00:00:00 2001 From: OTP Changelog Bot Date: Thu, 26 Oct 2023 18:35:42 +0000 Subject: [PATCH 33/60] Add changelog entry for #5425 [ci skip] --- docs/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Changelog.md b/docs/Changelog.md index 6d13e66cf57..64dc117a3b6 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -25,6 +25,7 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Allow multiple zones in an unscheduled flex trip [#5376](https://github.com/opentripplanner/OpenTripPlanner/pull/5376) - Filter out null, empty and blank elements when mapping feed-scoped ids [#5428](https://github.com/opentripplanner/OpenTripPlanner/pull/5428) - Validate stop id in Transit leg reference [#5440](https://github.com/opentripplanner/OpenTripPlanner/pull/5440) +- Add available types and spaces to `VehicleRentalStation` [#5425](https://github.com/opentripplanner/OpenTripPlanner/pull/5425) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.4.0 (2023-09-13) From 544a37d803e4decf84154b398ec746fbd0104ea8 Mon Sep 17 00:00:00 2001 From: OTP Changelog Bot Date: Fri, 27 Oct 2023 07:55:57 +0000 Subject: [PATCH 34/60] Add changelog entry for #5411 [ci skip] --- docs/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Changelog.md b/docs/Changelog.md index 64dc117a3b6..c50af19175c 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -26,6 +26,7 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Filter out null, empty and blank elements when mapping feed-scoped ids [#5428](https://github.com/opentripplanner/OpenTripPlanner/pull/5428) - Validate stop id in Transit leg reference [#5440](https://github.com/opentripplanner/OpenTripPlanner/pull/5440) - Add available types and spaces to `VehicleRentalStation` [#5425](https://github.com/opentripplanner/OpenTripPlanner/pull/5425) +- Make vehicleRentalStation query optionally accept id without feed [#5411](https://github.com/opentripplanner/OpenTripPlanner/pull/5411) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.4.0 (2023-09-13) From 4d5fc5ec7ee7f5ec6cd4723147e6ac0dcde50cf6 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Fri, 27 Oct 2023 11:06:10 +0200 Subject: [PATCH 35/60] Add stricter validation for flex areas --- .../netex/mapping/FlexStopsMapper.java | 3 -- .../netex/mapping/OpenGisMapper.java | 6 ++- .../netex/mapping/FlexStopsMapperTest.java | 52 +++++++++++++++---- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/opentripplanner/netex/mapping/FlexStopsMapper.java b/src/main/java/org/opentripplanner/netex/mapping/FlexStopsMapper.java index b7d8657554e..f43746f07e7 100644 --- a/src/main/java/org/opentripplanner/netex/mapping/FlexStopsMapper.java +++ b/src/main/java/org/opentripplanner/netex/mapping/FlexStopsMapper.java @@ -19,12 +19,9 @@ import org.rutebanken.netex.model.FlexibleStopPlace; import org.rutebanken.netex.model.KeyListStructure; import org.rutebanken.netex.model.KeyValueStructure; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; class FlexStopsMapper { - private static final Logger LOG = LoggerFactory.getLogger(FlexStopsMapper.class); /** * Key-value pair used until proper NeTEx support is added */ diff --git a/src/main/java/org/opentripplanner/netex/mapping/OpenGisMapper.java b/src/main/java/org/opentripplanner/netex/mapping/OpenGisMapper.java index 63b22ebc66a..ec5b4303fa4 100644 --- a/src/main/java/org/opentripplanner/netex/mapping/OpenGisMapper.java +++ b/src/main/java/org/opentripplanner/netex/mapping/OpenGisMapper.java @@ -19,7 +19,7 @@ class OpenGisMapper { static Geometry mapGeometry(PolygonType polygonType) { - return new Polygon( + Polygon polygon = new Polygon( new LinearRing( mapCoordinateSequence(polygonType.getExterior()), GeometryUtils.getGeometryFactory() @@ -31,6 +31,10 @@ static Geometry mapGeometry(PolygonType polygonType) { .toArray(LinearRing[]::new), GeometryUtils.getGeometryFactory() ); + if (!polygon.isValid()) { + throw new IllegalArgumentException("The polygon has an invalid geometry"); + } + return polygon; } private static CoordinateSequence mapCoordinateSequence( diff --git a/src/test/java/org/opentripplanner/netex/mapping/FlexStopsMapperTest.java b/src/test/java/org/opentripplanner/netex/mapping/FlexStopsMapperTest.java index 3d43a4bb793..c4731afd36f 100644 --- a/src/test/java/org/opentripplanner/netex/mapping/FlexStopsMapperTest.java +++ b/src/test/java/org/opentripplanner/netex/mapping/FlexStopsMapperTest.java @@ -85,7 +85,7 @@ class FlexStopsMapperTest { 6.3023991052849 ); - static final Collection INVALID_AREA_POS_LIST = List.of( + static final Collection INVALID_NON_CLOSED_POLYGON = List.of( 59.62575084033623, 6.3023991052849, 59.62883380609349, @@ -94,6 +94,29 @@ class FlexStopsMapperTest { 6.293494451572027 ); + static final Collection INVALID_SELF_INTERSECTING_POLYGON = List.of( + 63.596915083462335, + 10.878374152208456, + 63.65365163120023, + 10.885927252794394, + 63.66835971343224, + 10.878885368213025, + 63.64886239899589, + 10.847544187841429, + 63.64938508072749, + 10.785677008653767, + 63.56025960429534, + 10.535758055643848, + 63.52844559758193, + 10.668967284159471, + 63.59753465537067, + 10.879080809550098, + 63.617069269781574, + 10.88251403708916, + 63.596915083462335, + 10.878374152208456 + ); + @Test void testMapAreaStop() { FlexStopsMapper flexStopsMapper = new FlexStopsMapper( @@ -122,17 +145,14 @@ void testMapAreaStop() { } @Test - void testMapInvalidAreaStop() { - FlexStopsMapper flexStopsMapper = new FlexStopsMapper( - ID_FACTORY, - List.of(), - DataImportIssueStore.NOOP - ); - - FlexibleStopPlace flexibleStopPlace = getFlexibleStopPlace(INVALID_AREA_POS_LIST); - - AreaStop areaStop = (AreaStop) flexStopsMapper.map(flexibleStopPlace); + void testMapInvalidNonClosedAreaStop() { + AreaStop areaStop = createAreaStop(INVALID_NON_CLOSED_POLYGON); + assertNull(areaStop); + } + @Test + void testMapInvalidSelfIntersectingAreaStop() { + AreaStop areaStop = createAreaStop(INVALID_SELF_INTERSECTING_POLYGON); assertNull(areaStop); } @@ -269,4 +289,14 @@ private FlexibleStopPlace getFlexibleStopPlace(Collection areaPosList) { ) ); } + + private AreaStop createAreaStop(Collection polygonCoordinates) { + FlexStopsMapper flexStopsMapper = new FlexStopsMapper( + ID_FACTORY, + List.of(), + DataImportIssueStore.NOOP + ); + FlexibleStopPlace flexibleStopPlace = getFlexibleStopPlace(polygonCoordinates); + return (AreaStop) flexStopsMapper.map(flexibleStopPlace); + } } From b5d994c07c33fe1ebbee7d3d15504a2fd0372ff7 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 27 Oct 2023 11:54:12 +0200 Subject: [PATCH 36/60] Run OTP with one key push in interactive OTP launcher --- .../ext/interactivelauncher/views/MainView.java | 5 ++++- .../ext/interactivelauncher/views/StartOtpButtonView.java | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ext/java/org/opentripplanner/ext/interactivelauncher/views/MainView.java b/src/ext/java/org/opentripplanner/ext/interactivelauncher/views/MainView.java index cd078e029ce..44ede02e3eb 100644 --- a/src/ext/java/org/opentripplanner/ext/interactivelauncher/views/MainView.java +++ b/src/ext/java/org/opentripplanner/ext/interactivelauncher/views/MainView.java @@ -118,6 +118,7 @@ public class MainView { private final DataSourcesView dataSourcesView; private final OptionsView optionsView; + private final StartOtpButtonView startOtpButtonView; private final Runnable otpStarter; private final Model model; @@ -139,7 +140,7 @@ public MainView(Runnable otpStarter, Model model) throws HeadlessException { ); this.dataSourcesView = new DataSourcesView(model); this.optionsView = new OptionsView(model); - StartOtpButtonView startOtpButtonView = new StartOtpButtonView(); + this.startOtpButtonView = new StartOtpButtonView(); innerPanel.add(sourceDirectoryView.panel(), CONFIG_SOURCE_DIR_PANEL_CONSTRAINTS); innerPanel.add(dataSourcesView.panel(), CONFIG_DIRS_PANEL_CONSTRAINTS); @@ -176,6 +177,8 @@ public void start() { mainFrame.pack(); mainFrame.setLocationRelativeTo(null); mainFrame.setVisible(true); + + startOtpButtonView.grabFocus(); } private void startOtp() { diff --git a/src/ext/java/org/opentripplanner/ext/interactivelauncher/views/StartOtpButtonView.java b/src/ext/java/org/opentripplanner/ext/interactivelauncher/views/StartOtpButtonView.java index c5668ee3aa5..5c0c1e7a621 100644 --- a/src/ext/java/org/opentripplanner/ext/interactivelauncher/views/StartOtpButtonView.java +++ b/src/ext/java/org/opentripplanner/ext/interactivelauncher/views/StartOtpButtonView.java @@ -29,4 +29,8 @@ public void addActionListener(ActionListener l) { Box panel() { return panel; } + + void grabFocus() { + startOtpBtn.grabFocus(); + } } From ed8c514adbf241b2723419103ff75c45e4f804a0 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Fri, 27 Oct 2023 12:11:00 +0200 Subject: [PATCH 37/60] Apply suggestions from code review Co-authored-by: Joel Lappalainen --- docs/Basic-Tutorial.md | 10 ++++++---- .../standalone/server/GrizzlyServer.java | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/Basic-Tutorial.md b/docs/Basic-Tutorial.md index 51c164200e6..44782b23972 100644 --- a/docs/Basic-Tutorial.md +++ b/docs/Basic-Tutorial.md @@ -139,10 +139,12 @@ with your local OpenTripPlanner instance. This map-based user interface is in fact sending HTTP GET requests to the OTP server running on your local machine. It can be informative to watch the HTTP requests and responses being generated using -the developer tools in your web browser. OTP's built-in web server will run by default on ports 8080 -and 8081 for HTTP and HTTPS respectively. If by any chance some other software is already using one -or both of those port numbers, you can specify different port numbers with switches -like `--port 8801`. +```suggestion +the developer tools in your web browser. OTP's built-in web server will run by default on port 8080. +If by any chance some other software is already using that port number, you can specify a different +port number with a switch +`--port 8801`. + ## Saving a Graph diff --git a/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java b/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java index 09741bc475e..087d75cf5bf 100644 --- a/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java +++ b/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java @@ -83,7 +83,7 @@ public void run() { ); httpListener.setSecure(false); - // For both HTTP and HTTPS listeners: enable gzip compression, set thread pool, add listener to httpServer. + // For the HTTP listeners: enable gzip compression, set thread pool, add listener to httpServer. CompressionConfig cc = httpListener.getCompressionConfig(); cc.setCompressionMode(CompressionConfig.CompressionMode.ON); cc.setCompressionMinSize(50000); // the min number of bytes to compress From e0a5deb5a6e4fce374431e8233096bd049cf1ccf Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Fri, 27 Oct 2023 12:31:27 +0200 Subject: [PATCH 38/60] Update src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java Co-authored-by: Joel Lappalainen --- .../org/opentripplanner/standalone/server/GrizzlyServer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java b/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java index 087d75cf5bf..a0a2228529e 100644 --- a/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java +++ b/src/main/java/org/opentripplanner/standalone/server/GrizzlyServer.java @@ -83,7 +83,7 @@ public void run() { ); httpListener.setSecure(false); - // For the HTTP listeners: enable gzip compression, set thread pool, add listener to httpServer. + // For the HTTP listener: enable gzip compression, set thread pool, add listener to httpServer. CompressionConfig cc = httpListener.getCompressionConfig(); cc.setCompressionMode(CompressionConfig.CompressionMode.ON); cc.setCompressionMinSize(50000); // the min number of bytes to compress From 6c4497e5ed6f5b7e1988a88233e4b0a14e6bdfa4 Mon Sep 17 00:00:00 2001 From: OTP Changelog Bot Date: Fri, 27 Oct 2023 10:39:11 +0000 Subject: [PATCH 39/60] Add changelog entry for #5457 [ci skip] --- docs/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Changelog.md b/docs/Changelog.md index c50af19175c..0ad3737682f 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -27,6 +27,7 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Validate stop id in Transit leg reference [#5440](https://github.com/opentripplanner/OpenTripPlanner/pull/5440) - Add available types and spaces to `VehicleRentalStation` [#5425](https://github.com/opentripplanner/OpenTripPlanner/pull/5425) - Make vehicleRentalStation query optionally accept id without feed [#5411](https://github.com/opentripplanner/OpenTripPlanner/pull/5411) +- Add stricter validation for flex areas [#5457](https://github.com/opentripplanner/OpenTripPlanner/pull/5457) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.4.0 (2023-09-13) From cec1040fd51f80c7caa41a3c8adde865f080c16d Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Fri, 27 Oct 2023 13:56:18 +0200 Subject: [PATCH 40/60] Update docs/Basic-Tutorial.md Co-authored-by: Johan Torin --- docs/Basic-Tutorial.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Basic-Tutorial.md b/docs/Basic-Tutorial.md index 44782b23972..938372c7de2 100644 --- a/docs/Basic-Tutorial.md +++ b/docs/Basic-Tutorial.md @@ -139,7 +139,6 @@ with your local OpenTripPlanner instance. This map-based user interface is in fact sending HTTP GET requests to the OTP server running on your local machine. It can be informative to watch the HTTP requests and responses being generated using -```suggestion the developer tools in your web browser. OTP's built-in web server will run by default on port 8080. If by any chance some other software is already using that port number, you can specify a different port number with a switch From 2df233d2ead2c2b04b70d33db0b6445facdde5b8 Mon Sep 17 00:00:00 2001 From: OTP Changelog Bot Date: Fri, 27 Oct 2023 12:14:14 +0000 Subject: [PATCH 41/60] Add changelog entry for #5439 [ci skip] --- docs/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Changelog.md b/docs/Changelog.md index 0ad3737682f..802013c3921 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -28,6 +28,7 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Add available types and spaces to `VehicleRentalStation` [#5425](https://github.com/opentripplanner/OpenTripPlanner/pull/5425) - Make vehicleRentalStation query optionally accept id without feed [#5411](https://github.com/opentripplanner/OpenTripPlanner/pull/5411) - Add stricter validation for flex areas [#5457](https://github.com/opentripplanner/OpenTripPlanner/pull/5457) +- Remove HTTPS handling and its documentation [#5439](https://github.com/opentripplanner/OpenTripPlanner/pull/5439) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.4.0 (2023-09-13) From f127f4aebd660064dd30fb93f3f98b1e5917d89c Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 27 Oct 2023 11:56:35 +0200 Subject: [PATCH 42/60] refactor: Clean code --- .../org/opentripplanner/ext/siri/updater/SiriETUpdater.java | 4 +++- .../java/org/opentripplanner/model/TimetableSnapshot.java | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java index 739a56e28ef..f0d8a7a11f5 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java @@ -110,7 +110,9 @@ public void runPolling() { ); ResultLogger.logUpdateResult(feedId, "siri-et", result); recordMetrics.accept(result); - if (markPrimed) primed = true; + if (markPrimed) { + primed = true; + } }); } } diff --git a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java index 85f61710363..e9d61d9021b 100644 --- a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java +++ b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java @@ -202,7 +202,9 @@ public Result update( temp.addAll(sortedTimetables); sortedTimetables = temp; } - if (old.getServiceDate() != null) sortedTimetables.remove(old); + if (old.getServiceDate() != null) { + sortedTimetables.remove(old); + } sortedTimetables.add(tt); timetables.put(pattern, sortedTimetables); dirtyTimetables.add(tt); From e7e97d66ada59a708ee52aef1d94fe9ba7c85679 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 27 Oct 2023 12:15:01 +0200 Subject: [PATCH 43/60] refactor: Print date&time in ToString builder for addTime(String name, ZonedDateTime time) --- .../opentripplanner/framework/tostring/ToStringBuilder.java | 2 +- .../framework/tostring/ToStringBuilderTest.java | 5 ++++- .../algorithm/filterchain/groupids/GroupByDistanceTest.java | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/opentripplanner/framework/tostring/ToStringBuilder.java b/src/main/java/org/opentripplanner/framework/tostring/ToStringBuilder.java index 2c855cd05fc..f6c17b19af1 100644 --- a/src/main/java/org/opentripplanner/framework/tostring/ToStringBuilder.java +++ b/src/main/java/org/opentripplanner/framework/tostring/ToStringBuilder.java @@ -237,7 +237,7 @@ public ToStringBuilder addDateTime(String name, Instant time) { * DATE is not printed. {@code null} value is ignored. */ public ToStringBuilder addTime(String name, ZonedDateTime time) { - return addIfNotNull(name, time, DateTimeFormatter.ISO_LOCAL_TIME::format); + return addIfNotNull(name, time, DateTimeFormatter.ISO_LOCAL_DATE_TIME::format); } /** diff --git a/src/test/java/org/opentripplanner/framework/tostring/ToStringBuilderTest.java b/src/test/java/org/opentripplanner/framework/tostring/ToStringBuilderTest.java index 5f67d1df40a..ca35c8272db 100644 --- a/src/test/java/org/opentripplanner/framework/tostring/ToStringBuilderTest.java +++ b/src/test/java/org/opentripplanner/framework/tostring/ToStringBuilderTest.java @@ -251,7 +251,10 @@ public void addTime() { LocalDateTime.of(2012, 1, 28, 23, 45, 12), TIME_ZONE_ID_PARIS ); - assertEquals("ToStringBuilderTest{t: 23:45:12}", subject().addTime("t", c).toString()); + assertEquals( + "ToStringBuilderTest{t: 2012-01-28T23:45:12}", + subject().addTime("t", c).toString() + ); } @Test diff --git a/src/test/java/org/opentripplanner/routing/algorithm/filterchain/groupids/GroupByDistanceTest.java b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/groupids/GroupByDistanceTest.java index a1938f0c213..76d3b34fb5d 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/filterchain/groupids/GroupByDistanceTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/groupids/GroupByDistanceTest.java @@ -224,7 +224,7 @@ public void testToString() { var subject = new GroupByDistance(itinerary, 0.5); assertEquals( - "GroupByDistance{streetOnly, keySet: [StreetLeg{start: 11:00:00, end: 11:00:00, mode: BICYCLE}]}", + "GroupByDistance{streetOnly, keySet: [StreetLeg{start: 2020-02-02T11:00:00, end: 2020-02-02T11:00:00, mode: BICYCLE}]}", subject.toString() ); @@ -232,7 +232,7 @@ public void testToString() { subject = new GroupByDistance(itinerary, 0.5); assertEquals( - "GroupByDistance{keySet: [ScheduledTransitLeg{start: 11:10:00, end: 11:10:00, mode: BUS, tripId: F:11}]}", + "GroupByDistance{keySet: [ScheduledTransitLeg{start: 2020-02-02T11:10:00, end: 2020-02-02T11:10:00, mode: BUS, tripId: F:11}]}", subject.toString() ); } From aed1d4b020bef790db2e0a1b1853d02f5e74babf Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 27 Oct 2023 12:28:32 +0200 Subject: [PATCH 44/60] Add a CountdownTimer for use with throttling updaters --- .../framework/time/CountdownTimer.java | 73 +++++++++++++++++++ .../framework/time/CountdownTimerTest.java | 72 ++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 src/main/java/org/opentripplanner/framework/time/CountdownTimer.java create mode 100644 src/test/java/org/opentripplanner/framework/time/CountdownTimerTest.java diff --git a/src/main/java/org/opentripplanner/framework/time/CountdownTimer.java b/src/main/java/org/opentripplanner/framework/time/CountdownTimer.java new file mode 100644 index 00000000000..3ab66782f10 --- /dev/null +++ b/src/main/java/org/opentripplanner/framework/time/CountdownTimer.java @@ -0,0 +1,73 @@ +package org.opentripplanner.framework.time; + +import java.time.Duration; +import java.util.function.LongSupplier; + +/** + * This class will track time and is useful when for example throttling something. It does NOT + * notify anyone, but the caller must regularly check if the timer is finished. + */ +public class CountdownTimer { + + private final LongSupplier clock; + private final long countDownDuration; + private long timeLimit; + + /** + * Create new timer with the given count-down-duration time. + */ + public CountdownTimer(Duration countDownDuration) { + this(countDownDuration, System::currentTimeMillis); + } + + /** + * This constructor allows us to unit test the timer without using + * {@code System::currentTimeMillis}. + *

+ * The timer is started, no need to explicit call {@link #restart()} right + * after the construction. + */ + CountdownTimer(Duration countDownDuration, LongSupplier clock) { + this.clock = clock; + this.countDownDuration = countDownDuration.toMillis(); + restart(); + } + + /** + * Start/restart the counting down the time. + */ + public void restart() { + restart(now()); + } + + /** + * The timer has past the count-down-limit (startTime + countDownDuration) + */ + public boolean timeIsUp() { + return timeIsUp(now()); + } + + /** + * Return {@code true} if time is up, and then imitatively start the timer again. + */ + public boolean nextLap() { + long time = now(); + if (timeIsUp(time)) { + restart(time); + return true; + } + return false; + } + + private boolean timeIsUp(long time) { + return time >= timeLimit; + } + + private void restart(long time) { + this.timeLimit = time + countDownDuration; + } + + private long now() { + return clock.getAsLong(); + } +} diff --git a/src/test/java/org/opentripplanner/framework/time/CountdownTimerTest.java b/src/test/java/org/opentripplanner/framework/time/CountdownTimerTest.java new file mode 100644 index 00000000000..4d5bdd6fe1d --- /dev/null +++ b/src/test/java/org/opentripplanner/framework/time/CountdownTimerTest.java @@ -0,0 +1,72 @@ +package org.opentripplanner.framework.time; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import org.junit.jupiter.api.Test; + +class CountdownTimerTest { + + private long time = 9_999; + + @Test + void finished() { + time = 500; + var subject = new CountdownTimer(Duration.ofMillis(100), this::time); + assertFalse(subject.timeIsUp()); + + time = 599; + assertFalse(subject.timeIsUp()); + + time = 600; + assertTrue(subject.timeIsUp()); + + time = 600; + assertTrue(subject.timeIsUp()); + } + + @Test + void restart() { + time = 500; + var subject = new CountdownTimer(Duration.ofMillis(100), this::time); + + time = 620; + subject.restart(); + + time = 719; + assertFalse(subject.timeIsUp()); + + time = 720; + assertTrue(subject.timeIsUp()); + } + + @Test + void nextLap() { + time = 0; + var subject = new CountdownTimer(Duration.ofMillis(100), this::time); + assertFalse(subject.nextLap()); + + time = 99; + assertFalse(subject.nextLap()); + + time = 100; + assertTrue(subject.nextLap()); + + time = 99; + assertFalse(subject.nextLap()); + + time = 220; + assertTrue(subject.nextLap()); + + time = 319; + assertFalse(subject.nextLap()); + + time = 320; + assertTrue(subject.nextLap()); + } + + long time() { + return time; + } +} From 2349a14b94354dd57f652ad8f06d9e54d7168c41 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 27 Oct 2023 17:07:24 +0200 Subject: [PATCH 45/60] improve: Always commit the first update after a long break. In a busy updater it does not matter if we commit early or late, as long as we throttle the commit to happen with a given frequency. But, when testing OTP manually, it is easy to do mistakes and assume that an update is applied when you see "INFO 1 of 1 update messages were applied successfully (success rate: 100.0%)". In OTP before this commit, the update was not committed, waiting to fill up the "buffer", and if not other update was applied - nothing happened. --- .../ext/siri/SiriTimetableSnapshotSource.java | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java index 937adc112ab..3e6b4a62fd4 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java @@ -8,12 +8,12 @@ import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.TRIP_NOT_FOUND_IN_PATTERN; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.UNKNOWN; -import java.time.Duration; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.ReentrantLock; import javax.annotation.Nullable; +import org.opentripplanner.framework.time.CountdownTimer; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.model.TimetableSnapshotProvider; @@ -73,7 +73,7 @@ public class SiriTimetableSnapshotSource implements TimetableSnapshotProvider { * snapshot, just return the same one. Throttles the potentially resource-consuming task of * duplicating a TripPattern -> Timetable map and indexing the new Timetables. */ - private final Duration maxSnapshotFrequency; + protected CountdownTimer snapshotFrequencyThrottle; /** * The last committed snapshot that was handed off to a routing thread. This snapshot may be given @@ -85,7 +85,6 @@ public class SiriTimetableSnapshotSource implements TimetableSnapshotProvider { private final boolean purgeExpiredData; protected LocalDate lastPurgeDate = null; - protected long lastSnapshotTime = -1; public SiriTimetableSnapshotSource( TimetableSnapshotSourceParameters parameters, @@ -94,7 +93,7 @@ public SiriTimetableSnapshotSource( this.transitModel = transitModel; this.transitService = new DefaultTransitService(transitModel); this.transitLayerUpdater = transitModel.getTransitLayerUpdater(); - this.maxSnapshotFrequency = parameters.maxSnapshotFrequency(); + this.snapshotFrequencyThrottle = new CountdownTimer(parameters.maxSnapshotFrequency()); this.purgeExpiredData = parameters.purgeExpiredData(); this.tripPatternCache = new SiriTripPatternCache(tripPatternIdGenerator, transitService::getPatternForTrip); @@ -102,7 +101,7 @@ public SiriTimetableSnapshotSource( transitModel.initTimetableSnapshotProvider(this); // Force commit so that snapshot initializes - getTimetableSnapshot(true); + commitTimetableSnapshot(true); } /** @@ -118,17 +117,15 @@ public TimetableSnapshot getTimetableSnapshot() { if (bufferLock.tryLock()) { // Make a new snapshot if necessary try { - snapshotToReturn = getTimetableSnapshot(false); + commitTimetableSnapshot(false); + return snapshot; } finally { bufferLock.unlock(); } - } else { - // No lock could be obtained because there is either a snapshot commit busy or updates - // are applied at this moment, just return the current snapshot - snapshotToReturn = snapshot; } - - return snapshotToReturn; + // No lock could be obtained because there is either a snapshot commit busy or updates + // are applied at this moment, just return the current snapshot + return snapshot; } /** @@ -172,16 +169,15 @@ public UpdateResult applyEstimatedTimetable( } LOG.debug("message contains {} trip updates", updates.size()); - LOG.debug("end of update message"); // Make a snapshot after each message in anticipation of incoming requests // Purge data if necessary (and force new snapshot if anything was purged) // Make sure that the public (locking) getTimetableSnapshot function is not called. if (purgeExpiredData) { final boolean modified = purgeExpiredData(); - getTimetableSnapshot(modified); + commitTimetableSnapshot(modified); } else { - getTimetableSnapshot(false); + commitTimetableSnapshot(false); } } finally { // Always release lock @@ -246,20 +242,22 @@ private boolean shouldAddNewTrip( return entityResolver.resolveTrip(vehicleJourney) == null; } - private TimetableSnapshot getTimetableSnapshot(final boolean force) { - final long now = System.currentTimeMillis(); - if (force || now - lastSnapshotTime > maxSnapshotFrequency.toMillis()) { + private void commitTimetableSnapshot(final boolean force) { + if (force || snapshotFrequencyThrottle.timeIsUp()) { if (force || buffer.isDirty()) { LOG.debug("Committing {}", buffer); snapshot = buffer.commit(transitLayerUpdater, force); + + // We only reset the timer when the snapshot is updated. This will cause the first + // update to be committed after a silent period. This should not have any effect in + // a busy updater. It is however useful when manually testing the updater. + snapshotFrequencyThrottle.restart(); } else { LOG.debug("Buffer was unchanged, keeping old snapshot."); } - lastSnapshotTime = System.currentTimeMillis(); } else { LOG.debug("Snapshot frequency exceeded. Reusing snapshot {}", snapshot); } - return snapshot; } /** From 4f811330d01bc8f9386618e719d16ae214e67878 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 27 Oct 2023 17:43:18 +0200 Subject: [PATCH 46/60] feature: Add support for loading xml files for Siri SX and EX updates This feature make it much simpler to test realtime updates. --- docs/sandbox/SiriUpdater.md | 35 +++++- .../updater/EstimatedTimetableSource.java | 7 +- .../updater/SiriETHttpTripUpdateSource.java | 45 +++++--- .../ext/siri/updater/SiriETUpdater.java | 7 +- .../ext/siri/updater/SiriFileLoader.java | 101 ++++++++++++++++++ .../ext/siri/updater/SiriHttpLoader.java | 17 ++- .../ext/siri/updater/SiriLoader.java | 20 ++++ .../ext/siri/updater/SiriSXUpdater.java | 20 ++-- .../updaters/SiriETUpdaterConfig.java | 7 +- .../updaters/SiriSXUpdaterConfig.java | 21 +++- 10 files changed, 241 insertions(+), 39 deletions(-) create mode 100644 src/ext/java/org/opentripplanner/ext/siri/updater/SiriFileLoader.java create mode 100644 src/ext/java/org/opentripplanner/ext/siri/updater/SiriLoader.java diff --git a/docs/sandbox/SiriUpdater.md b/docs/sandbox/SiriUpdater.md index 5b257618fdd..1c90a030a4d 100644 --- a/docs/sandbox/SiriUpdater.md +++ b/docs/sandbox/SiriUpdater.md @@ -37,12 +37,27 @@ To enable the SIRI updater you need to add it to the updaters section of the `ro | previewInterval | `duration` | TODO | *Optional* | | 2.0 | | requestorRef | `string` | The requester reference. | *Optional* | | 2.0 | | timeout | `duration` | The HTTP timeout to download the updates. | *Optional* | `"PT15S"` | 2.0 | -| url | `string` | The URL to send the HTTP requests to. | *Required* | | 2.0 | +| [url](#u__8__url) | `string` | The URL to send the HTTP requests to. | *Required* | | 2.0 | | [headers](#u__8__headers) | `map of string` | HTTP headers to add to the request. Any header key, value can be inserted. | *Optional* | | 2.3 | ##### Parameter details +

url

+ +**Since version:** `2.0` ∙ **Type:** `string` ∙ **Cardinality:** `Required` +**Path:** /updaters/[8] + +The URL to send the HTTP requests to. + +Use the file protocol to set a directory for reading updates from a directory. The file +loader will look for xml files: '*.xml' in the configured directory. The files are +renamed by the loader when possessed: + +    _a.xml_   ➞   _a.xml.inProgress_   ➞   _a.xml.ok_   or   _a.xml.failed_ + + +

headers

**Since version:** `2.3` ∙ **Type:** `map of string` ∙ **Cardinality:** `Optional` @@ -87,7 +102,7 @@ HTTP headers to add to the request. Any header key, value can be inserted. | frequency | `duration` | How often the updates should be retrieved. | *Optional* | `"PT1M"` | 2.0 | | requestorRef | `string` | The requester reference. | *Optional* | | 2.0 | | timeout | `duration` | The HTTP timeout to download the updates. | *Optional* | `"PT15S"` | 2.0 | -| url | `string` | The URL to send the HTTP requests to. | *Required* | | 2.0 | +| [url](#u__9__url) | `string` | The URL to send the HTTP requests to. Support http/https and file protocol. | *Required* | | 2.0 | | [headers](#u__9__headers) | `map of string` | HTTP headers to add to the request. Any header key, value can be inserted. | *Optional* | | 2.3 | @@ -104,6 +119,22 @@ Normally the planned departure time is used, so setting this to 10s will cause t SX-message to be included in trip-results 10 seconds before the the planned departure time. +

url

+ +**Since version:** `2.0` ∙ **Type:** `string` ∙ **Cardinality:** `Required` +**Path:** /updaters/[9] + +The URL to send the HTTP requests to. Support http/https and file protocol. + + +Use the file protocol to set a directory for reading updates from a directory. The file +loader will look for xml files: '*.xml' in the configured directory. The files are +renamed by the loader when possessed: + +    _a.xml_   ➞   _a.xml.inProgress_   ➞   _a.xml.ok_   or   _a.xml.failed_ + + +

headers

**Since version:** `2.3` ∙ **Type:** `map of string` ∙ **Cardinality:** `Optional` diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java b/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java index 29026ae5c05..ac98839fb42 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java @@ -1,15 +1,16 @@ package org.opentripplanner.ext.siri.updater; +import java.util.Optional; import uk.org.siri.siri20.Siri; public interface EstimatedTimetableSource { /** * Wait for one message to arrive, and decode it into a List of TripUpdates. Blocking call. * - * @return a Siri-object potentially containing updates for several different trips, or null if an - * exception occurred while processing the message + * @return a Siri-object potentially containing updates for several trips, or empty if an + * exception occurred while processing the message. */ - Siri getUpdates(); + Optional getUpdates(); /** * @return true iff the last list with updates represent all updates that are active right now, diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETHttpTripUpdateSource.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETHttpTripUpdateSource.java index 1d448284700..3f1de760701 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETHttpTripUpdateSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETHttpTripUpdateSource.java @@ -2,6 +2,7 @@ import java.time.Duration; import java.time.ZonedDateTime; +import java.util.Optional; import java.util.UUID; import javax.annotation.Nullable; import org.opentripplanner.framework.io.OtpHttpClientException; @@ -21,7 +22,7 @@ public class SiriETHttpTripUpdateSource implements EstimatedTimetableSource { private final String url; - private final SiriHttpLoader siriHttpLoader; + private final SiriLoader siriLoader; private final String requestorRef; /** @@ -35,31 +36,29 @@ public SiriETHttpTripUpdateSource(Parameters parameters) { this.feedId = parameters.feedId(); this.url = parameters.url(); - requestorRef = + this.requestorRef = parameters.requestorRef() == null || parameters.requestorRef().isEmpty() ? "otp-" + UUID.randomUUID() : parameters.requestorRef(); - siriHttpLoader = - new SiriHttpLoader( - url, - parameters.timeout(), - parameters.httpRequestHeaders(), - parameters.previewInterval() - ); + this.siriLoader = createLoader(url, parameters); } @Override - public Siri getUpdates() { + public Optional getUpdates() { long t1 = System.currentTimeMillis(); try { - Siri siri = siriHttpLoader.fetchETFeed(requestorRef); + var siri = siriLoader.fetchETFeed(requestorRef); + if (siri.isEmpty()) { + return Optional.empty(); + } - if (siri.getServiceDelivery().getResponseTimestamp().isBefore(lastTimestamp)) { + var serviceDelivery = siri.get().getServiceDelivery(); + if (serviceDelivery.getResponseTimestamp().isBefore(lastTimestamp)) { LOG.info("Newer data has already been processed"); - return null; + return Optional.empty(); } - lastTimestamp = siri.getServiceDelivery().getResponseTimestamp(); + lastTimestamp = serviceDelivery.getResponseTimestamp(); //All subsequent requests will return changes since last request fullDataset = false; @@ -71,7 +70,7 @@ public Siri getUpdates() { LOG.info("Failed after {} ms", (System.currentTimeMillis() - t1)); LOG.warn("Failed to parse SIRI-ET feed from {}", url, e); } - return null; + return Optional.empty(); } @Override @@ -88,6 +87,22 @@ public String toString() { return "SiriETHttpTripUpdateSource(" + url + ")"; } + private static SiriLoader createLoader(String url, Parameters parameters) { + // Load realtime updates from a file. + if (SiriFileLoader.matchesUrl(url)) { + return new SiriFileLoader(url); + } + // Fallback to default loader + else { + return new SiriHttpLoader( + url, + parameters.timeout(), + parameters.httpRequestHeaders(), + parameters.previewInterval() + ); + } + } + public interface Parameters { String url(); diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java index f0d8a7a11f5..b4cf5f143e3 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java @@ -17,7 +17,6 @@ import org.slf4j.LoggerFactory; import uk.org.siri.siri20.EstimatedTimetableDeliveryStructure; import uk.org.siri.siri20.ServiceDelivery; -import uk.org.siri.siri20.Siri; /** * Update OTP stop timetables from some a Siri-ET HTTP sources. @@ -90,10 +89,10 @@ public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { public void runPolling() { boolean moreData = false; do { - Siri updates = updateSource.getUpdates(); - if (updates != null) { + var updates = updateSource.getUpdates(); + if (updates.isPresent()) { boolean fullDataset = updateSource.getFullDatasetValueOfLastUpdates(); - ServiceDelivery serviceDelivery = updates.getServiceDelivery(); + ServiceDelivery serviceDelivery = updates.get().getServiceDelivery(); moreData = Boolean.TRUE.equals(serviceDelivery.isMoreData()); // Mark this updater as primed after last page of updates. Copy moreData into a final // primitive, because the object moreData persists across iterations. diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriFileLoader.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriFileLoader.java new file mode 100644 index 00000000000..e329727b994 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriFileLoader.java @@ -0,0 +1,101 @@ +package org.opentripplanner.ext.siri.updater; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.org.siri.siri20.Siri; + +/** + * Load real-time updates from SIRI-SX and SIRI-ET feeds from a local directory. The + * files are renamed during the processing like this: + *
+ * update.xml  ➞  update.xml.inProgress  ➞  ( update.xml.ok | update.xml.failed )
+ * 
+ * The renaming of the file guarantee that the file is not processed twice. A {@code .ok} file + * indicate that the file is processed and the content parsed ok. The status of the update + * might be different - se logs for update errors. + *

+ * The file updater will pick up any file matching {@code *.xml} in the configured directory. + */ +public class SiriFileLoader implements SiriLoader { + + private static final Logger LOG = LoggerFactory.getLogger(SiriFileLoader.class); + public static final String SUFFIX_IN_PROGRESS = ".inProgress"; + public static final String SUFFIX_OK = ".ok"; + public static final String SUFFIX_FAILED = ".failed"; + private final File directory; + + public SiriFileLoader(String url) { + try { + this.directory = new File(new URL(url).toURI()); + + if (!directory.exists() || !directory.isDirectory()) { + throw new IllegalArgumentException("Could not find directory: " + url); + } + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + public static boolean matchesUrl(String url) { + return url.startsWith("file:"); + } + + /** + * Send a SIRI-SX service request and unmarshal the response as JAXB. + */ + @Override + public Optional fetchSXFeed(String requestorRef) { + return fetchFeed(); + } + + /** + * Send a SIRI-ET service request and unmarshal the response as JAXB. + */ + @Override + public Optional fetchETFeed(String requestorRef) { + return fetchFeed(); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private Optional fetchFeed() { + File[] files = directory.listFiles(); + if (files == null) { + return Optional.empty(); + } + + for (File file : files) { + if (!matchFilename(file)) { + continue; + } + LOG.info("Process realtime input file: " + file.getAbsolutePath()); + var inProgressFile = newFile(file, SUFFIX_IN_PROGRESS); + try { + file.renameTo(inProgressFile); + Siri siri = null; + + try (InputStream is = new FileInputStream(inProgressFile)) { + siri = SiriHelper.unmarshal(is); + inProgressFile.renameTo(newFile(file, SUFFIX_OK)); + return Optional.of(siri); + } + } catch (Exception ex) { + inProgressFile.renameTo(newFile(file, SUFFIX_FAILED)); + throw new RuntimeException(ex.getMessage(), ex); + } + } + return Optional.empty(); + } + + private static boolean matchFilename(File file) { + return file.getName().endsWith(".xml"); + } + + private static File newFile(File originalFile, String suffix) { + return new File(originalFile.getParentFile(), originalFile.getName() + suffix); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriHttpLoader.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriHttpLoader.java index 41e1f1c6e9b..7a094dc0863 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriHttpLoader.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriHttpLoader.java @@ -2,6 +2,7 @@ import jakarta.xml.bind.JAXBException; import java.time.Duration; +import java.util.Optional; import org.opentripplanner.framework.io.OtpHttpClient; import org.opentripplanner.updater.spi.HttpHeaders; import org.slf4j.Logger; @@ -11,7 +12,7 @@ /** * Load real-time updates from SIRI-SX and SIRI-ET feeds over HTTP. */ -public class SiriHttpLoader { +public class SiriHttpLoader implements SiriLoader { private static final Logger LOG = LoggerFactory.getLogger(SiriHttpLoader.class); private final HttpHeaders requestHeaders; @@ -40,7 +41,8 @@ public SiriHttpLoader( /** * Send a SIRI-SX service request and unmarshal the response as JAXB. */ - public Siri fetchSXFeed(String requestorRef) throws JAXBException { + @Override + public Optional fetchSXFeed(String requestorRef) throws JAXBException { RequestTimer requestTimer = new RequestTimer("SX"); requestTimer.init(); String sxServiceRequest = SiriHelper.createSXServiceRequestAsXml(requestorRef); @@ -51,7 +53,8 @@ public Siri fetchSXFeed(String requestorRef) throws JAXBException { /** * Send a SIRI-ET service request and unmarshal the response as JAXB. */ - public Siri fetchETFeed(String requestorRef) throws JAXBException { + @Override + public Optional fetchETFeed(String requestorRef) throws JAXBException { RequestTimer requestTimer = new RequestTimer("ET"); requestTimer.init(); String etServiceRequest = SiriHelper.createETServiceRequestAsXml(requestorRef, previewInterval); @@ -59,7 +62,11 @@ public Siri fetchETFeed(String requestorRef) throws JAXBException { return fetchFeed(etServiceRequest, requestTimer, requestorRef); } - private Siri fetchFeed(String serviceRequest, RequestTimer requestTimer, String requestorRef) { + private Optional fetchFeed( + String serviceRequest, + RequestTimer requestTimer, + String requestorRef + ) { try { return otpHttpClient.postXmlAndMap( url, @@ -70,7 +77,7 @@ private Siri fetchFeed(String serviceRequest, RequestTimer requestTimer, String requestTimer.responseFetched(); Siri siri = SiriHelper.unmarshal(is); requestTimer.responseUnmarshalled(); - return siri; + return Optional.of(siri); } ); } finally { diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriLoader.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriLoader.java new file mode 100644 index 00000000000..785bcbaa826 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriLoader.java @@ -0,0 +1,20 @@ +package org.opentripplanner.ext.siri.updater; + +import jakarta.xml.bind.JAXBException; +import java.util.Optional; +import uk.org.siri.siri20.Siri; + +/** + * The Siri loader is used to fetch updates from a source like http(s) or directory. + */ +public interface SiriLoader { + /** + * Request a new Siri SX update. + */ + Optional fetchSXFeed(String requestorRef) throws JAXBException; + + /** + * Request a new Siri ET update. + */ + Optional fetchETFeed(String requestorRef) throws JAXBException; +} diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java index 78ea5a90c34..014d5b24061 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java @@ -2,6 +2,7 @@ import java.time.Duration; import java.time.ZonedDateTime; +import java.util.Optional; import java.util.UUID; import org.opentripplanner.ext.siri.SiriAlertsUpdateHandler; import org.opentripplanner.ext.siri.SiriFuzzyTripMatcher; @@ -103,9 +104,9 @@ protected void runPolling() throws InterruptedException { private void updateSiri() { boolean moreData = false; do { - Siri updates = getUpdates(); - if (updates != null) { - ServiceDelivery serviceDelivery = updates.getServiceDelivery(); + var updates = getUpdates(); + if (updates.isPresent()) { + ServiceDelivery serviceDelivery = updates.get().getServiceDelivery(); moreData = Boolean.TRUE.equals(serviceDelivery.isMoreData()); // Mark this updater as primed after last page of updates. Copy moreData into a final // primitive, because the object moreData persists across iterations. @@ -122,12 +123,15 @@ private void updateSiri() { } while (moreData); } - private Siri getUpdates() { + private Optional getUpdates() { long t1 = System.currentTimeMillis(); try { - Siri siri = siriHttpLoader.fetchSXFeed(requestorRef); + Optional siri = siriHttpLoader.fetchSXFeed(requestorRef); + if (siri.isEmpty()) { + return Optional.empty(); + } - ServiceDelivery serviceDelivery = siri.getServiceDelivery(); + ServiceDelivery serviceDelivery = siri.get().getServiceDelivery(); if (serviceDelivery == null) { throw new RuntimeException("Failed to get serviceDelivery " + url); } @@ -135,7 +139,7 @@ private Siri getUpdates() { ZonedDateTime responseTimestamp = serviceDelivery.getResponseTimestamp(); if (responseTimestamp.isBefore(lastTimestamp)) { LOG.info("Ignoring feed with an old timestamp."); - return null; + return Optional.empty(); } lastTimestamp = responseTimestamp; @@ -154,7 +158,7 @@ private Siri getUpdates() { (System.currentTimeMillis() - t1) ); } - return null; + return Optional.empty(); } /** diff --git a/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/SiriETUpdaterConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/SiriETUpdaterConfig.java index 0fa73bb3ff6..5d04dd9c2a0 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/SiriETUpdaterConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/SiriETUpdaterConfig.java @@ -20,7 +20,12 @@ public static SiriETUpdaterParameters create(String configRef, NodeAdapter c) { "Whether catching up with the updates should block the readiness check from returning a 'ready' result." ) .asBoolean(false), - c.of("url").since(V2_0).summary("The URL to send the HTTP requests to.").asString(), + c + .of("url") + .since(V2_0) + .summary("The URL to send the HTTP requests to.") + .description(SiriSXUpdaterConfig.URL_DESCRIPTION) + .asString(), c .of("frequency") .since(V2_0) diff --git a/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/SiriSXUpdaterConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/SiriSXUpdaterConfig.java index 154f5e44058..6db667b045f 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/SiriSXUpdaterConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/SiriSXUpdaterConfig.java @@ -9,11 +9,30 @@ public class SiriSXUpdaterConfig { + static final String URL_DESCRIPTION = + """ + Use the file protocol to set a directory for reading updates from a directory. The file + loader will look for xml files: '*.xml' in the configured directory. The files are + renamed by the loader when possessed: + +     _a.xml_   ➞   _a.xml.inProgress_   ➞   _a.xml.ok_   or   _a.xml.failed_ + + """; + public static SiriSXUpdaterParameters create(String configRef, NodeAdapter c) { return new SiriSXUpdaterParameters( configRef, c.of("feedId").since(V2_0).summary("The ID of the feed to apply the updates to.").asString(), - c.of("url").since(V2_0).summary("The URL to send the HTTP requests to.").asString(), + c + .of("url") + .since(V2_0) + .summary( + """ + The URL to send the HTTP requests to. Support http/https and file protocol. + """ + ) + .description(URL_DESCRIPTION) + .asString(), c.of("requestorRef").since(V2_0).summary("The requester reference.").asString(null), c .of("frequency") From afc010d3832a8ed4b0fc2333a8403087863e0025 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 27 Oct 2023 18:37:13 +0200 Subject: [PATCH 47/60] doc: Apply doc update from code review --- .../services/OptimizePathDomainService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java index 89669569f2c..91b08212d2f 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java @@ -105,7 +105,7 @@ public OptimizePathDomainService( public Set> findBestTransitPath(RaptorPath originalPath) { List> transitLegs = originalPath.transitLegs().collect(Collectors.toList()); - // Find all possible transfers between each pair of transit legs, and sort on arrival time + // Find all possible transfers between each pair of transit legs, and sort on stop position var possibleTransfers = sortTransfersOnArrivalStopPosInDecOrder( transferGenerator.findAllPossibleTransfers(transitLegs) ); From ef106b305161573bff43fa7e4279f266c7b592d6 Mon Sep 17 00:00:00 2001 From: Daniel Heppner Date: Fri, 27 Oct 2023 15:17:10 -0700 Subject: [PATCH 48/60] fix bug with Kitsap Fast Ferry fares --- .../org/opentripplanner/ext/fares/impl/OrcaFareService.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java index 83929584bd5..3f9fe293f3d 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java +++ b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java @@ -168,10 +168,7 @@ static RideType getRideType(Leg leg) { case WASHINGTON_STATE_FERRIES_AGENCY_ID -> RideType.WASHINGTON_STATE_FERRIES; case T_LINK_AGENCY_ID -> RideType.SOUND_TRANSIT_T_LINK; case KITSAP_TRANSIT_AGENCY_ID -> { - if ( - route.getId().getId().equalsIgnoreCase("Kitsap Fast Ferry") && - route.getGtfsType() == ROUTE_TYPE_FERRY - ) { + if (route.getGtfsType() == ROUTE_TYPE_FERRY) { // Additional trip id checks are required to distinguish Kitsap fast ferry routes. if (tripId.contains("east")) { yield RideType.KITSAP_TRANSIT_FAST_FERRY_EASTBOUND; From 142103e48971caf62cf3bf42186041edddf6592a Mon Sep 17 00:00:00 2001 From: Daniel Heppner Date: Fri, 27 Oct 2023 15:21:51 -0700 Subject: [PATCH 49/60] update test to better match GTFS --- .../ext/fares/impl/OrcaFareServiceTest.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java b/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java index f437677e4be..a08b50b083d 100644 --- a/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java @@ -296,11 +296,11 @@ void calculateFareThatStartsWithACashFare() { } /** - * Single trip with Kitsap transit fast ferry east to confirm correct non Orca fares are applied. + * Single trip with Kitsap transit fast ferry to confirm correct non Orca fares are applied. */ @Test - void calculateFareForKitsapFastFerryEastAgency() { - List rides = List.of(getLeg(KITSAP_TRANSIT_AGENCY_ID, 0, 4, "Kitsap Fast Ferry", "east")); + void calculateFareForKitsapFastFerry() { + List rides = List.of(getLeg(KITSAP_TRANSIT_AGENCY_ID, 0, 4, "404", "east")); calculateFare(rides, regular, TWO_DOLLARS); calculateFare(rides, FareType.senior, TWO_DOLLARS); calculateFare(rides, FareType.youth, Money.ZERO_USD); @@ -308,6 +308,15 @@ void calculateFareForKitsapFastFerryEastAgency() { calculateFare(rides, FareType.electronicRegular, TWO_DOLLARS); calculateFare(rides, FareType.electronicSenior, ONE_DOLLAR); calculateFare(rides, FareType.electronicYouth, Money.ZERO_USD); + + rides = List.of(getLeg(KITSAP_TRANSIT_AGENCY_ID, 0, 4, "404", "west")); + calculateFare(rides, regular, usDollars(10f)); + calculateFare(rides, FareType.senior, usDollars(10f)); + calculateFare(rides, FareType.youth, Money.ZERO_USD); + calculateFare(rides, FareType.electronicSpecial, usDollars(5f)); + calculateFare(rides, FareType.electronicRegular, usDollars(10f)); + calculateFare(rides, FareType.electronicSenior, usDollars(5f)); + calculateFare(rides, FareType.electronicYouth, Money.ZERO_USD); } /** From 2126bc0a69806539696df5fdc42f3ad2d924c393 Mon Sep 17 00:00:00 2001 From: OTP Changelog Bot Date: Mon, 30 Oct 2023 07:45:27 +0000 Subject: [PATCH 50/60] Add changelog entry for #5455 [ci skip] --- docs/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Changelog.md b/docs/Changelog.md index 802013c3921..a9718089b42 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -29,6 +29,7 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Make vehicleRentalStation query optionally accept id without feed [#5411](https://github.com/opentripplanner/OpenTripPlanner/pull/5411) - Add stricter validation for flex areas [#5457](https://github.com/opentripplanner/OpenTripPlanner/pull/5457) - Remove HTTPS handling and its documentation [#5439](https://github.com/opentripplanner/OpenTripPlanner/pull/5439) +- Add support for DSJ in transit leg reference [#5455](https://github.com/opentripplanner/OpenTripPlanner/pull/5455) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.4.0 (2023-09-13) From c8a8cd3d04920107c8f7df5ac0978e05f780ef74 Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 31 Oct 2023 12:18:58 +0100 Subject: [PATCH 51/60] Apply suggestions from code review Co-authored-by: Leonard Ehrenfried --- .../org/opentripplanner/ext/siri/updater/SiriFileLoader.java | 4 ++-- .../org/opentripplanner/framework/time/CountdownTimer.java | 2 +- .../config/routerconfig/updaters/SiriSXUpdaterConfig.java | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriFileLoader.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriFileLoader.java index e329727b994..306deeca484 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriFileLoader.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriFileLoader.java @@ -15,9 +15,9 @@ *

  * update.xml  ➞  update.xml.inProgress  ➞  ( update.xml.ok | update.xml.failed )
  * 
- * The renaming of the file guarantee that the file is not processed twice. A {@code .ok} file + * The renaming of the file guarantees that the file is not processed twice. A {@code .ok} file * indicate that the file is processed and the content parsed ok. The status of the update - * might be different - se logs for update errors. + * might be different - see logs for update errors. *

* The file updater will pick up any file matching {@code *.xml} in the configured directory. */ diff --git a/src/main/java/org/opentripplanner/framework/time/CountdownTimer.java b/src/main/java/org/opentripplanner/framework/time/CountdownTimer.java index 3ab66782f10..5d6d89147a8 100644 --- a/src/main/java/org/opentripplanner/framework/time/CountdownTimer.java +++ b/src/main/java/org/opentripplanner/framework/time/CountdownTimer.java @@ -48,7 +48,7 @@ public boolean timeIsUp() { } /** - * Return {@code true} if time is up, and then imitatively start the timer again. + * Return {@code true} if time is up, and then immediately start the timer again. */ public boolean nextLap() { long time = now(); diff --git a/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/SiriSXUpdaterConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/SiriSXUpdaterConfig.java index 6db667b045f..dd0be8bc449 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/SiriSXUpdaterConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/SiriSXUpdaterConfig.java @@ -13,7 +13,7 @@ public class SiriSXUpdaterConfig { """ Use the file protocol to set a directory for reading updates from a directory. The file loader will look for xml files: '*.xml' in the configured directory. The files are - renamed by the loader when possessed: + renamed by the loader when processed:     _a.xml_   ➞   _a.xml.inProgress_   ➞   _a.xml.ok_   or   _a.xml.failed_ @@ -28,7 +28,7 @@ public static SiriSXUpdaterParameters create(String configRef, NodeAdapter c) { .since(V2_0) .summary( """ - The URL to send the HTTP requests to. Support http/https and file protocol. + The URL to send the HTTP requests to. Supports http/https and file protocol. """ ) .description(URL_DESCRIPTION) From abe901305a408c000e2f9aee3202784835a4c9d1 Mon Sep 17 00:00:00 2001 From: eibakke Date: Tue, 31 Oct 2023 13:05:47 +0100 Subject: [PATCH 52/60] Inverts dependency between model and domain logic with the PageCursorFactoryParameters. --- .../plan/pagecursor/PageCursorFactory.java | 20 ++-- .../PageCursorFactoryParameters.java | 13 +++ .../algorithm/filterchain/ListSection.java | 2 +- .../NumItinerariesFilterResults.java | 23 ++++- .../pagecursor/PageCursorFactoryTest.java | 94 ++++++------------- .../mapping/RoutingResponseMapperTest.java | 2 +- 6 files changed, 75 insertions(+), 79 deletions(-) create mode 100644 src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryParameters.java diff --git a/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactory.java b/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactory.java index 4a7bd71aa44..bc336ee34d6 100644 --- a/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactory.java +++ b/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactory.java @@ -9,7 +9,6 @@ import javax.annotation.Nullable; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.model.plan.SortOrder; -import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.NumItinerariesFilterResults; public class PageCursorFactory { @@ -19,7 +18,7 @@ public class PageCursorFactory { private SearchTime current = null; private Duration currentSearchWindow = null; private boolean wholeSwUsed = true; - private NumItinerariesFilterResults numItinerariesFilterResults = null; + private PageCursorFactoryParameters pageCursorFactoryParams = null; private PageCursor nextCursor = null; private PageCursor prevCursor = null; @@ -53,13 +52,13 @@ public PageCursorFactory withOriginalSearch( * removed in the next and previous pages. This means we will use information from when we cropped * the list of itineraries to create the new search encoded in the page cursors. * - * @param numItinerariesFilterResults the result from the {@code NumItinerariesFilter} + * @param pageCursorFactoryParams the result from the {@code NumItinerariesFilter} */ public PageCursorFactory withRemovedItineraries( - NumItinerariesFilterResults numItinerariesFilterResults + PageCursorFactoryParameters pageCursorFactoryParams ) { this.wholeSwUsed = false; - this.numItinerariesFilterResults = numItinerariesFilterResults; + this.pageCursorFactoryParams = pageCursorFactoryParams; return this; } @@ -85,7 +84,7 @@ public String toString() { .addDuration("currentSearchWindow", currentSearchWindow) .addDuration("newSearchWindow", newSearchWindow) .addBoolIfTrue("searchWindowCropped", !wholeSwUsed) - .addObj("numItinerariesFilterResults", numItinerariesFilterResults) + .addObj("pageCursorFactoryParams", pageCursorFactoryParams) .addObj("nextCursor", nextCursor) .addObj("prevCursor", prevCursor) .toString(); @@ -118,10 +117,9 @@ private void createPageCursors() { } else { // If the whole search window was not used (i.e. if there were removed itineraries) if (currentPageType == NEXT_PAGE) { prev.edt = edtBeforeNewSw(); - next.edt = numItinerariesFilterResults.earliestRemovedDeparture; + next.edt = pageCursorFactoryParams.earliestRemovedDeparture(); if (sortOrder.isSortedByArrivalTimeAscending()) { - prev.lat = - numItinerariesFilterResults.earliestKeptArrival.truncatedTo(ChronoUnit.MINUTES); + prev.lat = pageCursorFactoryParams.earliestKeptArrival().truncatedTo(ChronoUnit.MINUTES); } else { prev.lat = current.lat; } @@ -130,9 +128,9 @@ private void createPageCursors() { // search-window from the last time included in the search window we need to include one extra // minute at the end. prev.edt = - numItinerariesFilterResults.latestRemovedDeparture.minus(newSearchWindow).plusSeconds(60); + pageCursorFactoryParams.latestRemovedDeparture().minus(newSearchWindow).plusSeconds(60); next.edt = edtAfterUsedSw(); - prev.lat = numItinerariesFilterResults.latestRemovedArrival; + prev.lat = pageCursorFactoryParams.latestRemovedArrival(); } } prevCursor = new PageCursor(PREVIOUS_PAGE, sortOrder, prev.edt, prev.lat, newSearchWindow); diff --git a/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryParameters.java b/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryParameters.java new file mode 100644 index 00000000000..354bab51b63 --- /dev/null +++ b/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryParameters.java @@ -0,0 +1,13 @@ +package org.opentripplanner.model.plan.pagecursor; + +import java.time.Instant; + +public interface PageCursorFactoryParameters { + Instant earliestRemovedDeparture(); + + Instant earliestKeptArrival(); + + Instant latestRemovedDeparture(); + + Instant latestRemovedArrival(); +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ListSection.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ListSection.java index 650b4edfe37..a46756775df 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ListSection.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ListSection.java @@ -2,7 +2,7 @@ /** * This enum is used to signal which part of a list an operation apply to. You may remove elements - * from the HEAD or TAIL of the list. It may refere to on or more elements. + * from the HEAD or TAIL of the list. It may refer to one or more elements. */ public enum ListSection { /** The beginning of the list. */ diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterResults.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterResults.java index ed03dd3e64c..6d458bdb4c9 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterResults.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterResults.java @@ -3,9 +3,10 @@ import java.time.Instant; import java.util.List; import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.model.plan.pagecursor.PageCursorFactoryParameters; import org.opentripplanner.routing.algorithm.filterchain.ListSection; -public class NumItinerariesFilterResults { +public class NumItinerariesFilterResults implements PageCursorFactoryParameters { public final Instant earliestRemovedDeparture; public final Instant latestRemovedDeparture; @@ -92,4 +93,24 @@ public String toString() { '}' ); } + + @Override + public Instant earliestRemovedDeparture() { + return earliestRemovedDeparture; + } + + @Override + public Instant earliestKeptArrival() { + return earliestKeptArrival; + } + + @Override + public Instant latestRemovedDeparture() { + return latestRemovedDeparture; + } + + @Override + public Instant latestRemovedArrival() { + return latestRemovedArrival; + } } diff --git a/src/test/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryTest.java b/src/test/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryTest.java index 4a3e45622f3..29fcbcd2ce3 100644 --- a/src/test/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryTest.java +++ b/src/test/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryTest.java @@ -3,18 +3,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.opentripplanner.model.plan.SortOrder.STREET_AND_ARRIVAL_TIME; import static org.opentripplanner.model.plan.SortOrder.STREET_AND_DEPARTURE_TIME; -import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary; import static org.opentripplanner.model.plan.pagecursor.PageType.NEXT_PAGE; import static org.opentripplanner.model.plan.pagecursor.PageType.PREVIOUS_PAGE; import java.time.Duration; import java.time.Instant; -import java.util.List; import org.junit.jupiter.api.Test; import org.opentripplanner.framework.time.TimeUtils; import org.opentripplanner.model.plan.PlanTestConstants; -import org.opentripplanner.routing.algorithm.filterchain.ListSection; -import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.NumItinerariesFilterResults; @SuppressWarnings("ConstantConditions") class PageCursorFactoryTest implements PlanTestConstants { @@ -37,33 +33,23 @@ public void sortArrivalAscending() { .withOriginalSearch(null, T12_00, null, D1H); var nextPage = factory.nextPageCursor(); - assetPageCursor(nextPage, T13_00, null, D90M, NEXT_PAGE); + assertPageCursor(nextPage, T13_00, null, D90M, NEXT_PAGE); var prevPage = factory.previousPageCursor(); - assetPageCursor(prevPage, T10_30, null, D90M, PREVIOUS_PAGE); + assertPageCursor(prevPage, T10_30, null, D90M, PREVIOUS_PAGE); } @Test public void sortArrivalAscendingCropSearchWindow() { var factory = new PageCursorFactory(STREET_AND_ARRIVAL_TIME, D90M) .withOriginalSearch(NEXT_PAGE, T12_00, null, D1H) - .withRemovedItineraries( - new NumItinerariesFilterResults( - List.of( - newItinerary(A).bus(22, TimeUtils.time("12:00"), TimeUtils.time("13:00"), B).build() - ), - List.of( - newItinerary(A).bus(21, TimeUtils.time("12:30"), TimeUtils.time("13:30"), B).build() - ), - ListSection.TAIL - ) - ); + .withRemovedItineraries(new PageCursorFactoryParametersImpl(T12_00, T12_30, T12_30, T13_30)); var nextPage = factory.nextPageCursor(); - assetPageCursor(nextPage, T12_30, null, D90M, NEXT_PAGE); + assertPageCursor(nextPage, T12_30, null, D90M, NEXT_PAGE); var prevPage = factory.previousPageCursor(); - assetPageCursor(prevPage, T10_30, T13_00, D90M, PREVIOUS_PAGE); + assertPageCursor(prevPage, T10_30, T12_00, D90M, PREVIOUS_PAGE); } @Test @@ -72,33 +58,23 @@ public void sortArrivalAscendingPreviousPage() { .withOriginalSearch(PREVIOUS_PAGE, T12_00, null, D1H); var nextPage = factory.nextPageCursor(); - assetPageCursor(nextPage, T13_00, null, D90M, NEXT_PAGE); + assertPageCursor(nextPage, T13_00, null, D90M, NEXT_PAGE); var prevPage = factory.previousPageCursor(); - assetPageCursor(prevPage, T10_30, null, D90M, PREVIOUS_PAGE); + assertPageCursor(prevPage, T10_30, null, D90M, PREVIOUS_PAGE); } @Test public void sortArrivalAscendingCropSearchWindowPreviousPage() { var factory = new PageCursorFactory(STREET_AND_ARRIVAL_TIME, D90M) .withOriginalSearch(PREVIOUS_PAGE, T12_00, null, D1H) - .withRemovedItineraries( - new NumItinerariesFilterResults( - List.of( - newItinerary(A).bus(22, TimeUtils.time("12:00"), TimeUtils.time("13:00"), B).build() - ), - List.of( - newItinerary(A).bus(21, TimeUtils.time("12:30"), TimeUtils.time("13:30"), B).build() - ), - ListSection.HEAD - ) - ); + .withRemovedItineraries(new PageCursorFactoryParametersImpl(T12_00, T12_30, T12_30, T13_30)); var nextPage = factory.nextPageCursor(); - assetPageCursor(nextPage, T13_00, null, D90M, NEXT_PAGE); + assertPageCursor(nextPage, T13_00, null, D90M, NEXT_PAGE); var prevPage = factory.previousPageCursor(); - assetPageCursor(prevPage, T11_01, T13_30, D90M, PREVIOUS_PAGE); + assertPageCursor(prevPage, T11_01, T13_30, D90M, PREVIOUS_PAGE); } @Test @@ -107,33 +83,23 @@ public void sortDepartureDescending() { .withOriginalSearch(null, T12_00, T13_30, D1H); var nextPage = factory.nextPageCursor(); - assetPageCursor(nextPage, T13_00, null, D90M, NEXT_PAGE); + assertPageCursor(nextPage, T13_00, null, D90M, NEXT_PAGE); var prevPage = factory.previousPageCursor(); - assetPageCursor(prevPage, T10_30, T13_30, D90M, PREVIOUS_PAGE); + assertPageCursor(prevPage, T10_30, T13_30, D90M, PREVIOUS_PAGE); } @Test public void sortDepartureDescendingCropSearchWindow() { var factory = new PageCursorFactory(STREET_AND_DEPARTURE_TIME, D90M) .withOriginalSearch(PREVIOUS_PAGE, T12_00, T13_30, D1H) - .withRemovedItineraries( - new NumItinerariesFilterResults( - List.of( - newItinerary(A).bus(21, TimeUtils.time("12:30"), TimeUtils.time("13:30"), B).build() - ), - List.of( - newItinerary(A).bus(22, TimeUtils.time("12:30"), TimeUtils.time("13:00"), B).build() - ), - ListSection.TAIL - ) - ); + .withRemovedItineraries(new PageCursorFactoryParametersImpl(T12_30, T12_30, T12_30, T13_00)); var nextPage = factory.nextPageCursor(); - assetPageCursor(nextPage, T13_00, null, D90M, NEXT_PAGE); + assertPageCursor(nextPage, T13_00, null, D90M, NEXT_PAGE); var prevPage = factory.previousPageCursor(); - assetPageCursor(prevPage, T11_01, T13_00, D90M, PREVIOUS_PAGE); + assertPageCursor(prevPage, T11_01, T13_00, D90M, PREVIOUS_PAGE); } @Test @@ -142,40 +108,30 @@ public void sortDepartureDescendingNextPage() { .withOriginalSearch(NEXT_PAGE, T12_00, T13_30, D1H); var nextPage = factory.nextPageCursor(); - assetPageCursor(nextPage, T13_00, null, D90M, NEXT_PAGE); + assertPageCursor(nextPage, T13_00, null, D90M, NEXT_PAGE); var prevPage = factory.previousPageCursor(); - assetPageCursor(prevPage, T10_30, T13_30, D90M, PREVIOUS_PAGE); + assertPageCursor(prevPage, T10_30, T13_30, D90M, PREVIOUS_PAGE); } @Test public void sortDepartureDescendingCropSearchWindowNextPage() { var factory = new PageCursorFactory(STREET_AND_DEPARTURE_TIME, D90M) .withOriginalSearch(NEXT_PAGE, T12_00, T13_30, D1H) - .withRemovedItineraries( - new NumItinerariesFilterResults( - List.of( - newItinerary(A).bus(22, TimeUtils.time("12:00"), TimeUtils.time("13:00"), B).build() - ), - List.of( - newItinerary(A).bus(21, TimeUtils.time("12:30"), TimeUtils.time("13:30"), B).build() - ), - ListSection.HEAD - ) - ); + .withRemovedItineraries(new PageCursorFactoryParametersImpl(T12_00, T12_30, T12_30, T13_30)); var nextPage = factory.nextPageCursor(); - assetPageCursor(nextPage, T12_30, null, D90M, NEXT_PAGE); + assertPageCursor(nextPage, T12_30, null, D90M, NEXT_PAGE); var prevPage = factory.previousPageCursor(); - assetPageCursor(prevPage, T10_30, T13_30, D90M, PREVIOUS_PAGE); + assertPageCursor(prevPage, T10_30, T13_30, D90M, PREVIOUS_PAGE); } private static Instant time(String input) { return TIME_ZERO.plusSeconds(TimeUtils.time(input)); } - private void assetPageCursor( + private void assertPageCursor( PageCursor pageCursor, Instant expEdt, Instant expLat, @@ -187,4 +143,12 @@ private void assetPageCursor( assertEquals(expSearchWindow, pageCursor.searchWindow); assertEquals(expPageType, pageCursor.type); } + + private record PageCursorFactoryParametersImpl( + Instant earliestKeptArrival, + Instant earliestRemovedDeparture, + Instant latestRemovedDeparture, + Instant latestRemovedArrival + ) + implements PageCursorFactoryParameters {} } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapperTest.java b/src/test/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapperTest.java index 486a9ebf384..646eedd628a 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapperTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/mapping/RoutingResponseMapperTest.java @@ -113,7 +113,7 @@ public void testArriveByReversedRemovedInsidePreviousPage() { "currentSearchWindow: 1h, " + "newSearchWindow: 1h30m, " + "searchWindowCropped, " + - "numItinerariesFilterResults: " + + "pageCursorFactoryParams: " + new NumItinerariesFilterResults( List.of(KEPT_ITINERARY), List.of(REMOVED_ITINERARY), From 1bce429f1cf0af9e9a5bf5a9946d13df9c35d3cd Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Tue, 31 Oct 2023 13:26:10 +0100 Subject: [PATCH 53/60] doc: Generate doc. --- docs/sandbox/SiriUpdater.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sandbox/SiriUpdater.md b/docs/sandbox/SiriUpdater.md index 1c90a030a4d..f6c4c3f999f 100644 --- a/docs/sandbox/SiriUpdater.md +++ b/docs/sandbox/SiriUpdater.md @@ -52,7 +52,7 @@ The URL to send the HTTP requests to. Use the file protocol to set a directory for reading updates from a directory. The file loader will look for xml files: '*.xml' in the configured directory. The files are -renamed by the loader when possessed: +renamed by the loader when processed:     _a.xml_   ➞   _a.xml.inProgress_   ➞   _a.xml.ok_   or   _a.xml.failed_ @@ -102,7 +102,7 @@ HTTP headers to add to the request. Any header key, value can be inserted. | frequency | `duration` | How often the updates should be retrieved. | *Optional* | `"PT1M"` | 2.0 | | requestorRef | `string` | The requester reference. | *Optional* | | 2.0 | | timeout | `duration` | The HTTP timeout to download the updates. | *Optional* | `"PT15S"` | 2.0 | -| [url](#u__9__url) | `string` | The URL to send the HTTP requests to. Support http/https and file protocol. | *Required* | | 2.0 | +| [url](#u__9__url) | `string` | The URL to send the HTTP requests to. Supports http/https and file protocol. | *Required* | | 2.0 | | [headers](#u__9__headers) | `map of string` | HTTP headers to add to the request. Any header key, value can be inserted. | *Optional* | | 2.3 | @@ -124,12 +124,12 @@ time. **Since version:** `2.0` ∙ **Type:** `string` ∙ **Cardinality:** `Required` **Path:** /updaters/[9] -The URL to send the HTTP requests to. Support http/https and file protocol. +The URL to send the HTTP requests to. Supports http/https and file protocol. Use the file protocol to set a directory for reading updates from a directory. The file loader will look for xml files: '*.xml' in the configured directory. The files are -renamed by the loader when possessed: +renamed by the loader when processed:     _a.xml_   ➞   _a.xml.inProgress_   ➞   _a.xml.ok_   or   _a.xml.failed_ From 773f716cfbae8ffdbf095bd548978de2bd94be66 Mon Sep 17 00:00:00 2001 From: OTP Changelog Bot Date: Tue, 31 Oct 2023 16:28:13 +0000 Subject: [PATCH 54/60] Add changelog entry for #5443 [ci skip] --- docs/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Changelog.md b/docs/Changelog.md index a9718089b42..2ca99beb88b 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -30,6 +30,7 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Add stricter validation for flex areas [#5457](https://github.com/opentripplanner/OpenTripPlanner/pull/5457) - Remove HTTPS handling and its documentation [#5439](https://github.com/opentripplanner/OpenTripPlanner/pull/5439) - Add support for DSJ in transit leg reference [#5455](https://github.com/opentripplanner/OpenTripPlanner/pull/5455) +- Ignore negative travel-times in Raptor [#5443](https://github.com/opentripplanner/OpenTripPlanner/pull/5443) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.4.0 (2023-09-13) From cb845ce480e9718138d1b4f4cdaef21067a79fe2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:28:22 +0000 Subject: [PATCH 55/60] Update dependency org.entur.gbfs:gbfs-java-model to v3.0.11 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0a41eb308d1..f4c371f4578 100644 --- a/pom.xml +++ b/pom.xml @@ -714,7 +714,7 @@ org.entur.gbfs gbfs-java-model - 3.0.10 + 3.0.11 From 76439cc119b506be1fd980c986ef9f42e42ac83a Mon Sep 17 00:00:00 2001 From: OTP Changelog Bot Date: Tue, 31 Oct 2023 16:28:51 +0000 Subject: [PATCH 56/60] Add changelog entry for #5446 [ci skip] --- docs/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Changelog.md b/docs/Changelog.md index 2ca99beb88b..34d8dbd7367 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -31,6 +31,7 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Remove HTTPS handling and its documentation [#5439](https://github.com/opentripplanner/OpenTripPlanner/pull/5439) - Add support for DSJ in transit leg reference [#5455](https://github.com/opentripplanner/OpenTripPlanner/pull/5455) - Ignore negative travel-times in Raptor [#5443](https://github.com/opentripplanner/OpenTripPlanner/pull/5443) +- Fix sort order bug in optimized transfers [#5446](https://github.com/opentripplanner/OpenTripPlanner/pull/5446) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.4.0 (2023-09-13) From 851d4f64210d14fe2bb789ae43dacd4d66745c26 Mon Sep 17 00:00:00 2001 From: OTP Changelog Bot Date: Tue, 31 Oct 2023 16:29:26 +0000 Subject: [PATCH 57/60] Add changelog entry for #5460 [ci skip] --- docs/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Changelog.md b/docs/Changelog.md index 34d8dbd7367..238caddcf4b 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -32,6 +32,7 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Add support for DSJ in transit leg reference [#5455](https://github.com/opentripplanner/OpenTripPlanner/pull/5455) - Ignore negative travel-times in Raptor [#5443](https://github.com/opentripplanner/OpenTripPlanner/pull/5443) - Fix sort order bug in optimized transfers [#5446](https://github.com/opentripplanner/OpenTripPlanner/pull/5446) +- Siri file loader [#5460](https://github.com/opentripplanner/OpenTripPlanner/pull/5460) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.4.0 (2023-09-13) From c0d42a2f65fe283ac848b93385204aefead5e046 Mon Sep 17 00:00:00 2001 From: eibakke Date: Tue, 31 Oct 2023 13:05:47 +0100 Subject: [PATCH 58/60] Inverts dependency between model and domain logic with the PageCursorFactoryParameters. --- .../PageCursorFactoryParameters.java | 7 ++++ .../NumItinerariesFilterResults.java | 40 +++++++------------ 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryParameters.java b/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryParameters.java index 354bab51b63..f8e01ce5ce9 100644 --- a/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryParameters.java +++ b/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorFactoryParameters.java @@ -2,6 +2,13 @@ import java.time.Instant; +/** + * This class holds information needed to create the next/previous page cursors when there were + * itineraries removed due to cropping the list of itineraries using the numItineraries parameter. + *

+ * The Instant fields come from the sets of itineraries that were removed and the ones that were + * kept as a result of using the numItineraries parameter. + */ public interface PageCursorFactoryParameters { Instant earliestRemovedDeparture(); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterResults.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterResults.java index 6d458bdb4c9..36bbfff7d69 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterResults.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/NumItinerariesFilterResults.java @@ -2,6 +2,7 @@ import java.time.Instant; import java.util.List; +import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.model.plan.pagecursor.PageCursorFactoryParameters; import org.opentripplanner.routing.algorithm.filterchain.ListSection; @@ -49,7 +50,7 @@ public NumItinerariesFilterResults( .stream() .map(it -> it.endTime().toInstant()) .min(Instant::compareTo) - .orElse(null); + .orElseThrow(); Itinerary firstRemovedItinerary; if (cropSection == ListSection.HEAD) { @@ -68,30 +69,19 @@ public NumItinerariesFilterResults( @Override public String toString() { - return ( - "NumItinerariesFilterResults{" + - "earliestRemovedDeparture=" + - earliestRemovedDeparture + - ", latestRemovedDeparture=" + - latestRemovedDeparture + - ", earliestRemovedArrival=" + - earliestRemovedArrival + - ", latestRemovedArrival=" + - latestRemovedArrival + - ", earliestKeptArrival=" + - earliestKeptArrival + - ", firstRemovedArrivalTime=" + - firstRemovedArrivalTime + - ", firstRemovedGeneralizedCost=" + - firstRemovedGeneralizedCost + - ", firstRemovedNumOfTransfers=" + - firstRemovedNumOfTransfers + - ", firstRemovedDepartureTime=" + - firstRemovedDepartureTime + - ", cropSection=" + - cropSection + - '}' - ); + return ToStringBuilder + .of(NumItinerariesFilterResults.class) + .addDateTime("earliestRemovedDeparture", earliestRemovedDeparture) + .addDateTime("latestRemovedDeparture", latestRemovedDeparture) + .addDateTime("earliestRemovedArrival", earliestRemovedArrival) + .addDateTime("latestRemovedArrival", latestRemovedArrival) + .addDateTime("earliestKeptArrival", earliestKeptArrival) + .addDateTime("firstRemovedArrivalTime", firstRemovedArrivalTime) + .addNum("firstRemovedGeneralizedCost", firstRemovedGeneralizedCost) + .addNum("firstRemovedNumOfTransfers", firstRemovedNumOfTransfers) + .addDateTime("firstRemovedDepartureTime", firstRemovedDepartureTime) + .addEnum("cropSection", cropSection) + .toString(); } @Override From 55e510a89931abbf79a831feba578cdf35d1a1fe Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 1 Nov 2023 20:36:05 +0100 Subject: [PATCH 59/60] Update developer guide for mkdocs [ci skip] --- docs/Developers-Guide.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/Developers-Guide.md b/docs/Developers-Guide.md index 5f834580df8..e12d9cf6c1b 100644 --- a/docs/Developers-Guide.md +++ b/docs/Developers-Guide.md @@ -134,11 +134,11 @@ how OTP is used or configured should include changes to the documentation alongs modifications. The documentation files are in Markdown format and are in the `/docs` directory under the root of -the project. On every push to the master branch the documentation will be rebuilt and deployed as -static pages to our subdomain of [ReadTheDocs](http://opentripplanner.readthedocs.org/). MkDocs is a -Python program and should run on any major platform. See http://www.mkdocs.org/ for information on -how to install it and how to generate a live local preview of the documentation while you're working -on writing it. +the project. On every push to the `dev-2.x` branch the documentation will be rebuilt and deployed as +static pages to our subdomain of [Github Pages](https://github.com/opentripplanner/docs). +MkDocs is a Python program and should run on any major platform. +See [http://www.mkdocs.org/](http://www.mkdocs.org/) for information on how to install it and +how to generate a live local preview of the documentation while you're writing it. In short: @@ -149,11 +149,11 @@ $ mkdocs serve The OTP GTFS GraphQL API documentation is available online at -https://docs.opentripplanner.org/api/dev-2.x/graphql-gtfs/ +[https://docs.opentripplanner.org/api/dev-2.x/graphql-gtfs/](https://docs.opentripplanner.org/api/dev-2.x/graphql-gtfs/) You can also use the interactive GraphQL API client that is built into every instance at -http://localhost:8080/graphiql +[http://localhost:8080/graphiql](http://localhost:8080/graphiql) ### Debug layers From 56b19107af6a24d73470f24b90cdaa2e655094ab Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 2 Nov 2023 10:02:47 +0100 Subject: [PATCH 60/60] Fix doc formatting [ci skip] --- docs/Preparing-OSM.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/docs/Preparing-OSM.md b/docs/Preparing-OSM.md index 88434b37d5d..73909aff670 100644 --- a/docs/Preparing-OSM.md +++ b/docs/Preparing-OSM.md @@ -16,14 +16,11 @@ package. [Osmium-Tool](https://wiki.openstreetmap.org/wiki/Osmium) is a personal extremely fast but only straightforward to install on Linux and MacOS platforms. Below are some example crop commands for these different tools: -** -Osmosis:** `osmosis --rb input.osm.pbf --bounding-box left=4.34 right=5.84 bottom=43.10 top=43.97 --wb cropped.osm.pbf` +**Osmosis:** `osmosis --rb input.osm.pbf --bounding-box left=4.34 right=5.84 bottom=43.10 top=43.97 --wb cropped.osm.pbf` -** -OsmConvert:** `osmconvert input.osm.pbf -b=-77.255859375,38.77764022307335,-76.81365966796875,39.02345139405933 --complete-ways -o=cropped.osm.pbf` +**OsmConvert:** `osmconvert input.osm.pbf -b=-77.255859375,38.77764022307335,-76.81365966796875,39.02345139405933 --complete-ways -o=cropped.osm.pbf` -** -Osmium:** `osmium extract --strategy complete_ways --bbox 2.25,48.81,2.42,48.91 input.osm.pbf -o cropped.osm.pbf` +**Osmium:** `osmium extract --strategy complete_ways --bbox 2.25,48.81,2.42,48.91 input.osm.pbf -o cropped.osm.pbf` The latter two commands expect bounding boxes to be specified in the format `min_lon,min_lat,max_lon,max_lat`. We frequently find bounding boxes using the @@ -50,8 +47,6 @@ will also work on Windows as it's a multi-platform Java application. OSMFilter c format files so we rarely use it. Below are some example commands for retaining only OSM data useful for accessibility analysis. Here are some example commands: -** -Osmosis:** `osmosis --rb input.osm.pbf --tf reject-ways building=* --tf reject-ways waterway=* --tf reject-ways landuse=* --tf reject-ways natural=* --used-node --wb filtered.osm.pbf` +**Osmosis:** `osmosis --rb input.osm.pbf --tf reject-ways building=* --tf reject-ways waterway=* --tf reject-ways landuse=* --tf reject-ways natural=* --used-node --wb filtered.osm.pbf` -** -Osmium-Tool:** `osmium tags-filter input.osm.pbf w/highway wa/public_transport=platform wa/railway=platform w/park_ride=yes r/type=restriction r/type=route -o filtered.osm.pbf -f pbf,add_metadata=false` +**Osmium-Tool:** `osmium tags-filter input.osm.pbf w/highway wa/public_transport=platform wa/railway=platform w/park_ride=yes r/type=restriction r/type=route -o filtered.osm.pbf -f pbf,add_metadata=false`