diff --git a/client-next/src/components/MapView/MapView.tsx b/client-next/src/components/MapView/MapView.tsx index 96b4e820bae..36d98be79b6 100644 --- a/client-next/src/components/MapView/MapView.tsx +++ b/client-next/src/components/MapView/MapView.tsx @@ -75,7 +75,7 @@ export function MapView({ }} // it's unfortunate that you have to list these layers here. // maybe there is a way around it: https://github.com/visgl/react-map-gl/discussions/2343 - interactiveLayerIds={['regular-stop', 'area-stop', 'vertex', 'edge', 'link']} + interactiveLayerIds={['regular-stop', 'area-stop', 'group-stop', 'vertex', 'edge', 'link']} onClick={showFeaturePropPopup} // put lat/long in URL and pan to it on page reload hash={true} diff --git a/docs/Changelog.md b/docs/Changelog.md index 05731e6fbeb..f67b40f5a3b 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -88,6 +88,11 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Separate walk time from non-transit time [#5648](https://github.com/opentripplanner/OpenTripPlanner/pull/5648) - Remove "fare" [#5645](https://github.com/opentripplanner/OpenTripPlanner/pull/5645) - Refactor GroupStopBuilder addLocation method [#5651](https://github.com/opentripplanner/OpenTripPlanner/pull/5651) +- Remove `VehicleToStopHeuristics` [#5381](https://github.com/opentripplanner/OpenTripPlanner/pull/5381) +- Set defaults of the modes WALK, even if one and not the others are set [#5675](https://github.com/opentripplanner/OpenTripPlanner/pull/5675) +- Reduce flex default access/egress penalty [#5674](https://github.com/opentripplanner/OpenTripPlanner/pull/5674) +- Add scooter preferences [#5632](https://github.com/opentripplanner/OpenTripPlanner/pull/5632) +- Add GroupStop layer to new debug frontend [#5666](https://github.com/opentripplanner/OpenTripPlanner/pull/5666) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.4.0 (2023-09-13) diff --git a/docs/Configuration.md b/docs/Configuration.md index 858edf0f9b4..93ca1fa6c1e 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -250,7 +250,6 @@ Here is a list of all features which can be toggled on/off and their default val | `SandboxAPIParkAndRideApi` | Enable park-and-ride endpoint. | | ✓️ | | `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/docs/RouteRequest.md b/docs/RouteRequest.md index 199eda8a332..284618e713d 100644 --- a/docs/RouteRequest.md +++ b/docs/RouteRequest.md @@ -78,8 +78,8 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe |       [safety](#rd_bicycle_triangle_safety) | `double` | Relative importance of safety (range 0-1). | *Optional* | `0.0` | 2.0 | |       time | `double` | Relative importance of duration of travel (range 0-1). | *Optional* | `0.0` | 2.0 | |    walk | `object` | Preferences for walking a vehicle. | *Optional* | | 2.5 | -|       [hopCost](#rd_bicycle_walk_hopCost) | `integer` | The cost of hopping on or off a vehicle. | *Optional* | `0` | 2.0 | -|       [hopTime](#rd_bicycle_walk_hopTime) | `duration` | The time it takes the user to hop on or off a vehicle. | *Optional* | `"PT0S"` | 2.0 | +|       [mountDismountCost](#rd_bicycle_walk_mountDismountCost) | `integer` | The cost of hopping on or off a vehicle. | *Optional* | `0` | 2.0 | +|       [mountDismountTime](#rd_bicycle_walk_mountDismountTime) | `duration` | The time it takes the user to hop on or off a vehicle. | *Optional* | `"PT0S"` | 2.0 | |       reluctance | `double` | A multiplier for how bad walking with a vehicle is, compared to being in transit for equal lengths of time. | *Optional* | `5.0` | 2.1 | |       speed | `double` | The user's vehicle walking speed in meters/second. Defaults to approximately 3 MPH. | *Optional* | `1.33` | 2.1 | |       stairsReluctance | `double` | How bad is it to walk the vehicle up/down a flight of stairs compared to taking a detour. | *Optional* | `10.0` | 2.3 | @@ -125,6 +125,24 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe |       [costLimitFunction](#rd_if_transitGeneralizedCostLimit_costLimitFunction) | `cost-linear-function` | The base function used by the filter. | *Optional* | `"15m + 1.50 t"` | 2.2 | |       [intervalRelaxFactor](#rd_if_transitGeneralizedCostLimit_intervalRelaxFactor) | `double` | How much the filter should be relaxed for itineraries that do not overlap in time. | *Optional* | `0.4` | 2.2 | | [maxDirectStreetDurationForMode](#rd_maxDirectStreetDurationForMode) | `enum map of duration` | Limit direct route duration per street mode. | *Optional* | | 2.2 | +| scooter | `object` | Scooter preferences. | *Optional* | | 2.5 | +|    [optimization](#rd_scooter_optimization) | `enum` | The set of characteristics that the user wants to optimize for. | *Optional* | `"safe-streets"` | 2.0 | +|    reluctance | `double` | A multiplier for how bad scooter travel is, compared to being in transit for equal lengths of time. | *Optional* | `2.0` | 2.0 | +|    speed | `double` | Max scooter speed along streets, in meters per second | *Optional* | `5.0` | 2.0 | +|    rental | `object` | Vehicle rental options | *Optional* | | 2.3 | +|       allowKeepingAtDestination | `boolean` | If a vehicle should be allowed to be kept at the end of a station-based rental. | *Optional* | `false` | 2.2 | +|       dropOffCost | `integer` | Cost to drop-off a rented vehicle. | *Optional* | `30` | 2.0 | +|       dropOffTime | `duration` | Time to drop-off a rented vehicle. | *Optional* | `"PT30S"` | 2.0 | +|       keepingAtDestinationCost | `integer` | The cost of arriving at the destination with the rented vehicle, to discourage doing so. | *Optional* | `0` | 2.2 | +|       pickupCost | `integer` | Cost to rent a vehicle. | *Optional* | `120` | 2.0 | +|       pickupTime | `duration` | Time to rent a vehicle. | *Optional* | `"PT1M"` | 2.0 | +|       useAvailabilityInformation | `boolean` | Whether or not vehicle rental availability information will be used to plan vehicle rental trips. | *Optional* | `false` | 2.0 | +|       [allowedNetworks](#rd_scooter_rental_allowedNetworks) | `string[]` | The vehicle rental networks which may be used. If empty all networks may be used. | *Optional* | | 2.1 | +|       [bannedNetworks](#rd_scooter_rental_bannedNetworks) | `string[]` | The vehicle rental networks which may not be used. If empty, no networks are banned. | *Optional* | | 2.1 | +|    [triangle](#rd_scooter_triangle) | `object` | Triangle optimization criteria. | *Optional* | | 2.5 | +|       flatness | `double` | Relative importance of flat terrain (range 0-1). | *Optional* | `0.0` | 2.0 | +|       [safety](#rd_scooter_triangle_safety) | `double` | Relative importance of safety (range 0-1). | *Optional* | `0.0` | 2.0 | +|       time | `double` | Relative importance of duration of travel (range 0-1). | *Optional* | `0.0` | 2.0 | | [transferOptimization](#rd_transferOptimization) | `object` | Optimize where a transfer between to trip happens. | *Optional* | | 2.1 | |    [backTravelWaitTimeFactor](#rd_to_backTravelWaitTimeFactor) | `double` | To reduce back-travel we favor waiting, this reduces the cost of waiting. | *Optional* | `1.0` | 2.1 | |    [extraStopBoardAlightCostsFactor](#rd_to_extraStopBoardAlightCostsFactor) | `double` | Add an extra board- and alight-cost for prioritized stops. | *Optional* | `0.0` | 2.1 | @@ -406,7 +424,12 @@ since the search-window is increased with the same amount as the maximum penalty the access legs used. In other cases where the access(CAR) is faster than transit the performance will be better. -The default is no penalty, if not configured. +The default values are + +- `car-to-park` = (timePenalty: 20m + 2.0 t, costFactor: 1.50) +- `car-rental` = (timePenalty: 20m + 2.0 t, costFactor: 1.50) +- `car-hailing` = (timePenalty: 20m + 2.0 t, costFactor: 1.50) +- `flexible` = (timePenalty: 10m + 1.30 t, costFactor: 1.30) Example: `"car-to-park" : { "timePenalty": "10m + 1.5t", "costFactor": 2.5 }` @@ -525,7 +548,7 @@ This factor can also include other concerns such as convenience and general cycl preferences by taking into account road surface etc. -

hopCost

+

mountDismountCost

**Since version:** `2.0` ∙ **Type:** `integer` ∙ **Cardinality:** `Optional` ∙ **Default value:** `0` **Path:** /routingDefaults/bicycle/walk @@ -536,7 +559,7 @@ There are different parameters for the cost of renting or parking a vehicle and not meant for controlling the cost of those events. -

hopTime

+

mountDismountTime

**Since version:** `2.0` ∙ **Type:** `duration` ∙ **Cardinality:** `Optional` ∙ **Default value:** `"PT0S"` **Path:** /routingDefaults/bicycle/walk @@ -827,6 +850,50 @@ Override the settings in `maxDirectStreetDuration` for specific street modes. Th done because some street modes searches are much more resource intensive than others. +

optimization

+ +**Since version:** `2.0` ∙ **Type:** `enum` ∙ **Cardinality:** `Optional` ∙ **Default value:** `"safe-streets"` +**Path:** /routingDefaults/scooter +**Enum values:** `shortest-duration` | `safe-streets` | `flat-streets` | `safest-streets` | `triangle` + +The set of characteristics that the user wants to optimize for. + +If the triangle optimization is used, it's enough to just define the triangle parameters + +

allowedNetworks

+ +**Since version:** `2.1` ∙ **Type:** `string[]` ∙ **Cardinality:** `Optional` +**Path:** /routingDefaults/scooter/rental + +The vehicle rental networks which may be used. If empty all networks may be used. + +

bannedNetworks

+ +**Since version:** `2.1` ∙ **Type:** `string[]` ∙ **Cardinality:** `Optional` +**Path:** /routingDefaults/scooter/rental + +The vehicle rental networks which may not be used. If empty, no networks are banned. + +

triangle

+ +**Since version:** `2.5` ∙ **Type:** `object` ∙ **Cardinality:** `Optional` +**Path:** /routingDefaults/scooter + +Triangle optimization criteria. + +Optimization type doesn't need to be defined if these values are defined. + +

safety

+ +**Since version:** `2.0` ∙ **Type:** `double` ∙ **Cardinality:** `Optional` ∙ **Default value:** `0.0` +**Path:** /routingDefaults/scooter/triangle + +Relative importance of safety (range 0-1). + +This factor can also include other concerns such as convenience and general cyclist +preferences by taking into account road surface etc. + +

transferOptimization

