diff --git a/.github/workflows/prune-container-images.yml b/.github/workflows/prune-container-images.yml index f3987a064a9..c1653701c3b 100644 --- a/.github/workflows/prune-container-images.yml +++ b/.github/workflows/prune-container-images.yml @@ -19,4 +19,4 @@ jobs: # remove all snapshot container images that have not been pulled for over a year # --keep-semver makes sure that any image with a x.y.z version scheme is unaffected by this pip install prune-container-repo==0.0.4 - prune-container-repo -u ${CONTAINER_REGISTRY_USER} -r ${CONTAINER_REPO} --days=90 --keep-semver --activate + prune-container-repo -u ${CONTAINER_REGISTRY_USER} -r ${CONTAINER_REPO} --days=365 --keep-semver --activate diff --git a/doc/templates/OsmMapper.md b/doc/templates/OsmMapper.md new file mode 100644 index 00000000000..1690deac627 --- /dev/null +++ b/doc/templates/OsmMapper.md @@ -0,0 +1,30 @@ +# OSM tag mapping + +This page is intended to give an overview of which OpenStreetMap(OSM) tags OTP uses to evaluate its +walking and bicycling instructions. If a tag is not part of the documentation on this page +then this tag mapper (profile) does not use it. + +The exception are access permissions and wheelchair accessibility tags like + +- `access=no` +- `wheelchair=no` +- `oneway=yes` + +These are identical for all mappers and not separately listed on this page. + +### Way properties + +Way properties set a way's permission and optionally influences its walk and bicycle safety factors. + +These factors determine how desirable an OSM way is when routing for cyclists and pedestrians. +Lower safety values make an OSM way more desirable and higher values less desirable. + + + +### Safety mixins + +Mixins are selectors that have only an effect on the bicycle and walk safety factors but not on the +permission of an OSM way. Their safety values are multiplied with the base values from the selected +way properties. Multiple mixins can apply to the same way and their effects compound. + + diff --git a/doc/templates/sandbox/VehicleRentalServiceDirectory.md b/doc/templates/sandbox/VehicleRentalServiceDirectory.md index d9908de1a90..787a17e90e4 100644 --- a/doc/templates/sandbox/VehicleRentalServiceDirectory.md +++ b/doc/templates/sandbox/VehicleRentalServiceDirectory.md @@ -1,10 +1,10 @@ -# Vehicle Rental Service Directory API support. +# Vehicle Rental Service Directory API support -This adds support for the GBFS service directory endpoint component located at -https://github.com/entur/lamassu. OTP uses the service directory to lookup and connect to all GBFS -endpoints registered in the directory. This simplifies the management of the GBFS endpoints, since -multiple services/components like OTP can connect to the directory and get the necessary -configuration from it. +This adds support for the GBFS service directory endpoint component +[Lamassu](https://github.com/entur/lamassu). +OTP uses the service directory to lookup and connects to all GBFS endpoints registered in the +directory. This simplifies the management of the GBFS endpoints, since multiple services/components +like OTP can connect to the directory and get the necessary configuration from it. ## Contact Info @@ -17,6 +17,7 @@ configuration from it. - Initial implementation of bike share updater API support - Make json tag names configurable [#3447](https://github.com/opentripplanner/OpenTripPlanner/pull/3447) - Enable GBFS geofencing with VehicleRentalServiceDirectory [#5324](https://github.com/opentripplanner/OpenTripPlanner/pull/5324) +- Enable `allowKeepingVehicleAtDestination` [#5944](https://github.com/opentripplanner/OpenTripPlanner/pull/5944) ## Configuration diff --git a/doc/user/Changelog.md b/doc/user/Changelog.md index ca97c053229..1ae81820e72 100644 --- a/doc/user/Changelog.md +++ b/doc/user/Changelog.md @@ -40,6 +40,13 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Require valid polygons for AreaStop [#5915](https://github.com/opentripplanner/OpenTripPlanner/pull/5915) - Fix NullPointerException in stop transfer priority cost vector generation [#5943](https://github.com/opentripplanner/OpenTripPlanner/pull/5943) - Convert transferSlack configuration to duration [#5897](https://github.com/opentripplanner/OpenTripPlanner/pull/5897) +- Expose stop transfer priority in Transmodel API [#5942](https://github.com/opentripplanner/OpenTripPlanner/pull/5942) +- Add rental system to GraphQL API [#5909](https://github.com/opentripplanner/OpenTripPlanner/pull/5909) +- Improve handling of SIRI added trip with unresolvable agency [#5931](https://github.com/opentripplanner/OpenTripPlanner/pull/5931) +- Fix copy-on-write in TimetableSnapshot [#5941](https://github.com/opentripplanner/OpenTripPlanner/pull/5941) +- Generate documentation for OSM tag mappers [#5929](https://github.com/opentripplanner/OpenTripPlanner/pull/5929) +- Disable Legacy REST API by default [#5948](https://github.com/opentripplanner/OpenTripPlanner/pull/5948) +- Enforce non-null coordinates on multimodal station [#5971](https://github.com/opentripplanner/OpenTripPlanner/pull/5971) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.5.0 (2024-03-13) diff --git a/doc/user/Configuration.md b/doc/user/Configuration.md index e7d30fbaae5..340cae06091 100644 --- a/doc/user/Configuration.md +++ b/doc/user/Configuration.md @@ -241,7 +241,7 @@ Here is a list of all features which can be toggled on/off and their default val | `FaresV2` | Enable import of GTFS-Fares v2 data. | | ✓️ | | `FlexRouting` | Enable FLEX routing. | | ✓️ | | `GoogleCloudStorage` | Enable Google Cloud Storage integration. | | ✓️ | -| `LegacyRestApi` | Enable legacy REST API. This API will be removed in the future. | ✓️ | ✓️ | +| `LegacyRestApi` | Enable legacy REST API. This API will be removed in the future. | | ✓️ | | `RealtimeResolver` | When routing with ignoreRealtimeUpdates=true, add an extra step which populates results with real-time data | | ✓️ | | `ReportApi` | Enable the report API. | | ✓️ | | `RestAPIPassInDefaultConfigAsJson` | Enable a default RouteRequest to be passed in as JSON on the REST API - FOR DEBUGGING ONLY! | | | diff --git a/doc/user/Container-Image.md b/doc/user/Container-Image.md index d8e6db6d614..ed2441f5ac1 100644 --- a/doc/user/Container-Image.md +++ b/doc/user/Container-Image.md @@ -4,7 +4,9 @@ The CI pipeline deploys container images for runtimes like Docker, Kubernetes or [Dockerhub](https://hub.docker.com/r/opentripplanner/opentripplanner/tags). The image assumes you use a volume to mount the input data (GTFS/NeTex, OSM) and config files into -`/var/opentripplanner/`. When serving a graph it's also expected to be in this directory. +`/var/opentripplanner/`. When serving a graph it's also expected to be in this directory. If a logback +extensions file needs to be used, it should be mounted to the root location `/logback-include-extensions.xml` +instead of the `/var/opentripplanner/` directory. ## Quick start diff --git a/doc/user/apis/Apis.md b/doc/user/apis/Apis.md index ab6b41a25cd..9e8f31f6eeb 100644 --- a/doc/user/apis/Apis.md +++ b/doc/user/apis/Apis.md @@ -25,5 +25,5 @@ The [Geocoder API](../sandbox/GeocoderAPI.md) allows you to geocode stop names a The OTP REST API used to power many apps and frontends. For years it was the only way to access OTP programmatically. -Over time it has been replaced by the GraphQL APIs and is scheduled to be disabled by default -and eventually removed completely. It's therefore not recommended to use it. +Over time it has been replaced by the GraphQL APIs and is now disabled by default +and will eventually be removed completely. It's therefore not recommended to use it. diff --git a/doc/user/osm/Default.md b/doc/user/osm/Default.md new file mode 100644 index 00000000000..814420b791f --- /dev/null +++ b/doc/user/osm/Default.md @@ -0,0 +1,214 @@ +# OSM tag mapping + +This page is intended to give an overview of which OpenStreetMap(OSM) tags OTP uses to evaluate its +walking and bicycling instructions. If a tag is not part of the documentation on this page +then this tag mapper (profile) does not use it. + +The exception are access permissions and wheelchair accessibility tags like + +- `access=no` +- `wheelchair=no` +- `oneway=yes` + +These are identical for all mappers and not separately listed on this page. + +### Way properties + +Way properties set a way's permission and optionally influences its walk and bicycle safety factors. + +These factors determine how desirable an OSM way is when routing for cyclists and pedestrians. +Lower safety values make an OSM way more desirable and higher values less desirable. + + + + +| specifier | permission | bike safety | walk safety | +|---------------------------------------------------------|--------------------------|-------------------------------|-------------| +| `mtb:scale=3` | `NONE` | | | +| `mtb:scale=4` | `NONE` | | | +| `mtb:scale=5` | `NONE` | | | +| `mtb:scale=6` | `NONE` | | | +| `highway=corridor` | `PEDESTRIAN` | | | +| `highway=steps` | `PEDESTRIAN` | | | +| `highway=crossing` | `PEDESTRIAN` | | | +| `highway=platform` | `PEDESTRIAN` | | | +| `public_transport=platform` | `PEDESTRIAN` | | | +| `railway=platform` | `PEDESTRIAN` | | | +| `footway=sidewalk; highway=footway` | `PEDESTRIAN` | | | +| `mtb:scale=1` | `PEDESTRIAN` | | | +| `mtb:scale=2` | `PEDESTRIAN` | | | +| `mtb:scale=0` | `PEDESTRIAN_AND_BICYCLE` | | | +| `highway=cycleway` | `PEDESTRIAN_AND_BICYCLE` | 0.6 | | +| `highway=path` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `highway=pedestrian` | `PEDESTRIAN_AND_BICYCLE` | 0.9 | | +| `highway=footway` | `PEDESTRIAN_AND_BICYCLE` | 1.1 | | +| `highway=bridleway` | `PEDESTRIAN_AND_BICYCLE` | 1.3 | | +| `highway=living_street` | `ALL` | 0.9 | | +| `highway=unclassified` | `ALL` | | | +| `highway=road` | `ALL` | | | +| `highway=byway` | `ALL` | 1.3 | | +| `highway=track` | `ALL` | 1.3 | | +| `highway=service` | `ALL` | 1.1 | | +| `highway=residential` | `ALL` | 0.98 | | +| `highway=residential_link` | `ALL` | 0.98 | | +| `highway=tertiary` | `ALL` | | | +| `highway=tertiary_link` | `ALL` | | | +| `highway=secondary` | `ALL` | 1.5 | | +| `highway=secondary_link` | `ALL` | 1.5 | | +| `highway=primary` | `ALL` | 2.06 | | +| `highway=primary_link` | `ALL` | 2.06 | | +| `highway=trunk_link` | `CAR` | 2.06 | | +| `highway=motorway_link` | `CAR` | 2.06 | | +| `highway=trunk` | `CAR` | 7.47 | | +| `highway=motorway` | `CAR` | 8.0 | | +| `present(highway); cycleway=lane` | `PEDESTRIAN_AND_BICYCLE` | 0.87 | | +| `highway=service; cycleway=lane` | `ALL` | 0.77 | | +| `highway=residential; cycleway=lane` | `ALL` | 0.77 | | +| `highway=residential_link; cycleway=lane` | `ALL` | 0.77 | | +| `highway=tertiary; cycleway=lane` | `ALL` | 0.87 | | +| `highway=tertiary_link; cycleway=lane` | `ALL` | 0.87 | | +| `highway=secondary; cycleway=lane` | `ALL` | 0.96 | | +| `highway=secondary_link; cycleway=lane` | `ALL` | 0.96 | | +| `highway=primary; cycleway=lane` | `ALL` | 1.15 | | +| `highway=primary_link; cycleway=lane` | `ALL` | 1.15 | | +| `highway=trunk; cycleway=lane` | `BICYCLE_AND_CAR` | 1.5 | | +| `highway=trunk_link; cycleway=lane` | `BICYCLE_AND_CAR` | 1.15 | | +| `highway=motorway; cycleway=lane` | `BICYCLE_AND_CAR` | 2.0 | | +| `highway=motorway_link; cycleway=lane` | `BICYCLE_AND_CAR` | 1.15 | | +| `present(highway); cycleway=share_busway` | `PEDESTRIAN_AND_BICYCLE` | 0.92 | | +| `highway=service; cycleway=share_busway` | `ALL` | 0.85 | | +| `highway=residential; cycleway=share_busway` | `ALL` | 0.85 | | +| `highway=residential_link; cycleway=share_busway` | `ALL` | 0.85 | | +| `highway=tertiary; cycleway=share_busway` | `ALL` | 0.92 | | +| `highway=tertiary_link; cycleway=share_busway` | `ALL` | 0.92 | | +| `highway=secondary; cycleway=share_busway` | `ALL` | 0.99 | | +| `highway=secondary_link; cycleway=share_busway` | `ALL` | 0.99 | | +| `highway=primary; cycleway=share_busway` | `ALL` | 1.25 | | +| `highway=primary_link; cycleway=share_busway` | `ALL` | 1.25 | | +| `highway=trunk; cycleway=share_busway` | `BICYCLE_AND_CAR` | 1.75 | | +| `highway=trunk_link; cycleway=share_busway` | `BICYCLE_AND_CAR` | 1.25 | | +| `highway=motorway; cycleway=share_busway` | `BICYCLE_AND_CAR` | 2.5 | | +| `highway=motorway_link; cycleway=share_busway` | `BICYCLE_AND_CAR` | 1.25 | | +| `present(highway); cycleway=opposite_lane` | `PEDESTRIAN_AND_BICYCLE` | forward: 1.0
back: 0.87 | | +| `highway=service; cycleway=opposite_lane` | `ALL` | forward: 1.1
back: 0.77 | | +| `highway=residential; cycleway=opposite_lane` | `ALL` | forward: 0.98
back: 0.77 | | +| `highway=residential_link; cycleway=opposite_lane` | `ALL` | forward: 0.98
back: 0.77 | | +| `highway=tertiary; cycleway=opposite_lane` | `ALL` | forward: 1.0
back: 0.87 | | +| `highway=tertiary_link; cycleway=opposite_lane` | `ALL` | forward: 1.0
back: 0.87 | | +| `highway=secondary; cycleway=opposite_lane` | `ALL` | forward: 1.5
back: 0.96 | | +| `highway=secondary_link; cycleway=opposite_lane` | `ALL` | forward: 1.5
back: 0.96 | | +| `highway=primary; cycleway=opposite_lane` | `ALL` | forward: 2.06
back: 1.15 | | +| `highway=primary_link; cycleway=opposite_lane` | `ALL` | forward: 2.06
back: 1.15 | | +| `highway=trunk; cycleway=opposite_lane` | `BICYCLE_AND_CAR` | forward: 7.47
back: 1.5 | | +| `highway=trunk_link; cycleway=opposite_lane` | `BICYCLE_AND_CAR` | forward: 2.06
back: 1.15 | | +| `present(highway); cycleway=track` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `highway=service; cycleway=track` | `ALL` | 0.65 | | +| `highway=residential; cycleway=track` | `ALL` | 0.65 | | +| `highway=residential_link; cycleway=track` | `ALL` | 0.65 | | +| `highway=tertiary; cycleway=track` | `ALL` | 0.75 | | +| `highway=tertiary_link; cycleway=track` | `ALL` | 0.75 | | +| `highway=secondary; cycleway=track` | `ALL` | 0.8 | | +| `highway=secondary_link; cycleway=track` | `ALL` | 0.8 | | +| `highway=primary; cycleway=track` | `ALL` | 0.85 | | +| `highway=primary_link; cycleway=track` | `ALL` | 0.85 | | +| `highway=trunk; cycleway=track` | `BICYCLE_AND_CAR` | 0.95 | | +| `highway=trunk_link; cycleway=track` | `BICYCLE_AND_CAR` | 0.85 | | +| `present(highway); cycleway=opposite_track` | `PEDESTRIAN_AND_BICYCLE` | forward: 1.0
back: 0.75 | | +| `highway=service; cycleway=opposite_track` | `ALL` | forward: 1.1
back: 0.65 | | +| `highway=residential; cycleway=opposite_track` | `ALL` | forward: 0.98
back: 0.65 | | +| `highway=residential_link; cycleway=opposite_track` | `ALL` | forward: 0.98
back: 0.65 | | +| `highway=tertiary; cycleway=opposite_track` | `ALL` | forward: 1.0
back: 0.75 | | +| `highway=tertiary_link; cycleway=opposite_track` | `ALL` | forward: 1.0
back: 0.75 | | +| `highway=secondary; cycleway=opposite_track` | `ALL` | forward: 1.5
back: 0.8 | | +| `highway=secondary_link; cycleway=opposite_track` | `ALL` | forward: 1.5
back: 0.8 | | +| `highway=primary; cycleway=opposite_track` | `ALL` | forward: 2.06
back: 0.85 | | +| `highway=primary_link; cycleway=opposite_track` | `ALL` | forward: 2.06
back: 0.85 | | +| `highway=trunk; cycleway=opposite_track` | `BICYCLE_AND_CAR` | forward: 7.47
back: 0.95 | | +| `highway=trunk_link; cycleway=opposite_track` | `BICYCLE_AND_CAR` | forward: 2.06
back: 0.85 | | +| `present(highway); cycleway=shared_lane` | `PEDESTRIAN_AND_BICYCLE` | 0.77 | | +| `highway=service; cycleway=shared_lane` | `ALL` | 0.73 | | +| `highway=residential; cycleway=shared_lane` | `ALL` | 0.77 | | +| `highway=residential_link; cycleway=shared_lane` | `ALL` | 0.77 | | +| `highway=tertiary; cycleway=shared_lane` | `ALL` | 0.83 | | +| `highway=tertiary_link; cycleway=shared_lane` | `ALL` | 0.83 | | +| `highway=secondary; cycleway=shared_lane` | `ALL` | 1.25 | | +| `highway=secondary_link; cycleway=shared_lane` | `ALL` | 1.25 | | +| `highway=primary; cycleway=shared_lane` | `ALL` | 1.75 | | +| `highway=primary_link; cycleway=shared_lane` | `ALL` | 1.75 | | +| `present(highway); cycleway=opposite` | `PEDESTRIAN_AND_BICYCLE` | forward: 1.0
back: 1.4 | | +| `highway=service; cycleway=opposite` | `ALL` | 1.1 | | +| `highway=residential; cycleway=opposite` | `ALL` | 0.98 | | +| `highway=residential_link; cycleway=opposite` | `ALL` | 0.98 | | +| `highway=tertiary; cycleway=opposite` | `ALL` | | | +| `highway=tertiary_link; cycleway=opposite` | `ALL` | | | +| `highway=secondary; cycleway=opposite` | `ALL` | forward: 1.5
back: 1.71 | | +| `highway=secondary_link; cycleway=opposite` | `ALL` | forward: 1.5
back: 1.71 | | +| `highway=primary; cycleway=opposite` | `ALL` | forward: 2.06
back: 2.99 | | +| `highway=primary_link; cycleway=opposite` | `ALL` | forward: 2.06
back: 2.99 | | +| `highway=path; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.6 | | +| `highway=footway; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `highway=footway; bicycle=yes; area=yes` | `PEDESTRIAN_AND_BICYCLE` | 0.9 | | +| `highway=pedestrian; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `footway=sidewalk; highway=footway; bicycle=yes` | `PEDESTRIAN_AND_BICYCLE` | 2.5 | | +| `footway=sidewalk; highway=footway; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 1.1 | | +| `highway=footway; footway=crossing` | `PEDESTRIAN_AND_BICYCLE` | 2.5 | | +| `highway=footway; footway=crossing; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 1.1 | | +| `highway=track; bicycle=yes` | `PEDESTRIAN_AND_BICYCLE` | 1.18 | | +| `highway=track; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.99 | | +| `highway=track; bicycle=yes; present(surface)` | `PEDESTRIAN_AND_BICYCLE` | 1.18 | | +| `highway=track; bicycle=designated; present(surface)` | `PEDESTRIAN_AND_BICYCLE` | 0.99 | | +| `highway=track; present(surface)` | `PEDESTRIAN_AND_BICYCLE` | 1.3 | | +| `present(highway); bicycle=designated` | `ALL` | 0.97 | | +| `highway=service; bicycle=designated` | `ALL` | 0.84 | | +| `highway=residential; bicycle=designated` | `ALL` | 0.95 | | +| `highway=unclassified; bicycle=designated` | `ALL` | 0.95 | | +| `highway=residential_link; bicycle=designated` | `ALL` | 0.95 | | +| `highway=tertiary; bicycle=designated` | `ALL` | 0.97 | | +| `highway=tertiary_link; bicycle=designated` | `ALL` | 0.97 | | +| `highway=secondary; bicycle=designated` | `ALL` | 1.46 | | +| `highway=secondary_link; bicycle=designated` | `ALL` | 1.46 | | +| `highway=primary; bicycle=designated` | `ALL` | 2.0 | | +| `highway=primary_link; bicycle=designated` | `ALL` | 2.0 | | +| `highway=trunk; bicycle=designated` | `BICYCLE_AND_CAR` | 7.25 | | +| `highway=trunk_link; bicycle=designated` | `BICYCLE_AND_CAR` | 2.0 | | +| `highway=motorway; bicycle=designated` | `BICYCLE_AND_CAR` | 7.76 | | +| `highway=motorway_link; bicycle=designated` | `BICYCLE_AND_CAR` | 2.0 | | + + + +### Safety mixins + +Mixins are selectors that have only an effect on the bicycle and walk safety factors but not on the +permission of an OSM way. Their safety values are multiplied with the base values from the selected +way properties. Multiple mixins can apply to the same way and their effects compound. + + + + +| matcher | bicycle safety | walk safety | +|------------------------------------------------------------|----------------|-------------| +| `lcn=yes¦rcn=yes¦ncn=yes¦bicycle_road=yes¦cyclestreet=yes` | 0.7 | | +| `surface=unpaved` | 1.18 | | +| `surface=compacted` | 1.18 | | +| `surface=wood` | 1.18 | | +| `surface=cobblestone` | 1.3 | | +| `surface=sett` | 1.3 | | +| `surface=unhewn_cobblestone` | 1.5 | | +| `surface=grass_paver` | 1.3 | | +| `surface=pebblestone` | 1.3 | | +| `surface=metal` | 1.3 | | +| `surface=ground` | 1.5 | | +| `surface=dirt` | 1.5 | | +| `surface=earth` | 1.5 | | +| `surface=grass` | 1.5 | | +| `surface=mud` | 1.5 | | +| `surface=woodchip` | 1.5 | | +| `surface=gravel` | 1.5 | | +| `surface=artifical_turf` | 1.5 | | +| `surface=sand` | 100.0 | | +| `foot=discouraged` | | 3.0 | +| `bicycle=discouraged` | 3.0 | | +| `foot=use_sidepath` | | 5.0 | +| `bicycle=use_sidepath` | 5.0 | | + + diff --git a/doc/user/osm/Finland.md b/doc/user/osm/Finland.md new file mode 100644 index 00000000000..8a60b5f0b13 --- /dev/null +++ b/doc/user/osm/Finland.md @@ -0,0 +1,259 @@ +# OSM tag mapping + +This page is intended to give an overview of which OpenStreetMap(OSM) tags OTP uses to evaluate its +walking and bicycling instructions. If a tag is not part of the documentation on this page +then this tag mapper (profile) does not use it. + +The exception are access permissions and wheelchair accessibility tags like + +- `access=no` +- `wheelchair=no` +- `oneway=yes` + +These are identical for all mappers and not separately listed on this page. + +### Way properties + +Way properties set a way's permission and optionally influences its walk and bicycle safety factors. + +These factors determine how desirable an OSM way is when routing for cyclists and pedestrians. +Lower safety values make an OSM way more desirable and higher values less desirable. + + + + +| specifier | permission | bike safety | walk safety | +|---------------------------------------------------------------------------------|--------------------------|-------------------------------|-------------| +| `highway=living_street` | `ALL` | 0.9 | | +| `highway=unclassified` | `ALL` | | | +| `highway=road` | `ALL` | | | +| `highway=byway` | `ALL` | 1.3 | | +| `highway=track` | `ALL` | 1.3 | | +| `highway=service` | `ALL` | 1.1 | | +| `highway=residential` | `ALL` | 0.98 | | +| `highway=residential_link` | `ALL` | 0.98 | | +| `highway=tertiary` | `ALL` | | | +| `highway=tertiary_link` | `ALL` | | | +| `highway=secondary` | `ALL` | 1.5 | | +| `highway=secondary_link` | `ALL` | 1.5 | | +| `highway=primary` | `ALL` | 2.06 | | +| `highway=primary_link` | `ALL` | 2.06 | | +| `highway=trunk_link` | `ALL` | 2.06 | | +| `highway=trunk` | `ALL` | 7.47 | | +| `highway=trunk; tunnel=yes` | `CAR` | 7.47 | | +| `motorroad=yes` | `CAR` | 7.47 | | +| `present(highway); informal=yes` | `NONE` | | | +| `highway=service; access=private` | `NONE` | | | +| `highway=trail` | `NONE` | | | +| `present(highway); seasonal=winter` | `NONE` | | | +| `present(highway); ice_road=yes` | `NONE` | | | +| `present(highway); winter_road=yes` | `NONE` | | | +| `highway=footway` | `PEDESTRIAN` | | | +| `footway=sidewalk; highway=footway` | `PEDESTRIAN` | | | +| `highway=cycleway; segregated=yes` | `PEDESTRIAN_AND_BICYCLE` | 0.6 | 1.1 | +| `highway=footway; bridge=yes` | `PEDESTRIAN` | | | +| `highway=footway; tunnel=yes` | `PEDESTRIAN` | | | +| `highway=cycleway; bridge=yes` | `PEDESTRIAN_AND_BICYCLE` | 0.6 | | +| `highway=cycleway; tunnel=yes` | `PEDESTRIAN_AND_BICYCLE` | 0.6 | | +| `highway=footway; footway=crossing; crossing=traffic_signals` | `PEDESTRIAN` | | 1.1 | +| `highway=footway; footway=crossing` | `PEDESTRIAN` | | 1.2 | +| `highway=cycleway; cycleway=crossing; segregated=yes; crossing=traffic_signals` | `PEDESTRIAN_AND_BICYCLE` | 0.8 | 1.1 | +| `highway=cycleway; footway=crossing; segregated=yes; crossing=traffic_signals` | `PEDESTRIAN` | 0.8 | 1.1 | +| `highway=cycleway; cycleway=crossing; segregated=yes` | `PEDESTRIAN_AND_BICYCLE` | 1.2 | 1.2 | +| `highway=cycleway; footway=crossing; segregated=yes` | `PEDESTRIAN` | 1.2 | 1.2 | +| `highway=cycleway; cycleway=crossing; crossing=traffic_signals` | `PEDESTRIAN_AND_BICYCLE` | 0.8 | 1.15 | +| `highway=cycleway; footway=crossing; crossing=traffic_signals` | `PEDESTRIAN_AND_BICYCLE` | 0.8 | 1.15 | +| `highway=cycleway; cycleway=crossing` | `PEDESTRIAN_AND_BICYCLE` | 1.2 | 1.25 | +| `highway=cycleway; footway=crossing` | `PEDESTRIAN_AND_BICYCLE` | 1.2 | 1.25 | +| `highway=cycleway; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.6 | | +| `highway=service; tunnel=yes; access=destination` | `NONE` | | | +| `highway=service; access=destination` | `ALL` | 1.1 | | +| `mtb:scale=3` | `NONE` | | | +| `mtb:scale=4` | `NONE` | | | +| `mtb:scale=5` | `NONE` | | | +| `mtb:scale=6` | `NONE` | | | +| `highway=corridor` | `PEDESTRIAN` | | | +| `highway=steps` | `PEDESTRIAN` | | | +| `highway=crossing` | `PEDESTRIAN` | | | +| `highway=platform` | `PEDESTRIAN` | | | +| `public_transport=platform` | `PEDESTRIAN` | | | +| `railway=platform` | `PEDESTRIAN` | | | +| `footway=sidewalk; highway=footway` | `PEDESTRIAN` | | | +| `mtb:scale=1` | `PEDESTRIAN` | | | +| `mtb:scale=2` | `PEDESTRIAN` | | | +| `mtb:scale=0` | `PEDESTRIAN_AND_BICYCLE` | | | +| `highway=cycleway` | `PEDESTRIAN_AND_BICYCLE` | 0.6 | | +| `highway=path` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `highway=pedestrian` | `PEDESTRIAN_AND_BICYCLE` | 0.9 | | +| `highway=footway` | `PEDESTRIAN_AND_BICYCLE` | 1.1 | | +| `highway=bridleway` | `PEDESTRIAN_AND_BICYCLE` | 1.3 | | +| `highway=living_street` | `ALL` | 0.9 | | +| `highway=unclassified` | `ALL` | | | +| `highway=road` | `ALL` | | | +| `highway=byway` | `ALL` | 1.3 | | +| `highway=track` | `ALL` | 1.3 | | +| `highway=service` | `ALL` | 1.1 | | +| `highway=residential` | `ALL` | 0.98 | | +| `highway=residential_link` | `ALL` | 0.98 | | +| `highway=tertiary` | `ALL` | | | +| `highway=tertiary_link` | `ALL` | | | +| `highway=secondary` | `ALL` | 1.5 | | +| `highway=secondary_link` | `ALL` | 1.5 | | +| `highway=primary` | `ALL` | 2.06 | | +| `highway=primary_link` | `ALL` | 2.06 | | +| `highway=trunk_link` | `CAR` | 2.06 | | +| `highway=motorway_link` | `CAR` | 2.06 | | +| `highway=trunk` | `CAR` | 7.47 | | +| `highway=motorway` | `CAR` | 8.0 | | +| `present(highway); cycleway=lane` | `PEDESTRIAN_AND_BICYCLE` | 0.87 | | +| `highway=service; cycleway=lane` | `ALL` | 0.77 | | +| `highway=residential; cycleway=lane` | `ALL` | 0.77 | | +| `highway=residential_link; cycleway=lane` | `ALL` | 0.77 | | +| `highway=tertiary; cycleway=lane` | `ALL` | 0.87 | | +| `highway=tertiary_link; cycleway=lane` | `ALL` | 0.87 | | +| `highway=secondary; cycleway=lane` | `ALL` | 0.96 | | +| `highway=secondary_link; cycleway=lane` | `ALL` | 0.96 | | +| `highway=primary; cycleway=lane` | `ALL` | 1.15 | | +| `highway=primary_link; cycleway=lane` | `ALL` | 1.15 | | +| `highway=trunk; cycleway=lane` | `BICYCLE_AND_CAR` | 1.5 | | +| `highway=trunk_link; cycleway=lane` | `BICYCLE_AND_CAR` | 1.15 | | +| `highway=motorway; cycleway=lane` | `BICYCLE_AND_CAR` | 2.0 | | +| `highway=motorway_link; cycleway=lane` | `BICYCLE_AND_CAR` | 1.15 | | +| `present(highway); cycleway=share_busway` | `PEDESTRIAN_AND_BICYCLE` | 0.92 | | +| `highway=service; cycleway=share_busway` | `ALL` | 0.85 | | +| `highway=residential; cycleway=share_busway` | `ALL` | 0.85 | | +| `highway=residential_link; cycleway=share_busway` | `ALL` | 0.85 | | +| `highway=tertiary; cycleway=share_busway` | `ALL` | 0.92 | | +| `highway=tertiary_link; cycleway=share_busway` | `ALL` | 0.92 | | +| `highway=secondary; cycleway=share_busway` | `ALL` | 0.99 | | +| `highway=secondary_link; cycleway=share_busway` | `ALL` | 0.99 | | +| `highway=primary; cycleway=share_busway` | `ALL` | 1.25 | | +| `highway=primary_link; cycleway=share_busway` | `ALL` | 1.25 | | +| `highway=trunk; cycleway=share_busway` | `BICYCLE_AND_CAR` | 1.75 | | +| `highway=trunk_link; cycleway=share_busway` | `BICYCLE_AND_CAR` | 1.25 | | +| `highway=motorway; cycleway=share_busway` | `BICYCLE_AND_CAR` | 2.5 | | +| `highway=motorway_link; cycleway=share_busway` | `BICYCLE_AND_CAR` | 1.25 | | +| `present(highway); cycleway=opposite_lane` | `PEDESTRIAN_AND_BICYCLE` | forward: 1.0
back: 0.87 | | +| `highway=service; cycleway=opposite_lane` | `ALL` | forward: 1.1
back: 0.77 | | +| `highway=residential; cycleway=opposite_lane` | `ALL` | forward: 0.98
back: 0.77 | | +| `highway=residential_link; cycleway=opposite_lane` | `ALL` | forward: 0.98
back: 0.77 | | +| `highway=tertiary; cycleway=opposite_lane` | `ALL` | forward: 1.0
back: 0.87 | | +| `highway=tertiary_link; cycleway=opposite_lane` | `ALL` | forward: 1.0
back: 0.87 | | +| `highway=secondary; cycleway=opposite_lane` | `ALL` | forward: 1.5
back: 0.96 | | +| `highway=secondary_link; cycleway=opposite_lane` | `ALL` | forward: 1.5
back: 0.96 | | +| `highway=primary; cycleway=opposite_lane` | `ALL` | forward: 2.06
back: 1.15 | | +| `highway=primary_link; cycleway=opposite_lane` | `ALL` | forward: 2.06
back: 1.15 | | +| `highway=trunk; cycleway=opposite_lane` | `BICYCLE_AND_CAR` | forward: 7.47
back: 1.5 | | +| `highway=trunk_link; cycleway=opposite_lane` | `BICYCLE_AND_CAR` | forward: 2.06
back: 1.15 | | +| `present(highway); cycleway=track` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `highway=service; cycleway=track` | `ALL` | 0.65 | | +| `highway=residential; cycleway=track` | `ALL` | 0.65 | | +| `highway=residential_link; cycleway=track` | `ALL` | 0.65 | | +| `highway=tertiary; cycleway=track` | `ALL` | 0.75 | | +| `highway=tertiary_link; cycleway=track` | `ALL` | 0.75 | | +| `highway=secondary; cycleway=track` | `ALL` | 0.8 | | +| `highway=secondary_link; cycleway=track` | `ALL` | 0.8 | | +| `highway=primary; cycleway=track` | `ALL` | 0.85 | | +| `highway=primary_link; cycleway=track` | `ALL` | 0.85 | | +| `highway=trunk; cycleway=track` | `BICYCLE_AND_CAR` | 0.95 | | +| `highway=trunk_link; cycleway=track` | `BICYCLE_AND_CAR` | 0.85 | | +| `present(highway); cycleway=opposite_track` | `PEDESTRIAN_AND_BICYCLE` | forward: 1.0
back: 0.75 | | +| `highway=service; cycleway=opposite_track` | `ALL` | forward: 1.1
back: 0.65 | | +| `highway=residential; cycleway=opposite_track` | `ALL` | forward: 0.98
back: 0.65 | | +| `highway=residential_link; cycleway=opposite_track` | `ALL` | forward: 0.98
back: 0.65 | | +| `highway=tertiary; cycleway=opposite_track` | `ALL` | forward: 1.0
back: 0.75 | | +| `highway=tertiary_link; cycleway=opposite_track` | `ALL` | forward: 1.0
back: 0.75 | | +| `highway=secondary; cycleway=opposite_track` | `ALL` | forward: 1.5
back: 0.8 | | +| `highway=secondary_link; cycleway=opposite_track` | `ALL` | forward: 1.5
back: 0.8 | | +| `highway=primary; cycleway=opposite_track` | `ALL` | forward: 2.06
back: 0.85 | | +| `highway=primary_link; cycleway=opposite_track` | `ALL` | forward: 2.06
back: 0.85 | | +| `highway=trunk; cycleway=opposite_track` | `BICYCLE_AND_CAR` | forward: 7.47
back: 0.95 | | +| `highway=trunk_link; cycleway=opposite_track` | `BICYCLE_AND_CAR` | forward: 2.06
back: 0.85 | | +| `present(highway); cycleway=shared_lane` | `PEDESTRIAN_AND_BICYCLE` | 0.77 | | +| `highway=service; cycleway=shared_lane` | `ALL` | 0.73 | | +| `highway=residential; cycleway=shared_lane` | `ALL` | 0.77 | | +| `highway=residential_link; cycleway=shared_lane` | `ALL` | 0.77 | | +| `highway=tertiary; cycleway=shared_lane` | `ALL` | 0.83 | | +| `highway=tertiary_link; cycleway=shared_lane` | `ALL` | 0.83 | | +| `highway=secondary; cycleway=shared_lane` | `ALL` | 1.25 | | +| `highway=secondary_link; cycleway=shared_lane` | `ALL` | 1.25 | | +| `highway=primary; cycleway=shared_lane` | `ALL` | 1.75 | | +| `highway=primary_link; cycleway=shared_lane` | `ALL` | 1.75 | | +| `present(highway); cycleway=opposite` | `PEDESTRIAN_AND_BICYCLE` | forward: 1.0
back: 1.4 | | +| `highway=service; cycleway=opposite` | `ALL` | 1.1 | | +| `highway=residential; cycleway=opposite` | `ALL` | 0.98 | | +| `highway=residential_link; cycleway=opposite` | `ALL` | 0.98 | | +| `highway=tertiary; cycleway=opposite` | `ALL` | | | +| `highway=tertiary_link; cycleway=opposite` | `ALL` | | | +| `highway=secondary; cycleway=opposite` | `ALL` | forward: 1.5
back: 1.71 | | +| `highway=secondary_link; cycleway=opposite` | `ALL` | forward: 1.5
back: 1.71 | | +| `highway=primary; cycleway=opposite` | `ALL` | forward: 2.06
back: 2.99 | | +| `highway=primary_link; cycleway=opposite` | `ALL` | forward: 2.06
back: 2.99 | | +| `highway=path; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.6 | | +| `highway=footway; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `highway=footway; bicycle=yes; area=yes` | `PEDESTRIAN_AND_BICYCLE` | 0.9 | | +| `highway=pedestrian; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `footway=sidewalk; highway=footway; bicycle=yes` | `PEDESTRIAN_AND_BICYCLE` | 2.5 | | +| `footway=sidewalk; highway=footway; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 1.1 | | +| `highway=footway; footway=crossing` | `PEDESTRIAN_AND_BICYCLE` | 2.5 | | +| `highway=footway; footway=crossing; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 1.1 | | +| `highway=track; bicycle=yes` | `PEDESTRIAN_AND_BICYCLE` | 1.18 | | +| `highway=track; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.99 | | +| `highway=track; bicycle=yes; present(surface)` | `PEDESTRIAN_AND_BICYCLE` | 1.18 | | +| `highway=track; bicycle=designated; present(surface)` | `PEDESTRIAN_AND_BICYCLE` | 0.99 | | +| `highway=track; present(surface)` | `PEDESTRIAN_AND_BICYCLE` | 1.3 | | +| `present(highway); bicycle=designated` | `ALL` | 0.97 | | +| `highway=service; bicycle=designated` | `ALL` | 0.84 | | +| `highway=residential; bicycle=designated` | `ALL` | 0.95 | | +| `highway=unclassified; bicycle=designated` | `ALL` | 0.95 | | +| `highway=residential_link; bicycle=designated` | `ALL` | 0.95 | | +| `highway=tertiary; bicycle=designated` | `ALL` | 0.97 | | +| `highway=tertiary_link; bicycle=designated` | `ALL` | 0.97 | | +| `highway=secondary; bicycle=designated` | `ALL` | 1.46 | | +| `highway=secondary_link; bicycle=designated` | `ALL` | 1.46 | | +| `highway=primary; bicycle=designated` | `ALL` | 2.0 | | +| `highway=primary_link; bicycle=designated` | `ALL` | 2.0 | | +| `highway=trunk; bicycle=designated` | `BICYCLE_AND_CAR` | 7.25 | | +| `highway=trunk_link; bicycle=designated` | `BICYCLE_AND_CAR` | 2.0 | | +| `highway=motorway; bicycle=designated` | `BICYCLE_AND_CAR` | 7.76 | | +| `highway=motorway_link; bicycle=designated` | `BICYCLE_AND_CAR` | 2.0 | | + + + +### Safety mixins + +Mixins are selectors that have only an effect on the bicycle and walk safety factors but not on the +permission of an OSM way. Their safety values are multiplied with the base values from the selected +way properties. Multiple mixins can apply to the same way and their effects compound. + + + + +| matcher | bicycle safety | walk safety | +|------------------------------------------------------------|----------------|-------------| +| `bicycle=use_sidepath` | | 5.0 | +| `lcn=yes¦rcn=yes¦ncn=yes¦bicycle_road=yes¦cyclestreet=yes` | 0.7 | | +| `surface=unpaved` | 1.18 | | +| `surface=compacted` | 1.18 | | +| `surface=wood` | 1.18 | | +| `surface=cobblestone` | 1.3 | | +| `surface=sett` | 1.3 | | +| `surface=unhewn_cobblestone` | 1.5 | | +| `surface=grass_paver` | 1.3 | | +| `surface=pebblestone` | 1.3 | | +| `surface=metal` | 1.3 | | +| `surface=ground` | 1.5 | | +| `surface=dirt` | 1.5 | | +| `surface=earth` | 1.5 | | +| `surface=grass` | 1.5 | | +| `surface=mud` | 1.5 | | +| `surface=woodchip` | 1.5 | | +| `surface=gravel` | 1.5 | | +| `surface=artifical_turf` | 1.5 | | +| `surface=sand` | 100.0 | | +| `foot=discouraged` | | 3.0 | +| `bicycle=discouraged` | 3.0 | | +| `foot=use_sidepath` | | 5.0 | +| `bicycle=use_sidepath` | 5.0 | | + + diff --git a/doc/user/osm/Germany.md b/doc/user/osm/Germany.md new file mode 100644 index 00000000000..922aa3af836 --- /dev/null +++ b/doc/user/osm/Germany.md @@ -0,0 +1,234 @@ +# OSM tag mapping + +This page is intended to give an overview of which OpenStreetMap(OSM) tags OTP uses to evaluate its +walking and bicycling instructions. If a tag is not part of the documentation on this page +then this tag mapper (profile) does not use it. + +The exception are access permissions and wheelchair accessibility tags like + +- `access=no` +- `wheelchair=no` +- `oneway=yes` + +These are identical for all mappers and not separately listed on this page. + +### Way properties + +Way properties set a way's permission and optionally influences its walk and bicycle safety factors. + +These factors determine how desirable an OSM way is when routing for cyclists and pedestrians. +Lower safety values make an OSM way more desirable and higher values less desirable. + + + + +| specifier | permission | bike safety | walk safety | +|---------------------------------------------------------|--------------------------|-------------------------------|-------------| +| `highway=track` | `PEDESTRIAN_AND_BICYCLE` | | | +| `highway=track; present(surface)` | `PEDESTRIAN_AND_BICYCLE` | | | +| `highway=residential; junction=roundabout` | `ALL` | 0.98 | | +| `present(highway); junction=roundabout` | `BICYCLE_AND_CAR` | | | +| `highway=pedestrian` | `PEDESTRIAN` | | | +| `highway=residential; maxspeed=30` | `ALL` | 0.9 | | +| `highway=footway; bicycle=yes` | `PEDESTRIAN_AND_BICYCLE` | 0.8 | | +| `footway=sidewalk; highway=footway; bicycle=yes` | `PEDESTRIAN_AND_BICYCLE` | 1.2 | | +| `highway=unclassified; cycleway=lane` | `ALL` | 0.87 | | +| `mtb:scale=3` | `NONE` | | | +| `mtb:scale=4` | `NONE` | | | +| `mtb:scale=5` | `NONE` | | | +| `mtb:scale=6` | `NONE` | | | +| `highway=corridor` | `PEDESTRIAN` | | | +| `highway=steps` | `PEDESTRIAN` | | | +| `highway=crossing` | `PEDESTRIAN` | | | +| `highway=platform` | `PEDESTRIAN` | | | +| `public_transport=platform` | `PEDESTRIAN` | | | +| `railway=platform` | `PEDESTRIAN` | | | +| `footway=sidewalk; highway=footway` | `PEDESTRIAN` | | | +| `mtb:scale=1` | `PEDESTRIAN` | | | +| `mtb:scale=2` | `PEDESTRIAN` | | | +| `mtb:scale=0` | `PEDESTRIAN_AND_BICYCLE` | | | +| `highway=cycleway` | `PEDESTRIAN_AND_BICYCLE` | 0.6 | | +| `highway=path` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `highway=pedestrian` | `PEDESTRIAN_AND_BICYCLE` | 0.9 | | +| `highway=footway` | `PEDESTRIAN_AND_BICYCLE` | 1.1 | | +| `highway=bridleway` | `PEDESTRIAN_AND_BICYCLE` | 1.3 | | +| `highway=living_street` | `ALL` | 0.9 | | +| `highway=unclassified` | `ALL` | | | +| `highway=road` | `ALL` | | | +| `highway=byway` | `ALL` | 1.3 | | +| `highway=track` | `ALL` | 1.3 | | +| `highway=service` | `ALL` | 1.1 | | +| `highway=residential` | `ALL` | 0.98 | | +| `highway=residential_link` | `ALL` | 0.98 | | +| `highway=tertiary` | `ALL` | | | +| `highway=tertiary_link` | `ALL` | | | +| `highway=secondary` | `ALL` | 1.5 | | +| `highway=secondary_link` | `ALL` | 1.5 | | +| `highway=primary` | `ALL` | 2.06 | | +| `highway=primary_link` | `ALL` | 2.06 | | +| `highway=trunk_link` | `CAR` | 2.06 | | +| `highway=motorway_link` | `CAR` | 2.06 | | +| `highway=trunk` | `CAR` | 7.47 | | +| `highway=motorway` | `CAR` | 8.0 | | +| `present(highway); cycleway=lane` | `PEDESTRIAN_AND_BICYCLE` | 0.87 | | +| `highway=service; cycleway=lane` | `ALL` | 0.77 | | +| `highway=residential; cycleway=lane` | `ALL` | 0.77 | | +| `highway=residential_link; cycleway=lane` | `ALL` | 0.77 | | +| `highway=tertiary; cycleway=lane` | `ALL` | 0.87 | | +| `highway=tertiary_link; cycleway=lane` | `ALL` | 0.87 | | +| `highway=secondary; cycleway=lane` | `ALL` | 0.96 | | +| `highway=secondary_link; cycleway=lane` | `ALL` | 0.96 | | +| `highway=primary; cycleway=lane` | `ALL` | 1.15 | | +| `highway=primary_link; cycleway=lane` | `ALL` | 1.15 | | +| `highway=trunk; cycleway=lane` | `BICYCLE_AND_CAR` | 1.5 | | +| `highway=trunk_link; cycleway=lane` | `BICYCLE_AND_CAR` | 1.15 | | +| `highway=motorway; cycleway=lane` | `BICYCLE_AND_CAR` | 2.0 | | +| `highway=motorway_link; cycleway=lane` | `BICYCLE_AND_CAR` | 1.15 | | +| `present(highway); cycleway=share_busway` | `PEDESTRIAN_AND_BICYCLE` | 0.92 | | +| `highway=service; cycleway=share_busway` | `ALL` | 0.85 | | +| `highway=residential; cycleway=share_busway` | `ALL` | 0.85 | | +| `highway=residential_link; cycleway=share_busway` | `ALL` | 0.85 | | +| `highway=tertiary; cycleway=share_busway` | `ALL` | 0.92 | | +| `highway=tertiary_link; cycleway=share_busway` | `ALL` | 0.92 | | +| `highway=secondary; cycleway=share_busway` | `ALL` | 0.99 | | +| `highway=secondary_link; cycleway=share_busway` | `ALL` | 0.99 | | +| `highway=primary; cycleway=share_busway` | `ALL` | 1.25 | | +| `highway=primary_link; cycleway=share_busway` | `ALL` | 1.25 | | +| `highway=trunk; cycleway=share_busway` | `BICYCLE_AND_CAR` | 1.75 | | +| `highway=trunk_link; cycleway=share_busway` | `BICYCLE_AND_CAR` | 1.25 | | +| `highway=motorway; cycleway=share_busway` | `BICYCLE_AND_CAR` | 2.5 | | +| `highway=motorway_link; cycleway=share_busway` | `BICYCLE_AND_CAR` | 1.25 | | +| `present(highway); cycleway=opposite_lane` | `PEDESTRIAN_AND_BICYCLE` | forward: 1.0
back: 0.87 | | +| `highway=service; cycleway=opposite_lane` | `ALL` | forward: 1.1
back: 0.77 | | +| `highway=residential; cycleway=opposite_lane` | `ALL` | forward: 0.98
back: 0.77 | | +| `highway=residential_link; cycleway=opposite_lane` | `ALL` | forward: 0.98
back: 0.77 | | +| `highway=tertiary; cycleway=opposite_lane` | `ALL` | forward: 1.0
back: 0.87 | | +| `highway=tertiary_link; cycleway=opposite_lane` | `ALL` | forward: 1.0
back: 0.87 | | +| `highway=secondary; cycleway=opposite_lane` | `ALL` | forward: 1.5
back: 0.96 | | +| `highway=secondary_link; cycleway=opposite_lane` | `ALL` | forward: 1.5
back: 0.96 | | +| `highway=primary; cycleway=opposite_lane` | `ALL` | forward: 2.06
back: 1.15 | | +| `highway=primary_link; cycleway=opposite_lane` | `ALL` | forward: 2.06
back: 1.15 | | +| `highway=trunk; cycleway=opposite_lane` | `BICYCLE_AND_CAR` | forward: 7.47
back: 1.5 | | +| `highway=trunk_link; cycleway=opposite_lane` | `BICYCLE_AND_CAR` | forward: 2.06
back: 1.15 | | +| `present(highway); cycleway=track` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `highway=service; cycleway=track` | `ALL` | 0.65 | | +| `highway=residential; cycleway=track` | `ALL` | 0.65 | | +| `highway=residential_link; cycleway=track` | `ALL` | 0.65 | | +| `highway=tertiary; cycleway=track` | `ALL` | 0.75 | | +| `highway=tertiary_link; cycleway=track` | `ALL` | 0.75 | | +| `highway=secondary; cycleway=track` | `ALL` | 0.8 | | +| `highway=secondary_link; cycleway=track` | `ALL` | 0.8 | | +| `highway=primary; cycleway=track` | `ALL` | 0.85 | | +| `highway=primary_link; cycleway=track` | `ALL` | 0.85 | | +| `highway=trunk; cycleway=track` | `BICYCLE_AND_CAR` | 0.95 | | +| `highway=trunk_link; cycleway=track` | `BICYCLE_AND_CAR` | 0.85 | | +| `present(highway); cycleway=opposite_track` | `PEDESTRIAN_AND_BICYCLE` | forward: 1.0
back: 0.75 | | +| `highway=service; cycleway=opposite_track` | `ALL` | forward: 1.1
back: 0.65 | | +| `highway=residential; cycleway=opposite_track` | `ALL` | forward: 0.98
back: 0.65 | | +| `highway=residential_link; cycleway=opposite_track` | `ALL` | forward: 0.98
back: 0.65 | | +| `highway=tertiary; cycleway=opposite_track` | `ALL` | forward: 1.0
back: 0.75 | | +| `highway=tertiary_link; cycleway=opposite_track` | `ALL` | forward: 1.0
back: 0.75 | | +| `highway=secondary; cycleway=opposite_track` | `ALL` | forward: 1.5
back: 0.8 | | +| `highway=secondary_link; cycleway=opposite_track` | `ALL` | forward: 1.5
back: 0.8 | | +| `highway=primary; cycleway=opposite_track` | `ALL` | forward: 2.06
back: 0.85 | | +| `highway=primary_link; cycleway=opposite_track` | `ALL` | forward: 2.06
back: 0.85 | | +| `highway=trunk; cycleway=opposite_track` | `BICYCLE_AND_CAR` | forward: 7.47
back: 0.95 | | +| `highway=trunk_link; cycleway=opposite_track` | `BICYCLE_AND_CAR` | forward: 2.06
back: 0.85 | | +| `present(highway); cycleway=shared_lane` | `PEDESTRIAN_AND_BICYCLE` | 0.77 | | +| `highway=service; cycleway=shared_lane` | `ALL` | 0.73 | | +| `highway=residential; cycleway=shared_lane` | `ALL` | 0.77 | | +| `highway=residential_link; cycleway=shared_lane` | `ALL` | 0.77 | | +| `highway=tertiary; cycleway=shared_lane` | `ALL` | 0.83 | | +| `highway=tertiary_link; cycleway=shared_lane` | `ALL` | 0.83 | | +| `highway=secondary; cycleway=shared_lane` | `ALL` | 1.25 | | +| `highway=secondary_link; cycleway=shared_lane` | `ALL` | 1.25 | | +| `highway=primary; cycleway=shared_lane` | `ALL` | 1.75 | | +| `highway=primary_link; cycleway=shared_lane` | `ALL` | 1.75 | | +| `present(highway); cycleway=opposite` | `PEDESTRIAN_AND_BICYCLE` | forward: 1.0
back: 1.4 | | +| `highway=service; cycleway=opposite` | `ALL` | 1.1 | | +| `highway=residential; cycleway=opposite` | `ALL` | 0.98 | | +| `highway=residential_link; cycleway=opposite` | `ALL` | 0.98 | | +| `highway=tertiary; cycleway=opposite` | `ALL` | | | +| `highway=tertiary_link; cycleway=opposite` | `ALL` | | | +| `highway=secondary; cycleway=opposite` | `ALL` | forward: 1.5
back: 1.71 | | +| `highway=secondary_link; cycleway=opposite` | `ALL` | forward: 1.5
back: 1.71 | | +| `highway=primary; cycleway=opposite` | `ALL` | forward: 2.06
back: 2.99 | | +| `highway=primary_link; cycleway=opposite` | `ALL` | forward: 2.06
back: 2.99 | | +| `highway=path; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.6 | | +| `highway=footway; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `highway=footway; bicycle=yes; area=yes` | `PEDESTRIAN_AND_BICYCLE` | 0.9 | | +| `highway=pedestrian; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `footway=sidewalk; highway=footway; bicycle=yes` | `PEDESTRIAN_AND_BICYCLE` | 2.5 | | +| `footway=sidewalk; highway=footway; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 1.1 | | +| `highway=footway; footway=crossing` | `PEDESTRIAN_AND_BICYCLE` | 2.5 | | +| `highway=footway; footway=crossing; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 1.1 | | +| `highway=track; bicycle=yes` | `PEDESTRIAN_AND_BICYCLE` | 1.18 | | +| `highway=track; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.99 | | +| `highway=track; bicycle=yes; present(surface)` | `PEDESTRIAN_AND_BICYCLE` | 1.18 | | +| `highway=track; bicycle=designated; present(surface)` | `PEDESTRIAN_AND_BICYCLE` | 0.99 | | +| `highway=track; present(surface)` | `PEDESTRIAN_AND_BICYCLE` | 1.3 | | +| `present(highway); bicycle=designated` | `ALL` | 0.97 | | +| `highway=service; bicycle=designated` | `ALL` | 0.84 | | +| `highway=residential; bicycle=designated` | `ALL` | 0.95 | | +| `highway=unclassified; bicycle=designated` | `ALL` | 0.95 | | +| `highway=residential_link; bicycle=designated` | `ALL` | 0.95 | | +| `highway=tertiary; bicycle=designated` | `ALL` | 0.97 | | +| `highway=tertiary_link; bicycle=designated` | `ALL` | 0.97 | | +| `highway=secondary; bicycle=designated` | `ALL` | 1.46 | | +| `highway=secondary_link; bicycle=designated` | `ALL` | 1.46 | | +| `highway=primary; bicycle=designated` | `ALL` | 2.0 | | +| `highway=primary_link; bicycle=designated` | `ALL` | 2.0 | | +| `highway=trunk; bicycle=designated` | `BICYCLE_AND_CAR` | 7.25 | | +| `highway=trunk_link; bicycle=designated` | `BICYCLE_AND_CAR` | 2.0 | | +| `highway=motorway; bicycle=designated` | `BICYCLE_AND_CAR` | 7.76 | | +| `highway=motorway_link; bicycle=designated` | `BICYCLE_AND_CAR` | 2.0 | | + + + +### Safety mixins + +Mixins are selectors that have only an effect on the bicycle and walk safety factors but not on the +permission of an OSM way. Their safety values are multiplied with the base values from the selected +way properties. Multiple mixins can apply to the same way and their effects compound. + + + + +| matcher | bicycle safety | walk safety | +|------------------------------------------------------------|----------------|-------------| +| `highway=tertiary` | 1.2 | | +| `maxspeed=70` | 1.5 | | +| `maxspeed=80` | 2.0 | | +| `maxspeed=90` | 3.0 | | +| `maxspeed=100` | 5.0 | | +| `tracktype=grade1` | | | +| `tracktype=grade2` | 1.1 | | +| `tracktype=grade3` | 1.15 | | +| `tracktype=grade4` | 1.3 | | +| `tracktype=grade5` | 1.5 | | +| `lit=no` | 1.05 | | +| `lcn=yes¦rcn=yes¦ncn=yes¦bicycle_road=yes¦cyclestreet=yes` | 0.7 | | +| `surface=unpaved` | 1.18 | | +| `surface=compacted` | 1.18 | | +| `surface=wood` | 1.18 | | +| `surface=cobblestone` | 1.3 | | +| `surface=sett` | 1.3 | | +| `surface=unhewn_cobblestone` | 1.5 | | +| `surface=grass_paver` | 1.3 | | +| `surface=pebblestone` | 1.3 | | +| `surface=metal` | 1.3 | | +| `surface=ground` | 1.5 | | +| `surface=dirt` | 1.5 | | +| `surface=earth` | 1.5 | | +| `surface=grass` | 1.5 | | +| `surface=mud` | 1.5 | | +| `surface=woodchip` | 1.5 | | +| `surface=gravel` | 1.5 | | +| `surface=artifical_turf` | 1.5 | | +| `surface=sand` | 100.0 | | +| `foot=discouraged` | | 3.0 | +| `bicycle=discouraged` | 3.0 | | +| `foot=use_sidepath` | | 5.0 | +| `bicycle=use_sidepath` | 5.0 | | + + diff --git a/doc/user/osm/Norway.md b/doc/user/osm/Norway.md new file mode 100644 index 00000000000..d38fc4c04df --- /dev/null +++ b/doc/user/osm/Norway.md @@ -0,0 +1,122 @@ +# OSM tag mapping + +This page is intended to give an overview of which OpenStreetMap(OSM) tags OTP uses to evaluate its +walking and bicycling instructions. If a tag is not part of the documentation on this page +then this tag mapper (profile) does not use it. + +The exception are access permissions and wheelchair accessibility tags like + +- `access=no` +- `wheelchair=no` +- `oneway=yes` + +These are identical for all mappers and not separately listed on this page. + +### Way properties + +Way properties set a way's permission and optionally influences its walk and bicycle safety factors. + +These factors determine how desirable an OSM way is when routing for cyclists and pedestrians. +Lower safety values make an OSM way more desirable and higher values less desirable. + + + + +| specifier | permission | bike safety | walk safety | +|------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------|-------------|-------------| +| `highway one of [motorway, motorway_link]` | `CAR` | | | +| `highway one of [trunk, trunk_link, primary, primary_link]; motorroad=yes` | `CAR` | | | +| `highway one of [trunk, trunk_link, primary, primary_link, secondary, secondary_link, tertiary, tertiary_link, unclassified, residential]` | `ALL` | | | +| `cycleway=track; highway one of [trunk, trunk_link, primary, primary_link, secondary, secondary_link, tertiary, tertiary_link, unclassified, residential]` | `ALL` | | | +| `cycleway=lane; highway one of [trunk, trunk_link, primary, primary_link, secondary, secondary_link, tertiary, tertiary_link]` | `ALL` | 1.27 | | +| `cycleway=lane; maxspeed < 50; highway one of [trunk, trunk_link, primary, primary_link, secondary, secondary_link, tertiary, tertiary_link]` | `ALL` | 1.1 | | +| `cycleway=lane; highway one of [unclassified, residential]` | `ALL` | 1.1 | | +| `highway=service` | `ALL` | | | +| `highway=service; service=parking_aisle` | `ALL` | 2.5 | | +| `highway=service; service=drive-through` | `ALL` | 2.5 | | +| `highway=living_street` | `ALL` | 1.83 | | +| `highway=pedestrian` | `PEDESTRIAN_AND_BICYCLE` | 1.2 | | +| `highway=busway` | `PEDESTRIAN_AND_BICYCLE` | 2.37 | 1.9 | +| `highway=service; bus one of [yes, designated]` | `PEDESTRIAN_AND_BICYCLE` | 2.37 | 1.9 | +| `highway=footway` | `PEDESTRIAN_AND_BICYCLE` | 1.42 | | +| `highway=cycleway` | `PEDESTRIAN_AND_BICYCLE` | 1.05 | 1.4 | +| `highway=cycleway; lanes > 1` | `PEDESTRIAN_AND_BICYCLE` | | 1.4 | +| `highway=cycleway; oneway=yes` | `PEDESTRIAN_AND_BICYCLE` | | 1.4 | +| `highway=cycleway; sidewalk one of [yes, left, right, both]` | `PEDESTRIAN_AND_BICYCLE` | 1.05 | | +| `highway=cycleway; lanes > 1; sidewalk one of [yes, left, right, both]` | `PEDESTRIAN_AND_BICYCLE` | | | +| `highway=cycleway; oneway=yes; sidewalk one of [yes, left, right, both]` | `PEDESTRIAN_AND_BICYCLE` | | | +| `highway=cycleway; foot=designated; segregated=no` | `PEDESTRIAN_AND_BICYCLE` | 1.05 | 1.15 | +| `highway=path; foot=designated; bicycle=designated; segregated=no` | `PEDESTRIAN_AND_BICYCLE` | 1.05 | 1.15 | +| `highway=cycleway; foot=designated; segregated=yes` | `PEDESTRIAN_AND_BICYCLE` | 1.05 | | +| `highway=path; foot=designated; bicycle=designated; segregated=yes` | `PEDESTRIAN_AND_BICYCLE` | 1.05 | | +| `highway=cycleway; foot=designated; segregated=yes; lanes > 1` | `PEDESTRIAN_AND_BICYCLE` | | | +| `highway=cycleway; foot=designated; present(segregated); motor_vehicle=destination` | `PEDESTRIAN_AND_BICYCLE` | 1.57 | | +| `highway=path; foot=designated; bicycle=designated; present(segregated); motor_vehicle=destination` | `PEDESTRIAN_AND_BICYCLE` | 1.57 | | +| `highway=footway; footway=sidewalk` | `PEDESTRIAN_AND_BICYCLE` | 1.93 | 1.1 | +| `highway=footway; footway=crossing` | `PEDESTRIAN_AND_BICYCLE` | 2.33 | 1.35 | +| `highway=cycleway; cycleway=crossing` | `PEDESTRIAN_AND_BICYCLE` | 2.33 | 1.35 | +| `highway=track` | `PEDESTRIAN_AND_BICYCLE` | | | +| `highway=bridleway` | `PEDESTRIAN_AND_BICYCLE` | | | +| `highway=path` | `PEDESTRIAN_AND_BICYCLE` | | | +| `highway=steps` | `PEDESTRIAN` | | | +| `highway=corridor` | `PEDESTRIAN` | | | +| `highway=footway; indoor=yes` | `PEDESTRIAN` | | | +| `highway=platform` | `PEDESTRIAN` | | | +| `public_transport=platform` | `PEDESTRIAN` | | | +| `trail_visibility one of [bad, low, poor, horrible, no]; highway=path` | `NONE` | | | +| `sac_scale one of [demanding_mountain_hiking, alpine_hiking, demanding_alpine_hiking, difficult_alpine_hiking]; highway one of [path, steps]` | `NONE` | | | +| `smoothness one of [horrible, very_horrible]; highway one of [path, bridleway, track]` | `PEDESTRIAN` | | 1.15 | +| `smoothness=impassable; highway one of [path, bridleway, track]` | `NONE` | | | +| `1 > mtb:scale < 2; highway one of [path, bridleway, track]` | `PEDESTRIAN` | | 1.15 | +| `mtb:scale > 2; highway one of [path, bridleway, track]` | `NONE` | | | + + + +### Safety mixins + +Mixins are selectors that have only an effect on the bicycle and walk safety factors but not on the +permission of an OSM way. Their safety values are multiplied with the base values from the selected +way properties. Multiple mixins can apply to the same way and their effects compound. + + + + +| matcher | bicycle safety | walk safety | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------|-------------| +| `cycleway=shared_lane; highway one of [trunk, trunk_link, primary, primary_link, secondary, secondary_link, tertiary, tertiary_link, unclassified, residential]` | 0.85 | | +| `lcn=yes¦rcn=yes¦ncn=yes` | 0.85 | | +| `oneway=yes; cycleway not one of [no, none] or absent; highway one of [trunk, trunk_link, primary, primary_link, secondary, secondary_link, tertiary, tertiary_link, unclassified, residential]` | forward: 1.0
back: 1.15 | | +| `embedded_rails one of [tram, light_rail, disused]` | 1.2 | | +| `tunnel=yes; highway one of [trunk, trunk_link, primary, primary_link, secondary, secondary_link, tertiary, tertiary_link, unclassified]` | | 2.0 | +| `bridge=yes; sidewalk not one of [no, separate] or absent; highway one of [trunk, trunk_link, primary, primary_link, secondary, secondary_link, tertiary, tertiary_link, unclassified]¦verge=no; sidewalk not one of [no, separate] or absent; highway one of [trunk, trunk_link, primary, primary_link, secondary, secondary_link, tertiary, tertiary_link, unclassified]` | | 2.0 | +| `junction=roundabout; sidewalk not one of [no, separate] or absent` | | 2.0 | +| `surface=grass_paver` | 1.2 | | +| `surface=sett` | 1.2 | | +| `surface=cobblestone` | 1.2 | | +| `surface=unhewn_cobblestone` | 3.0 | | +| `surface=metal_grid` | 1.2 | | +| `surface=metal` | 1.2 | | +| `smoothness=intermediate; surface one of [asfalt, concrete, paving_stones, paved, wood]` | 1.2 | | +| `smoothness=bad; surface one of [asfalt, concrete, paving_stones, paved, wood]` | 1.4 | 1.6 | +| `surface=unpaved; !tracktype` | 1.8 | 1.6 | +| `surface=compacted` | 1.4 | 1.4 | +| `surface=fine_gravel` | 1.8 | 1.6 | +| `surface=pebblestone` | 1.8 | 1.6 | +| `surface=gravel` | 1.8 | 1.6 | +| `surface=woodchip` | 1.8 | 1.6 | +| `surface=ground` | 2.3 | 2.4 | +| `surface=dirt` | 2.3 | 2.4 | +| `surface=earth` | 2.3 | 2.4 | +| `surface=grass` | 2.3 | 1.8 | +| `surface=mud` | 3.0 | 3.0 | +| `surface=sand` | 3.0 | 1.8 | +| `!tracktype; surface not one of [unpaved] or absent; highway one of [track, bridleway]` | 1.8 | 1.6 | +| `tracktype=grade2; surface not one of [unpaved] or absent; highway one of [track, bridleway, service, unclassified]` | 1.4 | 1.4 | +| `tracktype=grade3; surface not one of [unpaved] or absent; highway one of [track, bridleway, service, unclassified]` | 1.8 | 1.6 | +| `tracktype=grade4; surface not one of [unpaved] or absent; highway one of [track, bridleway, service, unclassified]` | 2.3 | 1.8 | +| `tracktype=grade5; surface not one of [unpaved] or absent; highway one of [track, bridleway, service, unclassified]` | 2.3 | 2.4 | +| `surface not one of [no, none] or absent; highway=path` | 2.3 | 2.4 | +| `sac_scale=mountain_hiking` | | 1.8 | +| `trail_visibility=intermediate` | | 1.8 | + + diff --git a/doc/user/osm/UK.md b/doc/user/osm/UK.md new file mode 100644 index 00000000000..4a640caf95c --- /dev/null +++ b/doc/user/osm/UK.md @@ -0,0 +1,228 @@ +# OSM tag mapping + +This page is intended to give an overview of which OpenStreetMap(OSM) tags OTP uses to evaluate its +walking and bicycling instructions. If a tag is not part of the documentation on this page +then this tag mapper (profile) does not use it. + +The exception are access permissions and wheelchair accessibility tags like + +- `access=no` +- `wheelchair=no` +- `oneway=yes` + +These are identical for all mappers and not separately listed on this page. + +### Way properties + +Way properties set a way's permission and optionally influences its walk and bicycle safety factors. + +These factors determine how desirable an OSM way is when routing for cyclists and pedestrians. +Lower safety values make an OSM way more desirable and higher values less desirable. + + + + +| specifier | permission | bike safety | walk safety | +|---------------------------------------------------------|--------------------------|-------------------------------|-------------| +| `highway=trunk_link` | `ALL` | 2.06 | | +| `highway=trunk` | `ALL` | 7.47 | | +| `highway=trunk; cycleway=lane` | `ALL` | 1.5 | | +| `highway=trunk_link; cycleway=lane` | `ALL` | 1.15 | | +| `highway=trunk; cycleway=share_busway` | `ALL` | 1.75 | | +| `highway=trunk_link; cycleway=share_busway` | `ALL` | 1.25 | | +| `highway=trunk; cycleway=opposite_lane` | `ALL` | forward: 7.47
back: 1.5 | | +| `highway=trunk_link; cycleway=opposite_lane` | `ALL` | forward: 2.06
back: 1.15 | | +| `highway=trunk; cycleway=track` | `ALL` | 0.95 | | +| `highway=trunk_link; cycleway=track` | `ALL` | 0.85 | | +| `highway=trunk; cycleway=opposite_track` | `ALL` | forward: 7.47
back: 0.95 | | +| `highway=trunk_link; cycleway=opposite_track` | `ALL` | forward: 2.06
back: 0.85 | | +| `highway=trunk; bicycle=designated` | `ALL` | 7.25 | | +| `highway=trunk_link; bicycle=designated` | `ALL` | 2.0 | | +| `mtb:scale=3` | `NONE` | | | +| `mtb:scale=4` | `NONE` | | | +| `mtb:scale=5` | `NONE` | | | +| `mtb:scale=6` | `NONE` | | | +| `highway=corridor` | `PEDESTRIAN` | | | +| `highway=steps` | `PEDESTRIAN` | | | +| `highway=crossing` | `PEDESTRIAN` | | | +| `highway=platform` | `PEDESTRIAN` | | | +| `public_transport=platform` | `PEDESTRIAN` | | | +| `railway=platform` | `PEDESTRIAN` | | | +| `footway=sidewalk; highway=footway` | `PEDESTRIAN` | | | +| `mtb:scale=1` | `PEDESTRIAN` | | | +| `mtb:scale=2` | `PEDESTRIAN` | | | +| `mtb:scale=0` | `PEDESTRIAN_AND_BICYCLE` | | | +| `highway=cycleway` | `PEDESTRIAN_AND_BICYCLE` | 0.6 | | +| `highway=path` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `highway=pedestrian` | `PEDESTRIAN_AND_BICYCLE` | 0.9 | | +| `highway=footway` | `PEDESTRIAN_AND_BICYCLE` | 1.1 | | +| `highway=bridleway` | `PEDESTRIAN_AND_BICYCLE` | 1.3 | | +| `highway=living_street` | `ALL` | 0.9 | | +| `highway=unclassified` | `ALL` | | | +| `highway=road` | `ALL` | | | +| `highway=byway` | `ALL` | 1.3 | | +| `highway=track` | `ALL` | 1.3 | | +| `highway=service` | `ALL` | 1.1 | | +| `highway=residential` | `ALL` | 0.98 | | +| `highway=residential_link` | `ALL` | 0.98 | | +| `highway=tertiary` | `ALL` | | | +| `highway=tertiary_link` | `ALL` | | | +| `highway=secondary` | `ALL` | 1.5 | | +| `highway=secondary_link` | `ALL` | 1.5 | | +| `highway=primary` | `ALL` | 2.06 | | +| `highway=primary_link` | `ALL` | 2.06 | | +| `highway=trunk_link` | `CAR` | 2.06 | | +| `highway=motorway_link` | `CAR` | 2.06 | | +| `highway=trunk` | `CAR` | 7.47 | | +| `highway=motorway` | `CAR` | 8.0 | | +| `present(highway); cycleway=lane` | `PEDESTRIAN_AND_BICYCLE` | 0.87 | | +| `highway=service; cycleway=lane` | `ALL` | 0.77 | | +| `highway=residential; cycleway=lane` | `ALL` | 0.77 | | +| `highway=residential_link; cycleway=lane` | `ALL` | 0.77 | | +| `highway=tertiary; cycleway=lane` | `ALL` | 0.87 | | +| `highway=tertiary_link; cycleway=lane` | `ALL` | 0.87 | | +| `highway=secondary; cycleway=lane` | `ALL` | 0.96 | | +| `highway=secondary_link; cycleway=lane` | `ALL` | 0.96 | | +| `highway=primary; cycleway=lane` | `ALL` | 1.15 | | +| `highway=primary_link; cycleway=lane` | `ALL` | 1.15 | | +| `highway=trunk; cycleway=lane` | `BICYCLE_AND_CAR` | 1.5 | | +| `highway=trunk_link; cycleway=lane` | `BICYCLE_AND_CAR` | 1.15 | | +| `highway=motorway; cycleway=lane` | `BICYCLE_AND_CAR` | 2.0 | | +| `highway=motorway_link; cycleway=lane` | `BICYCLE_AND_CAR` | 1.15 | | +| `present(highway); cycleway=share_busway` | `PEDESTRIAN_AND_BICYCLE` | 0.92 | | +| `highway=service; cycleway=share_busway` | `ALL` | 0.85 | | +| `highway=residential; cycleway=share_busway` | `ALL` | 0.85 | | +| `highway=residential_link; cycleway=share_busway` | `ALL` | 0.85 | | +| `highway=tertiary; cycleway=share_busway` | `ALL` | 0.92 | | +| `highway=tertiary_link; cycleway=share_busway` | `ALL` | 0.92 | | +| `highway=secondary; cycleway=share_busway` | `ALL` | 0.99 | | +| `highway=secondary_link; cycleway=share_busway` | `ALL` | 0.99 | | +| `highway=primary; cycleway=share_busway` | `ALL` | 1.25 | | +| `highway=primary_link; cycleway=share_busway` | `ALL` | 1.25 | | +| `highway=trunk; cycleway=share_busway` | `BICYCLE_AND_CAR` | 1.75 | | +| `highway=trunk_link; cycleway=share_busway` | `BICYCLE_AND_CAR` | 1.25 | | +| `highway=motorway; cycleway=share_busway` | `BICYCLE_AND_CAR` | 2.5 | | +| `highway=motorway_link; cycleway=share_busway` | `BICYCLE_AND_CAR` | 1.25 | | +| `present(highway); cycleway=opposite_lane` | `PEDESTRIAN_AND_BICYCLE` | forward: 1.0
back: 0.87 | | +| `highway=service; cycleway=opposite_lane` | `ALL` | forward: 1.1
back: 0.77 | | +| `highway=residential; cycleway=opposite_lane` | `ALL` | forward: 0.98
back: 0.77 | | +| `highway=residential_link; cycleway=opposite_lane` | `ALL` | forward: 0.98
back: 0.77 | | +| `highway=tertiary; cycleway=opposite_lane` | `ALL` | forward: 1.0
back: 0.87 | | +| `highway=tertiary_link; cycleway=opposite_lane` | `ALL` | forward: 1.0
back: 0.87 | | +| `highway=secondary; cycleway=opposite_lane` | `ALL` | forward: 1.5
back: 0.96 | | +| `highway=secondary_link; cycleway=opposite_lane` | `ALL` | forward: 1.5
back: 0.96 | | +| `highway=primary; cycleway=opposite_lane` | `ALL` | forward: 2.06
back: 1.15 | | +| `highway=primary_link; cycleway=opposite_lane` | `ALL` | forward: 2.06
back: 1.15 | | +| `highway=trunk; cycleway=opposite_lane` | `BICYCLE_AND_CAR` | forward: 7.47
back: 1.5 | | +| `highway=trunk_link; cycleway=opposite_lane` | `BICYCLE_AND_CAR` | forward: 2.06
back: 1.15 | | +| `present(highway); cycleway=track` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `highway=service; cycleway=track` | `ALL` | 0.65 | | +| `highway=residential; cycleway=track` | `ALL` | 0.65 | | +| `highway=residential_link; cycleway=track` | `ALL` | 0.65 | | +| `highway=tertiary; cycleway=track` | `ALL` | 0.75 | | +| `highway=tertiary_link; cycleway=track` | `ALL` | 0.75 | | +| `highway=secondary; cycleway=track` | `ALL` | 0.8 | | +| `highway=secondary_link; cycleway=track` | `ALL` | 0.8 | | +| `highway=primary; cycleway=track` | `ALL` | 0.85 | | +| `highway=primary_link; cycleway=track` | `ALL` | 0.85 | | +| `highway=trunk; cycleway=track` | `BICYCLE_AND_CAR` | 0.95 | | +| `highway=trunk_link; cycleway=track` | `BICYCLE_AND_CAR` | 0.85 | | +| `present(highway); cycleway=opposite_track` | `PEDESTRIAN_AND_BICYCLE` | forward: 1.0
back: 0.75 | | +| `highway=service; cycleway=opposite_track` | `ALL` | forward: 1.1
back: 0.65 | | +| `highway=residential; cycleway=opposite_track` | `ALL` | forward: 0.98
back: 0.65 | | +| `highway=residential_link; cycleway=opposite_track` | `ALL` | forward: 0.98
back: 0.65 | | +| `highway=tertiary; cycleway=opposite_track` | `ALL` | forward: 1.0
back: 0.75 | | +| `highway=tertiary_link; cycleway=opposite_track` | `ALL` | forward: 1.0
back: 0.75 | | +| `highway=secondary; cycleway=opposite_track` | `ALL` | forward: 1.5
back: 0.8 | | +| `highway=secondary_link; cycleway=opposite_track` | `ALL` | forward: 1.5
back: 0.8 | | +| `highway=primary; cycleway=opposite_track` | `ALL` | forward: 2.06
back: 0.85 | | +| `highway=primary_link; cycleway=opposite_track` | `ALL` | forward: 2.06
back: 0.85 | | +| `highway=trunk; cycleway=opposite_track` | `BICYCLE_AND_CAR` | forward: 7.47
back: 0.95 | | +| `highway=trunk_link; cycleway=opposite_track` | `BICYCLE_AND_CAR` | forward: 2.06
back: 0.85 | | +| `present(highway); cycleway=shared_lane` | `PEDESTRIAN_AND_BICYCLE` | 0.77 | | +| `highway=service; cycleway=shared_lane` | `ALL` | 0.73 | | +| `highway=residential; cycleway=shared_lane` | `ALL` | 0.77 | | +| `highway=residential_link; cycleway=shared_lane` | `ALL` | 0.77 | | +| `highway=tertiary; cycleway=shared_lane` | `ALL` | 0.83 | | +| `highway=tertiary_link; cycleway=shared_lane` | `ALL` | 0.83 | | +| `highway=secondary; cycleway=shared_lane` | `ALL` | 1.25 | | +| `highway=secondary_link; cycleway=shared_lane` | `ALL` | 1.25 | | +| `highway=primary; cycleway=shared_lane` | `ALL` | 1.75 | | +| `highway=primary_link; cycleway=shared_lane` | `ALL` | 1.75 | | +| `present(highway); cycleway=opposite` | `PEDESTRIAN_AND_BICYCLE` | forward: 1.0
back: 1.4 | | +| `highway=service; cycleway=opposite` | `ALL` | 1.1 | | +| `highway=residential; cycleway=opposite` | `ALL` | 0.98 | | +| `highway=residential_link; cycleway=opposite` | `ALL` | 0.98 | | +| `highway=tertiary; cycleway=opposite` | `ALL` | | | +| `highway=tertiary_link; cycleway=opposite` | `ALL` | | | +| `highway=secondary; cycleway=opposite` | `ALL` | forward: 1.5
back: 1.71 | | +| `highway=secondary_link; cycleway=opposite` | `ALL` | forward: 1.5
back: 1.71 | | +| `highway=primary; cycleway=opposite` | `ALL` | forward: 2.06
back: 2.99 | | +| `highway=primary_link; cycleway=opposite` | `ALL` | forward: 2.06
back: 2.99 | | +| `highway=path; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.6 | | +| `highway=footway; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `highway=footway; bicycle=yes; area=yes` | `PEDESTRIAN_AND_BICYCLE` | 0.9 | | +| `highway=pedestrian; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.75 | | +| `footway=sidewalk; highway=footway; bicycle=yes` | `PEDESTRIAN_AND_BICYCLE` | 2.5 | | +| `footway=sidewalk; highway=footway; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 1.1 | | +| `highway=footway; footway=crossing` | `PEDESTRIAN_AND_BICYCLE` | 2.5 | | +| `highway=footway; footway=crossing; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 1.1 | | +| `highway=track; bicycle=yes` | `PEDESTRIAN_AND_BICYCLE` | 1.18 | | +| `highway=track; bicycle=designated` | `PEDESTRIAN_AND_BICYCLE` | 0.99 | | +| `highway=track; bicycle=yes; present(surface)` | `PEDESTRIAN_AND_BICYCLE` | 1.18 | | +| `highway=track; bicycle=designated; present(surface)` | `PEDESTRIAN_AND_BICYCLE` | 0.99 | | +| `highway=track; present(surface)` | `PEDESTRIAN_AND_BICYCLE` | 1.3 | | +| `present(highway); bicycle=designated` | `ALL` | 0.97 | | +| `highway=service; bicycle=designated` | `ALL` | 0.84 | | +| `highway=residential; bicycle=designated` | `ALL` | 0.95 | | +| `highway=unclassified; bicycle=designated` | `ALL` | 0.95 | | +| `highway=residential_link; bicycle=designated` | `ALL` | 0.95 | | +| `highway=tertiary; bicycle=designated` | `ALL` | 0.97 | | +| `highway=tertiary_link; bicycle=designated` | `ALL` | 0.97 | | +| `highway=secondary; bicycle=designated` | `ALL` | 1.46 | | +| `highway=secondary_link; bicycle=designated` | `ALL` | 1.46 | | +| `highway=primary; bicycle=designated` | `ALL` | 2.0 | | +| `highway=primary_link; bicycle=designated` | `ALL` | 2.0 | | +| `highway=trunk; bicycle=designated` | `BICYCLE_AND_CAR` | 7.25 | | +| `highway=trunk_link; bicycle=designated` | `BICYCLE_AND_CAR` | 2.0 | | +| `highway=motorway; bicycle=designated` | `BICYCLE_AND_CAR` | 7.76 | | +| `highway=motorway_link; bicycle=designated` | `BICYCLE_AND_CAR` | 2.0 | | + + + +### Safety mixins + +Mixins are selectors that have only an effect on the bicycle and walk safety factors but not on the +permission of an OSM way. Their safety values are multiplied with the base values from the selected +way properties. Multiple mixins can apply to the same way and their effects compound. + + + + +| matcher | bicycle safety | walk safety | +|------------------------------------------------------------|----------------|-------------| +| `lcn=yes¦rcn=yes¦ncn=yes¦bicycle_road=yes¦cyclestreet=yes` | 0.7 | | +| `surface=unpaved` | 1.18 | | +| `surface=compacted` | 1.18 | | +| `surface=wood` | 1.18 | | +| `surface=cobblestone` | 1.3 | | +| `surface=sett` | 1.3 | | +| `surface=unhewn_cobblestone` | 1.5 | | +| `surface=grass_paver` | 1.3 | | +| `surface=pebblestone` | 1.3 | | +| `surface=metal` | 1.3 | | +| `surface=ground` | 1.5 | | +| `surface=dirt` | 1.5 | | +| `surface=earth` | 1.5 | | +| `surface=grass` | 1.5 | | +| `surface=mud` | 1.5 | | +| `surface=woodchip` | 1.5 | | +| `surface=gravel` | 1.5 | | +| `surface=artifical_turf` | 1.5 | | +| `surface=sand` | 100.0 | | +| `foot=discouraged` | | 3.0 | +| `bicycle=discouraged` | 3.0 | | +| `foot=use_sidepath` | | 5.0 | +| `bicycle=use_sidepath` | 5.0 | | + + diff --git a/doc/user/sandbox/ReportApi.md b/doc/user/sandbox/ReportApi.md index 1a0668d1740..bc219ec2f98 100644 --- a/doc/user/sandbox/ReportApi.md +++ b/doc/user/sandbox/ReportApi.md @@ -24,14 +24,6 @@ This module mounts an endpoint for generating reports under `otp/report`. Availa - [/otp/report/transfers.csv](http://localhost:8080/otp/report/transfers.csv) - [/otp/report/graph.json](http://localhost:8080/otp/report/graph.json) Detailed numbers of transit and street entities in the graph -- [/otp/report/bicycle-safety.html](http://localhost:8080/otp/report/bicycle-safety.html): - Interactive viewer of the rules that determine how bicycle safety factors are calculated. -- [/otp/report/bicycle-safety.csv](http://localhost:8080/otp/report/bicycle-safety.csv): Raw CSV - data for the bicycle safety report. - - [Norwegian version](http://localhost:8080/otp/report/bicycle-safety.csv?osmWayPropertySet=norway) - - [German version](http://localhost:8080/otp/report/bicycle-safety.csv?osmWayPropertySet=germany) - - [UK version](http://localhost:8080/otp/report/bicycle-safety.csv?osmWayPropertySet=uk) - - [Finnish version](http://localhost:8080/otp/report/bicycle-safety.csv?osmWayPropertySet=finland) - [/otp/report/transit/group/priorities](http://localhost:8080/otp/report/transit/group/priorities): List all transit groups used for transit-group-priority (Competition neutral planning). diff --git a/doc/user/sandbox/VehicleRentalServiceDirectory.md b/doc/user/sandbox/VehicleRentalServiceDirectory.md index 8009a62f912..1be5418e155 100644 --- a/doc/user/sandbox/VehicleRentalServiceDirectory.md +++ b/doc/user/sandbox/VehicleRentalServiceDirectory.md @@ -1,10 +1,10 @@ -# Vehicle Rental Service Directory API support. +# Vehicle Rental Service Directory API support -This adds support for the GBFS service directory endpoint component located at -https://github.com/entur/lamassu. OTP uses the service directory to lookup and connect to all GBFS -endpoints registered in the directory. This simplifies the management of the GBFS endpoints, since -multiple services/components like OTP can connect to the directory and get the necessary -configuration from it. +This adds support for the GBFS service directory endpoint component +[Lamassu](https://github.com/entur/lamassu). +OTP uses the service directory to lookup and connects to all GBFS endpoints registered in the +directory. This simplifies the management of the GBFS endpoints, since multiple services/components +like OTP can connect to the directory and get the necessary configuration from it. ## Contact Info @@ -17,6 +17,7 @@ configuration from it. - Initial implementation of bike share updater API support - Make json tag names configurable [#3447](https://github.com/opentripplanner/OpenTripPlanner/pull/3447) - Enable GBFS geofencing with VehicleRentalServiceDirectory [#5324](https://github.com/opentripplanner/OpenTripPlanner/pull/5324) +- Enable `allowKeepingVehicleAtDestination` [#5944](https://github.com/opentripplanner/OpenTripPlanner/pull/5944) ## Configuration @@ -29,17 +30,18 @@ the `router-config.json` -| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | -|-----------------------------------------------------|:---------------:|---------------------------------------------------------------------------------|:----------:|---------------|:-----:| -| language | `string` | Language code. | *Optional* | | 2.1 | -| sourcesName | `string` | Json tag name for updater sources. | *Optional* | `"systems"` | 2.1 | -| updaterNetworkName | `string` | Json tag name for the network name for each source. | *Optional* | `"id"` | 2.1 | -| updaterUrlName | `string` | Json tag name for endpoint urls for each source. | *Optional* | `"url"` | 2.1 | -| url | `uri` | Endpoint for the VehicleRentalServiceDirectory | *Required* | | 2.1 | -| [headers](#vehicleRentalServiceDirectory_headers) | `map of string` | HTTP headers to add to the request. Any header key, value can be inserted. | *Optional* | | 2.1 | -| [networks](#vehicleRentalServiceDirectory_networks) | `object[]` | List all networks to include. Use "network": "default-network" to set defaults. | *Optional* | | 2.4 | -|       geofencingZones | `boolean` | Enables geofencingZones for the given network | *Optional* | `false` | 2.4 | -|       network | `string` | The network name | *Required* | | 2.4 | +| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | +|----------------------------------------------------------------------------------------------------------------------|:---------------:|---------------------------------------------------------------------------------|:----------:|---------------|:-----:| +| language | `string` | Language code. | *Optional* | | 2.1 | +| sourcesName | `string` | Json tag name for updater sources. | *Optional* | `"systems"` | 2.1 | +| updaterNetworkName | `string` | Json tag name for the network name for each source. | *Optional* | `"id"` | 2.1 | +| updaterUrlName | `string` | Json tag name for endpoint urls for each source. | *Optional* | `"url"` | 2.1 | +| url | `uri` | Endpoint for the VehicleRentalServiceDirectory | *Required* | | 2.1 | +| [headers](#vehicleRentalServiceDirectory_headers) | `map of string` | HTTP headers to add to the request. Any header key, value can be inserted. | *Optional* | | 2.1 | +| [networks](#vehicleRentalServiceDirectory_networks) | `object[]` | List all networks to include. Use "network": "default-network" to set defaults. | *Optional* | | 2.4 | +|       [allowKeepingVehicleAtDestination](#vehicleRentalServiceDirectory_networks_0_allowKeepingVehicleAtDestination) | `boolean` | Enables `allowKeepingVehicleAtDestination` for the given network. | *Optional* | `false` | 2.5 | +|       [geofencingZones](#vehicleRentalServiceDirectory_networks_0_geofencingZones) | `boolean` | Enables geofencingZones for the given network | *Optional* | `false` | 2.4 | +|       network | `string` | The network name | *Required* | | 2.4 | @@ -69,6 +71,28 @@ networks are dropped. Note! The values in the "default-network" are not used to missing field values in networks listed. +

allowKeepingVehicleAtDestination

+ +**Since version:** `2.5` ∙ **Type:** `boolean` ∙ **Cardinality:** `Optional` ∙ **Default value:** `false` +**Path:** /vehicleRentalServiceDirectory/networks/[0] + +Enables `allowKeepingVehicleAtDestination` for the given network. + +Configures if a vehicle rented from a station must be returned to another one or can +be kept at the end of the trip. + +See the regular [GBFS documentation](../UpdaterConfig.md#gbfs-vehicle-rental-systems) for more information. + + +

geofencingZones

+ +**Since version:** `2.4` ∙ **Type:** `boolean` ∙ **Cardinality:** `Optional` ∙ **Default value:** `false` +**Path:** /vehicleRentalServiceDirectory/networks/[0] + +Enables geofencingZones for the given network + +See the regular [GBFS documentation](../UpdaterConfig.md#gbfs-vehicle-rental-systems) for more information. + diff --git a/mkdocs.yml b/mkdocs.yml index 586f78566aa..145d3f3809e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -73,6 +73,12 @@ nav: - Configuration: - Introduction: 'Configuration.md' - Build: 'BuildConfiguration.md' + - OSM Tag Mapping: + - Default: 'osm/Default.md' + - Finland: 'osm/Finland.md' + - Germany: 'osm/Germany.md' + - Norway: 'osm/Norway.md' + - UK: 'osm/UK.md' - Router: 'RouterConfiguration.md' - "Route Request": 'RouteRequest.md' - "Realtime Updaters": 'UpdaterConfig.md' diff --git a/pom.xml b/pom.xml index 88c4933d866..6763eca09c1 100644 --- a/pom.xml +++ b/pom.xml @@ -56,15 +56,15 @@ - 154 + 155 - 31.2 + 31.3 2.51.1 - 2.17.1 + 2.17.2 3.1.7 5.10.3 - 1.13.0 - 5.5.3 + 1.13.2 + 5.6.0 1.5.6 9.11.1 2.0.13 @@ -242,12 +242,12 @@ org.apache.maven.plugins maven-surefire-plugin - 3.3.0 + 3.3.1 me.fabriciorby maven-surefire-junit5-tree-reporter - 1.2.1 + 1.3.0 @@ -321,7 +321,7 @@ but we need the Maven project version as well, so we perform substitution. --> io.github.git-commit-id git-commit-id-maven-plugin - 9.0.0 + 9.0.1 diff --git a/renovate.json5 b/renovate.json5 index 7b957c577e3..d8ba10984e5 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -113,6 +113,7 @@ "org.mockito:mockito-core", "com.tngtech.archunit:archunit", "org.apache.maven.plugins:maven-surefire-plugin", + "me.fabriciorby:maven-surefire-junit5-tree-reporter", "org.jacoco:jacoco-maven-plugin", // coverage plugin "org.apache.commons:commons-compress", // only used by tests // maven plugins diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java index 3971d03ac6d..6466d7bf6d6 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java @@ -41,14 +41,15 @@ import org.opentripplanner.street.search.request.StreetSearchRequest; import org.opentripplanner.street.search.state.State; import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; import org.opentripplanner.transit.model.site.AreaStop; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; /** * This tests that the feed for the Cobb County Flex service is processed correctly. This service - * contains both flex zones but also scheduled stops. Inside the zone passengers can get on or off - * anywhere so there it works more like a taxi. + * contains both flex zones but also scheduled stops. Inside the zone, passengers can get on or off + * anywhere, so there it works more like a taxi. *

* Read about the details at: https://www.cobbcounty.org/transportation/cobblinc/routes-and-schedules/flex */ @@ -212,6 +213,7 @@ private static List getItineraries( var result = TransitRouter.route( request, serverContext, + TransitGroupPriorityService.empty(), transitStartOfTime, additionalSearchDays, new DebugTimingAggregator() diff --git a/src/ext-test/java/org/opentripplanner/ext/siri/AddedTripBuilderTest.java b/src/ext-test/java/org/opentripplanner/ext/siri/AddedTripBuilderTest.java index b7431c16091..59995062eb7 100644 --- a/src/ext-test/java/org/opentripplanner/ext/siri/AddedTripBuilderTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/siri/AddedTripBuilderTest.java @@ -35,6 +35,7 @@ import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.StopModel; +import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.transit.service.TransitModel; import org.opentripplanner.updater.spi.UpdateError; import uk.org.siri.siri20.VehicleModesEnumeration; @@ -77,6 +78,7 @@ class AddedTripBuilderTest { private final Deduplicator DEDUPLICATOR = new Deduplicator(); private final TransitModel TRANSIT_MODEL = new TransitModel(STOP_MODEL, DEDUPLICATOR); + private TransitEditorService transitService; private EntityResolver ENTITY_RESOLVER; @BeforeEach @@ -101,6 +103,7 @@ void setUp() { // Create transit model index TRANSIT_MODEL.index(); + transitService = new DefaultTransitService(TRANSIT_MODEL); // Create the entity resolver only after the model has been indexed ENTITY_RESOLVER = @@ -110,7 +113,7 @@ void setUp() { @Test void testAddedTrip() { var addedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, @@ -151,37 +154,33 @@ void testAddedTrip() { assertEquals(SubMode.of(SUB_MODE), route.getNetexSubmode(), "submode should be mapped"); assertNotEquals(REPLACED_ROUTE, route, "Should not re-use replaced route"); - // Assert transit model index - var transitModelIndex = TRANSIT_MODEL.getTransitModelIndex(); - assertNotNull(transitModelIndex); assertEquals( route, - transitModelIndex.getRouteForId(TransitModelForTest.id(LINE_REF)), + transitService.getRouteForId(TransitModelForTest.id(LINE_REF)), "Route should be added to transit index" ); assertEquals( trip, - transitModelIndex.getTripForId().get(TRIP_ID), + transitService.getTripForId(TRIP_ID), "Route should be added to transit index" ); - var pattern = transitModelIndex.getPatternForTrip().get(trip); + var pattern = transitService.getPatternForTrip(trip); assertNotNull(pattern); assertEquals(route, pattern.getRoute()); assertTrue( - transitModelIndex - .getServiceCodesRunningForDate() - .get(SERVICE_DATE) + transitService + .getServiceCodesRunningForDate(SERVICE_DATE) .contains(TRANSIT_MODEL.getServiceCodes().get(trip.getServiceId())), "serviceId should be running on service date" ); assertNotNull( - transitModelIndex.getTripOnServiceDateById().get(TRIP_ID), + transitService.getTripOnServiceDateById(TRIP_ID), "TripOnServiceDate should be added to transit index by id" ); assertNotNull( - transitModelIndex - .getTripOnServiceDateForTripAndDay() - .get(new TripIdAndServiceDate(TRIP_ID, SERVICE_DATE)), + transitService.getTripOnServiceDateForTripAndDay( + new TripIdAndServiceDate(TRIP_ID, SERVICE_DATE) + ), "TripOnServiceDate should be added to transit index for trip and day" ); @@ -239,7 +238,7 @@ void testAddedTrip() { @Test void testAddedTripOnAddedRoute() { var firstAddedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, @@ -265,7 +264,7 @@ void testAddedTripOnAddedRoute() { var tripId2 = TransitModelForTest.id("TRIP_ID_2"); var secondAddedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, tripId2, @@ -296,10 +295,7 @@ void testAddedTripOnAddedRoute() { Route route = secondTrip.getRoute(); assertSame(firstTrip.getRoute(), route, "route be reused from the first trip"); - // Assert transit model index - var transitModelIndex = TRANSIT_MODEL.getTransitModelIndex(); - assertNotNull(transitModelIndex); - assertEquals(2, transitModelIndex.getPatternsForRoute().get(route).size()); + assertEquals(2, transitService.getPatternsForRoute(route).size()); // Assert trip times var times = secondAddedTrip.successValue().tripTimes(); @@ -316,7 +312,7 @@ void testAddedTripOnAddedRoute() { @Test void testAddedTripOnExistingRoute() { var addedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, @@ -347,7 +343,7 @@ void testAddedTripOnExistingRoute() { @Test void testAddedTripWithoutReplacedRoute() { var addedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, @@ -390,7 +386,7 @@ void testAddedTripWithoutReplacedRoute() { @Test void testAddedTripFailOnMissingServiceId() { var addedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, @@ -445,7 +441,7 @@ void testAddedTripFailOnNonIncreasingDwellTime() { ); var addedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, @@ -484,7 +480,7 @@ void testAddedTripFailOnTooFewCalls() { .build() ); var addedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, @@ -531,7 +527,7 @@ void testAddedTripFailOnUnknownStop() { .build() ); var addedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, diff --git a/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java b/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java index a069f5c6656..45e7979b353 100644 --- a/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java @@ -1,24 +1,13 @@ package org.opentripplanner.ext.siri; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertFailure; -import java.util.List; import java.util.Set; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.opentripplanner.model.PickDrop; import org.opentripplanner.transit.model.timetable.RealTimeState; -import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.model.timetable.TripOnServiceDate; -import org.opentripplanner.transit.model.timetable.TripTimes; -import org.opentripplanner.transit.model.timetable.TripTimesFactory; -import org.opentripplanner.transit.service.DefaultTransitService; -import org.opentripplanner.transit.service.StopModel; -import org.opentripplanner.transit.service.TransitModel; -import org.opentripplanner.transit.service.TransitService; -import org.opentripplanner.updater.TimetableSnapshotSourceParameters; import org.opentripplanner.updater.spi.UpdateError; -import org.opentripplanner.updater.spi.UpdateResult; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; class SiriTimetableSnapshotSourceTest { @@ -88,6 +77,30 @@ void testAddedJourneyWithInvalidScheduledData() { assertFailure(UpdateError.UpdateErrorType.NEGATIVE_HOP_TIME, result); } + @Test + void testAddedJourneyWithUnresolvableAgency() { + var env = RealtimeTestEnvironment.siri(); + + // Create an extra journey with unknown line and operator + var createExtraJourney = new SiriEtBuilder(env.getDateTimeHelper()) + .withEstimatedVehicleJourneyCode("newJourney") + .withIsExtraJourney(true) + .withOperatorRef("unknown operator") + .withLineRef("unknown line") + .withEstimatedCalls(builder -> + builder + .call(env.stopA1) + .departAimedExpected("10:58", "10:48") + .call(env.stopB1) + .arriveAimedExpected("10:08", "10:58") + ) + .buildEstimatedTimetableDeliveries(); + + var result = env.applyEstimatedTimetable(createExtraJourney); + assertEquals(0, result.successful()); + assertFailure(UpdateError.UpdateErrorType.CANNOT_RESOLVE_AGENCY, result); + } + @Test void testReplaceJourney() { var env = RealtimeTestEnvironment.siri(); @@ -402,10 +415,6 @@ void testExtraUnknownStop() { assertFailure(UpdateError.UpdateErrorType.INVALID_STOP_SEQUENCE, result); } - private void assertFailure(UpdateError.UpdateErrorType expectedError, UpdateResult result) { - assertEquals(Set.of(expectedError), result.failures().keySet()); - } - private static SiriEtBuilder updatedJourneyBuilder(RealtimeTestEnvironment env) { return new SiriEtBuilder(env.getDateTimeHelper()) .withEstimatedCalls(builder -> diff --git a/src/ext/java/org/opentripplanner/ext/reportapi/model/BicycleSafetyReport.java b/src/ext/java/org/opentripplanner/ext/reportapi/model/BicycleSafetyReport.java deleted file mode 100644 index 8830189107d..00000000000 --- a/src/ext/java/org/opentripplanner/ext/reportapi/model/BicycleSafetyReport.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.opentripplanner.ext.reportapi.model; - -import org.opentripplanner.openstreetmap.tagmapping.OsmTagMapperSource; -import org.opentripplanner.openstreetmap.wayproperty.WayPropertySet; - -public class BicycleSafetyReport { - - public static void main(String[] args) { - System.out.println(makeCsv(OsmTagMapperSource.NORWAY)); - } - - public static String makeCsv(OsmTagMapperSource source) { - var wayPropertySet = new WayPropertySet(); - - source.getInstance().populateProperties(wayPropertySet); - - var buf = new CsvReportBuilder(","); - - buf.addHeader( - "OSM tags for osmWayPropertySet " + source, - "mixin", - "permissions", - "safety penalty there", - "safety penalty back" - ); - - wayPropertySet - .getWayProperties() - .forEach(p -> { - buf.addText(p.specifier().toString()); - buf.addBoolean(false); - buf.addText(p.properties().getPermission().toString()); - - var safetyProps = p.properties().bicycleSafety(); - if (safetyProps != null) { - buf.addNumber(safetyProps.forward()); - buf.addNumber(safetyProps.back()); - } - buf.newLine(); - }); - - wayPropertySet - .getMixins() - .forEach(p -> { - buf.addText(p.specifier().toString()); - buf.addBoolean(true); - buf.addText(""); - - var safetyProps = p.bicycleSafety(); - buf.addNumber(safetyProps.forward()); - buf.addNumber(safetyProps.back()); - buf.newLine(); - }); - - return buf.toString(); - } -} diff --git a/src/ext/java/org/opentripplanner/ext/reportapi/model/TransitGroupPriorityReport.java b/src/ext/java/org/opentripplanner/ext/reportapi/model/TransitGroupPriorityReport.java index 635469cb3a2..66ad29fad56 100644 --- a/src/ext/java/org/opentripplanner/ext/reportapi/model/TransitGroupPriorityReport.java +++ b/src/ext/java/org/opentripplanner/ext/reportapi/model/TransitGroupPriorityReport.java @@ -4,9 +4,9 @@ import java.util.TreeMap; import java.util.TreeSet; import java.util.stream.Collectors; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.PriorityGroupConfigurator; import org.opentripplanner.routing.api.request.request.TransitRequest; import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; /** * This class is used to report all transit-groups used for transit-group-priority. The report is @@ -17,14 +17,14 @@ public class TransitGroupPriorityReport { public static String build(Collection patterns, TransitRequest request) { - var c = PriorityGroupConfigurator.of( + var service = new TransitGroupPriorityService( request.priorityGroupsByAgency(), request.priorityGroupsGlobal() ); var map = new TreeMap(); for (var it : patterns) { - int groupId = c.lookupTransitGroupPriorityId(it); + int groupId = service.lookupTransitGroupPriorityId(it); var de = map.computeIfAbsent(groupId, DebugEntity::new); de.add( it.getRoute().getAgency().getId().toString(), diff --git a/src/ext/java/org/opentripplanner/ext/reportapi/resource/ReportResource.java b/src/ext/java/org/opentripplanner/ext/reportapi/resource/ReportResource.java index a859b4ff78a..e06aec71b53 100644 --- a/src/ext/java/org/opentripplanner/ext/reportapi/resource/ReportResource.java +++ b/src/ext/java/org/opentripplanner/ext/reportapi/resource/ReportResource.java @@ -1,25 +1,18 @@ package org.opentripplanner.ext.reportapi.resource; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.time.Duration; -import org.opentripplanner.ext.reportapi.model.BicycleSafetyReport; import org.opentripplanner.ext.reportapi.model.CachedValue; import org.opentripplanner.ext.reportapi.model.GraphReportBuilder; import org.opentripplanner.ext.reportapi.model.GraphReportBuilder.GraphStats; import org.opentripplanner.ext.reportapi.model.TransfersReport; import org.opentripplanner.ext.reportapi.model.TransitGroupPriorityReport; import org.opentripplanner.model.transfer.TransferService; -import org.opentripplanner.openstreetmap.tagmapping.OsmTagMapperSource; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.standalone.api.OtpServerRequestContext; import org.opentripplanner.transit.service.TransitService; @@ -51,39 +44,6 @@ public String getTransfersAsCsv() { return TransfersReport.export(transferService.listAll(), transitService); } - @GET - @Path("/bicycle-safety.html") - @Produces(MediaType.TEXT_HTML) - public Response getBicycleSafetyPage() { - try (var is = getClass().getResourceAsStream("/reportapi/report.html")) { - return Response.ok(new String(is.readAllBytes(), StandardCharsets.UTF_8)).build(); - } catch (IOException e) { - return Response.serverError().build(); - } - } - - @GET - @Path("/bicycle-safety.csv") - @Produces("text/csv") - public Response getBicycleSafetyAsCsv( - @DefaultValue("DEFAULT") @QueryParam("osmWayPropertySet") String osmWayPropertySet - ) { - OsmTagMapperSource source; - try { - source = OsmTagMapperSource.valueOf(osmWayPropertySet.toUpperCase()); - } catch (IllegalArgumentException ignore) { - throw new BadRequestException("Unknown osmWayPropertySet: " + osmWayPropertySet); - } - - return Response - .ok(BicycleSafetyReport.makeCsv(source)) - .header( - "Content-Disposition", - "attachment; filename=\"" + osmWayPropertySet + "-bicycle-safety.csv\"" - ) - .build(); - } - @GET @Path("/transit/group/priorities") @Produces(MediaType.TEXT_PLAIN) diff --git a/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java b/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java index 8eae3be7077..be5b30b38b9 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java +++ b/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java @@ -2,6 +2,7 @@ import static java.lang.Boolean.TRUE; import static org.opentripplanner.ext.siri.mapper.SiriTransportModeMapper.mapTransitMainMode; +import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.CANNOT_RESOLVE_AGENCY; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_START_DATE; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_VALID_STOPS; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.TOO_FEW_STOPS; @@ -13,6 +14,8 @@ import java.util.List; import java.util.Objects; import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.opentripplanner.ext.siri.mapper.PickDropMapper; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.framework.time.ServiceDateUtils; @@ -33,7 +36,7 @@ import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimesFactory; -import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.updater.spi.DataValidationExceptionMapper; import org.opentripplanner.updater.spi.UpdateError; import org.rutebanken.netex.model.BusSubmodeEnumeration; @@ -49,7 +52,7 @@ class AddedTripBuilder { private static final Logger LOG = LoggerFactory.getLogger(AddedTripBuilder.class); - private final TransitModel transitModel; + private final TransitEditorService transitService; private final EntityResolver entityResolver; private final ZoneId timeZone; private final Function getTripPatternId; @@ -70,7 +73,7 @@ class AddedTripBuilder { AddedTripBuilder( EstimatedVehicleJourney estimatedVehicleJourney, - TransitModel transitModel, + TransitEditorService transitService, EntityResolver entityResolver, Function getTripPatternId ) { @@ -109,16 +112,16 @@ class AddedTripBuilder { calls = CallWrapper.of(estimatedVehicleJourney); - this.transitModel = transitModel; + this.transitService = transitService; this.entityResolver = entityResolver; this.getTripPatternId = getTripPatternId; - timeZone = transitModel.getTimeZone(); + timeZone = transitService.getTimeZone(); replacedTrips = getReplacedVehicleJourneys(estimatedVehicleJourney); } AddedTripBuilder( - TransitModel transitModel, + TransitEditorService transitService, EntityResolver entityResolver, Function getTripPatternId, FeedScopedId tripId, @@ -136,9 +139,9 @@ class AddedTripBuilder { String headsign, List replacedTrips ) { - this.transitModel = transitModel; + this.transitService = transitService; this.entityResolver = entityResolver; - this.timeZone = transitModel.getTimeZone(); + this.timeZone = transitService.getTimeZone(); this.getTripPatternId = getTripPatternId; this.tripId = tripId; this.operator = operator; @@ -165,16 +168,20 @@ Result build() { return UpdateError.result(tripId, NO_START_DATE); } - FeedScopedId calServiceId = transitModel.getOrCreateServiceIdForDate(serviceDate); + FeedScopedId calServiceId = transitService.getOrCreateServiceIdForDate(serviceDate); if (calServiceId == null) { return UpdateError.result(tripId, NO_START_DATE); } Route route = entityResolver.resolveRoute(lineRef); if (route == null) { - route = createRoute(); + Agency agency = resolveAgency(); + if (agency == null) { + return UpdateError.result(tripId, CANNOT_RESOLVE_AGENCY); + } + route = createRoute(agency); LOG.info("Adding route {} to transitModel.", route); - transitModel.getTransitModelIndex().addRoutes(route); + transitService.addRoutes(route); } Trip trip = createTrip(route, calServiceId); @@ -214,14 +221,14 @@ Result build() { RealTimeTripTimes tripTimes = TripTimesFactory.tripTimes( trip, aimedStopTimes, - transitModel.getDeduplicator() + transitService.getDeduplicator() ); // validate the scheduled trip times // they are in general superseded by real-time trip times // but in case of trip cancellation, OTP will fall back to scheduled trip times // therefore they must be valid tripTimes.validateNonIncreasingTimes(); - tripTimes.setServiceCode(transitModel.getServiceCodes().get(trip.getServiceId())); + tripTimes.setServiceCode(transitService.getServiceCodeForId(trip.getServiceId())); pattern.add(tripTimes); RealTimeTripTimes updatedTripTimes = tripTimes.copyScheduledTimes(); @@ -260,17 +267,14 @@ Result build() { // Adding trip to index necessary to include values in graphql-queries // TODO - SIRI: should more data be added to index? - transitModel.getTransitModelIndex().getTripForId().put(tripId, trip); - transitModel.getTransitModelIndex().getPatternForTrip().put(trip, pattern); - transitModel.getTransitModelIndex().getPatternsForRoute().put(route, pattern); - transitModel - .getTransitModelIndex() - .getTripOnServiceDateById() - .put(tripOnServiceDate.getId(), tripOnServiceDate); - transitModel - .getTransitModelIndex() - .getTripOnServiceDateForTripAndDay() - .put(new TripIdAndServiceDate(tripId, serviceDate), tripOnServiceDate); + transitService.addTripForId(tripId, trip); + transitService.addPatternForTrip(trip, pattern); + transitService.addPatternsForRoute(route, pattern); + transitService.addTripOnServiceDateById(tripOnServiceDate.getId(), tripOnServiceDate); + transitService.addTripOnServiceDateForTripAndDay( + new TripIdAndServiceDate(tripId, serviceDate), + tripOnServiceDate + ); return Result.success(new TripUpdate(stopPattern, updatedTripTimes, serviceDate)); } @@ -278,34 +282,40 @@ Result build() { /** * Method to create a Route. Commonly used to create a route if a real-time message * refers to a route that is not in the transit model. - * - * We will find the first Route with same Operator, and use the same Authority - * If no operator found, copy the agency from replaced route - * * If no name is given for the route, an empty string will be set as the name. * * @return a new Route */ - private Route createRoute() { + @Nonnull + private Route createRoute(Agency agency) { var routeBuilder = Route.of(entityResolver.resolveId(lineRef)); routeBuilder.withShortName(shortName); routeBuilder.withMode(transitMode); routeBuilder.withNetexSubmode(transitSubMode); routeBuilder.withOperator(operator); + routeBuilder.withAgency(agency); - // TODO - SIRI: Is there a better way to find authority/Agency? - Agency agency = transitModel - .getTransitModelIndex() + return routeBuilder.build(); + } + + /** + * Attempt to find the agency to which this new trip belongs. + * The algorithm retrieves any route operated by the same operator as the one operating this new + * trip and resolves its agency. + * If no route with the same operator can be found, the algorithm falls back to retrieving the + * agency operating the replaced route. + * If none can be found the method returns null. + */ + @Nullable + private Agency resolveAgency() { + return transitService .getAllRoutes() .stream() .filter(r -> r != null && r.getOperator() != null && r.getOperator().equals(operator)) .findFirst() .map(Route::getAgency) .orElseGet(() -> replacedRoute != null ? replacedRoute.getAgency() : null); - routeBuilder.withAgency(agency); - - return routeBuilder.build(); } private Trip createTrip(Route route, FeedScopedId calServiceId) { diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java index db9c1fcc441..7ab316bfc55 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java @@ -23,8 +23,8 @@ import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.transit.service.DefaultTransitService; +import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.transit.service.TransitModel; -import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.updater.TimetableSnapshotSourceParameters; import org.opentripplanner.updater.spi.DataValidationExceptionMapper; import org.opentripplanner.updater.spi.UpdateError; @@ -56,9 +56,8 @@ public class SiriTimetableSnapshotSource implements TimetableSnapshotProvider { * messages. */ private final SiriTripPatternCache tripPatternCache; - private final TransitModel transitModel; - private final TransitService transitService; + private final TransitEditorService transitService; private final TimetableSnapshotManager snapshotManager; @@ -72,7 +71,6 @@ public SiriTimetableSnapshotSource( parameters, () -> LocalDate.now(transitModel.getTimeZone()) ); - this.transitModel = transitModel; this.transitService = new DefaultTransitService(transitModel); this.tripPatternCache = new SiriTripPatternCache(tripPatternIdGenerator, transitService::getPatternForTrip); @@ -115,7 +113,7 @@ public UpdateResult applyEstimatedTimetable( var journeys = estimatedJourneyVersion.getEstimatedVehicleJourneies(); LOG.debug("Handling {} EstimatedVehicleJourneys.", journeys.size()); for (EstimatedVehicleJourney journey : journeys) { - results.add(apply(journey, transitModel, fuzzyTripMatcher, entityResolver)); + results.add(apply(journey, transitService, fuzzyTripMatcher, entityResolver)); } } } @@ -135,7 +133,7 @@ public TimetableSnapshot getTimetableSnapshot() { private Result apply( EstimatedVehicleJourney journey, - TransitModel transitModel, + TransitEditorService transitService, @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher, EntityResolver entityResolver ) { @@ -147,7 +145,7 @@ private Result apply( result = new AddedTripBuilder( journey, - transitModel, + transitService, entityResolver, tripPatternIdGenerator::generateUniqueTripPatternId ) @@ -265,7 +263,7 @@ private Result handleModifiedTrip( pattern, estimatedVehicleJourney, serviceDate, - transitModel.getTimeZone(), + transitService.getTimeZone(), entityResolver ) .build(); diff --git a/src/ext/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSource.java b/src/ext/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSource.java index 3876ff44651..b65c22b4727 100644 --- a/src/ext/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSource.java +++ b/src/ext/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSource.java @@ -8,6 +8,7 @@ import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; +import org.opentripplanner.service.vehiclerental.model.VehicleRentalSystem; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.updater.spi.DataSource; import org.opentripplanner.updater.spi.GenericJsonDataSource; @@ -32,6 +33,7 @@ public class SmooveBikeRentalDataSource private final String networkName; private final RentalVehicleType vehicleType; + private final VehicleRentalSystem system; public SmooveBikeRentalDataSource(SmooveBikeRentalDataSourceParameters config) { this(config, new OtpHttpClientFactory()); @@ -45,6 +47,24 @@ public SmooveBikeRentalDataSource( networkName = config.getNetwork(DEFAULT_NETWORK_NAME); vehicleType = RentalVehicleType.getDefaultType(networkName); overloadingAllowed = config.overloadingAllowed(); + system = + new VehicleRentalSystem( + networkName, + "fi", + "Helsinki/Espoo", + null, + null, + null, + null, + null, + null, + null, + null, + "Europe/Helsinki", + null, + null, + null + ); } /** @@ -94,6 +114,7 @@ protected VehicleRentalStation parseElement(JsonNode node) { station.vehicleTypesAvailable = Map.of(vehicleType, station.vehiclesAvailable); station.vehicleSpacesAvailable = Map.of(vehicleType, station.spacesAvailable); station.overloadingAllowed = overloadingAllowed; + station.system = system; return station; } } diff --git a/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java b/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java index 9b350dec345..03b0acd59cd 100644 --- a/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java +++ b/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java @@ -105,8 +105,7 @@ private static List buildListOfNetworksFr new GbfsVehicleRentalDataSourceParameters( updaterUrl.get(), parameters.getLanguage(), - // allowKeepingRentedVehicleAtDestination - not part of GBFS, not supported here - false, + networkParams.allowKeepingAtDestination(), parameters.getHeaders(), networkName, networkParams.geofencingZones(), diff --git a/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/api/NetworkParameters.java b/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/api/NetworkParameters.java index 25d394f72ec..c8b638edae3 100644 --- a/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/api/NetworkParameters.java +++ b/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/api/NetworkParameters.java @@ -5,15 +5,18 @@ /** * Parameters for a specific network. *

- * The {@link GbfsVehicleRentalDataSourceParameters} support {@code overloadingAllowed} and - * {@code allowKeepingRentedVehicleAtDestination} is not included here since they are not part of - * the GBFS specification. If there is a demand for these, they can be added. + * The {@link GbfsVehicleRentalDataSourceParameters} supports {@code overloadingAllowed} + * which is not included here since it is not part of + * the GBFS specification. If there is a demand for it, it can be added. *

* @param network The network name * @param geofencingZones enable geofencingZones for the given network + * @param allowKeepingAtDestination if a vehicle that was picked up from a station must be returned + * to another one or can be kept at the destination. + * {@link org.opentripplanner.standalone.config.routerconfig.updaters.sources.VehicleRentalSourceFactory#allOwKeepingRentedVehicleAtDestination()} */ -public record NetworkParameters(String network, boolean geofencingZones) { - public NetworkParameters withName(String network) { - return new NetworkParameters(network, geofencingZones); - } -} +public record NetworkParameters( + String network, + boolean geofencingZones, + boolean allowKeepingAtDestination +) {} diff --git a/src/ext/resources/reportapi/report.html b/src/ext/resources/reportapi/report.html deleted file mode 100644 index bfa5af66603..00000000000 --- a/src/ext/resources/reportapi/report.html +++ /dev/null @@ -1,93 +0,0 @@ - - - - - OTP Bicycle safety factor report browser - - - - - - - - - - - - - - - - -

- - -
-

OTP Bicycle safety factor report browser

-
- Documentation -
- -
- -
- -
- - - - - - - - - - - - -
OSM tag match expressionMixin?Traversal permissionsSafety factor there1Safety factor back1
-
- -
- 1: Smaller means more cycling friendly. Larger means less cycle friendly. Think of at as a multiplier for the cost of traversing the way. -
-
-
-
- - - - - - - diff --git a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java index e309346339b..ca059723acd 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java @@ -75,6 +75,7 @@ import org.opentripplanner.apis.gtfs.datafetchers.UnknownImpl; import org.opentripplanner.apis.gtfs.datafetchers.VehicleParkingImpl; import org.opentripplanner.apis.gtfs.datafetchers.VehiclePositionImpl; +import org.opentripplanner.apis.gtfs.datafetchers.VehicleRentalNetworkImpl; import org.opentripplanner.apis.gtfs.datafetchers.VehicleRentalStationImpl; import org.opentripplanner.apis.gtfs.datafetchers.debugOutputImpl; import org.opentripplanner.apis.gtfs.datafetchers.elevationProfileComponentImpl; @@ -166,6 +167,7 @@ protected static GraphQLSchema buildSchema() { .type(typeWiring.build(BookingTimeImpl.class)) .type(typeWiring.build(BookingInfoImpl.class)) .type(typeWiring.build(VehicleRentalStationImpl.class)) + .type(typeWiring.build(VehicleRentalNetworkImpl.class)) .type(typeWiring.build(RentalVehicleImpl.class)) .type(typeWiring.build(RentalVehicleTypeImpl.class)) .type(typeWiring.build(StopOnRouteImpl.class)) 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 eae4bc2ad33..f187a49d9c7 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java @@ -330,6 +330,7 @@ public DataFetcher> nearest() { List filterByPlaceTypes = args.getGraphQLFilterByPlaceTypes() != null ? args.getGraphQLFilterByPlaceTypes().stream().map(GraphQLUtils::toModel).toList() : DEFAULT_PLACE_TYPES; + List filterByNetwork = args.getGraphQLFilterByNetwork(); List places; try { @@ -347,6 +348,7 @@ public DataFetcher> nearest() { filterByStations, filterByRoutes, filterByBikeRentalStations, + filterByNetwork, getTransitService(environment) ) ); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RentalVehicleImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RentalVehicleImpl.java index 53c3ba1343d..c4fb92c0ef4 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RentalVehicleImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RentalVehicleImpl.java @@ -6,6 +6,7 @@ import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris; +import org.opentripplanner.service.vehiclerental.model.VehicleRentalSystem; import org.opentripplanner.service.vehiclerental.model.VehicleRentalVehicle; public class RentalVehicleImpl implements GraphQLDataFetchers.GraphQLRentalVehicle { @@ -61,6 +62,11 @@ public DataFetcher vehicleType() { return environment -> getSource(environment).vehicleType; } + @Override + public DataFetcher rentalNetwork() { + return environment -> getSource(environment).getVehicleRentalSystem(); + } + private VehicleRentalVehicle getSource(DataFetchingEnvironment environment) { return environment.getSource(); } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehicleRentalNetworkImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehicleRentalNetworkImpl.java new file mode 100644 index 00000000000..75f771eed83 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehicleRentalNetworkImpl.java @@ -0,0 +1,23 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; +import org.opentripplanner.service.vehiclerental.model.VehicleRentalSystem; + +public class VehicleRentalNetworkImpl implements GraphQLDataFetchers.GraphQLVehicleRentalNetwork { + + @Override + public DataFetcher networkId() { + return environment -> getSource(environment).systemId; + } + + @Override + public DataFetcher url() { + return environment -> getSource(environment).url; + } + + private VehicleRentalSystem getSource(DataFetchingEnvironment environment) { + return environment.getSource(); + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehicleRentalStationImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehicleRentalStationImpl.java index dc60a7c76e8..0603d19e412 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehicleRentalStationImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehicleRentalStationImpl.java @@ -7,6 +7,7 @@ import org.opentripplanner.service.vehiclerental.model.RentalVehicleEntityCounts; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris; +import org.opentripplanner.service.vehiclerental.model.VehicleRentalSystem; public class VehicleRentalStationImpl implements GraphQLDataFetchers.GraphQLVehicleRentalStation { @@ -107,6 +108,11 @@ public DataFetcher availableSpaces() { return environment -> getSource(environment).getVehicleSpaceCounts(); } + @Override + public DataFetcher rentalNetwork() { + return environment -> getSource(environment).getVehicleRentalSystem(); + } + private VehicleRentalStation getSource(DataFetchingEnvironment environment) { return environment.getSource(); } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java index 6c66d3992f4..67944543580 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java @@ -58,6 +58,7 @@ import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris; +import org.opentripplanner.service.vehiclerental.model.VehicleRentalSystem; import org.opentripplanner.service.vehiclerental.model.VehicleRentalVehicle; import org.opentripplanner.transit.model.basic.Money; import org.opentripplanner.transit.model.network.Route; @@ -852,6 +853,8 @@ public interface GraphQLRentalVehicle { public DataFetcher operative(); + public DataFetcher rentalNetwork(); + public DataFetcher rentalUris(); public DataFetcher vehicleId(); @@ -1266,6 +1269,17 @@ public interface GraphQLVehiclePosition { public DataFetcher vehicleId(); } + /** + * Vehicle rental network, which is referred as system in the GBFS terminology. Note, the same operator can operate in multiple + * regions either with the same network/system or with a different one. This can contain information about either the rental brand + * or about the operator. + */ + public interface GraphQLVehicleRentalNetwork { + public DataFetcher networkId(); + + public DataFetcher url(); + } + /** Vehicle rental station represents a location where users can rent bicycles etc. for a fee. */ public interface GraphQLVehicleRentalStation { public DataFetcher allowDropoff(); @@ -1298,6 +1312,8 @@ public interface GraphQLVehicleRentalStation { public DataFetcher realtime(); + public DataFetcher rentalNetwork(); + public DataFetcher rentalUris(); public DataFetcher spacesAvailable(); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java index 3c187ca3bbe..541219481ef 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java @@ -2409,6 +2409,7 @@ public static class GraphQLQueryTypeNearestArgs { private String before; private GraphQLInputFiltersInput filterByIds; private List filterByModes; + private List filterByNetwork; private List filterByPlaceTypes; private Integer first; private Integer last; @@ -2430,6 +2431,7 @@ public GraphQLQueryTypeNearestArgs(Map args) { .map(GraphQLMode.class::cast) .collect(Collectors.toList()); } + this.filterByNetwork = (List) args.get("filterByNetwork"); if (args.get("filterByPlaceTypes") != null) { this.filterByPlaceTypes = ((List) args.get("filterByPlaceTypes")).stream() @@ -2466,6 +2468,10 @@ public List getGraphQLFilterByModes() { return this.filterByModes; } + public List getGraphQLFilterByNetwork() { + return this.filterByNetwork; + } + public List getGraphQLFilterByPlaceTypes() { return this.filterByPlaceTypes; } @@ -2510,6 +2516,10 @@ public void setGraphQLFilterByModes(List filterByModes) { this.filterByModes = filterByModes; } + public void setGraphQLFilterByNetwork(List filterByNetwork) { + this.filterByNetwork = filterByNetwork; + } + public void setGraphQLFilterByPlaceTypes(List filterByPlaceTypes) { this.filterByPlaceTypes = filterByPlaceTypes; } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml b/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml index ff9af6f6aa0..b9ee0ac3e16 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml @@ -47,6 +47,7 @@ config: BikeRentalStation: org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace#VehicleRentalPlace BikeRentalStationUris: org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris#VehicleRentalStationUris VehicleRentalStation: org.opentripplanner.service.vehiclerental.model.VehicleRentalStation#VehicleRentalStation + VehicleRentalNetwork: org.opentripplanner.service.vehiclerental.model.VehicleRentalSystem#VehicleRentalSystem RentalVehicleEntityCounts: org.opentripplanner.service.vehiclerental.model.RentalVehicleEntityCounts#RentalVehicleEntityCounts RentalVehicleTypeCount: org.opentripplanner.service.vehiclerental.model.RentalVehicleTypeCount#RentalVehicleTypeCount RentalVehicle: org.opentripplanner.service.vehiclerental.model.VehicleRentalVehicle#VehicleRentalVehicle diff --git a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java index a88c36ac039..9ad43606420 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java @@ -912,6 +912,7 @@ private GraphQLSchema create() { List filterByBikeRentalStations = null; List filterByBikeParks = null; List filterByCarParks = null; + List filterByNetwork = null; @SuppressWarnings("rawtypes") Map filterByIds = environment.getArgument("filterByIds"); if (filterByIds != null) { @@ -960,6 +961,7 @@ private GraphQLSchema create() { filterByStations, filterByRoutes, filterByBikeRentalStations, + filterByNetwork, GqlUtil.getTransitService(environment) ); 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 6c173ba815f..6e63ddf3921 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java @@ -22,6 +22,7 @@ import org.opentripplanner.transit.model.basic.Accessibility; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.network.BikeAccess; +import org.opentripplanner.transit.model.site.StopTransferPriority; import org.opentripplanner.transit.model.timetable.Direction; import org.opentripplanner.transit.model.timetable.OccupancyStatus; import org.opentripplanner.transit.model.timetable.RealTimeState; @@ -133,6 +134,7 @@ public class EnumTypes { public static final GraphQLEnumType INTERCHANGE_WEIGHTING = GraphQLEnumType .newEnum() + .description("Deprecated. Use STOP_INTERCHANGE_PRIORITY") .name("InterchangeWeighting") .value("preferredInterchange", 2, "Highest priority interchange.") .value("recommendedInterchange", 1, "Second highest priority interchange.") @@ -140,6 +142,32 @@ public class EnumTypes { .value("noInterchange", -1, "Interchange not allowed.") .build(); + public static final GraphQLEnumType STOP_INTERCHANGE_PRIORITY = GraphQLEnumType + .newEnum() + .name("StopInterchangePriority") + .value( + "preferred", + StopTransferPriority.PREFERRED, + "Preferred place to transfer, strongly recommended. NeTEx equivalent is PREFERRED_INTERCHANGE." + ) + .value( + "recommended", + StopTransferPriority.RECOMMENDED, + "Recommended stop place. NeTEx equivalent is RECOMMENDED_INTERCHANGE." + ) + .value( + "allowed", + StopTransferPriority.ALLOWED, + "Allow transfers from/to this stop. This is the default. NeTEx equivalent is INTERCHANGE_ALLOWED." + ) + .value( + "discouraged", + StopTransferPriority.DISCOURAGED, + "Block transfers from/to this stop. In OTP this is not a definitive block," + + " just a huge penalty is added to the cost function. NeTEx equivalent is NO_INTERCHANGE." + ) + .build(); + public static final GraphQLEnumType ITINERARY_FILTER_DEBUG_PROFILE = createFromDocumentedEnum( "ItineraryFilterDebugProfile", List.of( diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/stop/MonoOrMultiModalStation.java b/src/main/java/org/opentripplanner/apis/transmodel/model/stop/MonoOrMultiModalStation.java index 3055c1a0115..9a7c92f6618 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/stop/MonoOrMultiModalStation.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/stop/MonoOrMultiModalStation.java @@ -8,6 +8,7 @@ import org.opentripplanner.transit.model.site.MultiModalStation; import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.site.StopTransferPriority; public class MonoOrMultiModalStation { @@ -40,6 +41,8 @@ public class MonoOrMultiModalStation { private final MonoOrMultiModalStation parentStation; + private final StopTransferPriority priority; + public MonoOrMultiModalStation(Station station, MultiModalStation parentStation) { this.id = station.getId(); this.name = station.getName(); @@ -50,6 +53,7 @@ public MonoOrMultiModalStation(Station station, MultiModalStation parentStation) this.url = station.getUrl(); this.timezone = station.getTimezone(); this.childStops = station.getChildStops(); + this.priority = station.getPriority(); this.parentStation = parentStation != null ? new MonoOrMultiModalStation(parentStation) : null; } @@ -63,6 +67,7 @@ public MonoOrMultiModalStation(MultiModalStation multiModalStation) { this.url = multiModalStation.getUrl(); this.timezone = null; this.childStops = multiModalStation.getChildStops(); + this.priority = null; this.parentStation = null; } @@ -102,6 +107,10 @@ public Collection getChildStops() { return childStops; } + public StopTransferPriority getPriority() { + return priority; + } + public MonoOrMultiModalStation getParentStation() { return parentStation; } 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 13a1521bf1a..53621ee9b5a 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 @@ -147,10 +147,22 @@ public static GraphQLObjectType create( .description( "Relative weighting of this stop with regards to interchanges. NOT IMPLEMENTED" ) + .deprecate("Not implemented. Use stopInterchangePriority") .type(EnumTypes.INTERCHANGE_WEIGHTING) .dataFetcher(environment -> 0) .build() ) + .field( + GraphQLFieldDefinition + .newFieldDefinition() + .name("stopInterchangePriority") + .description("Specify the priority of interchanges at this stop") + .type(EnumTypes.STOP_INTERCHANGE_PRIORITY) + .dataFetcher(environment -> + ((MonoOrMultiModalStation) environment.getSource()).getPriority() + ) + .build() + ) .field( GraphQLFieldDefinition .newFieldDefinition() diff --git a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java index 29489de19f2..8ee5d100b94 100644 --- a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java +++ b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java @@ -89,7 +89,7 @@ public enum OTPFeature { FaresV2(false, true, "Enable import of GTFS-Fares v2 data."), FlexRouting(false, true, "Enable FLEX routing."), GoogleCloudStorage(false, true, "Enable Google Cloud Storage integration."), - LegacyRestApi(true, true, "Enable legacy REST API. This API will be removed in the future."), + LegacyRestApi(false, true, "Enable legacy REST API. This API will be removed in the future."), RealtimeResolver( false, true, diff --git a/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java b/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java index 287a1a71c21..3137b66070f 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java @@ -67,9 +67,7 @@ public DirectTransferGenerator( @Override public void buildGraph() { /* Initialize transit model index which is needed by the nearby stop finder. */ - if (transitModel.getTransitModelIndex() == null) { - transitModel.index(); - } + transitModel.index(); /* The linker will use streets if they are available, or straight-line distance otherwise. */ NearbyStopFinder nearbyStopFinder = createNearbyStopFinder(); diff --git a/src/main/java/org/opentripplanner/inspector/vector/LayerParameters.java b/src/main/java/org/opentripplanner/inspector/vector/LayerParameters.java index ca4a6a64c76..cb849e0f0ac 100644 --- a/src/main/java/org/opentripplanner/inspector/vector/LayerParameters.java +++ b/src/main/java/org/opentripplanner/inspector/vector/LayerParameters.java @@ -10,7 +10,6 @@ public interface LayerParameters> { int MAX_ZOOM = 20; int CACHE_MAX_SECONDS = -1; double EXPANSION_FACTOR = 0.25d; - /** * User-visible name of the layer */ diff --git a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java index 5c018412572..7d3f6181d44 100644 --- a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java +++ b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java @@ -1,6 +1,8 @@ package org.opentripplanner.model; import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.SetMultimap; import java.time.LocalDate; import java.util.Collection; @@ -9,6 +11,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; @@ -80,8 +83,7 @@ public class TimetableSnapshot { * include ones from the scheduled GTFS, as well as ones added by realtime messages and * tracked by the TripPatternCache.

* Note that the keys do not include all scheduled TripPatterns, only those for which we have at - * least one update. The type of the field is specifically HashMap (rather than the more general - * Map interface) because we need to efficiently clone it.

+ * least one update.

* The members of the SortedSet (the Timetable for a particular day) are treated as copy-on-write * when we're updating them. If an update will modify the timetable for a particular day, that * timetable is replicated before any modifications are applied to avoid affecting any previous @@ -91,16 +93,15 @@ public class TimetableSnapshot { * The compound key approach better reflects the fact that there should be only one Timetable per * TripPattern and date. */ - private HashMap> timetables = new HashMap(); + private final Map> timetables; /** * For cases where the trip pattern (sequence of stops visited) has been changed by a realtime * update, a Map associating the updated trip pattern with a compound key of the feed-scoped - * trip ID and the service date. The type of this field is HashMap rather than the more general - * Map interface because we need to efficiently clone it whenever we start building up a new - * snapshot. TODO RT_AB: clarify if this is an index or the original source of truth. + * trip ID and the service date. + * TODO RT_AB: clarify if this is an index or the original source of truth. */ - private HashMap realtimeAddedTripPattern = new HashMap<>(); + private final Map realtimeAddedTripPattern; /** * This is an index of TripPatterns, not the primary collection. It tracks which TripPatterns @@ -110,13 +111,13 @@ public class TimetableSnapshot { * more than once. * TODO RT_AB: More general handling of all realtime indexes outside primary data structures. */ - private SetMultimap patternsForStop = HashMultimap.create(); + private final SetMultimap patternsForStop; /** * Boolean value indicating that timetable snapshot is read only if true. Once it is true, it * shouldn't be possible to change it to false anymore. */ - private boolean readOnly = false; + private final boolean readOnly; /** * Boolean value indicating that this timetable snapshot contains changes compared to the state of @@ -124,6 +125,22 @@ public class TimetableSnapshot { */ private boolean dirty = false; + public TimetableSnapshot() { + this(new HashMap<>(), new HashMap<>(), HashMultimap.create(), false); + } + + private TimetableSnapshot( + Map> timetables, + Map realtimeAddedTripPattern, + SetMultimap patternsForStop, + boolean readOnly + ) { + this.timetables = timetables; + this.realtimeAddedTripPattern = realtimeAddedTripPattern; + this.patternsForStop = patternsForStop; + this.readOnly = readOnly; + } + /** * Returns an updated timetable for the specified pattern if one is available in this snapshot, or * the originally scheduled timetable if there are no updates in this snapshot. @@ -186,25 +203,7 @@ public Result update( Timetable tt = resolve(pattern, serviceDate); // we need to perform the copy of Timetable here rather than in Timetable.update() // to avoid repeatedly copying in case several updates are applied to the same timetable - if (!dirtyTimetables.contains(tt)) { - Timetable old = tt; - tt = new Timetable(tt, serviceDate); - SortedSet sortedTimetables = timetables.get(pattern); - if (sortedTimetables == null) { - sortedTimetables = new TreeSet<>(new SortedTimetableComparator()); - } else { - SortedSet temp = new TreeSet<>(new SortedTimetableComparator()); - temp.addAll(sortedTimetables); - sortedTimetables = temp; - } - if (old.getServiceDate() != null) { - sortedTimetables.remove(old); - } - sortedTimetables.add(tt); - timetables.put(pattern, sortedTimetables); - dirtyTimetables.add(tt); - dirty = true; - } + tt = copyTimetable(pattern, serviceDate, tt); // Assume all trips in a pattern are from the same feed, which should be the case. // Find trip index @@ -252,13 +251,15 @@ public TimetableSnapshot commit(TransitLayerUpdater transitLayerUpdater, boolean throw new ConcurrentModificationException("This TimetableSnapshot is read-only."); } - TimetableSnapshot ret = new TimetableSnapshot(); if (!force && !this.isDirty()) { return null; } - ret.timetables = (HashMap>) this.timetables.clone(); - ret.realtimeAddedTripPattern = - (HashMap) this.realtimeAddedTripPattern.clone(); + TimetableSnapshot ret = new TimetableSnapshot( + Map.copyOf(timetables), + Map.copyOf(realtimeAddedTripPattern), + ImmutableSetMultimap.copyOf(patternsForStop), + true + ); if (transitLayerUpdater != null) { transitLayerUpdater.update(dirtyTimetables, timetables); @@ -267,9 +268,6 @@ public TimetableSnapshot commit(TransitLayerUpdater transitLayerUpdater, boolean this.dirtyTimetables.clear(); this.dirty = false; - ret.setPatternsForStop(HashMultimap.create(this.patternsForStop)); - - ret.readOnly = true; // mark the snapshot as henceforth immutable return ret; } @@ -304,6 +302,10 @@ public void clear(String feedId) { * message and an attempt was made to re-associate it with its originally scheduled trip pattern. */ public boolean revertTripToScheduledTripPattern(FeedScopedId tripId, LocalDate serviceDate) { + if (readOnly) { + throw new ConcurrentModificationException("This TimetableSnapshot is read-only."); + } + boolean success = false; final TripPattern pattern = getRealtimeAddedTripPattern(tripId, serviceDate); @@ -330,10 +332,10 @@ public boolean revertTripToScheduledTripPattern(FeedScopedId tripId, LocalDate s } if (tripTimesToRemove != null) { - for (Timetable sortedTimetable : sortedTimetables) { - boolean isDirty = sortedTimetable.getTripTimes().remove(tripTimesToRemove); - if (isDirty) { - dirtyTimetables.add(sortedTimetable); + for (Timetable originalTimetable : sortedTimetables) { + if (originalTimetable.getTripTimes().contains(tripTimesToRemove)) { + Timetable updatedTimetable = copyTimetable(pattern, serviceDate, originalTimetable); + updatedTimetable.getTripTimes().remove(tripTimesToRemove); } } } @@ -370,7 +372,7 @@ public boolean purgeExpiredData(LocalDate serviceDate) { if (toKeepTimetables.isEmpty()) { it.remove(); } else { - timetables.put(pattern, toKeepTimetables); + timetables.put(pattern, ImmutableSortedSet.copyOfSorted(toKeepTimetables)); } } @@ -407,10 +409,6 @@ public Collection getPatternsForStop(StopLocation stop) { return patternsForStop.get(stop); } - public void setPatternsForStop(SetMultimap patternsForStop) { - this.patternsForStop = patternsForStop; - } - /** * Does this snapshot contain any realtime data or is it completely empty? */ @@ -424,7 +422,7 @@ public boolean isEmpty() { * @param feedId feed id to clear out * @return true if the timetable changed as a result of the call */ - protected boolean clearTimetable(String feedId) { + private boolean clearTimetable(String feedId) { return timetables.keySet().removeIf(tripPattern -> feedId.equals(tripPattern.getFeedId())); } @@ -434,7 +432,7 @@ protected boolean clearTimetable(String feedId) { * @param feedId feed id to clear out * @return true if the realtimeAddedTripPattern changed as a result of the call */ - protected boolean clearRealtimeAddedTripPattern(String feedId) { + private boolean clearRealtimeAddedTripPattern(String feedId) { return realtimeAddedTripPattern .keySet() .removeIf(realtimeAddedTripPattern -> @@ -455,6 +453,39 @@ private void addPatternToIndex(TripPattern tripPattern) { } } + /** + * Make a copy of the given timetable for a given pattern and service date. + * If the timetable was already copied-on write in this snapshot, the same instance will be + * returned. The SortedSet that holds the collection of Timetables for that pattern + * (sorted by service date) is shared between multiple snapshots and must be copied as well.
+ * Note on performance: if multiple Timetables are modified in a SortedSet, the SortedSet will be + * copied multiple times. The impact on memory/garbage collection is assumed to be minimal + * since the collection is small. + * The SortedSet is made immutable to prevent change after snapshot publication. + */ + private Timetable copyTimetable(TripPattern pattern, LocalDate serviceDate, Timetable tt) { + if (!dirtyTimetables.contains(tt)) { + Timetable old = tt; + tt = new Timetable(tt, serviceDate); + SortedSet sortedTimetables = timetables.get(pattern); + if (sortedTimetables == null) { + sortedTimetables = new TreeSet<>(new SortedTimetableComparator()); + } else { + SortedSet temp = new TreeSet<>(new SortedTimetableComparator()); + temp.addAll(sortedTimetables); + sortedTimetables = temp; + } + if (old.getServiceDate() != null) { + sortedTimetables.remove(old); + } + sortedTimetables.add(tt); + timetables.put(pattern, ImmutableSortedSet.copyOfSorted(sortedTimetables)); + dirtyTimetables.add(tt); + dirty = true; + } + return tt; + } + protected static class SortedTimetableComparator implements Comparator { @Override diff --git a/src/main/java/org/opentripplanner/model/plan/Itinerary.java b/src/main/java/org/opentripplanner/model/plan/Itinerary.java index cb14227e83d..dee80addd91 100644 --- a/src/main/java/org/opentripplanner/model/plan/Itinerary.java +++ b/src/main/java/org/opentripplanner/model/plan/Itinerary.java @@ -177,11 +177,6 @@ public Leg lastLeg() { return getLegs().get(getLegs().size() - 1); } - /** Get the first transit leg if one exist */ - public Optional firstTransitLeg() { - return getLegs().stream().filter(TransitLeg.class::isInstance).findFirst(); - } - /** * An itinerary can be flagged for removal with a system notice. *

@@ -225,105 +220,6 @@ public Itinerary withTimeShiftToStartAt(ZonedDateTime afterTime) { return newItin; } - /** @see #equals(Object) */ - @Override - public final int hashCode() { - return super.hashCode(); - } - - /** - * Return {@code true} it the other object is the same object using the {@link - * Object#equals(Object)}. An itinerary is a temporary object and the equals method should not be - * used for comparision of 2 instances, only to check that to objects are the same instance. - */ - @Override - public final boolean equals(Object o) { - return super.equals(o); - } - - /** - * Used to convert a list of itineraries to a SHORT human-readable string. - * - * @see #toStr() - *

- * It is great for comparing lists of itineraries in a test: {@code - * assertEquals(toStr(List.of(it1)), toStr(result))}. - */ - public static String toStr(List list) { - return list.stream().map(Itinerary::toStr).collect(Collectors.joining(", ")); - } - - @Override - public String toString() { - return ToStringBuilder - .of(Itinerary.class) - .addStr("from", firstLeg().getFrom().toStringShort()) - .addStr("to", lastLeg().getTo().toStringShort()) - .addTime("start", firstLeg().getStartTime()) - .addTime("end", lastLeg().getEndTime()) - .addNum("nTransfers", numberOfTransfers) - .addDuration("duration", duration) - .addDuration("nonTransitTime", nonTransitDuration) - .addDuration("transitTime", transitDuration) - .addDuration("waitingTime", waitingDuration) - .addNum("generalizedCost", generalizedCost, UNKNOWN) - .addNum("generalizedCost2", generalizedCost2) - .addNum("waitTimeOptimizedCost", waitTimeOptimizedCost, UNKNOWN) - .addNum("transferPriorityCost", transferPriorityCost, UNKNOWN) - .addNum("nonTransitDistance", nonTransitDistanceMeters, "m") - .addBool("tooSloped", tooSloped) - .addNum("elevationLost", elevationLost, 0.0) - .addNum("elevationGained", elevationGained, 0.0) - .addCol("legs", legs) - .addObj("fare", fare) - .addObj("emissionsPerPerson", emissionsPerPerson) - .toString(); - } - - /** - * Used to convert an itinerary to a SHORT human readable string - including just a few of the - * most important fields. It is much shorter and easier to read then the {@link - * Itinerary#toString()}. - *

- * It is great for comparing to itineraries in a test: {@code assertEquals(toStr(it1), - * toStr(it2))}. - *

- * Example: {@code A ~ Walk 2m ~ B ~ BUS 55 12:04 12:14 ~ C [cost: 1066]} - *

- * Reads: Start at A, walk 2 minutes to stop B, take bus 55, board at 12:04 and alight at 12:14 - * ... - */ - public String toStr() { - // No translater needed, stop indexes are never passed to the builder - PathStringBuilder buf = new PathStringBuilder(null); - buf.stop(firstLeg().getFrom().name.toString()); - - for (Leg leg : legs) { - if (leg.isWalkingLeg()) { - buf.walk((int) leg.getDuration().toSeconds()); - } else if (leg instanceof TransitLeg transitLeg) { - buf.transit( - transitLeg.getMode().name(), - transitLeg.getTrip().logName(), - transitLeg.getStartTime(), - transitLeg.getEndTime() - ); - } else if (leg instanceof StreetLeg streetLeg) { - buf.street(streetLeg.getMode().name(), leg.getStartTime(), leg.getEndTime()); - } - buf.stop(leg.getTo().name.toString()); - } - - // The generalizedCost2 is printed as is, it is a special cost and the scale depends on the - // use-case. - buf.summary( - RaptorCostConverter.toRaptorCost(generalizedCost), - getGeneralizedCost2().orElse(RaptorConstants.NOT_SET) - ); - - return buf.toString(); - } - /** Total duration of the itinerary in seconds */ public Duration getDuration() { return duration; @@ -698,6 +594,105 @@ public Duration walkDuration() { return walkDuration; } + /** @see #equals(Object) */ + @Override + public final int hashCode() { + return super.hashCode(); + } + + /** + * Return {@code true} it the other object is the same object using the {@link + * Object#equals(Object)}. An itinerary is a temporary object and the equals method should not be + * used for comparision of 2 instances, only to check that to objects are the same instance. + */ + @Override + public final boolean equals(Object o) { + return super.equals(o); + } + + @Override + public String toString() { + return ToStringBuilder + .of(Itinerary.class) + .addStr("from", firstLeg().getFrom().toStringShort()) + .addStr("to", lastLeg().getTo().toStringShort()) + .addTime("start", firstLeg().getStartTime()) + .addTime("end", lastLeg().getEndTime()) + .addNum("nTransfers", numberOfTransfers) + .addDuration("duration", duration) + .addDuration("nonTransitTime", nonTransitDuration) + .addDuration("transitTime", transitDuration) + .addDuration("waitingTime", waitingDuration) + .addNum("generalizedCost", generalizedCost, UNKNOWN) + .addNum("generalizedCost2", generalizedCost2) + .addNum("waitTimeOptimizedCost", waitTimeOptimizedCost, UNKNOWN) + .addNum("transferPriorityCost", transferPriorityCost, UNKNOWN) + .addNum("nonTransitDistance", nonTransitDistanceMeters, "m") + .addBool("tooSloped", tooSloped) + .addNum("elevationLost", elevationLost, 0.0) + .addNum("elevationGained", elevationGained, 0.0) + .addCol("legs", legs) + .addObj("fare", fare) + .addObj("emissionsPerPerson", emissionsPerPerson) + .toString(); + } + + /** + * Used to convert a list of itineraries to a SHORT human-readable string. + * + * @see #toStr() + *

+ * It is great for comparing lists of itineraries in a test: {@code + * assertEquals(toStr(List.of(it1)), toStr(result))}. + */ + public static String toStr(List list) { + return list.stream().map(Itinerary::toStr).collect(Collectors.joining(", ")); + } + + /** + * Used to convert an itinerary to a SHORT human readable string - including just a few of the + * most important fields. It is much shorter and easier to read then the {@link + * Itinerary#toString()}. + *

+ * It is great for comparing to itineraries in a test: {@code assertEquals(toStr(it1), + * toStr(it2))}. + *

+ * Example: {@code A ~ Walk 2m ~ B ~ BUS 55 12:04 12:14 ~ C [cost: 1066]} + *

+ * Reads: Start at A, walk 2 minutes to stop B, take bus 55, board at 12:04 and alight at 12:14 + * ... + */ + public String toStr() { + // No translater needed, stop indexes are never passed to the builder + PathStringBuilder buf = new PathStringBuilder(null); + buf.stop(firstLeg().getFrom().name.toString()); + + for (Leg leg : legs) { + if (leg.isWalkingLeg()) { + buf.walk((int) leg.getDuration().toSeconds()); + } else if (leg instanceof TransitLeg transitLeg) { + buf.transit( + transitLeg.getMode().name(), + transitLeg.getTrip().logName(), + transitLeg.getStartTime(), + transitLeg.getEndTime() + ); + } else if (leg instanceof StreetLeg streetLeg) { + buf.street(streetLeg.getMode().name(), leg.getStartTime(), leg.getEndTime()); + } + buf.stop(leg.getTo().name.toString()); + } + + // The generalizedCost2 is printed as is, it is a special cost and the scale depends on the + // use-case. + buf.summary( + RaptorCostConverter.toRaptorCost(generalizedCost), + getGeneralizedCost2().orElse(RaptorConstants.NOT_SET) + ); + + return buf.toString(); + } + private static int penaltyCost(TimeAndCost penalty) { return penalty.cost().toSeconds(); } diff --git a/src/main/java/org/opentripplanner/model/plan/Leg.java b/src/main/java/org/opentripplanner/model/plan/Leg.java index 1ee72761d66..2a0b6726560 100644 --- a/src/main/java/org/opentripplanner/model/plan/Leg.java +++ b/src/main/java/org/opentripplanner/model/plan/Leg.java @@ -184,6 +184,7 @@ default Route getRoute() { /** * For transit legs, the trip. For non-transit legs, null. */ + @Nullable default Trip getTrip() { return null; } diff --git a/src/main/java/org/opentripplanner/model/plan/grouppriority/TransitGroupPriorityItineraryDecorator.java b/src/main/java/org/opentripplanner/model/plan/grouppriority/TransitGroupPriorityItineraryDecorator.java new file mode 100644 index 00000000000..80b7b39bd0c --- /dev/null +++ b/src/main/java/org/opentripplanner/model/plan/grouppriority/TransitGroupPriorityItineraryDecorator.java @@ -0,0 +1,49 @@ +package org.opentripplanner.model.plan.grouppriority; + +import java.util.Collection; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.model.plan.Leg; +import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; +import org.opentripplanner.transit.model.network.grouppriority.DefaultTransitGroupPriorityCalculator; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; + +/** + * This class will set the {@link Itinerary#getGeneralizedCost2()} value if the feature is + * enabled and no such value is set. The AStar router does not produce itineraries with this, + * so we decorate itineraries with this here to make sure the `c2` is set correct and can be + * used in the itinerary-filter-chain. + */ +public class TransitGroupPriorityItineraryDecorator { + + private final TransitGroupPriorityService priorityGroupConfigurator; + private final RaptorTransitGroupPriorityCalculator transitGroupCalculator; + + public TransitGroupPriorityItineraryDecorator( + TransitGroupPriorityService priorityGroupConfigurator + ) { + this.priorityGroupConfigurator = priorityGroupConfigurator; + this.transitGroupCalculator = new DefaultTransitGroupPriorityCalculator(); + } + + public void decorate(Collection itineraries) { + if (!priorityGroupConfigurator.isEnabled()) { + return; + } + for (Itinerary it : itineraries) { + decorate(it); + } + } + + public void decorate(Itinerary itinerary) { + if (itinerary.getGeneralizedCost2().isEmpty() && priorityGroupConfigurator.isEnabled()) { + int c2 = priorityGroupConfigurator.baseGroupId(); + for (Leg leg : itinerary.getLegs()) { + if (leg.getTrip() != null) { + int newGroupId = priorityGroupConfigurator.lookupTransitGroupPriorityId(leg.getTrip()); + c2 = transitGroupCalculator.mergeGroupIds(c2, newGroupId); + } + } + itinerary.setGeneralizedCost2(c2); + } + } +} diff --git a/src/main/java/org/opentripplanner/netex/mapping/MultiModalStationMapper.java b/src/main/java/org/opentripplanner/netex/mapping/MultiModalStationMapper.java index ef4b0a25cfd..5a4fb23bbb9 100644 --- a/src/main/java/org/opentripplanner/netex/mapping/MultiModalStationMapper.java +++ b/src/main/java/org/opentripplanner/netex/mapping/MultiModalStationMapper.java @@ -1,6 +1,7 @@ package org.opentripplanner.netex.mapping; import java.util.Collection; +import javax.annotation.Nullable; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; @@ -21,6 +22,7 @@ public MultiModalStationMapper(DataImportIssueStore issueStore, FeedScopedIdFact this.idFactory = idFactory; } + @Nullable MultiModalStation map(StopPlace stopPlace, Collection childStations) { MultiModalStationBuilder multiModalStation = MultiModalStation .of(idFactory.createId(stopPlace.getId())) @@ -34,13 +36,13 @@ MultiModalStation map(StopPlace stopPlace, Collection childStations) { if (coordinate == null) { issueStore.add( "MultiModalStationWithoutCoordinates", - "MultiModal station {} does not contain any coordinates.", + "MultiModal station %s does not contain any coordinates.", multiModalStation.getId() ); + return null; } else { multiModalStation.withCoordinate(coordinate); + return multiModalStation.build(); } - - return multiModalStation.build(); } } diff --git a/src/main/java/org/opentripplanner/netex/mapping/NetexMapper.java b/src/main/java/org/opentripplanner/netex/mapping/NetexMapper.java index c3c9ad2d0ae..991c0477266 100644 --- a/src/main/java/org/opentripplanner/netex/mapping/NetexMapper.java +++ b/src/main/java/org/opentripplanner/netex/mapping/NetexMapper.java @@ -332,8 +332,9 @@ private void mapMultiModalStopPlaces() { .getStationsByMultiModalStationRfs() .get(multiModalStopPlace.getId()); var multiModalStation = mapper.map(multiModalStopPlace, stations); - - transitBuilder.stopModel().withMultiModalStation(multiModalStation); + if (multiModalStation != null) { + transitBuilder.stopModel().withMultiModalStation(multiModalStation); + } } } diff --git a/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java b/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java index a1897d87124..d181cde4564 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java +++ b/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java @@ -38,7 +38,8 @@ public boolean hasCrossingTrafficLight() { return hasTag("crossing") && "traffic_signals".equals(getTag("crossing")); } - /* Checks if this node is a barrier which prevents motor vehicle traffic + /** + * Checks if this node is a barrier which prevents motor vehicle traffic. * * @return true if it is */ diff --git a/src/main/java/org/opentripplanner/openstreetmap/tagmapping/DefaultMapper.java b/src/main/java/org/opentripplanner/openstreetmap/tagmapping/DefaultMapper.java index 9989c102030..c5d1c0d1582 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/tagmapping/DefaultMapper.java +++ b/src/main/java/org/opentripplanner/openstreetmap/tagmapping/DefaultMapper.java @@ -602,25 +602,6 @@ public void populateProperties(WayPropertySet props) { /* Portland-local mixins */ - /* - * the RLIS/CCGIS:bicycle=designated mixins are coded out as they are no longer neccessary because of of the bicycle=designated block of code - * above. This switch makes our weighting system less reliant on tags that aren't generally used by the OSM community, and prevents the double - * counting that was occuring on streets with both bicycle infrastructure and an RLIS:bicycle=designated tag - */ - - /* - * props.setProperties("RLIS:bicycle=designated", StreetTraversalPermission.ALL, 0.97, 0.97, true); - */ - props.setMixinProperties("RLIS:bicycle=caution_area", ofBicycleSafety(1.45)); - props.setMixinProperties("RLIS:bicycle:right=caution_area", ofBicycleSafety(1.45, 1)); - props.setMixinProperties("RLIS:bicycle:left=caution_area", ofBicycleSafety(1, 1.45)); - /* - * props.setProperties("CCGIS:bicycle=designated", StreetTraversalPermission.ALL, 0.97, 0.97, true); - */ - props.setMixinProperties("CCGIS:bicycle=caution_area", ofBicycleSafety(1.45)); - props.setMixinProperties("CCGIS:bicycle:right=caution_area", ofBicycleSafety(1.45, 1)); - props.setMixinProperties("CCGIS:bicycle:left=caution_area", ofBicycleSafety(1, 1.45)); - props.setMixinProperties("foot=discouraged", ofWalkSafety(3)); props.setMixinProperties("bicycle=discouraged", ofBicycleSafety(3)); diff --git a/src/main/java/org/opentripplanner/openstreetmap/tagmapping/NorwayMapper.java b/src/main/java/org/opentripplanner/openstreetmap/tagmapping/NorwayMapper.java index 9de824a145b..efe7850f08b 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/tagmapping/NorwayMapper.java +++ b/src/main/java/org/opentripplanner/openstreetmap/tagmapping/NorwayMapper.java @@ -30,7 +30,7 @@ class NorwayMapper implements OsmTagMapper { @Override public void populateProperties(WayPropertySet props) { - var hasSidewalk = new Condition.EqualsAnyIn("sidewalk", "yes", "left", "right", "both"); + var hasSidewalk = new Condition.OneOf("sidewalk", "yes", "left", "right", "both"); var hasPrefixSidewalk = new Condition.Equals("sidewalk", "yes"); // e.g sidewalk:left=yes props.setDefaultWalkSafetyForPermission((permission, speedLimit, way) -> switch (permission) { @@ -74,16 +74,16 @@ else if (speedLimit >= 11.1f) { var cycleSafetyLowTraffic = 1.83; var cycleSafetyVeryLowTraffic = 1.57; - var isTrunkOrPrimary = new Condition.EqualsAnyIn( + var isTrunkOrPrimary = new Condition.OneOf( "highway", "trunk", "trunk_link", "primary", "primary_link" ); - var isSecondaryHighway = new Condition.EqualsAnyIn("highway", "secondary", "secondary_link"); - var isTertiaryHighway = new Condition.EqualsAnyIn("highway", "tertiary", "tertiary_link"); - var isClassifiedRoad = new Condition.EqualsAnyIn( + var isSecondaryHighway = new Condition.OneOf("highway", "secondary", "secondary_link"); + var isTertiaryHighway = new Condition.OneOf("highway", "tertiary", "tertiary_link"); + var isClassifiedRoad = new Condition.OneOf( "highway", "trunk", "trunk_link", @@ -94,7 +94,7 @@ else if (speedLimit >= 11.1f) { "tertiary", "tertiary_link" ); - var isClassifiedOrUnclassifiedRoad = new Condition.EqualsAnyIn( + var isClassifiedOrUnclassifiedRoad = new Condition.OneOf( "highway", "trunk", "trunk_link", @@ -107,7 +107,7 @@ else if (speedLimit >= 11.1f) { "unclassified" ); - var isNormalRoad = new Condition.EqualsAnyIn( + var isNormalRoad = new Condition.OneOf( "highway", "trunk", "trunk_link", @@ -166,7 +166,7 @@ else if (speedLimit >= 11.1f) { ); props.setProperties( - new ExactMatchSpecifier(new Condition.EqualsAnyIn("highway", "motorway", "motorway_link")), + new ExactMatchSpecifier(new Condition.OneOf("highway", "motorway", "motorway_link")), withModes(CAR) ); @@ -205,7 +205,7 @@ else if (speedLimit >= 11.1f) { props.setProperties( new ExactMatchSpecifier( new Condition.Equals("cycleway", "lane"), - new Condition.EqualsAnyIn("highway", "unclassified", "residential") + new Condition.OneOf("highway", "unclassified", "residential") ), cycleLaneInLowTraffic ); @@ -224,7 +224,7 @@ else if (speedLimit >= 11.1f) { props.setMixinProperties( new ExactMatchSpecifier( new Condition.Equals("oneway", "yes"), - new Condition.EqualsAnyInOrAbsent("cycleway"), + new Condition.OneOfOrAbsent("cycleway"), isNormalRoad ), ofBicycleSafety(1, 1.15) @@ -233,7 +233,7 @@ else if (speedLimit >= 11.1f) { // Discourage cycling along tram tracks props.setMixinProperties( new ExactMatchSpecifier( - new Condition.EqualsAnyIn("embedded_rails", "tram", "light_rail", "disused") + new Condition.OneOf("embedded_rails", "tram", "light_rail", "disused") ), ofBicycleSafety(1.2) ); @@ -252,12 +252,12 @@ else if (speedLimit >= 11.1f) { new LogicalOrSpecifier( new ExactMatchSpecifier( new Condition.Equals("bridge", "yes"), - new Condition.EqualsAnyInOrAbsent("sidewalk", "no", "separate"), + new Condition.OneOfOrAbsent("sidewalk", "no", "separate"), isClassifiedOrUnclassifiedRoad ), new ExactMatchSpecifier( new Condition.Equals("verge", "no"), - new Condition.EqualsAnyInOrAbsent("sidewalk", "no", "separate"), + new Condition.OneOfOrAbsent("sidewalk", "no", "separate"), isClassifiedOrUnclassifiedRoad ) ), @@ -268,7 +268,7 @@ else if (speedLimit >= 11.1f) { props.setMixinProperties( new ExactMatchSpecifier( new Condition.Equals("junction", "roundabout"), - new Condition.EqualsAnyInOrAbsent("sidewalk", "no", "separate") + new Condition.OneOfOrAbsent("sidewalk", "no", "separate") ), ofWalkSafety(2.) ); @@ -297,7 +297,7 @@ else if (speedLimit >= 11.1f) { props.setProperties( new ExactMatchSpecifier( new Condition.Equals("highway", "service"), - new Condition.EqualsAnyIn("bus", "yes", "designated") + new Condition.OneOf("bus", "yes", "designated") ), withModes(PEDESTRIAN_AND_BICYCLE).bicycleSafety(cycleSafetyMediumLowTraffic).walkSafety(1.9) ); @@ -404,49 +404,49 @@ else if (speedLimit >= 11.1f) { props.setProperties( new ExactMatchSpecifier( - new Condition.EqualsAnyIn("trail_visibility", "bad", "low", "poor", "horrible", "no"), + new Condition.OneOf("trail_visibility", "bad", "low", "poor", "horrible", "no"), new Condition.Equals("highway", "path") ), withModes(NONE) ); props.setProperties( new ExactMatchSpecifier( - new Condition.EqualsAnyIn( + new Condition.OneOf( "sac_scale", "demanding_mountain_hiking", "alpine_hiking", "demanding_alpine_hiking", "difficult_alpine_hiking" ), - new Condition.EqualsAnyIn("highway", "path", "steps") + new Condition.OneOf("highway", "path", "steps") ), withModes(NONE) ); props.setProperties( new ExactMatchSpecifier( - new Condition.EqualsAnyIn("smoothness", "horrible", "very_horrible"), - new Condition.EqualsAnyIn("highway", "path", "bridleway", "track") + new Condition.OneOf("smoothness", "horrible", "very_horrible"), + new Condition.OneOf("highway", "path", "bridleway", "track") ), withModes(PEDESTRIAN).walkSafety(1.15) ); props.setProperties( new ExactMatchSpecifier( new Condition.Equals("smoothness", "impassable"), - new Condition.EqualsAnyIn("highway", "path", "bridleway", "track") + new Condition.OneOf("highway", "path", "bridleway", "track") ), withModes(NONE) ); props.setProperties( new ExactMatchSpecifier( new Condition.InclusiveRange("mtb:scale", 2, 1), - new Condition.EqualsAnyIn("highway", "path", "bridleway", "track") + new Condition.OneOf("highway", "path", "bridleway", "track") ), withModes(PEDESTRIAN).walkSafety(1.15) ); props.setProperties( new ExactMatchSpecifier( new Condition.GreaterThan("mtb:scale", 2), - new Condition.EqualsAnyIn("highway", "path", "bridleway", "track") + new Condition.OneOf("highway", "path", "bridleway", "track") ), withModes(NONE) ); @@ -461,7 +461,7 @@ else if (speedLimit >= 11.1f) { props.setMixinProperties("surface=metal_grid", ofBicycleSafety(1.2)); props.setMixinProperties("surface=metal", ofBicycleSafety(1.2)); // Paved but damaged - var isPaved = new Condition.EqualsAnyIn( + var isPaved = new Condition.OneOf( "surface", "asfalt", "concrete", @@ -502,46 +502,46 @@ else if (speedLimit >= 11.1f) { props.setMixinProperties( new ExactMatchSpecifier( new Condition.Absent("tracktype"), - new Condition.EqualsAnyInOrAbsent("surface", "unpaved"), - new Condition.EqualsAnyIn("highway", "track", "bridleway") + new Condition.OneOfOrAbsent("surface", "unpaved"), + new Condition.OneOf("highway", "track", "bridleway") ), ofBicycleSafety(1.8).walkSafety(1.6) ); props.setMixinProperties( new ExactMatchSpecifier( new Condition.Equals("tracktype", "grade2"), - new Condition.EqualsAnyInOrAbsent("surface", "unpaved"), - new Condition.EqualsAnyIn("highway", "track", "bridleway", "service", "unclassified") + new Condition.OneOfOrAbsent("surface", "unpaved"), + new Condition.OneOf("highway", "track", "bridleway", "service", "unclassified") ), ofBicycleSafety(1.4).walkSafety(1.4) ); props.setMixinProperties( new ExactMatchSpecifier( new Condition.Equals("tracktype", "grade3"), - new Condition.EqualsAnyInOrAbsent("surface", "unpaved"), - new Condition.EqualsAnyIn("highway", "track", "bridleway", "service", "unclassified") + new Condition.OneOfOrAbsent("surface", "unpaved"), + new Condition.OneOf("highway", "track", "bridleway", "service", "unclassified") ), ofBicycleSafety(1.8).walkSafety(1.6) ); props.setMixinProperties( new ExactMatchSpecifier( new Condition.Equals("tracktype", "grade4"), - new Condition.EqualsAnyInOrAbsent("surface", "unpaved"), - new Condition.EqualsAnyIn("highway", "track", "bridleway", "service", "unclassified") + new Condition.OneOfOrAbsent("surface", "unpaved"), + new Condition.OneOf("highway", "track", "bridleway", "service", "unclassified") ), ofBicycleSafety(2.3).walkSafety(1.8) ); props.setMixinProperties( new ExactMatchSpecifier( new Condition.Equals("tracktype", "grade5"), - new Condition.EqualsAnyInOrAbsent("surface", "unpaved"), - new Condition.EqualsAnyIn("highway", "track", "bridleway", "service", "unclassified") + new Condition.OneOfOrAbsent("surface", "unpaved"), + new Condition.OneOf("highway", "track", "bridleway", "service", "unclassified") ), ofBicycleSafety(2.3).walkSafety(2.4) ); props.setMixinProperties( new ExactMatchSpecifier( - new Condition.EqualsAnyInOrAbsent("surface"), + new Condition.OneOfOrAbsent("surface"), new Condition.Equals("highway", "path") ), ofBicycleSafety(2.3).walkSafety(2.4) @@ -560,7 +560,7 @@ else if (speedLimit >= 11.1f) { */ props.setCarSpeed( - new ExactMatchSpecifier(new Condition.EqualsAnyIn("highway", "motorway", "motorway_link")), + new ExactMatchSpecifier(new Condition.OneOf("highway", "motorway", "motorway_link")), 30.56f // 110 km/t ); @@ -570,7 +570,7 @@ else if (speedLimit >= 11.1f) { ); props.setCarSpeed( new ExactMatchSpecifier( - new Condition.EqualsAnyIn( + new Condition.OneOf( "highway", "trunk", "trunk_link", @@ -589,8 +589,8 @@ else if (speedLimit >= 11.1f) { ); props.setCarSpeed( new ExactMatchSpecifier( - new Condition.EqualsAnyIn("sidewalk", "yes", "both", "left", "right", "separate"), - new Condition.EqualsAnyIn( + new Condition.OneOf("sidewalk", "yes", "both", "left", "right", "separate"), + new Condition.OneOf( "highway", "trunk", "trunk_link", diff --git a/src/main/java/org/opentripplanner/openstreetmap/tagmapping/PortlandMapper.java b/src/main/java/org/opentripplanner/openstreetmap/tagmapping/PortlandMapper.java index a532d279ba4..1a09b3b6714 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/tagmapping/PortlandMapper.java +++ b/src/main/java/org/opentripplanner/openstreetmap/tagmapping/PortlandMapper.java @@ -1,5 +1,6 @@ package org.opentripplanner.openstreetmap.tagmapping; +import static org.opentripplanner.openstreetmap.wayproperty.MixinPropertiesBuilder.ofBicycleSafety; import static org.opentripplanner.openstreetmap.wayproperty.MixinPropertiesBuilder.ofWalkSafety; import static org.opentripplanner.openstreetmap.wayproperty.specifier.ExactMatchSpecifier.exact; @@ -41,6 +42,18 @@ public void populateProperties(WayPropertySet props) { props.setMixinProperties(exact("sidewalk=no;maxspeed=35 mph"), ofWalkSafety(2)); props.setMixinProperties(exact("sidewalk=no;maxspeed=30 mph"), ofWalkSafety(1.5)); + // rarely used tags that are specific to counties near Portland + // https://taginfo.openstreetmap.org/keys/RLIS:bicycle#overview + + props.setMixinProperties("RLIS:bicycle=caution_area", ofBicycleSafety(1.45)); + props.setMixinProperties("RLIS:bicycle:right=caution_area", ofBicycleSafety(1.45, 1)); + props.setMixinProperties("RLIS:bicycle:left=caution_area", ofBicycleSafety(1, 1.45)); + + // https://taginfo.openstreetmap.org/keys/CCGIS:bicycle#overview + props.setMixinProperties("CCGIS:bicycle=caution_area", ofBicycleSafety(1.45)); + props.setMixinProperties("CCGIS:bicycle:right=caution_area", ofBicycleSafety(1.45, 1)); + props.setMixinProperties("CCGIS:bicycle:left=caution_area", ofBicycleSafety(1, 1.45)); + // Max speed limit in Oregon is 70 mph ~= 113kmh ~= 31.3m/s props.maxPossibleCarSpeed = 31.4f; diff --git a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/SafetyFeatures.java b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/SafetyFeatures.java index fc166a69927..ab68fd978c2 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/SafetyFeatures.java +++ b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/SafetyFeatures.java @@ -5,4 +5,18 @@ */ public record SafetyFeatures(double forward, double back) { public static final SafetyFeatures DEFAULT = new SafetyFeatures(1, 1); + + /** + * Does this instance actually modify the safety values? + */ + public boolean modifies() { + return !(forward == 1 && back == 1); + } + + /** + * Does forward and back have the same value? + */ + public boolean isSymmetric() { + return forward == back; + } } diff --git a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/BestMatchSpecifier.java b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/BestMatchSpecifier.java index d9f194aa4e5..293bcf84644 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/BestMatchSpecifier.java +++ b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/BestMatchSpecifier.java @@ -1,5 +1,7 @@ package org.opentripplanner.openstreetmap.wayproperty.specifier; +import java.util.Arrays; +import java.util.stream.Collectors; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.openstreetmap.model.OSMWithTags; @@ -72,6 +74,11 @@ public int matchScore(OSMWithTags way) { return score; } + @Override + public String toDocString() { + return Arrays.stream(conditions).map(Object::toString).collect(Collectors.joining("; ")); + } + @Override public String toString() { return ToStringBuilder.of(this.getClass()).addObj("conditions", conditions).toString(); diff --git a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/Condition.java b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/Condition.java index 4d8381963b3..4ad180c16c3 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/Condition.java +++ b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/Condition.java @@ -107,13 +107,24 @@ enum MatchResult { NONE, } + /** + * Selects tags where a given key/value matches. + */ record Equals(String key, String value) implements Condition { @Override public boolean isExtendedKeyMatch(OSMWithTags way, String exKey) { return way.hasTag(exKey) && way.isTag(exKey, value); } + + @Override + public String toString() { + return "%s=%s".formatted(key, value); + } } + /** + * Selects tags with a given key. + */ record Present(String key) implements Condition { @Override public MatchResult matchType() { @@ -123,31 +134,63 @@ public MatchResult matchType() { public boolean isExtendedKeyMatch(OSMWithTags way, String exKey) { return way.hasTag(exKey); } + + @Override + public String toString() { + return "present(%s)".formatted(key); + } } + /** + * Selects tags where a given tag is absent. + */ record Absent(String key) implements Condition { @Override public boolean isExtendedKeyMatch(OSMWithTags way, String exKey) { return !way.hasTag(exKey); } + + @Override + public String toString() { + return "!%s".formatted(key); + } } + /** + * Selects tags where the integer value is greater than a given number. + */ record GreaterThan(String key, int value) implements Condition { @Override public boolean isExtendedKeyMatch(OSMWithTags way, String exKey) { var maybeInt = way.getTagAsInt(exKey, ignored -> {}); return maybeInt.isPresent() && maybeInt.getAsInt() > value; } + + @Override + public String toString() { + return "%s > %s".formatted(key, value); + } } + /** + * Selects tags where the integer value is less than a given number. + */ record LessThan(String key, int value) implements Condition { @Override public boolean isExtendedKeyMatch(OSMWithTags way, String exKey) { var maybeInt = way.getTagAsInt(exKey, ignored -> {}); return maybeInt.isPresent() && maybeInt.getAsInt() < value; } + + @Override + public String toString() { + return "%s < %s".formatted(key, value); + } } + /** + * Selects integer tag values and checks if they are in between a lower and an upper bound. + */ record InclusiveRange(String key, int upper, int lower) implements Condition { public InclusiveRange { if (upper < lower) { @@ -160,18 +203,36 @@ public boolean isExtendedKeyMatch(OSMWithTags way, String exKey) { var maybeInt = way.getTagAsInt(exKey, ignored -> {}); return maybeInt.isPresent() && maybeInt.getAsInt() >= lower && maybeInt.getAsInt() <= upper; } + + @Override + public String toString() { + return "%s > %s < %s".formatted(lower, key, upper); + } } - record EqualsAnyIn(String key, String... values) implements Condition { + /** + * Selects a tag which has one of a set of given values. + */ + record OneOf(String key, String... values) implements Condition { @Override public boolean isExtendedKeyMatch(OSMWithTags way, String exKey) { return Arrays.stream(values).anyMatch(value -> way.isTag(exKey, value)); } + + @Override + public String toString() { + return "%s one of [%s]".formatted(key, String.join(", ", values)); + } } - record EqualsAnyInOrAbsent(String key, String... values) implements Condition { + /** + * Selects a tag where one of the following conditions is true: + * - one of a set of given values matches + * - the tag is absent + */ + record OneOfOrAbsent(String key, String... values) implements Condition { /* A use case for this is to detect the absence of a sidewalk, cycle lane or verge*/ - public EqualsAnyInOrAbsent(String key) { + public OneOfOrAbsent(String key) { this(key, "no", "none"); } @@ -181,5 +242,10 @@ public boolean isExtendedKeyMatch(OSMWithTags way, String exKey) { !way.hasTag(exKey) || Arrays.stream(values).anyMatch(value -> way.isTag(exKey, value)) ); } + + @Override + public String toString() { + return "%s not one of [%s] or absent".formatted(key, String.join(", ", values)); + } } } diff --git a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/ExactMatchSpecifier.java b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/ExactMatchSpecifier.java index e6eb3f37940..48c3b5edb64 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/ExactMatchSpecifier.java +++ b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/ExactMatchSpecifier.java @@ -2,6 +2,7 @@ import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; import org.opentripplanner.openstreetmap.model.OSMWithTags; /** @@ -54,6 +55,11 @@ public int matchScore(OSMWithTags way) { } } + @Override + public String toDocString() { + return conditions.stream().map(Object::toString).collect(Collectors.joining("; ")); + } + public boolean allTagsMatch(OSMWithTags way) { return conditions.stream().allMatch(o -> o.isMatch(way)); } diff --git a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/LogicalOrSpecifier.java b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/LogicalOrSpecifier.java index 229b26fa25a..74db280115b 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/LogicalOrSpecifier.java +++ b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/LogicalOrSpecifier.java @@ -2,6 +2,7 @@ import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; import org.opentripplanner.openstreetmap.model.OSMWithTags; /** @@ -25,10 +26,6 @@ public LogicalOrSpecifier(ExactMatchSpecifier... specifiers) { this.subSpecs = Arrays.asList(specifiers); } - public LogicalOrSpecifier(Condition... conditions) { - this.subSpecs = Arrays.stream(conditions).map(ExactMatchSpecifier::new).toList(); - } - public LogicalOrSpecifier(String... specs) { this.subSpecs = Arrays.stream(specs).map(ExactMatchSpecifier::new).toList(); } @@ -47,4 +44,9 @@ public int matchScore(OSMWithTags way) { return 0; } } + + @Override + public String toDocString() { + return subSpecs.stream().map(ExactMatchSpecifier::toDocString).collect(Collectors.joining("|")); + } } diff --git a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/OsmSpecifier.java b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/OsmSpecifier.java index 1e6c53a25c9..71d629552ff 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/OsmSpecifier.java +++ b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/OsmSpecifier.java @@ -42,6 +42,12 @@ static Condition[] parseConditions(String spec, String separator) { */ int matchScore(OSMWithTags way); + /** + * Convert this specifier to a human-readable identifier that represents this in (generated) + * documentation. + */ + String toDocString(); + record Scores(int forward, int backward) { public static Scores of(int s) { return new Scores(s, s); diff --git a/src/main/java/org/opentripplanner/raptor/api/request/MultiCriteriaRequest.java b/src/main/java/org/opentripplanner/raptor/api/request/MultiCriteriaRequest.java index 368b4660922..683c9807af5 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/MultiCriteriaRequest.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/MultiCriteriaRequest.java @@ -18,7 +18,7 @@ public class MultiCriteriaRequest { private final RelaxFunction relaxC1; @Nullable - private final RaptorTransitGroupCalculator transitPriorityCalculator; + private final RaptorTransitGroupPriorityCalculator transitPriorityCalculator; private final List passThroughPoints; @@ -63,7 +63,7 @@ public RelaxFunction relaxC1() { return relaxC1; } - public Optional transitPriorityCalculator() { + public Optional transitPriorityCalculator() { return Optional.ofNullable(transitPriorityCalculator); } @@ -140,7 +140,7 @@ public static class Builder { private final MultiCriteriaRequest original; private RelaxFunction relaxC1; - private RaptorTransitGroupCalculator transitPriorityCalculator; + private RaptorTransitGroupPriorityCalculator transitPriorityCalculator; private List passThroughPoints; private Double relaxCostAtDestination; @@ -163,11 +163,11 @@ public Builder withRelaxC1(RelaxFunction relaxC1) { } @Nullable - public RaptorTransitGroupCalculator transitPriorityCalculator() { + public RaptorTransitGroupPriorityCalculator transitPriorityCalculator() { return transitPriorityCalculator; } - public Builder withTransitPriorityCalculator(RaptorTransitGroupCalculator value) { + public Builder withTransitPriorityCalculator(RaptorTransitGroupPriorityCalculator value) { transitPriorityCalculator = value; return this; } diff --git a/src/main/java/org/opentripplanner/raptor/api/request/RaptorTransitGroupCalculator.java b/src/main/java/org/opentripplanner/raptor/api/request/RaptorTransitGroupPriorityCalculator.java similarity index 93% rename from src/main/java/org/opentripplanner/raptor/api/request/RaptorTransitGroupCalculator.java rename to src/main/java/org/opentripplanner/raptor/api/request/RaptorTransitGroupPriorityCalculator.java index b5f0598415e..06c10b51daf 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/RaptorTransitGroupCalculator.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/RaptorTransitGroupPriorityCalculator.java @@ -2,7 +2,7 @@ import org.opentripplanner.raptor.api.model.DominanceFunction; -public interface RaptorTransitGroupCalculator { +public interface RaptorTransitGroupPriorityCalculator { /** * Merge in the transit group id with an existing set. Note! Both the set * and the group id type is {@code int}. diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/configure/McRangeRaptorConfig.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/configure/McRangeRaptorConfig.java index 8eef90950dd..3673e78ee47 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/configure/McRangeRaptorConfig.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/configure/McRangeRaptorConfig.java @@ -6,7 +6,7 @@ import org.opentripplanner.raptor.api.model.DominanceFunction; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.request.MultiCriteriaRequest; -import org.opentripplanner.raptor.api.request.RaptorTransitGroupCalculator; +import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; import org.opentripplanner.raptor.rangeraptor.context.SearchContext; import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; import org.opentripplanner.raptor.rangeraptor.internalapi.ParetoSetCost; @@ -201,7 +201,7 @@ private DominanceFunction dominanceFunctionC2() { return null; } - private RaptorTransitGroupCalculator getTransitGroupPriorityCalculator() { + private RaptorTransitGroupPriorityCalculator getTransitGroupPriorityCalculator() { return mcRequest().transitPriorityCalculator().orElseThrow(); } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/ride/c2/TransitGroupPriorityRideFactory.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/ride/c2/TransitGroupPriorityRideFactory.java index 5d65c40d021..79ae1558836 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/ride/c2/TransitGroupPriorityRideFactory.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/ride/c2/TransitGroupPriorityRideFactory.java @@ -2,7 +2,7 @@ import org.opentripplanner.raptor.api.model.RaptorTripPattern; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; -import org.opentripplanner.raptor.api.request.RaptorTransitGroupCalculator; +import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; import org.opentripplanner.raptor.rangeraptor.multicriteria.arrivals.McStopArrival; import org.opentripplanner.raptor.rangeraptor.multicriteria.ride.PatternRide; import org.opentripplanner.raptor.rangeraptor.multicriteria.ride.PatternRideFactory; @@ -15,10 +15,10 @@ public class TransitGroupPriorityRideFactory implements PatternRideFactory> { private int currentPatternGroupPriority; - private final RaptorTransitGroupCalculator transitGroupPriorityCalculator; + private final RaptorTransitGroupPriorityCalculator transitGroupPriorityCalculator; public TransitGroupPriorityRideFactory( - RaptorTransitGroupCalculator transitGroupPriorityCalculator + RaptorTransitGroupPriorityCalculator transitGroupPriorityCalculator ) { this.transitGroupPriorityCalculator = transitGroupPriorityCalculator; } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java b/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java index 06ceaeebeb2..7ac3dd1caf6 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java @@ -16,6 +16,7 @@ import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.framework.time.ServiceDateUtils; import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.model.plan.grouppriority.TransitGroupPriorityItineraryDecorator; import org.opentripplanner.model.plan.paging.cursor.PageCursorInput; import org.opentripplanner.raptor.api.request.RaptorTuningParameters; import org.opentripplanner.raptor.api.request.SearchParams; @@ -36,6 +37,7 @@ import org.opentripplanner.routing.framework.DebugTimingAggregator; import org.opentripplanner.service.paging.PagingService; import org.opentripplanner.standalone.api.OtpServerRequestContext; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,6 +66,7 @@ public class RoutingWorker { */ private final ZonedDateTime transitSearchTimeZero; private final AdditionalSearchDays additionalSearchDays; + private final TransitGroupPriorityService transitGroupPriorityService; private SearchParams raptorSearchParamsUsed = null; private PageCursorInput pageCursorInput = null; @@ -79,6 +82,12 @@ public RoutingWorker(OtpServerRequestContext serverContext, RouteRequest request this.transitSearchTimeZero = ServiceDateUtils.asStartOfService(request.dateTime(), zoneId); this.additionalSearchDays = createAdditionalSearchDays(serverContext.raptorTuningParameters(), zoneId, request); + this.transitGroupPriorityService = + TransitGroupPriorityService.of( + request.preferences().transit().relaxTransitGroupPriority(), + request.journey().transit().priorityGroupsByAgency(), + request.journey().transit().priorityGroupsGlobal() + ); } public RoutingResponse route() { @@ -122,6 +131,9 @@ public RoutingResponse route() { routeTransit(itineraries, routingErrors); } + // Set C2 value for Street and FLEX if transit-group-priority is used + new TransitGroupPriorityItineraryDecorator(transitGroupPriorityService).decorate(itineraries); + debugTimingAggregator.finishedRouting(); // Filter itineraries @@ -258,6 +270,7 @@ private Void routeTransit(List itineraries, Collection var transitResults = TransitRouter.route( request, serverContext, + transitGroupPriorityService, transitSearchTimeZero, additionalSearchDays, debugTimingAggregator 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 06f4ff1cf45..37bb7270902 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 @@ -41,6 +41,7 @@ import org.opentripplanner.routing.framework.DebugTimingAggregator; import org.opentripplanner.standalone.api.OtpServerRequestContext; import org.opentripplanner.street.search.TemporaryVerticesContainer; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; public class TransitRouter { @@ -48,6 +49,7 @@ public class TransitRouter { private final RouteRequest request; private final OtpServerRequestContext serverContext; + private final TransitGroupPriorityService transitGroupPriorityService; private final DebugTimingAggregator debugTimingAggregator; private final ZonedDateTime transitSearchTimeZero; private final AdditionalSearchDays additionalSearchDays; @@ -56,12 +58,14 @@ public class TransitRouter { private TransitRouter( RouteRequest request, OtpServerRequestContext serverContext, + TransitGroupPriorityService transitGroupPriorityService, ZonedDateTime transitSearchTimeZero, AdditionalSearchDays additionalSearchDays, DebugTimingAggregator debugTimingAggregator ) { this.request = request; this.serverContext = serverContext; + this.transitGroupPriorityService = transitGroupPriorityService; this.transitSearchTimeZero = transitSearchTimeZero; this.additionalSearchDays = additionalSearchDays; this.debugTimingAggregator = debugTimingAggregator; @@ -71,6 +75,7 @@ private TransitRouter( public static TransitRouterResult route( RouteRequest request, OtpServerRequestContext serverContext, + TransitGroupPriorityService priorityGroupConfigurator, ZonedDateTime transitSearchTimeZero, AdditionalSearchDays additionalSearchDays, DebugTimingAggregator debugTimingAggregator @@ -78,6 +83,7 @@ public static TransitRouterResult route( TransitRouter transitRouter = new TransitRouter( request, serverContext, + priorityGroupConfigurator, transitSearchTimeZero, additionalSearchDays, debugTimingAggregator @@ -309,6 +315,7 @@ private RaptorRoutingRequestTransitData createRequestTransitDataProvider( ) { return new RaptorRoutingRequestTransitData( transitLayer, + transitGroupPriorityService, transitSearchTimeZero, additionalSearchDays.additionalSearchDaysInPast(), additionalSearchDays.additionalSearchDaysInFuture(), diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java index 879536fdcd0..948b132e408 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java @@ -21,10 +21,10 @@ import org.opentripplanner.raptor.rangeraptor.SystemErrDebugLogger; import org.opentripplanner.routing.algorithm.raptoradapter.router.performance.PerformanceTimersForRaptor; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.grouppriority.TransitGroupPriority32n; import org.opentripplanner.routing.api.request.DebugEventType; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.framework.CostLinearFunction; +import org.opentripplanner.transit.model.network.grouppriority.DefaultTransitGroupPriorityCalculator; import org.opentripplanner.transit.model.site.StopLocation; public class RaptorRequestMapper { @@ -119,7 +119,7 @@ private RaptorRequest doMap() { mcBuilder.withPassThroughPoints(mapPassThroughPoints()); r.relaxGeneralizedCostAtDestination().ifPresent(mcBuilder::withRelaxCostAtDestination); } else if (!pt.relaxTransitGroupPriority().isNormal()) { - mcBuilder.withTransitPriorityCalculator(TransitGroupPriority32n.priorityCalculator()); + mcBuilder.withTransitPriorityCalculator(new DefaultTransitGroupPriorityCalculator()); mcBuilder.withRelaxC1(mapRelaxCost(pt.relaxTransitGroupPriority())); } }); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransfersMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransfersMapper.java index 74a5d7a6352..c7e9ea05320 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransfersMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransfersMapper.java @@ -6,7 +6,7 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.Transfer; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.service.StopModel; -import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.transit.service.TransitService; class TransfersMapper { @@ -14,7 +14,7 @@ class TransfersMapper { * Copy pre-calculated transfers from the original graph * @return a list where each element is a list of transfers for the corresponding stop index */ - static List> mapTransfers(StopModel stopModel, TransitModel transitModel) { + static List> mapTransfers(StopModel stopModel, TransitService transitService) { List> transferByStopIndex = new ArrayList<>(); for (int i = 0; i < stopModel.stopIndexSize(); ++i) { @@ -26,7 +26,7 @@ static List> mapTransfers(StopModel stopModel, TransitModel trans ArrayList list = new ArrayList<>(); - for (PathTransfer pathTransfer : transitModel.getTransfersByStop(stop)) { + for (PathTransfer pathTransfer : transitService.getTransfersByStop(stop)) { if (pathTransfer.to instanceof RegularStop) { int toStopIndex = pathTransfer.to.getIndex(); Transfer newTransfer; diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java index 33a076bb8d6..8ce328fe1b6 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java @@ -27,8 +27,10 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.StopTransferPriority; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.StopModel; import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.transit.service.TransitService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,10 +49,12 @@ public class TransitLayerMapper { private static final Logger LOG = LoggerFactory.getLogger(TransitLayerMapper.class); - private final TransitModel transitModel; + private final TransitService transitService; + private final StopModel stopModel; private TransitLayerMapper(TransitModel transitModel) { - this.transitModel = transitModel; + this.transitService = new DefaultTransitService(transitModel); + this.stopModel = transitModel.getStopModel(); } public static TransitLayer map( @@ -74,20 +78,19 @@ private TransitLayer map(TransitTuningParameters tuningParameters) { HashMap> tripPatternsByStopByDate; List> transferByStopIndex; ConstrainedTransfersForPatterns constrainedTransfers = null; - StopModel stopModel = transitModel.getStopModel(); LOG.info("Mapping transitLayer from TransitModel..."); - Collection allTripPatterns = transitModel.getAllTripPatterns(); + Collection allTripPatterns = transitService.getAllTripPatterns(); tripPatternsByStopByDate = mapTripPatterns(allTripPatterns); - transferByStopIndex = mapTransfers(stopModel, transitModel); + transferByStopIndex = mapTransfers(stopModel, transitService); TransferIndexGenerator transferIndexGenerator = null; if (OTPFeature.TransferConstraints.isOn()) { transferIndexGenerator = - new TransferIndexGenerator(transitModel.getTransferService().listAll(), allTripPatterns); + new TransferIndexGenerator(transitService.getTransferService().listAll(), allTripPatterns); constrainedTransfers = transferIndexGenerator.generateTransfers(); } @@ -98,9 +101,9 @@ private TransitLayer map(TransitTuningParameters tuningParameters) { return new TransitLayer( tripPatternsByStopByDate, transferByStopIndex, - transitModel.getTransferService(), + transitService.getTransferService(), stopModel, - transitModel.getTimeZone(), + transitService.getTimeZone(), transferCache, constrainedTransfers, transferIndexGenerator, @@ -118,13 +121,10 @@ private HashMap> mapTripPatterns( Collection allTripPatterns ) { TripPatternForDateMapper tripPatternForDateMapper = new TripPatternForDateMapper( - transitModel.getTransitModelIndex().getServiceCodesRunningForDate() + transitService.getServiceCodesRunningForDate() ); - Set allServiceDates = transitModel - .getTransitModelIndex() - .getServiceCodesRunningForDate() - .keySet(); + Set allServiceDates = transitService.getAllServiceCodes(); List tripPatternForDates = Collections.synchronizedList(new ArrayList<>()); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java index 5188bdef8b1..934bec39c11 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java @@ -2,7 +2,6 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.SetMultimap; -import gnu.trove.set.TIntSet; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; @@ -20,7 +19,7 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; -import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.transit.service.TransitEditorService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,9 +39,7 @@ public class TransitLayerUpdater { private static final Logger LOG = LoggerFactory.getLogger(TransitLayerUpdater.class); - private final TransitModel transitModel; - - private final Map serviceCodesRunningForDate; + private final TransitEditorService transitService; /** * Cache the TripPatternForDates indexed on the original TripPatterns in order to avoid this @@ -58,19 +55,15 @@ public class TransitLayerUpdater { private final Map> tripPatternsRunningOnDateMapCache = new HashMap<>(); - public TransitLayerUpdater( - TransitModel transitModel, - Map serviceCodesRunningForDate - ) { - this.transitModel = transitModel; - this.serviceCodesRunningForDate = serviceCodesRunningForDate; + public TransitLayerUpdater(TransitEditorService transitService) { + this.transitService = transitService; } public void update( Set updatedTimetables, Map> timetables ) { - if (!transitModel.hasRealtimeTransitLayer()) { + if (!transitService.hasRealtimeTransitLayer()) { return; } @@ -78,11 +71,11 @@ public void update( // Make a shallow copy of the realtime transit layer. Only the objects that are copied will be // changed during this update process. - TransitLayer realtimeTransitLayer = new TransitLayer(transitModel.getRealtimeTransitLayer()); + TransitLayer realtimeTransitLayer = new TransitLayer(transitService.getRealtimeTransitLayer()); // Instantiate a TripPatternForDateMapper with the new TripPattern mappings TripPatternForDateMapper tripPatternForDateMapper = new TripPatternForDateMapper( - serviceCodesRunningForDate + transitService.getServiceCodesRunningForDate() ); Set datesToBeUpdated = new HashSet<>(); @@ -229,7 +222,7 @@ public void update( // Switch out the reference with the updated realtimeTransitLayer. This is synchronized to // guarantee that the reference is set after all the fields have been updated. - transitModel.setRealtimeTransitLayer(realtimeTransitLayer); + transitService.setRealtimeTransitLayer(realtimeTransitLayer); LOG.debug( "UPDATING {} tripPatterns took {} ms", diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapper.java index 30551421138..cd00b9356dc 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapper.java @@ -41,7 +41,7 @@ public class TripPatternForDateMapper { * @param serviceCodesRunningForDate - READ ONLY */ TripPatternForDateMapper(Map serviceCodesRunningForDate) { - this.serviceCodesRunningForDate = Collections.unmodifiableMap(serviceCodesRunningForDate); + this.serviceCodesRunningForDate = serviceCodesRunningForDate; } /** diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupConfigurator.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupConfigurator.java deleted file mode 100644 index 6ef82786b99..00000000000 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupConfigurator.java +++ /dev/null @@ -1,144 +0,0 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit.request; - -import gnu.trove.impl.Constants; -import gnu.trove.map.TObjectIntMap; -import gnu.trove.map.hash.TObjectIntHashMap; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.stream.Stream; -import org.opentripplanner.framework.lang.ArrayUtils; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.grouppriority.TransitGroupPriority32n; -import org.opentripplanner.routing.api.request.request.filter.TransitGroupSelect; -import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.network.TripPattern; - -/** - * This class dynamically builds an index of transit-group-ids from the - * provided {@link TransitGroupSelect}s while serving the caller with - * group-ids for each requested pattern. It is made for optimal - * performance, since it is used in request scope. - *

- * THIS CLASS IS NOT THREAD-SAFE. - */ -public class PriorityGroupConfigurator { - - /** - * There are two ways we can treat the base (local-traffic) transit priority group: - *

    - *
  1. We can assign group id 1 (one) to the base group and it will be treated as any other group. - *
  2. We can assign group id 0 (zero) to the base and it will not be added to the set of groups - * a given path has. - *
- * When we compare paths we compare sets of group ids. A set is dominating another set if it is - * a smaller subset or different from the other set. - *

- * Example - base-group-id = 0 (zero) - *

- * Let B be the base and G be concrete group. Then: (B) dominates (G), (G) dominates (B), (B) - * dominates (BG), but (G) does not dominate (BG). In other words, paths with only agency - * X (group G) is not given an advantage in the routing over paths with a combination of agency - * X (group G) and local traffic (group B). - *

- * TODO: Experiment with base-group-id=0 and make it configurable. - */ - private static final int GROUP_INDEX_COUNTER_START = 1; - - private final int baseGroupId = TransitGroupPriority32n.groupId(GROUP_INDEX_COUNTER_START); - private int groupIndexCounter = GROUP_INDEX_COUNTER_START; - private final boolean enabled; - private final PriorityGroupMatcher[] agencyMatchers; - private final PriorityGroupMatcher[] globalMatchers; - - // Index matchers and ids - private final List agencyMatchersIds; - private final List globalMatchersIds; - - private PriorityGroupConfigurator() { - this.enabled = false; - this.agencyMatchers = null; - this.globalMatchers = null; - this.agencyMatchersIds = List.of(); - this.globalMatchersIds = List.of(); - } - - private PriorityGroupConfigurator( - Collection byAgency, - Collection global - ) { - this.agencyMatchers = PriorityGroupMatcher.of(byAgency); - this.globalMatchers = PriorityGroupMatcher.of(global); - this.enabled = Stream.of(agencyMatchers, globalMatchers).anyMatch(ArrayUtils::hasContent); - this.globalMatchersIds = - Arrays.stream(globalMatchers).map(m -> new MatcherAndId(m, nextGroupId())).toList(); - // We need to populate this dynamically - this.agencyMatchersIds = Arrays.stream(agencyMatchers).map(MatcherAgencyAndIds::new).toList(); - } - - public static PriorityGroupConfigurator empty() { - return new PriorityGroupConfigurator(); - } - - public static PriorityGroupConfigurator of( - Collection byAgency, - Collection global - ) { - if (Stream.of(byAgency, global).allMatch(Collection::isEmpty)) { - return empty(); - } - return new PriorityGroupConfigurator(byAgency, global); - } - - /** - * Fetch/lookup the transit-group-id for the given pattern. - *

- * @throws IllegalArgumentException if more than 32 group-ids are requested. - */ - public int lookupTransitGroupPriorityId(TripPattern tripPattern) { - if (!enabled || tripPattern == null) { - return baseGroupId; - } - - for (var it : agencyMatchersIds) { - if (it.matcher().match(tripPattern)) { - var agencyId = tripPattern.getRoute().getAgency().getId(); - int groupId = it.ids().get(agencyId); - - if (groupId < 0) { - groupId = nextGroupId(); - it.ids.put(agencyId, groupId); - } - return groupId; - } - } - - for (var it : globalMatchersIds) { - if (it.matcher.match(tripPattern)) { - return it.groupId(); - } - } - // Fallback to base-group-id - return baseGroupId; - } - - public int baseGroupId() { - return baseGroupId; - } - - private int nextGroupId() { - return TransitGroupPriority32n.groupId(++groupIndexCounter); - } - - /** Pair of matcher and groupId. Used only inside this class. */ - record MatcherAndId(PriorityGroupMatcher matcher, int groupId) {} - - /** Matcher with map of ids by agency. */ - record MatcherAgencyAndIds(PriorityGroupMatcher matcher, TObjectIntMap ids) { - MatcherAgencyAndIds(PriorityGroupMatcher matcher) { - this( - matcher, - new TObjectIntHashMap<>(Constants.DEFAULT_CAPACITY, Constants.DEFAULT_LOAD_FACTOR, -1) - ); - } - } -} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java index 19b5ccf8502..5b9d81fa6c3 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java @@ -30,10 +30,11 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.GeneralizedCostParametersMapper; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.transit.model.network.RoutingTripPattern; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; /** * This is the data provider for the Range Raptor search engine. It uses data from the TransitLayer, - * but filters it by dates and modes per request. Transfers durations are pre-calculated per request + * but filters it by dates and modes per request. Transfer durations are pre-calculated per request * based on walk speed. */ public class RaptorRoutingRequestTransitData implements RaptorTransitDataProvider { @@ -71,6 +72,7 @@ public class RaptorRoutingRequestTransitData implements RaptorTransitDataProvide public RaptorRoutingRequestTransitData( TransitLayer transitLayer, + TransitGroupPriorityService transitGroupPriorityService, ZonedDateTime transitSearchTimeZero, int additionalPastSearchDays, int additionalFutureSearchDays, @@ -82,7 +84,7 @@ public RaptorRoutingRequestTransitData( this.transitSearchTimeZero = transitSearchTimeZero; // Delegate to the creator to construct the needed data structures. The code is messy so - // it is nice to NOT have it in the class. It isolate this code to only be available at + // it is nice to NOT have it in the class. It isolates this code to only be available at // the time of construction var transitDataCreator = new RaptorRoutingRequestTransitDataCreator( transitLayer, @@ -92,7 +94,7 @@ public RaptorRoutingRequestTransitData( additionalPastSearchDays, additionalFutureSearchDays, filter, - createTransitGroupPriorityConfigurator(request) + transitGroupPriorityService ); this.patternIndex = transitDataCreator.createPatternIndex(tripPatterns); this.activeTripPatternsPerStop = transitDataCreator.createTripPatternsPerStop(tripPatterns); @@ -242,15 +244,4 @@ public RaptorConstrainedBoardingSearch transferConstraintsReverseS } return new ConstrainedBoardingSearch(false, toStopTransfers, fromStopTransfers); } - - private PriorityGroupConfigurator createTransitGroupPriorityConfigurator(RouteRequest request) { - if (request.preferences().transit().relaxTransitGroupPriority().isNormal()) { - return PriorityGroupConfigurator.empty(); - } - var transitRequest = request.journey().transit(); - return PriorityGroupConfigurator.of( - transitRequest.priorityGroupsByAgency(), - transitRequest.priorityGroupsGlobal() - ); - } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreator.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreator.java index f987e4f7a21..815bf839e31 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreator.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreator.java @@ -19,6 +19,7 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripPatternForDate; import org.opentripplanner.transit.model.network.RoutingTripPattern; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; import org.opentripplanner.transit.model.timetable.TripTimes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -93,7 +94,7 @@ static List merge( ZonedDateTime transitSearchTimeZero, List patternForDateList, TransitDataProviderFilter filter, - PriorityGroupConfigurator priorityGroupConfigurator + TransitGroupPriorityService transitGroupPriorityService ) { // Group TripPatternForDate objects by TripPattern. // This is done in a loop to increase performance. @@ -147,7 +148,7 @@ static List merge( tripPattern.getAlightingPossible(), BoardAlight.ALIGHT ), - priorityGroupConfigurator.lookupTransitGroupPriorityId(tripPattern.getPattern()) + transitGroupPriorityService.lookupTransitGroupPriorityId(tripPattern.getPattern()) ) ); } @@ -159,7 +160,7 @@ List createTripPatterns( int additionalPastSearchDays, int additionalFutureSearchDays, TransitDataProviderFilter filter, - PriorityGroupConfigurator priorityGroupConfigurator + TransitGroupPriorityService transitGroupPriorityService ) { List tripPatternForDates = getTripPatternsForDateRange( additionalPastSearchDays, @@ -167,7 +168,7 @@ List createTripPatterns( filter ); - return merge(transitSearchTimeZero, tripPatternForDates, filter, priorityGroupConfigurator); + return merge(transitSearchTimeZero, tripPatternForDates, filter, transitGroupPriorityService); } private static List filterActiveTripPatterns( diff --git a/src/main/java/org/opentripplanner/routing/api/request/framework/LinearFunctionSerialization.java b/src/main/java/org/opentripplanner/routing/api/request/framework/LinearFunctionSerialization.java index bccad0568c5..cb9dfb5c4cc 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/framework/LinearFunctionSerialization.java +++ b/src/main/java/org/opentripplanner/routing/api/request/framework/LinearFunctionSerialization.java @@ -1,6 +1,7 @@ package org.opentripplanner.routing.api.request.framework; import java.time.Duration; +import java.time.format.DateTimeParseException; import java.util.Locale; import java.util.Optional; import java.util.function.BiFunction; @@ -39,6 +40,8 @@ public class LinearFunctionSerialization { String.join(SEP, DUR, PLUS, NUM, VARIABLE) ); + private static final Pattern DECIMAL_NUMBER_PATTERN = Pattern.compile("\\d+(\\.\\d+)?"); + private LinearFunctionSerialization() {} /** @@ -61,11 +64,7 @@ public static Optional parse(String text, BiFunction var coefficient = Double.parseDouble(coefficientText); coefficient = Units.normalizedFactor(coefficient, 0.0, 100.0); - // Unfortunately, to be backwards compatible we need to support decimal numbers. - // If a decimal number, then the value is converted to seconds - var constant = constantText.matches("\\d+(\\.\\d+)?") - ? Duration.ofSeconds(IntUtils.round(Double.parseDouble(constantText))) - : DurationUtils.duration(constantText); + var constant = parseDecimalSecondsOrDuration(constantText); return Optional.of(factory.apply(constant, coefficient)); } @@ -85,4 +84,24 @@ public static String serialize(Duration constant, double coefficient) { Units.factorToString(coefficient) ); } + + /** + * Parse a String as a Duration. + * Unfortunately, to be backwards compatible we need to support decimal numbers. + * If the text represents a decimal number, then the value is converted to seconds. + *
+ * The parsing function {@link DurationUtils#parseSecondsOrDuration(String)} cannot be used + * here since it supports only integer seconds, not decimal seconds. + * + */ + private static Duration parseDecimalSecondsOrDuration(String text) { + try { + if (DECIMAL_NUMBER_PATTERN.matcher(text).matches()) { + return Duration.ofSeconds(IntUtils.round(Double.parseDouble(text))); + } + return DurationUtils.duration(text); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("Unable to parse duration: '" + text + "'"); + } + } } diff --git a/src/main/java/org/opentripplanner/routing/graphfinder/DirectGraphFinder.java b/src/main/java/org/opentripplanner/routing/graphfinder/DirectGraphFinder.java index 5ffa7cd2301..c36efc59e5b 100644 --- a/src/main/java/org/opentripplanner/routing/graphfinder/DirectGraphFinder.java +++ b/src/main/java/org/opentripplanner/routing/graphfinder/DirectGraphFinder.java @@ -63,6 +63,7 @@ public List findClosestPlaces( List filterByStations, List filterByRoutes, List filterByBikeRentalStations, + List filterByNetwork, TransitService transitService ) { throw new UnsupportedOperationException("Not implemented"); diff --git a/src/main/java/org/opentripplanner/routing/graphfinder/GraphFinder.java b/src/main/java/org/opentripplanner/routing/graphfinder/GraphFinder.java index 4c0b0c81144..063ed221dd5 100644 --- a/src/main/java/org/opentripplanner/routing/graphfinder/GraphFinder.java +++ b/src/main/java/org/opentripplanner/routing/graphfinder/GraphFinder.java @@ -69,6 +69,7 @@ List findClosestPlaces( List filterByStations, List filterByRoutes, List filterByBikeRentalStations, + List filterByNetwork, TransitService transitService ); } diff --git a/src/main/java/org/opentripplanner/routing/graphfinder/PlaceFinderTraverseVisitor.java b/src/main/java/org/opentripplanner/routing/graphfinder/PlaceFinderTraverseVisitor.java index e71504d58f3..16420a0d9eb 100644 --- a/src/main/java/org/opentripplanner/routing/graphfinder/PlaceFinderTraverseVisitor.java +++ b/src/main/java/org/opentripplanner/routing/graphfinder/PlaceFinderTraverseVisitor.java @@ -31,6 +31,7 @@ public class PlaceFinderTraverseVisitor implements TraverseVisitor private final Set filterByStops; private final Set filterByStations; private final Set filterByRoutes; + private final Set filterByNetwork; private final Set filterByVehicleRental; private final Set seenPatternAtStops = new HashSet<>(); private final Set seenStops = new HashSet<>(); @@ -69,6 +70,7 @@ public PlaceFinderTraverseVisitor( List filterByStations, List filterByRoutes, List filterByBikeRentalStations, + List filterByNetwork, int maxResults, double radiusMeters ) { @@ -82,6 +84,7 @@ public PlaceFinderTraverseVisitor( this.filterByStations = toSet(filterByStations); this.filterByRoutes = toSet(filterByRoutes); this.filterByVehicleRental = toSet(filterByBikeRentalStations); + this.filterByNetwork = toSet(filterByNetwork); includeStops = shouldInclude(filterByPlaceTypes, PlaceType.STOP); includePatternAtStops = shouldInclude(filterByPlaceTypes, PlaceType.PATTERN_AT_STOP); @@ -264,6 +267,9 @@ private void handleVehicleRental(VehicleRentalPlace station, double distance) { if (seenVehicleRentalPlaces.contains(station.getId())) { return; } + if (!filterByNetwork.isEmpty() && !filterByNetwork.contains(station.getNetwork())) { + return; + } seenVehicleRentalPlaces.add(station.getId()); placesFound.add(new PlaceAtDistance(station, distance)); } diff --git a/src/main/java/org/opentripplanner/routing/graphfinder/StreetGraphFinder.java b/src/main/java/org/opentripplanner/routing/graphfinder/StreetGraphFinder.java index 1b2b1d8f522..71f65209ddf 100644 --- a/src/main/java/org/opentripplanner/routing/graphfinder/StreetGraphFinder.java +++ b/src/main/java/org/opentripplanner/routing/graphfinder/StreetGraphFinder.java @@ -56,6 +56,7 @@ public List findClosestPlaces( List filterByStations, List filterByRoutes, List filterByBikeRentalStations, + List filterByNetwork, TransitService transitService ) { PlaceFinderTraverseVisitor visitor = new PlaceFinderTraverseVisitor( @@ -66,6 +67,7 @@ public List findClosestPlaces( filterByStations, filterByRoutes, filterByBikeRentalStations, + filterByNetwork, maxResults, radiusMeters ); diff --git a/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalPlace.java b/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalPlace.java index 1725e7df2dd..cd806603c9d 100644 --- a/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalPlace.java +++ b/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalPlace.java @@ -80,6 +80,9 @@ public interface VehicleRentalPlace { /** Deep links for this rental station or individual vehicle */ VehicleRentalStationUris getRentalUris(); + /** System information for the vehicle rental provider */ + VehicleRentalSystem getVehicleRentalSystem(); + default boolean networkIsNotAllowed(VehicleRentalPreferences preferences) { if ( getNetwork() == null && diff --git a/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalStation.java b/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalStation.java index a31e5e88a0e..d6e72023a31 100644 --- a/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalStation.java +++ b/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalStation.java @@ -172,6 +172,11 @@ public VehicleRentalStationUris getRentalUris() { return rentalUris; } + @Override + public VehicleRentalSystem getVehicleRentalSystem() { + return system; + } + @Override public String toString() { return String.format( diff --git a/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalVehicle.java b/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalVehicle.java index 7dba5e714b4..042e608c88f 100644 --- a/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalVehicle.java +++ b/src/main/java/org/opentripplanner/service/vehiclerental/model/VehicleRentalVehicle.java @@ -128,4 +128,9 @@ public boolean isRealTimeData() { public VehicleRentalStationUris getRentalUris() { return rentalUris; } + + @Override + public VehicleRentalSystem getVehicleRentalSystem() { + return system; + } } diff --git a/src/main/java/org/opentripplanner/standalone/config/sandbox/VehicleRentalServiceDirectoryFetcherConfig.java b/src/main/java/org/opentripplanner/standalone/config/sandbox/VehicleRentalServiceDirectoryFetcherConfig.java index b30fc3d5b3f..f8ed1184c04 100644 --- a/src/main/java/org/opentripplanner/standalone/config/sandbox/VehicleRentalServiceDirectoryFetcherConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/sandbox/VehicleRentalServiceDirectoryFetcherConfig.java @@ -3,6 +3,7 @@ import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_0; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_1; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_4; +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_5; import java.util.List; import org.opentripplanner.ext.vehiclerentalservicedirectory.api.NetworkParameters; @@ -79,6 +80,22 @@ private static List mapNetworkParameters( .of("geofencingZones") .since(V2_4) .summary("Enables geofencingZones for the given network") + .description( + "See the regular [GBFS documentation](../UpdaterConfig.md#gbfs-vehicle-rental-systems) for more information." + ) + .asBoolean(false), + c + .of("allowKeepingVehicleAtDestination") + .since(V2_5) + .summary("Enables `allowKeepingVehicleAtDestination` for the given network.") + .description( + """ + Configures if a vehicle rented from a station must be returned to another one or can + be kept at the end of the trip. + + See the regular [GBFS documentation](../UpdaterConfig.md#gbfs-vehicle-rental-systems) for more information. + """ + ) .asBoolean(false) ) ); diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java index b1bc6888753..7d07ca29f0e 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java @@ -33,6 +33,7 @@ import org.opentripplanner.standalone.server.OTPWebApplication; import org.opentripplanner.street.model.StreetLimitationParameters; import org.opentripplanner.street.model.elevation.ElevationUtils; +import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; import org.opentripplanner.updater.configure.UpdaterConfigurator; import org.opentripplanner.visualizer.GraphVisualizer; @@ -202,7 +203,7 @@ public static void creatTransitLayerForRaptor( TransitModel transitModel, TransitTuningParameters tuningParameters ) { - if (!transitModel.hasTransit() || transitModel.getTransitModelIndex() == null) { + if (!transitModel.hasTransit() || !transitModel.isIndexed()) { LOG.warn( "Cannot create Raptor data, that requires the graph to have transit data and be indexed." ); @@ -211,10 +212,7 @@ public static void creatTransitLayerForRaptor( transitModel.setTransitLayer(TransitLayerMapper.map(tuningParameters, transitModel)); transitModel.setRealtimeTransitLayer(new TransitLayer(transitModel.getTransitLayer())); transitModel.setTransitLayerUpdater( - new TransitLayerUpdater( - transitModel, - transitModel.getTransitModelIndex().getServiceCodesRunningForDate() - ) + new TransitLayerUpdater(new DefaultTransitService(transitModel)) ); } diff --git a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java index f9734c1cb1a..57c71d06113 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java @@ -331,17 +331,6 @@ public boolean isBoardAndAlightAt(int stopIndex, PickDrop value) { /* METHODS THAT DELEGATE TO THE SCHEDULED TIMETABLE */ - // TODO: These should probably be deprecated. That would require grabbing the scheduled timetable, - // and would avoid mistakes where real-time updates are accidentally not taken into account. - - public boolean stopPatternIsEqual(TripPattern other) { - return stopPattern.equals(other.stopPattern); - } - - public Trip getTrip(int tripIndex) { - return scheduledTimetable.getTripTimes(tripIndex).getTrip(); - } - // TODO OTP2 this method modifies the state, it will be refactored in a subsequent step /** * Add the given tripTimes to this pattern's scheduled timetable, recording the corresponding trip diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/BinarySetOperator.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/BinarySetOperator.java similarity index 79% rename from src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/BinarySetOperator.java rename to src/main/java/org/opentripplanner/transit/model/network/grouppriority/BinarySetOperator.java index 35e5b8c0918..4d8ce2c8fd7 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/BinarySetOperator.java +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/BinarySetOperator.java @@ -1,4 +1,4 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit.request; +package org.opentripplanner.transit.model.network.grouppriority; /** * Used to concatenate matches with either the logical "AND" or "OR" operator. diff --git a/src/main/java/org/opentripplanner/transit/model/network/grouppriority/DefaultTransitGroupPriorityCalculator.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/DefaultTransitGroupPriorityCalculator.java new file mode 100644 index 00000000000..df5d2abef8a --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/DefaultTransitGroupPriorityCalculator.java @@ -0,0 +1,26 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +import org.opentripplanner.raptor.api.model.DominanceFunction; +import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; + +/** + * Implement {@link RaptorTransitGroupPriorityCalculator}. + */ +public final class DefaultTransitGroupPriorityCalculator + implements RaptorTransitGroupPriorityCalculator { + + @Override + public int mergeGroupIds(int currentGroupIds, int boardingGroupId) { + return TransitGroupPriority32n.mergeInGroupId(currentGroupIds, boardingGroupId); + } + + @Override + public DominanceFunction dominanceFunction() { + return TransitGroupPriority32n::dominate; + } + + @Override + public String toString() { + return "DefaultTransitGroupCalculator{Using TGP32n}"; + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/network/grouppriority/EntityAdapter.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/EntityAdapter.java new file mode 100644 index 00000000000..760da3d87ca --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/EntityAdapter.java @@ -0,0 +1,16 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +/** + * These are the keys used to group transit trips and trip-patterns. This is used to calculate a + * unique groupId based on the request config. We use the adapter pattern to be able to generate + * the groupId based on different input types (TripPattern and Trip). + */ +interface EntityAdapter { + TransitMode mode(); + String subMode(); + FeedScopedId agencyId(); + FeedScopedId routeId(); +} diff --git a/src/main/java/org/opentripplanner/transit/model/network/grouppriority/Matcher.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/Matcher.java new file mode 100644 index 00000000000..bb5b4075364 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/Matcher.java @@ -0,0 +1,9 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +interface Matcher { + boolean match(EntityAdapter entity); + + default boolean isEmpty() { + return false; + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupMatcher.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/Matchers.java similarity index 54% rename from src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupMatcher.java rename to src/main/java/org/opentripplanner/transit/model/network/grouppriority/Matchers.java index c017f2862ab..7e1e7e6853a 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupMatcher.java +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/Matchers.java @@ -1,7 +1,7 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit.request; +package org.opentripplanner.transit.model.network.grouppriority; -import static org.opentripplanner.routing.algorithm.raptoradapter.transit.request.BinarySetOperator.AND; -import static org.opentripplanner.routing.algorithm.raptoradapter.transit.request.BinarySetOperator.OR; +import static org.opentripplanner.transit.model.network.grouppriority.BinarySetOperator.AND; +import static org.opentripplanner.transit.model.network.grouppriority.BinarySetOperator.OR; import java.util.ArrayList; import java.util.Arrays; @@ -18,7 +18,6 @@ import org.opentripplanner.routing.api.request.request.filter.TransitGroupSelect; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.network.TripPattern; /** * This class turns a {@link TransitGroupSelect} into a matcher. @@ -28,49 +27,38 @@ * a `CompositeMatcher`. So, a new matcher is only created if the field in the * select is present. */ -public abstract class PriorityGroupMatcher { +final class Matchers { - private static final PriorityGroupMatcher NOOP = new PriorityGroupMatcher() { - @Override - boolean match(TripPattern pattern) { - return false; - } + private static final Matcher NOOP = new EmptyMatcher(); - @Override - boolean isEmpty() { - return true; - } - }; - - public static PriorityGroupMatcher of(TransitGroupSelect select) { + static Matcher of(TransitGroupSelect select) { if (select.isEmpty()) { return NOOP; } - List list = new ArrayList<>(); + List list = new ArrayList<>(); if (!select.modes().isEmpty()) { list.add(new ModeMatcher(select.modes())); } if (!select.subModeRegexp().isEmpty()) { - list.add( - new RegExpMatcher("SubMode", select.subModeRegexp(), p -> p.getNetexSubmode().name()) - ); + list.add(new RegExpMatcher("SubMode", select.subModeRegexp(), EntityAdapter::subMode)); } if (!select.agencyIds().isEmpty()) { - list.add(new IdMatcher("Agency", select.agencyIds(), p -> p.getRoute().getAgency().getId())); + list.add(new IdMatcher("Agency", select.agencyIds(), EntityAdapter::agencyId)); } if (!select.routeIds().isEmpty()) { - list.add(new IdMatcher("Route", select.routeIds(), p -> p.getRoute().getId())); + list.add(new IdMatcher("Route", select.routeIds(), EntityAdapter::routeId)); } return andOf(list); } - static PriorityGroupMatcher[] of(Collection selectors) { + @SuppressWarnings("unchecked") + static Matcher[] of(Collection selectors) { return selectors .stream() - .map(PriorityGroupMatcher::of) - .filter(Predicate.not(PriorityGroupMatcher::isEmpty)) - .toArray(PriorityGroupMatcher[]::new); + .map(Matchers::of) + .filter(Predicate.not(Matcher::isEmpty)) + .toArray(Matcher[]::new); } private static String arrayToString(BinarySetOperator op, T[] values) { @@ -81,9 +69,9 @@ private static String colToString(BinarySetOperator op, Collection values return values.stream().map(Objects::toString).collect(Collectors.joining(" " + op + " ")); } - private static PriorityGroupMatcher andOf(List list) { + private static Matcher andOf(List list) { // Remove empty/noop matchers - list = list.stream().filter(Predicate.not(PriorityGroupMatcher::isEmpty)).toList(); + list = list.stream().filter(Predicate.not(Matcher::isEmpty)).toList(); if (list.isEmpty()) { return NOOP; @@ -94,13 +82,25 @@ private static PriorityGroupMatcher andOf(List list) { return new AndMatcher(list); } - abstract boolean match(TripPattern pattern); + private static final class EmptyMatcher implements Matcher { + + @Override + public boolean match(EntityAdapter entity) { + return false; + } + + @Override + public boolean isEmpty() { + return true; + } - boolean isEmpty() { - return false; + @Override + public String toString() { + return "Empty"; + } } - private static final class ModeMatcher extends PriorityGroupMatcher { + private static final class ModeMatcher implements Matcher { private final Set modes; @@ -109,8 +109,8 @@ public ModeMatcher(List modes) { } @Override - boolean match(TripPattern pattern) { - return modes.contains(pattern.getMode()); + public boolean match(EntityAdapter entity) { + return modes.contains(entity.mode()); } @Override @@ -119,26 +119,26 @@ public String toString() { } } - private static final class RegExpMatcher extends PriorityGroupMatcher { + private static final class RegExpMatcher implements Matcher { private final String typeName; - private final Pattern[] subModeRegexp; - private final Function toValue; + private final Pattern[] patterns; + private final Function toValue; public RegExpMatcher( String typeName, - List subModeRegexp, - Function toValue + List regexps, + Function toValue ) { this.typeName = typeName; - this.subModeRegexp = subModeRegexp.stream().map(Pattern::compile).toArray(Pattern[]::new); + this.patterns = regexps.stream().map(Pattern::compile).toArray(Pattern[]::new); this.toValue = toValue; } @Override - boolean match(TripPattern pattern) { - var value = toValue.apply(pattern); - for (Pattern p : subModeRegexp) { + public boolean match(EntityAdapter entity) { + var value = toValue.apply(entity); + for (Pattern p : patterns) { if (p.matcher(value).matches()) { return true; } @@ -148,20 +148,20 @@ boolean match(TripPattern pattern) { @Override public String toString() { - return typeName + "Regexp(" + arrayToString(OR, subModeRegexp) + ')'; + return typeName + "Regexp(" + arrayToString(OR, patterns) + ')'; } } - private static final class IdMatcher extends PriorityGroupMatcher { + private static final class IdMatcher implements Matcher { private final String typeName; private final Set ids; - private final Function idProvider; + private final Function idProvider; public IdMatcher( String typeName, List ids, - Function idProvider + Function idProvider ) { this.typeName = typeName; this.ids = new HashSet<>(ids); @@ -169,8 +169,8 @@ public IdMatcher( } @Override - boolean match(TripPattern pattern) { - return ids.contains(idProvider.apply(pattern)); + public boolean match(EntityAdapter entity) { + return ids.contains(idProvider.apply(entity)); } @Override @@ -183,18 +183,18 @@ public String toString() { * Takes a list of matchers and provide a single interface. All matchers in the list must match * for the composite matcher to return a match. */ - private static final class AndMatcher extends PriorityGroupMatcher { + private static final class AndMatcher implements Matcher { - private final PriorityGroupMatcher[] matchers; + private final Matcher[] matchers; - public AndMatcher(List matchers) { - this.matchers = matchers.toArray(PriorityGroupMatcher[]::new); + public AndMatcher(List matchers) { + this.matchers = matchers.toArray(Matcher[]::new); } @Override - boolean match(TripPattern pattern) { + public boolean match(EntityAdapter entity) { for (var m : matchers) { - if (!m.match(pattern)) { + if (!m.match(entity)) { return false; } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/grouppriority/TransitGroupPriority32n.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriority32n.java similarity index 51% rename from src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/grouppriority/TransitGroupPriority32n.java rename to src/main/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriority32n.java index feb3f6f7b3a..32423070e09 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/grouppriority/TransitGroupPriority32n.java +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriority32n.java @@ -1,55 +1,31 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.grouppriority; - -import org.opentripplanner.raptor.api.model.DominanceFunction; -import org.opentripplanner.raptor.api.request.RaptorTransitGroupCalculator; +package org.opentripplanner.transit.model.network.grouppriority; /** - * This is a "BitSet" implementation for groupId. It can store upto 32 groups, + * This is a "BitSet" implementation for groupId. It can store up to 31 groups, * a set with few elements does NOT dominate a set with more elements. */ -public class TransitGroupPriority32n { +class TransitGroupPriority32n { private static final int GROUP_ZERO = 0; private static final int MIN_SEQ_NO = 0; private static final int MAX_SEQ_NO = 32; - public static RaptorTransitGroupCalculator priorityCalculator() { - return new RaptorTransitGroupCalculator() { - @Override - public int mergeGroupIds(int currentGroupIds, int boardingGroupId) { - return mergeInGroupId(currentGroupIds, boardingGroupId); - } - - @Override - public DominanceFunction dominanceFunction() { - return TransitGroupPriority32n::dominate; - } - - @Override - public String toString() { - return "TransitGroupPriority32nCalculator{}"; - } - }; - } - /** - * Left dominate right, if right contains a group which does not exist in left. Left - * do NOT dominate right if they are equals or left is a super set of right. + * Left dominates right: + * - if right contains a group which does not exist in the left. + * Left do NOT dominate right: + * - if they are equals or + * - left is a superset of right. */ - public static boolean dominate(int left, int right) { + static boolean dominate(int left, int right) { return ((left ^ right) & right) != 0; } - @Override - public String toString() { - return "TransitGroupPriority32n{}"; - } - /** * Use this method to map from a continuous group index [0..32) to the groupId used - * during routing. The ID is implementation specific and optimized for performance. + * during routing. The ID is implementation-specific and optimized for performance. */ - public static int groupId(final int priorityGroupIndex) { + static int groupId(final int priorityGroupIndex) { assertValidGroupSeqNo(priorityGroupIndex); return priorityGroupIndex == MIN_SEQ_NO ? GROUP_ZERO : 0x01 << (priorityGroupIndex - 1); } @@ -57,10 +33,15 @@ public static int groupId(final int priorityGroupIndex) { /** * Merge a groupId into a set of groupIds. */ - public static int mergeInGroupId(final int currentSetOfGroupIds, final int newGroupId) { + static int mergeInGroupId(final int currentSetOfGroupIds, final int newGroupId) { return currentSetOfGroupIds | newGroupId; } + @Override + public String toString() { + return "TransitGroupPriority32n{}"; + } + private static void assertValidGroupSeqNo(int priorityGroupIndex) { if (priorityGroupIndex < MIN_SEQ_NO) { throw new IllegalArgumentException( diff --git a/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriorityService.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriorityService.java new file mode 100644 index 00000000000..048b2279a88 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriorityService.java @@ -0,0 +1,176 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +import gnu.trove.impl.Constants; +import gnu.trove.map.TObjectIntMap; +import gnu.trove.map.hash.TObjectIntHashMap; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; +import org.opentripplanner.framework.lang.ArrayUtils; +import org.opentripplanner.routing.api.request.framework.CostLinearFunction; +import org.opentripplanner.routing.api.request.request.filter.TransitGroupSelect; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.Trip; + +/** + * This class dynamically builds an index of transit-group-ids from the provided + * {@link TransitGroupSelect}s while serving the caller with group-ids for each requested + * trip/pattern. It is made for optimal performance, since it is used in request scope. + *

+ * THIS CLASS IS NOT THREAD-SAFE. + */ +public class TransitGroupPriorityService { + + /** + * IMPLEMENTATION DETAILS + * + * There are two ways we can treat the base (local-traffic) transit priority group: + *

    + *
  1. + * We can assign group id 1 (one) to the base group and it will be treated as any other group. + *
  2. + *
  3. + * We can assign group id 0 (zero) to the base and it will not be added to the set of groups + * a given path has. + *
  4. + *
+ * When we compare paths, we compare sets of group ids. A set is dominating another set if it is + * a smaller subset or different from the other set. + */ + private static final int GROUP_INDEX_COUNTER_START = 1; + + private final int baseGroupId = TransitGroupPriority32n.groupId(GROUP_INDEX_COUNTER_START); + private int groupIndexCounter = GROUP_INDEX_COUNTER_START; + private final boolean enabled; + private final Matcher[] agencyMatchers; + private final Matcher[] globalMatchers; + + // Index matchers and ids + private final List agencyMatchersIds; + private final List globalMatchersIds; + + private TransitGroupPriorityService() { + this.enabled = false; + this.agencyMatchers = null; + this.globalMatchers = null; + this.agencyMatchersIds = List.of(); + this.globalMatchersIds = List.of(); + } + + public TransitGroupPriorityService( + Collection byAgency, + Collection global + ) { + this.agencyMatchers = Matchers.of(byAgency); + this.globalMatchers = Matchers.of(global); + this.enabled = Stream.of(agencyMatchers, globalMatchers).anyMatch(ArrayUtils::hasContent); + this.globalMatchersIds = + Arrays.stream(globalMatchers).map(m -> new MatcherAndId(m, nextGroupId())).toList(); + // We need to populate this dynamically + this.agencyMatchersIds = Arrays.stream(agencyMatchers).map(MatcherAgencyAndIds::new).toList(); + } + + public static TransitGroupPriorityService empty() { + return new TransitGroupPriorityService(); + } + + public static TransitGroupPriorityService of( + CostLinearFunction relaxTransitGroupPriority, + List groupByAgency, + List groupGlobal + ) { + if (relaxTransitGroupPriority.isNormal()) { + return TransitGroupPriorityService.empty(); + } else if (Stream.of(groupByAgency, groupGlobal).allMatch(Collection::isEmpty)) { + return TransitGroupPriorityService.empty(); + } else { + return new TransitGroupPriorityService(groupByAgency, groupGlobal); + } + } + + /** + * Return true is the feature is configured and the request a {@code relaxTransitGroupPriority} + * function. + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Fetch/lookup the transit-group-id for the given pattern. + *

+ * @throws IllegalArgumentException if more than 32 group-ids are requested. + */ + public int lookupTransitGroupPriorityId(TripPattern tripPattern) { + return tripPattern == null + ? baseGroupId + : lookupTransitGroupPriorityId(new TripPatternAdapter(tripPattern)); + } + + /** + * Fetch/lookup the transit-group-id for the given trip. + *

+ * @throws IllegalArgumentException if more than 32 group-ids are requested. + */ + public int lookupTransitGroupPriorityId(Trip trip) { + return trip == null ? baseGroupId : lookupTransitGroupPriorityId(new TripAdapter(trip)); + } + + /** + * Fetch/lookup the transit-group-id for the given entity. + *

+ * @throws IllegalArgumentException if more than 32 group-ids are requested. + */ + private int lookupTransitGroupPriorityId(EntityAdapter entity) { + if (!enabled) { + return baseGroupId; + } + for (var it : agencyMatchersIds) { + if (it.matcher().match(entity)) { + var agencyId = entity.agencyId(); + int groupId = it.ids().get(agencyId); + + if (groupId < 0) { + groupId = nextGroupId(); + it.ids.put(agencyId, groupId); + } + return groupId; + } + } + + for (var it : globalMatchersIds) { + if (it.matcher.match(entity)) { + return it.groupId(); + } + } + // Fallback to base-group-id + return baseGroupId; + } + + /** + * This is the group-id assigned to all transit trips/patterns witch does not match a + * specific group. + */ + public int baseGroupId() { + return baseGroupId; + } + + private int nextGroupId() { + return TransitGroupPriority32n.groupId(++groupIndexCounter); + } + + /** Pair of matcher and groupId. Used only inside this class. */ + private record MatcherAndId(Matcher matcher, int groupId) {} + + /** Matcher with a map of ids by agency. */ + private record MatcherAgencyAndIds(Matcher matcher, TObjectIntMap ids) { + MatcherAgencyAndIds(Matcher matcher) { + this( + matcher, + new TObjectIntHashMap<>(Constants.DEFAULT_CAPACITY, Constants.DEFAULT_LOAD_FACTOR, -1) + ); + } + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TripAdapter.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TripAdapter.java new file mode 100644 index 00000000000..7ff1d158e62 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TripAdapter.java @@ -0,0 +1,34 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.timetable.Trip; + +class TripAdapter implements EntityAdapter { + + private final Trip trip; + + public TripAdapter(Trip trip) { + this.trip = trip; + } + + @Override + public TransitMode mode() { + return trip.getMode(); + } + + @Override + public String subMode() { + return trip.getNetexSubMode().name(); + } + + @Override + public FeedScopedId agencyId() { + return trip.getRoute().getAgency().getId(); + } + + @Override + public FeedScopedId routeId() { + return trip.getRoute().getId(); + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TripPatternAdapter.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TripPatternAdapter.java new file mode 100644 index 00000000000..223f163f535 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TripPatternAdapter.java @@ -0,0 +1,34 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.TripPattern; + +class TripPatternAdapter implements EntityAdapter { + + private final TripPattern tripPattern; + + public TripPatternAdapter(TripPattern tripPattern) { + this.tripPattern = tripPattern; + } + + @Override + public TransitMode mode() { + return tripPattern.getMode(); + } + + @Override + public String subMode() { + return tripPattern.getNetexSubmode().name(); + } + + @Override + public FeedScopedId agencyId() { + return tripPattern.getRoute().getAgency().getId(); + } + + @Override + public FeedScopedId routeId() { + return tripPattern.getRoute().getId(); + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/site/MultiModalStation.java b/src/main/java/org/opentripplanner/transit/model/site/MultiModalStation.java index 749b156656e..70f4924e8e1 100644 --- a/src/main/java/org/opentripplanner/transit/model/site/MultiModalStation.java +++ b/src/main/java/org/opentripplanner/transit/model/site/MultiModalStation.java @@ -37,11 +37,11 @@ public class MultiModalStation super(builder.getId()); // Required fields this.childStations = Objects.requireNonNull(builder.childStations()); + this.coordinate = Objects.requireNonNull(builder.coordinate()); this.name = I18NString.assertHasValue(builder.name()); // Optional fields // TODO Make required - this.coordinate = builder.coordinate(); this.code = builder.code(); this.description = builder.description(); this.url = builder.url(); diff --git a/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java b/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java index ca88b7d3130..398cb806524 100644 --- a/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java @@ -9,6 +9,8 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -16,6 +18,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.Nullable; import org.locationtech.jts.geom.Envelope; import org.opentripplanner.ext.flex.FlexIndex; import org.opentripplanner.framework.application.OTPRequestTimeoutException; @@ -34,6 +37,7 @@ import org.opentripplanner.transit.model.basic.Notice; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.AbstractTransitEntity; +import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.GroupOfRoutes; import org.opentripplanner.transit.model.network.Route; @@ -181,6 +185,10 @@ public Route getRouteForId(FeedScopedId id) { return this.transitModelIndex.getRouteForId(id); } + /** + * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix + * this when doing the issue #3030. + */ @Override public void addRoutes(Route route) { this.transitModelIndex.addRoutes(route); @@ -259,6 +267,15 @@ public Trip getTripForId(FeedScopedId id) { return this.transitModelIndex.getTripForId().get(id); } + /** + * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix + * this when doing the issue #3030. + */ + @Override + public void addTripForId(FeedScopedId tripId, Trip trip) { + transitModelIndex.getTripForId().put(tripId, trip); + } + @Override public Collection getAllTrips() { OTPRequestTimeoutException.checkForTimeout(); @@ -276,6 +293,15 @@ public TripPattern getPatternForTrip(Trip trip) { return this.transitModelIndex.getPatternForTrip().get(trip); } + /** + * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix + * this when doing the issue #3030. + */ + @Override + public void addPatternForTrip(Trip trip, TripPattern pattern) { + transitModelIndex.getPatternForTrip().put(trip, pattern); + } + @Override public TripPattern getPatternForTrip(Trip trip, LocalDate serviceDate) { TripPattern realtimePattern = getRealtimeAddedTripPattern(trip.getId(), serviceDate); @@ -291,6 +317,15 @@ public Collection getPatternsForRoute(Route route) { return this.transitModelIndex.getPatternsForRoute().get(route); } + /** + * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix + * this when doing the issue #3030. + */ + @Override + public void addPatternsForRoute(Route route, TripPattern pattern) { + transitModelIndex.getPatternsForRoute().put(route, pattern); + } + @Override public MultiModalStation getMultiModalStationForStation(Station station) { return this.transitModel.getStopModel().getMultiModalStationForStation(station); @@ -398,15 +433,23 @@ public List stopTimesForPatternAtStop( /** * Returns all the patterns for a specific stop. If includeRealtimeUpdates is set, new patterns * added by realtime updates are added to the collection. + * A set is used here because trip patterns + * that were updated by realtime data is both part of the TransitModelIndex and the TimetableSnapshot */ @Override public Collection getPatternsForStop( StopLocation stop, boolean includeRealtimeUpdates ) { - return transitModel - .getTransitModelIndex() - .getPatternsForStop(stop, includeRealtimeUpdates ? lazyGetTimeTableSnapShot() : null); + Set tripPatterns = new HashSet<>(getPatternsForStop(stop)); + + if (includeRealtimeUpdates) { + TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); + if (currentSnapshot != null) { + tripPatterns.addAll(currentSnapshot.getPatternsForStop(stop)); + } + } + return tripPatterns; } @Override @@ -434,28 +477,28 @@ public GroupOfRoutes getGroupOfRoutesForId(FeedScopedId id) { @Override public Timetable getTimetableForTripPattern(TripPattern tripPattern, LocalDate serviceDate) { OTPRequestTimeoutException.checkForTimeout(); - TimetableSnapshot timetableSnapshot = lazyGetTimeTableSnapShot(); - return timetableSnapshot != null - ? timetableSnapshot.resolve(tripPattern, serviceDate) + TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); + return currentSnapshot != null + ? currentSnapshot.resolve(tripPattern, serviceDate) : tripPattern.getScheduledTimetable(); } @Override public TripPattern getRealtimeAddedTripPattern(FeedScopedId tripId, LocalDate serviceDate) { - TimetableSnapshot timetableSnapshot = lazyGetTimeTableSnapShot(); - if (timetableSnapshot == null) { + TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); + if (currentSnapshot == null) { return null; } - return timetableSnapshot.getRealtimeAddedTripPattern(tripId, serviceDate); + return currentSnapshot.getRealtimeAddedTripPattern(tripId, serviceDate); } @Override public boolean hasRealtimeAddedTripPatterns() { - TimetableSnapshot timetableSnapshot = lazyGetTimeTableSnapShot(); - if (timetableSnapshot == null) { + TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); + if (currentSnapshot == null) { return false; } - return timetableSnapshot.hasRealtimeAddedTripPatterns(); + return currentSnapshot.hasRealtimeAddedTripPatterns(); } /** @@ -463,6 +506,7 @@ public boolean hasRealtimeAddedTripPatterns() { * * @return The same TimetableSnapshot is returned throughout the lifecycle of this object. */ + @Nullable private TimetableSnapshot lazyGetTimeTableSnapShot() { if (this.timetableSnapshot == null) { timetableSnapshot = transitModel.getTimetableSnapshot(); @@ -475,6 +519,15 @@ public TripOnServiceDate getTripOnServiceDateById(FeedScopedId datedServiceJourn return transitModelIndex.getTripOnServiceDateById().get(datedServiceJourneyId); } + /** + * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix + * this when doing the issue #3030. + */ + @Override + public void addTripOnServiceDateById(FeedScopedId id, TripOnServiceDate tripOnServiceDate) { + transitModelIndex.getTripOnServiceDateById().put(id, tripOnServiceDate); + } + @Override public Collection getAllTripOnServiceDates() { return transitModelIndex.getTripOnServiceDateForTripAndDay().values(); @@ -487,6 +540,29 @@ public TripOnServiceDate getTripOnServiceDateForTripAndDay( return transitModelIndex.getTripOnServiceDateForTripAndDay().get(tripIdAndServiceDate); } + /** + * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix + * this when doing the issue #3030. + */ + @Override + public void addTripOnServiceDateForTripAndDay( + TripIdAndServiceDate tripIdAndServiceDate, + TripOnServiceDate tripOnServiceDate + ) { + transitModelIndex + .getTripOnServiceDateForTripAndDay() + .put(tripIdAndServiceDate, tripOnServiceDate); + } + + /** + * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix + * this when doing the issue #3030. + */ + @Override + public FeedScopedId getOrCreateServiceIdForDate(LocalDate serviceDate) { + return transitModel.getOrCreateServiceIdForDate(serviceDate); + } + @Override public void addTransitMode(TransitMode mode) { this.transitModel.addTransitMode(mode); @@ -519,6 +595,16 @@ public void setTransitLayer(TransitLayer transitLayer) { this.transitModel.setTransitLayer(transitLayer); } + @Override + public void setRealtimeTransitLayer(TransitLayer realtimeTransitLayer) { + transitModel.setRealtimeTransitLayer(realtimeTransitLayer); + } + + @Override + public boolean hasRealtimeTransitLayer() { + return transitModel.hasRealtimeTransitLayer(); + } + @Override public CalendarService getCalendarService() { return this.transitModel.getCalendarService(); @@ -579,6 +665,21 @@ public List getModesOfStopLocation(StopLocation stop) { return sortByOccurrenceAndReduce(getPatternModesOfStop(stop)).toList(); } + @Override + public Deduplicator getDeduplicator() { + return transitModel.getDeduplicator(); + } + + @Override + public Set getAllServiceCodes() { + return Collections.unmodifiableSet(transitModelIndex.getServiceCodesRunningForDate().keySet()); + } + + @Override + public Map getServiceCodesRunningForDate() { + return Collections.unmodifiableMap(transitModelIndex.getServiceCodesRunningForDate()); + } + /** * For each pattern visiting this {@link StopLocation} return its {@link TransitMode} */ diff --git a/src/main/java/org/opentripplanner/transit/service/TransitEditorService.java b/src/main/java/org/opentripplanner/transit/service/TransitEditorService.java index 7d2f99df71b..150d1749272 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitEditorService.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitEditorService.java @@ -1,10 +1,16 @@ package org.opentripplanner.transit.service; +import java.time.LocalDate; import org.opentripplanner.model.FeedInfo; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.organization.Agency; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; /** * Entry point for requests (both read-only and read-write) towards the transit API. @@ -14,9 +20,41 @@ public interface TransitEditorService extends TransitService { void addFeedInfo(FeedInfo info); + void addPatternForTrip(Trip trip, TripPattern pattern); + + void addPatternsForRoute(Route route, TripPattern pattern); + void addRoutes(Route route); void addTransitMode(TransitMode mode); + void addTripForId(FeedScopedId tripId, Trip trip); + + void addTripOnServiceDateById(FeedScopedId id, TripOnServiceDate tripOnServiceDate); + + void addTripOnServiceDateForTripAndDay( + TripIdAndServiceDate tripIdAndServiceDate, + TripOnServiceDate tripOnServiceDate + ); + + FeedScopedId getOrCreateServiceIdForDate(LocalDate serviceDate); + + /** + * Set the original, immutable, transit layer, + * based on scheduled data (not real-time data). + */ void setTransitLayer(TransitLayer transitLayer); + + /** + * Return true if a real-time transit layer is present. + * The real-time transit layer is optional, + * it is present only when real-time updaters are configured. + */ + boolean hasRealtimeTransitLayer(); + + /** + * Publish the latest snapshot of the real-time transit layer. + * Should be called only when creating a new TransitLayer, from the graph writer thread. + */ + void setRealtimeTransitLayer(TransitLayer realtimeTransitLayer); } diff --git a/src/main/java/org/opentripplanner/transit/service/TransitModel.java b/src/main/java/org/opentripplanner/transit/service/TransitModel.java index d5211d5dd2e..84c7597d562 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitModel.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitModel.java @@ -169,6 +169,7 @@ public void index() { } } + @Nullable public TimetableSnapshot getTimetableSnapshot() { return timetableSnapshotProvider == null ? null @@ -254,8 +255,6 @@ public void updateCalendarServiceData( * Get or create a serviceId for a given date. This method is used when a new trip is added from a * realtime data update. It make sure the date is in the existing transit service period. *

- * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix - * - this when doing the issue #3030. * * @param serviceDate service date for the added service id * @return service-id for date if it exist or is created. If the given service date is outside the @@ -540,10 +539,15 @@ public void setHasScheduledService(boolean hasScheduledService) { * The caller is responsible for calling the {@link #index()} method if it is a * possibility that the index is not initialized (during graph build). */ - public @Nullable TransitModelIndex getTransitModelIndex() { + @Nullable + TransitModelIndex getTransitModelIndex() { return index; } + public boolean isIndexed() { + return index != null; + } + public boolean hasFlexTrips() { return !flexTripsById.isEmpty(); } diff --git a/src/main/java/org/opentripplanner/transit/service/TransitModelIndex.java b/src/main/java/org/opentripplanner/transit/service/TransitModelIndex.java index 84759ffae72..36ab937416c 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitModelIndex.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitModelIndex.java @@ -15,7 +15,6 @@ import org.opentripplanner.ext.flex.FlexIndex; import org.opentripplanner.ext.flex.trip.FlexTrip; import org.opentripplanner.framework.application.OTPFeature; -import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.model.calendar.CalendarService; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.GroupOfRoutes; @@ -35,7 +34,7 @@ * For performance reasons these indexes are not part of the serialized state of the graph. * They are rebuilt at runtime after graph deserialization. */ -public class TransitModelIndex { +class TransitModelIndex { private static final Logger LOG = LoggerFactory.getLogger(TransitModelIndex.class); @@ -118,24 +117,20 @@ public class TransitModelIndex { LOG.info("Transit Model index init complete."); } - public Agency getAgencyForId(FeedScopedId id) { + Agency getAgencyForId(FeedScopedId id) { return agencyForId.get(id); } - public Route getRouteForId(FeedScopedId id) { + Route getRouteForId(FeedScopedId id) { return routeForId.get(id); } - /** - * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix - * - this when doing the issue #3030. - */ - public void addRoutes(Route route) { + void addRoutes(Route route) { routeForId.put(route.getId(), route); } /** Dynamically generate the set of Routes passing though a Stop on demand. */ - public Set getRoutesForStop(StopLocation stop) { + Set getRoutesForStop(StopLocation stop) { Set routes = new HashSet<>(); for (TripPattern p : getPatternsForStop(stop)) { routes.add(p.getRoute()); @@ -143,75 +138,57 @@ public Set getRoutesForStop(StopLocation stop) { return routes; } - public Collection getPatternsForStop(StopLocation stop) { + Collection getPatternsForStop(StopLocation stop) { return patternsForStopId.get(stop); } - public Collection getTripsForStop(StopLocation stop) { + Collection getTripsForStop(StopLocation stop) { return getPatternsForStop(stop) .stream() .flatMap(TripPattern::scheduledTripsAsStream) .collect(Collectors.toList()); } - /** - * Returns all the patterns for a specific stop. If timetableSnapshot is included, new patterns - * added by realtime updates are added to the collection. A set is used here because trip patterns - * that were updated by realtime data is both part of the TransitModelIndex and the TimetableSnapshot. - */ - public Collection getPatternsForStop( - StopLocation stop, - TimetableSnapshot timetableSnapshot - ) { - Set tripPatterns = new HashSet<>(getPatternsForStop(stop)); - - if (timetableSnapshot != null) { - tripPatterns.addAll(timetableSnapshot.getPatternsForStop(stop)); - } - - return tripPatterns; - } - /** * Get a list of all operators spanning across all feeds. */ - public Collection getAllOperators() { + Collection getAllOperators() { return getOperatorForId().values(); } - public Map getOperatorForId() { + Map getOperatorForId() { return operatorForId; } - public Map getTripForId() { + Map getTripForId() { return tripForId; } - public Map getTripOnServiceDateById() { + Map getTripOnServiceDateById() { return tripOnServiceDateById; } - public Map getTripOnServiceDateForTripAndDay() { + Map getTripOnServiceDateForTripAndDay() { return tripOnServiceDateForTripAndDay; } - public Collection getAllRoutes() { + Collection getAllRoutes() { return routeForId.values(); } - public Map getPatternForTrip() { + Map getPatternForTrip() { return patternForTrip; } - public Multimap getPatternsForRoute() { + Multimap getPatternsForRoute() { return patternsForRoute; } - public Map getServiceCodesRunningForDate() { + Map getServiceCodesRunningForDate() { return serviceCodesRunningForDate; } - public FlexIndex getFlexIndex() { + FlexIndex getFlexIndex() { return flexIndex; } @@ -252,11 +229,11 @@ private void initalizeServiceCodesForDate(TransitModel transitModel) { } } - public Multimap getRoutesForGroupOfRoutes() { + Multimap getRoutesForGroupOfRoutes() { return routesForGroupOfRoutes; } - public Map getGroupOfRoutesForId() { + Map getGroupOfRoutesForId() { return groupOfRoutesForId; } } diff --git a/src/main/java/org/opentripplanner/transit/service/TransitService.java b/src/main/java/org/opentripplanner/transit/service/TransitService.java index 83b65c44d12..94870643f71 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitService.java @@ -8,8 +8,10 @@ import java.time.ZonedDateTime; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; +import javax.annotation.Nullable; import org.locationtech.jts.geom.Envelope; import org.opentripplanner.ext.flex.FlexIndex; import org.opentripplanner.model.FeedInfo; @@ -25,6 +27,7 @@ import org.opentripplanner.transit.model.basic.Notice; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.AbstractTransitEntity; +import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.GroupOfRoutes; import org.opentripplanner.transit.model.network.Route; @@ -93,8 +96,16 @@ public interface TransitService { Set getRoutesForStop(StopLocation stop); + /** + * Return all the scheduled trip patterns for a specific stop + * (not taking into account real-time updates). + */ Collection getPatternsForStop(StopLocation stop); + /** + * Returns all the patterns for a specific stop. If includeRealtimeUpdates is set, new patterns + * added by realtime updates are added to the collection. + */ Collection getPatternsForStop(StopLocation stop, boolean includeRealtimeUpdates); Collection getTripsForStop(StopLocation stop); @@ -127,8 +138,16 @@ public interface TransitService { Collection getAllRoutes(); + /** + * Return the scheduled trip pattern for a given trip (not taking into account real-time updates) + */ TripPattern getPatternForTrip(Trip trip); + /** + * Return the trip pattern for a given trip on a service date. The real-time updated version + * is returned if it exists, otherwise the scheduled trip pattern is returned. + * + */ TripPattern getPatternForTrip(Trip trip, LocalDate serviceDate); Collection getPatternsForRoute(Route route); @@ -167,6 +186,11 @@ List stopTimesForPatternAtStop( GroupOfRoutes getGroupOfRoutesForId(FeedScopedId id); + /** + * Return the timetable for a given trip pattern and date, taking into account real-time updates. + * If no real-times update are applied, fall back to scheduled data. + */ + @Nullable Timetable getTimetableForTripPattern(TripPattern tripPattern, LocalDate serviceDate); TripPattern getRealtimeAddedTripPattern(FeedScopedId tripId, LocalDate serviceDate); @@ -231,4 +255,10 @@ List stopTimesForPatternAtStop( * So, if more patterns of mode BUS than RAIL visit the stop, the result will be [BUS,RAIL]. */ List getModesOfStopLocation(StopLocation stop); + + Deduplicator getDeduplicator(); + + Set getAllServiceCodes(); + + Map getServiceCodesRunningForDate(); } diff --git a/src/main/java/org/opentripplanner/updater/spi/UpdateError.java b/src/main/java/org/opentripplanner/updater/spi/UpdateError.java index 548ef0210eb..1f568ba99a4 100644 --- a/src/main/java/org/opentripplanner/updater/spi/UpdateError.java +++ b/src/main/java/org/opentripplanner/updater/spi/UpdateError.java @@ -49,6 +49,7 @@ public enum UpdateErrorType { NOT_IMPLEMENTED_UNSCHEDULED, NOT_IMPLEMENTED_DUPLICATED, NOT_MONITORED, + CANNOT_RESOLVE_AGENCY, } public static Result result(FeedScopedId tripId, UpdateErrorType errorType) { diff --git a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java index cc39d82369b..c452d5f58f8 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java +++ b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java @@ -1,7 +1,9 @@ package org.opentripplanner.updater.trip; +import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_ARRIVAL_TIME; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_DEPARTURE_TIME; +import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_INPUT_STRUCTURE; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NOT_IMPLEMENTED_DUPLICATED; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NOT_IMPLEMENTED_UNSCHEDULED; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_SERVICE_ON_DATE; @@ -19,6 +21,7 @@ import com.google.common.collect.Multimaps; import com.google.transit.realtime.GtfsRealtime; import com.google.transit.realtime.GtfsRealtime.TripDescriptor; +import com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship; import com.google.transit.realtime.GtfsRealtime.TripUpdate; import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate; import de.mfdz.MfdzRealtimeExtensions; @@ -150,7 +153,7 @@ public UpdateResult applyTripUpdates( return UpdateResult.empty(); } - Map failuresByRelationship = new HashMap<>(); + Map failuresByRelationship = new HashMap<>(); List> results = new ArrayList<>(); snapshotManager.withLock(() -> { @@ -176,6 +179,7 @@ public UpdateResult applyTripUpdates( if (!tripDescriptor.hasTripId() || tripDescriptor.getTripId().isBlank()) { debug(feedId, "", "No trip id found for gtfs-rt trip update: \n{}", tripUpdate); + results.add(Result.failure(UpdateError.noTripId(INVALID_INPUT_STRUCTURE))); continue; } @@ -199,11 +203,12 @@ public UpdateResult applyTripUpdates( serviceDate = localDateNow.get(); } // Determine what kind of trip update this is - final TripDescriptor.ScheduleRelationship tripScheduleRelationship = determineTripScheduleRelationship( - tripDescriptor + var scheduleRelationship = Objects.requireNonNullElse( + tripDescriptor.getScheduleRelationship(), + SCHEDULED ); if (updateIncrementality == DIFFERENTIAL) { - purgePatternModifications(tripScheduleRelationship, tripId, serviceDate); + purgePatternModifications(scheduleRelationship, tripId, serviceDate); } uIndex += 1; @@ -213,7 +218,7 @@ public UpdateResult applyTripUpdates( Result result; try { result = - switch (tripScheduleRelationship) { + switch (scheduleRelationship) { case SCHEDULED -> handleScheduledTrip( tripUpdate, tripId, @@ -255,11 +260,11 @@ public UpdateResult applyTripUpdates( if (result.isFailure()) { debug(tripId, "Failed to apply TripUpdate."); LOG.trace(" Contents: {}", tripUpdate); - if (failuresByRelationship.containsKey(tripScheduleRelationship)) { - var c = failuresByRelationship.get(tripScheduleRelationship); - failuresByRelationship.put(tripScheduleRelationship, ++c); + if (failuresByRelationship.containsKey(scheduleRelationship)) { + var c = failuresByRelationship.get(scheduleRelationship); + failuresByRelationship.put(scheduleRelationship, ++c); } else { - failuresByRelationship.put(tripScheduleRelationship, 1); + failuresByRelationship.put(scheduleRelationship, 1); } } } @@ -282,7 +287,7 @@ public UpdateResult applyTripUpdates( * added trip pattern. */ private void purgePatternModifications( - TripDescriptor.ScheduleRelationship tripScheduleRelationship, + ScheduleRelationship tripScheduleRelationship, FeedScopedId tripId, LocalDate serviceDate ) { @@ -290,8 +295,8 @@ private void purgePatternModifications( if ( !isPreviouslyAddedTrip(tripId, pattern, serviceDate) || ( - tripScheduleRelationship != TripDescriptor.ScheduleRelationship.CANCELED && - tripScheduleRelationship != TripDescriptor.ScheduleRelationship.DELETED + tripScheduleRelationship != ScheduleRelationship.CANCELED && + tripScheduleRelationship != ScheduleRelationship.DELETED ) ) { // Remove previous realtime updates for this trip. This is necessary to avoid previous @@ -327,7 +332,7 @@ public TimetableSnapshot getTimetableSnapshot() { private static void logUpdateResult( String feedId, - Map failuresByRelationship, + Map failuresByRelationship, UpdateResult updateResult ) { ResultLogger.logUpdateResult(feedId, "gtfs-rt-trip-updates", updateResult); @@ -345,27 +350,6 @@ private static void logUpdateResult( }); } - /** - * Determine how the trip update should be handled. - * - * @param tripDescriptor trip descriptor - * @return TripDescriptor.ScheduleRelationship indicating how the trip update should be handled - */ - private TripDescriptor.ScheduleRelationship determineTripScheduleRelationship( - final TripDescriptor tripDescriptor - ) { - // Assume default value - TripDescriptor.ScheduleRelationship tripScheduleRelationship = - TripDescriptor.ScheduleRelationship.SCHEDULED; - - // If trip update contains schedule relationship, use it - if (tripDescriptor.hasScheduleRelationship()) { - tripScheduleRelationship = tripDescriptor.getScheduleRelationship(); - } - - return tripScheduleRelationship; - } - private Result handleScheduledTrip( TripUpdate tripUpdate, FeedScopedId tripId, diff --git a/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java b/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java index 745418f5132..f1355ca0fa4 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java @@ -3,13 +3,13 @@ import com.google.transit.realtime.GtfsRealtime.VehiclePosition; import java.time.LocalDate; import java.util.List; -import java.util.Optional; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.updater.GtfsRealtimeFuzzyTripMatcher; import org.opentripplanner.updater.spi.PollingGraphUpdater; import org.opentripplanner.updater.spi.WriteToGraphCallback; @@ -45,18 +45,20 @@ public PollingVehiclePositionUpdater( super(params); this.vehiclePositionSource = new GtfsRealtimeHttpVehiclePositionSource(params.url(), params.headers()); - var index = transitModel.getTransitModelIndex(); + // TODO Inject TransitService, do not create it here. We currently do not + // support dagger injection in updaters, so this is ok for now. + TransitService transitService = new DefaultTransitService(transitModel); var fuzzyTripMatcher = params.fuzzyTripMatching() - ? new GtfsRealtimeFuzzyTripMatcher(new DefaultTransitService(transitModel)) + ? new GtfsRealtimeFuzzyTripMatcher(transitService) : null; this.realtimeVehiclePatternMatcher = new RealtimeVehiclePatternMatcher( params.feedId(), - tripId -> index.getTripForId().get(tripId), - trip -> index.getPatternForTrip().get(trip), + transitService::getTripForId, + transitService::getPatternForTrip, (trip, date) -> getPatternIncludingRealtime(transitModel, trip, date), realtimeVehicleRepository, - transitModel.getTimeZone(), + transitService.getTimeZone(), fuzzyTripMatcher, params.vehiclePositionFeatures() ); @@ -99,9 +101,8 @@ private static TripPattern getPatternIncludingRealtime( Trip trip, LocalDate sd ) { - return Optional - .ofNullable(transitModel.getTimetableSnapshot()) - .map(snapshot -> snapshot.getRealtimeAddedTripPattern(trip.getId(), sd)) - .orElseGet(() -> transitModel.getTransitModelIndex().getPatternForTrip().get(trip)); + // a new instance of DefaultTransitService must be created to retrieve + // the current TimetableSnapshot + return (new DefaultTransitService(transitModel)).getPatternForTrip(trip, sd); } } diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsGeofencingZoneMapper.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsGeofencingZoneMapper.java index caf5d97c0d4..22a4131f338 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsGeofencingZoneMapper.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsGeofencingZoneMapper.java @@ -10,6 +10,7 @@ import org.mobilitydata.gbfs.v2_3.geofencing_zones.GBFSGeofencingZones; import org.opentripplanner.framework.geometry.GeometryUtils; import org.opentripplanner.framework.geometry.UnsupportedGeometryException; +import org.opentripplanner.framework.lang.StringUtils; import org.opentripplanner.service.vehiclerental.model.GeofencingZone; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.slf4j.Logger; @@ -52,6 +53,9 @@ private GeofencingZone toInternalModel(GBFSFeature f) { return null; } var name = Objects.requireNonNullElseGet(f.getProperties().getName(), () -> fallbackId(g)); + if (!StringUtils.hasValue(name)) { + name = fallbackId(g); + } var dropOffBanned = !f.getProperties().getRules().get(0).getRideAllowed(); var passThroughBanned = !f.getProperties().getRules().get(0).getRideThroughAllowed(); return new GeofencingZone( diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 57fe17a55ab..6e1195f5901 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -1183,6 +1183,8 @@ type QueryType { nearest places related to bicycling. """ filterByModes: [Mode], + "Only include vehicle rental networks that match one of the given network ids." + filterByNetwork: [String!], "Only return places that are one of these types, e.g. `STOP` or `VEHICLE_RENT`" filterByPlaceTypes: [FilterPlaceType], first: Int, @@ -1739,9 +1741,11 @@ type RentalVehicle implements Node & PlaceInterface { "Name of the vehicle" name: String! "ID of the rental network." - network: String + network: String @deprecated(reason : "Use `networkId` from `rentalNetwork` instead.") "If true, vehicle is not disabled." operative: Boolean + "The vehicle rental network information. This is referred as system in the GBFS terminology." + rentalNetwork: VehicleRentalNetwork! "Platform-specific URLs to begin the vehicle." rentalUris: VehicleRentalUris "ID of the vehicle in the format of network:id" @@ -2413,6 +2417,21 @@ type VehiclePosition { vehicleId: String } +""" +Vehicle rental network, which is referred as system in the GBFS terminology. Note, the same operator can operate in multiple +regions either with the same network/system or with a different one. This can contain information about either the rental brand +or about the operator. +""" +type VehicleRentalNetwork { + """ + ID of the vehicle rental network. In GBFS, this is the `system_id` field from the system information, but it can + be overridden in the configuration to have a different value so this field doesn't necessarily match the source data. + """ + networkId: String! + "The rental vehicle operator's network/system URL. In GBFS, this is the `url` field from the system information." + url: String +} + "Vehicle rental station represents a location where users can rent bicycles etc. for a fee." type VehicleRentalStation implements Node & PlaceInterface { """ @@ -2443,7 +2462,7 @@ type VehicleRentalStation implements Node & PlaceInterface { "Name of the vehicle rental station" name: String! "ID of the rental network." - network: String + network: String @deprecated(reason : "Use `networkId` from `rentalNetwork` instead.") "If true, station is on and in service." operative: Boolean """ @@ -2452,6 +2471,8 @@ type VehicleRentalStation implements Node & PlaceInterface { are always the total capacity divided by two. """ realtime: Boolean + "The vehicle rental network information. This is referred as system in the GBFS terminology." + rentalNetwork: VehicleRentalNetwork! "Platform-specific URLs to begin renting a vehicle from this station." rentalUris: VehicleRentalUris """ diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index 5c1912df4e6..64440780f78 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -1167,6 +1167,8 @@ type StopPlace implements PlaceInterface { ): [Quay] @timingData "Get all situations active for the stop place. Situations affecting individual quays are not returned, and should be fetched directly from the quay." situations: [PtSituationElement!]! + "Specify the priority of interchanges at this stop" + stopInterchangePriority: StopInterchangePriority tariffZones: [TariffZone]! timeZone: String "The transport modes of quays under this stop place." @@ -1174,7 +1176,7 @@ type StopPlace implements PlaceInterface { "The transport submode serviced by this stop place." transportSubmode: [TransportSubmode] "Relative weighting of this stop with regards to interchanges. NOT IMPLEMENTED" - weighting: InterchangeWeighting + weighting: InterchangeWeighting @deprecated(reason : "Not implemented. Use stopInterchangePriority") } "List of coordinates between two stops as a polyline" @@ -1531,6 +1533,7 @@ enum InterchangePriority { recommended } +"Deprecated. Use STOP_INTERCHANGE_PRIORITY" enum InterchangeWeighting { "Third highest priority interchange." interchangeAllowed @@ -1746,6 +1749,17 @@ enum StopCondition { startPoint } +enum StopInterchangePriority { + "Allow transfers from/to this stop. This is the default. NeTEx equivalent is INTERCHANGE_ALLOWED." + allowed + "Block transfers from/to this stop. In OTP this is not a definitive block, just a huge penalty is added to the cost function. NeTEx equivalent is NO_INTERCHANGE." + discouraged + "Preferred place to transfer, strongly recommended. NeTEx equivalent is PREFERRED_INTERCHANGE." + preferred + "Recommended stop place. NeTEx equivalent is RECOMMENDED_INTERCHANGE." + recommended +} + enum StreetMode { "Bike only. This can be used as access/egress, but transfers will still be walk only." bicycle diff --git a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index 614c8778c6b..79590ca2775 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -73,8 +73,10 @@ import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; +import org.opentripplanner.service.vehiclerental.model.TestFreeFloatingRentalVehicleBuilder; import org.opentripplanner.service.vehiclerental.model.TestVehicleRentalStationBuilder; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; +import org.opentripplanner.service.vehiclerental.model.VehicleRentalVehicle; import org.opentripplanner.standalone.config.framework.json.JsonSupport; import org.opentripplanner.test.support.FilePatternSource; import org.opentripplanner.transit.model._data.TransitModelForTest; @@ -91,6 +93,7 @@ import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.opentripplanner.transit.service.DefaultTransitService; +import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.transit.service.TransitModel; import org.opentripplanner.transit.service.TransitService; @@ -112,6 +115,18 @@ class GraphQLIntegrationTest { .map(p -> (RegularStop) p.stop) .toList(); + private static VehicleRentalStation VEHICLE_RENTAL_STATION = new TestVehicleRentalStationBuilder() + .withVehicles(10) + .withSpaces(10) + .withVehicleTypeBicycle(5, 7) + .withVehicleTypeElectricBicycle(5, 3) + .withSystem("Network-1", "https://foo.bar") + .build(); + + private static VehicleRentalVehicle RENTAL_VEHICLE = new TestFreeFloatingRentalVehicleBuilder() + .withSystem("Network-1", "https://foo.bar") + .build(); + static final Graph GRAPH = new Graph(); static final Instant ALERT_START_TIME = OffsetDateTime @@ -182,8 +197,20 @@ static void setup() { .toList(); var busRoute = routes.stream().filter(r -> r.getMode().equals(BUS)).findFirst().get(); + TransitEditorService transitService = new DefaultTransitService(transitModel) { + private final TransitAlertService alertService = new TransitAlertServiceImpl(transitModel); + + @Override + public List getModesOfStopLocation(StopLocation stop) { + return List.of(BUS, FERRY); + } - routes.forEach(route -> transitModel.getTransitModelIndex().addRoutes(route)); + @Override + public TransitAlertService getTransitAlertService() { + return alertService; + } + }; + routes.forEach(transitService::addRoutes); var step1 = walkStep("street") .withRelativeDirection(RelativeDirection.DEPART) @@ -240,20 +267,6 @@ static void setup() { var emissions = new Emissions(new Grams(123.0)); i1.setEmissionsPerPerson(emissions); - var transitService = new DefaultTransitService(transitModel) { - private final TransitAlertService alertService = new TransitAlertServiceImpl(transitModel); - - @Override - public List getModesOfStopLocation(StopLocation stop) { - return List.of(BUS, FERRY); - } - - @Override - public TransitAlertService getTransitAlertService() { - return alertService; - } - }; - var alerts = ListUtils.combine(List.of(alert), getTransitAlert(entitySelector)); transitService.getTransitAlertService().setAlerts(alerts); @@ -280,13 +293,8 @@ public TransitAlertService getTransitAlertService() { realtimeVehicleService.setRealtimeVehicles(pattern, List.of(occypancyVehicle, positionVehicle)); DefaultVehicleRentalService defaultVehicleRentalService = new DefaultVehicleRentalService(); - VehicleRentalStation vehicleRentalStation = new TestVehicleRentalStationBuilder() - .withVehicles(10) - .withSpaces(10) - .withVehicleTypeBicycle(5, 7) - .withVehicleTypeElectricBicycle(5, 3) - .build(); - defaultVehicleRentalService.addVehicleRentalStation(vehicleRentalStation); + defaultVehicleRentalService.addVehicleRentalStation(VEHICLE_RENTAL_STATION); + defaultVehicleRentalService.addVehicleRentalStation(RENTAL_VEHICLE); context = new GraphQLRequestContext( @@ -448,13 +456,15 @@ public List findClosestPlaces( List filterByStations, List filterByRoutes, List filterByBikeRentalStations, + List filterByNetwork, TransitService transitService ) { - return List - .of(TransitModelForTest.of().stop("A").build()) - .stream() - .map(stop -> new PlaceAtDistance(stop, 0)) - .toList(); + var stop = TransitModelForTest.of().stop("A").build(); + return List.of( + new PlaceAtDistance(stop, 0), + new PlaceAtDistance(VEHICLE_RENTAL_STATION, 30), + new PlaceAtDistance(RENTAL_VEHICLE, 50) + ); } }; } diff --git a/src/test/java/org/opentripplanner/generate/doc/OsmMapperDocTest.java b/src/test/java/org/opentripplanner/generate/doc/OsmMapperDocTest.java new file mode 100644 index 00000000000..d3eb0a44122 --- /dev/null +++ b/src/test/java/org/opentripplanner/generate/doc/OsmMapperDocTest.java @@ -0,0 +1,115 @@ +package org.opentripplanner.generate.doc; + +import static org.opentripplanner.framework.io.FileUtils.assertFileEquals; +import static org.opentripplanner.framework.io.FileUtils.readFile; +import static org.opentripplanner.framework.io.FileUtils.writeFile; +import static org.opentripplanner.generate.doc.framework.DocsTestConstants.TEMPLATE_PATH; +import static org.opentripplanner.generate.doc.framework.DocsTestConstants.USER_DOC_PATH; +import static org.opentripplanner.generate.doc.framework.TemplateUtil.replaceSection; +import static org.opentripplanner.openstreetmap.tagmapping.OsmTagMapperSource.ATLANTA; +import static org.opentripplanner.openstreetmap.tagmapping.OsmTagMapperSource.CONSTANT_SPEED_FINLAND; +import static org.opentripplanner.openstreetmap.tagmapping.OsmTagMapperSource.HAMBURG; +import static org.opentripplanner.openstreetmap.tagmapping.OsmTagMapperSource.HOUSTON; +import static org.opentripplanner.openstreetmap.tagmapping.OsmTagMapperSource.PORTLAND; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.framework.text.Table; +import org.opentripplanner.framework.text.TableBuilder; +import org.opentripplanner.generate.doc.framework.DocsTestConstants; +import org.opentripplanner.generate.doc.framework.GeneratesDocumentation; +import org.opentripplanner.openstreetmap.tagmapping.OsmTagMapper; +import org.opentripplanner.openstreetmap.tagmapping.OsmTagMapperSource; +import org.opentripplanner.openstreetmap.wayproperty.SafetyFeatures; +import org.opentripplanner.openstreetmap.wayproperty.WayPropertySet; + +@GeneratesDocumentation +public class OsmMapperDocTest { + + private static final String FILE_NAME = "OsmMapper.md"; + private static final File TEMPLATE = new File(TEMPLATE_PATH, FILE_NAME); + private static final Set SKIP_MAPPERS = Set.of( + ATLANTA, + HOUSTON, + PORTLAND, + HAMBURG, + CONSTANT_SPEED_FINLAND + ); + + public static List mappers() { + return Arrays + .stream(OsmTagMapperSource.values()) + .filter(m -> !SKIP_MAPPERS.contains(m)) + .toList(); + } + + @ParameterizedTest + @MethodSource("mappers") + public void updateDocs(OsmTagMapperSource source) { + var mapper = source.getInstance(); + var wps = new WayPropertySet(); + mapper.populateProperties(wps); + + var outFile = outputFile(mapper); + + // Read and close input file (same as output file) + String template = readFile(TEMPLATE); + String original = readFile(outFile); + + var propTable = propTable(wps); + var mixinTable = mixinTable(wps); + + template = replaceSection(template, "props", propTable.toMarkdownTable()); + template = replaceSection(template, "mixins", mixinTable.toMarkdownTable()); + writeFile(outFile, template); + assertFileEquals(original, outFile); + } + + private static File outputFile(OsmTagMapper mapper) { + var name = mapper.getClass().getSimpleName().replaceAll("Mapper", ".md"); + return new File("%s/osm/".formatted(USER_DOC_PATH), name); + } + + private static Table propTable(WayPropertySet wps) { + var propTable = new TableBuilder(); + propTable.withHeaders("specifier", "permission", "bike safety", "walk safety"); + + for (var prop : wps.getWayProperties()) { + propTable.addRow( + "`%s`".formatted(prop.specifier().toDocString()), + "`%s`".formatted(prop.properties().getPermission()), + tableValues(prop.properties().bicycleSafety()), + tableValues(prop.properties().walkSafety()) + ); + } + return propTable.build(); + } + + private static Table mixinTable(WayPropertySet wps) { + var propTable = new TableBuilder(); + propTable.withHeaders("matcher", "bicycle safety", "walk safety"); + + for (var prop : wps.getMixins()) { + propTable.addRow( + "`%s`".formatted(prop.specifier().toDocString()), + tableValues(prop.bicycleSafety()), + tableValues(prop.walkSafety()) + ); + } + return propTable.build(); + } + + private static String tableValues(SafetyFeatures safety) { + if (!safety.modifies()) { + return ""; + } else if (safety.isSymmetric()) { + return Double.toString(safety.forward()); + } else { + return "forward: %s
back: %s".formatted(safety.forward(), safety.back()); + } + } +} diff --git a/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java b/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java index 67ca61f0403..a1aa4f9d753 100644 --- a/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java +++ b/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java @@ -125,9 +125,8 @@ void testInterline( void staySeatedNotAllowed() { var transferService = new DefaultTransferService(); - var fromTrip = patterns.get(0).getTrip(0); - var toTrip = patterns.get(1).getTrip(0); - + var fromTrip = patterns.get(0).getScheduledTimetable().getTripTimes().get(0).getTrip(); + var toTrip = patterns.get(1).getScheduledTimetable().getTripTimes().get(0).getTrip(); var notAllowed = new StaySeatedNotAllowed(fromTrip, toTrip); var calendarService = new CalendarServiceData(); diff --git a/src/test/java/org/opentripplanner/model/TimetableSnapshotTest.java b/src/test/java/org/opentripplanner/model/TimetableSnapshotTest.java index 26d93f6a848..89c30da79e4 100644 --- a/src/test/java/org/opentripplanner/model/TimetableSnapshotTest.java +++ b/src/test/java/org/opentripplanner/model/TimetableSnapshotTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.transit.realtime.GtfsRealtime.TripDescriptor; @@ -261,6 +262,52 @@ public void testPurge() { assertFalse(resolver.isDirty()); } + @Test + void testCannotUpdateReadOnlyTimetableSnapshot() { + TimetableSnapshot committedSnapshot = createCommittedSnapshot(); + LocalDate today = LocalDate.now(timeZone); + TripPattern pattern = patternIndex.get(new FeedScopedId(feedId, "1.1")); + assertThrows( + ConcurrentModificationException.class, + () -> committedSnapshot.update(pattern, null, today) + ); + } + + @Test + void testCannotCommitReadOnlyTimetableSnapshot() { + TimetableSnapshot committedSnapshot = createCommittedSnapshot(); + assertThrows(ConcurrentModificationException.class, () -> committedSnapshot.commit(null, true)); + } + + @Test + void testCannotClearReadOnlyTimetableSnapshot() { + TimetableSnapshot committedSnapshot = createCommittedSnapshot(); + assertThrows(ConcurrentModificationException.class, () -> committedSnapshot.clear(null)); + } + + @Test + void testCannotPurgeReadOnlyTimetableSnapshot() { + TimetableSnapshot committedSnapshot = createCommittedSnapshot(); + assertThrows( + ConcurrentModificationException.class, + () -> committedSnapshot.purgeExpiredData(null) + ); + } + + @Test + void testCannotRevertReadOnlyTimetableSnapshot() { + TimetableSnapshot committedSnapshot = createCommittedSnapshot(); + assertThrows( + ConcurrentModificationException.class, + () -> committedSnapshot.revertTripToScheduledTripPattern(null, null) + ); + } + + private static TimetableSnapshot createCommittedSnapshot() { + TimetableSnapshot timetableSnapshot = new TimetableSnapshot(); + return timetableSnapshot.commit(null, true); + } + private Result updateResolver( TimetableSnapshot resolver, TripPattern pattern, diff --git a/src/test/java/org/opentripplanner/netex/mapping/MultiModalStationMapperTest.java b/src/test/java/org/opentripplanner/netex/mapping/MultiModalStationMapperTest.java new file mode 100644 index 00000000000..218942d29ef --- /dev/null +++ b/src/test/java/org/opentripplanner/netex/mapping/MultiModalStationMapperTest.java @@ -0,0 +1,33 @@ +package org.opentripplanner.netex.mapping; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.graph_builder.issue.service.DefaultDataImportIssueStore; +import org.opentripplanner.netex.NetexTestDataSupport; +import org.opentripplanner.netex.mapping.support.FeedScopedIdFactory; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.rutebanken.netex.model.StopPlace; + +class MultiModalStationMapperTest { + + @Test + void testMissingCoordinates() { + DataImportIssueStore dataIssueStore = new DefaultDataImportIssueStore(); + FeedScopedIdFactory feedScopeIdFactory = new FeedScopedIdFactory(TransitModelForTest.FEED_ID); + MultiModalStationMapper multiModalStationMapper = new MultiModalStationMapper( + dataIssueStore, + feedScopeIdFactory + ); + StopPlace stopPlace = new StopPlace(); + stopPlace.setId(NetexTestDataSupport.STOP_PLACE_ID); + assertNull(multiModalStationMapper.map(stopPlace, List.of())); + assertEquals(1, dataIssueStore.listIssues().size()); + assertEquals( + "MultiModalStationWithoutCoordinates", + dataIssueStore.listIssues().getFirst().getType() + ); + } +} diff --git a/src/test/java/org/opentripplanner/openstreetmap/wayproperty/specifier/ConditionTest.java b/src/test/java/org/opentripplanner/openstreetmap/wayproperty/specifier/ConditionTest.java index def272652f6..6386aa902e2 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/wayproperty/specifier/ConditionTest.java +++ b/src/test/java/org/opentripplanner/openstreetmap/wayproperty/specifier/ConditionTest.java @@ -29,11 +29,11 @@ import org.opentripplanner.openstreetmap.model.OSMWithTags; import org.opentripplanner.openstreetmap.wayproperty.specifier.Condition.Absent; import org.opentripplanner.openstreetmap.wayproperty.specifier.Condition.Equals; -import org.opentripplanner.openstreetmap.wayproperty.specifier.Condition.EqualsAnyIn; import org.opentripplanner.openstreetmap.wayproperty.specifier.Condition.GreaterThan; import org.opentripplanner.openstreetmap.wayproperty.specifier.Condition.InclusiveRange; import org.opentripplanner.openstreetmap.wayproperty.specifier.Condition.LessThan; import org.opentripplanner.openstreetmap.wayproperty.specifier.Condition.MatchResult; +import org.opentripplanner.openstreetmap.wayproperty.specifier.Condition.OneOf; import org.opentripplanner.openstreetmap.wayproperty.specifier.Condition.Present; class ConditionTest { @@ -46,7 +46,7 @@ class ConditionTest { static Condition moreThanFourLanes = new GreaterThan("lanes", 4); static Condition lessThanFourLanes = new LessThan("lanes", 4); static Condition betweenFiveAndThreeLanes = new InclusiveRange("lanes", 5, 3); - static Condition smoothnessBadAndWorseThanBad = new EqualsAnyIn( + static Condition smoothnessBadAndWorseThanBad = new OneOf( "smoothness", "bad", "very_bad", @@ -54,7 +54,7 @@ class ConditionTest { "very_horrible", "impassable" ); - static Condition noSidewalk = new Condition.EqualsAnyInOrAbsent("sidewalk"); + static Condition noSidewalk = new Condition.OneOfOrAbsent("sidewalk"); static Stream equalsCases() { return Stream.of( diff --git a/src/test/java/org/opentripplanner/raptor/moduletests/K01_TransitPriorityTest.java b/src/test/java/org/opentripplanner/raptor/moduletests/K01_TransitPriorityTest.java index 00b1d528e7e..f03eb5335a0 100644 --- a/src/test/java/org/opentripplanner/raptor/moduletests/K01_TransitPriorityTest.java +++ b/src/test/java/org/opentripplanner/raptor/moduletests/K01_TransitPriorityTest.java @@ -22,7 +22,7 @@ import org.opentripplanner.raptor._data.transit.TestTripSchedule; import org.opentripplanner.raptor.api.request.RaptorProfile; import org.opentripplanner.raptor.api.request.RaptorRequestBuilder; -import org.opentripplanner.raptor.api.request.RaptorTransitGroupCalculator; +import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.raptor.moduletests.support.TestGroupPriorityCalculator; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter; @@ -34,7 +34,7 @@ */ public class K01_TransitPriorityTest { - private static final RaptorTransitGroupCalculator PRIORITY_GROUP_CALCULATOR = + private static final RaptorTransitGroupPriorityCalculator PRIORITY_GROUP_CALCULATOR = TestGroupPriorityCalculator.PRIORITY_CALCULATOR; private static final int C1_SLACK_90s = RaptorCostConverter.toRaptorCost(90); diff --git a/src/test/java/org/opentripplanner/raptor/moduletests/K02_TransitPriorityDestinationTest.java b/src/test/java/org/opentripplanner/raptor/moduletests/K02_TransitPriorityDestinationTest.java index c6ab8e337ea..fb21128728c 100644 --- a/src/test/java/org/opentripplanner/raptor/moduletests/K02_TransitPriorityDestinationTest.java +++ b/src/test/java/org/opentripplanner/raptor/moduletests/K02_TransitPriorityDestinationTest.java @@ -25,7 +25,7 @@ import org.opentripplanner.raptor._data.transit.TestTripSchedule; import org.opentripplanner.raptor.api.request.RaptorProfile; import org.opentripplanner.raptor.api.request.RaptorRequestBuilder; -import org.opentripplanner.raptor.api.request.RaptorTransitGroupCalculator; +import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.raptor.moduletests.support.TestGroupPriorityCalculator; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter; @@ -37,7 +37,7 @@ */ public class K02_TransitPriorityDestinationTest { - private static final RaptorTransitGroupCalculator PRIORITY_GROUP_CALCULATOR = + private static final RaptorTransitGroupPriorityCalculator PRIORITY_GROUP_CALCULATOR = TestGroupPriorityCalculator.PRIORITY_CALCULATOR; private static final int C1_SLACK_90s = RaptorCostConverter.toRaptorCost(90); diff --git a/src/test/java/org/opentripplanner/raptor/moduletests/support/TestGroupPriorityCalculator.java b/src/test/java/org/opentripplanner/raptor/moduletests/support/TestGroupPriorityCalculator.java index cdbe82f18a6..3234dc126fb 100644 --- a/src/test/java/org/opentripplanner/raptor/moduletests/support/TestGroupPriorityCalculator.java +++ b/src/test/java/org/opentripplanner/raptor/moduletests/support/TestGroupPriorityCalculator.java @@ -5,11 +5,11 @@ import org.junit.jupiter.api.Test; import org.opentripplanner.raptor.api.model.DominanceFunction; -import org.opentripplanner.raptor.api.request.RaptorTransitGroupCalculator; +import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; -public class TestGroupPriorityCalculator implements RaptorTransitGroupCalculator { +public class TestGroupPriorityCalculator implements RaptorTransitGroupPriorityCalculator { - public static final RaptorTransitGroupCalculator PRIORITY_CALCULATOR = new TestGroupPriorityCalculator(); + public static final RaptorTransitGroupPriorityCalculator PRIORITY_CALCULATOR = new TestGroupPriorityCalculator(); public static final int GROUP_A = 0x01; public static final int GROUP_B = 0x02; diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/grouppriority/TransitGroupPriority32nTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/grouppriority/TransitGroupPriority32nTest.java deleted file mode 100644 index 2713a190dbf..00000000000 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/grouppriority/TransitGroupPriority32nTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.grouppriority; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; -import org.opentripplanner.raptor.api.request.RaptorTransitGroupCalculator; - -class TransitGroupPriority32nTest { - - private static final int GROUP_INDEX_0 = 0; - private static final int GROUP_INDEX_1 = 1; - private static final int GROUP_INDEX_2 = 2; - private static final int GROUP_INDEX_30 = 30; - private static final int GROUP_INDEX_31 = 31; - - private static final int GROUP_0 = TransitGroupPriority32n.groupId(GROUP_INDEX_0); - private static final int GROUP_1 = TransitGroupPriority32n.groupId(GROUP_INDEX_1); - private static final int GROUP_2 = TransitGroupPriority32n.groupId(GROUP_INDEX_2); - private static final int GROUP_30 = TransitGroupPriority32n.groupId(GROUP_INDEX_30); - private static final int GROUP_31 = TransitGroupPriority32n.groupId(GROUP_INDEX_31); - private static final RaptorTransitGroupCalculator subjct = TransitGroupPriority32n.priorityCalculator(); - - @Test - void groupId() { - assertEqualsHex(0x00_00_00_00, TransitGroupPriority32n.groupId(0)); - assertEqualsHex(0x00_00_00_01, TransitGroupPriority32n.groupId(1)); - assertEqualsHex(0x00_00_00_02, TransitGroupPriority32n.groupId(2)); - assertEqualsHex(0x00_00_00_04, TransitGroupPriority32n.groupId(3)); - assertEqualsHex(0x40_00_00_00, TransitGroupPriority32n.groupId(31)); - assertEqualsHex(0x80_00_00_00, TransitGroupPriority32n.groupId(32)); - - assertThrows(IllegalArgumentException.class, () -> TransitGroupPriority32n.groupId(-1)); - assertThrows(IllegalArgumentException.class, () -> TransitGroupPriority32n.groupId(33)); - } - - @Test - void mergeTransitGroupPriorityIds() { - assertEqualsHex(GROUP_0, subjct.mergeGroupIds(GROUP_0, GROUP_0)); - assertEqualsHex(GROUP_1, subjct.mergeGroupIds(GROUP_1, GROUP_1)); - assertEqualsHex(GROUP_0 | GROUP_1, subjct.mergeGroupIds(GROUP_0, GROUP_1)); - assertEqualsHex(GROUP_30 | GROUP_31, subjct.mergeGroupIds(GROUP_30, GROUP_31)); - assertEqualsHex( - GROUP_0 | GROUP_1 | GROUP_2 | GROUP_30 | GROUP_31, - subjct.mergeGroupIds(GROUP_0 | GROUP_1 | GROUP_2 | GROUP_30, GROUP_31) - ); - } - - @Test - void dominanceFunction() { - assertFalse(subjct.dominanceFunction().leftDominateRight(GROUP_0, GROUP_0)); - assertFalse(subjct.dominanceFunction().leftDominateRight(GROUP_31, GROUP_31)); - assertFalse(subjct.dominanceFunction().leftDominateRight(GROUP_1 | GROUP_2, GROUP_1 | GROUP_2)); - - assertTrue(subjct.dominanceFunction().leftDominateRight(GROUP_0, GROUP_1)); - assertFalse(subjct.dominanceFunction().leftDominateRight(GROUP_1, GROUP_0)); - - assertTrue(subjct.dominanceFunction().leftDominateRight(GROUP_1, GROUP_1 | GROUP_2)); - assertFalse(subjct.dominanceFunction().leftDominateRight(GROUP_1 | GROUP_2, GROUP_1)); - } - - static void assertEqualsHex(int expected, int actual) { - assertEquals(expected, actual, "%08x == %08x".formatted(expected, actual)); - } -} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreatorTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreatorTest.java index ed71b3de400..ea815a2f47f 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreatorTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreatorTest.java @@ -19,6 +19,7 @@ import org.opentripplanner.transit.model.network.RoutingTripPattern; import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; import org.opentripplanner.transit.model.timetable.TripTimes; @@ -60,12 +61,13 @@ public void testMergeTripPatterns() { tripPatternsForDates.add(new TripPatternForDate(tripPattern1, tripTimes, List.of(), third)); tripPatternsForDates.add(new TripPatternForDate(tripPattern3, tripTimes, List.of(), third)); - // Patterns containing trip schedules for all 3 days. Trip schedules for later days are offset in time when requested. + // Patterns containing trip schedules for all 3 days. Trip schedules for later days are offset + // in time when requested. List combinedTripPatterns = RaptorRoutingRequestTransitDataCreator.merge( startOfTime, tripPatternsForDates, new TestTransitDataProviderFilter(), - PriorityGroupConfigurator.empty() + TransitGroupPriorityService.empty() ); // Get the results diff --git a/src/test/java/org/opentripplanner/routing/api/request/framework/LinearFunctionSerializationTest.java b/src/test/java/org/opentripplanner/routing/api/request/framework/LinearFunctionSerializationTest.java index e1785db61b1..46ef4b50e77 100644 --- a/src/test/java/org/opentripplanner/routing/api/request/framework/LinearFunctionSerializationTest.java +++ b/src/test/java/org/opentripplanner/routing/api/request/framework/LinearFunctionSerializationTest.java @@ -36,6 +36,8 @@ static Stream parseTestCases() { 3h + 5.111 t || 3h | 5.1 7m + 10.1 x || 7m | 10.0 PT7s + 10.1 x || 7s | 10.0 + 0.1 + 10.1 x || 0s | 10.0 + 0.5 + 10.1 x || 1s | 10.0 """ ); } @@ -53,7 +55,7 @@ void parseTest(String input, String expectedConstant, double expectedCoefficient } @Test - void parseEmtpy() { + void parseEmpty() { assertEquals(Optional.empty(), LinearFunctionSerialization.parse(null, fail())); assertEquals(Optional.empty(), LinearFunctionSerialization.parse("", fail())); assertEquals(Optional.empty(), LinearFunctionSerialization.parse(" \r\n", fail())); @@ -77,6 +79,15 @@ void parseIllegalArgument() { assertEquals("Unable to parse function: 'foo'", ex.getMessage()); } + @Test + void parseIllegalDuration() { + var ex = assertThrows( + IllegalArgumentException.class, + () -> LinearFunctionSerialization.parse("600ss + 1.3 t", fail()) + ); + assertEquals("Unable to parse duration: '600ss'", ex.getMessage()); + } + private static BiFunction fail() { return (a, b) -> Assertions.fail("Factory method called, not expected!"); } diff --git a/src/test/java/org/opentripplanner/routing/graph/DefaultRoutingServiceTest.java b/src/test/java/org/opentripplanner/routing/graph/DefaultRoutingServiceTest.java index e9d286d5a92..8c56bb89a1f 100644 --- a/src/test/java/org/opentripplanner/routing/graph/DefaultRoutingServiceTest.java +++ b/src/test/java/org/opentripplanner/routing/graph/DefaultRoutingServiceTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Collection; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; @@ -18,6 +19,8 @@ import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.service.DefaultTransitService; +import org.opentripplanner.transit.service.TransitService; /** * Check that the graph index is created, that GTFS elements can be found in the index, and that the @@ -27,6 +30,15 @@ */ public class DefaultRoutingServiceTest extends GtfsTest { + private TransitService transitService; + + @BeforeEach + @Override + public void setUp() throws Exception { + super.setUp(); + transitService = new DefaultTransitService(transitModel); + } + @Override public String getFeedName() { return "gtfs/simple"; @@ -44,16 +56,16 @@ public void testIdLookup() { } /* Agencies */ - String feedId = transitModel.getFeedIds().iterator().next(); + String feedId = transitService.getFeedIds().iterator().next(); Agency agency; - agency = transitModel.getTransitModelIndex().getAgencyForId(new FeedScopedId(feedId, "azerty")); + agency = transitService.getAgencyForId(new FeedScopedId(feedId, "azerty")); assertNull(agency); - agency = transitModel.getTransitModelIndex().getAgencyForId(new FeedScopedId(feedId, "agency")); + agency = transitService.getAgencyForId(new FeedScopedId(feedId, "agency")); assertEquals(feedId + ":" + "agency", agency.getId().toString()); assertEquals("Fake Agency", agency.getName()); /* Stops */ - transitModel.getStopModel().getRegularStop(new FeedScopedId("X", "Y")); + transitService.getRegularStop(new FeedScopedId("X", "Y")); /* Trips */ // graph.index.tripForId; // graph.index.routeForId; @@ -67,21 +79,18 @@ public void testIdLookup() { */ @Test public void testPatternsCoherent() { - for (Trip trip : transitModel.getTransitModelIndex().getTripForId().values()) { - TripPattern pattern = transitModel.getTransitModelIndex().getPatternForTrip().get(trip); + for (Trip trip : transitService.getAllTrips()) { + TripPattern pattern = transitService.getPatternForTrip(trip); assertTrue(pattern.scheduledTripsAsStream().anyMatch(t -> t.equals(trip))); } /* This one depends on a feed where each TripPattern appears on only one route. */ - for (Route route : transitModel.getTransitModelIndex().getAllRoutes()) { - for (TripPattern pattern : transitModel - .getTransitModelIndex() - .getPatternsForRoute() - .get(route)) { + for (Route route : transitService.getAllRoutes()) { + for (TripPattern pattern : transitService.getPatternsForRoute(route)) { assertEquals(pattern.getRoute(), route); } } - for (var stop : transitModel.getStopModel().listStopLocations()) { - for (TripPattern pattern : transitModel.getTransitModelIndex().getPatternsForStop(stop)) { + for (var stop : transitService.listStopLocations()) { + for (TripPattern pattern : transitService.getPatternsForStop(stop)) { int stopPos = pattern.findStopPosition(stop); assertTrue(stopPos >= 0, "Stop position exist"); } @@ -90,13 +99,13 @@ public void testPatternsCoherent() { @Test public void testSpatialIndex() { - String feedId = transitModel.getFeedIds().iterator().next(); + String feedId = transitService.getFeedIds().iterator().next(); FeedScopedId idJ = new FeedScopedId(feedId, "J"); - var stopJ = transitModel.getStopModel().getRegularStop(idJ); + var stopJ = transitService.getRegularStop(idJ); FeedScopedId idL = new FeedScopedId(feedId, "L"); - var stopL = transitModel.getStopModel().getRegularStop(idL); + var stopL = transitService.getRegularStop(idL); FeedScopedId idM = new FeedScopedId(feedId, "M"); - var stopM = transitModel.getStopModel().getRegularStop(idM); + var stopM = transitService.getRegularStop(idM); TransitStopVertex stopvJ = graph.getStopVertexForStopId(idJ); TransitStopVertex stopvL = graph.getStopVertexForStopId(idL); TransitStopVertex stopvM = graph.getStopVertexForStopId(idM); @@ -106,7 +115,7 @@ public void testSpatialIndex() { SphericalDistanceLibrary.metersToLonDegrees(100, stopJ.getLat()), SphericalDistanceLibrary.metersToDegrees(100) ); - Collection stops = transitModel.getStopModel().findRegularStops(env); + Collection stops = transitService.findRegularStops(env); assertTrue(stops.contains(stopJ)); assertTrue(stops.contains(stopL)); assertTrue(stops.contains(stopM)); diff --git a/src/test/java/org/opentripplanner/routing/graphfinder/PlaceFinderTraverseVisitorTest.java b/src/test/java/org/opentripplanner/routing/graphfinder/PlaceFinderTraverseVisitorTest.java index 1ef675c8101..b69a6533334 100644 --- a/src/test/java/org/opentripplanner/routing/graphfinder/PlaceFinderTraverseVisitorTest.java +++ b/src/test/java/org/opentripplanner/routing/graphfinder/PlaceFinderTraverseVisitorTest.java @@ -13,6 +13,7 @@ import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.model.StopTime; +import org.opentripplanner.service.vehiclerental.model.TestVehicleRentalStationBuilder; import org.opentripplanner.street.search.state.TestStateBuilder; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.basic.TransitMode; @@ -96,6 +97,7 @@ void stopsOnly() { null, null, null, + null, 1, 500 ); @@ -124,6 +126,7 @@ void stationsOnly() { null, null, null, + null, 1, 500 ); @@ -152,6 +155,7 @@ void stopsAndStations() { null, null, null, + null, 1, 500 ); @@ -183,6 +187,7 @@ void stopsAndStationsWithStationFilter() { List.of(STATION1.getId()), null, null, + null, 1, 500 ); @@ -217,6 +222,7 @@ void stopsAndStationsWithStopFilter() { null, null, null, + null, 1, 500 ); @@ -250,6 +256,7 @@ void stopsAndStationsWithStopAndStationFilter() { List.of(STATION1.getId()), null, null, + null, 1, 500 ); @@ -274,4 +281,74 @@ void stopsAndStationsWithStopAndStationFilter() { visitor.visitVertex(state1); } + + @Test + void rentalStation() { + var visitor = new PlaceFinderTraverseVisitor( + transitService, + null, + List.of(PlaceType.VEHICLE_RENT), + null, + null, + null, + null, + null, + 1, + 500 + ); + var station = new TestVehicleRentalStationBuilder().build(); + assertEquals(List.of(), visitor.placesFound); + var state1 = TestStateBuilder.ofWalking().rentalStation(station).build(); + visitor.visitVertex(state1); + + var res = visitor.placesFound.stream().map(PlaceAtDistance::place).toList(); + + assertEquals(List.of(station), res); + } + + @Test + void rentalStationWithNetworksFilter() { + var visitor = new PlaceFinderTraverseVisitor( + transitService, + null, + List.of(PlaceType.VEHICLE_RENT), + null, + null, + null, + null, + List.of("Network-1"), + 1, + 500 + ); + var station = new TestVehicleRentalStationBuilder().build(); + assertEquals(List.of(), visitor.placesFound); + var state1 = TestStateBuilder.ofWalking().rentalStation(station).build(); + visitor.visitVertex(state1); + + var res = visitor.placesFound.stream().map(PlaceAtDistance::place).toList(); + + assertEquals(List.of(station), res); + + visitor = + new PlaceFinderTraverseVisitor( + transitService, + null, + List.of(PlaceType.VEHICLE_RENT), + null, + null, + null, + null, + List.of("Network-2"), + 1, + 500 + ); + + assertEquals(List.of(), visitor.placesFound); + state1 = TestStateBuilder.ofWalking().rentalStation(station).build(); + visitor.visitVertex(state1); + + res = visitor.placesFound.stream().map(PlaceAtDistance::place).toList(); + + assertEquals(List.of(), res); + } } diff --git a/src/test/java/org/opentripplanner/routing/graphfinder/StreetGraphFinderTest.java b/src/test/java/org/opentripplanner/routing/graphfinder/StreetGraphFinderTest.java index 3285e27594c..76f231577dd 100644 --- a/src/test/java/org/opentripplanner/routing/graphfinder/StreetGraphFinderTest.java +++ b/src/test/java/org/opentripplanner/routing/graphfinder/StreetGraphFinderTest.java @@ -156,6 +156,7 @@ void findClosestPlacesLimiting() { null, null, null, + null, transitService ) ); @@ -179,6 +180,7 @@ void findClosestPlacesLimiting() { null, null, null, + null, transitService ) ); @@ -196,6 +198,7 @@ void findClosestPlacesLimiting() { null, null, null, + null, transitService ) ); @@ -220,6 +223,7 @@ void findClosestPlacesWithAModeFilter() { null, null, null, + null, transitService ) ); @@ -237,6 +241,7 @@ void findClosestPlacesWithAModeFilter() { null, null, null, + null, transitService ) ); @@ -262,6 +267,7 @@ void findClosestPlacesWithAStopFilter() { null, null, null, + null, transitService ) ); @@ -279,6 +285,7 @@ void findClosestPlacesWithAStopFilter() { null, null, null, + null, transitService ) ); @@ -304,6 +311,7 @@ void findClosestPlacesWithAStopAndRouteFilter() { null, null, null, + null, transitService ) ); @@ -321,6 +329,7 @@ void findClosestPlacesWithAStopAndRouteFilter() { null, List.of(R1.getId()), null, + null, transitService ) ); @@ -347,6 +356,7 @@ void findClosestPlacesWithARouteFilter() { null, null, null, + null, transitService ) ); @@ -364,6 +374,7 @@ void findClosestPlacesWithARouteFilter() { null, List.of(R2.getId()), null, + null, transitService ) ); @@ -387,6 +398,7 @@ void findClosestPlacesWithAVehicleRentalFilter() { null, null, null, + null, transitService ) ); @@ -404,6 +416,7 @@ void findClosestPlacesWithAVehicleRentalFilter() { null, null, List.of("BR2"), + null, transitService ) ); @@ -426,6 +439,7 @@ void findClosestPlacesWithABikeParkFilter() { null, null, null, + null, transitService ) ); @@ -448,6 +462,7 @@ void findClosestPlacesWithACarParkFilter() { null, null, null, + null, transitService ) ); diff --git a/src/test/java/org/opentripplanner/service/vehiclerental/model/TestFreeFloatingRentalVehicleBuilder.java b/src/test/java/org/opentripplanner/service/vehiclerental/model/TestFreeFloatingRentalVehicleBuilder.java index 4864fab5e43..c3837942426 100644 --- a/src/test/java/org/opentripplanner/service/vehiclerental/model/TestFreeFloatingRentalVehicleBuilder.java +++ b/src/test/java/org/opentripplanner/service/vehiclerental/model/TestFreeFloatingRentalVehicleBuilder.java @@ -13,6 +13,7 @@ public class TestFreeFloatingRentalVehicleBuilder { private double latitude = DEFAULT_LATITUDE; private double longitude = DEFAULT_LONGITUDE; + private VehicleRentalSystem system = null; private RentalVehicleType vehicleType = RentalVehicleType.getDefaultType(NETWORK_1); @@ -30,6 +31,28 @@ public TestFreeFloatingRentalVehicleBuilder withLongitude(double longitude) { return this; } + public TestFreeFloatingRentalVehicleBuilder withSystem(String id, String url) { + this.system = + new VehicleRentalSystem( + id, + null, + null, + null, + null, + url, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + return this; + } + public TestFreeFloatingRentalVehicleBuilder withVehicleScooter() { return buildVehicleType(RentalFormFactor.SCOOTER); } @@ -63,6 +86,7 @@ public VehicleRentalVehicle build() { vehicle.latitude = latitude; vehicle.longitude = longitude; vehicle.vehicleType = vehicleType; + vehicle.system = system; return vehicle; } } diff --git a/src/test/java/org/opentripplanner/service/vehiclerental/model/TestVehicleRentalStationBuilder.java b/src/test/java/org/opentripplanner/service/vehiclerental/model/TestVehicleRentalStationBuilder.java index 33f922ff0b9..0fb9f8b620f 100644 --- a/src/test/java/org/opentripplanner/service/vehiclerental/model/TestVehicleRentalStationBuilder.java +++ b/src/test/java/org/opentripplanner/service/vehiclerental/model/TestVehicleRentalStationBuilder.java @@ -19,6 +19,7 @@ public class TestVehicleRentalStationBuilder { private int spaces = 10; private boolean overloadingAllowed = false; private boolean stationOn = false; + private VehicleRentalSystem system = null; private final Map vehicleTypesAvailable = new HashMap<>(); private final Map vehicleSpacesAvailable = new HashMap<>(); @@ -52,6 +53,28 @@ public TestVehicleRentalStationBuilder withStationOn(boolean stationOn) { return this; } + public TestVehicleRentalStationBuilder withSystem(String id, String url) { + this.system = + new VehicleRentalSystem( + id, + null, + null, + null, + null, + url, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + return this; + } + public TestVehicleRentalStationBuilder withVehicleTypeBicycle(int numAvailable, int numSpaces) { return buildVehicleType( RentalFormFactor.BICYCLE, @@ -127,6 +150,7 @@ public VehicleRentalStation build() { station.isRenting = stationOn; station.isReturning = stationOn; station.realTimeData = true; + station.system = system; return station; } } diff --git a/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java b/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java index e43c5a769d0..3750c4619b9 100644 --- a/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java +++ b/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java @@ -16,6 +16,7 @@ import org.opentripplanner.service.vehiclerental.model.TestFreeFloatingRentalVehicleBuilder; import org.opentripplanner.service.vehiclerental.model.TestVehicleRentalStationBuilder; import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; +import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; import org.opentripplanner.service.vehiclerental.street.StreetVehicleRentalLink; import org.opentripplanner.service.vehiclerental.street.VehicleRentalEdge; import org.opentripplanner.service.vehiclerental.street.VehicleRentalPlaceVertex; @@ -219,6 +220,19 @@ public TestStateBuilder stop() { return arriveAtStop(testModel.stop("stop", count, count).build()); } + /** + * Add a state that arrives at a rental station. + */ + public TestStateBuilder rentalStation(VehicleRentalStation station) { + count++; + var from = (StreetVertex) currentState.vertex; + var to = new VehicleRentalPlaceVertex(station); + + var link = StreetVehicleRentalLink.createStreetVehicleRentalLink(from, to); + currentState = link.traverse(currentState)[0]; + return this; + } + public TestStateBuilder enterStation(String id) { count++; var from = (StreetVertex) currentState.vertex; diff --git a/src/test/java/org/opentripplanner/transit/model/network/grouppriority/DefaultTransitGroupPriorityCalculatorTest.java b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/DefaultTransitGroupPriorityCalculatorTest.java new file mode 100644 index 00000000000..745442e3e7b --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/DefaultTransitGroupPriorityCalculatorTest.java @@ -0,0 +1,32 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class DefaultTransitGroupPriorityCalculatorTest { + + private final DefaultTransitGroupPriorityCalculator subject = new DefaultTransitGroupPriorityCalculator(); + + @Test + void mergeGroupIds() { + // Smoke test, should not fail + subject.mergeGroupIds(1, 2); + } + + @Test + void dominanceFunction() { + // This is assuming 1 & 2 represent different transit-groups - this just a smoke test to + // see that the delegation works as expected. The 'leftDominateRight' is unit-tested elsewhere. + assertTrue(subject.dominanceFunction().leftDominateRight(1, 2)); + assertTrue(subject.dominanceFunction().leftDominateRight(2, 1)); + assertFalse(subject.dominanceFunction().leftDominateRight(1, 1)); + } + + @Test + void testToString() { + assertEquals("DefaultTransitGroupCalculator{Using TGP32n}", subject.toString()); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupMatcherTest.java b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/MatchersTest.java similarity index 70% rename from src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupMatcherTest.java rename to src/test/java/org/opentripplanner/transit/model/network/grouppriority/MatchersTest.java index 91d0142f9ef..cdda3755d37 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupMatcherTest.java +++ b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/MatchersTest.java @@ -1,4 +1,4 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit.request; +package org.opentripplanner.transit.model.network.grouppriority; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -6,12 +6,12 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TestRouteData; import org.opentripplanner.routing.api.request.request.filter.TransitGroupSelect; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.network.TripPattern; -class PriorityGroupMatcherTest { +class MatchersTest { private final TestRouteData r1 = TestRouteData .rail("R1") @@ -25,16 +25,24 @@ class PriorityGroupMatcherTest { .withSubmode("localFerry") .build(); - private final TripPattern rail1 = r1.getTripPattern(); - private final TripPattern bus = b1.getTripPattern(); - private final TripPattern ferry = f1.getTripPattern(); - private final FeedScopedId r1agencyId = rail1.getRoute().getAgency().getId(); - private final FeedScopedId r1routeId = rail1.getRoute().getId(); + private final EntityAdapter rail1 = new TripPatternAdapter(r1.getTripPattern()); + private final EntityAdapter bus = new TripPatternAdapter(b1.getTripPattern()); + private final EntityAdapter ferry = new TripPatternAdapter(f1.getTripPattern()); + private final FeedScopedId r1agencyId = rail1.agencyId(); + private final FeedScopedId r1routeId = rail1.routeId(); private final FeedScopedId anyId = new FeedScopedId("F", "ANY"); + @Test + void testEmptySelect() { + var m = Matchers.of(TransitGroupSelect.of().build()); + assertEquals("Empty", m.toString()); + assertTrue(m.isEmpty()); + assertFalse(m.match(bus)); + } + @Test void testMode() { - var m = PriorityGroupMatcher.of( + var m = Matchers.of( TransitGroupSelect.of().addModes(List.of(TransitMode.BUS, TransitMode.TRAM)).build() ); assertEquals("Mode(BUS | TRAM)", m.toString()); @@ -46,18 +54,14 @@ void testMode() { @Test void testAgencyIds() { - var m1 = PriorityGroupMatcher.of( - TransitGroupSelect.of().addAgencyIds(List.of(r1agencyId)).build() - ); - var m2 = PriorityGroupMatcher.of( - TransitGroupSelect.of().addAgencyIds(List.of(r1agencyId, anyId)).build() - ); + var m1 = Matchers.of(TransitGroupSelect.of().addAgencyIds(List.of(r1agencyId)).build()); + var m2 = Matchers.of(TransitGroupSelect.of().addAgencyIds(List.of(r1agencyId, anyId)).build()); var matchers = List.of(m1, m2); assertEquals("AgencyId(F:A1)", m1.toString()); assertEquals("AgencyId(F:A1 | F:ANY)", m2.toString()); - for (PriorityGroupMatcher m : matchers) { + for (Matcher m : matchers) { assertFalse(m.isEmpty()); assertTrue(m.match(rail1)); assertTrue(m.match(ferry)); @@ -67,18 +71,14 @@ void testAgencyIds() { @Test void routeIds() { - var m1 = PriorityGroupMatcher.of( - TransitGroupSelect.of().addRouteIds(List.of(r1routeId)).build() - ); - var m2 = PriorityGroupMatcher.of( - TransitGroupSelect.of().addRouteIds(List.of(r1routeId, anyId)).build() - ); + var m1 = Matchers.of(TransitGroupSelect.of().addRouteIds(List.of(r1routeId)).build()); + var m2 = Matchers.of(TransitGroupSelect.of().addRouteIds(List.of(r1routeId, anyId)).build()); var matchers = List.of(m1, m2); assertEquals("RouteId(F:R1)", m1.toString()); assertEquals("RouteId(F:R1 | F:ANY)", m2.toString()); - for (PriorityGroupMatcher m : matchers) { + for (Matcher m : matchers) { assertFalse(m.isEmpty()); assertTrue(m.match(rail1)); assertFalse(m.match(ferry)); @@ -88,7 +88,7 @@ void routeIds() { @Test void testSubMode() { - var subject = PriorityGroupMatcher.of( + var subject = Matchers.of( TransitGroupSelect.of().addSubModeRegexp(List.of(".*local.*")).build() ); @@ -102,7 +102,7 @@ void testSubMode() { @Test void testAnd() { - var subject = PriorityGroupMatcher.of( + var subject = Matchers.of( TransitGroupSelect .of() .addSubModeRegexp(List.of("express")) @@ -124,7 +124,7 @@ void testAnd() { @Test void testToString() { - var subject = PriorityGroupMatcher.of( + var subject = Matchers.of( TransitGroupSelect .of() .addModes(List.of(TransitMode.BUS, TransitMode.TRAM)) diff --git a/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriority32nTest.java b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriority32nTest.java new file mode 100644 index 00000000000..9f681dc16c6 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriority32nTest.java @@ -0,0 +1,68 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriority32n.dominate; +import static org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriority32n.groupId; +import static org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriority32n.mergeInGroupId; + +import org.junit.jupiter.api.Test; + +class TransitGroupPriority32nTest { + + private static final int GROUP_INDEX_0 = 0; + private static final int GROUP_INDEX_1 = 1; + private static final int GROUP_INDEX_2 = 2; + private static final int GROUP_INDEX_30 = 30; + private static final int GROUP_INDEX_31 = 31; + + private static final int GROUP_0 = groupId(GROUP_INDEX_0); + private static final int GROUP_1 = groupId(GROUP_INDEX_1); + private static final int GROUP_2 = groupId(GROUP_INDEX_2); + private static final int GROUP_30 = groupId(GROUP_INDEX_30); + private static final int GROUP_31 = groupId(GROUP_INDEX_31); + + @Test + void testGroupId() { + assertEqualsHex(0x00_00_00_00, groupId(0)); + assertEqualsHex(0x00_00_00_01, groupId(1)); + assertEqualsHex(0x00_00_00_02, groupId(2)); + assertEqualsHex(0x00_00_00_04, groupId(3)); + assertEqualsHex(0x40_00_00_00, groupId(31)); + assertEqualsHex(0x80_00_00_00, groupId(32)); + + assertThrows(IllegalArgumentException.class, () -> groupId(-1)); + assertThrows(IllegalArgumentException.class, () -> groupId(33)); + } + + @Test + void mergeTransitGroupPriorityIds() { + assertEqualsHex(GROUP_0, mergeInGroupId(GROUP_0, GROUP_0)); + assertEqualsHex(GROUP_1, mergeInGroupId(GROUP_1, GROUP_1)); + assertEqualsHex(GROUP_0 | GROUP_1, mergeInGroupId(GROUP_0, GROUP_1)); + assertEqualsHex(GROUP_30 | GROUP_31, mergeInGroupId(GROUP_30, GROUP_31)); + assertEqualsHex( + GROUP_0 | GROUP_1 | GROUP_2 | GROUP_30 | GROUP_31, + mergeInGroupId(GROUP_0 | GROUP_1 | GROUP_2 | GROUP_30, GROUP_31) + ); + } + + @Test + void dominanceFunction() { + assertFalse(dominate(GROUP_0, GROUP_0)); + assertFalse(dominate(GROUP_31, GROUP_31)); + assertFalse(dominate(GROUP_1 | GROUP_2, GROUP_1 | GROUP_2)); + + assertTrue(dominate(GROUP_0, GROUP_1)); + assertFalse(dominate(GROUP_1, GROUP_0)); + + assertTrue(dominate(GROUP_1, GROUP_1 | GROUP_2)); + assertFalse(dominate(GROUP_1 | GROUP_2, GROUP_1)); + } + + static void assertEqualsHex(int expected, int actual) { + assertEquals(expected, actual, "%08x == %08x".formatted(expected, actual)); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupConfiguratorTest.java b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriorityServiceTest.java similarity index 75% rename from src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupConfiguratorTest.java rename to src/test/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriorityServiceTest.java index 7f974927c1b..fe2cb361f07 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupConfiguratorTest.java +++ b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriorityServiceTest.java @@ -1,4 +1,4 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit.request; +package org.opentripplanner.transit.model.network.grouppriority; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TestTransitCaseData.STOP_A; @@ -7,12 +7,14 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TestRouteData; import org.opentripplanner.routing.api.request.request.filter.TransitGroupSelect; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.timetable.Trip; -class PriorityGroupConfiguratorTest { +class TransitGroupPriorityServiceTest { private static final String AGENCY_A1 = "A1"; private static final String AGENCY_A2 = "A2"; @@ -65,13 +67,15 @@ class PriorityGroupConfiguratorTest { private final TripPattern railR3 = routeR3.getTripPattern(); private final TripPattern ferryF3 = routeF3.getTripPattern(); private final TripPattern busB3 = routeB3.getTripPattern(); + private final TripPattern nullTripPattern = null; + private final Trip nullTrip = null; @Test void emptyConfigurationShouldReturnGroupZero() { - var subject = PriorityGroupConfigurator.of(List.of(), List.of()); + var subject = TransitGroupPriorityService.empty(); assertEquals(subject.baseGroupId(), subject.lookupTransitGroupPriorityId(railR1)); assertEquals(subject.baseGroupId(), subject.lookupTransitGroupPriorityId(busB2)); - assertEquals(subject.baseGroupId(), subject.lookupTransitGroupPriorityId(null)); + assertEquals(subject.baseGroupId(), subject.lookupTransitGroupPriorityId(nullTripPattern)); } @Test @@ -82,21 +86,32 @@ void lookupTransitGroupIdByAgency() { .build(); // Add matcher `byAgency` for bus and real - var subject = PriorityGroupConfigurator.of(List.of(select), List.of()); + var subject = new TransitGroupPriorityService(List.of(select), List.of()); // Agency groups are indexed (group-id set) at request time - assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(null)); + assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(nullTripPattern)); assertEquals(EXP_GROUP_1, subject.lookupTransitGroupPriorityId(busB2)); assertEquals(EXP_GROUP_2, subject.lookupTransitGroupPriorityId(railR3)); assertEquals(EXP_GROUP_3, subject.lookupTransitGroupPriorityId(railR1)); assertEquals(EXP_GROUP_2, subject.lookupTransitGroupPriorityId(busB3)); assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(ferryF3)); + + // Verify we get the same result with using the trip, not trip-pattern + assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(nullTrip)); + assertEquals( + EXP_GROUP_1, + subject.lookupTransitGroupPriorityId(busB2.getScheduledTimetable().getTripTimes(0).getTrip()) + ); + assertEquals( + EXP_GROUP_2, + subject.lookupTransitGroupPriorityId(railR3.getScheduledTimetable().getTripTimes(0).getTrip()) + ); } @Test void lookupTransitPriorityGroupIdByGlobalMode() { // Global groups are indexed (group-id set) at construction time - var subject = PriorityGroupConfigurator.of( + var subject = new TransitGroupPriorityService( List.of(), List.of( TransitGroupSelect.of().addModes(List.of(TransitMode.BUS)).build(), @@ -104,12 +119,19 @@ void lookupTransitPriorityGroupIdByGlobalMode() { ) ); - assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(null)); + assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(nullTripPattern)); assertEquals(EXP_GROUP_2, subject.lookupTransitGroupPriorityId(railR1)); assertEquals(EXP_GROUP_1, subject.lookupTransitGroupPriorityId(busB2)); assertEquals(EXP_GROUP_2, subject.lookupTransitGroupPriorityId(railR3)); assertEquals(EXP_GROUP_1, subject.lookupTransitGroupPriorityId(busB3)); assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(ferryF3)); + + // Verify we get the same result with using the trip, not trip-pattern + assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(nullTrip)); + assertEquals( + EXP_GROUP_2, + subject.lookupTransitGroupPriorityId(railR1.getScheduledTimetable().getTripTimes(0).getTrip()) + ); } private static TestRouteData route( diff --git a/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TripAdapterTest.java b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TripAdapterTest.java new file mode 100644 index 00000000000..c1aeeea1a59 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TripAdapterTest.java @@ -0,0 +1,34 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.timetable.Trip; + +class TripAdapterTest { + + private final Trip trip = TransitModelForTest.trip("Trip").build(); + + private final TripAdapter subject = new TripAdapter(trip); + + @Test + void mode() { + assertEquals(trip.getMode(), subject.mode()); + } + + @Test + void subMode() { + assertEquals(trip.getNetexSubMode().name(), subject.subMode()); + } + + @Test + void agencyId() { + assertEquals(trip.getRoute().getAgency().getId(), subject.agencyId()); + } + + @Test + void routeId() { + assertEquals(trip.getRoute().getId(), subject.routeId()); + } +} diff --git a/src/test/java/org/opentripplanner/transit/model/site/MultiModalStationTest.java b/src/test/java/org/opentripplanner/transit/model/site/MultiModalStationTest.java index 4ca8e8d33ea..615b0c49674 100644 --- a/src/test/java/org/opentripplanner/transit/model/site/MultiModalStationTest.java +++ b/src/test/java/org/opentripplanner/transit/model/site/MultiModalStationTest.java @@ -8,6 +8,7 @@ import java.util.Set; import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.transit.model._data.TransitModelForTest; @@ -26,6 +27,7 @@ class MultiModalStationTest { .of(TransitModelForTest.id(ID)) .withName(NAME) .withChildStations(CHILD_STATIONS) + .withCoordinate(new WgsCoordinate(1, 1)) .build(); @Test diff --git a/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java b/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java index f04fe782a0a..54733f084d6 100644 --- a/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java +++ b/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java @@ -6,16 +6,22 @@ import static org.opentripplanner.transit.model.basic.TransitMode.RAIL; import static org.opentripplanner.transit.model.basic.TransitMode.TRAM; +import java.time.LocalDate; import java.util.Collection; import java.util.List; +import java.util.Set; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.framework.Deduplicator; +import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; +import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; class DefaultTransitServiceTest { @@ -33,6 +39,13 @@ class DefaultTransitServiceTest { static TripPattern FERRY_PATTERN = TEST_MODEL.pattern(FERRY).build(); static TripPattern BUS_PATTERN = TEST_MODEL.pattern(BUS).build(); + static StopPattern REAL_TIME_STOP_PATTERN = TransitModelForTest.stopPattern(STOP_A, STOP_B); + static TripPattern REAL_TIME_PATTERN = TEST_MODEL + .pattern(BUS) + .withStopPattern(REAL_TIME_STOP_PATTERN) + .withCreatedByRealtimeUpdater(true) + .build(); + @BeforeAll static void setup() { var stopModel = TEST_MODEL @@ -46,6 +59,20 @@ static void setup() { transitModel.addTripPattern(RAIL_PATTERN.getId(), RAIL_PATTERN); transitModel.index(); + transitModel.initTimetableSnapshotProvider(() -> { + TimetableSnapshot timetableSnapshot = new TimetableSnapshot(); + RealTimeTripTimes tripTimes = RealTimeTripTimes.of( + ScheduledTripTimes + .of() + .withTrip(TransitModelForTest.trip("REAL_TIME_TRIP").build()) + .withDepartureTimes(new int[] { 0, 1 }) + .build() + ); + timetableSnapshot.update(REAL_TIME_PATTERN, tripTimes, LocalDate.now()); + + return timetableSnapshot.commit(); + }); + service = new DefaultTransitService(transitModel) { @Override @@ -76,4 +103,16 @@ void stationModes() { var modes = service.getModesOfStopLocationsGroup(STATION); assertEquals(List.of(RAIL, FERRY, TRAM), modes); } + + @Test + void getPatternForStopsWithoutRealTime() { + Collection patternsForStop = service.getPatternsForStop(STOP_B, false); + assertEquals(Set.of(FERRY_PATTERN, RAIL_PATTERN), patternsForStop); + } + + @Test + void getPatternForStopsWithRealTime() { + Collection patternsForStop = service.getPatternsForStop(STOP_B, true); + assertEquals(Set.of(FERRY_PATTERN, RAIL_PATTERN, REAL_TIME_PATTERN), patternsForStop); + } } diff --git a/src/test/java/org/opentripplanner/updater/spi/UpdateResultAssertions.java b/src/test/java/org/opentripplanner/updater/spi/UpdateResultAssertions.java new file mode 100644 index 00000000000..38468744886 --- /dev/null +++ b/src/test/java/org/opentripplanner/updater/spi/UpdateResultAssertions.java @@ -0,0 +1,23 @@ +package org.opentripplanner.updater.spi; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Set; + +public class UpdateResultAssertions { + + public static void assertFailure(UpdateError.UpdateErrorType expectedError, UpdateResult result) { + assertEquals(Set.of(expectedError), result.failures().keySet()); + } + + public static void assertSuccess(UpdateResult updateResult) { + var errorCodes = updateResult.failures().keySet(); + assertEquals( + Set.of(), + errorCodes, + "Update result should have no error codes but had %s".formatted(errorCodes) + ); + assertTrue(updateResult.successful() > 0); + } +} diff --git a/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java b/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java index d557aa1319b..6adce735fb0 100644 --- a/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java +++ b/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java @@ -15,6 +15,7 @@ import org.opentripplanner.ext.siri.SiriFuzzyTripMatcher; import org.opentripplanner.ext.siri.SiriTimetableSnapshotSource; import org.opentripplanner.ext.siri.updater.EstimatedTimetableHandler; +import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.model.StopTime; import org.opentripplanner.model.TimetableSnapshot; @@ -55,16 +56,19 @@ public final class RealtimeTestEnvironment { ); public static final LocalDate SERVICE_DATE = LocalDate.of(2024, 5, 8); public static final FeedScopedId SERVICE_ID = TransitModelForTest.id("CAL_1"); + public static final String STOP_A1_ID = "A1"; + public static final String STOP_B1_ID = "B1"; + public static final String STOP_C1_ID = "C1"; private final TransitModelForTest testModel = TransitModelForTest.of(); public final ZoneId timeZone = ZoneId.of(TransitModelForTest.TIME_ZONE_ID); public final Station stationA = testModel.station("A").build(); public final Station stationB = testModel.station("B").build(); public final Station stationC = testModel.station("C").build(); public final Station stationD = testModel.station("D").build(); - public final RegularStop stopA1 = testModel.stop("A1").withParentStation(stationA).build(); - public final RegularStop stopB1 = testModel.stop("B1").withParentStation(stationB).build(); + public final RegularStop stopA1 = testModel.stop(STOP_A1_ID).withParentStation(stationA).build(); + public final RegularStop stopB1 = testModel.stop(STOP_B1_ID).withParentStation(stationB).build(); public final RegularStop stopB2 = testModel.stop("B2").withParentStation(stationB).build(); - public final RegularStop stopC1 = testModel.stop("C1").withParentStation(stationC).build(); + public final RegularStop stopC1 = testModel.stop(STOP_C1_ID).withParentStation(stationC).build(); public final RegularStop stopD1 = testModel.stop("D1").withParentStation(stationD).build(); public final StopModel stopModel = testModel .stopModelBuilder() @@ -110,12 +114,20 @@ private RealtimeTestEnvironment(SourceType sourceType) { Route route1 = TransitModelForTest.route(route1Id).build(); trip1 = - createTrip("TestTrip1", route1, List.of(new Stop(stopA1, 10, 11), new Stop(stopB1, 20, 21))); + createTrip( + "TestTrip1", + route1, + List.of(new StopCall(stopA1, 10, 11), new StopCall(stopB1, 20, 21)) + ); trip2 = createTrip( "TestTrip2", route1, - List.of(new Stop(stopA1, 60, 61), new Stop(stopB1, 70, 71), new Stop(stopC1, 80, 81)) + List.of( + new StopCall(stopA1, 60, 61), + new StopCall(stopB1, 70, 71), + new StopCall(stopC1, 80, 81) + ) ); CalendarServiceData calendarServiceData = new CalendarServiceData(); @@ -205,7 +217,7 @@ public DateTimeHelper getDateTimeHelper() { } public TripPattern getPatternForTrip(Trip trip) { - return transitModel.getTransitModelIndex().getPatternForTrip().get(trip); + return getTransitService().getPatternForTrip(trip); } public TimetableSnapshot getTimetableSnapshot() { @@ -291,8 +303,13 @@ private UpdateResult applyEstimatedTimetable( return getEstimatedTimetableHandler(fuzzyMatching).applyUpdate(updates, DIFFERENTIAL); } - private Trip createTrip(String id, Route route, List stops) { - var trip = Trip.of(id(id)).withRoute(route).withServiceId(SERVICE_ID).build(); + private Trip createTrip(String id, Route route, List stops) { + var trip = Trip + .of(id(id)) + .withRoute(route) + .withHeadsign(I18NString.of("Headsign of %s".formatted(id))) + .withServiceId(SERVICE_ID) + .build(); var tripOnServiceDate = TripOnServiceDate .of(trip.getId()) @@ -314,7 +331,7 @@ private Trip createTrip(String id, Route route, List stops) { final TripPattern pattern = TransitModelForTest .tripPattern(id + "Pattern", route) - .withStopPattern(TransitModelForTest.stopPattern(stops.stream().map(Stop::stop).toList())) + .withStopPattern(TransitModelForTest.stopPattern(stops.stream().map(StopCall::stop).toList())) .build(); pattern.add(tripTimes); @@ -339,5 +356,5 @@ private StopTime createStopTime( return st; } - protected record Stop(RegularStop stop, int arrivalTime, int departureTime) {} + private record StopCall(RegularStop stop, int arrivalTime, int departureTime) {} } diff --git a/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java b/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java index 1dad90b416b..84b53c95f9c 100644 --- a/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java @@ -1,8 +1,6 @@ package org.opentripplanner.updater.trip; -import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.ADDED; import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.CANCELED; -import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED; import static com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.SKIPPED; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -18,33 +16,28 @@ import com.google.transit.realtime.GtfsRealtime.TripUpdate; import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent; import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate; -import de.mfdz.MfdzRealtimeExtensions.StopTimePropertiesExtension.DropOffPickupType; import java.time.Duration; import java.time.LocalDate; import java.util.List; -import java.util.stream.Stream; import javax.annotation.Nonnull; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.opentripplanner.ConstantsForTests; import org.opentripplanner.TestOtpModel; import org.opentripplanner._support.time.ZoneIds; -import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.framework.time.ServiceDateUtils; -import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TimetableSnapshot; -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.timetable.RealTimeState; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.updater.GtfsRealtimeFuzzyTripMatcher; import org.opentripplanner.updater.TimetableSnapshotSourceParameters; -import org.opentripplanner.updater.spi.UpdateSuccess.WarningType; public class TimetableSnapshotSourceTest { @@ -57,6 +50,7 @@ public class TimetableSnapshotSourceTest { ) .build(); private TransitModel transitModel; + private TransitService transitService; private final GtfsRealtimeFuzzyTripMatcher TRIP_MATCHER_NOOP = null; @@ -66,8 +60,9 @@ public class TimetableSnapshotSourceTest { public void setUp() { TestOtpModel model = ConstantsForTests.buildGtfsGraph(ConstantsForTests.SIMPLE_GTFS); transitModel = model.transitModel(); + transitService = new DefaultTransitService(transitModel); - feedId = transitModel.getFeedIds().stream().findFirst().get(); + feedId = transitService.getFeedIds().stream().findFirst().get(); } @Test @@ -109,41 +104,6 @@ public void testGetSnapshotWithMaxSnapshotFrequencyCleared() { assertNotSame(snapshot, newSnapshot); } - /** - * This test just asserts that invalid trip ids don't throw an exception and are ignored instead - */ - @Test - public void invalidTripId() { - var updater = new TimetableSnapshotSource( - TimetableSnapshotSourceParameters.DEFAULT, - transitModel - ); - - Stream - .of("", null) - .forEach(id -> { - var tripDescriptorBuilder = TripDescriptor.newBuilder(); - tripDescriptorBuilder.setTripId(""); - tripDescriptorBuilder.setScheduleRelationship( - TripDescriptor.ScheduleRelationship.SCHEDULED - ); - var tripUpdateBuilder = TripUpdate.newBuilder(); - - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - var tripUpdate = tripUpdateBuilder.build(); - - var result = updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - - assertEquals(0, result.successful()); - }); - } - @Test public void testHandleModifiedTrip() { // GIVEN @@ -159,7 +119,7 @@ public void testHandleModifiedTrip() { tripDescriptorBuilder.setStartDate(ServiceDateUtils.asCompactString(SERVICE_DATE)); final long midnightSecondsSinceEpoch = ServiceDateUtils - .asStartOfService(SERVICE_DATE, transitModel.getTimeZone()) + .asStartOfService(SERVICE_DATE, transitService.getTimeZone()) .toEpochSecond(); final TripUpdate.Builder tripUpdateBuilder = TripUpdate.newBuilder(); @@ -268,11 +228,8 @@ public void testHandleModifiedTrip() { // Original trip pattern { final FeedScopedId tripId = new FeedScopedId(feedId, modifiedTripId); - final Trip trip = transitModel.getTransitModelIndex().getTripForId().get(tripId); - final TripPattern originalTripPattern = transitModel - .getTransitModelIndex() - .getPatternForTrip() - .get(trip); + final Trip trip = transitService.getTripForId(tripId); + final TripPattern originalTripPattern = transitService.getPatternForTrip(trip); final Timetable originalTimetableForToday = snapshot.resolve( originalTripPattern, @@ -346,430 +303,6 @@ public void testHandleModifiedTrip() { } } - @Nested - class Scheduled { - - @Test - public void scheduled() { - // GIVEN - - String scheduledTripId = "1.1"; - - var builder = new TripUpdateBuilder( - scheduledTripId, - SERVICE_DATE, - SCHEDULED, - transitModel.getTimeZone() - ) - .addDelayedStopTime(1, 0) - .addDelayedStopTime(2, 60, 80) - .addDelayedStopTime(3, 90, 90); - - var tripUpdate = builder.build(); - - var updater = defaultUpdater(); - - // WHEN - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - - // THEN - final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); - - final FeedScopedId tripId = new FeedScopedId(feedId, scheduledTripId); - final Trip trip = transitModel.getTransitModelIndex().getTripForId().get(tripId); - final TripPattern originalTripPattern = transitModel - .getTransitModelIndex() - .getPatternForTrip() - .get(trip); - - final Timetable originalTimetableForToday = snapshot.resolve( - originalTripPattern, - SERVICE_DATE - ); - final Timetable originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); - - assertNotSame(originalTimetableForToday, originalTimetableScheduled); - - final int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(tripId); - assertTrue( - originalTripIndexScheduled > -1, - "Original trip should be found in scheduled time table" - ); - final TripTimes originalTripTimesScheduled = originalTimetableScheduled.getTripTimes( - originalTripIndexScheduled - ); - assertFalse( - originalTripTimesScheduled.isCanceledOrDeleted(), - "Original trip times should not be canceled in scheduled time table" - ); - assertEquals(RealTimeState.SCHEDULED, originalTripTimesScheduled.getRealTimeState()); - - final int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); - assertTrue( - originalTripIndexForToday > -1, - "Original trip should be found in time table for service date" - ); - final TripTimes originalTripTimesForToday = originalTimetableForToday.getTripTimes( - originalTripIndexForToday - ); - assertEquals(RealTimeState.UPDATED, originalTripTimesForToday.getRealTimeState()); - assertEquals(0, originalTripTimesForToday.getArrivalDelay(0)); - assertEquals(0, originalTripTimesForToday.getDepartureDelay(0)); - assertEquals(60, originalTripTimesForToday.getArrivalDelay(1)); - assertEquals(80, originalTripTimesForToday.getDepartureDelay(1)); - assertEquals(90, originalTripTimesForToday.getArrivalDelay(2)); - assertEquals(90, originalTripTimesForToday.getDepartureDelay(2)); - } - - @Test - public void scheduledTripWithSkippedAndNoData() { - // GIVEN - - String scheduledTripId = "1.1"; - - var builder = new TripUpdateBuilder( - scheduledTripId, - SERVICE_DATE, - SCHEDULED, - transitModel.getTimeZone() - ) - .addNoDataStop(1) - .addSkippedStop(2) - .addNoDataStop(3); - - var tripUpdate = builder.build(); - - var updater = defaultUpdater(); - - // WHEN - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - - // THEN - final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); - - // Original trip pattern - { - final FeedScopedId tripId = new FeedScopedId(feedId, scheduledTripId); - final Trip trip = transitModel.getTransitModelIndex().getTripForId().get(tripId); - final TripPattern originalTripPattern = transitModel - .getTransitModelIndex() - .getPatternForTrip() - .get(trip); - - final Timetable originalTimetableForToday = snapshot.resolve( - originalTripPattern, - SERVICE_DATE - ); - final Timetable originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); - - assertNotSame(originalTimetableForToday, originalTimetableScheduled); - - final int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(tripId); - assertTrue( - originalTripIndexScheduled > -1, - "Original trip should be found in scheduled time table" - ); - final TripTimes originalTripTimesScheduled = originalTimetableScheduled.getTripTimes( - originalTripIndexScheduled - ); - assertFalse( - originalTripTimesScheduled.isCanceledOrDeleted(), - "Original trip times should not be canceled in scheduled time table" - ); - assertEquals(RealTimeState.SCHEDULED, originalTripTimesScheduled.getRealTimeState()); - - final int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); - assertTrue( - originalTripIndexForToday > -1, - "Original trip should be found in time table for service date" - ); - final TripTimes originalTripTimesForToday = originalTimetableForToday.getTripTimes( - originalTripIndexForToday - ); - assertTrue( - originalTripTimesForToday.isDeleted(), - "Original trip times should be deleted in time table for service date" - ); - // original trip should be deleted - assertEquals(RealTimeState.DELETED, originalTripTimesForToday.getRealTimeState()); - } - - // New trip pattern - { - final TripPattern newTripPattern = snapshot.getRealtimeAddedTripPattern( - new FeedScopedId(feedId, scheduledTripId), - SERVICE_DATE - ); - assertNotNull(newTripPattern, "New trip pattern should be found"); - - final Timetable newTimetableForToday = snapshot.resolve(newTripPattern, SERVICE_DATE); - final Timetable newTimetableScheduled = snapshot.resolve(newTripPattern, null); - - assertNotSame(newTimetableForToday, newTimetableScheduled); - - assertTrue(newTripPattern.canBoard(0)); - assertFalse(newTripPattern.canBoard(1)); - assertTrue(newTripPattern.canBoard(2)); - - assertEquals(new NonLocalizedString("foo"), newTripPattern.getTripHeadsign()); - assertEquals( - newTripPattern.getOriginalTripPattern().getTripHeadsign(), - newTripPattern.getTripHeadsign() - ); - - final int newTimetableForTodayModifiedTripIndex = newTimetableForToday.getTripIndex( - scheduledTripId - ); - assertTrue( - newTimetableForTodayModifiedTripIndex > -1, - "New trip should be found in time table for service date" - ); - - var newTripTimes = newTimetableForToday.getTripTimes(newTimetableForTodayModifiedTripIndex); - assertEquals(RealTimeState.UPDATED, newTripTimes.getRealTimeState()); - - assertEquals( - -1, - newTimetableScheduled.getTripIndex(scheduledTripId), - "New trip should not be found in scheduled time table" - ); - - assertEquals(0, newTripTimes.getArrivalDelay(0)); - assertEquals(0, newTripTimes.getDepartureDelay(0)); - assertEquals(0, newTripTimes.getArrivalDelay(1)); - assertEquals(0, newTripTimes.getDepartureDelay(1)); - assertEquals(0, newTripTimes.getArrivalDelay(2)); - assertEquals(0, newTripTimes.getDepartureDelay(2)); - assertTrue(newTripTimes.isNoDataStop(0)); - assertTrue(newTripTimes.isCancelledStop(1)); - assertTrue(newTripTimes.isNoDataStop(2)); - } - } - } - - @Nested - class Added { - - final String addedTripId = "added_trip"; - - @Test - public void addedTrip() { - var builder = new TripUpdateBuilder( - addedTripId, - SERVICE_DATE, - ADDED, - transitModel.getTimeZone() - ); - - builder.addStopTime("A", 30).addStopTime("C", 40).addStopTime("E", 55); - - var tripUpdate = builder.build(); - - var updater = defaultUpdater(); - - // WHEN - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - - // THEN - assertAddedTrip(SERVICE_DATE, this.addedTripId, updater); - } - - private TripPattern assertAddedTrip( - LocalDate serviceDate, - String tripId, - TimetableSnapshotSource updater - ) { - var stopA = transitModel.getStopModel().getRegularStop(new FeedScopedId(feedId, "A")); - // Get the trip pattern of the added trip which goes through stopA - var snapshot = updater.getTimetableSnapshot(); - var patternsAtA = snapshot.getPatternsForStop(stopA); - - assertNotNull(patternsAtA, "Added trip pattern should be found"); - assertEquals(1, patternsAtA.size()); - var tripPattern = patternsAtA.stream().findFirst().get(); - - final Timetable forToday = snapshot.resolve(tripPattern, serviceDate); - final Timetable schedule = snapshot.resolve(tripPattern, null); - - assertNotSame(forToday, schedule); - - final int forTodayAddedTripIndex = forToday.getTripIndex(tripId); - assertTrue( - forTodayAddedTripIndex > -1, - "Added trip should be found in time table for service date" - ); - assertEquals( - RealTimeState.ADDED, - forToday.getTripTimes(forTodayAddedTripIndex).getRealTimeState() - ); - - final int scheduleTripIndex = schedule.getTripIndex(tripId); - assertEquals(-1, scheduleTripIndex, "Added trip should not be found in scheduled time table"); - return tripPattern; - } - - @Test - public void addedTripWithNewRoute() { - // GIVEN - - final var builder = new TripUpdateBuilder( - addedTripId, - SERVICE_DATE, - ADDED, - transitModel.getTimeZone() - ); - // add extension to set route name, url, mode - builder.addTripExtension(); - - builder - .addStopTime("A", 30, DropOffPickupType.PHONE_AGENCY) - .addStopTime("C", 40, DropOffPickupType.COORDINATE_WITH_DRIVER) - .addStopTime("E", 55, DropOffPickupType.NONE); - - var tripUpdate = builder.build(); - - var updater = defaultUpdater(); - - // WHEN - var result = updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - - // THEN - - assertTrue(result.warnings().isEmpty()); - - var pattern = assertAddedTrip(SERVICE_DATE, addedTripId, updater); - - var route = pattern.getRoute(); - assertEquals(TripUpdateBuilder.ROUTE_URL, route.getUrl()); - assertEquals(TripUpdateBuilder.ROUTE_NAME, route.getName()); - assertEquals(TransitMode.RAIL, route.getMode()); - - var fromTransitModel = transitModel.getTransitModelIndex().getRouteForId(route.getId()); - assertEquals(fromTransitModel, route); - - assertEquals(PickDrop.CALL_AGENCY, pattern.getBoardType(0)); - assertEquals(PickDrop.CALL_AGENCY, pattern.getAlightType(0)); - - assertEquals(PickDrop.COORDINATE_WITH_DRIVER, pattern.getBoardType(1)); - assertEquals(PickDrop.COORDINATE_WITH_DRIVER, pattern.getAlightType(1)); - } - - @Test - public void addedWithUnknownStop() { - // GIVEN - final var builder = new TripUpdateBuilder( - addedTripId, - SERVICE_DATE, - ADDED, - transitModel.getTimeZone() - ); - // add extension to set route name, url, mode - builder.addTripExtension(); - - builder - .addStopTime("A", 30, DropOffPickupType.PHONE_AGENCY) - .addStopTime("UNKNOWN_STOP_ID", 40, DropOffPickupType.COORDINATE_WITH_DRIVER) - .addStopTime("E", 55, DropOffPickupType.NONE); - - var tripUpdate = builder.build(); - - var updater = defaultUpdater(); - - // WHEN - var result = updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - - // THEN - - assertFalse(result.warnings().isEmpty()); - - assertEquals(List.of(WarningType.UNKNOWN_STOPS_REMOVED_FROM_ADDED_TRIP), result.warnings()); - - var pattern = assertAddedTrip(SERVICE_DATE, addedTripId, updater); - - assertEquals(2, pattern.getStops().size()); - } - - @Test - public void repeatedlyAddedTripWithNewRoute() { - // GIVEN - - final var builder = new TripUpdateBuilder( - addedTripId, - SERVICE_DATE, - ADDED, - transitModel.getTimeZone() - ); - // add extension to set route name, url, mode - builder.addTripExtension(); - - builder - .addStopTime("A", 30, DropOffPickupType.PHONE_AGENCY) - .addStopTime("C", 40, DropOffPickupType.COORDINATE_WITH_DRIVER) - .addStopTime("E", 55, DropOffPickupType.NONE); - - var tripUpdate = builder.build(); - - var updater = defaultUpdater(); - - // WHEN - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - var pattern = assertAddedTrip(SERVICE_DATE, addedTripId, updater); - var firstRoute = pattern.getRoute(); - - // apply the update a second time to check that no new route instance is created but the old one is reused - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - var secondPattern = assertAddedTrip(SERVICE_DATE, addedTripId, updater); - var secondRoute = secondPattern.getRoute(); - - // THEN - - assertSame(firstRoute, secondRoute); - assertNotNull(transitModel.getTransitModelIndex().getRouteForId(firstRoute.getId())); - } - } - @Nonnull private TimetableSnapshotSource defaultUpdater() { return new TimetableSnapshotSource( diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/addition/AddedTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/addition/AddedTest.java new file mode 100644 index 00000000000..8371c5dda3a --- /dev/null +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/addition/AddedTest.java @@ -0,0 +1,152 @@ +package org.opentripplanner.updater.trip.moduletests.addition; + +import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.ADDED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess; +import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.SERVICE_DATE; +import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.STOP_A1_ID; +import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.STOP_B1_ID; +import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.STOP_C1_ID; + +import de.mfdz.MfdzRealtimeExtensions.StopTimePropertiesExtension.DropOffPickupType; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.model.PickDrop; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.RealTimeState; +import org.opentripplanner.updater.spi.UpdateSuccess; +import org.opentripplanner.updater.trip.RealtimeTestEnvironment; +import org.opentripplanner.updater.trip.TripUpdateBuilder; + +class AddedTest { + + final String ADDED_TRIP_ID = "added_trip"; + + @Test + void addedTrip() { + var env = RealtimeTestEnvironment.gtfs(); + + var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, env.timeZone) + .addStopTime(STOP_A1_ID, 30) + .addStopTime(STOP_B1_ID, 40) + .addStopTime(STOP_C1_ID, 55) + .build(); + + assertSuccess(env.applyTripUpdate(tripUpdate)); + assertAddedTrip(this.ADDED_TRIP_ID, env); + } + + @Test + void addedTripWithNewRoute() { + var env = RealtimeTestEnvironment.gtfs(); + var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, env.timeZone) + .addTripExtension() + .addStopTime(STOP_A1_ID, 30, DropOffPickupType.PHONE_AGENCY) + .addStopTime(STOP_B1_ID, 40, DropOffPickupType.COORDINATE_WITH_DRIVER) + .addStopTime(STOP_B1_ID, 55, DropOffPickupType.NONE) + .build(); + + var result = env.applyTripUpdate(tripUpdate); + assertSuccess(result); + assertTrue(result.warnings().isEmpty()); + + var pattern = assertAddedTrip(ADDED_TRIP_ID, env); + + var route = pattern.getRoute(); + assertEquals(TripUpdateBuilder.ROUTE_URL, route.getUrl()); + assertEquals(TripUpdateBuilder.ROUTE_NAME, route.getName()); + assertEquals(TransitMode.RAIL, route.getMode()); + + var fromTransitModel = env.getTransitService().getRouteForId(route.getId()); + assertEquals(fromTransitModel, route); + + assertEquals(PickDrop.CALL_AGENCY, pattern.getBoardType(0)); + assertEquals(PickDrop.CALL_AGENCY, pattern.getAlightType(0)); + + assertEquals(PickDrop.COORDINATE_WITH_DRIVER, pattern.getBoardType(1)); + assertEquals(PickDrop.COORDINATE_WITH_DRIVER, pattern.getAlightType(1)); + } + + @Test + void addedWithUnknownStop() { + var env = RealtimeTestEnvironment.gtfs(); + var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, env.timeZone) + // add extension to set route name, url, mode + .addTripExtension() + .addStopTime(STOP_A1_ID, 30, DropOffPickupType.PHONE_AGENCY) + .addStopTime("UNKNOWN_STOP_ID", 40, DropOffPickupType.COORDINATE_WITH_DRIVER) + .addStopTime(STOP_C1_ID, 55, DropOffPickupType.NONE) + .build(); + + var result = env.applyTripUpdate(tripUpdate); + assertSuccess(result); + + assertEquals( + List.of(UpdateSuccess.WarningType.UNKNOWN_STOPS_REMOVED_FROM_ADDED_TRIP), + result.warnings() + ); + + var pattern = assertAddedTrip(ADDED_TRIP_ID, env); + + assertEquals(2, pattern.getStops().size()); + } + + @Test + void repeatedlyAddedTripWithNewRoute() { + var env = RealtimeTestEnvironment.gtfs(); + var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, env.timeZone) + // add extension to set route name, url, mode + .addTripExtension() + .addStopTime(STOP_A1_ID, 30, DropOffPickupType.PHONE_AGENCY) + .addStopTime(STOP_B1_ID, 40, DropOffPickupType.COORDINATE_WITH_DRIVER) + .addStopTime(STOP_C1_ID, 55, DropOffPickupType.NONE) + .build(); + + assertSuccess(env.applyTripUpdate(tripUpdate)); + var pattern = assertAddedTrip(ADDED_TRIP_ID, env); + var firstRoute = pattern.getRoute(); + + // apply the update a second time to check that no new route instance is created but the old one is reused + env.applyTripUpdate(tripUpdate); + var secondPattern = assertAddedTrip(ADDED_TRIP_ID, env); + var secondRoute = secondPattern.getRoute(); + + assertSame(firstRoute, secondRoute); + assertNotNull(env.getTransitService().getRouteForId(firstRoute.getId())); + } + + private TripPattern assertAddedTrip(String tripId, RealtimeTestEnvironment env) { + var snapshot = env.getTimetableSnapshot(); + var stopA = env.transitModel.getStopModel().getRegularStop(env.stopA1.getId()); + // Get the trip pattern of the added trip which goes through stopA + var patternsAtA = env.getTimetableSnapshot().getPatternsForStop(stopA); + + assertNotNull(patternsAtA, "Added trip pattern should be found"); + assertEquals(1, patternsAtA.size()); + var tripPattern = patternsAtA.stream().findFirst().get(); + + var forToday = snapshot.resolve(tripPattern, SERVICE_DATE); + var schedule = snapshot.resolve(tripPattern, null); + + assertNotSame(forToday, schedule); + + final int forTodayAddedTripIndex = forToday.getTripIndex(tripId); + assertTrue( + forTodayAddedTripIndex > -1, + "Added trip should be found in time table for service date" + ); + assertEquals( + RealTimeState.ADDED, + forToday.getTripTimes(forTodayAddedTripIndex).getRealTimeState() + ); + + final int scheduleTripIndex = schedule.getTripIndex(tripId); + assertEquals(-1, scheduleTripIndex, "Added trip should not be found in scheduled time table"); + return tripPattern; + } +} diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java index b2c31e2254e..c85225b7828 100644 --- a/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess; import static org.opentripplanner.updater.trip.UpdateIncrementality.DIFFERENTIAL; import com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship; @@ -11,8 +12,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.opentripplanner.model.Timetable; -import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.transit.model.timetable.RealTimeState; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; import org.opentripplanner.updater.trip.TripUpdateBuilder; @@ -32,7 +31,7 @@ static List cases() { @ParameterizedTest @MethodSource("cases") - public void cancelledTrip(ScheduleRelationship relationship, RealTimeState state) { + void cancelledTrip(ScheduleRelationship relationship, RealTimeState state) { var env = RealtimeTestEnvironment.gtfs(); var pattern1 = env.getPatternForTrip(env.trip1); @@ -45,13 +44,11 @@ public void cancelledTrip(ScheduleRelationship relationship, RealTimeState state env.timeZone ) .build(); - var result = env.applyTripUpdate(update); + assertSuccess(env.applyTripUpdate(update)); - assertEquals(1, result.successful()); - - final TimetableSnapshot snapshot = env.getTimetableSnapshot(); - final Timetable forToday = snapshot.resolve(pattern1, RealtimeTestEnvironment.SERVICE_DATE); - final Timetable schedule = snapshot.resolve(pattern1, null); + var snapshot = env.getTimetableSnapshot(); + var forToday = snapshot.resolve(pattern1, RealtimeTestEnvironment.SERVICE_DATE); + var schedule = snapshot.resolve(pattern1, null); assertNotSame(forToday, schedule); assertNotSame(forToday.getTripTimes(tripIndex1), schedule.getTripTimes(tripIndex1)); @@ -73,7 +70,7 @@ public void cancelledTrip(ScheduleRelationship relationship, RealTimeState state */ @ParameterizedTest @MethodSource("cases") - public void cancelingAddedTrip(ScheduleRelationship relationship, RealTimeState state) { + void cancelingAddedTrip(ScheduleRelationship relationship, RealTimeState state) { var env = RealtimeTestEnvironment.gtfs(); var addedTripId = "added-trip"; // First add ADDED trip @@ -88,9 +85,7 @@ public void cancelingAddedTrip(ScheduleRelationship relationship, RealTimeState .addStopTime(env.stopC1.getId().getId(), 55) .build(); - var result = env.applyTripUpdate(update, DIFFERENTIAL); - - assertEquals(1, result.successful()); + assertSuccess(env.applyTripUpdate(update, DIFFERENTIAL)); // Cancel or delete the added trip update = @@ -101,19 +96,17 @@ public void cancelingAddedTrip(ScheduleRelationship relationship, RealTimeState env.timeZone ) .build(); - result = env.applyTripUpdate(update, DIFFERENTIAL); - - assertEquals(1, result.successful()); + assertSuccess(env.applyTripUpdate(update, DIFFERENTIAL)); - final TimetableSnapshot snapshot = env.getTimetableSnapshot(); + var snapshot = env.getTimetableSnapshot(); // Get the trip pattern of the added trip which goes through stopA var patternsAtA = snapshot.getPatternsForStop(env.stopA1); assertNotNull(patternsAtA, "Added trip pattern should be found"); var tripPattern = patternsAtA.stream().findFirst().get(); - final Timetable forToday = snapshot.resolve(tripPattern, RealtimeTestEnvironment.SERVICE_DATE); - final Timetable schedule = snapshot.resolve(tripPattern, null); + var forToday = snapshot.resolve(tripPattern, RealtimeTestEnvironment.SERVICE_DATE); + var schedule = snapshot.resolve(tripPattern, null); assertNotSame(forToday, schedule); diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java index 4cfe1e5500d..5298853f36d 100644 --- a/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java @@ -2,26 +2,29 @@ import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess; +import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.SERVICE_DATE; import org.junit.jupiter.api.Test; -import org.opentripplanner.model.Timetable; -import org.opentripplanner.model.TimetableSnapshot; +import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.RealTimeState; +import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; import org.opentripplanner.updater.trip.TripUpdateBuilder; /** * Delays should be applied to the first trip but should leave the second trip untouched. */ -public class DelayedTest { +class DelayedTest { private static final int DELAY = 1; private static final int STOP_SEQUENCE = 1; @Test - public void delayed() { + void singleStopDelay() { var env = RealtimeTestEnvironment.gtfs(); var tripUpdate = new TripUpdateBuilder( @@ -37,52 +40,86 @@ public void delayed() { assertEquals(1, result.successful()); - // trip1 should be modified - { - var pattern1 = env.getPatternForTrip(env.trip1); - final int trip1Index = pattern1.getScheduledTimetable().getTripIndex(env.trip1.getId()); - - final TimetableSnapshot snapshot = env.getTimetableSnapshot(); - final Timetable trip1Realtime = snapshot.resolve( - pattern1, - RealtimeTestEnvironment.SERVICE_DATE - ); - final Timetable trip1Scheduled = snapshot.resolve(pattern1, null); - - assertNotSame(trip1Realtime, trip1Scheduled); - assertNotSame( - trip1Realtime.getTripTimes(trip1Index), - trip1Scheduled.getTripTimes(trip1Index) - ); - assertEquals(1, trip1Realtime.getTripTimes(trip1Index).getArrivalDelay(STOP_SEQUENCE)); - assertEquals(1, trip1Realtime.getTripTimes(trip1Index).getDepartureDelay(STOP_SEQUENCE)); - - assertEquals( - RealTimeState.SCHEDULED, - trip1Scheduled.getTripTimes(trip1Index).getRealTimeState() - ); - assertEquals( - RealTimeState.UPDATED, - trip1Realtime.getTripTimes(trip1Index).getRealTimeState() - ); - } - - // trip2 should keep the scheduled information - { - var pattern = env.getPatternForTrip(env.trip2); - final int tripIndex = pattern.getScheduledTimetable().getTripIndex(env.trip2.getId()); - - final TimetableSnapshot snapshot = env.getTimetableSnapshot(); - final Timetable realtime = snapshot.resolve(pattern, RealtimeTestEnvironment.SERVICE_DATE); - final Timetable scheduled = snapshot.resolve(pattern, null); - - assertSame(realtime, scheduled); - assertSame(realtime.getTripTimes(tripIndex), scheduled.getTripTimes(tripIndex)); - assertEquals(0, realtime.getTripTimes(tripIndex).getArrivalDelay(STOP_SEQUENCE)); - assertEquals(0, realtime.getTripTimes(tripIndex).getDepartureDelay(STOP_SEQUENCE)); - - assertEquals(RealTimeState.SCHEDULED, scheduled.getTripTimes(tripIndex).getRealTimeState()); - assertEquals(RealTimeState.SCHEDULED, realtime.getTripTimes(tripIndex).getRealTimeState()); - } + var pattern1 = env.getPatternForTrip(env.trip1); + int trip1Index = pattern1.getScheduledTimetable().getTripIndex(env.trip1.getId()); + + var snapshot = env.getTimetableSnapshot(); + var trip1Realtime = snapshot.resolve(pattern1, RealtimeTestEnvironment.SERVICE_DATE); + var trip1Scheduled = snapshot.resolve(pattern1, null); + + assertNotSame(trip1Realtime, trip1Scheduled); + assertNotSame(trip1Realtime.getTripTimes(trip1Index), trip1Scheduled.getTripTimes(trip1Index)); + assertEquals(DELAY, trip1Realtime.getTripTimes(trip1Index).getArrivalDelay(STOP_SEQUENCE)); + assertEquals(DELAY, trip1Realtime.getTripTimes(trip1Index).getDepartureDelay(STOP_SEQUENCE)); + + assertEquals( + RealTimeState.SCHEDULED, + trip1Scheduled.getTripTimes(trip1Index).getRealTimeState() + ); + + assertEquals( + "SCHEDULED | A1 0:00:10 0:00:11 | B1 0:00:20 0:00:21", + env.getScheduledTimetable(env.trip1.getId()) + ); + assertEquals( + "UPDATED | A1 [ND] 0:00:10 0:00:11 | B1 0:00:21 0:00:22", + env.getRealtimeTimetable(env.trip1.getId().getId()) + ); + } + + /** + * Tests delays to multiple stop times, where arrival and departure do not have the same delay. + */ + @Test + void complexDelay() { + var env = RealtimeTestEnvironment.gtfs(); + + var tripId = env.trip2.getId().getId(); + + var tripUpdate = new TripUpdateBuilder(tripId, SERVICE_DATE, SCHEDULED, env.timeZone) + .addDelayedStopTime(0, 0) + .addDelayedStopTime(1, 60, 80) + .addDelayedStopTime(2, 90, 90) + .build(); + + assertSuccess(env.applyTripUpdate(tripUpdate)); + + var snapshot = env.getTimetableSnapshot(); + + final TripPattern originalTripPattern = env.getTransitService().getPatternForTrip(env.trip2); + + var originalTimetableForToday = snapshot.resolve(originalTripPattern, SERVICE_DATE); + var originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); + + assertNotSame(originalTimetableForToday, originalTimetableScheduled); + + final int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(tripId); + assertTrue( + originalTripIndexScheduled > -1, + "Original trip should be found in scheduled time table" + ); + final TripTimes originalTripTimesScheduled = originalTimetableScheduled.getTripTimes( + originalTripIndexScheduled + ); + assertFalse( + originalTripTimesScheduled.isCanceledOrDeleted(), + "Original trip times should not be canceled in scheduled time table" + ); + assertEquals(RealTimeState.SCHEDULED, originalTripTimesScheduled.getRealTimeState()); + + final int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); + assertTrue( + originalTripIndexForToday > -1, + "Original trip should be found in time table for service date" + ); + + assertEquals( + "SCHEDULED | A1 0:01 0:01:01 | B1 0:01:10 0:01:11 | C1 0:01:20 0:01:21", + env.getScheduledTimetable(env.trip2.getId()) + ); + assertEquals( + "UPDATED | A1 0:01 0:01:01 | B1 0:02:10 0:02:31 | C1 0:02:50 0:02:51", + env.getRealtimeTimetable(env.trip2) + ); } } diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java index 9afb7e76261..de699324bb6 100644 --- a/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java @@ -6,16 +6,14 @@ import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess; +import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.SERVICE_DATE; import static org.opentripplanner.updater.trip.UpdateIncrementality.DIFFERENTIAL; import org.junit.jupiter.api.Test; -import org.opentripplanner.model.Timetable; -import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.RealTimeState; -import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesStringBuilder; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; import org.opentripplanner.updater.trip.TripUpdateBuilder; @@ -25,98 +23,26 @@ public class SkippedTest { @Test - public void scheduledTripWithSkippedAndScheduled() { + void scheduledTripWithSkippedAndScheduled() { var env = RealtimeTestEnvironment.gtfs(); String scheduledTripId = env.trip2.getId().getId(); - var tripUpdate = new TripUpdateBuilder( - scheduledTripId, - RealtimeTestEnvironment.SERVICE_DATE, - SCHEDULED, - env.timeZone - ) + var tripUpdate = new TripUpdateBuilder(scheduledTripId, SERVICE_DATE, SCHEDULED, env.timeZone) .addDelayedStopTime(0, 0) .addSkippedStop(1) .addDelayedStopTime(2, 90) .build(); - var result = env.applyTripUpdate(tripUpdate); - - assertEquals(1, result.successful()); - - final TimetableSnapshot snapshot = env.getTimetableSnapshot(); - - // Original trip pattern - { - final FeedScopedId tripId = env.trip2.getId(); - final Trip trip = env.transitModel.getTransitModelIndex().getTripForId().get(tripId); - final TripPattern originalTripPattern = env.transitModel - .getTransitModelIndex() - .getPatternForTrip() - .get(trip); - - final Timetable originalTimetableForToday = snapshot.resolve( - originalTripPattern, - RealtimeTestEnvironment.SERVICE_DATE - ); - final Timetable originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); - - assertNotSame(originalTimetableForToday, originalTimetableScheduled); - - final int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); - final TripTimes originalTripTimesForToday = originalTimetableForToday.getTripTimes( - originalTripIndexForToday - ); - assertTrue( - originalTripTimesForToday.isDeleted(), - "Original trip times should be deleted in time table for service date" - ); - // original trip should be canceled - assertEquals(RealTimeState.DELETED, originalTripTimesForToday.getRealTimeState()); - } - - // New trip pattern - { - final TripPattern newTripPattern = snapshot.getRealtimeAddedTripPattern( - env.trip2.getId(), - RealtimeTestEnvironment.SERVICE_DATE - ); - - final Timetable newTimetableForToday = snapshot.resolve( - newTripPattern, - RealtimeTestEnvironment.SERVICE_DATE - ); - final Timetable newTimetableScheduled = snapshot.resolve(newTripPattern, null); - - assertNotSame(newTimetableForToday, newTimetableScheduled); - - assertTrue(newTripPattern.canBoard(0)); - assertFalse(newTripPattern.canBoard(1)); - assertTrue(newTripPattern.canBoard(2)); - - final int newTimetableForTodayModifiedTripIndex = newTimetableForToday.getTripIndex( - scheduledTripId - ); - - var newTripTimes = newTimetableForToday.getTripTimes(newTimetableForTodayModifiedTripIndex); - assertEquals(RealTimeState.UPDATED, newTripTimes.getRealTimeState()); - - assertEquals( - -1, - newTimetableScheduled.getTripIndex(scheduledTripId), - "New trip should not be found in scheduled time table" - ); - - assertEquals(0, newTripTimes.getArrivalDelay(0)); - assertEquals(0, newTripTimes.getDepartureDelay(0)); - assertEquals(42, newTripTimes.getArrivalDelay(1)); - assertEquals(47, newTripTimes.getDepartureDelay(1)); - assertEquals(90, newTripTimes.getArrivalDelay(2)); - assertEquals(90, newTripTimes.getDepartureDelay(2)); - assertFalse(newTripTimes.isCancelledStop(0)); - assertTrue(newTripTimes.isCancelledStop(1)); - assertFalse(newTripTimes.isNoDataStop(2)); - } + assertSuccess(env.applyTripUpdate(tripUpdate)); + + assertOriginalTripPatternIsDeleted(env, env.trip2.getId()); + + assertNewTripTimesIsUpdated(env, env.trip2.getId()); + + assertEquals( + "UPDATED | A1 0:01 0:01:01 | B1 [C] 0:01:52 0:01:58 | C1 0:02:50 0:02:51", + env.getRealtimeTimetable(scheduledTripId) + ); } /** @@ -129,29 +55,22 @@ public void scheduledTripWithSkippedAndScheduled() { * the new stop-skipping trip pattern should also be removed. */ @Test - public void scheduledTripWithPreviouslySkipped() { + void scheduledTripWithPreviouslySkipped() { var env = RealtimeTestEnvironment.gtfs(); var tripId = env.trip2.getId(); - var tripUpdate = new TripUpdateBuilder( - tripId.getId(), - RealtimeTestEnvironment.SERVICE_DATE, - SCHEDULED, - env.timeZone - ) + var tripUpdate = new TripUpdateBuilder(tripId.getId(), SERVICE_DATE, SCHEDULED, env.timeZone) .addDelayedStopTime(0, 0) .addSkippedStop(1) .addDelayedStopTime(2, 90) .build(); - var result = env.applyTripUpdate(tripUpdate, DIFFERENTIAL); - - assertEquals(1, result.successful()); + assertSuccess(env.applyTripUpdate(tripUpdate, DIFFERENTIAL)); // Create update to the same trip but now the skipped stop is no longer skipped var scheduledBuilder = new TripUpdateBuilder( tripId.getId(), - RealtimeTestEnvironment.SERVICE_DATE, + SERVICE_DATE, SCHEDULED, env.timeZone ) @@ -162,64 +81,130 @@ public void scheduledTripWithPreviouslySkipped() { tripUpdate = scheduledBuilder.build(); // apply the update with the previously skipped stop now scheduled - result = env.applyTripUpdate(tripUpdate, DIFFERENTIAL); + assertSuccess(env.applyTripUpdate(tripUpdate, DIFFERENTIAL)); - assertEquals(1, result.successful()); // Check that the there is no longer a realtime added trip pattern for the trip and that the // stoptime updates have gone through var snapshot = env.getTimetableSnapshot(); - { - final TripPattern newTripPattern = snapshot.getRealtimeAddedTripPattern( - env.trip2.getId(), - RealtimeTestEnvironment.SERVICE_DATE - ); - assertNull(newTripPattern); - final Trip trip = env.transitModel.getTransitModelIndex().getTripForId().get(tripId); - - final TripPattern originalTripPattern = env.transitModel - .getTransitModelIndex() - .getPatternForTrip() - .get(trip); - final Timetable originalTimetableForToday = snapshot.resolve( - originalTripPattern, - RealtimeTestEnvironment.SERVICE_DATE - ); - - final Timetable originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); - - assertNotSame(originalTimetableForToday, originalTimetableScheduled); - - final int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(tripId); - - assertTrue( - originalTripIndexScheduled > -1, - "Original trip should be found in scheduled time table" - ); - final TripTimes originalTripTimesScheduled = originalTimetableScheduled.getTripTimes( - originalTripIndexScheduled - ); - assertFalse( - originalTripTimesScheduled.isCanceledOrDeleted(), - "Original trip times should not be canceled in scheduled time table" - ); - assertEquals(RealTimeState.SCHEDULED, originalTripTimesScheduled.getRealTimeState()); - final int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); - - assertTrue( - originalTripIndexForToday > -1, - "Original trip should be found in time table for service date" - ); - final TripTimes originalTripTimesForToday = originalTimetableForToday.getTripTimes( - originalTripIndexForToday - ); - assertEquals(RealTimeState.UPDATED, originalTripTimesForToday.getRealTimeState()); - assertEquals(0, originalTripTimesForToday.getArrivalDelay(0)); - assertEquals(0, originalTripTimesForToday.getDepartureDelay(0)); - assertEquals(50, originalTripTimesForToday.getArrivalDelay(1)); - assertEquals(50, originalTripTimesForToday.getDepartureDelay(1)); - assertEquals(90, originalTripTimesForToday.getArrivalDelay(2)); - assertEquals(90, originalTripTimesForToday.getDepartureDelay(2)); - } + assertNull(snapshot.getRealtimeAddedTripPattern(tripId, SERVICE_DATE)); + + assertNewTripTimesIsUpdated(env, tripId); + + assertEquals( + "SCHEDULED | A1 0:01 0:01:01 | B1 0:01:10 0:01:11 | C1 0:01:20 0:01:21", + env.getScheduledTimetable(tripId) + ); + assertEquals( + "UPDATED | A1 0:01 0:01:01 | B1 0:02 0:02:01 | C1 0:02:50 0:02:51", + env.getRealtimeTimetable(tripId, SERVICE_DATE) + ); + } + + /** + * Tests a mixture of SKIPPED and NO_DATA. + */ + @Test + void skippedNoData() { + var env = RealtimeTestEnvironment.gtfs(); + + final FeedScopedId tripId = env.trip2.getId(); + + var tripUpdate = new TripUpdateBuilder(tripId.getId(), SERVICE_DATE, SCHEDULED, env.timeZone) + .addNoDataStop(0) + .addSkippedStop(1) + .addNoDataStop(2) + .build(); + + assertSuccess(env.applyTripUpdate(tripUpdate)); + + assertOriginalTripPatternIsDeleted(env, tripId); + + assertNewTripTimesIsUpdated(env, tripId); + + assertEquals( + "UPDATED | A1 [ND] 0:01 0:01:01 | B1 [C] 0:01:10 0:01:11 | C1 [ND] 0:01:20 0:01:21", + env.getRealtimeTimetable(env.trip2) + ); + } + + private static void assertOriginalTripPatternIsDeleted( + RealtimeTestEnvironment env, + FeedScopedId tripId + ) { + var trip = env.getTransitService().getTripForId(tripId); + var originalTripPattern = env.getTransitService().getPatternForTrip(trip); + var snapshot = env.getTimetableSnapshot(); + var originalTimetableForToday = snapshot.resolve(originalTripPattern, SERVICE_DATE); + var originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); + + assertNotSame(originalTimetableForToday, originalTimetableScheduled); + + int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(tripId); + assertTrue( + originalTripIndexScheduled > -1, + "Original trip should be found in scheduled time table" + ); + var originalTripTimesScheduled = originalTimetableScheduled.getTripTimes( + originalTripIndexScheduled + ); + assertFalse( + originalTripTimesScheduled.isCanceledOrDeleted(), + "Original trip times should not be canceled in scheduled time table" + ); + + assertEquals( + "SCHEDULED | A1 0:01 0:01:01 | B1 0:01:10 0:01:11 | C1 0:01:20 0:01:21", + TripTimesStringBuilder.encodeTripTimes(originalTripTimesScheduled, originalTripPattern) + ); + + int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); + assertTrue( + originalTripIndexForToday > -1, + "Original trip should be found in time table for service date" + ); + var originalTripTimesForToday = originalTimetableForToday.getTripTimes( + originalTripIndexForToday + ); + assertTrue( + originalTripTimesForToday.isDeleted(), + "Original trip times should be deleted in time table for service date" + ); + // original trip should be deleted + assertEquals(RealTimeState.DELETED, originalTripTimesForToday.getRealTimeState()); + } + + private static void assertNewTripTimesIsUpdated( + RealtimeTestEnvironment env, + FeedScopedId tripId + ) { + var originalTripPattern = env.getTransitService().getPatternForTrip(env.trip2); + var snapshot = env.getTimetableSnapshot(); + var originalTimetableForToday = snapshot.resolve(originalTripPattern, SERVICE_DATE); + + var originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); + + assertNotSame(originalTimetableForToday, originalTimetableScheduled); + + int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(tripId); + + assertTrue( + originalTripIndexScheduled > -1, + "Original trip should be found in scheduled time table" + ); + var originalTripTimesScheduled = originalTimetableScheduled.getTripTimes( + originalTripIndexScheduled + ); + assertFalse( + originalTripTimesScheduled.isCanceledOrDeleted(), + "Original trip times should not be canceled in scheduled time table" + ); + assertEquals(RealTimeState.SCHEDULED, originalTripTimesScheduled.getRealTimeState()); + int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); + + assertTrue( + originalTripIndexForToday > -1, + "Original trip should be found in time table for service date" + ); } } diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java index 00831333943..da362451753 100644 --- a/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java @@ -1,14 +1,13 @@ package org.opentripplanner.updater.trip.moduletests.rejection; import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_SERVICE_ON_DATE; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertFailure; import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.SERVICE_DATE; import java.time.LocalDate; import java.util.List; -import java.util.Set; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; @@ -18,7 +17,7 @@ * A trip with start date that is outside the service period shouldn't throw an exception and is * ignored instead. */ -public class InvalidInputTest { +class InvalidInputTest { public static List cases() { return List.of(SERVICE_DATE.minusYears(10), SERVICE_DATE.plusYears(10)); @@ -26,21 +25,17 @@ public static List cases() { @ParameterizedTest @MethodSource("cases") - public void invalidTripDate(LocalDate date) { + void invalidTripDate(LocalDate date) { var env = RealtimeTestEnvironment.gtfs(); var update = new TripUpdateBuilder(env.trip1.getId().getId(), date, SCHEDULED, env.timeZone) - .addDelayedStopTime(1, 0) .addDelayedStopTime(2, 60, 80) - .addDelayedStopTime(3, 90, 90) .build(); var result = env.applyTripUpdate(update); var snapshot = env.getTimetableSnapshot(); assertTrue(snapshot.isEmpty()); - assertEquals(1, result.failed()); - var errors = result.failures().keySet(); - assertEquals(Set.of(NO_SERVICE_ON_DATE), errors); + assertFailure(NO_SERVICE_ON_DATE, result); } } diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidTripIdTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidTripIdTest.java new file mode 100644 index 00000000000..83c2547dbc7 --- /dev/null +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidTripIdTest.java @@ -0,0 +1,38 @@ +package org.opentripplanner.updater.trip.moduletests.rejection; + +import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED; +import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_INPUT_STRUCTURE; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertFailure; + +import com.google.transit.realtime.GtfsRealtime; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.updater.trip.RealtimeTestEnvironment; + +class InvalidTripIdTest { + + static Stream invalidCases() { + return Stream.of(null, "", " "); + } + + /** + * This test just asserts that invalid trip ids don't throw an exception and are ignored instead + */ + @ParameterizedTest(name = "tripId=\"{0}\"") + @MethodSource("invalidCases") + void invalidTripId(String tripId) { + var env = RealtimeTestEnvironment.gtfs(); + var tripDescriptorBuilder = GtfsRealtime.TripDescriptor.newBuilder(); + if (tripId != null) { + tripDescriptorBuilder.setTripId(tripId); + } + tripDescriptorBuilder.setScheduleRelationship(SCHEDULED); + var tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); + + tripUpdateBuilder.setTrip(tripDescriptorBuilder); + var tripUpdate = tripUpdateBuilder.build(); + + assertFailure(INVALID_INPUT_STRUCTURE, env.applyTripUpdate(tripUpdate)); + } +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/nearest.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/nearest.json index 82d929adeb1..b6b5b7ee674 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/nearest.json +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/nearest.json @@ -10,6 +10,20 @@ "parentStation" : null } } + }, + { + "node" : { + "place" : { + "stationId" : "Network-1:FooStation" + } + } + }, + { + "node" : { + "place" : { + "vehicleId" : "Network-1:free-floating-bicycle" + } + } } ] } diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/rental-vehicle.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/rental-vehicle.json new file mode 100644 index 00000000000..9017fe77a93 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/rental-vehicle.json @@ -0,0 +1,21 @@ +{ + "data": { + "rentalVehicle": { + "vehicleId":"Network-1:free-floating-bicycle", + "name":"free-floating-bicycle", + "allowPickupNow":true, + "lon":19.01, + "lat":47.52, + "rentalUris":null, + "operative":true, + "vehicleType": { + "formFactor":"BICYCLE", + "propulsionType":"HUMAN" + }, + "rentalNetwork": { + "networkId":"Network-1", + "url":"https://foo.bar" + } + } + } +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json index ef1284c5c5e..ad1ce76d9be 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/vehicle-rental-station.json @@ -47,13 +47,16 @@ "allowPickup" : false, "allowDropoffNow" : false, "allowPickupNow" : false, - "network" : "Network-1", "lon" : 18.99, "lat" : 47.51, "capacity" : null, "allowOverloading" : false, "rentalUris" : null, - "operative" : false + "operative" : false, + "rentalNetwork" : { + "networkId" : "Network-1", + "url" : "https://foo.bar" + } } } } diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/nearest.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/nearest.graphql index c7f8eed4213..469a117bab4 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/queries/nearest.graphql +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/nearest.graphql @@ -1,5 +1,11 @@ { - nearest(lat: 60.19915, lon: 24.94089, maxDistance: 500) { + nearest( + lat: 60.19915 + lon: 24.94089 + maxDistance: 500 + filterByPlaceTypes: [STOP, VEHICLE_RENT] + filterByNetwork: ["Network-1"] + ) { edges { node { place { @@ -10,6 +16,12 @@ id } } + ... on RentalVehicle { + vehicleId + } + ... on VehicleRentalStation { + stationId + } } } } diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/rental-vehicle.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/rental-vehicle.graphql new file mode 100644 index 00000000000..9a912781c56 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/rental-vehicle.graphql @@ -0,0 +1,23 @@ +{ + rentalVehicle(id: "Network-1:free-floating-bicycle") { + vehicleId + name + allowPickupNow + lon + lat + rentalUris { + android + ios + web + } + operative + vehicleType { + formFactor + propulsionType + } + rentalNetwork { + networkId + url + } + } +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql index 8e555ffdbdd..a2200465912 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/vehicle-rental-station.graphql @@ -28,7 +28,6 @@ allowPickup allowDropoffNow allowPickupNow - network lon lat capacity @@ -39,5 +38,9 @@ web } operative + rentalNetwork { + networkId + url + } } }