**Since version:** `2.1` ∙ **Type:** `object` ∙ **Cardinality:** `Optional` @@ -1093,6 +1160,20 @@ include stairs as a last result. "cost" : 600 } }, + "scooter" : { + "speed" : 5, + "reluctance" : 5.0, + "rental" : { + "pickupCost" : 120, + "dropOffTime" : "30s", + "dropOffCost" : 30 + }, + "triangle" : { + "safety" : 0.4, + "flatness" : 0.3, + "time" : 0.3 + } + }, "walk" : { "speed" : 1.3, "reluctance" : 4.0, diff --git a/docs/RouterConfiguration.md b/docs/RouterConfiguration.md index 62fb5d9617a..73998f06278 100644 --- a/docs/RouterConfiguration.md +++ b/docs/RouterConfiguration.md @@ -496,6 +496,20 @@ Used to group requests when monitoring OTP. "cost" : 600 } }, + "scooter" : { + "speed" : 5, + "reluctance" : 5.0, + "rental" : { + "pickupCost" : 120, + "dropOffTime" : "30s", + "dropOffCost" : 30 + }, + "triangle" : { + "safety" : 0.4, + "flatness" : 0.3, + "time" : 0.3 + } + }, "walk" : { "speed" : 1.3, "reluctance" : 4.0, diff --git a/docs/SandboxExtension.md b/docs/SandboxExtension.md index 4b978313ca6..914be238da4 100644 --- a/docs/SandboxExtension.md +++ b/docs/SandboxExtension.md @@ -23,7 +23,6 @@ nearby stops generated by routing via OSM data. - [Park and Ride API](sandbox/ParkAndRideApi.md) - Park and Ride API - [Data Overlay](sandbox/DataOverlay.md) - StreetEdge grid data populating affecting the route planning - [Vehicle Parking](sandbox/VehicleParking.md) - Vehicle Parking updaters -- [Vehicle-to-stop heuristics](sandbox/VehicleToStopHeuristics.md) - Speeding up Park+Ride, Bike+Ride and Bike+Transit searches - [Travel Time (Isochrone & Surface) API](sandbox/TravelTime.md) - Travel Time API - [IBI accessibility score](sandbox/IBIAccessibilityScore.md) - IBI accessibility score - [Fares](sandbox/Fares.md) - Fare calculation diff --git a/docs/examples/entur/otp-config.json b/docs/examples/entur/otp-config.json index 1746cd186d4..a072a3d4f30 100644 --- a/docs/examples/entur/otp-config.json +++ b/docs/examples/entur/otp-config.json @@ -6,7 +6,6 @@ "OptimizeTransfers": true, "TransferConstraints": true, "ParallelRouting": false, - "ReportApi" : true, - "VehicleToStopHeuristics": true + "ReportApi" : true } } \ No newline at end of file diff --git a/docs/examples/entur/router-config.json b/docs/examples/entur/router-config.json index ffdec9ded79..1fc56bd8401 100644 --- a/docs/examples/entur/router-config.json +++ b/docs/examples/entur/router-config.json @@ -36,6 +36,15 @@ "dropOffCost": 30 } }, + "scooter": { + "speed": 5, + "reluctance": 5.0, + "rental": { + "pickupCost": 120, + "dropOffTime": "30s", + "dropOffCost": 30 + } + }, "walk": { "speed": 1.3, "reluctance": 4.0, diff --git a/docs/examples/ibi/atlanta/router-config.json b/docs/examples/ibi/atlanta/router-config.json index 909c164ec43..7d6e7272fcd 100644 --- a/docs/examples/ibi/atlanta/router-config.json +++ b/docs/examples/ibi/atlanta/router-config.json @@ -17,6 +17,17 @@ "pickupCost": 850 } }, + "scooter": { + "triangle": { + "time": 0.3, + "flatness": 0.3, + "safety": 0.4 + }, + "rental": { + "pickupTime": "3m", + "pickupCost": 850 + } + }, "itineraryFilters": { // only show non-transit (ie. walking) when it's at least as good as the transit option "nonTransitGeneralizedCostLimit": "0 + 1.0 x", diff --git a/docs/sandbox/MapboxVectorTilesApi.md b/docs/sandbox/MapboxVectorTilesApi.md index da9fd1120e1..7183bdf8b7f 100644 --- a/docs/sandbox/MapboxVectorTilesApi.md +++ b/docs/sandbox/MapboxVectorTilesApi.md @@ -148,6 +148,7 @@ For each layer, the configuration includes: | Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | |----------------------------------------------------------------|:----------:|--------------------------------------------------------------------------------------------|:----------:|---------------|:-----:| +| [attribution](#vectorTiles_attribution) | `string` | Custom attribution to be returned in `tilejson.json` | *Optional* | | 2.5 | | [basePath](#vectorTiles_basePath) | `string` | The path of the vector tile source URLs in `tilejson.json`. | *Optional* | | 2.5 | | [layers](#vectorTiles_layers) | `object[]` | Configuration of the individual layers for the Mapbox vector tiles. | *Optional* | | 2.0 | |       type = "stop" | `enum` | Type of the layer. | *Required* | | 2.0 | @@ -161,6 +162,22 @@ For each layer, the configuration includes: #### Details +

attribution

+ +**Since version:** `2.5` ∙ **Type:** `string` ∙ **Cardinality:** `Optional` +**Path:** /vectorTiles + +Custom attribution to be returned in `tilejson.json` + +By default the, `attribution` property in `tilejson.json` is computed from the names and +URLs of the feed publishers. +If the OTP deployment contains many fields, this can become very unwieldy. + +This configuration parameter allows you to set the `attribution` to any string you wish +including HTML tags, +for example `Regional Partners`. + +

basePath

**Since version:** `2.5` ∙ **Type:** `string` ∙ **Cardinality:** `Optional` diff --git a/docs/sandbox/VehicleToStopHeuristics.md b/docs/sandbox/VehicleToStopHeuristics.md deleted file mode 100644 index 3197bbdca9d..00000000000 --- a/docs/sandbox/VehicleToStopHeuristics.md +++ /dev/null @@ -1,77 +0,0 @@ -# Vehicle-to-Stop heuristics - OTP Sandbox Extension - -## Contact Info - -- Leonard Ehrenfried ([mail@leonard.io](mailto:mail@leonard.io)) - -## Changelog - -- Create initial - implementation [#3906](https://github.com/opentripplanner/OpenTripPlanner/pull/3906) - -## Documentation - -This feature is meant to improve the performance and result quality for routing requests where a -vehicle (car, bike, scooter) is ridden to a stop where transit is boarded. - -Before this feature existed a search for nearby stops was executed finding all candidate stops for -boarding transit. For walking this yields a low number of stops but when driving a car this can -easily mean to an entire city of stops, since the default was a drive of 45 minutes. - -Having a very long driving time has several problems: - -- the access search itself is comparatively slow -- having many candidate stops slows down the transit search as many routes have to be checked -- often the quickest route would be to drive almost all the way to the destination and board transit - for a single stop - -We did not want to lower the maximum access time since in rural regions 45 minutes might be a useful -maximum, but we want it to scale with the density of well-connected stops. - -### Vehicle-to-stop heuristic - -In order to improve the Park+Ride and Bike+Ride results we reduced the number of candidate stops -with the following heuristic: - -1. When a stop is encountered check which routes depart from it -2. Each route adds to an "importance score" -3. Modes which are fast (RAIL, SUBWAY, FERRY) have a higher score than for example BUS -4. Once a maximum score is reached, the search is complete - -The code for this is located in `VehicleToStopSkipEdgeStrategy.java`. - -### Bicycle-on-transit heuristic - -This heuristic works slightly differently in that it doesn't assign a score but simply stops the -access search when a certain number of routes were encountered that allow you to take your bike onto -transit. - -The code for this is located in `BikeToStopSkipEdgeStrategy.java`. - -### Configuration - -Enable the feature by adding it to the ```otp-config.json```: - -```json -// otp-config.json -{ - "otpFeatures": { - "VehicleToStopHeuristics": true - } -} -``` - -### Collaborators wanted - -Since the current settings, scores and weights are hardcoded in the source code we are looking for -collaborators that can help to make it more adaptable for different environments. - -These are some the goals for the future: - -- make the scores that are assigned for routes of a certain mode configurable in JSON -- pre-calculate stop importance scores during the graph build - -If you want to help making this feature more flexible, please -contact [Leonard Ehrenfried](mailto:mail@leonard.io) -or use the regular channels of communication outlined -in [CONTRIBUTING.md](https://github.com/opentripplanner/OpenTripPlanner/blob/dev-2.x/CONTRIBUTING.md#primary-channels-of-communication) \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index e4341b296fc..c4717d4d2e0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -106,7 +106,6 @@ nav: - Park and Ride API: 'sandbox/ParkAndRideApi.md' - Data Overlay: 'sandbox/DataOverlay.md' - Vehicle Parking Updaters: 'sandbox/VehicleParking.md' - - Vehicle-to-stop Heuristics: 'sandbox/VehicleToStopHeuristics.md' - Geocoder API: 'sandbox/GeocoderAPI.md' - Travel Time Isochrones: 'sandbox/TravelTime.md' - IBI Accessibility Score: 'sandbox/IBIAccessibilityScore.md' diff --git a/pom.xml b/pom.xml index be1e49f2a16..8adaaec352a 100644 --- a/pom.xml +++ b/pom.xml @@ -56,18 +56,18 @@ - 143 + 146 30.2 2.50 2.16.1 3.1.5 - 5.10.1 - 1.12.1 + 5.10.2 + 1.12.2 5.5.3 1.4.14 9.9.1 - 2.0.11 + 2.0.12 2.0.15 1.26 4.0.4 @@ -545,7 +545,7 @@ com.google.cloud libraries-bom - 26.27.0 + 26.31.0 pom import @@ -713,7 +713,7 @@ org.mockito mockito-core - 5.9.0 + 5.10.0 test diff --git a/src/client/debug-client-preview/index.html b/src/client/debug-client-preview/index.html index 2653fdf36c2..d72598680fe 100644 --- a/src/client/debug-client-preview/index.html +++ b/src/client/debug-client-preview/index.html @@ -5,8 +5,8 @@ OTP Debug Client - - + +
diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java index a0585a9e6f2..f414572ecf2 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java @@ -32,6 +32,7 @@ import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.routing.api.RoutingService; import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.routing.api.request.framework.TimeAndCostPenalty; import org.opentripplanner.routing.api.request.request.filter.AllowAllTransitFilter; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.transit.service.TransitModel; @@ -224,6 +225,11 @@ private static List getItineraries( request.setTo(to); request.setNumItineraries(10); request.setSearchWindow(Duration.ofHours(2)); + request.withPreferences(p -> + p.withStreet(s -> + s.withAccessEgress(ae -> ae.withPenalty(Map.of(FLEXIBLE, TimeAndCostPenalty.ZERO))) + ) + ); var modes = request.journey().modes().copyOf(); modes.withEgressMode(FLEXIBLE); diff --git a/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/DecorateConsolidatedStopNamesTest.java b/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/DecorateConsolidatedStopNamesTest.java index 38af8ea7187..d823a55cc4a 100644 --- a/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/DecorateConsolidatedStopNamesTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/DecorateConsolidatedStopNamesTest.java @@ -37,8 +37,8 @@ void changeNames() { filter.decorate(itinerary); - var updatedLeg = itinerary.getLegs().get(0); - assertEquals(STOP_D.getName(), updatedLeg.getFrom().name); + var updatedLeg = itinerary.getLegs().getFirst(); + assertEquals(STOP_C.getName(), updatedLeg.getFrom().name); assertEquals(STOP_D.getName(), updatedLeg.getTo().name); } } diff --git a/src/ext/java/org/opentripplanner/ext/parkAndRideApi/ParkAndRideResource.java b/src/ext/java/org/opentripplanner/ext/parkAndRideApi/ParkAndRideResource.java index 02dfc1bef5d..4a577433bfe 100644 --- a/src/ext/java/org/opentripplanner/ext/parkAndRideApi/ParkAndRideResource.java +++ b/src/ext/java/org/opentripplanner/ext/parkAndRideApi/ParkAndRideResource.java @@ -42,7 +42,7 @@ public ParkAndRideResource( // - serverContext.graphFinder(). This needs at least a comment! // - This can be replaced with a search done with the StopModel // - if we have a radius search there. - this.graphFinder = new DirectGraphFinder(serverContext.transitService()::findRegularStop); + this.graphFinder = new DirectGraphFinder(serverContext.transitService()::findRegularStops); } /** Envelopes are in latitude, longitude format */ diff --git a/src/ext/java/org/opentripplanner/ext/restapi/mapping/LegacyBicycleOptimizeType.java b/src/ext/java/org/opentripplanner/ext/restapi/mapping/LegacyBicycleOptimizeType.java deleted file mode 100644 index ff50a03534b..00000000000 --- a/src/ext/java/org/opentripplanner/ext/restapi/mapping/LegacyBicycleOptimizeType.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.opentripplanner.ext.restapi.mapping; - -import org.opentripplanner.routing.core.BicycleOptimizeType; - -/** - * Bicycle optimization types that are only meant to be used by the REST API. Related to {@link org.opentripplanner.routing.core.BicycleOptimizeType} - */ -public enum LegacyBicycleOptimizeType { - QUICK, - SAFE, - FLAT, - GREENWAYS, - TRIANGLE; - - public static BicycleOptimizeType map(LegacyBicycleOptimizeType type) { - return switch (type) { - case QUICK -> BicycleOptimizeType.SHORTEST_DURATION; - case FLAT -> BicycleOptimizeType.FLAT_STREETS; - case SAFE -> BicycleOptimizeType.SAFE_STREETS; - case GREENWAYS -> BicycleOptimizeType.SAFEST_STREETS; - case TRIANGLE -> BicycleOptimizeType.TRIANGLE; - }; - } -} diff --git a/src/ext/java/org/opentripplanner/ext/restapi/mapping/LegacyVehicleRoutingOptimizeType.java b/src/ext/java/org/opentripplanner/ext/restapi/mapping/LegacyVehicleRoutingOptimizeType.java new file mode 100644 index 00000000000..675e8541048 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/restapi/mapping/LegacyVehicleRoutingOptimizeType.java @@ -0,0 +1,25 @@ +package org.opentripplanner.ext.restapi.mapping; + +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; + +/** + * Bicycle and scooter optimization types that are only meant to be used by the REST API. Related to + * {@link VehicleRoutingOptimizeType} + */ +public enum LegacyVehicleRoutingOptimizeType { + QUICK, + SAFE, + FLAT, + GREENWAYS, + TRIANGLE; + + public static VehicleRoutingOptimizeType map(LegacyVehicleRoutingOptimizeType type) { + return switch (type) { + case QUICK -> VehicleRoutingOptimizeType.SHORTEST_DURATION; + case FLAT -> VehicleRoutingOptimizeType.FLAT_STREETS; + case SAFE -> VehicleRoutingOptimizeType.SAFE_STREETS; + case GREENWAYS -> VehicleRoutingOptimizeType.SAFEST_STREETS; + case TRIANGLE -> VehicleRoutingOptimizeType.TRIANGLE; + }; + } +} diff --git a/src/ext/java/org/opentripplanner/ext/restapi/resources/IndexAPI.java b/src/ext/java/org/opentripplanner/ext/restapi/resources/IndexAPI.java index 5bdda8a57a2..cab2be13ad0 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/resources/IndexAPI.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/resources/IndexAPI.java @@ -200,7 +200,7 @@ public List getStopsInRadius( radius = Math.min(radius, MAX_STOP_SEARCH_RADIUS); - return new DirectGraphFinder(serverContext.transitService()::findRegularStop) + return new DirectGraphFinder(serverContext.transitService()::findRegularStops) .findClosestStops(new Coordinate(lon, lat), radius) .stream() .map(it -> StopMapper.mapToApiShort(it.stop, it.distance)) @@ -221,7 +221,7 @@ public List getStopsInRadius( new Coordinate(maxLon, maxLat) ); - var stops = transitService().findRegularStop(envelope); + var stops = transitService().findRegularStops(envelope); return stops .stream() .filter(stop -> envelope.contains(stop.getCoordinate().asJtsCoordinate())) diff --git a/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java b/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java index bfac498ff8c..6248f71e214 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/resources/RequestToPreferencesMapper.java @@ -2,7 +2,7 @@ import jakarta.validation.constraints.NotNull; import java.util.function.Consumer; -import org.opentripplanner.ext.restapi.mapping.LegacyBicycleOptimizeType; +import org.opentripplanner.ext.restapi.mapping.LegacyVehicleRoutingOptimizeType; import org.opentripplanner.framework.lang.ObjectUtils; import org.opentripplanner.routing.algorithm.filterchain.api.TransitGeneralizedCostFilterParams; import org.opentripplanner.routing.api.request.framework.CostLinearFunction; @@ -32,6 +32,7 @@ void map() { mapCar(); mapWalk(); mapBike(); + mapScooter(); var boardAndAlightSlack = mapTransit(); @@ -69,10 +70,10 @@ private void mapBike() { setIfNotNull(req.bikeBoardCost, bike::withBoardCost); setIfNotNull( req.bikeOptimizeType, - optimizeType -> bike.withOptimizeType(LegacyBicycleOptimizeType.map(optimizeType)) + optimizeType -> bike.withOptimizeType(LegacyVehicleRoutingOptimizeType.map(optimizeType)) ); - if (req.bikeOptimizeType == LegacyBicycleOptimizeType.TRIANGLE) { + if (req.bikeOptimizeType == LegacyVehicleRoutingOptimizeType.TRIANGLE) { bike.withOptimizeTriangle(triangle -> { setIfNotNull(req.triangleTimeFactor, triangle::withTime); setIfNotNull(req.triangleSlopeFactor, triangle::withSlope); @@ -89,12 +90,33 @@ private void mapBike() { bike.withWalking(walk -> { setIfNotNull(req.bikeWalkingSpeed, walk::withSpeed); setIfNotNull(req.bikeWalkingReluctance, walk::withReluctance); - setIfNotNull(req.bikeSwitchTime, walk::withHopTime); - setIfNotNull(req.bikeSwitchCost, walk::withHopCost); + setIfNotNull(req.bikeSwitchTime, walk::withMountDismountTime); + setIfNotNull(req.bikeSwitchCost, walk::withMountDismountCost); }); }); } + private void mapScooter() { + preferences.withScooter(scooter -> { + setIfNotNull(req.bikeSpeed, scooter::withSpeed); + setIfNotNull(req.bikeReluctance, scooter::withReluctance); + setIfNotNull( + req.bikeOptimizeType, + optimizeType -> scooter.withOptimizeType(LegacyVehicleRoutingOptimizeType.map(optimizeType)) + ); + + if (req.bikeOptimizeType == LegacyVehicleRoutingOptimizeType.TRIANGLE) { + scooter.withOptimizeTriangle(triangle -> { + setIfNotNull(req.triangleTimeFactor, triangle::withTime); + setIfNotNull(req.triangleSlopeFactor, triangle::withSlope); + setIfNotNull(req.triangleSafetyFactor, triangle::withSafety); + }); + } + + scooter.withRental(this::mapRental); + }); + } + private BoardAndAlightSlack mapTransit() { preferences.withTransit(tr -> { setIfNotNull(req.boardSlack, tr::withDefaultBoardSlackSec); diff --git a/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java b/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java index 1c234e3ff9e..1d985f0e555 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java @@ -20,7 +20,7 @@ import javax.xml.datatype.XMLGregorianCalendar; import org.opentripplanner.api.parameter.QualifiedModeSet; import org.opentripplanner.ext.dataoverlay.api.DataOverlayParameters; -import org.opentripplanner.ext.restapi.mapping.LegacyBicycleOptimizeType; +import org.opentripplanner.ext.restapi.mapping.LegacyVehicleRoutingOptimizeType; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.lang.StringUtils; import org.opentripplanner.framework.time.DurationUtils; @@ -239,17 +239,15 @@ public abstract class RoutingResource { protected Double triangleTimeFactor; /** - * The set of characteristics that the user wants to optimize for. @See OptimizeType. + * The set of characteristics that the user wants to optimize for in bicycle and scooter routing. + * @See OptimizeType. * * @deprecated TODO OTP2 this should be completely removed and done only with individual cost * parameters - * Also: apparently OptimizeType only affects BICYCLE mode traversal of - * street segments. If this is the case it should be very well - * documented and carried over into the Enum name. */ @Deprecated @QueryParam("optimize") - protected LegacyBicycleOptimizeType bikeOptimizeType; + protected LegacyVehicleRoutingOptimizeType bikeOptimizeType; /** * The set of modes that a user is willing to use, with qualifiers stating whether vehicles should diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/DecorateConsolidatedStopNames.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/DecorateConsolidatedStopNames.java index 8fcba82a357..a287e6a7d66 100644 --- a/src/ext/java/org/opentripplanner/ext/stopconsolidation/DecorateConsolidatedStopNames.java +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/DecorateConsolidatedStopNames.java @@ -7,9 +7,9 @@ import org.opentripplanner.routing.algorithm.filterchain.framework.spi.ItineraryDecorator; /** - * A decorating filter that checks if a transit leg contains any primary stops and if it does, - * then replaces it with the secondary, agency-specific stop name. This is so that the in-vehicle - * display matches what OTP returns as a board/alight stop name. + * A decorating filter that checks if a transit leg contains any consolidated stops and if it does, + * then replaces it with the appropriate, agency-specific stop name. This is so that the physical + * signage and in-vehicle display matches what OTP returns as a board/alight stop name. */ public class DecorateConsolidatedStopNames implements ItineraryDecorator { @@ -21,31 +21,45 @@ public DecorateConsolidatedStopNames(StopConsolidationService service) { @Override public void decorate(Itinerary itinerary) { - replacePrimaryNamesWithSecondary(itinerary); + replaceConsolidatedStops(itinerary); } /** - * If the itinerary has a from/to that is the primary stop of a {@link org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup} - * then we replace its name with the secondary name of the agency that is - * operating the route, so that the name in the result matches the name in the in-vehicle - * display. + * If the itinerary has a "from" stop that is the secondary stop of a + * {@link org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup} + * then we replace its name with the primary name of the agency that is + * operating the route, so that the name in the result matches the physical signage on the stop. + *

+ * If the leg has a "to" stop that is a primary stop, then we don't want to show the stop that's on + * the signage but what is shown _inside_ the vehicle. That's why we use the agency-specific (aka + * secondary) stop. + *

+ * This follows the somewhat idiosyncratic logic of the consolidated stops feature. */ - private void replacePrimaryNamesWithSecondary(Itinerary i) { + private void replaceConsolidatedStops(Itinerary i) { i.transformTransitLegs(leg -> { if (leg instanceof ScheduledTransitLeg stl && needsToRenameStops(stl)) { var agency = leg.getAgency(); - return new ConsolidatedStopLeg( - stl, - service.agencySpecificName(stl.getFrom().stop, agency), - service.agencySpecificName(stl.getTo().stop, agency) - ); + // to show the name on the stop signage we use the primary stop's name + var from = service.primaryStop(stl.getFrom().stop.getId()); + // to show the name that's on the display inside the vehicle we use the agency-specific name + var to = service.agencySpecificStop(stl.getTo().stop, agency); + return new ConsolidatedStopLeg(stl, from, to); } else { return leg; } }); } + /** + * Figures out if the from/to stops are part of a consolidated stop group and therefore + * some stops need to be replaced. + *

+ * Please consult the Javadoc of {@link DecorateConsolidatedStopNames#replaceConsolidatedStops(Itinerary)} + * for details of this idiosyncratic business logic and in particular why the logic is not the same + * for the from/to stops. + */ private boolean needsToRenameStops(ScheduledTransitLeg stl) { - return (service.isPrimaryStop(stl.getFrom().stop) || service.isPrimaryStop(stl.getTo().stop)); + return (service.isSecondaryStop(stl.getFrom().stop) || service.isPrimaryStop(stl.getTo().stop)); } } diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationService.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationService.java index 2418d8f5625..a829a905e27 100644 --- a/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationService.java +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationService.java @@ -2,7 +2,6 @@ import java.util.List; import org.opentripplanner.ext.stopconsolidation.model.StopReplacement; -import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.site.StopLocation; @@ -24,13 +23,23 @@ public interface StopConsolidationService { */ boolean isPrimaryStop(StopLocation stop); + /** + * Is the given stop a secondary stop as defined by the stop consolidation configuration? + */ + boolean isSecondaryStop(StopLocation stop); + /** * Are any stop consolidations defined? */ boolean isActive(); /** - * For a given primary stop look up the name as it was originally defined in the agency's feed. + * For a given primary stop look up secondary feed as it was originally defined in the agency's feed. + */ + StopLocation agencySpecificStop(StopLocation stop, Agency agency); + + /** + * For a given stop id return the primary stop if it is part of a consolidated stop group. */ - I18NString agencySpecificName(StopLocation stop, Agency agency); + StopLocation primaryStop(FeedScopedId id); } diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java index 54bd960d078..51a57028121 100644 --- a/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java @@ -2,14 +2,15 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; +import javax.annotation.Nonnull; import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; +import org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup; import org.opentripplanner.ext.stopconsolidation.model.StopReplacement; -import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.organization.Agency; -import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.service.TransitModel; import org.slf4j.Logger; @@ -61,26 +62,46 @@ public boolean isPrimaryStop(StopLocation stop) { return repo.groups().stream().anyMatch(r -> r.primary().equals(stop.getId())); } + @Override + public boolean isSecondaryStop(StopLocation stop) { + return repo.groups().stream().anyMatch(r -> r.secondaries().contains(stop.getId())); + } + @Override public boolean isActive() { return !repo.groups().isEmpty(); } @Override - public I18NString agencySpecificName(StopLocation stop, Agency agency) { + public StopLocation agencySpecificStop(StopLocation stop, Agency agency) { if (agency.getId().getFeedId().equals(stop.getId().getFeedId())) { - return stop.getName(); + return stop; } else { - return repo - .groups() - .stream() - .filter(r -> r.primary().equals(stop.getId())) - .flatMap(g -> g.secondaries().stream()) - .filter(secondary -> secondary.getFeedId().equals(agency.getId().getFeedId())) - .findAny() - .map(id -> transitModel.getStopModel().getRegularStop(id)) - .map(RegularStop::getName) - .orElseGet(stop::getName); + return findAgencySpecificStop(stop, agency).orElse(stop); } } + + @Nonnull + private Optional findAgencySpecificStop(StopLocation stop, Agency agency) { + return repo + .groups() + .stream() + .filter(r -> r.primary().equals(stop.getId())) + .flatMap(g -> g.secondaries().stream()) + .filter(secondary -> secondary.getFeedId().equals(agency.getId().getFeedId())) + .findAny() + .map(id -> transitModel.getStopModel().getRegularStop(id)); + } + + @Override + public StopLocation primaryStop(FeedScopedId id) { + var primaryId = repo + .groups() + .stream() + .filter(g -> g.secondaries().contains(id)) + .map(ConsolidatedStopGroup::primary) + .findAny() + .orElse(id); + return transitModel.getStopModel().getRegularStop(primaryId); + } } diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopLeg.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopLeg.java index e201c7ed805..39f06bd6347 100644 --- a/src/ext/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopLeg.java +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopLeg.java @@ -1,28 +1,28 @@ package org.opentripplanner.ext.stopconsolidation.model; -import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.model.plan.Place; import org.opentripplanner.model.plan.ScheduledTransitLeg; import org.opentripplanner.model.plan.ScheduledTransitLegBuilder; +import org.opentripplanner.transit.model.site.StopLocation; public class ConsolidatedStopLeg extends ScheduledTransitLeg { - private final I18NString fromName; - private final I18NString toName; + private final StopLocation from; + private final StopLocation to; - public ConsolidatedStopLeg(ScheduledTransitLeg original, I18NString fromName, I18NString toName) { + public ConsolidatedStopLeg(ScheduledTransitLeg original, StopLocation from, StopLocation to) { super(new ScheduledTransitLegBuilder<>(original)); - this.fromName = fromName; - this.toName = toName; + this.from = from; + this.to = to; } @Override public Place getFrom() { - return Place.forStop(super.getFrom().stop, fromName); + return Place.forStop(from); } @Override public Place getTo() { - return Place.forStop(super.getTo().stop, toName); + return Place.forStop(to); } } diff --git a/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java b/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java index 772db7394f3..a1e9a83c85a 100644 --- a/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java +++ b/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java @@ -28,6 +28,7 @@ import org.opentripplanner.inspector.vector.LayerBuilder; import org.opentripplanner.inspector.vector.LayerParameters; import org.opentripplanner.inspector.vector.VectorTileResponseFactory; +import org.opentripplanner.model.FeedInfo; import org.opentripplanner.standalone.api.OtpServerRequestContext; @Path("/routers/{ignoreRouterId}/vectorTiles") @@ -81,13 +82,6 @@ public TileJson getTileJson( @PathParam("layers") String requestedLayers ) { var envelope = serverContext.worldEnvelopeService().envelope().orElseThrow(); - var feedInfos = serverContext - .transitService() - .getFeedIds() - .stream() - .map(serverContext.transitService()::getFeedInfo) - .filter(Predicate.not(Objects::isNull)) - .toList(); List rLayers = Arrays.asList(requestedLayers.split(",")); @@ -101,7 +95,24 @@ public TileJson getTileJson( TileJson.urlWithDefaultPath(uri, headers, rLayers, ignoreRouterId, "vectorTiles") ); - return new TileJson(url, envelope, feedInfos); + return serverContext + .vectorTileConfig() + .attribution() + .map(attr -> new TileJson(url, envelope, attr)) + .orElseGet(() -> { + var feedInfos = getFeedInfos(); + return new TileJson(url, envelope, feedInfos); + }); + } + + private List getFeedInfos() { + return serverContext + .transitService() + .getFeedIds() + .stream() + .map(serverContext.transitService()::getFeedInfo) + .filter(Predicate.not(Objects::isNull)) + .toList(); } private static LayerBuilder crateLayerBuilder( diff --git a/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerBuilder.java b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerBuilder.java index 6d15816669e..2ad85587fe5 100644 --- a/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerBuilder.java +++ b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerBuilder.java @@ -37,7 +37,7 @@ public StopsLayerBuilder( protected List getGeometries(Envelope query) { return transitService - .findRegularStop(query) + .findRegularStops(query) .stream() .map(stop -> { Geometry point = stop.getGeometry(); diff --git a/src/ext/java/org/opentripplanner/ext/vehicletostopheuristics/BikeToStopSkipEdgeStrategy.java b/src/ext/java/org/opentripplanner/ext/vehicletostopheuristics/BikeToStopSkipEdgeStrategy.java deleted file mode 100644 index ad503b1212d..00000000000 --- a/src/ext/java/org/opentripplanner/ext/vehicletostopheuristics/BikeToStopSkipEdgeStrategy.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.opentripplanner.ext.vehicletostopheuristics; - -import java.util.Collection; -import java.util.function.Function; -import org.opentripplanner.astar.spi.SkipEdgeStrategy; -import org.opentripplanner.street.model.edge.Edge; -import org.opentripplanner.street.model.vertex.TransitStopVertex; -import org.opentripplanner.street.search.state.State; -import org.opentripplanner.transit.model.network.BikeAccess; -import org.opentripplanner.transit.model.site.RegularStop; -import org.opentripplanner.transit.model.timetable.Trip; - -/** - * When wanting to take a bike onto transit we want to improve the performance by limiting the - * number of accesses to those stops which actually have trips where you can take the bike on. Once - * we have reached enough of trips we skip all further edges. - */ -public class BikeToStopSkipEdgeStrategy implements SkipEdgeStrategy { - - private static final int LIMIT = 100; - private static final double MAX_FACTOR = 1.2; - - private final Function> getTripsForStop; - - int numberOfBikeableTripsReached = 0; - double distanceLimit = Double.MAX_VALUE; - - public BikeToStopSkipEdgeStrategy(Function> getTripsForStop) { - this.getTripsForStop = getTripsForStop; - } - - public static boolean bikeAccessForTrip(Trip trip) { - if (trip.getBikesAllowed() != BikeAccess.UNKNOWN) { - return trip.getBikesAllowed() == BikeAccess.ALLOWED; - } - - return trip.getRoute().getBikesAllowed() == BikeAccess.ALLOWED; - } - - @Override - public boolean shouldSkipEdge(State current, Edge edge) { - if ( - current.getVertex() instanceof TransitStopVertex stopVertex && - distanceLimit == Double.MAX_VALUE - ) { - numberOfBikeableTripsReached += - getTripsForStop - .apply(stopVertex.getStop()) - .stream() - .filter(BikeToStopSkipEdgeStrategy::bikeAccessForTrip) - .count(); - if (numberOfBikeableTripsReached >= LIMIT) { - distanceLimit = current.getWalkDistance() * MAX_FACTOR; - } - } - return current.getWalkDistance() > distanceLimit; - } -} diff --git a/src/ext/java/org/opentripplanner/ext/vehicletostopheuristics/VehicleToStopSkipEdgeStrategy.java b/src/ext/java/org/opentripplanner/ext/vehicletostopheuristics/VehicleToStopSkipEdgeStrategy.java deleted file mode 100644 index 3eb466f97b9..00000000000 --- a/src/ext/java/org/opentripplanner/ext/vehicletostopheuristics/VehicleToStopSkipEdgeStrategy.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.opentripplanner.ext.vehicletostopheuristics; - -import static org.opentripplanner.routing.api.request.StreetMode.BIKE_RENTAL; -import static org.opentripplanner.routing.api.request.StreetMode.BIKE_TO_PARK; -import static org.opentripplanner.routing.api.request.StreetMode.CAR_HAILING; -import static org.opentripplanner.routing.api.request.StreetMode.CAR_PICKUP; -import static org.opentripplanner.routing.api.request.StreetMode.CAR_RENTAL; -import static org.opentripplanner.routing.api.request.StreetMode.CAR_TO_PARK; -import static org.opentripplanner.routing.api.request.StreetMode.SCOOTER_RENTAL; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Function; -import org.opentripplanner.astar.spi.SkipEdgeStrategy; -import org.opentripplanner.routing.api.request.StreetMode; -import org.opentripplanner.routing.api.request.request.filter.TransitFilter; -import org.opentripplanner.street.model.edge.Edge; -import org.opentripplanner.street.model.vertex.TransitStopVertex; -import org.opentripplanner.street.search.state.State; -import org.opentripplanner.transit.model.basic.TransitMode; -import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.site.RegularStop; - -/** - * This strategy terminates when enough "important" stops are found. - *

- * The definition of important is a stop where many routes of mode RAIL, SUBWAY or FERRY depart. - *

- * This means that the search radius scales with density of "important" stops: - *

- *

  • in a city the radius is quite small - *
  • in more rural regions the radius is bigger and stops further away are considered - *

    - * The strategy is useful when you want to limit the number of accesses of Park+Ride, Bike+Ride and - * Bike+Transit: it improves both performance the quality of results. - *

    - * {@see https://github.com/opentripplanner/OpenTripPlanner/pull/3906} - */ -public class VehicleToStopSkipEdgeStrategy implements SkipEdgeStrategy { - - public static final Set applicableModes = Set.of( - BIKE_TO_PARK, - BIKE_RENTAL, - CAR_TO_PARK, - CAR_PICKUP, - CAR_HAILING, - CAR_RENTAL, - SCOOTER_RENTAL - ); - private final Function> getPatternsForStop; - private final int maxScore; - private final List filters; - private double sumOfScores; - - private final Set stopsCounted = new HashSet<>(); - - public VehicleToStopSkipEdgeStrategy( - Function> getPatternsForStop, - Collection filters - ) { - this.filters = new ArrayList<>(filters); - this.maxScore = 300; - this.getPatternsForStop = getPatternsForStop; - } - - @Override - public boolean shouldSkipEdge(State current, Edge edge) { - if (current.currentMode().isWalking()) { - if ( - current.getVertex() instanceof TransitStopVertex stopVertex && - !stopsCounted.contains(stopVertex.getStop().getId()) - ) { - // TODO: 2022-12-05 filters: check performance on that and verify that this is right. Previously we were filtering just on modes - var stop = stopVertex.getStop(); - - // Not using streams. Performance is important here - var patterns = getPatternsForStop.apply(stop); - var score = 0; - for (var pattern : patterns) { - for (var filter : filters) { - if (filter.matchTripPattern(pattern)) { - score += VehicleToStopSkipEdgeStrategy.score(pattern.getMode()); - break; - } - } - } - - stopsCounted.add(stop.getId()); - - sumOfScores = sumOfScores + score; - } - return false; - } else { - return sumOfScores >= maxScore; - } - } - - private static int score(TransitMode mode) { - return switch (mode) { - case RAIL, FERRY, SUBWAY -> 20; - case BUS -> 1; - default -> 2; - }; - } -} 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 d749987384c..a4f5cb00e2f 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java @@ -21,7 +21,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.locationtech.jts.geom.Coordinate; @@ -710,7 +709,7 @@ public DataFetcher> stopsByBbox() { ); Stream stopStream = getTransitService(environment) - .findRegularStop(envelope) + .findRegularStops(envelope) .stream() .filter(stop -> envelope.contains(stop.getCoordinate().asJtsCoordinate())); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/package.json b/src/main/java/org/opentripplanner/apis/gtfs/generated/package.json index 6a2eb3343b9..db865eaa003 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/package.json +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/package.json @@ -10,8 +10,8 @@ }, "license": "LGPL-3.0", "dependencies": { - "@graphql-codegen/add": "5.0.0", - "@graphql-codegen/cli": "5.0.0", + "@graphql-codegen/add": "5.0.2", + "@graphql-codegen/cli": "5.0.2", "@graphql-codegen/java": "4.0.0", "@graphql-codegen/java-resolvers": "3.0.0", "graphql": "16.8.1" diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/yarn.lock b/src/main/java/org/opentripplanner/apis/gtfs/generated/yarn.lock index 7e3c815cfb1..fffe602db18 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/yarn.lock +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/yarn.lock @@ -264,7 +264,7 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== -"@babel/helper-plugin-utils@^7.22.5": +"@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== @@ -699,24 +699,25 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" -"@graphql-codegen/add@5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@graphql-codegen/add/-/add-5.0.0.tgz#578ebaf4fa87c1e934c381cd679bcedcf79feaba" - integrity sha512-ynWDOsK2yxtFHwcJTB9shoSkUd7YXd6ZE57f0nk7W5cu/nAgxZZpEsnTPEpZB/Mjf14YRGe2uJHQ7AfElHjqUQ== +"@graphql-codegen/add@5.0.2", "@graphql-codegen/add@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@graphql-codegen/add/-/add-5.0.2.tgz#71b3ae0465a4537172dddb84531b6967ca5545f2" + integrity sha512-ouBkSvMFUhda5VoKumo/ZvsZM9P5ZTyDsI8LW18VxSNWOjrTeLXBWHG8Gfaai0HwhflPtCYVABbriEcOmrRShQ== dependencies: - "@graphql-codegen/plugin-helpers" "^5.0.0" - tslib "~2.5.0" + "@graphql-codegen/plugin-helpers" "^5.0.3" + tslib "~2.6.0" -"@graphql-codegen/cli@5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@graphql-codegen/cli/-/cli-5.0.0.tgz#761dcf08cfee88bbdd9cdf8097b2343445ec6f0a" - integrity sha512-A7J7+be/a6e+/ul2KI5sfJlpoqeqwX8EzktaKCeduyVKgOLA6W5t+NUGf6QumBDXU8PEOqXk3o3F+RAwCWOiqA== +"@graphql-codegen/cli@5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@graphql-codegen/cli/-/cli-5.0.2.tgz#07ff691c16da4c3dcc0e1995d3231530379ab317" + integrity sha512-MBIaFqDiLKuO4ojN6xxG9/xL9wmfD3ZjZ7RsPjwQnSHBCUXnEkdKvX+JVpx87Pq29Ycn8wTJUguXnTZ7Di0Mlw== dependencies: "@babel/generator" "^7.18.13" "@babel/template" "^7.18.10" "@babel/types" "^7.18.13" - "@graphql-codegen/core" "^4.0.0" - "@graphql-codegen/plugin-helpers" "^5.0.1" + "@graphql-codegen/client-preset" "^4.2.2" + "@graphql-codegen/core" "^4.0.2" + "@graphql-codegen/plugin-helpers" "^5.0.3" "@graphql-tools/apollo-engine-loader" "^8.0.0" "@graphql-tools/code-file-loader" "^8.0.0" "@graphql-tools/git-loader" "^8.0.0" @@ -747,15 +748,45 @@ yaml "^2.3.1" yargs "^17.0.0" -"@graphql-codegen/core@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@graphql-codegen/core/-/core-4.0.0.tgz#b29c911746a532a675e33720acb4eb2119823e01" - integrity sha512-JAGRn49lEtSsZVxeIlFVIRxts2lWObR+OQo7V2LHDJ7ohYYw3ilv7nJ8pf8P4GTg/w6ptcYdSdVVdkI8kUHB/Q== +"@graphql-codegen/client-preset@^4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@graphql-codegen/client-preset/-/client-preset-4.2.2.tgz#545c62789a5687bee5df8b4738b4911e72ea8051" + integrity sha512-DF9pNWj3TEdA90E9FH5SsUIqiZfr872vqaQOspLVuVXGsaDx8F/JLLzaN+7ucmoo0ff/bLW8munVXYXTmgwwEA== dependencies: - "@graphql-codegen/plugin-helpers" "^5.0.0" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/template" "^7.20.7" + "@graphql-codegen/add" "^5.0.2" + "@graphql-codegen/gql-tag-operations" "4.0.4" + "@graphql-codegen/plugin-helpers" "^5.0.3" + "@graphql-codegen/typed-document-node" "^5.0.4" + "@graphql-codegen/typescript" "^4.0.4" + "@graphql-codegen/typescript-operations" "^4.1.2" + "@graphql-codegen/visitor-plugin-common" "^4.1.2" + "@graphql-tools/documents" "^1.0.0" + "@graphql-tools/utils" "^10.0.0" + "@graphql-typed-document-node/core" "3.2.0" + tslib "~2.6.0" + +"@graphql-codegen/core@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@graphql-codegen/core/-/core-4.0.2.tgz#7e6ec266276f54bbf02f60599d9e518f4a59d85e" + integrity sha512-IZbpkhwVqgizcjNiaVzNAzm/xbWT6YnGgeOLwVjm4KbJn3V2jchVtuzHH09G5/WkkLSk2wgbXNdwjM41JxO6Eg== + dependencies: + "@graphql-codegen/plugin-helpers" "^5.0.3" "@graphql-tools/schema" "^10.0.0" "@graphql-tools/utils" "^10.0.0" - tslib "~2.5.0" + tslib "~2.6.0" + +"@graphql-codegen/gql-tag-operations@4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-4.0.4.tgz#572be5db804af5efdc3ca24e4bcac815448730c5" + integrity sha512-dypul0iDLjb07yv+/cRb6qPbn42cFPcwlsJertVl9G6qkS4+3V4806WwSfUht4QVMWnvGfgDkJJqG0yUVKOHwA== + dependencies: + "@graphql-codegen/plugin-helpers" "^5.0.3" + "@graphql-codegen/visitor-plugin-common" "4.1.2" + "@graphql-tools/utils" "^10.0.0" + auto-bind "~4.0.0" + tslib "~2.6.0" "@graphql-codegen/java-common@^3.0.0": version "3.0.0" @@ -813,29 +844,59 @@ lodash "~4.17.0" tslib "~2.4.0" -"@graphql-codegen/plugin-helpers@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@graphql-codegen/plugin-helpers/-/plugin-helpers-5.0.0.tgz#40c18217454af5cf8317e5f46cf4d38e8cc78ae4" - integrity sha512-suL2ZMkBAU2a4YbBHaZvUPsV1z0q3cW6S96Z/eYYfkRIsJoe2vN+wNZ9Xdzmqx0JLmeeFCBSoBGC0imFyXlkDQ== +"@graphql-codegen/plugin-helpers@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@graphql-codegen/plugin-helpers/-/plugin-helpers-5.0.3.tgz#7027b9d911d7cb594663590fcf5d63e9cf7ec2ff" + integrity sha512-yZ1rpULIWKBZqCDlvGIJRSyj1B2utkEdGmXZTBT/GVayP4hyRYlkd36AJV/LfEsVD8dnsKL5rLz2VTYmRNlJ5Q== dependencies: "@graphql-tools/utils" "^10.0.0" change-case-all "1.0.15" common-tags "1.8.2" import-from "4.0.0" lodash "~4.17.0" - tslib "~2.5.0" + tslib "~2.6.0" -"@graphql-codegen/plugin-helpers@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@graphql-codegen/plugin-helpers/-/plugin-helpers-5.0.1.tgz#e2429fcfba3f078d5aa18aa062d46c922bbb0d55" - integrity sha512-6L5sb9D8wptZhnhLLBcheSPU7Tg//DGWgc5tQBWX46KYTOTQHGqDpv50FxAJJOyFVJrveN9otWk9UT9/yfY4ww== +"@graphql-codegen/schema-ast@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@graphql-codegen/schema-ast/-/schema-ast-4.0.2.tgz#aeaa104e4555cca73a058f0a9350b4b0e290b377" + integrity sha512-5mVAOQQK3Oz7EtMl/l3vOQdc2aYClUzVDHHkMvZlunc+KlGgl81j8TLa+X7ANIllqU4fUEsQU3lJmk4hXP6K7Q== dependencies: + "@graphql-codegen/plugin-helpers" "^5.0.3" "@graphql-tools/utils" "^10.0.0" + tslib "~2.6.0" + +"@graphql-codegen/typed-document-node@^5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@graphql-codegen/typed-document-node/-/typed-document-node-5.0.4.tgz#06e286caacdd66c3566f98433dcb8f1a9c9a9f1d" + integrity sha512-t66Z6erQ4Dh1j6f9pRZmc8uYtHoUI3A49tLmJAlg9/3IV0kCmwrWKJut/G8SeOefDLG8cXBTVtI/YuZOe1Te+w== + dependencies: + "@graphql-codegen/plugin-helpers" "^5.0.3" + "@graphql-codegen/visitor-plugin-common" "4.1.2" + auto-bind "~4.0.0" change-case-all "1.0.15" - common-tags "1.8.2" - import-from "4.0.0" - lodash "~4.17.0" - tslib "~2.5.0" + tslib "~2.6.0" + +"@graphql-codegen/typescript-operations@^4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@graphql-codegen/typescript-operations/-/typescript-operations-4.1.2.tgz#a0f455ae19e16961e5870420ca7515bbe51b5568" + integrity sha512-CtCWK+gW7hS+Ely3lohr8CL1HVLswQzMcaUk3k1sxdWCWKTNq7abMsWa31rTVwRCJ+WNEkM/7S8sIBTpEG683A== + dependencies: + "@graphql-codegen/plugin-helpers" "^5.0.3" + "@graphql-codegen/typescript" "^4.0.4" + "@graphql-codegen/visitor-plugin-common" "4.1.2" + auto-bind "~4.0.0" + tslib "~2.6.0" + +"@graphql-codegen/typescript@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@graphql-codegen/typescript/-/typescript-4.0.4.tgz#e791c61f675ae454951ea077b0ae519ae352cc3e" + integrity sha512-x79CKLfP9UQCX+/I78qxQlMs2Mmq3pF1lKafZo7lAno0f/fvJ+qWUduzdgjRNz+YL+5blGeWcC0pWEDxniO7hw== + dependencies: + "@graphql-codegen/plugin-helpers" "^5.0.3" + "@graphql-codegen/schema-ast" "^4.0.2" + "@graphql-codegen/visitor-plugin-common" "4.1.2" + auto-bind "~4.0.0" + tslib "~2.6.0" "@graphql-codegen/visitor-plugin-common@2.13.1": version "2.13.1" @@ -853,6 +914,22 @@ parse-filepath "^1.0.2" tslib "~2.4.0" +"@graphql-codegen/visitor-plugin-common@4.1.2", "@graphql-codegen/visitor-plugin-common@^4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-4.1.2.tgz#674c5d5813f6c00dd65e1ee148a62536879e65e2" + integrity sha512-yk7iEAL1kYZ2Gi/pvVjdsZhul5WsYEM4Zcgh2Ev15VicMdJmPHsMhNUsZWyVJV0CaQCYpNOFlGD/11Ea3pn4GA== + dependencies: + "@graphql-codegen/plugin-helpers" "^5.0.3" + "@graphql-tools/optimize" "^2.0.0" + "@graphql-tools/relay-operation-optimizer" "^7.0.0" + "@graphql-tools/utils" "^10.0.0" + auto-bind "~4.0.0" + change-case-all "1.0.15" + dependency-graph "^0.11.0" + graphql-tag "^2.11.0" + parse-filepath "^1.0.2" + tslib "~2.6.0" + "@graphql-tools/apollo-engine-loader@^8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.0.tgz#ac1f351cbe41508411784f25757f5557b0f27489" @@ -897,6 +974,14 @@ tslib "^2.5.0" value-or-promise "^1.0.12" +"@graphql-tools/documents@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/documents/-/documents-1.0.0.tgz#e3ed97197cc22ec830ca227fd7d17e86d8424bdf" + integrity sha512-rHGjX1vg/nZ2DKqRGfDPNC55CWZBMldEVcH+91BThRa6JeT80NqXknffLLEZLRUxyikCfkwMsk6xR3UNMqG0Rg== + dependencies: + lodash.sortby "^4.7.0" + tslib "^2.4.0" + "@graphql-tools/executor-graphql-ws@^1.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-1.0.2.tgz#29890f9370c5bebd4a2380e29904f8eaf9f013ca" @@ -1036,6 +1121,13 @@ dependencies: tslib "^2.4.0" +"@graphql-tools/optimize@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/optimize/-/optimize-2.0.0.tgz#7a9779d180824511248a50c5a241eff6e7a2d906" + integrity sha512-nhdT+CRGDZ+bk68ic+Jw1OZ99YCDIKYA5AlVAnBHJvMawSx9YQqQAIj4refNc1/LRieGiuWvhbG3jvPVYho0Dg== + dependencies: + tslib "^2.4.0" + "@graphql-tools/prisma-loader@^8.0.0": version "8.0.1" resolved "https://registry.yarnpkg.com/@graphql-tools/prisma-loader/-/prisma-loader-8.0.1.tgz#0a013c69b04e0779b5be15757173d458cdf94e35" @@ -1069,6 +1161,15 @@ "@graphql-tools/utils" "9.1.1" tslib "^2.4.0" +"@graphql-tools/relay-operation-optimizer@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.0.tgz#24367666af87bc5a81748de5e8e9b3c523fd4207" + integrity sha512-UNlJi5y3JylhVWU4MBpL0Hun4Q7IoJwv9xYtmAz+CgRa066szzY7dcuPfxrA7cIGgG/Q6TVsKsYaiF4OHPs1Fw== + dependencies: + "@ardatan/relay-compiler" "12.0.0" + "@graphql-tools/utils" "^10.0.0" + tslib "^2.4.0" + "@graphql-tools/schema@^10.0.0": version "10.0.0" resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-10.0.0.tgz#7b5f6b6a59f51c927de8c9069bde4ebbfefc64b3" @@ -2374,6 +2475,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + lodash@^4.17.20, lodash@^4.17.21, lodash@~4.17.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -3049,7 +3155,7 @@ ts-log@^2.2.3: resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.3.tgz#4da5640fe25a9fb52642cd32391c886721318efb" integrity sha512-XvB+OdKSJ708Dmf9ore4Uf/q62AYDTzFcAdxc8KNML1mmAWywRFVt/dn1KYJH8Agt5UJNujfM3znU5PxgAzA2w== -tslib@^2.0.0, tslib@^2.3.1, tslib@^2.4.1, tslib@~2.5.0: +tslib@^2.0.0, tslib@^2.3.1, tslib@^2.4.1: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/OptimizationTypeMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/OptimizationTypeMapper.java index 1f53652f7b7..7f4f5200256 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/mapping/OptimizationTypeMapper.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/OptimizationTypeMapper.java @@ -2,18 +2,18 @@ import javax.annotation.Nonnull; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; -import org.opentripplanner.routing.core.BicycleOptimizeType; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; public final class OptimizationTypeMapper { @Nonnull - public static BicycleOptimizeType map(GraphQLTypes.GraphQLOptimizeType optimizeType) { + public static VehicleRoutingOptimizeType map(GraphQLTypes.GraphQLOptimizeType optimizeType) { return switch (optimizeType) { - case QUICK -> BicycleOptimizeType.SHORTEST_DURATION; - case FLAT -> BicycleOptimizeType.FLAT_STREETS; - case SAFE -> BicycleOptimizeType.SAFE_STREETS; - case GREENWAYS -> BicycleOptimizeType.SAFEST_STREETS; - case TRIANGLE -> BicycleOptimizeType.TRIANGLE; + case QUICK -> VehicleRoutingOptimizeType.SHORTEST_DURATION; + case FLAT -> VehicleRoutingOptimizeType.FLAT_STREETS; + case SAFE -> VehicleRoutingOptimizeType.SAFE_STREETS; + case GREENWAYS -> VehicleRoutingOptimizeType.SAFEST_STREETS; + case TRIANGLE -> VehicleRoutingOptimizeType.TRIANGLE; }; } } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapper.java index 212bfbcf380..4cd6c04b044 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapper.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapper.java @@ -28,7 +28,7 @@ import org.opentripplanner.routing.api.request.preference.VehicleWalkingPreferences; import org.opentripplanner.routing.api.request.request.filter.SelectRequest; import org.opentripplanner.routing.api.request.request.filter.TransitFilterRequest; -import org.opentripplanner.routing.core.BicycleOptimizeType; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; import org.opentripplanner.transit.model.basic.MainAndSubMode; import org.opentripplanner.transit.model.basic.TransitMode; @@ -79,7 +79,7 @@ public static RouteRequest toRouteRequest( ) ); } - if (bike.optimizeType() == BicycleOptimizeType.TRIANGLE) { + if (bike.optimizeType() == VehicleRoutingOptimizeType.TRIANGLE) { bike.withOptimizeTriangle(triangle -> { callWith.argument("triangle.timeFactor", triangle::withTime); callWith.argument("triangle.slopeFactor", triangle::withSlope); @@ -98,6 +98,28 @@ public static RouteRequest toRouteRequest( car.withRental(rental -> setRentalPreferences(callWith, request, rental)); }); + preferences.withScooter(scooter -> { + callWith.argument("bikeReluctance", scooter::withReluctance); + callWith.argument("bikeSpeed", scooter::withSpeed); + + if (environment.getArgument("optimize") != null) { + scooter.withOptimizeType( + OptimizationTypeMapper.map( + GraphQLTypes.GraphQLOptimizeType.valueOf(environment.getArgument("optimize")) + ) + ); + } + if (scooter.optimizeType() == VehicleRoutingOptimizeType.TRIANGLE) { + scooter.withOptimizeTriangle(triangle -> { + callWith.argument("triangle.timeFactor", triangle::withTime); + callWith.argument("triangle.slopeFactor", triangle::withSlope); + callWith.argument("triangle.safetyFactor", triangle::withSafety); + }); + } + + scooter.withRental(rental -> setRentalPreferences(callWith, request, rental)); + }); + preferences.withWalk(b -> { callWith.argument("walkReluctance", b::withReluctance); callWith.argument("walkSpeed", b::withSpeed); @@ -340,8 +362,8 @@ private static void setVehicleWalkingPreferences( ) { callWith.argument("bikeWalkingReluctance", walking::withReluctance); callWith.argument("bikeWalkingSpeed", walking::withSpeed); - callWith.argument("bikeSwitchTime", time -> walking.withHopTime((int) time)); - callWith.argument("bikeSwitchCost", cost -> walking.withHopCost((int) cost)); + callWith.argument("bikeSwitchTime", time -> walking.withMountDismountTime((int) time)); + callWith.argument("bikeSwitchCost", cost -> walking.withMountDismountCost((int) cost)); } private static class CallerWithEnvironment { diff --git a/src/main/java/org/opentripplanner/apis/support/TileJson.java b/src/main/java/org/opentripplanner/apis/support/TileJson.java index 75aabb2b6c6..1f5c090c16d 100644 --- a/src/main/java/org/opentripplanner/apis/support/TileJson.java +++ b/src/main/java/org/opentripplanner/apis/support/TileJson.java @@ -36,15 +36,8 @@ public class TileJson implements Serializable { public final double[] bounds; public final double[] center; - public TileJson(String tileUrl, WorldEnvelope envelope, Collection feedInfos) { - attribution = - feedInfos - .stream() - .map(feedInfo -> - "" + feedInfo.getPublisherName() + "" - ) - .collect(Collectors.joining(", ")); - + public TileJson(String tileUrl, WorldEnvelope envelope, String attribution) { + this.attribution = attribution; tiles = new String[] { tileUrl }; bounds = @@ -59,6 +52,10 @@ public TileJson(String tileUrl, WorldEnvelope envelope, Collection fee center = new double[] { c.longitude(), c.latitude(), 9 }; } + public TileJson(String tileUrl, WorldEnvelope envelope, Collection feedInfos) { + this(tileUrl, envelope, attributionFromFeedInfo(feedInfos)); + } + /** * Creates a vector source layer URL from a hard-coded path plus information from the incoming * HTTP request. @@ -96,4 +93,14 @@ public static String urlFromOverriddenBasePath( String.join(",", layers) ); } + + private static String attributionFromFeedInfo(Collection feedInfos) { + return feedInfos + .stream() + .map(feedInfo -> + "%s".formatted(feedInfo.getPublisherUrl(), feedInfo.getPublisherName()) + ) + .distinct() + .collect(Collectors.joining(", ")); + } } diff --git a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java index 00bf98c703f..638d7783e9a 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java @@ -687,7 +687,7 @@ private GraphQLSchema create() { ); return GqlUtil .getTransitService(environment) - .findRegularStop(envelope) + .findRegularStops(envelope) .stream() .filter(stop -> envelope.contains(stop.getCoordinate().asJtsCoordinate())) .filter(stop -> diff --git a/src/main/java/org/opentripplanner/apis/transmodel/mapping/PreferencesMapper.java b/src/main/java/org/opentripplanner/apis/transmodel/mapping/PreferencesMapper.java index 846437952e4..a86193553f2 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/mapping/PreferencesMapper.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/mapping/PreferencesMapper.java @@ -3,6 +3,7 @@ import static org.opentripplanner.apis.transmodel.mapping.preferences.BikePreferencesMapper.mapBikePreferences; import static org.opentripplanner.apis.transmodel.mapping.preferences.CarPreferencesMapper.mapCarPreferences; import static org.opentripplanner.apis.transmodel.mapping.preferences.ItineraryFilterPreferencesMapper.mapItineraryFilterPreferences; +import static org.opentripplanner.apis.transmodel.mapping.preferences.ScooterPreferencesMapper.mapScooterPreferences; import static org.opentripplanner.apis.transmodel.mapping.preferences.StreetPreferencesMapper.mapStreetPreferences; import static org.opentripplanner.apis.transmodel.mapping.preferences.TransferPreferencesMapper.mapTransferPreferences; import static org.opentripplanner.apis.transmodel.mapping.preferences.TransitPreferencesMapper.mapTransitPreferences; @@ -24,6 +25,7 @@ static void mapPreferences( preferences.withWalk(walk -> mapWalkPreferences(walk, callWith)); preferences.withBike(bike -> mapBikePreferences(bike, callWith)); preferences.withCar(car -> mapCarPreferences(car, callWith)); + preferences.withScooter(scooter -> mapScooterPreferences(scooter, callWith)); preferences.withTransfer(transfer -> mapTransferPreferences(transfer, environment, callWith)); preferences.withTransit(transit -> mapTransitPreferences(transit, environment, callWith)); preferences.withItineraryFilter(itineraryFilter -> diff --git a/src/main/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapper.java b/src/main/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapper.java index d02e251f4e2..59259905944 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapper.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapper.java @@ -7,23 +7,30 @@ class RequestModesMapper { + private static final String accessModeKey = "accessMode"; + private static final String egressModeKey = "egressMode"; + private static final String directModeKey = "directMode"; + /** - * Maps a GraphQL Modes input type to a RequestModes. - * - * This only maps access, egress, direct & transfer. - * Transport modes are now part of filters. - * Only in case filters are not present we will use this mapping + * Maps GraphQL Modes input type to RequestModes. + *

    + * This only maps access, egress, direct & transfer modes. Transport modes are set using filters. + * Default modes are WALK for access, egress, direct & transfer. */ - @SuppressWarnings("unchecked") static RequestModes mapRequestModes(Map modesInput) { - StreetMode accessMode = (StreetMode) modesInput.get("accessMode"); - RequestModesBuilder mBuilder = RequestModes - .of() - .withAccessMode(accessMode) - .withEgressMode((StreetMode) modesInput.get("egressMode")) - .withDirectMode((StreetMode) modesInput.get("directMode")); + RequestModesBuilder mBuilder = RequestModes.of(); - mBuilder.withTransferMode(accessMode == StreetMode.BIKE ? StreetMode.BIKE : StreetMode.WALK); + if (modesInput.containsKey(accessModeKey)) { + StreetMode accessMode = (StreetMode) modesInput.get(accessModeKey); + mBuilder.withAccessMode(accessMode); + mBuilder.withTransferMode(accessMode == StreetMode.BIKE ? StreetMode.BIKE : StreetMode.WALK); + } + if (modesInput.containsKey(egressModeKey)) { + mBuilder.withEgressMode((StreetMode) modesInput.get(egressModeKey)); + } + if (modesInput.containsKey(directModeKey)) { + mBuilder.withDirectMode((StreetMode) modesInput.get(directModeKey)); + } return mBuilder.build(); } diff --git a/src/main/java/org/opentripplanner/apis/transmodel/mapping/preferences/BikePreferencesMapper.java b/src/main/java/org/opentripplanner/apis/transmodel/mapping/preferences/BikePreferencesMapper.java index 1179c034e93..76826d31e2d 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/mapping/preferences/BikePreferencesMapper.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/mapping/preferences/BikePreferencesMapper.java @@ -4,7 +4,7 @@ import org.opentripplanner.apis.transmodel.support.DataFetcherDecorator; import org.opentripplanner.routing.api.request.preference.BikePreferences; -import org.opentripplanner.routing.core.BicycleOptimizeType; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; public class BikePreferencesMapper { @@ -37,7 +37,7 @@ public static void mapBikePreferences( // bike.withWalkingReluctance(WALK_BIKE_RELATIVE_RELUCTANCE * (double)r ); //}); - if (bike.optimizeType() == BicycleOptimizeType.TRIANGLE) { + if (bike.optimizeType() == VehicleRoutingOptimizeType.TRIANGLE) { bike.withOptimizeTriangle(triangle -> { callWith.argument("triangleFactors.time", triangle::withTime); callWith.argument("triangleFactors.slope", triangle::withSlope); diff --git a/src/main/java/org/opentripplanner/apis/transmodel/mapping/preferences/ScooterPreferencesMapper.java b/src/main/java/org/opentripplanner/apis/transmodel/mapping/preferences/ScooterPreferencesMapper.java new file mode 100644 index 00000000000..d7d5c019f93 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/mapping/preferences/ScooterPreferencesMapper.java @@ -0,0 +1,36 @@ +package org.opentripplanner.apis.transmodel.mapping.preferences; + +import static org.opentripplanner.apis.transmodel.mapping.preferences.RentalPreferencesMapper.mapRentalPreferences; + +import org.opentripplanner.apis.transmodel.support.DataFetcherDecorator; +import org.opentripplanner.routing.api.request.preference.ScooterPreferences; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; + +public class ScooterPreferencesMapper { + + public static void mapScooterPreferences( + ScooterPreferences.Builder scooter, + DataFetcherDecorator callWith + ) { + callWith.argument("bikeSpeed", scooter::withSpeed); + callWith.argument("bicycleOptimisationMethod", scooter::withOptimizeType); + + // WALK reluctance is used for backwards compatibility, then overridden + callWith.argument( + "walkReluctance", + r -> { + scooter.withReluctance((double) r); + } + ); + + if (scooter.optimizeType() == VehicleRoutingOptimizeType.TRIANGLE) { + scooter.withOptimizeTriangle(triangle -> { + callWith.argument("triangleFactors.time", triangle::withTime); + callWith.argument("triangleFactors.slope", triangle::withSlope); + callWith.argument("triangleFactors.safety", triangle::withSafety); + }); + } + + scooter.withRental(rental -> mapRentalPreferences(rental, callWith)); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java b/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java index e2897d868c9..b85d4ce19b9 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java @@ -17,7 +17,7 @@ import org.opentripplanner.routing.api.request.preference.ItineraryFilterDebugProfile; import org.opentripplanner.routing.api.response.InputField; import org.opentripplanner.routing.api.response.RoutingErrorCode; -import org.opentripplanner.routing.core.BicycleOptimizeType; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; import org.opentripplanner.routing.stoptimes.ArrivalDeparture; import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.transit.model.basic.Accessibility; @@ -64,11 +64,11 @@ public class EnumTypes { public static final GraphQLEnumType BICYCLE_OPTIMISATION_METHOD = GraphQLEnumType .newEnum() .name("BicycleOptimisationMethod") - .value("quick", BicycleOptimizeType.SHORTEST_DURATION) - .value("safe", BicycleOptimizeType.SAFE_STREETS) - .value("flat", BicycleOptimizeType.FLAT_STREETS) - .value("greenways", BicycleOptimizeType.SAFEST_STREETS) - .value("triangle", BicycleOptimizeType.TRIANGLE) + .value("quick", VehicleRoutingOptimizeType.SHORTEST_DURATION) + .value("safe", VehicleRoutingOptimizeType.SAFE_STREETS) + .value("flat", VehicleRoutingOptimizeType.FLAT_STREETS) + .value("greenways", VehicleRoutingOptimizeType.SAFEST_STREETS) + .value("triangle", VehicleRoutingOptimizeType.TRIANGLE) .build(); public static final GraphQLEnumType BIKES_ALLOWED = GraphQLEnumType diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java index abeeb187342..bf34d9928e4 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java @@ -20,7 +20,7 @@ import org.opentripplanner.apis.transmodel.model.framework.PenaltyForStreetModeType; import org.opentripplanner.apis.transmodel.support.GqlUtil; import org.opentripplanner.routing.api.request.preference.RoutingPreferences; -import org.opentripplanner.routing.core.BicycleOptimizeType; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; public class TripQuery { @@ -371,7 +371,10 @@ public static GraphQLFieldDefinition create( "When setting the " + EnumTypes.BICYCLE_OPTIMISATION_METHOD.getName() + " to '" + - enumValAsString(EnumTypes.BICYCLE_OPTIMISATION_METHOD, BicycleOptimizeType.TRIANGLE) + + enumValAsString( + EnumTypes.BICYCLE_OPTIMISATION_METHOD, + VehicleRoutingOptimizeType.TRIANGLE + ) + "', use these values to tell the routing engine how important each of the factors is compared to the others. All values should add up to 1." ) .type(TriangleFactorsInputType.INPUT_TYPE) diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/stop/StopPlaceType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/stop/StopPlaceType.java index accaf1e35fb..d178e5125b5 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/stop/StopPlaceType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/stop/StopPlaceType.java @@ -531,7 +531,7 @@ public static Collection fetchStopPlaces( ); Stream stations = transitService - .findRegularStop(envelope) + .findRegularStops(envelope) .stream() .filter(stop -> envelope.contains(stop.getCoordinate().asJtsCoordinate())) .map(StopLocation::getParentStation) diff --git a/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java b/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java index f45d7d36413..8350a10d670 100644 --- a/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java +++ b/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java @@ -53,6 +53,7 @@ public class DebugStyleSpec { static StyleSpec build( VectorSourceLayer regularStops, VectorSourceLayer areaStops, + VectorSourceLayer groupStops, VectorSourceLayer edges, VectorSourceLayer vertices ) { @@ -122,6 +123,15 @@ static StyleSpec build( .fillOutlineColor(BLACK) .minZoom(6) .maxZoom(MAX_ZOOM), + StyleBuilder + .ofId("group-stop") + .typeFill() + .vectorSourceLayer(groupStops) + .fillColor(GREEN) + .fillOpacity(0.5f) + .fillOutlineColor(BLACK) + .minZoom(6) + .maxZoom(MAX_ZOOM), StyleBuilder .ofId("regular-stop") .typeCircle() diff --git a/src/main/java/org/opentripplanner/apis/vectortiles/GraphInspectorVectorTileResource.java b/src/main/java/org/opentripplanner/apis/vectortiles/GraphInspectorVectorTileResource.java index 03f4357e540..f97830232bd 100644 --- a/src/main/java/org/opentripplanner/apis/vectortiles/GraphInspectorVectorTileResource.java +++ b/src/main/java/org/opentripplanner/apis/vectortiles/GraphInspectorVectorTileResource.java @@ -3,6 +3,7 @@ import static org.opentripplanner.apis.vectortiles.model.LayerType.AreaStop; import static org.opentripplanner.apis.vectortiles.model.LayerType.Edge; import static org.opentripplanner.apis.vectortiles.model.LayerType.GeofencingZones; +import static org.opentripplanner.apis.vectortiles.model.LayerType.GroupStop; import static org.opentripplanner.apis.vectortiles.model.LayerType.RegularStop; import static org.opentripplanner.apis.vectortiles.model.LayerType.Vertex; import static org.opentripplanner.framework.io.HttpUtils.APPLICATION_X_PROTOBUF; @@ -35,6 +36,7 @@ import org.opentripplanner.inspector.vector.VectorTileResponseFactory; import org.opentripplanner.inspector.vector.edge.EdgeLayerBuilder; import org.opentripplanner.inspector.vector.geofencing.GeofencingZonesLayerBuilder; +import org.opentripplanner.inspector.vector.stop.GroupStopLayerBuilder; import org.opentripplanner.inspector.vector.stop.StopLayerBuilder; import org.opentripplanner.inspector.vector.vertex.VertexLayerBuilder; import org.opentripplanner.model.FeedInfo; @@ -49,6 +51,7 @@ public class GraphInspectorVectorTileResource { private static final LayerParams REGULAR_STOPS = new LayerParams("regularStops", RegularStop); private static final LayerParams AREA_STOPS = new LayerParams("areaStops", AreaStop); + private static final LayerParams GROUP_STOPS = new LayerParams("groupStops", GroupStop); private static final LayerParams GEOFENCING_ZONES = new LayerParams( "geofencingZones", GeofencingZones @@ -58,6 +61,7 @@ public class GraphInspectorVectorTileResource { private static final List> DEBUG_LAYERS = List.of( REGULAR_STOPS, AREA_STOPS, + GROUP_STOPS, GEOFENCING_ZONES, EDGES, VERTICES @@ -128,11 +132,11 @@ public TileJson getTileJson( public StyleSpec getTileJson(@Context UriInfo uri, @Context HttpHeaders headers) { var base = HttpUtils.getBaseAddress(uri, headers); - // these two could also be loaded together but are put into separate sources because + // these could also be loaded together but are put into separate sources because // the stops are fast and the edges are relatively slow var stopsSource = new VectorSource( "stops", - tileJsonUrl(base, List.of(REGULAR_STOPS, AREA_STOPS)) + tileJsonUrl(base, List.of(REGULAR_STOPS, AREA_STOPS, GROUP_STOPS)) ); var streetSource = new VectorSource( "street", @@ -142,6 +146,7 @@ public StyleSpec getTileJson(@Context UriInfo uri, @Context HttpHeaders headers) return DebugStyleSpec.build( REGULAR_STOPS.toVectorSourceLayer(stopsSource), AREA_STOPS.toVectorSourceLayer(stopsSource), + GROUP_STOPS.toVectorSourceLayer(stopsSource), EDGES.toVectorSourceLayer(streetSource), VERTICES.toVectorSourceLayer(streetSource) ); @@ -179,13 +184,19 @@ private static LayerBuilder createLayerBuilder( case RegularStop -> new StopLayerBuilder<>( layerParameters, locale, - e -> context.transitService().findRegularStop(e) + e -> context.transitService().findRegularStops(e) ); case AreaStop -> new StopLayerBuilder<>( layerParameters, locale, e -> context.transitService().findAreaStops(e) ); + case GroupStop -> new GroupStopLayerBuilder( + layerParameters, + locale, + // There are not many GroupStops, so we can just list them all. + context.transitService().listGroupStops() + ); case GeofencingZones -> new GeofencingZonesLayerBuilder(context.graph(), layerParameters); case Edge -> new EdgeLayerBuilder(context.graph(), layerParameters); case Vertex -> new VertexLayerBuilder(context.graph(), layerParameters); diff --git a/src/main/java/org/opentripplanner/apis/vectortiles/model/LayerType.java b/src/main/java/org/opentripplanner/apis/vectortiles/model/LayerType.java index ece75f29b60..585725d0707 100644 --- a/src/main/java/org/opentripplanner/apis/vectortiles/model/LayerType.java +++ b/src/main/java/org/opentripplanner/apis/vectortiles/model/LayerType.java @@ -3,6 +3,7 @@ public enum LayerType { RegularStop, AreaStop, + GroupStop, GeofencingZones, Edge, Vertex, diff --git a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java index 4847b204077..4ae5004cf6b 100644 --- a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java +++ b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java @@ -105,8 +105,7 @@ public enum OTPFeature { SandboxAPIMapboxVectorTilesApi(false, true, "Enable Mapbox vector tiles API."), SandboxAPIParkAndRideApi(false, true, "Enable park-and-ride endpoint."), SandboxAPITravelTime(false, true, "Enable the isochrone/travel time surface API."), - TransferAnalyzer(false, true, "Analyze transfers during graph build."), - VehicleToStopHeuristics(false, true, "Enable improved heuristic for park-and-ride queries."); + TransferAnalyzer(false, true, "Analyze transfers during graph build."); private static final Object TEST_LOCK = new Object(); diff --git a/src/main/java/org/opentripplanner/framework/io/HttpUtils.java b/src/main/java/org/opentripplanner/framework/io/HttpUtils.java index 4981a8ab91b..672c9b9c481 100644 --- a/src/main/java/org/opentripplanner/framework/io/HttpUtils.java +++ b/src/main/java/org/opentripplanner/framework/io/HttpUtils.java @@ -3,6 +3,7 @@ import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.UriInfo; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import org.apache.hc.core5.http.ContentType; public final class HttpUtils { @@ -31,7 +32,7 @@ public static String getBaseAddress(UriInfo uri, HttpHeaders headers) { String host; if (headers.getRequestHeader(HEADER_X_FORWARDED_HOST) != null) { - host = headers.getRequestHeader(HEADER_X_FORWARDED_HOST).getFirst(); + host = extractHost(headers.getRequestHeader(HEADER_X_FORWARDED_HOST).getFirst()); } else if (headers.getRequestHeader(HEADER_HOST) != null) { host = headers.getRequestHeader(HEADER_HOST).getFirst(); } else { @@ -40,4 +41,12 @@ public static String getBaseAddress(UriInfo uri, HttpHeaders headers) { return protocol + "://" + host; } + + /** + * The X-Forwarded-Host header can contain a comma-separated list so we account for that. + * https://stackoverflow.com/questions/66042952/http-proxy-behavior-for-x-forwarded-host-header + */ + private static String extractHost(String xForwardedFor) { + return Arrays.stream(xForwardedFor.split(",")).map(String::strip).findFirst().get(); + } } diff --git a/src/main/java/org/opentripplanner/framework/lang/StringUtils.java b/src/main/java/org/opentripplanner/framework/lang/StringUtils.java index c726d03c66c..cbb32b9eaca 100644 --- a/src/main/java/org/opentripplanner/framework/lang/StringUtils.java +++ b/src/main/java/org/opentripplanner/framework/lang/StringUtils.java @@ -110,4 +110,13 @@ public static String padRight(String value, char ch, int width) { public static String quoteReplace(@Nonnull String text) { return text.replace('\'', '\"'); } + + /** + * Convert "HELLO_WORLD" or "HellO_WorlD" to "hello-world". + *

    + * https://developer.mozilla.org/en-US/docs/Glossary/Kebab_case + */ + public static String kebabCase(String input) { + return input.toLowerCase().replace('_', '-'); + } } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/NearbyStopFinder.java b/src/main/java/org/opentripplanner/graph_builder/module/NearbyStopFinder.java index 360cdaee363..7386b60b452 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/NearbyStopFinder.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/NearbyStopFinder.java @@ -18,8 +18,6 @@ import org.opentripplanner.astar.strategy.MaxCountSkipEdgeStrategy; import org.opentripplanner.ext.dataoverlay.routing.DataOverlayContext; import org.opentripplanner.ext.flex.trip.FlexTrip; -import org.opentripplanner.ext.vehicletostopheuristics.BikeToStopSkipEdgeStrategy; -import org.opentripplanner.ext.vehicletostopheuristics.VehicleToStopSkipEdgeStrategy; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.routing.api.request.RouteRequest; @@ -85,7 +83,7 @@ public NearbyStopFinder( // We need to accommodate straight line distance (in meters) but when streets are present we // use an earliest arrival search, which optimizes on time. Ideally we'd specify in meters, // but we don't have much of a choice here. Use the default walking speed to convert. - this.directGraphFinder = new DirectGraphFinder(transitService::findRegularStop); + this.directGraphFinder = new DirectGraphFinder(transitService::findRegularStops); } } @@ -205,7 +203,7 @@ public List findNearbyStopsViaStreets( ShortestPathTree spt = StreetSearchBuilder .of() - .setSkipEdgeStrategy(getSkipEdgeStrategy(reverseDirection, request)) + .setSkipEdgeStrategy(getSkipEdgeStrategy()) .setDominanceFunction(new DominanceFunctions.MinimumWeight()) .setRequest(request) .setArriveBy(reverseDirection) @@ -271,48 +269,14 @@ private List findNearbyStopsViaDirectTransfers(Vertex vertex) { return directGraphFinder.findClosestStops(c0, limitMeters); } - private SkipEdgeStrategy getSkipEdgeStrategy( - boolean reverseDirection, - RouteRequest routingRequest - ) { + private SkipEdgeStrategy getSkipEdgeStrategy() { var durationSkipEdgeStrategy = new DurationSkipEdgeStrategy(durationLimit); - // if we compute the accesses for Park+Ride, Bike+Ride and Bike+Transit we don't want to - // search the full durationLimit as this returns way too many stops. - // this is both slow and returns suboptimal results as it favours long drives with short - // transit legs. - // therefore, we use a heuristic based on the number of routes and their mode to determine - // what are "good" stops for those accesses. if we have reached a threshold of "good" stops - // we stop the access search. - if ( - !reverseDirection && - OTPFeature.VehicleToStopHeuristics.isOn() && - VehicleToStopSkipEdgeStrategy.applicableModes.contains( - routingRequest.journey().access().mode() - ) - ) { - var strategy = new VehicleToStopSkipEdgeStrategy( - transitService::getPatternsForStop, - routingRequest.journey().transit().filters() - ); - + if (maxStopCount > 0) { + var strategy = new MaxCountSkipEdgeStrategy<>(maxStopCount, NearbyStopFinder::hasReachedStop); return new ComposingSkipEdgeStrategy<>(strategy, durationSkipEdgeStrategy); - } else if ( - OTPFeature.VehicleToStopHeuristics.isOn() && - routingRequest.journey().access().mode() == StreetMode.BIKE - ) { - var strategy = new BikeToStopSkipEdgeStrategy(transitService::getTripsForStop); - return new ComposingSkipEdgeStrategy<>(strategy, durationSkipEdgeStrategy); - } else { - if (maxStopCount > 0) { - var strategy = new MaxCountSkipEdgeStrategy<>( - maxStopCount, - NearbyStopFinder::hasReachedStop - ); - return new ComposingSkipEdgeStrategy<>(strategy, durationSkipEdgeStrategy); - } - return durationSkipEdgeStrategy; } + return durationSkipEdgeStrategy; } private static List createDirectlyConnectedStops( diff --git a/src/main/java/org/opentripplanner/inspector/vector/stop/GroupStopLayerBuilder.java b/src/main/java/org/opentripplanner/inspector/vector/stop/GroupStopLayerBuilder.java new file mode 100644 index 00000000000..1e66397bc05 --- /dev/null +++ b/src/main/java/org/opentripplanner/inspector/vector/stop/GroupStopLayerBuilder.java @@ -0,0 +1,50 @@ +package org.opentripplanner.inspector.vector.stop; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.opentripplanner.inspector.vector.LayerBuilder; +import org.opentripplanner.inspector.vector.LayerParameters; +import org.opentripplanner.transit.model.site.GroupStop; +import org.opentripplanner.transit.model.site.StopLocation; + +/** + * A vector tile layer for {@link GroupStop}s inside the vector tile bounds. The builder does not + * query for the GroupStops to draw, but instead uses the geometries of the GroupStops that are + * passed to the constructor. + */ +public class GroupStopLayerBuilder extends LayerBuilder { + + private final List geometries; + + public GroupStopLayerBuilder( + LayerParameters layerParameters, + Locale locale, + Collection groupStops + ) { + super( + new StopLocationPropertyMapper(locale), + layerParameters.name(), + layerParameters.expansionFactor() + ); + // Because there are very few GroupStops with relevant geometries, we can precompute the + // geometries and store them in a list at the time of construction. + this.geometries = + groupStops + .stream() + .filter(groupStop -> groupStop.getEncompassingAreaGeometry().isPresent()) + .map(stop -> { + Geometry geometry = stop.getEncompassingAreaGeometry().get().copy(); + geometry.setUserData(stop); + return geometry; + }) + .toList(); + } + + @Override + protected List getGeometries(Envelope query) { + return geometries; + } +} diff --git a/src/main/java/org/opentripplanner/model/plan/Place.java b/src/main/java/org/opentripplanner/model/plan/Place.java index 71c6d9bc188..fe3a9dee420 100644 --- a/src/main/java/org/opentripplanner/model/plan/Place.java +++ b/src/main/java/org/opentripplanner/model/plan/Place.java @@ -90,11 +90,7 @@ public static Place normal(Vertex vertex, I18NString name) { } public static Place forStop(StopLocation stop) { - return forStop(stop, stop.getName()); - } - - public static Place forStop(StopLocation stop, I18NString nameOverride) { - return new Place(nameOverride, stop.getCoordinate(), VertexType.TRANSIT, stop, null, null); + return new Place(stop.getName(), stop.getCoordinate(), VertexType.TRANSIT, stop, null, null); } public static Place forFlexStop(StopLocation stop, Vertex vertex) { diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java index c9e7f92263f..ab11d654ef7 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java @@ -232,6 +232,8 @@ private Collection fetchAccessEgresses(AccessEgressType typ accessRequest.withPreferences(p -> { p.withBike(b -> b.withRental(r -> r.withAllowArrivingInRentedVehicleAtDestination(false))); p.withCar(c -> c.withRental(r -> r.withAllowArrivingInRentedVehicleAtDestination(false))); + p.withScooter(s -> s.withRental(r -> r.withAllowArrivingInRentedVehicleAtDestination(false)) + ); }); } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java index 2c6b09c2fb2..6229cbb022c 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectStreetRouter.java @@ -96,6 +96,8 @@ private static double calculateDistanceMaxLimit(RouteRequest request) { distanceLimit = durationLimit * preferences.car().speed(); } else if (mode.includesBiking()) { distanceLimit = durationLimit * preferences.bike().speed(); + } else if (mode.includesScooter()) { + distanceLimit = durationLimit * preferences.scooter().speed(); } else if (mode.includesWalking()) { distanceLimit = durationLimit * preferences.walk().speed(); } else { diff --git a/src/main/java/org/opentripplanner/routing/api/request/RequestModes.java b/src/main/java/org/opentripplanner/routing/api/request/RequestModes.java index 5253bb2018d..72b06f62979 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/RequestModes.java +++ b/src/main/java/org/opentripplanner/routing/api/request/RequestModes.java @@ -2,12 +2,9 @@ import static org.opentripplanner.routing.api.request.StreetMode.NOT_SET; -import java.util.Collection; -import java.util.List; import java.util.Objects; import javax.annotation.Nonnull; import org.opentripplanner.framework.tostring.ToStringBuilder; -import org.opentripplanner.transit.model.basic.MainAndSubMode; public class RequestModes { @@ -39,10 +36,11 @@ private RequestModes( StreetMode directMode, StreetMode transferMode ) { - this.accessMode = (accessMode != null && accessMode.access) ? accessMode : NOT_SET; - this.egressMode = (egressMode != null && egressMode.egress) ? egressMode : NOT_SET; + this.accessMode = (accessMode != null && accessMode.accessAllowed()) ? accessMode : NOT_SET; + this.egressMode = (egressMode != null && egressMode.egressAllowed()) ? egressMode : NOT_SET; this.directMode = directMode != null ? directMode : NOT_SET; - this.transferMode = (transferMode != null && transferMode.transfer) ? transferMode : NOT_SET; + this.transferMode = + (transferMode != null && transferMode.transferAllowed()) ? transferMode : NOT_SET; } public RequestModes(RequestModesBuilder builder) { diff --git a/src/main/java/org/opentripplanner/routing/api/request/StreetMode.java b/src/main/java/org/opentripplanner/routing/api/request/StreetMode.java index 0f5d65305b3..47073aa7eeb 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/StreetMode.java +++ b/src/main/java/org/opentripplanner/routing/api/request/StreetMode.java @@ -1,5 +1,7 @@ package org.opentripplanner.routing.api.request; +import java.util.EnumSet; +import java.util.Set; import org.opentripplanner.framework.doc.DocumentedEnum; public enum StreetMode implements DocumentedEnum { @@ -7,126 +9,121 @@ public enum StreetMode implements DocumentedEnum { * No street mode is set. This option is used if we do not want street routing at all in this part * of the search. */ - NOT_SET(true, true, true, false, false, false, false, false, false), + NOT_SET(Feature.ACCESS, Feature.TRANSFER, Feature.EGRESS), /** * Walk only */ - WALK(true, true, true, true, false, false, false, false, false), + WALK(Feature.ACCESS, Feature.TRANSFER, Feature.EGRESS, Feature.WALKING), /** * Bike only */ - BIKE(true, true, true, false, true, false, false, false, false), + BIKE(Feature.ACCESS, Feature.TRANSFER, Feature.EGRESS, Feature.CYCLING), /** * Bike to a bike parking area, then walk the rest of the way. *

    * Direct mode and access mode only. */ - BIKE_TO_PARK(true, false, false, true, true, false, false, true, false), + BIKE_TO_PARK(Feature.ACCESS, Feature.WALKING, Feature.CYCLING, Feature.PARKING), /** * Walk to a bike rental point, bike to a bike rental drop-off point, and walk the rest of the * way. This can include bike rental at fixed locations or free-floating services. */ - BIKE_RENTAL(true, true, true, true, true, false, true, false, false), + BIKE_RENTAL(Feature.ACCESS, Feature.EGRESS, Feature.WALKING, Feature.CYCLING, Feature.RENTING), /** * Walk to a scooter rental point, ride a scooter to a scooter rental drop-off point, and walk the * rest of the way. This can include scooter rental at fixed locations or free-floating services. */ - SCOOTER_RENTAL(true, true, true, true, true, false, true, false, false), + SCOOTER_RENTAL(Feature.ACCESS, Feature.EGRESS, Feature.WALKING, Feature.SCOOTER, Feature.RENTING), /** * Car only *

    * Direct mode only. */ - CAR(true, false, false, false, false, true, false, false, false), + CAR(Feature.ACCESS, Feature.DRIVING), /** * Start in the car, drive to a parking area, and walk the rest of the way. *

    * Direct mode and access mode only. */ - CAR_TO_PARK(true, false, false, true, false, true, false, true, false), + CAR_TO_PARK(Feature.ACCESS, Feature.WALKING, Feature.DRIVING, Feature.PARKING), /** * Walk to a pickup point along the road, drive to a drop-off point along the road, and walk the * rest of the way. This can include various taxi-services or kiss & ride. */ - CAR_PICKUP(true, false, true, true, false, true, false, false, true), + CAR_PICKUP(Feature.ACCESS, Feature.EGRESS, Feature.WALKING, Feature.DRIVING, Feature.PICKUP), /** * Walk to a car rental point, drive to a car rental drop-off point and walk the rest of the way. * This can include car rental at fixed locations or free-floating services. */ - CAR_RENTAL(true, true, true, true, false, true, true, false, false), + CAR_RENTAL(Feature.ACCESS, Feature.EGRESS, Feature.WALKING, Feature.DRIVING, Feature.RENTING), /** * Using a car hailing app like Uber or Lyft to get to a train station or all the way to the destination. */ - CAR_HAILING(true, false, true, false, false, true, false, false, true), + CAR_HAILING(Feature.ACCESS, Feature.EGRESS, Feature.DRIVING, Feature.PICKUP), /** * Encompasses all types of on-demand and flexible transportation. */ - FLEXIBLE(true, false, true, true, false, false, false, false, false); - - final boolean access; - - final boolean transfer; - - final boolean egress; - - final boolean includesWalking; - - final boolean includesBiking; + FLEXIBLE(Feature.ACCESS, Feature.EGRESS, Feature.WALKING); + + private enum Feature { + ACCESS, + EGRESS, + TRANSFER, + WALKING, + CYCLING, + DRIVING, + SCOOTER, + RENTING, + PARKING, + PICKUP, + } - final boolean includesDriving; + private final Set features; - final boolean includesRenting; + StreetMode(Feature first, Feature... rest) { + this.features = EnumSet.of(first, rest); + } - final boolean includesParking; + public boolean accessAllowed() { + return features.contains(Feature.ACCESS); + } - final boolean includesPickup; + public boolean transferAllowed() { + return features.contains(Feature.TRANSFER); + } - StreetMode( - boolean access, - boolean transfer, - boolean egress, - boolean includesWalking, - boolean includesBiking, - boolean includesDriving, - boolean includesRenting, - boolean includesParking, - boolean includesPickup - ) { - this.access = access; - this.transfer = transfer; - this.egress = egress; - this.includesWalking = includesWalking; - this.includesBiking = includesBiking; - this.includesDriving = includesDriving; - this.includesRenting = includesRenting; - this.includesParking = includesParking; - this.includesPickup = includesPickup; + public boolean egressAllowed() { + return features.contains(Feature.EGRESS); } public boolean includesWalking() { - return includesWalking; + return features.contains(Feature.WALKING); } public boolean includesBiking() { - return includesBiking; + return features.contains(Feature.CYCLING); } public boolean includesDriving() { - return includesDriving; + return features.contains(Feature.DRIVING); + } + + public boolean includesScooter() { + return features.contains(Feature.SCOOTER); } public boolean includesRenting() { - return includesRenting; + return features.contains(Feature.RENTING); } public boolean includesParking() { - return includesParking; + return features.contains(Feature.PARKING); } public boolean includesPickup() { - return includesPickup; + return features.contains(Feature.PICKUP); } @Override diff --git a/src/main/java/org/opentripplanner/routing/api/request/framework/TimeAndCostPenaltyForEnum.java b/src/main/java/org/opentripplanner/routing/api/request/framework/TimeAndCostPenaltyForEnum.java index 60bd4d4c769..c45fc3e33a8 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/framework/TimeAndCostPenaltyForEnum.java +++ b/src/main/java/org/opentripplanner/routing/api/request/framework/TimeAndCostPenaltyForEnum.java @@ -81,6 +81,13 @@ private EnumMap copyValues() { return values.isEmpty() ? new EnumMap<>(type) : new EnumMap<>(values); } + /** + * Convert the values to an {@link EnumMap}. + */ + public EnumMap asEnumMap() { + return copyValues(); + } + private static > String toString( Class clazz, Map values diff --git a/src/main/java/org/opentripplanner/routing/api/request/preference/AccessEgressPreferences.java b/src/main/java/org/opentripplanner/routing/api/request/preference/AccessEgressPreferences.java index db8845e7c41..9998f0a3b1c 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/preference/AccessEgressPreferences.java +++ b/src/main/java/org/opentripplanner/routing/api/request/preference/AccessEgressPreferences.java @@ -7,12 +7,12 @@ import java.util.Map; import java.util.Objects; import java.util.function.Consumer; -import org.opentripplanner.framework.model.Units; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.api.request.framework.DurationForEnum; import org.opentripplanner.routing.api.request.framework.TimeAndCostPenalty; import org.opentripplanner.routing.api.request.framework.TimeAndCostPenaltyForEnum; +import org.opentripplanner.routing.api.request.framework.TimePenalty; /** * Preferences for access/egress routing on street network @@ -21,14 +21,31 @@ */ public final class AccessEgressPreferences implements Serializable { + private static final TimeAndCostPenalty DEFAULT_PENALTY = TimeAndCostPenalty.of( + TimePenalty.of(ofMinutes(20), 2f), + 1.5 + ); + private static final TimeAndCostPenalty FLEX_DEFAULT_PENALTY = TimeAndCostPenalty.of( + TimePenalty.of(ofMinutes(10), 1.3f), + 1.3 + ); + private static final TimeAndCostPenaltyForEnum DEFAULT_TIME_AND_COST = TimeAndCostPenaltyForEnum + .of(StreetMode.class) + .with(StreetMode.CAR_TO_PARK, DEFAULT_PENALTY) + .with(StreetMode.CAR_HAILING, DEFAULT_PENALTY) + .with(StreetMode.CAR_RENTAL, DEFAULT_PENALTY) + .with(StreetMode.FLEXIBLE, FLEX_DEFAULT_PENALTY) + .build(); + public static final AccessEgressPreferences DEFAULT = new AccessEgressPreferences(); + private final TimeAndCostPenaltyForEnum penalty; private final DurationForEnum maxDuration; private final int maxStopCount; private AccessEgressPreferences() { this.maxDuration = durationForStreetModeOf(ofMinutes(45)); - this.penalty = TimeAndCostPenaltyForEnum.ofDefault(StreetMode.class); + this.penalty = DEFAULT_TIME_AND_COST; this.maxStopCount = 500; } diff --git a/src/main/java/org/opentripplanner/routing/api/request/preference/BikePreferences.java b/src/main/java/org/opentripplanner/routing/api/request/preference/BikePreferences.java index 26da1476b38..5767b74eaa8 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/preference/BikePreferences.java +++ b/src/main/java/org/opentripplanner/routing/api/request/preference/BikePreferences.java @@ -2,8 +2,8 @@ import static org.opentripplanner.framework.lang.DoubleUtils.doubleEquals; import static org.opentripplanner.framework.lang.ObjectUtils.ifNotNull; -import static org.opentripplanner.routing.core.BicycleOptimizeType.SAFE_STREETS; -import static org.opentripplanner.routing.core.BicycleOptimizeType.TRIANGLE; +import static org.opentripplanner.routing.core.VehicleRoutingOptimizeType.SAFE_STREETS; +import static org.opentripplanner.routing.core.VehicleRoutingOptimizeType.TRIANGLE; import java.io.Serializable; import java.util.Objects; @@ -11,7 +11,7 @@ import org.opentripplanner.framework.model.Cost; import org.opentripplanner.framework.model.Units; import org.opentripplanner.framework.tostring.ToStringBuilder; -import org.opentripplanner.routing.core.BicycleOptimizeType; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; /** * The bike preferences contain all speed, reluctance, cost and factor preferences for biking @@ -29,7 +29,7 @@ public final class BikePreferences implements Serializable { private final Cost boardCost; private final VehicleParkingPreferences parking; private final VehicleRentalPreferences rental; - private final BicycleOptimizeType optimizeType; + private final VehicleRoutingOptimizeType optimizeType; private final TimeSlopeSafetyTriangle optimizeTriangle; private final VehicleWalkingPreferences walking; @@ -96,7 +96,7 @@ public VehicleRentalPreferences rental() { /** * The set of characteristics that the user wants to optimize for -- defaults to SAFE_STREETS. */ - public BicycleOptimizeType optimizeType() { + public VehicleRoutingOptimizeType optimizeType() { return optimizeType; } @@ -164,7 +164,7 @@ public static class Builder { private Cost boardCost; private VehicleParkingPreferences parking; private VehicleRentalPreferences rental; - private BicycleOptimizeType optimizeType; + private VehicleRoutingOptimizeType optimizeType; private TimeSlopeSafetyTriangle optimizeTriangle; private VehicleWalkingPreferences walking; @@ -221,11 +221,11 @@ public Builder withRental(Consumer body) { return this; } - public BicycleOptimizeType optimizeType() { + public VehicleRoutingOptimizeType optimizeType() { return optimizeType; } - public Builder withOptimizeType(BicycleOptimizeType optimizeType) { + public Builder withOptimizeType(VehicleRoutingOptimizeType optimizeType) { this.optimizeType = optimizeType; return this; } diff --git a/src/main/java/org/opentripplanner/routing/api/request/preference/RoutingPreferences.java b/src/main/java/org/opentripplanner/routing/api/request/preference/RoutingPreferences.java index d3d22160935..3fec7a9e46e 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/preference/RoutingPreferences.java +++ b/src/main/java/org/opentripplanner/routing/api/request/preference/RoutingPreferences.java @@ -7,6 +7,7 @@ import java.util.Objects; import java.util.function.Consumer; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.street.search.TraverseMode; @@ -23,6 +24,7 @@ public final class RoutingPreferences implements Serializable { private final WheelchairPreferences wheelchair; private final BikePreferences bike; private final CarPreferences car; + private final ScooterPreferences scooter; private final SystemPreferences system; private final ItineraryFilterPreferences itineraryFilter; @@ -34,6 +36,7 @@ public RoutingPreferences() { this.wheelchair = WheelchairPreferences.DEFAULT; this.bike = BikePreferences.DEFAULT; this.car = CarPreferences.DEFAULT; + this.scooter = ScooterPreferences.DEFAULT; this.system = SystemPreferences.DEFAULT; this.itineraryFilter = ItineraryFilterPreferences.DEFAULT; } @@ -46,6 +49,7 @@ private RoutingPreferences(Builder builder) { this.street = requireNonNull(builder.street()); this.bike = requireNonNull(builder.bike()); this.car = requireNonNull(builder.car()); + this.scooter = requireNonNull(builder.scooter()); this.system = requireNonNull(builder.system()); this.itineraryFilter = requireNonNull(builder.itineraryFilter()); } @@ -90,6 +94,10 @@ public CarPreferences car() { return car; } + public ScooterPreferences scooter() { + return scooter; + } + /** * Get parking preferences for the traverse mode. Note, only car and bike are supported. */ @@ -99,20 +107,28 @@ public VehicleParkingPreferences parking(TraverseMode mode) { /** * Get rental preferences for the traverse mode. Note, only car, scooter and bike are supported. - * - * TODO make scooter preferences independent of bike */ + @Nonnull public VehicleRentalPreferences rental(TraverseMode mode) { - return mode == TraverseMode.CAR ? car.rental() : bike.rental(); + return switch (mode) { + case BICYCLE -> bike.rental(); + case CAR -> car.rental(); + case SCOOTER -> scooter.rental(); + default -> throw new IllegalArgumentException("rental(): Invalid mode " + mode); + }; } /** * Get rental preferences for the traverse mode. Note, only car, scooter and bike are supported. - * - * TODO make scooter preferences independent of bike */ + @Nullable public VehicleRentalPreferences rental(StreetMode mode) { - return mode == StreetMode.CAR_RENTAL ? car.rental() : bike.rental(); + return switch (mode) { + case BIKE_RENTAL -> bike.rental(); + case CAR_RENTAL -> car.rental(); + case SCOOTER_RENTAL -> scooter.rental(); + default -> null; + }; } @Nonnull @@ -126,12 +142,15 @@ public SystemPreferences system() { /** * The road speed for a specific traverse mode. + * + * NOTE, this is only used for tests and doesn't support scooter walking */ public double getSpeed(TraverseMode mode, boolean walkingBike) { return switch (mode) { case WALK -> walkingBike ? bike.walking().speed() : walk.speed(); case BICYCLE -> bike.speed(); case CAR -> car.speed(); + case SCOOTER -> scooter.speed(); default -> throw new IllegalArgumentException("getSpeed(): Invalid mode " + mode); }; } @@ -149,6 +168,7 @@ public boolean equals(Object o) { Objects.equals(wheelchair, that.wheelchair) && Objects.equals(bike, that.bike) && Objects.equals(car, that.car) && + Objects.equals(scooter, that.scooter) && Objects.equals(system, that.system) && Objects.equals(itineraryFilter, that.itineraryFilter) ); @@ -164,6 +184,7 @@ public int hashCode() { wheelchair, bike, car, + scooter, system, itineraryFilter ); @@ -179,6 +200,7 @@ public static class Builder { private WheelchairPreferences wheelchair = null; private BikePreferences bike = null; private CarPreferences car = null; + private ScooterPreferences scooter = null; private SystemPreferences system = null; private ItineraryFilterPreferences itineraryFilter = null; @@ -259,6 +281,15 @@ public Builder withCar(Consumer body) { return this; } + public ScooterPreferences scooter() { + return scooter == null ? original.scooter : scooter; + } + + public Builder withScooter(Consumer body) { + this.scooter = ifNotNull(this.scooter, original.scooter).copyOf().apply(body).build(); + return this; + } + public SystemPreferences system() { return system == null ? original.system : system; } diff --git a/src/main/java/org/opentripplanner/routing/api/request/preference/ScooterPreferences.java b/src/main/java/org/opentripplanner/routing/api/request/preference/ScooterPreferences.java new file mode 100644 index 00000000000..1d5cf83a54b --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/api/request/preference/ScooterPreferences.java @@ -0,0 +1,203 @@ +package org.opentripplanner.routing.api.request.preference; + +import static org.opentripplanner.framework.lang.DoubleUtils.doubleEquals; +import static org.opentripplanner.framework.lang.ObjectUtils.ifNotNull; +import static org.opentripplanner.routing.core.VehicleRoutingOptimizeType.SAFE_STREETS; +import static org.opentripplanner.routing.core.VehicleRoutingOptimizeType.TRIANGLE; + +import java.io.Serializable; +import java.util.Objects; +import java.util.function.Consumer; +import org.opentripplanner.framework.model.Units; +import org.opentripplanner.framework.tostring.ToStringBuilder; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; + +/** + * The scooter preferences contain all speed, reluctance, cost and factor preferences for scooter + * related to street and transit routing. The values are normalized(rounded) so the class can used + * as a cache key. + * + * Only Scooter rental is supported currently. + *

    + * THIS CLASS IS IMMUTABLE AND THREAD-SAFE. + */ +public final class ScooterPreferences implements Serializable { + + public static final ScooterPreferences DEFAULT = new ScooterPreferences(); + + private final double speed; + private final double reluctance; + private final VehicleRentalPreferences rental; + private final VehicleRoutingOptimizeType optimizeType; + private final TimeSlopeSafetyTriangle optimizeTriangle; + + private ScooterPreferences() { + this.speed = 5; + this.reluctance = 2.0; + this.rental = VehicleRentalPreferences.DEFAULT; + this.optimizeType = SAFE_STREETS; + this.optimizeTriangle = TimeSlopeSafetyTriangle.DEFAULT; + } + + private ScooterPreferences(Builder builder) { + this.speed = Units.speed(builder.speed); + this.reluctance = Units.reluctance(builder.reluctance); + this.rental = builder.rental; + this.optimizeType = Objects.requireNonNull(builder.optimizeType); + this.optimizeTriangle = Objects.requireNonNull(builder.optimizeTriangle); + } + + public static ScooterPreferences.Builder of() { + return new Builder(DEFAULT); + } + + public ScooterPreferences.Builder copyOf() { + return new Builder(this); + } + + /** + * Default: 5 m/s, ~11 mph, a random scooter speed + */ + public double speed() { + return speed; + } + + public double reluctance() { + return reluctance; + } + + /** Rental preferences that can be different per request */ + public VehicleRentalPreferences rental() { + return rental; + } + + /** + * The set of characteristics that the user wants to optimize for -- defaults to SAFE_STREETS. + */ + public VehicleRoutingOptimizeType optimizeType() { + return optimizeType; + } + + public TimeSlopeSafetyTriangle optimizeTriangle() { + return optimizeTriangle; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ScooterPreferences that = (ScooterPreferences) o; + return ( + doubleEquals(that.speed, speed) && + doubleEquals(that.reluctance, reluctance) && + Objects.equals(rental, that.rental) && + optimizeType == that.optimizeType && + optimizeTriangle.equals(that.optimizeTriangle) + ); + } + + @Override + public int hashCode() { + return Objects.hash(speed, reluctance, rental, optimizeType, optimizeTriangle); + } + + @Override + public String toString() { + return ToStringBuilder + .of(ScooterPreferences.class) + .addNum("speed", speed, DEFAULT.speed) + .addNum("reluctance", reluctance, DEFAULT.reluctance) + .addObj("rental", rental, DEFAULT.rental) + .addEnum("optimizeType", optimizeType, DEFAULT.optimizeType) + .addObj("optimizeTriangle", optimizeTriangle, DEFAULT.optimizeTriangle) + .toString(); + } + + @SuppressWarnings("UnusedReturnValue") + public static class Builder { + + private final ScooterPreferences original; + private double speed; + private double reluctance; + private VehicleRentalPreferences rental; + private VehicleRoutingOptimizeType optimizeType; + private TimeSlopeSafetyTriangle optimizeTriangle; + + public Builder(ScooterPreferences original) { + this.original = original; + this.speed = original.speed; + this.reluctance = original.reluctance; + this.rental = original.rental; + this.optimizeType = original.optimizeType; + this.optimizeTriangle = original.optimizeTriangle; + } + + public ScooterPreferences original() { + return original; + } + + public double speed() { + return speed; + } + + public Builder withSpeed(double speed) { + this.speed = speed; + return this; + } + + public double reluctance() { + return reluctance; + } + + public Builder withReluctance(double reluctance) { + this.reluctance = reluctance; + return this; + } + + public Builder withRental(Consumer body) { + this.rental = ifNotNull(this.rental, original.rental).copyOf().apply(body).build(); + return this; + } + + public VehicleRoutingOptimizeType optimizeType() { + return optimizeType; + } + + public Builder withOptimizeType(VehicleRoutingOptimizeType optimizeType) { + this.optimizeType = optimizeType; + return this; + } + + public TimeSlopeSafetyTriangle optimizeTriangle() { + return optimizeTriangle; + } + + /** This also sets the optimization type as TRIANGLE if triangle parameters are defined */ + public Builder withForcedOptimizeTriangle(Consumer body) { + var builder = TimeSlopeSafetyTriangle.of(); + body.accept(builder); + this.optimizeTriangle = builder.buildOrDefault(this.optimizeTriangle); + if (!builder.isEmpty()) { + this.optimizeType = TRIANGLE; + } + return this; + } + + public Builder withOptimizeTriangle(Consumer body) { + var builder = TimeSlopeSafetyTriangle.of(); + body.accept(builder); + this.optimizeTriangle = builder.buildOrDefault(this.optimizeTriangle); + return this; + } + + public Builder apply(Consumer body) { + body.accept(this); + return this; + } + + public ScooterPreferences build() { + var value = new ScooterPreferences(this); + return original.equals(value) ? original : value; + } + } +} diff --git a/src/main/java/org/opentripplanner/routing/api/request/preference/TimeSlopeSafetyTriangle.java b/src/main/java/org/opentripplanner/routing/api/request/preference/TimeSlopeSafetyTriangle.java index 99925a441fd..ae4f1039f32 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/preference/TimeSlopeSafetyTriangle.java +++ b/src/main/java/org/opentripplanner/routing/api/request/preference/TimeSlopeSafetyTriangle.java @@ -4,13 +4,14 @@ import static org.opentripplanner.framework.lang.DoubleUtils.roundTo2Decimals; /** - * Sets the (bicycle) triangle routing parameters -- the relative importance of safety, flatness, - * and speed. These three fields should have values between 0 and 1, and should add up to 1. + * Sets the (bicycle or scooter) triangle routing parameters -- the relative importance of safety, + * flatness, and speed. These three fields should have values between 0 and 1, and should add up to + * 1. *

    * The constructor accepts any three numbers and will normalize them to add up to 1. {@code time} * and {@code slope} are rounded to the closest two decimal number, then - * {@code safety := 1.0 - (time + slope)}. This is done to make the rounding predictable and - * to allways add up to one. This allows this class to be used in an index of a cache. For example: + * {@code safety := 1.0 - (time + slope)}. This is done to make the rounding predictable and to + * allways add up to one. This allows this class to be used in an index of a cache. For example: *

      *   ( 1.0, 1.0, 1.0 ) => ( time: 0.33, slope: 0.33, safety: 0.34 )
      * 
    @@ -25,10 +26,10 @@ public record TimeSlopeSafetyTriangle(double time, double slope, double safety) public static final TimeSlopeSafetyTriangle DEFAULT = new TimeSlopeSafetyTriangle(1, 1, 1); /** - * Sets the bicycle triangle routing parameters -- the relative importance of safety, flatness, - * and speed. These three fields of the RouteRequest should have values between 0 and 1, and - * should add up to 1. This setter function accepts any three numbers and will normalize them to - * add up to 1. + * Sets the bicycle or scooter triangle routing parameters -- the relative importance of safety, + * flatness, and speed. These three fields of the RouteRequest should have values between 0 and 1, + * and should add up to 1. This setter function accepts any three numbers and will normalize them + * to add up to 1. */ public TimeSlopeSafetyTriangle(double time, double slope, double safety) { safety = positiveValueOrZero(safety); diff --git a/src/main/java/org/opentripplanner/routing/api/request/preference/VehicleWalkingPreferences.java b/src/main/java/org/opentripplanner/routing/api/request/preference/VehicleWalkingPreferences.java index aa3631e1c6b..b7adc04df1c 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/preference/VehicleWalkingPreferences.java +++ b/src/main/java/org/opentripplanner/routing/api/request/preference/VehicleWalkingPreferences.java @@ -19,15 +19,15 @@ public class VehicleWalkingPreferences implements Serializable { private final double speed; private final double reluctance; - private final Duration hopTime; - private final Cost hopCost; + private final Duration mountDismountTime; + private final Cost mountDismountCost; private final double stairsReluctance; private VehicleWalkingPreferences() { this.speed = 1.33; this.reluctance = 5.0; - this.hopTime = Duration.ZERO; - this.hopCost = Cost.ZERO; + this.mountDismountTime = Duration.ZERO; + this.mountDismountCost = Cost.ZERO; // very high reluctance to carry the bike up/down a flight of stairs this.stairsReluctance = 10; } @@ -39,8 +39,8 @@ private VehicleWalkingPreferences() { private VehicleWalkingPreferences(Builder builder) { this.speed = Units.speed(builder.speed); this.reluctance = Units.reluctance(builder.reluctance); - this.hopTime = Duration.ofSeconds(Units.duration(builder.hopTime)); - this.hopCost = Cost.costOfSeconds(builder.hopCost); + this.mountDismountTime = Duration.ofSeconds(Units.duration(builder.mountDismountTime)); + this.mountDismountCost = Cost.costOfSeconds(builder.mountDismountCost); this.stairsReluctance = Units.reluctance(builder.stairsReluctance); } @@ -73,13 +73,13 @@ public double reluctance() { } /** Time to get on and off your own vehicle. */ - public Duration hopTime() { - return hopTime; + public Duration mountDismountTime() { + return mountDismountTime; } /** Cost of getting on and off your own vehicle. */ - public Cost hopCost() { - return hopCost; + public Cost mountDismountCost() { + return mountDismountCost; } /** Reluctance of walking carrying a vehicle up a flight of stairs. */ @@ -95,15 +95,15 @@ public boolean equals(Object o) { return ( speed == that.speed && reluctance == that.reluctance && - Objects.equals(hopTime, that.hopTime) && - Objects.equals(hopCost, that.hopCost) && + Objects.equals(mountDismountTime, that.mountDismountTime) && + Objects.equals(mountDismountCost, that.mountDismountCost) && stairsReluctance == that.stairsReluctance ); } @Override public int hashCode() { - return Objects.hash(speed, reluctance, hopTime, hopCost, stairsReluctance); + return Objects.hash(speed, reluctance, mountDismountTime, mountDismountCost, stairsReluctance); } @Override @@ -112,8 +112,8 @@ public String toString() { .of(VehicleWalkingPreferences.class) .addNum("speed", speed, DEFAULT.speed) .addNum("reluctance", reluctance, DEFAULT.reluctance) - .addObj("hopTime", hopTime, DEFAULT.hopTime) - .addObj("hopCost", hopCost, DEFAULT.hopCost) + .addObj("mountDismountTime", mountDismountTime, DEFAULT.mountDismountTime) + .addObj("mountDismountCost", mountDismountCost, DEFAULT.mountDismountCost) .addNum("stairsReluctance", stairsReluctance, DEFAULT.stairsReluctance) .toString(); } @@ -123,16 +123,16 @@ public static class Builder { private final VehicleWalkingPreferences original; private double speed; private double reluctance; - private int hopTime; - private int hopCost; + private int mountDismountTime; + private int mountDismountCost; private double stairsReluctance; private Builder(VehicleWalkingPreferences original) { this.original = original; this.speed = original.speed; this.reluctance = original.reluctance; - this.hopTime = (int) original.hopTime.toSeconds(); - this.hopCost = original.hopCost.toSeconds(); + this.mountDismountTime = (int) original.mountDismountTime.toSeconds(); + this.mountDismountCost = original.mountDismountCost.toSeconds(); this.stairsReluctance = original.stairsReluctance; } @@ -146,18 +146,18 @@ public VehicleWalkingPreferences.Builder withReluctance(double reluctance) { return this; } - public VehicleWalkingPreferences.Builder withHopTime(Duration hopTime) { - this.hopTime = (int) hopTime.toSeconds(); + public VehicleWalkingPreferences.Builder withMountDismountTime(Duration mountDismountTime) { + this.mountDismountTime = (int) mountDismountTime.toSeconds(); return this; } - public VehicleWalkingPreferences.Builder withHopTime(int hopTime) { - this.hopTime = hopTime; + public VehicleWalkingPreferences.Builder withMountDismountTime(int mountDismountTime) { + this.mountDismountTime = mountDismountTime; return this; } - public VehicleWalkingPreferences.Builder withHopCost(int hopCost) { - this.hopCost = hopCost; + public VehicleWalkingPreferences.Builder withMountDismountCost(int mountDismountCost) { + this.mountDismountCost = mountDismountCost; return this; } diff --git a/src/main/java/org/opentripplanner/routing/core/BicycleOptimizeType.java b/src/main/java/org/opentripplanner/routing/core/VehicleRoutingOptimizeType.java similarity index 52% rename from src/main/java/org/opentripplanner/routing/core/BicycleOptimizeType.java rename to src/main/java/org/opentripplanner/routing/core/VehicleRoutingOptimizeType.java index 1d639e0af8b..cb8436b83b3 100644 --- a/src/main/java/org/opentripplanner/routing/core/BicycleOptimizeType.java +++ b/src/main/java/org/opentripplanner/routing/core/VehicleRoutingOptimizeType.java @@ -5,10 +5,10 @@ import java.util.Set; /** - * When planning a bicycle route what should be optimized for. Optimize types are basically - * combined presets of routing parameters, except for triangle. + * When planning a bicycle or scooter route what should be optimized for. Optimize types are + * basically combined presets of routing parameters, except for triangle. */ -public enum BicycleOptimizeType { +public enum VehicleRoutingOptimizeType { /** This was previously called QUICK */ SHORTEST_DURATION, /** This was previously called SAFE */ @@ -19,14 +19,14 @@ public enum BicycleOptimizeType { SAFEST_STREETS, TRIANGLE; - private static final Set NON_TRIANGLE_VALUES = Collections.unmodifiableSet( + private static final Set NON_TRIANGLE_VALUES = Collections.unmodifiableSet( EnumSet.complementOf(EnumSet.of(TRIANGLE)) ); /** - * Return all values that are not {@link BicycleOptimizeType#TRIANGLE}. + * Return all values that are not {@link VehicleRoutingOptimizeType#TRIANGLE}. */ - public static Set nonTriangleValues() { + public static Set nonTriangleValues() { return NON_TRIANGLE_VALUES; } } diff --git a/src/main/java/org/opentripplanner/routing/linking/FlexLocationAdder.java b/src/main/java/org/opentripplanner/routing/linking/FlexLocationAdder.java index 830a80c39a5..047e44f229a 100644 --- a/src/main/java/org/opentripplanner/routing/linking/FlexLocationAdder.java +++ b/src/main/java/org/opentripplanner/routing/linking/FlexLocationAdder.java @@ -16,7 +16,7 @@ static void addFlexLocations(StreetEdge edge, IntersectionVertex v0, StopModel s if (edge.getPermission().allows(StreetTraversalPermission.PEDESTRIAN_AND_CAR)) { Point p = GeometryUtils.getGeometryFactory().createPoint(v0.getCoordinate()); Envelope env = p.getEnvelopeInternal(); - for (AreaStop areaStop : stopModel.queryLocationIndex(env)) { + for (AreaStop areaStop : stopModel.findAreaStops(env)) { if (!areaStop.getGeometry().disjoint(p)) { v0.addAreaStops(Set.of(areaStop)); } diff --git a/src/main/java/org/opentripplanner/service/vehiclerental/street/StreetVehicleRentalLink.java b/src/main/java/org/opentripplanner/service/vehiclerental/street/StreetVehicleRentalLink.java index 4a18aae03c2..385b347d24a 100644 --- a/src/main/java/org/opentripplanner/service/vehiclerental/street/StreetVehicleRentalLink.java +++ b/src/main/java/org/opentripplanner/service/vehiclerental/street/StreetVehicleRentalLink.java @@ -53,7 +53,10 @@ public State[] traverse(State s0) { } var preferences = s0.getPreferences().rental(s0.getRequest().mode()); - if (vehicleRentalPlaceVertex.getStation().networkIsNotAllowed(preferences)) { + // preferences will be null while finding nearest places with WALK mode + if ( + preferences != null && vehicleRentalPlaceVertex.getStation().networkIsNotAllowed(preferences) + ) { return State.empty(); } diff --git a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index fa3a7069e2d..bbc6733a40c 100644 --- a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -114,7 +114,7 @@ public interface OtpServerRequestContext { TraverseVisitor traverseVisitor(); default GraphFinder graphFinder() { - return GraphFinder.getInstance(graph(), transitService()::findRegularStop); + return GraphFinder.getInstance(graph(), transitService()::findRegularStops); } FlexConfig flexConfig(); diff --git a/src/main/java/org/opentripplanner/standalone/config/framework/json/EnumMapper.java b/src/main/java/org/opentripplanner/standalone/config/framework/json/EnumMapper.java index ce880058005..0e048a2e3e7 100644 --- a/src/main/java/org/opentripplanner/standalone/config/framework/json/EnumMapper.java +++ b/src/main/java/org/opentripplanner/standalone/config/framework/json/EnumMapper.java @@ -3,6 +3,7 @@ import java.util.Arrays; import java.util.Optional; import org.opentripplanner.framework.doc.DocumentedEnum; +import org.opentripplanner.framework.lang.StringUtils; public class EnumMapper { @@ -23,11 +24,7 @@ public static Optional> mapToEnum2(String text, Class en) { - return kebabCase(en.name()); - } - - public static String kebabCase(String input) { - return input.toLowerCase().replace('_', '-'); + return StringUtils.kebabCase(en.name()); } /** diff --git a/src/main/java/org/opentripplanner/standalone/config/framework/json/NodeInfo.java b/src/main/java/org/opentripplanner/standalone/config/framework/json/NodeInfo.java index 296c07805f9..06675475fd1 100644 --- a/src/main/java/org/opentripplanner/standalone/config/framework/json/NodeInfo.java +++ b/src/main/java/org/opentripplanner/standalone/config/framework/json/NodeInfo.java @@ -7,6 +7,7 @@ import java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.opentripplanner.framework.lang.StringUtils; import org.opentripplanner.framework.tostring.ValueObjectToStringBuilder; /** @@ -137,7 +138,7 @@ public List> enumTypeValues() { */ public String toMarkdownString(Object value) { if (enumType != null) { - value = EnumMapper.kebabCase(value.toString()); + value = StringUtils.kebabCase(value.toString()); } return type.quote(value); } diff --git a/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java b/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java index f1d90d0ec40..088a7794585 100644 --- a/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java +++ b/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java @@ -303,14 +303,15 @@ public > Map asEnumMap(Class enumType, Class el */ public > Map asEnumMap( Class enumType, - Function typeMapper + Function typeMapper, + Map defaultValue ) { info.withOptional().withEnumMap(enumType, OBJECT); var mapNode = buildObject(); if (mapNode.isEmpty()) { - return Map.of(); + return defaultValue; } EnumMap result = new EnumMap<>(enumType); diff --git a/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java index 6f7d6967ce8..d6beac6d256 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java @@ -18,18 +18,23 @@ public class VectorTileConfig implements VectorTilesResource.LayersParameters { - public static final VectorTileConfig DEFAULT = new VectorTileConfig(List.of(), null); + public static final VectorTileConfig DEFAULT = new VectorTileConfig(List.of(), null, null); private final List> layers; @Nullable private final String basePath; + @Nullable + private final String attribution; + VectorTileConfig( Collection> layers, - @Nullable String basePath + @Nullable String basePath, + @Nullable String attribution ) { this.layers = List.copyOf(layers); this.basePath = basePath; + this.attribution = attribution; } @Override @@ -41,6 +46,10 @@ public Optional basePath() { return Optional.ofNullable(basePath); } + public Optional attribution() { + return Optional.ofNullable(attribution); + } + public static VectorTileConfig mapVectorTilesParameters(NodeAdapter node, String paramName) { var root = node.of(paramName).summary("Vector tile configuration").asObject(); return new VectorTileConfig( @@ -71,7 +80,23 @@ public static VectorTileConfig mapVectorTilesParameters(NodeAdapter node, String is expected to be handled by a proxy. """ ) - .asString(DEFAULT.basePath) + .asString(DEFAULT.basePath), + root + .of("attribution") + .since(V2_5) + .summary("Custom attribution to be returned in `tilejson.json`") + .description( + """ + By default the, `attribution` property in `tilejson.json` is computed from the names and + URLs of the feed publishers. + If the OTP deployment contains many fields, this can become very unwieldy. + + This configuration parameter allows you to set the `attribution` to any string you wish + including HTML tags, + for example `Regional Partners`. + """ + ) + .asString(DEFAULT.attribution) ); } diff --git a/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java index 4538f6de84d..19a494a3024 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java @@ -15,15 +15,20 @@ import static org.opentripplanner.standalone.config.routerequest.WheelchairConfig.mapWheelchairPreferences; import java.time.Duration; +import java.util.List; +import java.util.stream.Collectors; import org.opentripplanner.api.parameter.QualifiedModeSet; import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.framework.lang.StringUtils; import org.opentripplanner.routing.api.request.RequestModes; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.api.request.framework.CostLinearFunction; +import org.opentripplanner.routing.api.request.preference.AccessEgressPreferences; import org.opentripplanner.routing.api.request.preference.BikePreferences; import org.opentripplanner.routing.api.request.preference.CarPreferences; import org.opentripplanner.routing.api.request.preference.RoutingPreferences; +import org.opentripplanner.routing.api.request.preference.ScooterPreferences; import org.opentripplanner.routing.api.request.preference.StreetPreferences; import org.opentripplanner.routing.api.request.preference.SystemPreferences; import org.opentripplanner.routing.api.request.preference.TransitPreferences; @@ -181,6 +186,7 @@ private static void mapPreferences(NodeAdapter c, RoutingPreferences.Builder pre preferences.withBike(it -> mapBikePreferences(c, it)); preferences.withStreet(it -> mapStreetPreferences(c, it)); preferences.withCar(it -> mapCarPreferences(c, it)); + preferences.withScooter(it -> mapScooterPreferences(c, it)); preferences.withSystem(it -> mapSystemPreferences(c, it)); preferences.withTransfer(it -> mapTransferPreferences(c, it)); preferences.withWalk(it -> mapWalkPreferences(c, it)); @@ -455,7 +461,9 @@ private static void mapStreetPreferences(NodeAdapter c, StreetPreferences.Builde the access legs used. In other cases where the access(CAR) is faster than transit the performance will be better. - The default is no penalty, if not configured. + The default values are + + %s Example: `"car-to-park" : { "timePenalty": "10m + 1.5t", "costFactor": 2.5 }` @@ -470,9 +478,15 @@ the access legs used. In other cases where the access(CAR) is faster than transi The `costFactor` is used to add an additional cost to the leg´s generalized-cost. The time-penalty is multiplied with the cost-factor. A cost-factor of zero, gives no extra cost, while 1.0 will add the same amount to both time and cost. - """ + """.formatted( + formatPenaltyDefaultValues(dftAccessEgress) + ) + ) + .asEnumMap( + StreetMode.class, + TimeAndCostPenaltyMapper::map, + dftAccessEgress.penalty().asEnumMap() ) - .asEnumMap(StreetMode.class, TimeAndCostPenaltyMapper::map) ) .withMaxDuration( cae @@ -569,6 +583,16 @@ The street search(AStar) aborts after this duration and any paths found are retu ); } + private static String formatPenaltyDefaultValues(AccessEgressPreferences dftAccessEgress) { + return dftAccessEgress + .penalty() + .asEnumMap() + .entrySet() + .stream() + .map(s -> "- `%s` = %s".formatted(StringUtils.kebabCase(s.getKey().toString()), s.getValue())) + .collect(Collectors.joining("\n")); + } + private static void mapCarPreferences(NodeAdapter root, CarPreferences.Builder builder) { var dft = builder.original(); NodeAdapter c = root.of("car").since(V2_5).summary("Car preferences.").asObject(); @@ -621,6 +645,41 @@ private static void mapCarPreferences(NodeAdapter root, CarPreferences.Builder b .withRental(it -> mapRental(c, it)); } + private static void mapScooterPreferences(NodeAdapter root, ScooterPreferences.Builder builder) { + var dft = builder.original(); + NodeAdapter c = root.of("scooter").since(V2_5).summary("Scooter preferences.").asObject(); + builder + .withSpeed( + c + .of("speed") + .since(V2_0) + .summary("Max scooter speed along streets, in meters per second") + .asDouble(dft.speed()) + ) + .withReluctance( + c + .of("reluctance") + .since(V2_0) + .summary( + "A multiplier for how bad scooter travel is, compared to being in transit for equal lengths of time." + ) + .asDouble(dft.reluctance()) + ) + .withOptimizeType( + c + .of("optimization") + .since(V2_0) + .summary("The set of characteristics that the user wants to optimize for.") + .description( + "If the triangle optimization is used, it's enough to just define the triangle parameters" + ) + .asEnum(dft.optimizeType()) + ) + // triangle overrides the optimization type if defined + .withForcedOptimizeTriangle(it -> mapOptimizationTriangle(c, it)) + .withRental(it -> mapRental(c, it)); + } + private static void mapSystemPreferences(NodeAdapter c, SystemPreferences.Builder builder) { var dft = builder.original(); builder diff --git a/src/main/java/org/opentripplanner/standalone/config/routerequest/VehicleWalkingConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerequest/VehicleWalkingConfig.java index f2ba922c8e3..2b20e12f755 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerequest/VehicleWalkingConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerequest/VehicleWalkingConfig.java @@ -43,9 +43,9 @@ private static void mapVehicleWalkingPreferences( ) .asDouble(dft.reluctance()) ) - .withHopTime( + .withMountDismountTime( c - .of("hopTime") + .of("mountDismountTime") .since(V2_0) .summary("The time it takes the user to hop on or off a vehicle.") .description( @@ -54,11 +54,11 @@ private static void mapVehicleWalkingPreferences( for controlling the duration of those events. """ ) - .asDuration(dft.hopTime()) + .asDuration(dft.mountDismountTime()) ) - .withHopCost( + .withMountDismountCost( c - .of("hopCost") + .of("mountDismountCost") .since(V2_0) .summary("The cost of hopping on or off a vehicle.") .description( @@ -67,7 +67,7 @@ private static void mapVehicleWalkingPreferences( not meant for controlling the cost of those events. """ ) - .asInt(dft.hopCost().toSeconds()) + .asInt(dft.mountDismountCost().toSeconds()) ) .withStairsReluctance( c diff --git a/src/main/java/org/opentripplanner/street/model/edge/BikeWalkableEdge.java b/src/main/java/org/opentripplanner/street/model/edge/BikeWalkableEdge.java index 799a5b006e6..7f61982434d 100644 --- a/src/main/java/org/opentripplanner/street/model/edge/BikeWalkableEdge.java +++ b/src/main/java/org/opentripplanner/street/model/edge/BikeWalkableEdge.java @@ -17,8 +17,10 @@ default void switchToWalkingBike(RoutingPreferences preferences, StateEditor edi editor.setBackWalkingBike(true); if (shouldIncludeCost) { - editor.incrementWeight(preferences.bike().walking().hopCost().toSeconds()); - editor.incrementTimeInSeconds((int) preferences.bike().walking().hopTime().toSeconds()); + editor.incrementWeight(preferences.bike().walking().mountDismountCost().toSeconds()); + editor.incrementTimeInSeconds( + (int) preferences.bike().walking().mountDismountTime().toSeconds() + ); } } @@ -28,8 +30,10 @@ default void switchToBiking(RoutingPreferences preferences, StateEditor editor) editor.setBackWalkingBike(false); if (shouldIncludeCost) { - editor.incrementWeight(preferences.bike().walking().hopCost().toSeconds()); - editor.incrementTimeInSeconds((int) preferences.bike().walking().hopTime().toSeconds()); + editor.incrementWeight(preferences.bike().walking().mountDismountCost().toSeconds()); + editor.incrementTimeInSeconds( + (int) preferences.bike().walking().mountDismountTime().toSeconds() + ); } } diff --git a/src/main/java/org/opentripplanner/street/model/edge/StreetEdge.java b/src/main/java/org/opentripplanner/street/model/edge/StreetEdge.java index 1e3594ee04b..ca33396b0a3 100644 --- a/src/main/java/org/opentripplanner/street/model/edge/StreetEdge.java +++ b/src/main/java/org/opentripplanner/street/model/edge/StreetEdge.java @@ -234,8 +234,9 @@ public double calculateSpeed( case WALK -> walkingBike ? preferences.bike().walking().speed() : preferences.walk().speed(); - case BICYCLE, SCOOTER -> preferences.bike().speed(); + case BICYCLE -> preferences.bike().speed(); case CAR -> getCarSpeed(); + case SCOOTER -> preferences.scooter().speed(); case FLEX -> throw new IllegalArgumentException("getSpeed(): Invalid mode " + traverseMode); }; @@ -1098,7 +1099,7 @@ private StateEditor doTraverse(State s0, TraverseMode traverseMode, boolean walk var traversalCosts = switch (traverseMode) { - case BICYCLE, SCOOTER -> bicycleTraversalCost(preferences, speed); + case BICYCLE, SCOOTER -> bicycleOrScooterTraversalCost(preferences, traverseMode, speed); case WALK -> walkingTraversalCosts( preferences, traverseMode, @@ -1213,10 +1214,17 @@ private TraversalCosts otherTraversalCosts( } @Nonnull - private TraversalCosts bicycleTraversalCost(RoutingPreferences pref, double speed) { + private TraversalCosts bicycleOrScooterTraversalCost( + RoutingPreferences pref, + TraverseMode mode, + double speed + ) { double time = getEffectiveBikeDistance() / speed; double weight; - switch (pref.bike().optimizeType()) { + var optimizeType = mode == TraverseMode.BICYCLE + ? pref.bike().optimizeType() + : pref.scooter().optimizeType(); + switch (optimizeType) { case SAFEST_STREETS -> { weight = bicycleSafetyFactor * getDistanceMeters() / speed; if (bicycleSafetyFactor <= SAFEST_STREETS_SAFETY_FACTOR) { @@ -1232,20 +1240,17 @@ private TraversalCosts bicycleTraversalCost(RoutingPreferences pref, double spee double quick = getEffectiveBikeDistance(); double safety = getEffectiveBicycleSafetyDistance(); double slope = getEffectiveBikeDistanceForWorkCost(); - weight = - quick * - pref.bike().optimizeTriangle().time() + - slope * - pref.bike().optimizeTriangle().slope() + - safety * - pref.bike().optimizeTriangle().safety(); + var triangle = mode == TraverseMode.BICYCLE + ? pref.bike().optimizeTriangle() + : pref.scooter().optimizeTriangle(); + weight = quick * triangle.time() + slope * triangle.slope() + safety * triangle.safety(); weight /= speed; } default -> weight = getDistanceMeters() / speed; } var reluctance = StreetEdgeReluctanceCalculator.computeReluctance( pref, - TraverseMode.BICYCLE, + mode, false, isStairs() ); diff --git a/src/main/java/org/opentripplanner/street/model/edge/StreetEdgeReluctanceCalculator.java b/src/main/java/org/opentripplanner/street/model/edge/StreetEdgeReluctanceCalculator.java index ee2f3d833ee..a6fdf4ce6f1 100644 --- a/src/main/java/org/opentripplanner/street/model/edge/StreetEdgeReluctanceCalculator.java +++ b/src/main/java/org/opentripplanner/street/model/edge/StreetEdgeReluctanceCalculator.java @@ -25,6 +25,7 @@ static double computeReluctance( case WALK -> walkingBike ? pref.bike().walking().reluctance() : pref.walk().reluctance(); case BICYCLE -> pref.bike().reluctance(); case CAR -> pref.car().reluctance(); + case SCOOTER -> pref.scooter().reluctance(); default -> throw new IllegalArgumentException( "getReluctance(): Invalid mode " + traverseMode ); diff --git a/src/main/java/org/opentripplanner/street/model/edge/StreetTransitEntityLink.java b/src/main/java/org/opentripplanner/street/model/edge/StreetTransitEntityLink.java index df5b24a5928..dabd70bf28d 100644 --- a/src/main/java/org/opentripplanner/street/model/edge/StreetTransitEntityLink.java +++ b/src/main/java/org/opentripplanner/street/model/edge/StreetTransitEntityLink.java @@ -137,13 +137,13 @@ else if ( @Nonnull private State[] buildState(State s0, StateEditor s1, RoutingPreferences pref) { - var rentalPreferences = s0.getRequest().preferences().rental(s0.getRequest().mode()); - if ( - s0.isRentingVehicleFromStation() && - s0.mayKeepRentedVehicleAtDestination() && - rentalPreferences.allowArrivingInRentedVehicleAtDestination() - ) { - s1.incrementWeight(rentalPreferences.arrivingInRentalVehicleAtDestinationCost().toSeconds()); + if (s0.isRentingVehicleFromStation() && s0.mayKeepRentedVehicleAtDestination()) { + var rentalPreferences = s0.getRequest().preferences().rental(s0.getRequest().mode()); + if (rentalPreferences.allowArrivingInRentedVehicleAtDestination()) { + s1.incrementWeight( + rentalPreferences.arrivingInRentalVehicleAtDestinationCost().toSeconds() + ); + } } s1.setBackMode(null); diff --git a/src/main/java/org/opentripplanner/street/model/edge/TemporaryFreeEdge.java b/src/main/java/org/opentripplanner/street/model/edge/TemporaryFreeEdge.java index d825a8fcbea..e93c6523d89 100644 --- a/src/main/java/org/opentripplanner/street/model/edge/TemporaryFreeEdge.java +++ b/src/main/java/org/opentripplanner/street/model/edge/TemporaryFreeEdge.java @@ -42,13 +42,13 @@ public State[] traverse(State s0) { s1.incrementWeight(1); s1.setBackMode(null); - var rentalPreferences = s0.getPreferences().rental(s0.getRequest().mode()); - if ( - s0.isRentingVehicleFromStation() && - s0.mayKeepRentedVehicleAtDestination() && - rentalPreferences.allowArrivingInRentedVehicleAtDestination() - ) { - s1.incrementWeight(rentalPreferences.arrivingInRentalVehicleAtDestinationCost().toSeconds()); + if (s0.isRentingVehicleFromStation() && s0.mayKeepRentedVehicleAtDestination()) { + var rentalPreferences = s0.getPreferences().rental(s0.getRequest().mode()); + if (rentalPreferences.allowArrivingInRentedVehicleAtDestination()) { + s1.incrementWeight( + rentalPreferences.arrivingInRentalVehicleAtDestinationCost().toSeconds() + ); + } } return s1.makeStateArray(); diff --git a/src/main/java/org/opentripplanner/street/search/state/StateData.java b/src/main/java/org/opentripplanner/street/search/state/StateData.java index cc2bfa5a57a..d1db14d91f4 100644 --- a/src/main/java/org/opentripplanner/street/search/state/StateData.java +++ b/src/main/java/org/opentripplanner/street/search/state/StateData.java @@ -87,7 +87,9 @@ public static List getInitialStateDatas( return getInitialStateDatas( request.mode(), request.arriveBy(), - rentalPreferences.allowArrivingInRentedVehicleAtDestination(), + rentalPreferences != null + ? rentalPreferences.allowArrivingInRentedVehicleAtDestination() + : false, stateDataConstructor ); } @@ -102,7 +104,9 @@ public static StateData getBaseCaseStateData(StreetSearchRequest request) { var stateDatas = getInitialStateDatas( request.mode(), request.arriveBy(), - rentalPreferences.allowArrivingInRentedVehicleAtDestination(), + rentalPreferences != null + ? rentalPreferences.allowArrivingInRentedVehicleAtDestination() + : false, StateData::new ); diff --git a/src/main/java/org/opentripplanner/street/search/strategy/EuclideanRemainingWeightHeuristic.java b/src/main/java/org/opentripplanner/street/search/strategy/EuclideanRemainingWeightHeuristic.java index e43278901d4..7bccdbb94df 100644 --- a/src/main/java/org/opentripplanner/street/search/strategy/EuclideanRemainingWeightHeuristic.java +++ b/src/main/java/org/opentripplanner/street/search/strategy/EuclideanRemainingWeightHeuristic.java @@ -55,6 +55,9 @@ private double getStreetSpeedUpperBound(RoutingPreferences preferences, StreetMo if (streetMode.includesBiking()) { return preferences.bike().speed(); } + if (streetMode.includesScooter()) { + return preferences.scooter().speed(); + } return preferences.walk().speed(); } diff --git a/src/main/java/org/opentripplanner/transit/model/site/GroupStop.java b/src/main/java/org/opentripplanner/transit/model/site/GroupStop.java index edee00e27e1..c674d588238 100644 --- a/src/main/java/org/opentripplanner/transit/model/site/GroupStop.java +++ b/src/main/java/org/opentripplanner/transit/model/site/GroupStop.java @@ -94,13 +94,12 @@ public Geometry getGeometry() { /** * Returns the geometry of the area that encompasses the bounds of this StopLocation group. If the - * group is defined only as a list of stops, this will return the same as getGeometry. If on the - * other hand the group is defined as an area and the stops are inferred from that area, then this - * will return the geometry of the area. + * group is defined as all the stops within an area, then this will return the geometry of the + * area. If the group is defined simply as a list of stops, this will return an empty optional. */ @Override public Optional getEncompassingAreaGeometry() { - return Optional.ofNullable(encompassingAreaGeometry).or(() -> Optional.of(geometry)); + return Optional.ofNullable(encompassingAreaGeometry); } @Override diff --git a/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java b/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java index 0d08bcf34b2..ca88b7d3130 100644 --- a/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java @@ -41,6 +41,7 @@ import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.organization.Operator; import org.opentripplanner.transit.model.site.AreaStop; +import org.opentripplanner.transit.model.site.GroupStop; import org.opentripplanner.transit.model.site.MultiModalStation; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.Station; @@ -226,6 +227,12 @@ public Collection listRegularStops() { return transitModel.getStopModel().listRegularStops(); } + @Override + public Collection listGroupStops() { + OTPRequestTimeoutException.checkForTimeout(); + return transitModel.getStopModel().listGroupStops(); + } + @Override public StopLocation getStopLocation(FeedScopedId id) { return transitModel.getStopModel().getStopLocation(id); @@ -543,7 +550,7 @@ public ZonedDateTime getTransitServiceStarts() { } @Override - public Collection findRegularStop(Envelope envelope) { + public Collection findRegularStops(Envelope envelope) { OTPRequestTimeoutException.checkForTimeout(); return transitModel.getStopModel().findRegularStops(envelope); } @@ -551,7 +558,7 @@ public Collection findRegularStop(Envelope envelope) { @Override public Collection findAreaStops(Envelope envelope) { OTPRequestTimeoutException.checkForTimeout(); - return transitModel.getStopModel().queryLocationIndex(envelope); + return transitModel.getStopModel().findAreaStops(envelope); } @Override diff --git a/src/main/java/org/opentripplanner/transit/service/StopModel.java b/src/main/java/org/opentripplanner/transit/service/StopModel.java index dd6b65e5b33..763aa504c40 100644 --- a/src/main/java/org/opentripplanner/transit/service/StopModel.java +++ b/src/main/java/org/opentripplanner/transit/service/StopModel.java @@ -108,7 +108,7 @@ public StopModelBuilder withContext() { } /** - * Return a regular transit stop if found(not flex stops). + * Return a regular transit stop if found (not flex stops). */ public RegularStop getRegularStop(FeedScopedId id) { return regularStopById.get(id); @@ -121,6 +121,9 @@ public Collection listRegularStops() { return regularStopById.values(); } + /** + * Find regular stops within a geographical area. + */ public Collection findRegularStops(Envelope envelope) { return index.findRegularStops(envelope); } @@ -131,21 +134,30 @@ public boolean hasAreaStops() { /** * Flex locations are generated by GTFS graph builder, but consumed only after the street graph is - * built + * built. */ @Nullable public AreaStop getAreaStop(FeedScopedId id) { return areaStopById.get(id); } + /** + * Return all flex stops, not regular transit stops and flex group of stops. + */ public Collection listAreaStops() { return areaStopById.values(); } - public Collection queryLocationIndex(Envelope envelope) { + /** + * Find flex stops within a geographical area. + */ + public Collection findAreaStops(Envelope envelope) { return index.findAreaStops(envelope); } + /** + * Return all flex groups of stops. + */ public Collection listGroupStops() { return groupStopById.values(); } diff --git a/src/main/java/org/opentripplanner/transit/service/TransitService.java b/src/main/java/org/opentripplanner/transit/service/TransitService.java index d0664aa292d..17354ec4a20 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitService.java @@ -32,6 +32,7 @@ import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.organization.Operator; import org.opentripplanner.transit.model.site.AreaStop; +import org.opentripplanner.transit.model.site.GroupStop; import org.opentripplanner.transit.model.site.MultiModalStation; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.Station; @@ -95,6 +96,8 @@ public interface TransitService { Collection listRegularStops(); + Collection listGroupStops(); + StopLocation getStopLocation(FeedScopedId parseId); Collection getStopOrChildStops(FeedScopedId id); @@ -187,7 +190,7 @@ List stopTimesForPatternAtStop( boolean transitFeedCovers(Instant dateTime); - Collection findRegularStop(Envelope envelope); + Collection findRegularStops(Envelope envelope); Collection findAreaStops(Envelope envelope); diff --git a/src/main/java/org/opentripplanner/visualizer/GraphVisualizer.java b/src/main/java/org/opentripplanner/visualizer/GraphVisualizer.java index f15aff9048b..6ab7d86917c 100644 --- a/src/main/java/org/opentripplanner/visualizer/GraphVisualizer.java +++ b/src/main/java/org/opentripplanner/visualizer/GraphVisualizer.java @@ -64,7 +64,7 @@ import org.opentripplanner.astar.spi.TraverseVisitor; import org.opentripplanner.graph_builder.issue.api.DataImportIssue; import org.opentripplanner.routing.api.request.RouteRequest; -import org.opentripplanner.routing.core.BicycleOptimizeType; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.impl.GraphPathFinder; import org.opentripplanner.street.model.edge.Edge; @@ -490,6 +490,12 @@ protected void route(String from, String to) { // there should be a ui element for walk distance and optimize type .withOptimizeType(getSelectedOptimizeType()) ); + preferences.withScooter(scooter -> + scooter + .withSpeed(Float.parseFloat(bikeSpeed.getText())) + // there should be a ui element for walk distance and optimize type + .withOptimizeType(getSelectedOptimizeType()) + ); }); System.out.println("--------"); @@ -547,20 +553,20 @@ protected void route(String from, String to) { } } - BicycleOptimizeType getSelectedOptimizeType() { + VehicleRoutingOptimizeType getSelectedOptimizeType() { if (opQuick.isSelected()) { - return BicycleOptimizeType.SHORTEST_DURATION; + return VehicleRoutingOptimizeType.SHORTEST_DURATION; } if (opSafe.isSelected()) { - return BicycleOptimizeType.SAFE_STREETS; + return VehicleRoutingOptimizeType.SAFE_STREETS; } if (opFlat.isSelected()) { - return BicycleOptimizeType.FLAT_STREETS; + return VehicleRoutingOptimizeType.FLAT_STREETS; } if (opGreenways.isSelected()) { - return BicycleOptimizeType.SAFEST_STREETS; + return VehicleRoutingOptimizeType.SAFEST_STREETS; } - return BicycleOptimizeType.SHORTEST_DURATION; + return VehicleRoutingOptimizeType.SHORTEST_DURATION; } private Container makeDiffTab() { diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index 25cf95ca2f9..9c0eae88fa1 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -774,7 +774,7 @@ type QueryType { "Input type for executing a travel search for a trip between two locations. Returns trip patterns describing suggested alternatives for the trip." trip( "Time and cost penalty on access/egress modes." - accessEgressPenalty: [PenaltyForStreetMode!] = [], + accessEgressPenalty: [PenaltyForStreetMode!] = [{streetMode : car_park, timePenalty : "20m + 2.0 t", costFactor : 1.5}, {streetMode : flexible, timePenalty : "10m + 1.30 t", costFactor : 1.3}], "The alightSlack is the minimum extra time after exiting a public transport vehicle. This is the default value used, if not overridden by the 'alightSlackList'." alightSlackDefault: Int = 0, "List of alightSlack for a given set of modes. Defaults: []" diff --git a/src/test/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapperTest.java b/src/test/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapperTest.java index b05dd77e2a9..ecba3320838 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapperTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapperTest.java @@ -5,8 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.params.provider.Arguments.of; -import static org.opentripplanner.routing.core.BicycleOptimizeType.SAFE_STREETS; -import static org.opentripplanner.routing.core.BicycleOptimizeType.TRIANGLE; +import static org.opentripplanner.routing.core.VehicleRoutingOptimizeType.SAFE_STREETS; +import static org.opentripplanner.routing.core.VehicleRoutingOptimizeType.TRIANGLE; import graphql.ExecutionInput; import graphql.execution.ExecutionId; @@ -55,7 +55,7 @@ class RouteRequestMapperTest implements PlanTestConstants { graph.getVehicleParkingService(), new DefaultVehicleRentalService(), new DefaultRealtimeVehicleService(transitService), - GraphFinder.getInstance(graph, transitService::findRegularStop), + GraphFinder.getInstance(graph, transitService::findRegularStops), new RouteRequest() ); } diff --git a/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java b/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java index ac3b7bca522..73c8cd1369e 100644 --- a/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java +++ b/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java @@ -2,16 +2,33 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.time.LocalDate; import java.util.List; import org.glassfish.jersey.server.internal.routing.UriRoutingContext; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.opentripplanner.model.FeedInfo; +import org.opentripplanner.service.worldenvelope.model.WorldEnvelope; import org.opentripplanner.test.support.HttpForTest; class TileJsonTest { private static final List LAYERS = List.of("stops", "rentalVehicles"); + private static final WorldEnvelope ENVELOPE = WorldEnvelope + .of() + .expandToIncludeStreetEntities(1, 1) + .expandToIncludeStreetEntities(2, 2) + .build(); + private static final FeedInfo FEED_INFO = new FeedInfo( + "1", + "Trimet", + "https://trimet.org", + "en", + LocalDate.MIN, + LocalDate.MIN, + "1" + ); @ParameterizedTest @ValueSource( @@ -40,4 +57,23 @@ void defaultPath() { TileJson.urlWithDefaultPath(uriInfo, req, LAYERS, "default", "vectorTiles") ); } + + @Test + void attributionFromFeedInfo() { + var tileJson = new TileJson("http://example.com", ENVELOPE, List.of(FEED_INFO)); + assertEquals("Trimet", tileJson.attribution); + } + + @Test + void duplicateAttribution() { + var tileJson = new TileJson("http://example.com", ENVELOPE, List.of(FEED_INFO, FEED_INFO)); + assertEquals("Trimet", tileJson.attribution); + } + + @Test + void attributionFromOverride() { + var override = "OVERRIDE"; + var tileJson = new TileJson("http://example.com", ENVELOPE, override); + assertEquals(override, tileJson.attribution); + } } diff --git a/src/test/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapperTest.java b/src/test/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapperTest.java new file mode 100644 index 00000000000..160014213d3 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapperTest.java @@ -0,0 +1,57 @@ +package org.opentripplanner.apis.transmodel.mapping; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.opentripplanner.routing.api.request.RequestModes; +import org.opentripplanner.routing.api.request.StreetMode; + +class RequestModesMapperTest { + + @Test + void testMapRequestModesEmptyMapReturnsDefaults() { + Map inputModes = Map.of(); + + RequestModes mappedModes = RequestModesMapper.mapRequestModes(inputModes); + + assertEquals(RequestModes.of().build(), mappedModes); + } + + @Test + void testMapRequestModesAccessSetReturnsDefaultsForOthers() { + Map inputModes = Map.of("accessMode", StreetMode.BIKE); + + RequestModes wantModes = RequestModes + .of() + .withAccessMode(StreetMode.BIKE) + .withTransferMode(StreetMode.BIKE) + .build(); + + RequestModes mappedModes = RequestModesMapper.mapRequestModes(inputModes); + + assertEquals(wantModes, mappedModes); + } + + @Test + void testMapRequestModesEgressSetReturnsDefaultsForOthers() { + Map inputModes = Map.of("egressMode", StreetMode.CAR); + + RequestModes wantModes = RequestModes.of().withEgressMode(StreetMode.CAR).build(); + + RequestModes mappedModes = RequestModesMapper.mapRequestModes(inputModes); + + assertEquals(wantModes, mappedModes); + } + + @Test + void testMapRequestModesDirectSetReturnsDefaultsForOthers() { + Map inputModes = Map.of("directMode", StreetMode.CAR); + + RequestModes wantModes = RequestModes.of().withDirectMode(StreetMode.CAR).build(); + + RequestModes mappedModes = RequestModesMapper.mapRequestModes(inputModes); + + assertEquals(wantModes, mappedModes); + } +} diff --git a/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java b/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java index 127fe66f0ea..87004fd9cfa 100644 --- a/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java +++ b/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java @@ -40,7 +40,7 @@ import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.api.request.preference.StreetPreferences; import org.opentripplanner.routing.api.request.preference.TimeSlopeSafetyTriangle; -import org.opentripplanner.routing.core.BicycleOptimizeType; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; @@ -255,14 +255,14 @@ public void testMaxDirectDurationForFlexWithTooLongDuration() { public void testBikeTriangleFactors() { Map arguments = Map.of( "bicycleOptimisationMethod", - BicycleOptimizeType.TRIANGLE, + VehicleRoutingOptimizeType.TRIANGLE, "triangleFactors", Map.of("safety", 0.1, "slope", 0.1, "time", 0.8) ); var req1 = TripRequestMapper.createRequest(executionContext(arguments)); - assertEquals(BicycleOptimizeType.TRIANGLE, req1.preferences().bike().optimizeType()); + assertEquals(VehicleRoutingOptimizeType.TRIANGLE, req1.preferences().bike().optimizeType()); assertEquals( new TimeSlopeSafetyTriangle(0.8, 0.1, 0.1), req1.preferences().bike().optimizeTriangle() @@ -272,18 +272,18 @@ public void testBikeTriangleFactors() { @Test void testDefaultTriangleFactors() { var req2 = TripRequestMapper.createRequest(executionContext(Map.of())); - assertEquals(BicycleOptimizeType.SAFE_STREETS, req2.preferences().bike().optimizeType()); + assertEquals(VehicleRoutingOptimizeType.SAFE_STREETS, req2.preferences().bike().optimizeType()); assertEquals(TimeSlopeSafetyTriangle.DEFAULT, req2.preferences().bike().optimizeTriangle()); } - static Stream noTriangleCases = BicycleOptimizeType + static Stream noTriangleCases = VehicleRoutingOptimizeType .nonTriangleValues() .stream() .map(Arguments::of); @ParameterizedTest @VariableSource("noTriangleCases") - public void testBikeTriangleFactorsHasNoEffect(BicycleOptimizeType bot) { + public void testBikeTriangleFactorsHasNoEffect(VehicleRoutingOptimizeType bot) { Map arguments = Map.of( "bicycleOptimisationMethod", bot, diff --git a/src/test/java/org/opentripplanner/apis/transmodel/mapping/preferences/BikePreferencesMapperTest.java b/src/test/java/org/opentripplanner/apis/transmodel/mapping/preferences/BikePreferencesMapperTest.java index a72fd25cbfa..786ddc5c198 100644 --- a/src/test/java/org/opentripplanner/apis/transmodel/mapping/preferences/BikePreferencesMapperTest.java +++ b/src/test/java/org/opentripplanner/apis/transmodel/mapping/preferences/BikePreferencesMapperTest.java @@ -9,7 +9,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.opentripplanner.apis.transmodel._support.TestDataFetcherDecorator; import org.opentripplanner.routing.api.request.preference.BikePreferences; -import org.opentripplanner.routing.core.BicycleOptimizeType; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; class BikePreferencesMapperTest { @@ -23,7 +23,7 @@ static List mapBikePreferencesTestCases() { Arguments.of("bikeSpeed", 10.0, "BikePreferences{speed: 10.0}"), Arguments.of( "bicycleOptimisationMethod", - BicycleOptimizeType.TRIANGLE, + VehicleRoutingOptimizeType.TRIANGLE, "BikePreferences{optimizeType: TRIANGLE}" ), // No effect unless BicycleOptimize is TRIANGLE @@ -64,7 +64,7 @@ static List mapBikePreferencesOptimizeTriangleTestCases() { @ParameterizedTest @MethodSource("mapBikePreferencesOptimizeTriangleTestCases") void testMapBikePreferencesOptimizeTriangle(String field, Object value, String expected) { - var preferences = BikePreferences.of().withOptimizeType(BicycleOptimizeType.TRIANGLE); + var preferences = BikePreferences.of().withOptimizeType(VehicleRoutingOptimizeType.TRIANGLE); mapBikePreferences(preferences, TestDataFetcherDecorator.of(field, value)); assertEquals( "BikePreferences{optimizeType: TRIANGLE, optimizeTriangle: " + expected + "}", diff --git a/src/test/java/org/opentripplanner/apis/transmodel/mapping/preferences/ScooterPreferencesMapperTest.java b/src/test/java/org/opentripplanner/apis/transmodel/mapping/preferences/ScooterPreferencesMapperTest.java new file mode 100644 index 00000000000..2b5c7563ac2 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/transmodel/mapping/preferences/ScooterPreferencesMapperTest.java @@ -0,0 +1,70 @@ +package org.opentripplanner.apis.transmodel.mapping.preferences; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.apis.transmodel.mapping.preferences.ScooterPreferencesMapper.mapScooterPreferences; + +import java.util.List; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.apis.transmodel._support.TestDataFetcherDecorator; +import org.opentripplanner.routing.api.request.preference.ScooterPreferences; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; + +class ScooterPreferencesMapperTest { + + static List mapScooterPreferencesTestCases() { + return List.of( + Arguments.of("walkReluctance", 10.0, "ScooterPreferences{reluctance: 10.0}"), + Arguments.of("bikeSpeed", 10.0, "ScooterPreferences{speed: 10.0}"), + Arguments.of( + "bicycleOptimisationMethod", + VehicleRoutingOptimizeType.TRIANGLE, + "ScooterPreferences{optimizeType: TRIANGLE}" + ), + // No effect unless BicycleOptimize is TRIANGLE + Arguments.of("triangleFactors.time", 0.17, "ScooterPreferences{}"), + Arguments.of("triangleFactors.slope", 0.12, "ScooterPreferences{}"), + Arguments.of("triangleFactors.safety", 0.13, "ScooterPreferences{}") + ); + } + + @ParameterizedTest + @MethodSource("mapScooterPreferencesTestCases") + void testMapScooterPreferences(String field, Object value, String expected) { + var preferences = ScooterPreferences.of(); + mapScooterPreferences(preferences, TestDataFetcherDecorator.of(field, value)); + assertEquals(expected, preferences.build().toString()); + } + + static List mapScooterPreferencesOptimizeTriangleTestCases() { + return List.of( + Arguments.of( + "triangleFactors.time", + 0.17, + "TimeSlopeSafetyTriangle[time=1.0, slope=0.0, safety=0.0]" + ), + Arguments.of( + "triangleFactors.slope", + 0.12, + "TimeSlopeSafetyTriangle[time=0.0, slope=1.0, safety=0.0]" + ), + Arguments.of( + "triangleFactors.safety", + 0.13, + "TimeSlopeSafetyTriangle[time=0.0, slope=0.0, safety=1.0]" + ) + ); + } + + @ParameterizedTest + @MethodSource("mapScooterPreferencesOptimizeTriangleTestCases") + void testMapScooterPreferencesOptimizeTriangle(String field, Object value, String expected) { + var preferences = ScooterPreferences.of().withOptimizeType(VehicleRoutingOptimizeType.TRIANGLE); + mapScooterPreferences(preferences, TestDataFetcherDecorator.of(field, value)); + assertEquals( + "ScooterPreferences{optimizeType: TRIANGLE, optimizeTriangle: " + expected + "}", + preferences.build().toString() + ); + } +} diff --git a/src/test/java/org/opentripplanner/apis/vectortiles/DebugStyleSpecTest.java b/src/test/java/org/opentripplanner/apis/vectortiles/DebugStyleSpecTest.java index f2c3ebf49de..d971cefe2e2 100644 --- a/src/test/java/org/opentripplanner/apis/vectortiles/DebugStyleSpecTest.java +++ b/src/test/java/org/opentripplanner/apis/vectortiles/DebugStyleSpecTest.java @@ -17,9 +17,10 @@ void spec() { var vectorSource = new VectorSource("vectorSource", "https://example.com"); var regularStops = new VectorSourceLayer(vectorSource, "stops"); var areaStops = new VectorSourceLayer(vectorSource, "stops"); + var groupStops = new VectorSourceLayer(vectorSource, "stops"); var edges = new VectorSourceLayer(vectorSource, "edges"); var vertices = new VectorSourceLayer(vectorSource, "vertices"); - var spec = DebugStyleSpec.build(regularStops, areaStops, edges, vertices); + var spec = DebugStyleSpec.build(regularStops, areaStops, groupStops, edges, vertices); var json = ObjectMappers.ignoringExtraFields().valueToTree(spec); var expectation = RESOURCES.fileToString("style.json"); diff --git a/src/test/java/org/opentripplanner/framework/io/HttpUtilsTest.java b/src/test/java/org/opentripplanner/framework/io/HttpUtilsTest.java new file mode 100644 index 00000000000..ac8ae52848c --- /dev/null +++ b/src/test/java/org/opentripplanner/framework/io/HttpUtilsTest.java @@ -0,0 +1,38 @@ +package org.opentripplanner.framework.io; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.Map; +import org.glassfish.jersey.server.internal.routing.UriRoutingContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.test.support.HttpForTest; + +class HttpUtilsTest { + + private static final String HOSTNAME = "example.com"; + + static List testCases() { + return List.of( + HOSTNAME, + "example.com,", + " example.com ,", + "example.com,example.com", + "example.com, example.com", + "example.com, example.net", + "example.com, example.net, example.com", + " example.com, example.net, example.com" + ); + } + + @ParameterizedTest + @MethodSource("testCases") + void extractHost(String headerValue) { + var req = HttpForTest.containerRequest(); + req.headers(Map.of("X-Forwarded-Host", List.of(headerValue))); + var uriInfo = new UriRoutingContext(req); + var baseAddress = HttpUtils.getBaseAddress(uriInfo, req); + assertEquals("https://" + HOSTNAME, baseAddress); + } +} diff --git a/src/test/java/org/opentripplanner/graph_builder/module/osm/TriangleInequalityTest.java b/src/test/java/org/opentripplanner/graph_builder/module/osm/TriangleInequalityTest.java index 4cdada95e4b..52069a7a72e 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/osm/TriangleInequalityTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/osm/TriangleInequalityTest.java @@ -182,6 +182,7 @@ private void checkTriangleInequality(RequestModes modes, List fil .withStreet(street -> street.withTurnReluctance(1.0)) .withCar(car -> car.withSpeed(1.0).withReluctance(1.0)) .withBike(bike -> bike.withSpeed(1.0).withReluctance(1.0)) + .withScooter(scooter -> scooter.withSpeed(1.0).withReluctance(1.0)) ); if (modes != null) { diff --git a/src/test/java/org/opentripplanner/routing/algorithm/TurnCostTest.java b/src/test/java/org/opentripplanner/routing/algorithm/TurnCostTest.java index 53e340dcc63..dfb062226a4 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/TurnCostTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/TurnCostTest.java @@ -94,6 +94,7 @@ public void before() { preferences .withCar(it -> it.withSpeed(1.0).withReluctance(1.0)) .withBike(bike -> bike.withSpeed(1.0).withReluctance(1.0)) + .withScooter(scooter -> scooter.withSpeed(1.0).withReluctance(1.0)) .withWalk(walk -> walk.withSpeed(1.0).withStairsReluctance(1.0).withReluctance(1.0)) .withStreet(it -> it.withTurnReluctance(1.0)) ); diff --git a/src/test/java/org/opentripplanner/routing/algorithm/mapping/ElevationSnapshotTest.java b/src/test/java/org/opentripplanner/routing/algorithm/mapping/ElevationSnapshotTest.java index 9027b0a0dd5..677edfcbabf 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/mapping/ElevationSnapshotTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/mapping/ElevationSnapshotTest.java @@ -19,7 +19,7 @@ import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.api.request.request.filter.AllowAllTransitFilter; -import org.opentripplanner.routing.core.BicycleOptimizeType; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; import org.opentripplanner.routing.error.RoutingValidationException; @ExtendWith(SnapshotExtension.class) @@ -98,7 +98,7 @@ public void directBike() { request.withPreferences(pref -> pref.withBike(bike -> bike - .withOptimizeType(BicycleOptimizeType.TRIANGLE) + .withOptimizeType(VehicleRoutingOptimizeType.TRIANGLE) .withOptimizeTriangle(b -> b.withTime(0.3).withSlope(0.4).withSafety(0.3)) ) ); diff --git a/src/test/java/org/opentripplanner/routing/api/request/preference/BikePreferencesTest.java b/src/test/java/org/opentripplanner/routing/api/request/preference/BikePreferencesTest.java index 8a6ac3f4f45..3bbaf0105e5 100644 --- a/src/test/java/org/opentripplanner/routing/api/request/preference/BikePreferencesTest.java +++ b/src/test/java/org/opentripplanner/routing/api/request/preference/BikePreferencesTest.java @@ -5,7 +5,7 @@ import static org.opentripplanner.routing.api.request.preference.ImmutablePreferencesAsserts.assertEqualsAndHashCode; import org.junit.jupiter.api.Test; -import org.opentripplanner.routing.core.BicycleOptimizeType; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; class BikePreferencesTest { @@ -16,7 +16,8 @@ class BikePreferencesTest { .of() .withSlope(1) .build(); - public static final BicycleOptimizeType OPTIMIZE_TYPE = BicycleOptimizeType.TRIANGLE; + public static final VehicleRoutingOptimizeType OPTIMIZE_TYPE = + VehicleRoutingOptimizeType.TRIANGLE; public static final int RENTAL_PICKUP_TIME = 30; public static final int PARK_COST = 30; @@ -106,20 +107,20 @@ void testForcedTriangleOptimization() { .of() .withForcedOptimizeTriangle(it -> it.withSlope(1).build()) .build(); - assertEquals(BicycleOptimizeType.TRIANGLE, trianglePreferences.optimizeType()); + assertEquals(VehicleRoutingOptimizeType.TRIANGLE, trianglePreferences.optimizeType()); var conflictingPreferences = BikePreferences .of() - .withOptimizeType(BicycleOptimizeType.SAFE_STREETS) + .withOptimizeType(VehicleRoutingOptimizeType.SAFE_STREETS) .withForcedOptimizeTriangle(it -> it.withSlope(1).build()) .build(); - assertEquals(BicycleOptimizeType.TRIANGLE, conflictingPreferences.optimizeType()); + assertEquals(VehicleRoutingOptimizeType.TRIANGLE, conflictingPreferences.optimizeType()); var emptyTrianglePreferences = BikePreferences .of() - .withOptimizeType(BicycleOptimizeType.SAFE_STREETS) + .withOptimizeType(VehicleRoutingOptimizeType.SAFE_STREETS) .withForcedOptimizeTriangle(it -> it.build()) .build(); - assertEquals(BicycleOptimizeType.SAFE_STREETS, emptyTrianglePreferences.optimizeType()); + assertEquals(VehicleRoutingOptimizeType.SAFE_STREETS, emptyTrianglePreferences.optimizeType()); } } diff --git a/src/test/java/org/opentripplanner/routing/api/request/preference/ScooterPreferencesTest.java b/src/test/java/org/opentripplanner/routing/api/request/preference/ScooterPreferencesTest.java new file mode 100644 index 00000000000..01a12ef5cbb --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/api/request/preference/ScooterPreferencesTest.java @@ -0,0 +1,109 @@ +package org.opentripplanner.routing.api.request.preference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.opentripplanner.routing.api.request.preference.ImmutablePreferencesAsserts.assertEqualsAndHashCode; + +import org.junit.jupiter.api.Test; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; + +class ScooterPreferencesTest { + + public static final double SPEED = 2.0; + public static final double RELUCTANCE = 1.2; + public static final TimeSlopeSafetyTriangle TRIANGLE = TimeSlopeSafetyTriangle + .of() + .withSlope(1) + .build(); + public static final VehicleRoutingOptimizeType OPTIMIZE_TYPE = + VehicleRoutingOptimizeType.TRIANGLE; + public static final int RENTAL_PICKUP_TIME = 30; + + private final ScooterPreferences subject = ScooterPreferences + .of() + .withSpeed(SPEED) + .withReluctance(RELUCTANCE) + .withOptimizeType(OPTIMIZE_TYPE) + .withRental(rental -> rental.withPickupTime(RENTAL_PICKUP_TIME).build()) + .withOptimizeTriangle(it -> it.withSlope(1).build()) + .build(); + + @Test + void speed() { + assertEquals(SPEED, subject.speed()); + } + + @Test + void reluctance() { + assertEquals(RELUCTANCE, subject.reluctance()); + } + + @Test + void optimizeType() { + assertEquals(OPTIMIZE_TYPE, subject.optimizeType()); + } + + @Test + void optimizeTriangle() { + assertEquals(TRIANGLE, subject.optimizeTriangle()); + } + + @Test + void rental() { + var vehicleRental = VehicleRentalPreferences.of().withPickupTime(RENTAL_PICKUP_TIME).build(); + assertEquals(vehicleRental, subject.rental()); + } + + @Test + void testOfAndCopyOf() { + // Return same object if no value is set + assertSame(ScooterPreferences.DEFAULT, ScooterPreferences.of().build()); + assertSame(subject, subject.copyOf().build()); + } + + @Test + void testCopyOfEqualsAndHashCode() { + // Create a copy, make a change and set it back again to force creating a new object + var other = subject.copyOf().withSpeed(0.7).build(); + var same = other.copyOf().withSpeed(SPEED).build(); + assertEqualsAndHashCode(subject, other, same); + } + + @Test + void testToString() { + assertEquals("ScooterPreferences{}", ScooterPreferences.DEFAULT.toString()); + assertEquals( + "ScooterPreferences{" + + "speed: 2.0, " + + "reluctance: 1.2, " + + "rental: VehicleRentalPreferences{pickupTime: 30s}, " + + "optimizeType: TRIANGLE, " + + "optimizeTriangle: TimeSlopeSafetyTriangle[time=0.0, slope=1.0, safety=0.0]" + + "}", + subject.toString() + ); + } + + @Test + void testForcedTriangleOptimization() { + var trianglePreferences = ScooterPreferences + .of() + .withForcedOptimizeTriangle(it -> it.withSlope(1).build()) + .build(); + assertEquals(VehicleRoutingOptimizeType.TRIANGLE, trianglePreferences.optimizeType()); + + var conflictingPreferences = ScooterPreferences + .of() + .withOptimizeType(VehicleRoutingOptimizeType.SAFE_STREETS) + .withForcedOptimizeTriangle(it -> it.withSlope(1).build()) + .build(); + assertEquals(VehicleRoutingOptimizeType.TRIANGLE, conflictingPreferences.optimizeType()); + + var emptyTrianglePreferences = ScooterPreferences + .of() + .withOptimizeType(VehicleRoutingOptimizeType.SAFE_STREETS) + .withForcedOptimizeTriangle(it -> it.build()) + .build(); + assertEquals(VehicleRoutingOptimizeType.SAFE_STREETS, emptyTrianglePreferences.optimizeType()); + } +} diff --git a/src/test/java/org/opentripplanner/routing/api/request/preference/StreetPreferencesTest.java b/src/test/java/org/opentripplanner/routing/api/request/preference/StreetPreferencesTest.java index 1d47337a312..a318474eab7 100644 --- a/src/test/java/org/opentripplanner/routing/api/request/preference/StreetPreferencesTest.java +++ b/src/test/java/org/opentripplanner/routing/api/request/preference/StreetPreferencesTest.java @@ -109,10 +109,10 @@ void testToString() { "routingTimeout: 3s, " + "elevator: ElevatorPreferences{boardTime: 2m}, " + "intersectionTraversalModel: CONSTANT, " + - "accessEgress: AccessEgressPreferences{" + - "penalty: TimeAndCostPenaltyForEnum{CAR_TO_PARK: " + + "accessEgress: AccessEgressPreferences{penalty: TimeAndCostPenaltyForEnum{CAR_TO_PARK: " + CAR_PENALTY + - "}, " + + ", CAR_RENTAL: (timePenalty: 20m + 2.0 t, costFactor: 1.50), CAR_HAILING: (timePenalty: 20m + 2.0 t, costFactor: 1.50), " + + "FLEXIBLE: (timePenalty: 10m + 1.30 t, costFactor: 1.30)}, " + "maxDuration: DurationForStreetMode{default:5m}" + "}, " + "maxDirectDuration: DurationForStreetMode{default:10m}" + diff --git a/src/test/java/org/opentripplanner/routing/api/request/preference/VehicleWalkingPreferencesTest.java b/src/test/java/org/opentripplanner/routing/api/request/preference/VehicleWalkingPreferencesTest.java index 9571eee8cc5..fdc416d7c0f 100644 --- a/src/test/java/org/opentripplanner/routing/api/request/preference/VehicleWalkingPreferencesTest.java +++ b/src/test/java/org/opentripplanner/routing/api/request/preference/VehicleWalkingPreferencesTest.java @@ -11,8 +11,8 @@ class VehicleWalkingPreferencesTest { private static final double SPEED = 1.45; private static final double RELUCTANCE = 5.5; - private static final Duration HOP_TIME = Duration.ofSeconds(15); - private static final Cost HOP_COST = Cost.costOfSeconds(20); + private static final Duration MOUNT_DISMOUNT_TIME = Duration.ofSeconds(15); + private static final Cost MOUNT_DISMOUNT_COST = Cost.costOfSeconds(20); private static final double STAIRS_RELUCTANCE = 11; private final VehicleWalkingPreferences subject = createPreferences(); @@ -28,13 +28,13 @@ void reluctance() { } @Test - void hopTime() { - assertEquals(HOP_TIME, subject.hopTime()); + void mountDismountTime() { + assertEquals(MOUNT_DISMOUNT_TIME, subject.mountDismountTime()); } @Test - void hopCost() { - assertEquals(HOP_COST, subject.hopCost()); + void mountDismountCost() { + assertEquals(MOUNT_DISMOUNT_COST, subject.mountDismountCost()); } @Test @@ -57,8 +57,8 @@ void testToString() { "VehicleWalkingPreferences{" + "speed: 1.45, " + "reluctance: 5.5, " + - "hopTime: PT15S, " + - "hopCost: $20, " + + "mountDismountTime: PT15S, " + + "mountDismountCost: $20, " + "stairsReluctance: 11.0}", subject.toString() ); @@ -69,8 +69,8 @@ private VehicleWalkingPreferences createPreferences() { .of() .withSpeed(SPEED) .withReluctance(RELUCTANCE) - .withHopTime(HOP_TIME) - .withHopCost(HOP_COST.toSeconds()) + .withMountDismountTime(MOUNT_DISMOUNT_TIME) + .withMountDismountCost(MOUNT_DISMOUNT_COST.toSeconds()) .withStairsReluctance(STAIRS_RELUCTANCE) .build(); } diff --git a/src/test/java/org/opentripplanner/routing/core/RoutingPreferencesTest.java b/src/test/java/org/opentripplanner/routing/core/RoutingPreferencesTest.java index c67d539e902..c3cff43b1a9 100644 --- a/src/test/java/org/opentripplanner/routing/core/RoutingPreferencesTest.java +++ b/src/test/java/org/opentripplanner/routing/core/RoutingPreferencesTest.java @@ -48,6 +48,16 @@ public void copyOfWithBikeChanges() { assertSame(pref.walk(), copy.walk()); } + @Test + public void copyOfWithScooterChanges() { + var pref = new RoutingPreferences(); + var copy = pref.copyOf().withScooter(b -> b.withReluctance(2.5)).build(); + + assertNotSame(pref, copy); + assertNotSame(pref.scooter(), copy.scooter()); + assertSame(pref.walk(), copy.walk()); + } + @Test public void copyOfWithWalkChanges() { var pref = new RoutingPreferences(); diff --git a/src/test/java/org/opentripplanner/standalone/config/framework/json/NodeAdapterTest.java b/src/test/java/org/opentripplanner/standalone/config/framework/json/NodeAdapterTest.java index f975579cbf5..be44d1ea86f 100644 --- a/src/test/java/org/opentripplanner/standalone/config/framework/json/NodeAdapterTest.java +++ b/src/test/java/org/opentripplanner/standalone/config/framework/json/NodeAdapterTest.java @@ -225,15 +225,23 @@ public void asEnumMapWithCustomType() { NodeAdapter subject = newNodeAdapterForTest("{ key : { A: {a:'Foo'} } }"); assertEquals( Map.of(AnEnum.A, new ARecord("Foo")), - subject.of("key").asEnumMap(AnEnum.class, ARecord::fromJson) + subject.of("key").asEnumMap(AnEnum.class, ARecord::fromJson, Map.of()) ); assertEquals( Collections.emptyMap(), - subject.of("missing-key").asEnumMap(AnEnum.class, ARecord::fromJson) + subject.of("missing-key").asEnumMap(AnEnum.class, ARecord::fromJson, Map.of()) ); assertEquals(NON_UNUSED_PARAMETERS, unusedParams(subject)); } + @Test + public void asEnumMapWithDefaultValue() { + var subject = newNodeAdapterForTest("{}"); + final Map dflt = Map.of(AnEnum.A, new ARecord("Foo")); + assertEquals(dflt, subject.of("key").asEnumMap(AnEnum.class, ARecord::fromJson, dflt)); + assertEquals(NON_UNUSED_PARAMETERS, unusedParams(subject)); + } + @Test public void asEnumMapWithUnknownKey() { NodeAdapter subject = newNodeAdapterForTest("{ enumMap : { unknown : 7 } }"); diff --git a/src/test/java/org/opentripplanner/street/integration/BicycleRoutingTest.java b/src/test/java/org/opentripplanner/street/integration/BicycleRoutingTest.java index d5b7733f3e8..db937ff2a6e 100644 --- a/src/test/java/org/opentripplanner/street/integration/BicycleRoutingTest.java +++ b/src/test/java/org/opentripplanner/street/integration/BicycleRoutingTest.java @@ -16,7 +16,7 @@ import org.opentripplanner.routing.algorithm.mapping.GraphPathToItineraryMapper; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; -import org.opentripplanner.routing.core.BicycleOptimizeType; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.impl.GraphPathFinder; import org.opentripplanner.street.search.TemporaryVerticesContainer; @@ -76,7 +76,7 @@ private static String computePolyline(Graph graph, GenericLocation from, Generic request.setFrom(from); request.setTo(to); request.withPreferences(p -> - p.withBike(it -> it.withOptimizeType(BicycleOptimizeType.SHORTEST_DURATION)) + p.withBike(it -> it.withOptimizeType(VehicleRoutingOptimizeType.SHORTEST_DURATION)) ); request.journey().direct().setMode(StreetMode.BIKE); diff --git a/src/test/java/org/opentripplanner/street/integration/BikeWalkingTest.java b/src/test/java/org/opentripplanner/street/integration/BikeWalkingTest.java index 417f1b87347..41ec677b3bb 100644 --- a/src/test/java/org/opentripplanner/street/integration/BikeWalkingTest.java +++ b/src/test/java/org/opentripplanner/street/integration/BikeWalkingTest.java @@ -375,7 +375,10 @@ private List runStreetSearchAndCreateDescriptor( preferences .withWalk(w -> w.withSpeed(10)) .withBike(it -> - it.withSpeed(20d).withWalking(w -> w.withSpeed(5d).withHopTime(100).withHopCost(1000)) + it + .withSpeed(20d) + .withWalking(w -> w.withSpeed(5d).withMountDismountTime(100).withMountDismountCost(1000) + ) ) ); request.setArriveBy(arriveBy); diff --git a/src/test/java/org/opentripplanner/street/model/_data/StreetModelForTest.java b/src/test/java/org/opentripplanner/street/model/_data/StreetModelForTest.java index 1f3dbf8c4c9..d50c8a74dfc 100644 --- a/src/test/java/org/opentripplanner/street/model/_data/StreetModelForTest.java +++ b/src/test/java/org/opentripplanner/street/model/_data/StreetModelForTest.java @@ -9,6 +9,9 @@ import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.service.vehiclerental.model.TestFreeFloatingRentalVehicleBuilder; +import org.opentripplanner.service.vehiclerental.street.VehicleRentalPlaceVertex; +import org.opentripplanner.street.model.RentalFormFactor; import org.opentripplanner.street.model.StreetTraversalPermission; import org.opentripplanner.street.model.edge.StreetEdge; import org.opentripplanner.street.model.edge.StreetEdgeBuilder; @@ -93,4 +96,19 @@ public static StreetEdge streetEdge( ) { return streetEdge(from, to, 1, permissions); } + + public static VehicleRentalPlaceVertex rentalVertex(RentalFormFactor formFactor) { + var rentalVehicleBuilder = TestFreeFloatingRentalVehicleBuilder + .of() + .withLatitude(-122.575133) + .withLongitude(45.456773); + if (formFactor == RentalFormFactor.SCOOTER) { + rentalVehicleBuilder.withVehicleScooter(); + } else if (formFactor == RentalFormFactor.BICYCLE) { + rentalVehicleBuilder.withVehicleBicycle(); + } else if (formFactor == RentalFormFactor.CAR) { + rentalVehicleBuilder.withVehicleCar(); + } + return new VehicleRentalPlaceVertex(rentalVehicleBuilder.build()); + } } diff --git a/src/test/java/org/opentripplanner/street/model/edge/StreetEdgeScooterTraversalTest.java b/src/test/java/org/opentripplanner/street/model/edge/StreetEdgeScooterTraversalTest.java new file mode 100644 index 00000000000..734b1efa2b9 --- /dev/null +++ b/src/test/java/org/opentripplanner/street/model/edge/StreetEdgeScooterTraversalTest.java @@ -0,0 +1,255 @@ +package org.opentripplanner.street.model.edge; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.impl.PackedCoordinateSequence; +import org.opentripplanner.routing.api.request.StreetMode; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; +import org.opentripplanner.routing.util.ElevationUtils; +import org.opentripplanner.routing.util.SlopeCosts; +import org.opentripplanner.service.vehiclerental.street.StreetVehicleRentalLink; +import org.opentripplanner.service.vehiclerental.street.VehicleRentalEdge; +import org.opentripplanner.service.vehiclerental.street.VehicleRentalPlaceVertex; +import org.opentripplanner.street.model.RentalFormFactor; +import org.opentripplanner.street.model.StreetTraversalPermission; +import org.opentripplanner.street.model._data.StreetModelForTest; +import org.opentripplanner.street.model.vertex.StreetVertex; +import org.opentripplanner.street.search.TraverseMode; +import org.opentripplanner.street.search.request.StreetSearchRequest; +import org.opentripplanner.street.search.state.State; + +public class StreetEdgeScooterTraversalTest { + + private static final double DELTA = 0.00001; + private static final double SPEED = 6.0; + + @Test + public void testTraverseFloatingScooter() { + // This test does not depend on the setup method - and can probably be simplified + Coordinate c1 = new Coordinate(-122.575033, 45.456773); + Coordinate c2 = new Coordinate(-122.576668, 45.451426); + + var formFactor = RentalFormFactor.SCOOTER; + var rentalVertex = StreetModelForTest.rentalVertex(formFactor); + var vehicleRentalEdge = VehicleRentalEdge.createVehicleRentalEdge(rentalVertex, formFactor); + + StreetVertex v1 = StreetModelForTest.intersectionVertex("v1", c1.x, c1.y); + StreetVertex v2 = StreetModelForTest.intersectionVertex("v2", c2.x, c2.y); + + var link = StreetVehicleRentalLink.createStreetVehicleRentalLink(rentalVertex, v1); + + GeometryFactory factory = new GeometryFactory(); + LineString geometry = factory.createLineString(new Coordinate[] { c1, c2 }); + + double length = 650.0; + + StreetEdge testStreet = new StreetEdgeBuilder<>() + .withFromVertex(v1) + .withToVertex(v2) + .withGeometry(geometry) + .withName("Test Lane") + .withMeterLength(length) + .withPermission(StreetTraversalPermission.ALL) + .buildAndConnect(); + + var request = StreetSearchRequest.of().withMode(StreetMode.SCOOTER_RENTAL); + + request.withPreferences(pref -> pref.withScooter(scooter -> scooter.withSpeed(5))); + + State slowResult = traverseStreetFromRental( + testStreet, + vehicleRentalEdge, + link, + rentalVertex, + request.build() + ); + request.withPreferences(pref -> pref.withScooter(scooter -> scooter.withSpeed(10))); + + State fastResult = traverseStreetFromRental( + testStreet, + vehicleRentalEdge, + link, + rentalVertex, + request.build() + ); + + // Cost and time should be less when scooter speed is higher. + assertTrue(slowResult.getWeight() > fastResult.getWeight() + DELTA); + assertTrue(slowResult.getElapsedTimeSeconds() > fastResult.getElapsedTimeSeconds()); + + request.withPreferences(pref -> pref.withScooter(scooter -> scooter.withReluctance(1))); + State lowReluctanceResult = traverseStreetFromRental( + testStreet, + vehicleRentalEdge, + link, + rentalVertex, + request.build() + ); + + request.withPreferences(pref -> pref.withScooter(scooter -> scooter.withReluctance(5))); + + State highReluctanceResult = traverseStreetFromRental( + testStreet, + vehicleRentalEdge, + link, + rentalVertex, + request.build() + ); + // Cost should be more when reluctance is higher but the time should be the same. + assertTrue(highReluctanceResult.getWeight() > lowReluctanceResult.getWeight() + DELTA); + + assertEquals( + highReluctanceResult.getElapsedTimeSeconds(), + lowReluctanceResult.getElapsedTimeSeconds() + ); + } + + @Test + public void testWalkingBeforeScooter() { + StreetEdge e1 = StreetModelForTest + .streetEdgeBuilder( + StreetModelForTest.V1, + StreetModelForTest.V2, + 100.0, + StreetTraversalPermission.ALL + ) + .withCarSpeed(10.0f) + .buildAndConnect(); + + var request = StreetSearchRequest + .of() + .withPreferences(pref -> pref.withWalk(walk -> walk.withReluctance(1))) + .withMode(StreetMode.SCOOTER_RENTAL); + + State s0 = new State(StreetModelForTest.V1, request.build()); + State result = e1.traverse(s0)[0]; + + request.withPreferences(pref -> + pref.withScooter(scooter -> scooter.withReluctance(5).withSpeed(8.5)) + ); + + s0 = new State(StreetModelForTest.V1, request.build()); + var scooterReluctanceResult = e1.traverse(s0)[0]; + + // Scooter preferences shouldn't affect walking when SCOOTER_RENTAL is used as mode + assertEquals(TraverseMode.WALK, result.currentMode()); + assertEquals(result.getWeight(), scooterReluctanceResult.getWeight(), DELTA); + assertEquals(result.getElapsedTimeSeconds(), scooterReluctanceResult.getElapsedTimeSeconds()); + } + + @Test + public void testScooterOptimizeTriangle() { + // This test does not depend on the setup method - and can probably be simplified + Coordinate c1 = new Coordinate(-122.575033, 45.456773); + Coordinate c2 = new Coordinate(-122.576668, 45.451426); + + var formFactor = RentalFormFactor.SCOOTER; + + var rentalVertex = StreetModelForTest.rentalVertex(formFactor); + var vehicleRentalEdge = VehicleRentalEdge.createVehicleRentalEdge(rentalVertex, formFactor); + + StreetVertex v1 = StreetModelForTest.intersectionVertex("v1", c1.x, c1.y); + StreetVertex v2 = StreetModelForTest.intersectionVertex("v2", c2.x, c2.y); + + var link = StreetVehicleRentalLink.createStreetVehicleRentalLink(rentalVertex, v1); + + GeometryFactory factory = new GeometryFactory(); + LineString geometry = factory.createLineString(new Coordinate[] { c1, c2 }); + + double length = 650.0; + + StreetEdge testStreet = new StreetEdgeBuilder<>() + .withFromVertex(v1) + .withToVertex(v2) + .withGeometry(geometry) + .withName("Test Lane") + .withMeterLength(length) + .withPermission(StreetTraversalPermission.ALL) + // a safe street + .withBicycleSafetyFactor(0.74f) + .buildAndConnect(); + + Coordinate[] profile = new Coordinate[] { + new Coordinate(0, 0), // slope = 0.1 + new Coordinate(length / 2, length / 20.0), + new Coordinate(length, 0), // slope = -0.1 + }; + PackedCoordinateSequence elev = new PackedCoordinateSequence.Double(profile); + StreetElevationExtensionBuilder + .of(testStreet) + .withElevationProfile(elev) + .withComputed(false) + .build() + .ifPresent(testStreet::setElevationExtension); + + SlopeCosts costs = ElevationUtils.getSlopeCosts(elev, true); + double trueLength = costs.lengthMultiplier * length; + double slopeWorkLength = testStreet.getEffectiveBikeDistanceForWorkCost(); + double slopeSpeedLength = testStreet.getEffectiveBikeDistance(); + + var request = StreetSearchRequest.of().withMode(StreetMode.SCOOTER_RENTAL); + + request.withPreferences(pref -> + pref + .withScooter(scooter -> + scooter + .withSpeed(SPEED) + .withOptimizeType(VehicleRoutingOptimizeType.TRIANGLE) + .withOptimizeTriangle(it -> it.withTime(1)) + .withReluctance(1) + ) + .withWalk(walk -> walk.withReluctance(1)) + .withCar(car -> car.withReluctance(1)) + ); + + var rentedState = vehicleRentalEdge.traverse(new State(rentalVertex, request.build())); + var startState = link.traverse(rentedState[0])[0]; + + State result = testStreet.traverse(startState)[0]; + double expectedTimeWeight = slopeSpeedLength / SPEED; + assertEquals(TraverseMode.SCOOTER, result.currentMode()); + assertEquals(expectedTimeWeight, result.getWeight() - startState.getWeight(), DELTA); + + request.withPreferences(p -> + p.withScooter(scooter -> scooter.withOptimizeTriangle(it -> it.withSlope(1))) + ); + rentedState = vehicleRentalEdge.traverse(new State(rentalVertex, request.build())); + startState = link.traverse(rentedState[0])[0]; + + result = testStreet.traverse(startState)[0]; + double slopeWeight = result.getWeight(); + double expectedSlopeWeight = slopeWorkLength / SPEED; + assertEquals(expectedSlopeWeight, slopeWeight - startState.getWeight(), DELTA); + assertTrue(length * 1.5 / SPEED < slopeWeight); + assertTrue(length * 1.5 * 10 / SPEED > slopeWeight); + + request.withPreferences(p -> + p.withScooter(scooter -> scooter.withOptimizeTriangle(it -> it.withSafety(1))) + ); + rentedState = vehicleRentalEdge.traverse(new State(rentalVertex, request.build())); + startState = link.traverse(rentedState[0])[0]; + + result = testStreet.traverse(startState)[0]; + double slopeSafety = costs.slopeSafetyCost; + double safetyWeight = result.getWeight(); + double expectedSafetyWeight = (trueLength * 0.74 + slopeSafety) / SPEED; + assertEquals(expectedSafetyWeight, safetyWeight - startState.getWeight(), DELTA); + } + + private State traverseStreetFromRental( + StreetEdge streetEdge, + VehicleRentalEdge rentalEdge, + StreetVehicleRentalLink link, + VehicleRentalPlaceVertex rentalVertex, + StreetSearchRequest request + ) { + var rentedState = rentalEdge.traverse(new State(rentalVertex, request)); + var startState = link.traverse(rentedState[0])[0]; + return streetEdge.traverse(startState)[0]; + } +} diff --git a/src/test/java/org/opentripplanner/street/model/edge/StreetEdgeTest.java b/src/test/java/org/opentripplanner/street/model/edge/StreetEdgeTest.java index 42d841b9fa3..a2bf7475bdc 100644 --- a/src/test/java/org/opentripplanner/street/model/edge/StreetEdgeTest.java +++ b/src/test/java/org/opentripplanner/street/model/edge/StreetEdgeTest.java @@ -18,7 +18,7 @@ import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.impl.PackedCoordinateSequence; import org.opentripplanner.routing.api.request.StreetMode; -import org.opentripplanner.routing.core.BicycleOptimizeType; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; import org.opentripplanner.routing.util.ElevationUtils; import org.opentripplanner.routing.util.SlopeCosts; import org.opentripplanner.street.model.StreetTraversalPermission; @@ -221,7 +221,7 @@ public void testBikeSwitch() { StreetSearchRequestBuilder noPenalty = StreetSearchRequest.copyOf(proto); noPenalty.withPreferences(p -> - p.withBike(it -> it.withWalking(w -> w.withHopTime(0).withHopCost(0))) + p.withBike(it -> it.withWalking(w -> w.withMountDismountTime(0).withMountDismountCost(0))) ); State s0 = new State(v0, noPenalty.withMode(StreetMode.BIKE).build()); @@ -231,7 +231,7 @@ public void testBikeSwitch() { StreetSearchRequestBuilder withPenalty = StreetSearchRequest.copyOf(proto); withPenalty.withPreferences(p -> - p.withBike(it -> it.withWalking(w -> w.withHopTime(42).withHopCost(23))) + p.withBike(it -> it.withWalking(w -> w.withMountDismountTime(42).withMountDismountCost(23))) ); State s4 = new State(v0, withPenalty.withMode(StreetMode.BIKE).build()); @@ -357,7 +357,7 @@ public void testBikeOptimizeTriangle() { .withBike(bike -> bike .withSpeed(SPEED) - .withOptimizeType(BicycleOptimizeType.TRIANGLE) + .withOptimizeType(VehicleRoutingOptimizeType.TRIANGLE) .withOptimizeTriangle(it -> it.withTime(1)) .withReluctance(1) ) diff --git a/src/test/java/org/opentripplanner/street/model/edge/VehicleRentalEdgeTest.java b/src/test/java/org/opentripplanner/street/model/edge/VehicleRentalEdgeTest.java index 84f5efcc1c5..a3fbfcb018e 100644 --- a/src/test/java/org/opentripplanner/street/model/edge/VehicleRentalEdgeTest.java +++ b/src/test/java/org/opentripplanner/street/model/edge/VehicleRentalEdgeTest.java @@ -18,6 +18,7 @@ import org.opentripplanner.service.vehiclerental.street.VehicleRentalEdge; import org.opentripplanner.service.vehiclerental.street.VehicleRentalPlaceVertex; import org.opentripplanner.street.model.RentalFormFactor; +import org.opentripplanner.street.model._data.StreetModelForTest; import org.opentripplanner.street.search.request.StreetSearchRequest; import org.opentripplanner.street.search.state.State; import org.opentripplanner.street.search.state.VehicleRentalState; @@ -30,7 +31,7 @@ class VehicleRentalEdgeTest { VehicleRentalPlaceVertex vertex; @Test - void testRentingWithAvailableVehicles() { + void testRentingWithAvailableBikes() { initEdgeAndRequest(StreetMode.BIKE_RENTAL, 3, 3); var s1 = rent(); @@ -49,7 +50,7 @@ void testRentingWithNoAvailableVehicles() { @Test void testRentingWithNoAvailableVehiclesAndNoRealtimeUsage() { - initEdgeAndRequest(StreetMode.BIKE_RENTAL, 0, 3, false, true, false); + initEdgeAndRequest(StreetMode.BIKE_RENTAL, 0, 3, false, true, false, false); var s1 = rent(); @@ -76,7 +77,7 @@ void testReturningWithNoAvailableSpaces() { @Test void testReturningWithNoAvailableSpacesAndOverloading() { - initEdgeAndRequest(StreetMode.BIKE_RENTAL, 3, 0, true, true, true); + initEdgeAndRequest(StreetMode.BIKE_RENTAL, 3, 0, true, true, true, false); var s1 = rentAndDropOff(); @@ -85,7 +86,7 @@ void testReturningWithNoAvailableSpacesAndOverloading() { @Test void testReturningWithNoAvailableSpacesAndNoRealtimeUsage() { - initEdgeAndRequest(StreetMode.BIKE_RENTAL, 3, 0, false, true, false); + initEdgeAndRequest(StreetMode.BIKE_RENTAL, 3, 0, false, true, false, false); var s1 = rentAndDropOff(); @@ -94,7 +95,7 @@ void testReturningWithNoAvailableSpacesAndNoRealtimeUsage() { @Test void testRentingFromClosedStation() { - initEdgeAndRequest(StreetMode.BIKE_RENTAL, 3, 0, true, false, true); + initEdgeAndRequest(StreetMode.BIKE_RENTAL, 3, 0, true, false, true, false); var s1 = rent(); @@ -103,13 +104,13 @@ void testRentingFromClosedStation() { @Test void testReturningToClosedStation() { - initEdgeAndRequest(StreetMode.BIKE_RENTAL, 3, 3, true, true, true); + initEdgeAndRequest(StreetMode.BIKE_RENTAL, 3, 3, true, true, true, false); var s1 = rent(); assertFalse(State.isEmpty(s1)); - initEdgeAndRequest(StreetMode.BIKE_RENTAL, 3, 3, true, false, true); + initEdgeAndRequest(StreetMode.BIKE_RENTAL, 3, 3, true, false, true, false); var s2 = dropOff(s1[0]); @@ -118,15 +119,78 @@ void testReturningToClosedStation() { @Test void testReturningAndReturningToClosedStationWithNoRealtimeUsage() { - initEdgeAndRequest(StreetMode.BIKE_RENTAL, 3, 3, false, true, false); + initEdgeAndRequest(StreetMode.BIKE_RENTAL, 3, 3, false, true, false, false); var s1 = rentAndDropOff(); assertFalse(State.isEmpty(s1)); } + @Test + void testRentingWithFreeFloatingBicycle() { + initFreeFloatingEdgeAndRequest(StreetMode.BIKE_RENTAL, RentalFormFactor.BICYCLE, false); + + var s1 = rent(); + + assertFalse(State.isEmpty(s1)); + } + + @Test + void testRentingWithFreeFloatingScooter() { + initFreeFloatingEdgeAndRequest(StreetMode.SCOOTER_RENTAL, RentalFormFactor.SCOOTER, false); + + var s1 = rent(); + + assertFalse(State.isEmpty(s1)); + } + + @Test + void testRentingWithFreeFloatingCar() { + initFreeFloatingEdgeAndRequest(StreetMode.CAR_RENTAL, RentalFormFactor.CAR, false); + + var s1 = rent(); + + assertFalse(State.isEmpty(s1)); + } + + @Test + void testBannedBicycleNetworkStation() { + initEdgeAndRequest(StreetMode.BIKE_RENTAL, 3, 3, false, true, true, true); + + var s1 = rent(); + + assertTrue(State.isEmpty(s1)); + } + + @Test + void testBannedBicycleNetworkFreeFloating() { + initFreeFloatingEdgeAndRequest(StreetMode.BIKE_RENTAL, RentalFormFactor.BICYCLE, true); + + var s1 = rent(); + + assertTrue(State.isEmpty(s1)); + } + + @Test + void testBannedScooterNetworkFreeFloating() { + initFreeFloatingEdgeAndRequest(StreetMode.SCOOTER_RENTAL, RentalFormFactor.SCOOTER, true); + + var s1 = rent(); + + assertTrue(State.isEmpty(s1)); + } + + @Test + void testBannedCarNetworkFreeFloating() { + initFreeFloatingEdgeAndRequest(StreetMode.CAR_RENTAL, RentalFormFactor.CAR, true); + + var s1 = rent(); + + assertTrue(State.isEmpty(s1)); + } + private void initEdgeAndRequest(StreetMode mode, int vehicles, int spaces) { - initEdgeAndRequest(mode, vehicles, spaces, false, true, true); + initEdgeAndRequest(mode, vehicles, spaces, false, true, true, false); } @Nested @@ -204,7 +268,8 @@ private void initEdgeAndRequest( int spaces, boolean overloadingAllowed, boolean stationOn, - boolean useRealtime + boolean useRealtime, + boolean banNetwork ) { var station = TestVehicleRentalStationBuilder .of() @@ -218,17 +283,49 @@ private void initEdgeAndRequest( vehicleRentalEdge = VehicleRentalEdge.createVehicleRentalEdge(vertex, RentalFormFactor.BICYCLE); + Set bannedNetworks = banNetwork ? Set.of(station.getNetwork()) : Set.of(); + this.request = StreetSearchRequest .of() .withMode(mode) .withPreferences(preferences -> preferences - .withCar(car -> - car.withRental(rental -> rental.withUseAvailabilityInformation(useRealtime)) - ) .withBike(bike -> - bike.withRental(rental -> rental.withUseAvailabilityInformation(useRealtime)) + bike.withRental(rental -> + rental + .withUseAvailabilityInformation(useRealtime) + .withBannedNetworks(bannedNetworks) + ) + ) + .build() + ) + .build(); + } + + private void initFreeFloatingEdgeAndRequest( + StreetMode mode, + RentalFormFactor formFactor, + boolean banNetwork + ) { + this.vertex = StreetModelForTest.rentalVertex(formFactor); + + vehicleRentalEdge = VehicleRentalEdge.createVehicleRentalEdge(vertex, formFactor); + + Set bannedNetworks = banNetwork + ? Set.of(this.vertex.getStation().getNetwork()) + : Set.of(); + + this.request = + StreetSearchRequest + .of() + .withMode(mode) + .withPreferences(preferences -> + preferences + .withCar(car -> car.withRental(rental -> rental.withBannedNetworks(bannedNetworks))) + .withBike(bike -> bike.withRental(rental -> rental.withBannedNetworks(bannedNetworks))) + .withScooter(scooter -> + scooter.withRental(rental -> rental.withBannedNetworks(bannedNetworks)) ) .build() ) diff --git a/src/test/resources/org/opentripplanner/apis/vectortiles/style.json b/src/test/resources/org/opentripplanner/apis/vectortiles/style.json index e62c3968924..7e611171409 100644 --- a/src/test/resources/org/opentripplanner/apis/vectortiles/style.json +++ b/src/test/resources/org/opentripplanner/apis/vectortiles/style.json @@ -154,6 +154,19 @@ "fill-outline-color" : "#140d0e" } }, + { + "id" : "group-stop", + "type" : "fill", + "source" : "vectorSource", + "source-layer" : "stops", + "minzoom" : 6, + "maxzoom" : 23, + "paint" : { + "fill-color" : "#22DD9E", + "fill-opacity" : 0.5, + "fill-outline-color" : "#140d0e" + } + }, { "id" : "regular-stop", "type" : "circle", diff --git a/src/test/resources/standalone/config/router-config.json b/src/test/resources/standalone/config/router-config.json index 9293cfad8f4..2b2449a7415 100644 --- a/src/test/resources/standalone/config/router-config.json +++ b/src/test/resources/standalone/config/router-config.json @@ -57,6 +57,20 @@ "cost": 600 } }, + "scooter": { + "speed": 5, + "reluctance": 5.0, + "rental": { + "pickupCost": 120, + "dropOffTime": "30s", + "dropOffCost": 30 + }, + "triangle": { + "safety": 0.4, + "flatness": 0.3, + "time": 0.3 + } + }, "walk": { "speed": 1.3, "reluctance": 4.0,