diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index 0c085b12678..5cae15849ef 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -166,9 +166,9 @@ jobs: if [ ${{ github.ref }} = 'refs/heads/master' ]; then - mike deploy --branch $LOCAL_BRANCH --prefix en --title=$MASTER_BRANCH_VERSION --update-aliases v$MASTER_BRANCH_VERSION latest + mike deploy --branch $LOCAL_BRANCH --deploy-prefix en --title=$MASTER_BRANCH_VERSION --update-aliases v$MASTER_BRANCH_VERSION latest else - mike deploy --branch $LOCAL_BRANCH --prefix en dev-2.x + mike deploy --branch $LOCAL_BRANCH --deploy-prefix en dev-2.x fi # commit and push the GraphQL documentation if the schema file is newer than the @@ -218,7 +218,7 @@ jobs: run: mvn --batch-mode compile -DskipTests -P prettierSkip container-image: - if: github.repository_owner == 'opentripplanner' && github.event_name == 'push' && github.ref == 'refs/heads/dev-2.x' + if: github.repository_owner == 'opentripplanner' && github.event_name == 'push' && (github.ref == 'refs/heads/dev-2.x' || github.ref == 'refs/heads/master') runs-on: ubuntu-latest needs: - build-linux @@ -244,7 +244,14 @@ jobs: version_with_snapshot=`mvn -q help:evaluate -Dexpression=project.version -q -DforceStdout` version=${version_with_snapshot/-SNAPSHOT/} - image_date=`date +%Y-%m-%dT%H-%M` - image_version="${version}_${image_date}" + + image_version=${version} + + ## if the Maven version contains SNAPSHOT, then add date to tag + if [[ $version_with_snapshot == *"SNAPSHOT"* ]]; then + image_date=`date +%Y-%m-%dT%H-%M` + image_version="${version}_${image_date}" + echo "Maven version ${version_with_snapshot} contains SNAPSHOT, adding date to container image tag" + fi mvn --batch-mode -P prettierSkip compile com.google.cloud.tools:jib-maven-plugin:build -Djib.to.tags=latest,$image_version diff --git a/.gitignore b/.gitignore index 3c2c29d911e..b98d0263a70 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,8 @@ src/ext/resources/reportapi/report.csv streetGraph.obj graph.obj +# IntelliJ creates these pid files when you attach the debugger to tests +.attach_pid* smoke-tests/*.jar smoke-tests/**/*.obj diff --git a/doc-templates/Configuration.md b/doc-templates/Configuration.md index ebbe5501b61..003fe28d794 100644 --- a/doc-templates/Configuration.md +++ b/doc-templates/Configuration.md @@ -146,7 +146,7 @@ text inserted is valid JSON (starts with `{` and ends with `}`). Variable substitution is performed on configuration file after the include file directive; Hence variable substitution is also performed on the text in the injected file. -Here is an example including variable substitution, assuming version 2.3.0 of OTP: +Here is an example including variable substitution, assuming version 2.4.0 of OTP: ```JSON // build-config.json @@ -170,7 +170,7 @@ The result will look like this: { "transitFeeds": [ { - "source": "netex-v2.3.0.obj" + "source": "netex-v2.4.0.obj" } ] } diff --git a/docs/Basic-Tutorial.md b/docs/Basic-Tutorial.md index 33473c5d526..788c85aaad0 100644 --- a/docs/Basic-Tutorial.md +++ b/docs/Basic-Tutorial.md @@ -18,9 +18,9 @@ JAR containing all other libraries needed for OTP to work, and is available from repository. You will be able to go to [the OTP directory at Maven Central](https://repo1.maven.org/maven2/org/opentripplanner/otp/), navigate to -the [directory of releases](https://repo1.maven.org/maven2/org/opentripplanner/otp/2.3.0/), +the [directory of releases](https://repo1.maven.org/maven2/org/opentripplanner/otp/2.4.0/), and download -the [file with `shaded.jar` suffix](https://repo1.maven.org/maven2/org/opentripplanner/otp/2.3.0/otp-2.3.0-shaded.jar) +the [file with `shaded.jar` suffix](https://repo1.maven.org/maven2/org/opentripplanner/otp/2.4.0/otp-2.4.0-shaded.jar) . You may also want to get your own copy of the OTP source code @@ -127,7 +127,7 @@ below and in other tutorials. The simplest way to use OTP is to build a graph in a single step and start a server immediately, without saving it to disk. The command to do so is: - $ java -Xmx2G -jar otp-2.3.0-shaded.jar --build --serve /home/username/otp + $ java -Xmx2G -jar otp-2.4.0-shaded.jar --build --serve /home/username/otp where `/home/username/otp` should be the directory where you put your configuration and input files. @@ -151,13 +151,13 @@ build a graph from street and transit data then save it to a file using the `--b command line parameters together. If for example your current working directory (`.`) contains the input files and the OTP JAR file, you can use this command: - $ java -Xmx2G -jar otp-2.3.0-shaded.jar --build --save . + $ java -Xmx2G -jar otp-2.4.0-shaded.jar --build --save . This will produce a file called `graph.obj` in the same directory as the inputs. The server can then be started later using the `--load` parameter, and will read this file instead of building the graph from scratch: - $ java -Xmx2G -jar otp-2.3.0-shaded.jar --load . + $ java -Xmx2G -jar otp-2.4.0-shaded.jar --load . Another reason to perform these two phases separately is that the building process loads the entire GTFS and OSM data sets into memory, so can require significantly more memory than just running a @@ -174,16 +174,16 @@ graph once, and then layer transit data on top of the streets to make the final Again assuming the input files and OTP JAR file are in the current working directory, you can build a street graph with OSM and elevation data only (ignoring transit input files) with this command: - $ java -Xmx2G -jar otp-2.3.0-shaded.jar --buildStreet . + $ java -Xmx2G -jar otp-2.4.0-shaded.jar --buildStreet . Then, to build a graph layering transit data on top of the saved street graph (built using the previous command): - $ java -Xmx2G -jar otp-2.3.0-shaded.jar --loadStreet --save . + $ java -Xmx2G -jar otp-2.4.0-shaded.jar --loadStreet --save . Finally, the server can be started using the `--load` parameter: - $ java -Xmx2G -jar otp-2.3.0-shaded.jar --load . + $ java -Xmx2G -jar otp-2.4.0-shaded.jar --load . ## Command Line Switches diff --git a/docs/BuildConfiguration.md b/docs/BuildConfiguration.md index 9c8eb5ad5c0..d382d07f673 100644 --- a/docs/BuildConfiguration.md +++ b/docs/BuildConfiguration.md @@ -20,8 +20,6 @@ Sections follow that describe particular settings in more depth. | Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | |--------------------------------------------------------------------------|:-----------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------:|-----------------------------------|:-----:| | [areaVisibility](#areaVisibility) | `boolean` | Perform visibility calculations. | *Optional* | `false` | 1.5 | -| banDiscouragedBiking | `boolean` | Should biking be allowed on OSM ways tagged with `bicycle=discouraged` | *Optional* | `false` | 2.0 | -| banDiscouragedWalking | `boolean` | Should walking be allowed on OSM ways tagged with `foot=discouraged` | *Optional* | `false` | 2.0 | | [buildReportDir](#buildReportDir) | `uri` | URI to the directory where the graph build report should be written to. | *Optional* | | 2.0 | | [configVersion](#configVersion) | `string` | Deployment version of the *build-config.json*. | *Optional* | | 2.1 | | [dataImportReport](#dataImportReport) | `boolean` | Generate nice HTML report of Graph errors/warnings | *Optional* | `false` | 2.0 | diff --git a/docs/Changelog.md b/docs/Changelog.md index 9b91d562dea..5240802ac9e 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -3,28 +3,45 @@ The changelog lists most feature changes between each release. The list is automatically created based on merged pull requests. Search GitHub issues and pull requests for smaller issues. -## 2.4.0 (in progress) +## 2.5.0 (under development) + +- Gracefully handle nullable fields in TransitAlert [#5349](https://github.com/opentripplanner/OpenTripPlanner/pull/5349) +- Remove transit with higher cost than best on-street itinerary filter [#5222](https://github.com/opentripplanner/OpenTripPlanner/pull/5222) +- Remove banDiscouragedCycling and banDiscouragedWalking [#5341](https://github.com/opentripplanner/OpenTripPlanner/pull/5341) +- Fix rental scooter access [#5361](https://github.com/opentripplanner/OpenTripPlanner/pull/5361) +- De-duplicate stops returned by `stopsByRadius` [#5366](https://github.com/opentripplanner/OpenTripPlanner/pull/5366) +- Fix value mapping for bikesAllowed in GTFS GraphQL API [#5368](https://github.com/opentripplanner/OpenTripPlanner/pull/5368) +[](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) + +## 2.4.0 (2023-09-13) + +### Notable Changes + +- Improved support for Fares V2 [#4917](https://github.com/opentripplanner/OpenTripPlanner/pull/4917) [#5227](https://github.com/opentripplanner/OpenTripPlanner/pull/5227) +- Improved error and timeout handling [#5047](https://github.com/opentripplanner/OpenTripPlanner/pull/5047) [#5092](https://github.com/opentripplanner/OpenTripPlanner/pull/5092) [#5121](https://github.com/opentripplanner/OpenTripPlanner/pull/5121) [#5114](https://github.com/opentripplanner/OpenTripPlanner/pull/5114) [#5133](https://github.com/opentripplanner/OpenTripPlanner/pull/5133) [#5130](https://github.com/opentripplanner/OpenTripPlanner/pull/5130) [#5192](https://github.com/opentripplanner/OpenTripPlanner/pull/5192) +- Enable GTFS GraphQL API by default, remove the word "legacy" from its name and documentation [#5202](https://github.com/opentripplanner/OpenTripPlanner/pull/5202) +- Access and egress penalty on time and cost [#5180](https://github.com/opentripplanner/OpenTripPlanner/pull/5180) +- Improve support for GBFS geofencing zones [#5201](https://github.com/opentripplanner/OpenTripPlanner/pull/5201) +- Reduce memory consumption by 5-8% [#5223](https://github.com/opentripplanner/OpenTripPlanner/pull/5223) +- Stop count limit for access/egress routing and new accessEgress configuration object [#5214](https://github.com/opentripplanner/OpenTripPlanner/pull/5214) + +### Detailed changes by Pull Request - Generate static documentation for GTFS GraphQL API [#5069](https://github.com/opentripplanner/OpenTripPlanner/pull/5069) - Create a valid area even when it has too many vertices [#5019](https://github.com/opentripplanner/OpenTripPlanner/pull/5019) - Constant speed street routing [#5057](https://github.com/opentripplanner/OpenTripPlanner/pull/5057) - Remove dead build configuration parameter (extraEdgesStopPlatformLink) [#5080](https://github.com/opentripplanner/OpenTripPlanner/pull/5080) - Configure HTTP WebServer Transaction timeouts [#5047](https://github.com/opentripplanner/OpenTripPlanner/pull/5047) -- Add Fares v2 to GraphQL API [#4917](https://github.com/opentripplanner/OpenTripPlanner/pull/4917) - Improve Graph updaters shutdown [#5092](https://github.com/opentripplanner/OpenTripPlanner/pull/5092) -- Add deduplicated "stop clusters" to geocoding sandbox API [#5084](https://github.com/opentripplanner/OpenTripPlanner/pull/5084) - Fix flex timeshift [#5063](https://github.com/opentripplanner/OpenTripPlanner/pull/5063) - Merge norway traversal calculator into default [#5106](https://github.com/opentripplanner/OpenTripPlanner/pull/5106) - Use correct GTFS sequence number in vehicle position matcher [#5090](https://github.com/opentripplanner/OpenTripPlanner/pull/5090) -- Fix SIRI ET PubSub updater shutdown [#5104](https://github.com/opentripplanner/OpenTripPlanner/pull/5104) - OSM: Break out of processing a malformed level map [#5096](https://github.com/opentripplanner/OpenTripPlanner/pull/5096) -- Create unique SIRI-ET PubSub subscription [#5118](https://github.com/opentripplanner/OpenTripPlanner/pull/5118) - Handle JsonParseException [#5121](https://github.com/opentripplanner/OpenTripPlanner/pull/5121) -- Add modes to geocoding [#5115](https://github.com/opentripplanner/OpenTripPlanner/pull/5115) - Do not enforce API processing timeout for parallel routing [#5114](https://github.com/opentripplanner/OpenTripPlanner/pull/5114) - Rename bikeRental options to vehicleRental [#5089](https://github.com/opentripplanner/OpenTripPlanner/pull/5089) - Fare sandbox cleanup, remove MultipleFareService [#5100](https://github.com/opentripplanner/OpenTripPlanner/pull/5100) -- Add validation of NeTEX timetabled passing times [#5081](https://github.com/opentripplanner/OpenTripPlanner/pull/5081) +- Add validation of NeTEx timetabled passing times [#5081](https://github.com/opentripplanner/OpenTripPlanner/pull/5081) - Return WALKING_BETTER_THAN_TRANSIT only on a fully walking leg [#5091](https://github.com/opentripplanner/OpenTripPlanner/pull/5091) - Handle CoercingParseValueException [#5133](https://github.com/opentripplanner/OpenTripPlanner/pull/5133) - Remove broken Jersey tracing configuration [#5142](https://github.com/opentripplanner/OpenTripPlanner/pull/5142) @@ -33,58 +50,40 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Fix vertex removal race condition [#5141](https://github.com/opentripplanner/OpenTripPlanner/pull/5141) - Comment out replacing DSJ-ID from planned data with ID from realtime-data [#5140](https://github.com/opentripplanner/OpenTripPlanner/pull/5140) - Remove San Francisco and vehicle rental fare calculators [#5145](https://github.com/opentripplanner/OpenTripPlanner/pull/5145) -- Make update frequency a Duration [#5152](https://github.com/opentripplanner/OpenTripPlanner/pull/5152) - Remove batch query from Transmodel API [#5147](https://github.com/opentripplanner/OpenTripPlanner/pull/5147) - Fix nullable absolute direction in GTFS GraphQL API [#5159](https://github.com/opentripplanner/OpenTripPlanner/pull/5159) - Fix error in flex validation [#5161](https://github.com/opentripplanner/OpenTripPlanner/pull/5161) - Check service dates instead of service ids for block id transfers [#5162](https://github.com/opentripplanner/OpenTripPlanner/pull/5162) -- Improve updater log messages [#5168](https://github.com/opentripplanner/OpenTripPlanner/pull/5168) - Add support for mapping NeTEx operating day in operating period [#5167](https://github.com/opentripplanner/OpenTripPlanner/pull/5167) -- Relax validity check on flex trip with null duration [#5169](https://github.com/opentripplanner/OpenTripPlanner/pull/5169) -- Log unexpected vehicle type in GBFS update [#5175](https://github.com/opentripplanner/OpenTripPlanner/pull/5175) - Validate to/from in routing request [#5164](https://github.com/opentripplanner/OpenTripPlanner/pull/5164) - Changing default value for earlyStartSec [#5165](https://github.com/opentripplanner/OpenTripPlanner/pull/5165) - Add GTFS stop sequence to GTFS GraphQL API [#5153](https://github.com/opentripplanner/OpenTripPlanner/pull/5153) - Remove walk leg in a stay seated transfer [#5135](https://github.com/opentripplanner/OpenTripPlanner/pull/5135) - Validate distinct from/to temporary vertices [#5181](https://github.com/opentripplanner/OpenTripPlanner/pull/5181) -- Add support for taxi mode [#5183](https://github.com/opentripplanner/OpenTripPlanner/pull/5183) +- Add support for NeTEx taxi mode [#5183](https://github.com/opentripplanner/OpenTripPlanner/pull/5183) - Fix bike triangle in Transmodel API [#5179](https://github.com/opentripplanner/OpenTripPlanner/pull/5179) - Bug fixes in stop area relation processing [#5166](https://github.com/opentripplanner/OpenTripPlanner/pull/5166) -- Enable GTFS GraphQL API by default, remove the word "legacy" from its documentation [#5202](https://github.com/opentripplanner/OpenTripPlanner/pull/5202) - Allow underscores in GTFS feed IDs [#5191](https://github.com/opentripplanner/OpenTripPlanner/pull/5191) - Area vertex linking improvements [#5209](https://github.com/opentripplanner/OpenTripPlanner/pull/5209) - Allow multiple FlexibleAreas in a FlexibleStopPlace [#4922](https://github.com/opentripplanner/OpenTripPlanner/pull/4922) -- Preemptively make a GraphQL FareProduct an interface [#5207](https://github.com/opentripplanner/OpenTripPlanner/pull/5207) -- Access and egress penalty on time and cost [#5180](https://github.com/opentripplanner/OpenTripPlanner/pull/5180) - Prevent NPE in vehicle position matching [#5212](https://github.com/opentripplanner/OpenTripPlanner/pull/5212) - Empty stop_headsign will fall back to trip_headsign [#5205](https://github.com/opentripplanner/OpenTripPlanner/pull/5205) -- Refactor HTTP client [#5213](https://github.com/opentripplanner/OpenTripPlanner/pull/5213) -- Add escalator reluctance parameter [#5046](https://github.com/opentripplanner/OpenTripPlanner/pull/5046) -- Handle reverse search when starting in no-drop-off zone [#5201](https://github.com/opentripplanner/OpenTripPlanner/pull/5201) -- Refactor edge constructors [#5221](https://github.com/opentripplanner/OpenTripPlanner/pull/5221) -- Upgrade to Apache HTTP Client 5 [#5228](https://github.com/opentripplanner/OpenTripPlanner/pull/5228) -- Deduplicate vertex labels to save memory [#5223](https://github.com/opentripplanner/OpenTripPlanner/pull/5223) -- Fix duplicate publishing of speed test data [#5237](https://github.com/opentripplanner/OpenTripPlanner/pull/5237) +- Treat escalator differently from stairs, add escalator reluctance [#5046](https://github.com/opentripplanner/OpenTripPlanner/pull/5046) - Flex build time and memory optimization for large zones [#5233](https://github.com/opentripplanner/OpenTripPlanner/pull/5233) - Fix pathway traversal time calculation when none is supplied [#5242](https://github.com/opentripplanner/OpenTripPlanner/pull/5242) - Add check for null value of serviceCodesRunning in TripPatternForDateMapper [#5239](https://github.com/opentripplanner/OpenTripPlanner/pull/5239) - Improve error handling in TransmodelGraph [#5192](https://github.com/opentripplanner/OpenTripPlanner/pull/5192) -- Fix SIRI SX retry logic [#5262](https://github.com/opentripplanner/OpenTripPlanner/pull/5262) - Fix filtering by submode [#5261](https://github.com/opentripplanner/OpenTripPlanner/pull/5261) - Add leg.headsign to GTFS GraphQL API [#5290](https://github.com/opentripplanner/OpenTripPlanner/pull/5290) - Return client error for invalid Transmodel query JSON format [#5277](https://github.com/opentripplanner/OpenTripPlanner/pull/5277) - Validate missing intermediate location in via requests [#5253](https://github.com/opentripplanner/OpenTripPlanner/pull/5253) - Support Fares v2 FareMedium and update spec implementation [#5227](https://github.com/opentripplanner/OpenTripPlanner/pull/5227) -- Replace DoubleFunction with linear function [#5267](https://github.com/opentripplanner/OpenTripPlanner/pull/5267) - Improve walk step narrative for entering/exiting stations and signposted pathways [#5285](https://github.com/opentripplanner/OpenTripPlanner/pull/5285) - Fix walk board cost comparison and add escalatorReluctance to hash [#5310](https://github.com/opentripplanner/OpenTripPlanner/pull/5310) -- Stop count limit for access/egress routing and new accessEgress configuration object [#5214](https://github.com/opentripplanner/OpenTripPlanner/pull/5214) - Remove pathway id from REST API [#5303](https://github.com/opentripplanner/OpenTripPlanner/pull/5303) - Remove Winkki street note updater [#5305](https://github.com/opentripplanner/OpenTripPlanner/pull/5305) - Extend stop area relation linking to include bus stop and platform nodes [#5319](https://github.com/opentripplanner/OpenTripPlanner/pull/5319) -- Document in-station navigation [#5321](https://github.com/opentripplanner/OpenTripPlanner/pull/5321) - Add access/egress penalty transmodel api [#5268](https://github.com/opentripplanner/OpenTripPlanner/pull/5268) -[](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.3.0 (2023-04-24) diff --git a/docs/Configuration.md b/docs/Configuration.md index 4a36c764c19..5a33c70fd43 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -173,7 +173,7 @@ text inserted is valid JSON (starts with `{` and ends with `}`). Variable substitution is performed on configuration file after the include file directive; Hence variable substitution is also performed on the text in the injected file. -Here is an example including variable substitution, assuming version 2.3.0 of OTP: +Here is an example including variable substitution, assuming version 2.4.0 of OTP: ```JSON // build-config.json @@ -197,7 +197,7 @@ The result will look like this: { "transitFeeds": [ { - "source": "netex-v2.3.0.obj" + "source": "netex-v2.4.0.obj" } ] } diff --git a/docs/Deployments.md b/docs/Deployments.md index 12ce8cdefad..f2946a9f0ba 100644 --- a/docs/Deployments.md +++ b/docs/Deployments.md @@ -44,6 +44,7 @@ The following are known deployments of OTP in a government- or agency-sponsored the [OneBusAway native apps](http://onebusaway.org/) in the Puget Sound region. Technical details are [here](https://github.com/OneBusAway/onebusaway-android/blob/master/SYSTEM_ARCHITECTURE.md#add-trip-planning-andor-bike-share-optional) . +* [**Ride Metro Houston**](https://planyourtrip.ridemetro.org/) * **Tampa, Florida** Hillsoborough Area Regional Transit uses an OpenTripPlanner server to power the trip planning feature of the [OneBusAway native apps](http://onebusaway.org/) in their region. Technical details diff --git a/docs/Getting-OTP.md b/docs/Getting-OTP.md index 35aeef48f65..bafb5fc61c0 100644 --- a/docs/Getting-OTP.md +++ b/docs/Getting-OTP.md @@ -9,8 +9,8 @@ the [release pages on GitHub](https://github.com/opentripplanner/OpenTripPlanner or [the OTP directory at Maven Central](https://repo1.maven.org/maven2/org/opentripplanner/otp/), navigate to the highest version number, and download the file whose name ends with `shaded.jar`. -Note that version numbers like `v2.1.0-rc1` or `v2.3.0-SNAPSHOT` refer to development builds _ -before_ the release version `v2.3.0`. The existence of a build `vX.Y.Z-SNAPSHOT` does not mean +Note that version numbers like `v2.1.0-rc1` or `v2.4.0-SNAPSHOT` refer to development builds _ +before_ the release version `v2.4.0`. The existence of a build `vX.Y.Z-SNAPSHOT` does not mean that `vX.Y.Z` has been released yet. We use the [Github Actions CI system](https://github.com/opentripplanner/OpenTripPlanner/actions) to @@ -87,7 +87,7 @@ For example, you could do the following: ```bash cd OpenTripPlanner -git checkout v2.3.0 +git checkout v2.4.0 git clean -df mvn clean package -DskipTests ``` @@ -110,8 +110,8 @@ file) to the Maven repository, from which it can be automatically included in ot This repository is machine-readable (by Maven or other build systems) and also provides human readable directory listings via HTTP. You can fetch an OTP JAR from this repository by constructing -the proper URL for the release you want. For example, release 2.3.0 will be found -at `https://repo1.maven.org/maven2/org/opentripplanner/otp/2.3.0/otp-2.3.0-shaded.jar`. +the proper URL for the release you want. For example, release 2.4.0 will be found +at `https://repo1.maven.org/maven2/org/opentripplanner/otp/2.4.0/otp-2.4.0-shaded.jar`. To make use of OTP in another Maven project, you must specify it as a dependency in that project's `pom.xml`: @@ -120,6 +120,6 @@ project's `pom.xml`: org.opentripplanner otp - 2.3.0 + 2.4.0 ``` diff --git a/docs/GraphQL-Tutorial.md b/docs/GraphQL-Tutorial.md index 73fba7d743e..e70294fcebd 100644 --- a/docs/GraphQL-Tutorial.md +++ b/docs/GraphQL-Tutorial.md @@ -41,6 +41,7 @@ GraphQL query in the left hand panel of the page: name } mode + bikesAllowed } } ``` diff --git a/docs/ReleaseChecklist.md b/docs/ReleaseChecklist.md index 372222d104f..6a46c0074cb 100644 --- a/docs/ReleaseChecklist.md +++ b/docs/ReleaseChecklist.md @@ -5,16 +5,16 @@ the actions taken by the Maven release plugin. Based on past experience, the Mav can fail at various points in the process leaving the repo in a confusing state. Taking each action manually is more tedious, but keeps eyes on each step and is less prone to failure. -* Make sure the documentation is up to date - * Check all links and references to the release and update to the target release version. Search - all files for with a regular expression: `2\.[012]\.0` and replace if appropriate with the new - version. * Check that your local copy of the dev branch is up to date with no uncommitted changes * `git status` * `git checkout dev-2.x` * `git clean -df` * `git pull` -* Verify that all dependencies in the POM are non-SNAPSHOT versions (e.g. with `grep`) +* Make sure the documentation is up to date + * Check all links and references to the release and update to the target release version. Search + all files for with a regular expression: `2\.[012]\.0` and replace if appropriate with the new + version. + * In `docs/index.md` replace what is the latest version and add a new line for the previous one * Update `docs/Changelog.md` * Lines should have been added or updated as each pull request was merged * If you suspect any changes are not reflected in the Changelog, review the commit log and add @@ -27,8 +27,8 @@ manually is more tedious, but keeps eyes on each step and is less prone to failu * This tells the GH Action that pushes the documentation on master what the name of the current version is. For version 2.3.0 Leonard has already done it: [Example commit](https://github.com/opentripplanner/OpenTripPlanner/commit/3cb061ab1e4253c3977a5d08fa5abab1b0baefd7) -* Check [on GH Actions](https://github.com/opentripplanner/OpenTripPlanner/actions/workflows/) that - the build is currently passing +* Verify that all dependencies in the POM are non-SNAPSHOT versions (e.g. with `grep`) +* Check [on GH Actions](https://github.com/opentripplanner/OpenTripPlanner/actions/workflows/) that the build is currently passing * Switch to the HEAD of master branch, and ensure it's up to date with no uncommitted changes * `git checkout master` * `git status` @@ -48,18 +48,6 @@ manually is more tedious, but keeps eyes on each step and is less prone to failu * You can also use the `package` goal instead of the `install` goal to avoid signing if you don't have the GPG certificate installed. * All tests should pass - * Run `MAVEN_OPTS="--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED" mvn com.webcohesion.enunciate:enunciate-maven-plugin:docs` - * This build will also create Enunciate API docs and Javadoc with the correct non-snapshot - version number -* Deploy the documentation to AWS S3 - * You have to do this right after the test release build to ensure the right version number in - the docs - * You will need AWSCLI tools (`sudo pip install -U awscli`) - * You will need AWS credentials with write access to the bucket `s3://dev.opentripplanner.org` - * `aws s3 cp --recursive target/site/apidocs s3://dev.opentripplanner.org/javadoc/x.y.z --acl public-read` - * `aws s3 cp --recursive target/site/enunciate/apidocs s3://dev.opentripplanner.org/apidoc/x.y.z --acl public-read` - * Check that docs are readable and show the correct version via - the [development documentation landing page](http://dev.opentripplanner.org). * Finally, if everything looks good, tag and push this release to make it official * `git tag -a vX.Y.Z -m "release X.Y.Z"` * `git push origin vX.Y.Z` diff --git a/docs/RouteRequest.md b/docs/RouteRequest.md index 83f6d2ddad8..f814753df4a 100644 --- a/docs/RouteRequest.md +++ b/docs/RouteRequest.md @@ -13,131 +13,132 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe -| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | -|------------------------------------------------------------------------------------------------------|:----------------------:|------------------------------------------------------------------------------------------------------------------------------------------------|:----------:|------------------|:-----:| -| [alightSlack](#rd_alightSlack) | `duration` | The minimum extra time after exiting a public transport vehicle. | *Optional* | `"PT0S"` | 2.0 | -| arriveBy | `boolean` | Whether the trip should depart or arrive at the specified date and time. | *Optional* | `false` | 2.0 | -| [bikeBoardCost](#rd_bikeBoardCost) | `integer` | Prevents unnecessary transfers by adding a cost for boarding a vehicle. | *Optional* | `600` | 2.0 | -| bikeParkCost | `integer` | Cost to park a bike. | *Optional* | `120` | 2.0 | -| bikeParkTime | `integer` | Time to park a bike. | *Optional* | `60` | 2.0 | -| bikeReluctance | `double` | A multiplier for how bad biking is, compared to being in transit for equal lengths of time. | *Optional* | `2.0` | 2.0 | -| bikeSpeed | `double` | Max bike speed along streets, in meters per second | *Optional* | `5.0` | 2.0 | -| bikeStairsReluctance | `double` | How bad is it to walk the bicycle up/down a flight of stairs compared to taking a detour. | *Optional* | `10.0` | 2.3 | -| bikeSwitchCost | `integer` | The cost of the user fetching their bike and parking it again. | *Optional* | `0` | 2.0 | -| bikeSwitchTime | `integer` | The time it takes the user to fetch their bike and park it again in seconds. | *Optional* | `0` | 2.0 | -| bikeTriangleSafetyFactor | `double` | For bike triangle routing, how much safety matters (range 0-1). | *Optional* | `0.0` | 2.0 | -| bikeTriangleSlopeFactor | `double` | For bike triangle routing, how much slope matters (range 0-1). | *Optional* | `0.0` | 2.0 | -| bikeTriangleTimeFactor | `double` | For bike triangle routing, how much time matters (range 0-1). | *Optional* | `0.0` | 2.0 | -| bikeWalkingReluctance | `double` | A multiplier for how bad walking with a bike is, compared to being in transit for equal lengths of time. | *Optional* | `5.0` | 2.1 | -| bikeWalkingSpeed | `double` | The user's bike walking speed in meters/second. Defaults to approximately 3 MPH. | *Optional* | `1.33` | 2.1 | -| [boardSlack](#rd_boardSlack) | `duration` | The boardSlack is the minimum extra time to board a public transport vehicle. | *Optional* | `"PT0S"` | 2.0 | -| carAccelerationSpeed | `double` | The acceleration speed of an automobile, in meters per second per second. | *Optional* | `2.9` | 2.0 | -| carDecelerationSpeed | `double` | The deceleration speed of an automobile, in meters per second per second. | *Optional* | `2.9` | 2.0 | -| carDropoffTime | `integer` | Time to park a car in a park and ride, w/o taking into account driving and walking cost. | *Optional* | `120` | 2.0 | -| carParkCost | `integer` | Cost of parking a car. | *Optional* | `120` | 2.1 | -| carParkTime | `integer` | Time to park a car | *Optional* | `60` | 2.1 | -| carPickupCost | `integer` | Add a cost for car pickup changes when a pickup or drop off takes place | *Optional* | `120` | 2.1 | -| carPickupTime | `integer` | Add a time for car pickup changes when a pickup or drop off takes place | *Optional* | `60` | 2.1 | -| carReluctance | `double` | A multiplier for how bad driving is, compared to being in transit for equal lengths of time. | *Optional* | `2.0` | 2.0 | -| carSpeed | `double` | Max car speed along streets, in meters per second | *Optional* | `40.0` | 2.0 | -| [drivingDirection](#rd_drivingDirection) | `enum` | The driving direction to use in the intersection traversal calculation | *Optional* | `"right"` | 2.2 | -| elevatorBoardCost | `integer` | What is the cost of boarding a elevator? | *Optional* | `90` | 2.0 | -| elevatorBoardTime | `integer` | How long does it take to get on an elevator, on average. | *Optional* | `90` | 2.0 | -| elevatorHopCost | `integer` | What is the cost of travelling one floor on an elevator? | *Optional* | `20` | 2.0 | -| elevatorHopTime | `integer` | How long does it take to advance one floor on an elevator? | *Optional* | `20` | 2.0 | -| escalatorReluctance | `double` | A multiplier for how bad being in an escalator is compared to being in transit for equal lengths of time | *Optional* | `1.5` | 2.4 | -| geoidElevation | `boolean` | If true, the Graph's ellipsoidToGeoidDifference is applied to all elevations returned by this query. | *Optional* | `false` | 2.0 | -| ignoreRealtimeUpdates | `boolean` | When true, realtime updates are ignored during this search. | *Optional* | `false` | 2.0 | -| [intersectionTraversalModel](#rd_intersectionTraversalModel) | `enum` | The model that computes the costs of turns. | *Optional* | `"simple"` | 2.2 | -| locale | `locale` | TODO | *Optional* | `"en_US"` | 2.0 | -| [maxDirectStreetDuration](#rd_maxDirectStreetDuration) | `duration` | This is the maximum duration for a direct street search for each mode. | *Optional* | `"PT4H"` | 2.1 | -| [maxJourneyDuration](#rd_maxJourneyDuration) | `duration` | The expected maximum time a journey can last across all possible journeys for the current deployment. | *Optional* | `"PT24H"` | 2.1 | -| modes | `string` | The set of access/egress/direct/transit modes to be used for the route search. | *Optional* | `"TRANSIT,WALK"` | 2.0 | -| nonpreferredTransferPenalty | `integer` | Penalty (in seconds) for using a non-preferred transfer. | *Optional* | `180` | 2.0 | -| numItineraries | `integer` | The maximum number of itineraries to return. | *Optional* | `50` | 2.0 | -| [optimize](#rd_optimize) | `enum` | The set of characteristics that the user wants to optimize for. | *Optional* | `"safe"` | 2.0 | -| [otherThanPreferredRoutesPenalty](#rd_otherThanPreferredRoutesPenalty) | `integer` | Penalty added for using every route that is not preferred if user set any route as preferred. | *Optional* | `300` | 2.0 | -| [relaxTransitSearchGeneralizedCostAtDestination](#rd_relaxTransitSearchGeneralizedCostAtDestination) | `double` | Whether non-optimal transit paths at the destination should be returned | *Optional* | | 2.3 | -| [searchWindow](#rd_searchWindow) | `duration` | The duration of the search-window. | *Optional* | | 2.0 | -| stairsReluctance | `double` | Used instead of walkReluctance for stairs. | *Optional* | `2.0` | 2.0 | -| [stairsTimeFactor](#rd_stairsTimeFactor) | `double` | How much more time does it take to walk a flight of stairs compared to walking a similar horizontal length. | *Optional* | `3.0` | 2.1 | -| [streetRoutingTimeout](#rd_streetRoutingTimeout) | `duration` | The maximum time a street routing request is allowed to take before returning the results. | *Optional* | `"PT5S"` | 2.2 | -| [transferPenalty](#rd_transferPenalty) | `integer` | An additional penalty added to boardings after the first. | *Optional* | `0` | 2.0 | -| [transferSlack](#rd_transferSlack) | `integer` | The extra time needed to make a safe transfer in seconds. | *Optional* | `120` | 2.0 | -| turnReluctance | `double` | Multiplicative factor on expected turning time. | *Optional* | `1.0` | 2.0 | -| [unpreferredCost](#rd_unpreferredCost) | `cost-linear-function` | A cost function used to calculate penalty for an unpreferred route. | *Optional* | `"0s + 1.00 t"` | 2.2 | -| [unpreferredVehicleParkingTagCost](#rd_unpreferredVehicleParkingTagCost) | `integer` | What cost to add if a parking facility doesn't contain a preferred tag. | *Optional* | `300` | 2.3 | -| waitReluctance | `double` | How much worse is waiting for a transit vehicle than being on a transit vehicle, as a multiplier. | *Optional* | `1.0` | 2.0 | -| walkBoardCost | `integer` | Prevents unnecessary transfers by adding a cost for boarding a vehicle. This is the cost that is used when boarding while walking. | *Optional* | `600` | 2.0 | -| [walkReluctance](#rd_walkReluctance) | `double` | A multiplier for how bad walking is, compared to being in transit for equal lengths of time. | *Optional* | `2.0` | 2.0 | -| [walkSafetyFactor](#rd_walkSafetyFactor) | `double` | Factor for how much the walk safety is considered in routing. | *Optional* | `1.0` | 2.2 | -| walkSpeed | `double` | The user's walking speed in meters/second. | *Optional* | `1.33` | 2.0 | -| accessEgress | `object` | Parameters for access and egress routing. | *Optional* | | 2.4 | -|    [maxDuration](#rd_accessEgress_maxDuration) | `duration` | This is the maximum duration for access/egress for street searches. | *Optional* | `"PT45M"` | 2.1 | -|    [maxStopCount](#rd_accessEgress_maxStopCount) | `integer` | Maximal number of stops collected in access/egress routing | *Optional* | `500` | 2.4 | -|    [maxDurationForMode](#rd_accessEgress_maxDurationForMode) | `enum map of duration` | Limit access/egress per street mode. | *Optional* | | 2.1 | -|    [penalty](#rd_accessEgress_penalty) | `enum map of object` | Penalty for access/egress by street mode. | *Optional* | | 2.4 | -|       FLEXIBLE | `object` | NA | *Optional* | | 2.4 | -|          costFactor | `double` | A factor multiplied with the time-penalty to get the cost-penalty. | *Optional* | `0.0` | 2.4 | -|          timePenalty | `time-penalty` | Penalty added to the time of a path/leg. | *Optional* | `"0s + 0.00 t"` | 2.4 | -| [alightSlackForMode](#rd_alightSlackForMode) | `enum map of duration` | How much extra time should be given when alighting a vehicle for each given mode. | *Optional* | | 2.0 | -| [bannedVehicleParkingTags](#rd_bannedVehicleParkingTags) | `string[]` | Tags with which a vehicle parking will not be used. If empty, no tags are banned. | *Optional* | | 2.1 | -| [boardSlackForMode](#rd_boardSlackForMode) | `enum map of duration` | How much extra time should be given when boarding a vehicle for each given mode. | *Optional* | | 2.0 | -| [itineraryFilters](#rd_itineraryFilters) | `object` | Configure itinerary filters that may modify itineraries, sort them, and filter away less preferable results. | *Optional* | | 2.0 | -|    [accessibilityScore](#rd_if_accessibilityScore) | `boolean` | An experimental feature contributed by IBI which adds a sandbox accessibility *score* between 0 and 1 for each leg and itinerary. | *Optional* | `false` | 2.2 | -|    [bikeRentalDistanceRatio](#rd_if_bikeRentalDistanceRatio) | `double` | Filter routes that consist of bike-rental and walking by the minimum fraction of the bike-rental leg using _distance_. | *Optional* | `0.0` | 2.1 | -|    [debug](#rd_if_debug) | `enum` | Enable this to attach a system notice to itineraries instead of removing them. This is very convenient when tuning the itinerary-filter-chain. | *Optional* | `"off"` | 2.0 | -|    [filterItinerariesWithSameFirstOrLastTrip](#rd_if_filterItinerariesWithSameFirstOrLastTrip) | `boolean` | If more than one itinerary begins or ends with same trip, filter out one of those itineraries so that only one remains. | *Optional* | `false` | 2.2 | -|    groupSimilarityKeepOne | `double` | Pick ONE itinerary from each group after putting itineraries that are 85% similar together. | *Optional* | `0.85` | 2.1 | -|    groupSimilarityKeepThree | `double` | Reduce the number of itineraries to three itineraries by reducing each group of itineraries grouped by 68% similarity. | *Optional* | `0.68` | 2.1 | -|    [groupedOtherThanSameLegsMaxCostMultiplier](#rd_if_groupedOtherThanSameLegsMaxCostMultiplier) | `double` | Filter grouped itineraries, where the non-grouped legs are more expensive than in the lowest cost one. | *Optional* | `2.0` | 2.1 | -|    [minBikeParkingDistance](#rd_if_minBikeParkingDistance) | `double` | Filter out bike park+ride results that have fewer meters of cycling than this value. | *Optional* | `0.0` | 2.3 | -|    [nonTransitGeneralizedCostLimit](#rd_if_nonTransitGeneralizedCostLimit) | `cost-linear-function` | The function define a max-limit for generalized-cost for non-transit itineraries. | *Optional* | `"1h + 2.0 t"` | 2.1 | -|    [parkAndRideDurationRatio](#rd_if_parkAndRideDurationRatio) | `double` | Filter P+R routes that consist of driving and walking by the minimum fraction of the driving using of _time_. | *Optional* | `0.0` | 2.1 | -|    [removeItinerariesWithSameRoutesAndStops](#rd_if_removeItinerariesWithSameRoutesAndStops) | `boolean` | Set to true if you want to list only the first itinerary which goes through the same stops and routes. | *Optional* | `false` | 2.2 | -|    [transitGeneralizedCostLimit](#rd_if_transitGeneralizedCostLimit) | `object` | A relative limit for the generalized-cost for transit itineraries. | *Optional* | | 2.1 | -|       [costLimitFunction](#rd_if_transitGeneralizedCostLimit_costLimitFunction) | `cost-linear-function` | The base function used by the filter. | *Optional* | `"15m + 1.50 t"` | 2.2 | -|       [intervalRelaxFactor](#rd_if_transitGeneralizedCostLimit_intervalRelaxFactor) | `double` | How much the filter should be relaxed for itineraries that do not overlap in time. | *Optional* | `0.4` | 2.2 | -| [maxDirectStreetDurationForMode](#rd_maxDirectStreetDurationForMode) | `enum map of duration` | Limit direct route duration per street mode. | *Optional* | | 2.2 | -| [preferredVehicleParkingTags](#rd_preferredVehicleParkingTags) | `string[]` | Vehicle parking facilities that don't have one of these tags will receive an extra cost and will therefore be penalised. | *Optional* | | 2.3 | -| [requiredVehicleParkingTags](#rd_requiredVehicleParkingTags) | `string[]` | Tags without which a vehicle parking will not be used. If empty, no tags are required. | *Optional* | | 2.1 | -| [transferOptimization](#rd_transferOptimization) | `object` | Optimize where a transfer between to trip happens. | *Optional* | | 2.1 | -|    [backTravelWaitTimeFactor](#rd_to_backTravelWaitTimeFactor) | `double` | To reduce back-travel we favor waiting, this reduces the cost of waiting. | *Optional* | `1.0` | 2.1 | -|    [extraStopBoardAlightCostsFactor](#rd_to_extraStopBoardAlightCostsFactor) | `double` | Add an extra board- and alight-cost for prioritized stops. | *Optional* | `0.0` | 2.1 | -|    [minSafeWaitTimeFactor](#rd_to_minSafeWaitTimeFactor) | `double` | Used to set a maximum wait-time cost, base on min-safe-transfer-time. | *Optional* | `5.0` | 2.1 | -|    [optimizeTransferWaitTime](#rd_to_optimizeTransferWaitTime) | `boolean` | This enables the transfer wait time optimization. | *Optional* | `true` | 2.1 | -| [transitReluctanceForMode](#rd_transitReluctanceForMode) | `enum map of double` | Transit reluctance for a given transport mode | *Optional* | | 2.1 | -| [unpreferred](#rd_unpreferred) | `object` | Parameters listing authorities or lines that preferably should not be used in trip patters. | *Optional* | | 2.2 | -|    [agencies](#rd_unpreferred_agencies) | `feed-scoped-id[]` | The ids of the agencies that incur an extra cost when being used. Format: `FeedId:AgencyId` | *Optional* | | 2.2 | -|    [routes](#rd_unpreferred_routes) | `feed-scoped-id[]` | The ids of the routes that incur an extra cost when being used. Format: `FeedId:RouteId` | *Optional* | | 2.2 | -| vehicleRental | `object` | Vehicle rental options | *Optional* | | 2.3 | -|    allowKeepingAtDestination | `boolean` | If a vehicle should be allowed to be kept at the end of a station-based rental. | *Optional* | `false` | 2.2 | -|    dropOffCost | `integer` | Cost to drop-off a rented vehicle. | *Optional* | `30` | 2.0 | -|    dropOffTime | `integer` | Time to drop-off a rented vehicle. | *Optional* | `30` | 2.0 | -|    keepingAtDestinationCost | `double` | The cost of arriving at the destination with the rented vehicle, to discourage doing so. | *Optional* | `0.0` | 2.2 | -|    pickupCost | `integer` | Cost to rent a vehicle. | *Optional* | `120` | 2.0 | -|    pickupTime | `integer` | Time to rent a vehicle. | *Optional* | `60` | 2.0 | -|    useAvailabilityInformation | `boolean` | Whether or not vehicle rental availability information will be used to plan vehicle rental trips. | *Optional* | `false` | 2.0 | -|    [allowedNetworks](#rd_vehicleRental_allowedNetworks) | `string[]` | The vehicle rental networks which may be used. If empty all networks may be used. | *Optional* | | 2.1 | -|    [bannedNetworks](#rd_vehicleRental_bannedNetworks) | `string[]` | The vehicle rental networks which may not be used. If empty, no networks are banned. | *Optional* | | 2.1 | -| wheelchairAccessibility | `object` | See [Wheelchair Accessibility](Accessibility.md) | *Optional* | | 2.2 | -|    enabled | `boolean` | Enable wheelchair accessibility. | *Optional* | `false` | 2.0 | -|    inaccessibleStreetReluctance | `double` | The factor to multiply the cost of traversing a street edge that is not wheelchair-accessible. | *Optional* | `25.0` | 2.2 | -|    [maxSlope](#rd_wheelchairAccessibility_maxSlope) | `double` | The maximum slope as a fraction of 1. | *Optional* | `0.083` | 2.0 | -|    [slopeExceededReluctance](#rd_wheelchairAccessibility_slopeExceededReluctance) | `double` | How much streets with high slope should be avoided. | *Optional* | `1.0` | 2.2 | -|    [stairsReluctance](#rd_wheelchairAccessibility_stairsReluctance) | `double` | How much stairs should be avoided. | *Optional* | `100.0` | 2.2 | -|    elevator | `object` | Configuration for when to use inaccessible elevators. | *Optional* | | 2.2 | -|       inaccessibleCost | `integer` | The cost to add when traversing an entity which is know to be inaccessible. | *Optional* | `3600` | 2.2 | -|       onlyConsiderAccessible | `boolean` | Whether to only use this entity if it is explicitly marked as wheelchair accessible. | *Optional* | `false` | 2.2 | -|       unknownCost | `integer` | The cost to add when traversing an entity with unknown accessibility information. | *Optional* | `20` | 2.2 | -|    stop | `object` | Configuration for when to use inaccessible stops. | *Optional* | | 2.2 | -|       inaccessibleCost | `integer` | The cost to add when traversing an entity which is know to be inaccessible. | *Optional* | `3600` | 2.2 | -|       onlyConsiderAccessible | `boolean` | Whether to only use this entity if it is explicitly marked as wheelchair accessible. | *Optional* | `true` | 2.2 | -|       unknownCost | `integer` | The cost to add when traversing an entity with unknown accessibility information. | *Optional* | `600` | 2.2 | -|    trip | `object` | Configuration for when to use inaccessible trips. | *Optional* | | 2.2 | -|       inaccessibleCost | `integer` | The cost to add when traversing an entity which is know to be inaccessible. | *Optional* | `3600` | 2.2 | -|       onlyConsiderAccessible | `boolean` | Whether to only use this entity if it is explicitly marked as wheelchair accessible. | *Optional* | `true` | 2.2 | -|       unknownCost | `integer` | The cost to add when traversing an entity with unknown accessibility information. | *Optional* | `600` | 2.2 | +| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | +|--------------------------------------------------------------------------------------------------------------|:----------------------:|------------------------------------------------------------------------------------------------------------------------------------------------|:----------:|------------------|:-----:| +| [alightSlack](#rd_alightSlack) | `duration` | The minimum extra time after exiting a public transport vehicle. | *Optional* | `"PT0S"` | 2.0 | +| arriveBy | `boolean` | Whether the trip should depart or arrive at the specified date and time. | *Optional* | `false` | 2.0 | +| [bikeBoardCost](#rd_bikeBoardCost) | `integer` | Prevents unnecessary transfers by adding a cost for boarding a vehicle. | *Optional* | `600` | 2.0 | +| bikeParkCost | `integer` | Cost to park a bike. | *Optional* | `120` | 2.0 | +| bikeParkTime | `integer` | Time to park a bike. | *Optional* | `60` | 2.0 | +| bikeReluctance | `double` | A multiplier for how bad biking is, compared to being in transit for equal lengths of time. | *Optional* | `2.0` | 2.0 | +| bikeSpeed | `double` | Max bike speed along streets, in meters per second | *Optional* | `5.0` | 2.0 | +| bikeStairsReluctance | `double` | How bad is it to walk the bicycle up/down a flight of stairs compared to taking a detour. | *Optional* | `10.0` | 2.3 | +| bikeSwitchCost | `integer` | The cost of the user fetching their bike and parking it again. | *Optional* | `0` | 2.0 | +| bikeSwitchTime | `integer` | The time it takes the user to fetch their bike and park it again in seconds. | *Optional* | `0` | 2.0 | +| bikeTriangleSafetyFactor | `double` | For bike triangle routing, how much safety matters (range 0-1). | *Optional* | `0.0` | 2.0 | +| bikeTriangleSlopeFactor | `double` | For bike triangle routing, how much slope matters (range 0-1). | *Optional* | `0.0` | 2.0 | +| bikeTriangleTimeFactor | `double` | For bike triangle routing, how much time matters (range 0-1). | *Optional* | `0.0` | 2.0 | +| bikeWalkingReluctance | `double` | A multiplier for how bad walking with a bike is, compared to being in transit for equal lengths of time. | *Optional* | `5.0` | 2.1 | +| bikeWalkingSpeed | `double` | The user's bike walking speed in meters/second. Defaults to approximately 3 MPH. | *Optional* | `1.33` | 2.1 | +| [boardSlack](#rd_boardSlack) | `duration` | The boardSlack is the minimum extra time to board a public transport vehicle. | *Optional* | `"PT0S"` | 2.0 | +| carAccelerationSpeed | `double` | The acceleration speed of an automobile, in meters per second per second. | *Optional* | `2.9` | 2.0 | +| carDecelerationSpeed | `double` | The deceleration speed of an automobile, in meters per second per second. | *Optional* | `2.9` | 2.0 | +| carDropoffTime | `integer` | Time to park a car in a park and ride, w/o taking into account driving and walking cost. | *Optional* | `120` | 2.0 | +| carParkCost | `integer` | Cost of parking a car. | *Optional* | `120` | 2.1 | +| carParkTime | `integer` | Time to park a car | *Optional* | `60` | 2.1 | +| carPickupCost | `integer` | Add a cost for car pickup changes when a pickup or drop off takes place | *Optional* | `120` | 2.1 | +| carPickupTime | `integer` | Add a time for car pickup changes when a pickup or drop off takes place | *Optional* | `60` | 2.1 | +| carReluctance | `double` | A multiplier for how bad driving is, compared to being in transit for equal lengths of time. | *Optional* | `2.0` | 2.0 | +| carSpeed | `double` | Max car speed along streets, in meters per second | *Optional* | `40.0` | 2.0 | +| [drivingDirection](#rd_drivingDirection) | `enum` | The driving direction to use in the intersection traversal calculation | *Optional* | `"right"` | 2.2 | +| elevatorBoardCost | `integer` | What is the cost of boarding a elevator? | *Optional* | `90` | 2.0 | +| elevatorBoardTime | `integer` | How long does it take to get on an elevator, on average. | *Optional* | `90` | 2.0 | +| elevatorHopCost | `integer` | What is the cost of travelling one floor on an elevator? | *Optional* | `20` | 2.0 | +| elevatorHopTime | `integer` | How long does it take to advance one floor on an elevator? | *Optional* | `20` | 2.0 | +| escalatorReluctance | `double` | A multiplier for how bad being in an escalator is compared to being in transit for equal lengths of time | *Optional* | `1.5` | 2.4 | +| geoidElevation | `boolean` | If true, the Graph's ellipsoidToGeoidDifference is applied to all elevations returned by this query. | *Optional* | `false` | 2.0 | +| ignoreRealtimeUpdates | `boolean` | When true, realtime updates are ignored during this search. | *Optional* | `false` | 2.0 | +| [intersectionTraversalModel](#rd_intersectionTraversalModel) | `enum` | The model that computes the costs of turns. | *Optional* | `"simple"` | 2.2 | +| locale | `locale` | TODO | *Optional* | `"en_US"` | 2.0 | +| [maxDirectStreetDuration](#rd_maxDirectStreetDuration) | `duration` | This is the maximum duration for a direct street search for each mode. | *Optional* | `"PT4H"` | 2.1 | +| [maxJourneyDuration](#rd_maxJourneyDuration) | `duration` | The expected maximum time a journey can last across all possible journeys for the current deployment. | *Optional* | `"PT24H"` | 2.1 | +| modes | `string` | The set of access/egress/direct/transit modes to be used for the route search. | *Optional* | `"TRANSIT,WALK"` | 2.0 | +| nonpreferredTransferPenalty | `integer` | Penalty (in seconds) for using a non-preferred transfer. | *Optional* | `180` | 2.0 | +| numItineraries | `integer` | The maximum number of itineraries to return. | *Optional* | `50` | 2.0 | +| [optimize](#rd_optimize) | `enum` | The set of characteristics that the user wants to optimize for. | *Optional* | `"safe"` | 2.0 | +| [otherThanPreferredRoutesPenalty](#rd_otherThanPreferredRoutesPenalty) | `integer` | Penalty added for using every route that is not preferred if user set any route as preferred. | *Optional* | `300` | 2.0 | +| [relaxTransitSearchGeneralizedCostAtDestination](#rd_relaxTransitSearchGeneralizedCostAtDestination) | `double` | Whether non-optimal transit paths at the destination should be returned | *Optional* | | 2.3 | +| [searchWindow](#rd_searchWindow) | `duration` | The duration of the search-window. | *Optional* | | 2.0 | +| stairsReluctance | `double` | Used instead of walkReluctance for stairs. | *Optional* | `2.0` | 2.0 | +| [stairsTimeFactor](#rd_stairsTimeFactor) | `double` | How much more time does it take to walk a flight of stairs compared to walking a similar horizontal length. | *Optional* | `3.0` | 2.1 | +| [streetRoutingTimeout](#rd_streetRoutingTimeout) | `duration` | The maximum time a street routing request is allowed to take before returning the results. | *Optional* | `"PT5S"` | 2.2 | +| [transferPenalty](#rd_transferPenalty) | `integer` | An additional penalty added to boardings after the first. | *Optional* | `0` | 2.0 | +| [transferSlack](#rd_transferSlack) | `integer` | The extra time needed to make a safe transfer in seconds. | *Optional* | `120` | 2.0 | +| turnReluctance | `double` | Multiplicative factor on expected turning time. | *Optional* | `1.0` | 2.0 | +| [unpreferredCost](#rd_unpreferredCost) | `cost-linear-function` | A cost function used to calculate penalty for an unpreferred route. | *Optional* | `"0s + 1.00 t"` | 2.2 | +| [unpreferredVehicleParkingTagCost](#rd_unpreferredVehicleParkingTagCost) | `integer` | What cost to add if a parking facility doesn't contain a preferred tag. | *Optional* | `300` | 2.3 | +| waitReluctance | `double` | How much worse is waiting for a transit vehicle than being on a transit vehicle, as a multiplier. | *Optional* | `1.0` | 2.0 | +| walkBoardCost | `integer` | Prevents unnecessary transfers by adding a cost for boarding a vehicle. This is the cost that is used when boarding while walking. | *Optional* | `600` | 2.0 | +| [walkReluctance](#rd_walkReluctance) | `double` | A multiplier for how bad walking is, compared to being in transit for equal lengths of time. | *Optional* | `2.0` | 2.0 | +| [walkSafetyFactor](#rd_walkSafetyFactor) | `double` | Factor for how much the walk safety is considered in routing. | *Optional* | `1.0` | 2.2 | +| walkSpeed | `double` | The user's walking speed in meters/second. | *Optional* | `1.33` | 2.0 | +| accessEgress | `object` | Parameters for access and egress routing. | *Optional* | | 2.4 | +|    [maxDuration](#rd_accessEgress_maxDuration) | `duration` | This is the maximum duration for access/egress for street searches. | *Optional* | `"PT45M"` | 2.1 | +|    [maxStopCount](#rd_accessEgress_maxStopCount) | `integer` | Maximal number of stops collected in access/egress routing | *Optional* | `500` | 2.4 | +|    [maxDurationForMode](#rd_accessEgress_maxDurationForMode) | `enum map of duration` | Limit access/egress per street mode. | *Optional* | | 2.1 | +|    [penalty](#rd_accessEgress_penalty) | `enum map of object` | Penalty for access/egress by street mode. | *Optional* | | 2.4 | +|       FLEXIBLE | `object` | NA | *Optional* | | 2.4 | +|          costFactor | `double` | A factor multiplied with the time-penalty to get the cost-penalty. | *Optional* | `0.0` | 2.4 | +|          timePenalty | `time-penalty` | Penalty added to the time of a path/leg. | *Optional* | `"0s + 0.00 t"` | 2.4 | +| [alightSlackForMode](#rd_alightSlackForMode) | `enum map of duration` | How much extra time should be given when alighting a vehicle for each given mode. | *Optional* | | 2.0 | +| [bannedVehicleParkingTags](#rd_bannedVehicleParkingTags) | `string[]` | Tags with which a vehicle parking will not be used. If empty, no tags are banned. | *Optional* | | 2.1 | +| [boardSlackForMode](#rd_boardSlackForMode) | `enum map of duration` | How much extra time should be given when boarding a vehicle for each given mode. | *Optional* | | 2.0 | +| [itineraryFilters](#rd_itineraryFilters) | `object` | Configure itinerary filters that may modify itineraries, sort them, and filter away less preferable results. | *Optional* | | 2.0 | +|    [accessibilityScore](#rd_if_accessibilityScore) | `boolean` | An experimental feature contributed by IBI which adds a sandbox accessibility *score* between 0 and 1 for each leg and itinerary. | *Optional* | `false` | 2.2 | +|    [bikeRentalDistanceRatio](#rd_if_bikeRentalDistanceRatio) | `double` | Filter routes that consist of bike-rental and walking by the minimum fraction of the bike-rental leg using _distance_. | *Optional* | `0.0` | 2.1 | +|    [debug](#rd_if_debug) | `enum` | Enable this to attach a system notice to itineraries instead of removing them. This is very convenient when tuning the itinerary-filter-chain. | *Optional* | `"off"` | 2.0 | +|    [filterItinerariesWithSameFirstOrLastTrip](#rd_if_filterItinerariesWithSameFirstOrLastTrip) | `boolean` | If more than one itinerary begins or ends with same trip, filter out one of those itineraries so that only one remains. | *Optional* | `false` | 2.2 | +|    groupSimilarityKeepOne | `double` | Pick ONE itinerary from each group after putting itineraries that are 85% similar together. | *Optional* | `0.85` | 2.1 | +|    groupSimilarityKeepThree | `double` | Reduce the number of itineraries to three itineraries by reducing each group of itineraries grouped by 68% similarity. | *Optional* | `0.68` | 2.1 | +|    [groupedOtherThanSameLegsMaxCostMultiplier](#rd_if_groupedOtherThanSameLegsMaxCostMultiplier) | `double` | Filter grouped itineraries, where the non-grouped legs are more expensive than in the lowest cost one. | *Optional* | `2.0` | 2.1 | +|    [minBikeParkingDistance](#rd_if_minBikeParkingDistance) | `double` | Filter out bike park+ride results that have fewer meters of cycling than this value. | *Optional* | `0.0` | 2.3 | +|    [nonTransitGeneralizedCostLimit](#rd_if_nonTransitGeneralizedCostLimit) | `cost-linear-function` | The function define a max-limit for generalized-cost for non-transit itineraries. | *Optional* | `"1h + 2.0 t"` | 2.1 | +|    [parkAndRideDurationRatio](#rd_if_parkAndRideDurationRatio) | `double` | Filter P+R routes that consist of driving and walking by the minimum fraction of the driving using of _time_. | *Optional* | `0.0` | 2.1 | +|    [removeItinerariesWithSameRoutesAndStops](#rd_if_removeItinerariesWithSameRoutesAndStops) | `boolean` | Set to true if you want to list only the first itinerary which goes through the same stops and routes. | *Optional* | `false` | 2.2 | +|    [removeTransitWithHigherCostThanBestOnStreetOnly](#rd_if_removeTransitWithHigherCostThanBestOnStreetOnly) | `cost-linear-function` | Limit function for generalized-cost computed from street-only itineries applied to transit itineraries. | *Optional* | `"1m + 1.30 t"` | 2.4 | +|    [transitGeneralizedCostLimit](#rd_if_transitGeneralizedCostLimit) | `object` | A relative limit for the generalized-cost for transit itineraries. | *Optional* | | 2.1 | +|       [costLimitFunction](#rd_if_transitGeneralizedCostLimit_costLimitFunction) | `cost-linear-function` | The base function used by the filter. | *Optional* | `"15m + 1.50 t"` | 2.2 | +|       [intervalRelaxFactor](#rd_if_transitGeneralizedCostLimit_intervalRelaxFactor) | `double` | How much the filter should be relaxed for itineraries that do not overlap in time. | *Optional* | `0.4` | 2.2 | +| [maxDirectStreetDurationForMode](#rd_maxDirectStreetDurationForMode) | `enum map of duration` | Limit direct route duration per street mode. | *Optional* | | 2.2 | +| [preferredVehicleParkingTags](#rd_preferredVehicleParkingTags) | `string[]` | Vehicle parking facilities that don't have one of these tags will receive an extra cost and will therefore be penalised. | *Optional* | | 2.3 | +| [requiredVehicleParkingTags](#rd_requiredVehicleParkingTags) | `string[]` | Tags without which a vehicle parking will not be used. If empty, no tags are required. | *Optional* | | 2.1 | +| [transferOptimization](#rd_transferOptimization) | `object` | Optimize where a transfer between to trip happens. | *Optional* | | 2.1 | +|    [backTravelWaitTimeFactor](#rd_to_backTravelWaitTimeFactor) | `double` | To reduce back-travel we favor waiting, this reduces the cost of waiting. | *Optional* | `1.0` | 2.1 | +|    [extraStopBoardAlightCostsFactor](#rd_to_extraStopBoardAlightCostsFactor) | `double` | Add an extra board- and alight-cost for prioritized stops. | *Optional* | `0.0` | 2.1 | +|    [minSafeWaitTimeFactor](#rd_to_minSafeWaitTimeFactor) | `double` | Used to set a maximum wait-time cost, base on min-safe-transfer-time. | *Optional* | `5.0` | 2.1 | +|    [optimizeTransferWaitTime](#rd_to_optimizeTransferWaitTime) | `boolean` | This enables the transfer wait time optimization. | *Optional* | `true` | 2.1 | +| [transitReluctanceForMode](#rd_transitReluctanceForMode) | `enum map of double` | Transit reluctance for a given transport mode | *Optional* | | 2.1 | +| [unpreferred](#rd_unpreferred) | `object` | Parameters listing authorities or lines that preferably should not be used in trip patters. | *Optional* | | 2.2 | +|    [agencies](#rd_unpreferred_agencies) | `feed-scoped-id[]` | The ids of the agencies that incur an extra cost when being used. Format: `FeedId:AgencyId` | *Optional* | | 2.2 | +|    [routes](#rd_unpreferred_routes) | `feed-scoped-id[]` | The ids of the routes that incur an extra cost when being used. Format: `FeedId:RouteId` | *Optional* | | 2.2 | +| vehicleRental | `object` | Vehicle rental options | *Optional* | | 2.3 | +|    allowKeepingAtDestination | `boolean` | If a vehicle should be allowed to be kept at the end of a station-based rental. | *Optional* | `false` | 2.2 | +|    dropOffCost | `integer` | Cost to drop-off a rented vehicle. | *Optional* | `30` | 2.0 | +|    dropOffTime | `integer` | Time to drop-off a rented vehicle. | *Optional* | `30` | 2.0 | +|    keepingAtDestinationCost | `double` | The cost of arriving at the destination with the rented vehicle, to discourage doing so. | *Optional* | `0.0` | 2.2 | +|    pickupCost | `integer` | Cost to rent a vehicle. | *Optional* | `120` | 2.0 | +|    pickupTime | `integer` | Time to rent a vehicle. | *Optional* | `60` | 2.0 | +|    useAvailabilityInformation | `boolean` | Whether or not vehicle rental availability information will be used to plan vehicle rental trips. | *Optional* | `false` | 2.0 | +|    [allowedNetworks](#rd_vehicleRental_allowedNetworks) | `string[]` | The vehicle rental networks which may be used. If empty all networks may be used. | *Optional* | | 2.1 | +|    [bannedNetworks](#rd_vehicleRental_bannedNetworks) | `string[]` | The vehicle rental networks which may not be used. If empty, no networks are banned. | *Optional* | | 2.1 | +| wheelchairAccessibility | `object` | See [Wheelchair Accessibility](Accessibility.md) | *Optional* | | 2.2 | +|    enabled | `boolean` | Enable wheelchair accessibility. | *Optional* | `false` | 2.0 | +|    inaccessibleStreetReluctance | `double` | The factor to multiply the cost of traversing a street edge that is not wheelchair-accessible. | *Optional* | `25.0` | 2.2 | +|    [maxSlope](#rd_wheelchairAccessibility_maxSlope) | `double` | The maximum slope as a fraction of 1. | *Optional* | `0.083` | 2.0 | +|    [slopeExceededReluctance](#rd_wheelchairAccessibility_slopeExceededReluctance) | `double` | How much streets with high slope should be avoided. | *Optional* | `1.0` | 2.2 | +|    [stairsReluctance](#rd_wheelchairAccessibility_stairsReluctance) | `double` | How much stairs should be avoided. | *Optional* | `100.0` | 2.2 | +|    elevator | `object` | Configuration for when to use inaccessible elevators. | *Optional* | | 2.2 | +|       inaccessibleCost | `integer` | The cost to add when traversing an entity which is know to be inaccessible. | *Optional* | `3600` | 2.2 | +|       onlyConsiderAccessible | `boolean` | Whether to only use this entity if it is explicitly marked as wheelchair accessible. | *Optional* | `false` | 2.2 | +|       unknownCost | `integer` | The cost to add when traversing an entity with unknown accessibility information. | *Optional* | `20` | 2.2 | +|    stop | `object` | Configuration for when to use inaccessible stops. | *Optional* | | 2.2 | +|       inaccessibleCost | `integer` | The cost to add when traversing an entity which is know to be inaccessible. | *Optional* | `3600` | 2.2 | +|       onlyConsiderAccessible | `boolean` | Whether to only use this entity if it is explicitly marked as wheelchair accessible. | *Optional* | `true` | 2.2 | +|       unknownCost | `integer` | The cost to add when traversing an entity with unknown accessibility information. | *Optional* | `600` | 2.2 | +|    trip | `object` | Configuration for when to use inaccessible trips. | *Optional* | | 2.2 | +|       inaccessibleCost | `integer` | The cost to add when traversing an entity which is know to be inaccessible. | *Optional* | `3600` | 2.2 | +|       onlyConsiderAccessible | `boolean` | Whether to only use this entity if it is explicitly marked as wheelchair accessible. | *Optional* | `true` | 2.2 | +|       unknownCost | `integer` | The cost to add when traversing an entity with unknown accessibility information. | *Optional* | `600` | 2.2 | @@ -628,6 +629,22 @@ Set to true if you want to list only the first itinerary which goes through the Itineraries visiting the same set of stops and riding the exact same routes, departing later are removed from the result. +

removeTransitWithHigherCostThanBestOnStreetOnly

+ +**Since version:** `2.4` ∙ **Type:** `cost-linear-function` ∙ **Cardinality:** `Optional` ∙ **Default value:** `"1m + 1.30 t"` +**Path:** /routingDefaults/itineraryFilters + +Limit function for generalized-cost computed from street-only itineries applied to transit itineraries. + +The max-limit is applied to itineraries with transit *legs*, and only itineraries +without transit legs are considered when calculating the minimum cost. The smallest +generalized-cost value is used as input to the function. The function is used to calculate a +*max-limit*. The max-limit is then used to filter *transit* itineraries by +*generalized-cost*. Itineraries with a cost higher than the max-limit are dropped from the result +set. Walking is handled with a different logic: if a transit itinerary has higher cost than +a plain walk itinerary, it will be removed even if the cost limit function would keep it. + +

transitGeneralizedCostLimit

**Since version:** `2.1` ∙ **Type:** `object` ∙ **Cardinality:** `Optional` @@ -930,6 +947,8 @@ include stairs as a last result. "costLimitFunction" : "15m + 1.5 x", "intervalRelaxFactor" : 0.4 }, + "nonTransitGeneralizedCostLimit" : "400 + 1.5x", + "removeTransitWithHigherCostThanBestOnStreetOnly" : "60 + 1.3x", "bikeRentalDistanceRatio" : 0.3, "accessibilityScore" : true, "minBikeParkingDistance" : 300, diff --git a/docs/RouterConfiguration.md b/docs/RouterConfiguration.md index 709988d042c..1e77d3513a2 100644 --- a/docs/RouterConfiguration.md +++ b/docs/RouterConfiguration.md @@ -434,7 +434,7 @@ HTTP headers to add to the request. Any header key, value can be inserted. ```JSON // router-config.json { - "configVersion" : "v2.3.0-EN000121", + "configVersion" : "v2.4.0-EN000121", "server" : { "apiProcessingTimeout" : "7s", "traceParameters" : [ @@ -504,6 +504,8 @@ HTTP headers to add to the request. Any header key, value can be inserted. "costLimitFunction" : "15m + 1.5 x", "intervalRelaxFactor" : 0.4 }, + "nonTransitGeneralizedCostLimit" : "400 + 1.5x", + "removeTransitWithHigherCostThanBestOnStreetOnly" : "60 + 1.3x", "bikeRentalDistanceRatio" : 0.3, "accessibilityScore" : true, "minBikeParkingDistance" : 300, diff --git a/docs/RoutingModes.md b/docs/RoutingModes.md index 9b96e65e97d..88148d370e5 100644 --- a/docs/RoutingModes.md +++ b/docs/RoutingModes.md @@ -17,7 +17,7 @@ Cycling for the entirety of the route or taking a bicycle onto the public transp Taking a rented, shared-mobility bike for part or the entirety of the route. -_Prerequisite:_ Vehicle or station location need to be added to OTP from dynamic data feeds. +_Prerequisite:_ Vehicle or station locations need to be added to OTP from dynamic data feeds. See [Configuring GBFS](UpdaterConfig.md#gbfs-vehicle-rental-systems) on how to add one. @@ -25,6 +25,7 @@ See [Configuring GBFS](UpdaterConfig.md#gbfs-vehicle-rental-systems) on how to a Leaving the bicycle at the departure station and walking from the arrival station to the destination. This mode needs to be combined with at least one transit mode otherwise it behaves like an ordinary bicycle journey. + _Prerequisite:_ Bicycle parking stations present in the OSM file and visible to OTP by enabling the property `staticBikeParkAndRide` during graph build. @@ -51,7 +52,7 @@ Walking to a pickup point along the road, driving to a drop-off point along the Walk to a car rental point, drive to a car rental drop-off point and walk the rest of the way. This can include car rental at fixed locations or free-floating services. -_Prerequisite:_ Vehicle or station location need to be added to OTP from dynamic data feeds. +_Prerequisite:_ Vehicle or station locations need to be added to OTP from dynamic data feeds. See [Configuring GBFS](UpdaterConfig.md#gbfs-vehicle-rental-systems) on how to add one. @@ -64,14 +65,14 @@ _Prerequisite:_ Park-and-ride areas near the stations need to be present in the

FLEXIBLE

-Encompasses all types of on-demand and flexible transportation for example GTFS Flex or NeTex Flexible Stop Places. +Encompasses all types of on-demand and flexible transportation for example GTFS Flex or NeTEx Flexible Stop Places.

SCOOTER_RENTAL

Walking to a scooter rental point, riding a scooter to a scooter rental drop-off point, and walking the rest of the way. This can include scooter rental at fixed locations or free-floating services. -_Prerequisite:_ Vehicle or station location need to be added to OTP from dynamic data feeds. +_Prerequisite:_ Vehicle or station locations need to be added to OTP from dynamic data feeds. See [Configuring GBFS](UpdaterConfig.md#gbfs-vehicle-rental-systems) on how to add one. diff --git a/docs/examples/entur/build-config.json b/docs/examples/entur/build-config.json index 5102107c7d6..3829e975f34 100644 --- a/docs/examples/entur/build-config.json +++ b/docs/examples/entur/build-config.json @@ -13,8 +13,6 @@ "islandWithoutStopsMaxSize": 5, "islandWithStopsMaxSize": 5 }, - "banDiscouragedWalking": false, - "banDiscouragedBiking": false, "maxTransferDuration": "30m", "distanceBetweenElevationSamples": 25, "multiThreadElevationCalculations": true, diff --git a/docs/index.md b/docs/index.md index 49f28850f68..b70c99e4d52 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,7 +26,8 @@ the selector in the upper left of the published documentation. **Releases** -- [Latest](http://docs.opentripplanner.org/en/latest) - Version 2.3 (the git master branch) +- [Latest](http://docs.opentripplanner.org/en/latest) - Version 2.4.0 (the git master branch) +- [v2.3.0](http://docs.opentripplanner.org/en/v2.3.0) - Version 2.3 - [v2.2.0](http://docs.opentripplanner.org/en/v2.2.0) - Version 2.2 - [v2.1.0](http://docs.opentripplanner.org/en/v2.1.0) - Version 2.1 - [v2.0.0](http://docs.opentripplanner.org/en/v2.0.0) - Version 2.0 diff --git a/docs/requirements.txt b/docs/requirements.txt index e8863d98147..5cd0396ae85 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,4 @@ mkdocs==1.5.2 mkdocs-material==9.1.17 -mike==1.1.2 +mike@git+https://github.com/jimporter/mike.git@f0522f245e64687dd18384fbd86b721175711474 mkdocs-no-sitemap-plugin==0.0.1 -# remove this after OTP 2.4.0 has been released -mkdocs-redirects==1.2.1 diff --git a/enunciate.xml b/enunciate.xml deleted file mode 100644 index 5b5a0cbfaeb..00000000000 --- a/enunciate.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mkdocs.yml b/mkdocs.yml index 417e81c6c8d..27d0a6b2b1e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,10 +13,6 @@ strict: true plugins: - "no-sitemap" - search - # remove this after OTP 2.4.0 has been released - - redirects: - redirect_maps: - 'sandbox/LegacyGraphQLApi.md': 'sandbox/GtfsGraphQlApi.md' theme: name: material diff --git a/pom.xml b/pom.xml index 33c275041ba..afe0af5a35d 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ https://opentripplanner.org org.opentripplanner otp - 2.4.0-SNAPSHOT + 2.5.0-SNAPSHOT jar @@ -56,18 +56,18 @@ - 119 + 120 29.2 2.48 2.15.2 3.1.3 5.10.0 - 1.11.3 + 1.11.4 5.5.3 1.4.11 9.7.0 - 2.0.7 + 2.0.9 2.0.14 1.22 3.0.2 @@ -187,23 +187,6 @@ - - com.webcohesion.enunciate - enunciate-maven-plugin - 2.15.1 - - - - site - - docs - - - - - ${project.build.directory}/site/enunciate - - org.apache.maven.plugins maven-source-plugin @@ -261,13 +244,6 @@ - - - org.apache.maven.plugins - maven-site-plugin - 3.12.1 - - org.apache.maven.plugins maven-surefire-plugin @@ -395,7 +371,7 @@ properly if some input files are missing a terminating newline) --> org.apache.maven.plugins maven-shade-plugin - 3.5.0 + 3.5.1 package @@ -745,7 +721,7 @@ org.entur.gbfs gbfs-java-model - 3.0.1 + 3.0.9 @@ -902,7 +878,13 @@ org.onebusaway onebusaway-gtfs - 1.4.4 + 1.4.5 + + + org.slf4j + slf4j-simple + + @@ -931,7 +913,7 @@ com.graphql-java graphql-java - 21.0 + 21.1 com.graphql-java @@ -962,7 +944,7 @@ io.github.ci-cmg mapbox-vector-tile - 4.0.5 + 4.0.6 net.objecthunter @@ -999,7 +981,7 @@ org.apache.commons commons-compress - 1.23.0 + 1.24.0 test diff --git a/src/ext-test/java/org/opentripplanner/ext/fares/impl/DefaultFareServiceTest.java b/src/ext-test/java/org/opentripplanner/ext/fares/impl/DefaultFareServiceTest.java index d93acc8de74..779ea7bd4b7 100644 --- a/src/ext-test/java/org/opentripplanner/ext/fares/impl/DefaultFareServiceTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/fares/impl/DefaultFareServiceTest.java @@ -8,6 +8,10 @@ import static org.opentripplanner.ext.fares.impl.FareModelForTest.CITY_CENTER_A_STOP; import static org.opentripplanner.ext.fares.impl.FareModelForTest.CITY_CENTER_B_STOP; import static org.opentripplanner.ext.fares.impl.FareModelForTest.INSIDE_CITY_CENTER_SET; +import static org.opentripplanner.ext.fares.impl.FareModelForTest.OTHER_FEED_ATTRIBUTE; +import static org.opentripplanner.ext.fares.impl.FareModelForTest.OTHER_FEED_ROUTE; +import static org.opentripplanner.ext.fares.impl.FareModelForTest.OTHER_FEED_SET; +import static org.opentripplanner.ext.fares.impl.FareModelForTest.OTHER_FEED_STOP; import static org.opentripplanner.ext.fares.impl.FareModelForTest.SUBURB_STOP; import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary; @@ -98,4 +102,79 @@ void unknownLeg() { var firstBusLeg = itin.firstTransitLeg().get(); assertEquals(List.of(firstBusLeg), component.legs()); } + + @Test + void multipleFeeds() { + var service = new DefaultFareService(); + service.addFareRules(FareType.regular, List.of(AIRPORT_TO_CITY_CENTER_SET, OTHER_FEED_SET)); + var itin = newItinerary(Place.forStop(AIRPORT_STOP)) + .bus(1, T11_00, T11_05, Place.forStop(CITY_CENTER_A_STOP)) + .walk(10, Place.forStop(OTHER_FEED_STOP)) + .bus(OTHER_FEED_ROUTE, 2, T11_20, T11_32, Place.forStop(OTHER_FEED_STOP)) + .build(); + var result = service.calculateFares(itin); + + var resultComponents = result + .getComponents(FareType.regular) + .stream() + .map(r -> r.fareId()) + .toList(); + + var resultPrice = result.getFare(FareType.regular); + + assertEquals( + List.of(AIRPORT_TO_CITY_CENTER_SET.getFareAttribute().getId(), OTHER_FEED_ATTRIBUTE.getId()), + resultComponents + ); + + assertEquals(Money.usDollars(20), resultPrice); + } + + @Test + void multipleFeedsWithTransfersWithinFeed() { + var service = new DefaultFareService(); + service.addFareRules(FareType.regular, List.of(INSIDE_CITY_CENTER_SET, OTHER_FEED_SET)); + var itin = newItinerary(Place.forStop(OTHER_FEED_STOP)) + .bus(OTHER_FEED_ROUTE, 2, T11_00, T11_05, Place.forStop(OTHER_FEED_STOP)) + .walk(10, Place.forStop(CITY_CENTER_A_STOP)) + .bus(1, T11_00, T11_05, Place.forStop(CITY_CENTER_A_STOP)) + .walk(10, Place.forStop(OTHER_FEED_STOP)) + .bus(OTHER_FEED_ROUTE, 2, T11_20, T11_32, Place.forStop(OTHER_FEED_STOP)) + .build(); + var result = service.calculateFares(itin); + + var resultComponents = result + .getComponents(FareType.regular) + .stream() + .map(r -> r.fareId()) + .toList(); + + var resultPrice = result.getFare(FareType.regular); + assertEquals( + List.of(INSIDE_CITY_CENTER_SET.getFareAttribute().getId(), OTHER_FEED_ATTRIBUTE.getId()), + resultComponents + ); + + assertEquals(Money.usDollars(20), resultPrice); + } + + @Test + void multipleFeedsWithUnknownFareLegs() { + var service = new DefaultFareService(); + service.addFareRules(FareType.regular, List.of(AIRPORT_TO_CITY_CENTER_SET, OTHER_FEED_SET)); + var itin = newItinerary(Place.forStop(AIRPORT_STOP)) + .bus(1, T11_00, T11_05, Place.forStop(OTHER_FEED_STOP)) + .walk(10, Place.forStop(OTHER_FEED_STOP)) + .bus(OTHER_FEED_ROUTE, 2, T11_20, T11_32, Place.forStop(OTHER_FEED_STOP)) + .build(); + var result = service.calculateFares(itin); + var resultComponents = result + .getComponents(FareType.regular) + .stream() + .map(r -> r.fareId()) + .toList(); + var resultPrice = result.getFare(FareType.regular); + assertEquals(List.of(OTHER_FEED_ATTRIBUTE.getId()), resultComponents); + assertEquals(Money.usDollars(-0.01f), resultPrice); + } } diff --git a/src/ext-test/java/org/opentripplanner/ext/fares/impl/FareModelForTest.java b/src/ext-test/java/org/opentripplanner/ext/fares/impl/FareModelForTest.java index 74c9baa5422..5c8b52548e7 100644 --- a/src/ext-test/java/org/opentripplanner/ext/fares/impl/FareModelForTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/fares/impl/FareModelForTest.java @@ -1,11 +1,17 @@ package org.opentripplanner.ext.fares.impl; +import static org.opentripplanner.transit.model._data.TransitModelForTest.FEED_ID; +import static org.opentripplanner.transit.model._data.TransitModelForTest.OTHER_AGENCY; +import static org.opentripplanner.transit.model._data.TransitModelForTest.OTHER_FEED_AGENCY; import static org.opentripplanner.transit.model._data.TransitModelForTest.id; import org.opentripplanner.ext.fares.model.FareAttribute; import org.opentripplanner.ext.fares.model.FareRuleSet; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.NonLocalizedString; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.site.FareZone; import org.opentripplanner.transit.model.site.RegularStop; @@ -14,6 +20,10 @@ public class FareModelForTest { public static final FareZone AIRPORT_ZONE = FareZone.of(id("airport-zone")).build(); public static final FareZone CITY_CENTER_ZONE = FareZone.of(id("city-center")).build(); + public static final FareZone OTHER_FEED_ZONE = FareZone + .of(FeedScopedId.ofNullable("F2", "other-feed-zone")) + .build(); + static RegularStop AIRPORT_STOP = RegularStop .of(id("airport")) .withCoordinate(new WgsCoordinate(1, 1)) @@ -39,16 +49,33 @@ public class FareModelForTest { .withName(new NonLocalizedString("Suburb")) .build(); + static RegularStop OTHER_FEED_STOP = RegularStop + .of(FeedScopedId.ofNullable("F2", "other-feed-stop")) + .withCoordinate(new WgsCoordinate(1, 5)) + .withName(new NonLocalizedString("Other feed stop")) + .addFareZones(OTHER_FEED_ZONE) + .build(); static FareAttribute TEN_DOLLARS = FareAttribute .of(id("airport-to-city-center")) .setCurrencyType("USD") .setPrice(10) .setTransfers(0) .build(); + + static FareAttribute OTHER_FEED_ATTRIBUTE = FareAttribute + .of(FeedScopedId.ofNullable("F2", "other-feed-attribute")) + .setCurrencyType("USD") + .setPrice(10) + .setTransfers(1) + .setAgency(OTHER_FEED_AGENCY.getId()) + .build(); + // Fare rule sets static FareRuleSet AIRPORT_TO_CITY_CENTER_SET = new FareRuleSet(TEN_DOLLARS); static FareRuleSet INSIDE_CITY_CENTER_SET = new FareRuleSet(TEN_DOLLARS); + static FareRuleSet OTHER_FEED_SET = new FareRuleSet(OTHER_FEED_ATTRIBUTE); + static { AIRPORT_TO_CITY_CENTER_SET.addOriginDestination( AIRPORT_ZONE.getId().getId(), @@ -58,5 +85,16 @@ public class FareModelForTest { CITY_CENTER_ZONE.getId().getId(), CITY_CENTER_ZONE.getId().getId() ); + OTHER_FEED_SET.addOriginDestination( + OTHER_FEED_ZONE.getId().getId(), + OTHER_FEED_ZONE.getId().getId() + ); } + + static Route OTHER_FEED_ROUTE = Route + .of(new FeedScopedId("F2", "other-feed-route")) + .withAgency(OTHER_FEED_AGENCY) + .withLongName(new NonLocalizedString("other-feed-route")) + .withMode(TransitMode.BUS) + .build(); } diff --git a/src/ext-test/java/org/opentripplanner/ext/fares/impl/HSLFareServiceTest.java b/src/ext-test/java/org/opentripplanner/ext/fares/impl/HSLFareServiceTest.java index 8960128ff0e..d590f43ebe6 100644 --- a/src/ext-test/java/org/opentripplanner/ext/fares/impl/HSLFareServiceTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/fares/impl/HSLFareServiceTest.java @@ -61,6 +61,12 @@ private static List createTestCases() { .withTimezone("Europe/Helsinki") .build(); + Agency agency3 = Agency + .of(new FeedScopedId("FEED2", "AG3")) + .withName("Agency 3") + .withTimezone("Europe/Helsinki") + .build(); + FareZone A = FareZone.of(new FeedScopedId(FEED_ID, "A")).build(); FareZone B = FareZone.of(new FeedScopedId(FEED_ID, "B")).build(); FareZone C = FareZone.of(new FeedScopedId(FEED_ID, "C")).build(); @@ -147,6 +153,12 @@ private static List createTestCases() { .setAgency(agency2.getId()) .build(); + FareAttribute fareAttributeAgency3 = FareAttribute + .of(new FeedScopedId("FEED2", "attribute")) + .setCurrencyType("EUR") + .setAgency(agency3.getId()) + .build(); + // Fare rule sets FareRuleSet ruleSetAB = new FareRuleSet(fareAttributeAB); ruleSetAB.addContains("A"); @@ -183,6 +195,9 @@ private static List createTestCases() { ruleSetD2.addContains("D"); ruleSetD2.setAgency(agency2.getId()); + FareRuleSet ruleSetAgency3 = new FareRuleSet(fareAttributeAgency3); + ruleSetAgency3.addContains("B"); + hslFareService.addFareRules( FareType.regular, List.of( @@ -193,7 +208,8 @@ private static List createTestCases() { ruleSetBCD, ruleSetABCD, ruleSetD, - ruleSetD2 + ruleSetD2, + ruleSetAgency3 ) ); @@ -211,6 +227,13 @@ private static List createTestCases() { .withMode(TransitMode.BUS) .build(); + Route routeAgency3 = Route + .of(new FeedScopedId("FEED2", "R3")) + .withAgency(agency3) + .withLongName(new NonLocalizedString("Route agency 3")) + .withMode(TransitMode.BUS) + .build(); + // Itineraries within zone A Itinerary A1_A2 = newItinerary(A1, T11_06).bus(1, T11_06, T11_12, A2).build(); @@ -366,6 +389,49 @@ private static List createTestCases() { ) ); + // Multifeed case + Itinerary A1_A2_2 = newItinerary(A1, T11_06) + .bus(routeAgency3, 1, T11_06, T11_14, A2) + .bus(routeAgency1, 2, T11_30, T11_50, A1) + .build(); + + args.add( + Arguments.of( + "Bus ride within zone A with two legs using different agencies from different feeds ", + hslFareService, + A1_A2_2, + List.of(fareAttributeAB.getId()) + ) + ); + + Itinerary i = newItinerary(D1, T11_06) + .bus(routeAgency1, 1, T11_06, T11_10, D2) + .walk(10, D1) + .bus(routeAgency2, 2, T11_20, T11_30, D2) + .build(); + + args.add( + Arguments.of( + "Multi-agency itinerary", + hslFareService, + i, + List.of(fareAttributeD.getId(), fareAttributeD2.getId()) + ) + ); + + Itinerary i2 = newItinerary(B1) + .bus(routeAgency1, 1, T11_06, T11_12, B1) + .bus(routeAgency3, 1, T11_14, T11_15, B2) + .build(); + + args.add( + Arguments.of( + "", + hslFareService, + i2, + List.of(fareAttributeAB.getId(), fareAttributeAgency3.getId()) + ) + ); return args; } diff --git a/src/ext-test/java/org/opentripplanner/ext/gtfsgraphqlapi/GraphQLIntegrationTest.java b/src/ext-test/java/org/opentripplanner/ext/gtfsgraphqlapi/GraphQLIntegrationTest.java index 1d358e17a71..c57a05735c0 100644 --- a/src/ext-test/java/org/opentripplanner/ext/gtfsgraphqlapi/GraphQLIntegrationTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/gtfsgraphqlapi/GraphQLIntegrationTest.java @@ -31,13 +31,16 @@ import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.stream.Stream; import javax.annotation.Nonnull; import org.glassfish.jersey.message.internal.OutboundJaxrsResponse; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; +import org.opentripplanner._support.text.I18NStrings; import org.opentripplanner._support.time.ZoneIds; import org.opentripplanner.ext.fares.FaresToItineraryMapper; import org.opentripplanner.ext.fares.impl.DefaultFareService; +import org.opentripplanner.framework.collection.ListUtils; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.NonLocalizedString; @@ -62,6 +65,8 @@ import org.opentripplanner.routing.core.FareType; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graphfinder.GraphFinder; +import org.opentripplanner.routing.impl.TransitAlertServiceImpl; +import org.opentripplanner.routing.services.TransitAlertService; import org.opentripplanner.routing.vehicle_parking.VehicleParking; import org.opentripplanner.service.vehiclepositions.internal.DefaultVehiclePositionService; import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; @@ -70,6 +75,7 @@ import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.basic.Money; import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.framework.AbstractBuilder; import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.RegularStop; @@ -109,7 +115,8 @@ static void setup() { var stopModel = StopModel.of(); PlanTestConstants.listStops().forEach(sl -> stopModel.withRegularStop((RegularStop) sl)); - var transitModel = new TransitModel(stopModel.build(), DEDUPLICATOR); + var model = stopModel.build(); + var transitModel = new TransitModel(model, DEDUPLICATOR); final TripPattern pattern = TransitModelForTest.pattern(BUS).build(); var trip = TransitModelForTest.trip("123").withHeadsign(I18NString.of("Trip Headsign")).build(); @@ -128,7 +135,7 @@ static void setup() { TransitModelForTest .route(m.name()) .withMode(m) - .withLongName(new NonLocalizedString("Long name for %s".formatted(m))) + .withLongName(I18NString.of("Long name for %s".formatted(m))) .build() ) .toList(); @@ -175,27 +182,40 @@ static void setup() { railLeg.withAccessibilityScore(.3f); + var entitySelector = new EntitySelector.Stop(A.stop.getId()); var alert = TransitAlert .of(id("an-alert")) - .withHeaderText(new NonLocalizedString("A header")) - .withDescriptionText(new NonLocalizedString("A description")) - .withUrl(new NonLocalizedString("https://example.com")) + .withHeaderText(I18NString.of("A header")) + .withDescriptionText(I18NString.of("A description")) + .withUrl(I18NString.of("https://example.com")) .withCause(AlertCause.MAINTENANCE) .withEffect(AlertEffect.REDUCED_SERVICE) .withSeverity(AlertSeverity.VERY_SEVERE) - .addEntity(new EntitySelector.Stop(A.stop.getId())) + .addEntity(entitySelector) .addTimePeriod( new TimePeriod(ALERT_START_TIME.getEpochSecond(), ALERT_END_TIME.getEpochSecond()) ) .build(); + railLeg.addAlert(alert); 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); + context = new GraphQLRequestContext( new TestRoutingService(List.of(i1)), @@ -222,10 +242,10 @@ void graphQL(Path path) throws IOException { Locale.ENGLISH, context ); - var actualJson = extracted(response); + var actualJson = responseBody(response); assertEquals(200, response.getStatus()); - Path expectationFile = getPath(path); + Path expectationFile = getExpectation(path); if (!expectationFile.toFile().exists()) { Files.writeString( @@ -242,11 +262,32 @@ void graphQL(Path path) throws IOException { assertEqualJson(expectedJson, actualJson); } + @Nonnull + private static List getTransitAlert(EntitySelector.Stop entitySelector) { + var alertWithoutDescription = TransitAlert + .of(id("no-description")) + .withHeaderText(I18NStrings.TRANSLATED_STRING_1) + .addEntity(entitySelector); + + var alertWithoutHeader = TransitAlert + .of(id("no-header")) + .withDescriptionText(I18NStrings.TRANSLATED_STRING_2) + .addEntity(entitySelector); + var alertWithNothing = TransitAlert + .of(id("neither-header-nor-description")) + .addEntity(entitySelector); + + return Stream + .of(alertWithoutDescription, alertWithoutHeader, alertWithNothing) + .map(AbstractBuilder::build) + .toList(); + } + @Nonnull private static WalkStepBuilder walkStep(String name) { return WalkStep .builder() - .withDirectionText(new NonLocalizedString(name)) + .withDirectionText(I18NString.of(name)) .withStartLocation(WgsCoordinate.GREENWICH) .withAngle(10); } @@ -268,7 +309,7 @@ private static FareProduct fareProduct(String name) { * subdirectories are expected to be in the same directory. */ @Nonnull - private static Path getPath(Path path) { + private static Path getExpectation(Path path) { return path .getParent() .getParent() @@ -276,7 +317,7 @@ private static Path getPath(Path path) { .resolve(path.getFileName().toString().replace(".graphql", ".json")); } - private static String extracted(Response response) { + private static String responseBody(Response response) { if (response instanceof OutboundJaxrsResponse outbound) { return (String) outbound.getContext().getEntity(); } diff --git a/src/ext-test/java/org/opentripplanner/ext/gtfsgraphqlapi/mapping/BikesAllowedMapperTest.java b/src/ext-test/java/org/opentripplanner/ext/gtfsgraphqlapi/mapping/BikesAllowedMapperTest.java new file mode 100644 index 00000000000..ba1b6a2a5f8 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/gtfsgraphqlapi/mapping/BikesAllowedMapperTest.java @@ -0,0 +1,21 @@ +package org.opentripplanner.ext.gtfsgraphqlapi.mapping; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.model.network.BikeAccess; + +class BikesAllowedMapperTest { + + @Test + void mapping() { + Arrays + .stream(BikeAccess.values()) + .filter(ba -> ba != BikeAccess.UNKNOWN) + .forEach(d -> { + var mapped = BikesAllowedMapper.map(d); + assertEquals(d.toString(), mapped.toString()); + }); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/gtfsgraphqlapi/mapping/StreetNoteMapperTest.java b/src/ext-test/java/org/opentripplanner/ext/gtfsgraphqlapi/mapping/StreetNoteMapperTest.java index 976db86fbe5..5c7cd8be008 100644 --- a/src/ext-test/java/org/opentripplanner/ext/gtfsgraphqlapi/mapping/StreetNoteMapperTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/gtfsgraphqlapi/mapping/StreetNoteMapperTest.java @@ -1,13 +1,13 @@ package org.opentripplanner.ext.gtfsgraphqlapi.mapping; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.Date; import java.util.Locale; +import java.util.Optional; import org.junit.jupiter.api.Test; import org.opentripplanner.routing.alertpatch.TransitAlert; import org.opentripplanner.street.model.note.StreetNote; @@ -25,9 +25,9 @@ class StreetNoteMapperTest { void mapRegularAlert() { var note = note(); TransitAlert alert = StreetNoteMapper.mapStreetNoteToAlert(note); - assertEquals(TEST_STREET_NOTE_HEADER, alert.headerText().toString(Locale.ROOT)); - assertEquals(TEST_STREET_NOTE_DESCRIPTION, alert.descriptionText().toString(Locale.ROOT)); - assertEquals(TEST_STREET_NOTE_URL, alert.url().toString(Locale.ROOT)); + assertEquals(TEST_STREET_NOTE_HEADER, alert.headerText().get().toString(Locale.ROOT)); + assertEquals(TEST_STREET_NOTE_DESCRIPTION, alert.descriptionText().get().toString(Locale.ROOT)); + assertEquals(TEST_STREET_NOTE_URL, alert.url().get().toString(Locale.ROOT)); assertEquals(START_INSTANCE, alert.getEffectiveStartDate()); assertEquals(END_INSTANCE, alert.getEffectiveEndDate()); } @@ -37,7 +37,7 @@ void mapNullUrl() { var note = note(); note.url = null; TransitAlert alert = StreetNoteMapper.mapStreetNoteToAlert(note); - assertNull(alert.url()); + assertEquals(Optional.empty(), alert.url()); } @Test diff --git a/src/ext-test/resources/gtfsgraphqlapi/expectations/alerts.json b/src/ext-test/resources/gtfsgraphqlapi/expectations/alerts.json new file mode 100644 index 00000000000..eb7b3d24154 --- /dev/null +++ b/src/ext-test/resources/gtfsgraphqlapi/expectations/alerts.json @@ -0,0 +1,64 @@ +{ + "data" : { + "alerts" : [ + { + "id" : "QWxlcnQ6RjpuZWl0aGVyLWhlYWRlci1ub3ItZGVzY3JpcHRpb24", + "alertHeaderText" : "", + "alertDescriptionText" : "", + "alertUrl" : null, + "alertDescriptionTextTranslations" : [ ], + "alertHeaderTextTranslations" : [ ] + }, + { + "id" : "QWxlcnQ6Rjpuby1oZWFkZXI", + "alertHeaderText" : "Second string", + "alertDescriptionText" : "Second string", + "alertUrl" : null, + "alertDescriptionTextTranslations" : [ + { + "language" : null, + "text" : "Second string" + }, + { + "language" : "de", + "text" : "Zweite Zeichenabfolge" + }, + { + "language" : "fi", + "text" : "Etkö ole varma, mitä tämä tarkoittaa" + } + ], + "alertHeaderTextTranslations" : [ ] + }, + { + "id" : "QWxlcnQ6Rjphbi1hbGVydA", + "alertHeaderText" : "A header", + "alertDescriptionText" : "A description", + "alertUrl" : "https://example.com", + "alertDescriptionTextTranslations" : [ ], + "alertHeaderTextTranslations" : [ ] + }, + { + "id" : "QWxlcnQ6Rjpuby1kZXNjcmlwdGlvbg", + "alertHeaderText" : "First string", + "alertDescriptionText" : "First string", + "alertUrl" : null, + "alertDescriptionTextTranslations" : [ ], + "alertHeaderTextTranslations" : [ + { + "text" : "First string", + "language" : null + }, + { + "text" : "Erste Zeichenabfolge", + "language" : "de" + }, + { + "text" : "Minulla ei ole aavistustakaan kuinka puhua suomea", + "language" : "fi" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/src/ext-test/resources/gtfsgraphqlapi/expectations/routes.json b/src/ext-test/resources/gtfsgraphqlapi/expectations/routes.json index e2bdb289c32..d6cb1b481ed 100644 --- a/src/ext-test/resources/gtfsgraphqlapi/expectations/routes.json +++ b/src/ext-test/resources/gtfsgraphqlapi/expectations/routes.json @@ -9,7 +9,8 @@ "gtfsId" : "F:A1", "name" : "Agency Test" }, - "mode" : "CARPOOL" + "mode" : "CARPOOL", + "bikesAllowed" : "NO_INFORMATION" }, { "longName" : "Long name for SUBWAY", @@ -19,7 +20,8 @@ "gtfsId" : "F:A1", "name" : "Agency Test" }, - "mode" : "SUBWAY" + "mode" : "SUBWAY", + "bikesAllowed" : "NO_INFORMATION" }, { "longName" : "Long name for BUS", @@ -29,7 +31,8 @@ "gtfsId" : "F:A1", "name" : "Agency Test" }, - "mode" : "BUS" + "mode" : "BUS", + "bikesAllowed" : "NO_INFORMATION" }, { "longName" : "Long name for FERRY", @@ -39,7 +42,8 @@ "gtfsId" : "F:A1", "name" : "Agency Test" }, - "mode" : "FERRY" + "mode" : "FERRY", + "bikesAllowed" : "NO_INFORMATION" }, { "longName" : "Long name for COACH", @@ -49,7 +53,8 @@ "gtfsId" : "F:A1", "name" : "Agency Test" }, - "mode" : "COACH" + "mode" : "COACH", + "bikesAllowed" : "NO_INFORMATION" }, { "longName" : "Long name for TRAM", @@ -59,7 +64,8 @@ "gtfsId" : "F:A1", "name" : "Agency Test" }, - "mode" : "TRAM" + "mode" : "TRAM", + "bikesAllowed" : "NO_INFORMATION" }, { "longName" : "Long name for CABLE_CAR", @@ -69,7 +75,8 @@ "gtfsId" : "F:A1", "name" : "Agency Test" }, - "mode" : "CABLE_CAR" + "mode" : "CABLE_CAR", + "bikesAllowed" : "NO_INFORMATION" }, { "longName" : "Long name for FUNICULAR", @@ -79,7 +86,8 @@ "gtfsId" : "F:A1", "name" : "Agency Test" }, - "mode" : "FUNICULAR" + "mode" : "FUNICULAR", + "bikesAllowed" : "NO_INFORMATION" }, { "longName" : "Long name for RAIL", @@ -89,7 +97,8 @@ "gtfsId" : "F:A1", "name" : "Agency Test" }, - "mode" : "RAIL" + "mode" : "RAIL", + "bikesAllowed" : "NO_INFORMATION" }, { "longName" : "Long name for MONORAIL", @@ -99,7 +108,8 @@ "gtfsId" : "F:A1", "name" : "Agency Test" }, - "mode" : "MONORAIL" + "mode" : "MONORAIL", + "bikesAllowed" : "NO_INFORMATION" }, { "longName" : "Long name for GONDOLA", @@ -109,7 +119,8 @@ "gtfsId" : "F:A1", "name" : "Agency Test" }, - "mode" : "GONDOLA" + "mode" : "GONDOLA", + "bikesAllowed" : "NO_INFORMATION" }, { "longName" : "Long name for TROLLEYBUS", @@ -119,7 +130,8 @@ "gtfsId" : "F:A1", "name" : "Agency Test" }, - "mode" : "TROLLEYBUS" + "mode" : "TROLLEYBUS", + "bikesAllowed" : "NO_INFORMATION" }, { "longName" : "Long name for AIRPLANE", @@ -129,7 +141,8 @@ "gtfsId" : "F:A1", "name" : "Agency Test" }, - "mode" : "AIRPLANE" + "mode" : "AIRPLANE", + "bikesAllowed" : "NO_INFORMATION" }, { "longName" : "Long name for TAXI", @@ -139,7 +152,8 @@ "gtfsId" : "F:A1", "name" : "Agency Test" }, - "mode" : "TAXI" + "mode" : "TAXI", + "bikesAllowed" : "NO_INFORMATION" } ] } diff --git a/src/ext-test/resources/gtfsgraphqlapi/queries/alerts.graphql b/src/ext-test/resources/gtfsgraphqlapi/queries/alerts.graphql new file mode 100644 index 00000000000..923d9f027cb --- /dev/null +++ b/src/ext-test/resources/gtfsgraphqlapi/queries/alerts.graphql @@ -0,0 +1,18 @@ +{ + alerts { + id + alertHeaderText + alertDescriptionText + alertUrl + # these translations are a bit questionable, the above fields are already translated into the + # language selected in the request + alertDescriptionTextTranslations { + language + text + } + alertHeaderTextTranslations { + text + language + } + } +} \ No newline at end of file diff --git a/src/ext-test/resources/gtfsgraphqlapi/queries/routes.graphql b/src/ext-test/resources/gtfsgraphqlapi/queries/routes.graphql index 66d7f370c3f..38585d6a836 100644 --- a/src/ext-test/resources/gtfsgraphqlapi/queries/routes.graphql +++ b/src/ext-test/resources/gtfsgraphqlapi/queries/routes.graphql @@ -8,5 +8,6 @@ name } mode + bikesAllowed } } \ No newline at end of file diff --git a/src/ext/java/org/opentripplanner/ext/fares/impl/DefaultFareService.java b/src/ext/java/org/opentripplanner/ext/fares/impl/DefaultFareService.java index 50871355951..744782f7b56 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/impl/DefaultFareService.java +++ b/src/ext/java/org/opentripplanner/ext/fares/impl/DefaultFareService.java @@ -12,6 +12,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.Nullable; import org.opentripplanner.ext.fares.model.FareAttribute; import org.opentripplanner.ext.fares.model.FareRuleSet; @@ -60,11 +61,10 @@ class FareSearch { record FareAndId(Money fare, FeedScopedId fareId) {} /** - * This fare service module handles the cases that GTFS handles within a single feed. It cannot - * necessarily handle multi-feed graphs, because a rule-less fare attribute might be applied to - * rides on routes in another feed, for example. For more interesting fare structures like New - * York's MTA, or cities with multiple feeds and inter-feed transfer rules, you get to implement - * your own FareService. See this thread on gtfs-changes explaining the proper interpretation of + * This fare service module handles GTFS fares in multiple feeds separately so that each fare attribute + * is only applicable for legs that operated by an agency within the same feed. Interfeed transfer rules + * are not considered in this fare service and for those situations you get to implement your own Fare Service + * See this thread on gtfs-changes explaining the proper interpretation of * fares.txt: * http://groups.google.com/group/gtfs-changes/browse_thread/thread/8a4a48ae1e742517/4f81b826cb732f3b */ @@ -72,6 +72,8 @@ public class DefaultFareService implements FareService { private static final Logger LOG = LoggerFactory.getLogger(DefaultFareService.class); + private final float UNKNOWN_FARE_PRICE = -0.01f; + /** For each fare type (regular, student, etc...) the collection of rules that apply. */ protected Map> fareRulesPerType; @@ -102,18 +104,89 @@ public ItineraryFares calculateFares(Itinerary itinerary) { if (fareLegs.isEmpty()) { return null; } + var fareLegsByFeed = fareLegs + .stream() + .collect(Collectors.groupingBy(leg -> leg.getAgency().getId().getFeedId())); + var fareRulesByTypeAndFeed = fareRulesPerType + .entrySet() + .stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + rules -> + rules + .getValue() + .stream() + .collect(Collectors.groupingBy(rule -> rule.getFareAttribute().getId().getFeedId())) + ) + ); ItineraryFares fare = ItineraryFares.empty(); boolean hasFare = false; - for (Map.Entry> kv : fareRulesPerType.entrySet()) { - FareType fareType = kv.getKey(); - Collection fareRules = kv.getValue(); - // Get the currency from the first fareAttribute, assuming that all tickets use the same currency. - if (fareRules.size() > 0) { - Currency currency = Currency.getInstance( - fareRules.iterator().next().getFareAttribute().getCurrencyType() + for (FareType fareType : fareRulesPerType.keySet()) { + List components = new ArrayList<>(); + List fares = new ArrayList<>(); + ItineraryFares currentFare = ItineraryFares.empty(); + boolean legWithoutRulesFound = false; + boolean legsWithoutMatchingRulesFound = false; + boolean fareTypeHasFare = false; + for (String feedId : fareLegsByFeed.keySet()) { + var fareRules = fareRulesByTypeAndFeed.get(fareType).get(feedId); + + // Get the currency from the first fareAttribute, assuming that all tickets use the same currency. + if (fareRules != null && fareRules.size() > 0) { + Currency currency = Currency.getInstance( + fareRules.iterator().next().getFareAttribute().getCurrencyType() + ); + boolean feedHasFare = populateFare( + currentFare, + currency, + fareType, + fareLegsByFeed.get(feedId), + fareRules + ); + + if (!feedHasFare) { + legsWithoutMatchingRulesFound = true; + } + hasFare = feedHasFare || hasFare; // Other feeds might still have fare for some legs + + components.addAll(currentFare.getComponents(fareType)); + fare.addFare(fareType, currentFare.getFare(fareType)); + fares.add(currentFare.getFare(fareType)); + + // If all the legs are from one feed, consider itinerary products + if (fareLegs.equals(fareLegsByFeed.get(feedId))) { + fare.addItineraryProducts(currentFare.getItineraryProducts()); + } + } else { + legWithoutRulesFound = true; + } + } + + fare.addFareComponent(fareType, components); + + // No fares will be discovered after this point + if (!hasFare) { + continue; + } + + // Accumulate the final price of the fare or indicate that no final fare could be found + if (legWithoutRulesFound || legsWithoutMatchingRulesFound) { + fare.addFare( + fareType, + Money.ofFractionalAmount(fares.get(0).currency(), UNKNOWN_FARE_PRICE) + ); + } else { + fare.addFare( + fareType, + fares + .stream() + .reduce( + Money.ofFractionalAmount(fare.getFare(fareType).currency(), 0), + (r1, r2) -> r1.plus(r2) + ) ); - hasFare = populateFare(fare, currency, fareType, fareLegs, fareRules); } } return hasFare ? fare : null; @@ -202,11 +275,11 @@ protected Optional getBestFareAndId( String startZone = firstRide.getFrom().stop.getFirstZoneAsString(); String endZone = null; // stops don't really have an agency id, they have the per-feed default id - String feedId = firstRide.getTrip().getId().getFeedId(); + String feedId = firstRide.getAgency().getId().getFeedId(); ZonedDateTime lastRideStartTime = null; ZonedDateTime lastRideEndTime = null; for (var leg : legs) { - if (!leg.getTrip().getId().getFeedId().equals(feedId)) { + if (!leg.getAgency().getId().getFeedId().equals(feedId)) { LOG.debug("skipped multi-feed ride sequence {}", legs); return Optional.empty(); } diff --git a/src/ext/java/org/opentripplanner/ext/fares/impl/HSLFareServiceImpl.java b/src/ext/java/org/opentripplanner/ext/fares/impl/HSLFareServiceImpl.java index e8ec215b149..ba62e899f70 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/impl/HSLFareServiceImpl.java +++ b/src/ext/java/org/opentripplanner/ext/fares/impl/HSLFareServiceImpl.java @@ -1,17 +1,24 @@ package org.opentripplanner.ext.fares.impl; +import com.google.common.collect.Sets; import java.time.Duration; import java.time.ZonedDateTime; +import java.util.ArrayList; import java.util.Collection; +import java.util.Currency; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import org.opentripplanner.ext.fares.model.FareAttribute; import org.opentripplanner.ext.fares.model.FareRuleSet; import org.opentripplanner.ext.fares.model.RouteOriginDestination; +import org.opentripplanner.model.fare.ItineraryFares; +import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.model.plan.Leg; import org.opentripplanner.model.plan.ScheduledTransitLeg; +import org.opentripplanner.routing.core.FareComponent; import org.opentripplanner.routing.core.FareType; import org.opentripplanner.transit.model.basic.Money; import org.slf4j.Logger; @@ -51,6 +58,19 @@ protected Optional getBestFareAndId( String agency = null; boolean singleAgency = true; + // Do not consider fares for legs that do not have fare rules in the same feed + Set fareRuleFeedIds = fareRules + .stream() + .map(fr -> fr.getFareAttribute().getId().getFeedId()) + .collect(Collectors.toSet()); + Set legFeedIds = legs + .stream() + .map(leg -> leg.getAgency().getId().getFeedId()) + .collect(Collectors.toSet()); + if (!Sets.difference(legFeedIds, fareRuleFeedIds).isEmpty()) { + return Optional.empty(); + } + for (Leg leg : legs) { lastRideStartTime = leg.getStartTime(); if (agency == null) { @@ -58,11 +78,12 @@ protected Optional getBestFareAndId( } else if (agency != leg.getAgency().getId().getId().toString()) { singleAgency = false; } + /* HSL specific logic: all exception routes start and end from the defined zone set, but visit temporarily (maybe 1 stop only) an 'external' zone */ Money bestSpecialFare = MAX_PRICE; - Set ruleZones = null; + Set ruleZones = null; for (FareRuleSet ruleSet : fareRules) { if ( ruleSet.hasAgencyDefined() && @@ -114,6 +135,7 @@ but visit temporarily (maybe 1 stop only) an 'external' zone */ } } } + if (ruleZones != null) { // the special case // evaluate boolean ride.zones AND rule.zones Set zoneIntersection = new HashSet( diff --git a/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java b/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java index 775a64323e0..f375b24fefa 100644 --- a/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java +++ b/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java @@ -2,13 +2,10 @@ import java.util.Collection; import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.function.Predicate; import java.util.stream.Stream; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.framework.lang.PredicateUtils; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.site.StopLocationsGroup; import org.opentripplanner.transit.service.TransitService; @@ -44,7 +41,7 @@ Stream generateStopClusters( .filter(sl -> sl.getName() != null) // if they are very close to each other and have the same name, only one is chosen (at random) .filter( - distinctByKey(sl -> + PredicateUtils.distinctByKey(sl -> new DeduplicationKey(sl.getName(), sl.getCoordinate().roundToApproximate10m()) ) ) @@ -84,10 +81,5 @@ private static StopCluster.Coordinate toCoordinate(WgsCoordinate c) { return new StopCluster.Coordinate(c.latitude(), c.longitude()); } - private static Predicate distinctByKey(Function keyExtractor) { - Set seen = ConcurrentHashMap.newKeySet(); - return t -> seen.add(keyExtractor.apply(t)); - } - private record DeduplicationKey(I18NString name, WgsCoordinate coordinate) {} } diff --git a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/AlertImpl.java b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/AlertImpl.java index 5708a151c31..b9e7c650b93 100644 --- a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/AlertImpl.java +++ b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/AlertImpl.java @@ -23,7 +23,6 @@ import org.opentripplanner.ext.gtfsgraphqlapi.model.StopOnRouteModel; import org.opentripplanner.ext.gtfsgraphqlapi.model.StopOnTripModel; import org.opentripplanner.ext.gtfsgraphqlapi.model.UnknownModel; -import org.opentripplanner.framework.graphql.GraphQLUtils; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.TranslatedString; import org.opentripplanner.routing.alertpatch.EntitySelector; @@ -40,6 +39,8 @@ public class AlertImpl implements GraphQLDataFetchers.GraphQLAlert { + private static final String FALLBACK_EMPTY_STRING = ""; + @Override public DataFetcher agency() { return environment -> @@ -62,16 +63,20 @@ public DataFetcher alertCause() { @Override public DataFetcher alertDescriptionText() { - return environment -> - getSource(environment).descriptionText().toString(environment.getLocale()); + return environment -> { + var alert = getSource(environment); + return alert + .descriptionText() + .or(alert::headerText) + .map(t -> t.toString(environment.getLocale())) + .orElse(FALLBACK_EMPTY_STRING); + }; } @Override public DataFetcher>> alertDescriptionTextTranslations() { - return environment -> { - var text = getSource(environment).descriptionText(); - return getTranslations(text); - }; + return environment -> + getSource(environment).descriptionText().map(this::getTranslations).orElse(List.of()); } @Override @@ -96,16 +101,20 @@ public DataFetcher alertHash() { @Override public DataFetcher alertHeaderText() { - return environment -> - GraphQLUtils.getTranslation(getSource(environment).headerText(), environment); + return environment -> { + var alert = getSource(environment); + return alert + .headerText() + .or(alert::descriptionText) + .map(h -> h.toString(environment.getLocale())) + .orElse(FALLBACK_EMPTY_STRING); + }; } @Override public DataFetcher>> alertHeaderTextTranslations() { - return environment -> { - var text = getSource(environment).headerText(); - return getTranslations(text); - }; + return environment -> + getSource(environment).headerText().map(this::getTranslations).orElse(List.of()); } @Override @@ -115,18 +124,13 @@ public DataFetcher alertSeverityLevel() { @Override public DataFetcher alertUrl() { - return environment -> { - var alertUrl = getSource(environment).url(); - return alertUrl == null ? null : alertUrl.toString(environment.getLocale()); - }; + return environment -> + getSource(environment).url().map(u -> u.toString(environment.getLocale())).orElse(null); } @Override public DataFetcher>> alertUrlTranslations() { - return environment -> { - var url = getSource(environment).url(); - return getTranslations(url); - }; + return environment -> getSource(environment).url().map(this::getTranslations).orElse(List.of()); } @Override diff --git a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/QueryTypeImpl.java b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/QueryTypeImpl.java index 11a297a0011..ad1907016e6 100644 --- a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/QueryTypeImpl.java +++ b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/QueryTypeImpl.java @@ -31,6 +31,7 @@ import org.opentripplanner.ext.gtfsgraphqlapi.GraphQLUtils; import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLDataFetchers; import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes; +import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes.GraphQLQueryTypeStopsByRadiusArgs; import org.opentripplanner.ext.gtfsgraphqlapi.mapping.RouteRequestMapper; import org.opentripplanner.framework.time.ServiceDateUtils; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; @@ -721,7 +722,7 @@ public DataFetcher> stopsByBbox() { public DataFetcher> stopsByRadius() { return environment -> { // TODO implement rest of the args - GraphQLTypes.GraphQLQueryTypeStopsByRadiusArgs args = new GraphQLTypes.GraphQLQueryTypeStopsByRadiusArgs( + GraphQLQueryTypeStopsByRadiusArgs args = new GraphQLQueryTypeStopsByRadiusArgs( environment.getArguments() ); diff --git a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/RouteImpl.java b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/RouteImpl.java index c61020db759..2453257cdf6 100644 --- a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/RouteImpl.java +++ b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/RouteImpl.java @@ -11,7 +11,9 @@ import org.opentripplanner.ext.gtfsgraphqlapi.GraphQLUtils; import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLDataFetchers; import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes; +import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes.GraphQLBikesAllowed; import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes.GraphQLTransitMode; +import org.opentripplanner.ext.gtfsgraphqlapi.mapping.BikesAllowedMapper; import org.opentripplanner.routing.alertpatch.EntitySelector; import org.opentripplanner.routing.alertpatch.TransitAlert; import org.opentripplanner.routing.services.TransitAlertService; @@ -131,13 +133,8 @@ public DataFetcher> alerts() { } @Override - public DataFetcher bikesAllowed() { - return environment -> - switch (getSource(environment).getBikesAllowed()) { - case UNKNOWN -> "NO_INFORMATION"; - case ALLOWED -> "POSSIBLE"; - case NOT_ALLOWED -> "NOT_POSSIBLE"; - }; + public DataFetcher bikesAllowed() { + return environment -> BikesAllowedMapper.map(getSource(environment).getBikesAllowed()); } @Override diff --git a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/TripImpl.java b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/TripImpl.java index a12cedff82c..adab6fe169d 100644 --- a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/TripImpl.java +++ b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/datafetchers/TripImpl.java @@ -20,7 +20,9 @@ import org.opentripplanner.ext.gtfsgraphqlapi.GraphQLUtils; import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLDataFetchers; import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes; +import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes.GraphQLBikesAllowed; import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes.GraphQLWheelchairBoarding; +import org.opentripplanner.ext.gtfsgraphqlapi.mapping.BikesAllowedMapper; import org.opentripplanner.framework.time.ServiceDateUtils; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TripTimeOnDate; @@ -165,13 +167,8 @@ public DataFetcher arrivalStoptime() { } @Override - public DataFetcher bikesAllowed() { - return environment -> - switch (getSource(environment).getBikesAllowed()) { - case UNKNOWN -> "NO_INFORMATION"; - case ALLOWED -> "POSSIBLE"; - case NOT_ALLOWED -> "NOT_POSSIBLE"; - }; + public DataFetcher bikesAllowed() { + return environment -> BikesAllowedMapper.map(getSource(environment).getBikesAllowed()); } @Override diff --git a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/GraphQLDataFetchers.java b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/GraphQLDataFetchers.java index 318c4fc2a11..171e648dbcd 100644 --- a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/GraphQLDataFetchers.java +++ b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/GraphQLDataFetchers.java @@ -15,6 +15,7 @@ import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes.GraphQLAlertCauseType; import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes.GraphQLAlertEffectType; import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes.GraphQLAlertSeverityLevelType; +import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes.GraphQLBikesAllowed; import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes.GraphQLInputField; import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes.GraphQLRelativeDirection; import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes.GraphQLRoutingErrorCode; @@ -309,9 +310,12 @@ public interface GraphQLDefaultFareProduct { } /** - * Departure row is a location, which lists departures of a certain pattern from a - * stop. Departure rows are identified with the pattern, so querying departure rows - * will return only departures from one stop per pattern + * Departure row is a combination of a pattern and a stop of that pattern. + * + * They are de-duplicated so for each pattern there will only be a single departure row. + * + * This is useful if you want to show a list of stop/pattern combinations but want each pattern to be + * listed only once. */ public interface GraphQLDepartureRow { public DataFetcher id(); @@ -789,7 +793,7 @@ public interface GraphQLRoute { public DataFetcher> alerts(); - public DataFetcher bikesAllowed(); + public DataFetcher bikesAllowed(); public DataFetcher color(); @@ -1015,7 +1019,7 @@ public interface GraphQLTrip { public DataFetcher arrivalStoptime(); - public DataFetcher bikesAllowed(); + public DataFetcher bikesAllowed(); public DataFetcher blockId(); diff --git a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/graphql-codegen.yml b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/graphql-codegen.yml index 7408e72cbca..6605044b36a 100644 --- a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/graphql-codegen.yml +++ b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/graphql-codegen.yml @@ -40,7 +40,7 @@ config: VehicleRentalStation: org.opentripplanner.service.vehiclerental.model.VehicleRentalStation#VehicleRentalStation RentalVehicle: org.opentripplanner.service.vehiclerental.model.VehicleRentalVehicle#VehicleRentalVehicle VehicleRentalUris: org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris#VehicleRentalStationUris - BikesAllowed: String + BikesAllowed: org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes.GraphQLBikesAllowed#GraphQLBikesAllowed BookingInfo: org.opentripplanner.model.BookingInfo BookingTime: org.opentripplanner.model.BookingTime CarPark: org.opentripplanner.routing.vehicle_parking.VehicleParking#VehicleParking diff --git a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/package.json b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/package.json index 4716c905ef3..c37b2e5a83b 100644 --- a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/package.json +++ b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/package.json @@ -14,6 +14,6 @@ "@graphql-codegen/cli": "4.0.1", "@graphql-codegen/java": "3.3.6", "@graphql-codegen/java-resolvers": "2.3.6", - "graphql": "16.7.1" + "graphql": "16.8.1" } } diff --git a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/yarn.lock b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/yarn.lock index 643f5e3e2b3..350a1b440fc 100644 --- a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/yarn.lock +++ b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/generated/yarn.lock @@ -2051,10 +2051,10 @@ graphql-ws@^5.14.0: resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.14.0.tgz#766f249f3974fc2c48fae0d1fb20c2c4c79cd591" integrity sha512-itrUTQZP/TgswR4GSSYuwWUzrE/w5GhbwM2GX3ic2U7aw33jgEsayfIlvaj7/GcIvZgNMzsPTrE5hqPuFUiE5g== -graphql@16.7.1: - version "16.7.1" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.7.1.tgz#11475b74a7bff2aefd4691df52a0eca0abd9b642" - integrity sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg== +graphql@16.8.1: + version "16.8.1" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" + integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== has-flag@^3.0.0: version "3.0.0" diff --git a/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/mapping/BikesAllowedMapper.java b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/mapping/BikesAllowedMapper.java new file mode 100644 index 00000000000..ad7019ac343 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/gtfsgraphqlapi/mapping/BikesAllowedMapper.java @@ -0,0 +1,17 @@ +package org.opentripplanner.ext.gtfsgraphqlapi.mapping; + +import javax.annotation.Nonnull; +import org.opentripplanner.ext.gtfsgraphqlapi.generated.GraphQLTypes.GraphQLBikesAllowed; +import org.opentripplanner.transit.model.network.BikeAccess; + +public class BikesAllowedMapper { + + @Nonnull + public static GraphQLBikesAllowed map(@Nonnull BikeAccess bikesAllowed) { + return switch (bikesAllowed) { + case UNKNOWN -> GraphQLBikesAllowed.NO_INFORMATION; + case ALLOWED -> GraphQLBikesAllowed.ALLOWED; + case NOT_ALLOWED -> GraphQLBikesAllowed.NOT_ALLOWED; + }; + } +} diff --git a/src/ext/java/org/opentripplanner/ext/reportapi/model/BicycleSafetyReport.java b/src/ext/java/org/opentripplanner/ext/reportapi/model/BicycleSafetyReport.java index 0c299f824d5..8830189107d 100644 --- a/src/ext/java/org/opentripplanner/ext/reportapi/model/BicycleSafetyReport.java +++ b/src/ext/java/org/opentripplanner/ext/reportapi/model/BicycleSafetyReport.java @@ -31,7 +31,7 @@ public static String makeCsv(OsmTagMapperSource source) { buf.addBoolean(false); buf.addText(p.properties().getPermission().toString()); - var safetyProps = p.properties().getBicycleSafetyFeatures(); + var safetyProps = p.properties().bicycleSafety(); if (safetyProps != null) { buf.addNumber(safetyProps.forward()); buf.addNumber(safetyProps.back()); diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java index a5e6f2bcae6..937adc112ab 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java @@ -218,7 +218,7 @@ private Result apply( } /* commit */ - return addTripToGraphAndBuffer(result.successValue(), journey, entityResolver); + return addTripToGraphAndBuffer(result.successValue()); } catch (Exception e) { LOG.warn( "{} EstimatedJourney {} failed.", @@ -359,11 +359,7 @@ private Result handleModifiedTrip( /** * Add a (new) trip to the transitModel and the buffer */ - private Result addTripToGraphAndBuffer( - TripUpdate tripUpdate, - EstimatedVehicleJourney estimatedVehicleJourney, - EntityResolver entityResolver - ) { + private Result addTripToGraphAndBuffer(TripUpdate tripUpdate) { Trip trip = tripUpdate.tripTimes().getTrip(); LocalDate serviceDate = tripUpdate.serviceDate(); diff --git a/src/ext/java/org/opentripplanner/ext/siri/mapper/AffectsMapper.java b/src/ext/java/org/opentripplanner/ext/siri/mapper/AffectsMapper.java index e789aa9a11d..734cc8edbd3 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/mapper/AffectsMapper.java +++ b/src/ext/java/org/opentripplanner/ext/siri/mapper/AffectsMapper.java @@ -378,25 +378,6 @@ private static FeedScopedId getStop( return null; } - private static FeedScopedId getTripId( - String vehicleJourney, - String feedId, - TransitService transitService - ) { - Trip trip = transitService.getTripForId(new FeedScopedId(feedId, vehicleJourney)); - if (trip != null) { - return trip.getId(); - } - //Attempt to find trip using datedServiceJourneys - TripOnServiceDate tripOnServiceDate = transitService.getTripOnServiceDateById( - new FeedScopedId(feedId, vehicleJourney) - ); - if (tripOnServiceDate != null) { - return tripOnServiceDate.getTrip().getId(); - } - return null; - } - private static Set resolveStopConditions( List stopConditions ) { diff --git a/src/ext/java/org/opentripplanner/ext/transmodelapi/model/siri/sx/PtSituationElementType.java b/src/ext/java/org/opentripplanner/ext/transmodelapi/model/siri/sx/PtSituationElementType.java index f9239d16c37..6cc72568aab 100644 --- a/src/ext/java/org/opentripplanner/ext/transmodelapi/model/siri/sx/PtSituationElementType.java +++ b/src/ext/java/org/opentripplanner/ext/transmodelapi/model/siri/sx/PtSituationElementType.java @@ -175,16 +175,19 @@ public static GraphQLObjectType create( .name("summary") .type(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(multilingualStringType)))) .description("Summary of situation in all different translations available") - .dataFetcher(environment -> { - I18NString headerText = environment.getSource().headerText(); - if (headerText instanceof TranslatedString translatedString) { - return translatedString.getTranslations(); - } else if (headerText != null) { - return List.of(new AbstractMap.SimpleEntry<>(null, headerText.toString())); - } else { - return emptyList(); - } - }) + .dataFetcher(environment -> + environment + .getSource() + .headerText() + .map(headerText -> { + if (headerText instanceof TranslatedString translatedString) { + return translatedString.getTranslations(); + } else { + return List.of(new AbstractMap.SimpleEntry<>(null, headerText.toString())); + } + }) + .orElse(emptyList()) + ) .build() ) .field( @@ -193,16 +196,19 @@ public static GraphQLObjectType create( .name("description") .type(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(multilingualStringType)))) .description("Description of situation in all different translations available") - .dataFetcher(environment -> { - I18NString descriptionText = environment.getSource().descriptionText(); - if (descriptionText instanceof TranslatedString translatedString) { - return translatedString.getTranslations(); - } else if (descriptionText != null) { - return List.of(new AbstractMap.SimpleEntry<>(null, descriptionText.toString())); - } else { - return emptyList(); - } - }) + .dataFetcher(environment -> + environment + .getSource() + .descriptionText() + .map(descriptionText -> { + if (descriptionText instanceof TranslatedString translatedString) { + return translatedString.getTranslations(); + } else { + return List.of(new AbstractMap.SimpleEntry<>(null, descriptionText.toString())); + } + }) + .orElse(emptyList()) + ) .build() ) .field( diff --git a/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslParkUpdater.java b/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslParkUpdater.java index 36699aee1af..a921836d907 100644 --- a/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslParkUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslParkUpdater.java @@ -60,7 +60,7 @@ public HslParkUpdater( parameters.utilizationsUrl(), "", parkPatchMapper::parseUtilization, - null + Map.of() ); this.facilitiesFrequencySec = parameters.facilitiesFrequencySec(); } diff --git a/src/main/java/org/opentripplanner/api/mapping/AlertMapper.java b/src/main/java/org/opentripplanner/api/mapping/AlertMapper.java index 24a1aeb07c5..e01961a5dca 100644 --- a/src/main/java/org/opentripplanner/api/mapping/AlertMapper.java +++ b/src/main/java/org/opentripplanner/api/mapping/AlertMapper.java @@ -30,17 +30,9 @@ public List mapToApi(Collection alerts) { ApiAlert mapToApi(TransitAlert domain) { ApiAlert api = new ApiAlert(); - if (domain.headerText() != null) { - api.alertHeaderText = domain.headerText().toString(locale); - } - - if (domain.descriptionText() != null) { - api.alertDescriptionText = domain.descriptionText().toString(locale); - } - - if (domain.url() != null) { - api.alertUrl = domain.url().toString(locale); - } + api.alertHeaderText = domain.headerText().map(h -> h.toString(locale)).orElse(null); + api.alertDescriptionText = domain.descriptionText().map(t -> t.toString(locale)).orElse(null); + api.alertUrl = domain.url().map(u -> u.toString(locale)).orElse(null); api.effectiveStartDate = ofNullableInstant(domain.getEffectiveStartDate()); api.effectiveEndDate = ofNullableInstant(domain.getEffectiveEndDate()); diff --git a/src/main/java/org/opentripplanner/astar/spi/AStarEdge.java b/src/main/java/org/opentripplanner/astar/spi/AStarEdge.java index facb29ba06d..91872021fc1 100644 --- a/src/main/java/org/opentripplanner/astar/spi/AStarEdge.java +++ b/src/main/java/org/opentripplanner/astar/spi/AStarEdge.java @@ -2,6 +2,15 @@ import javax.annotation.Nonnull; +/** + * Represents an edge in the street network. Most edges have a one-to-one mapping to real world + * things like street segments or stairs. + * However, there can be other edges that represent concepts that are invisible and only present + * in OTP's model. These are things that link entities like transit stops and rental vehicles to the + * street network. The links are necessary in order for A* to discover them and can contain logic for + * access permissions. For example, a car can not use an edge that links the + * street network with a transit stop to prevent a straight transfer from car to transit. + */ public interface AStarEdge< State extends AStarState, Edge extends AStarEdge, @@ -11,6 +20,29 @@ public interface AStarEdge< Vertex getToVertex(); + /** + * Traverse the edge from a given state and return the result of the traversal. + * + * @param s0 The 'current' state when arriving at the fromVertex. + * @return The array of states that are the result of the state (the passenger) moving (traversing) + * through the edge. + *

+ * In most cases this is a single state where, for example, the weight (cost) and time are + * increased according to the properties of the edge. + *

+ * However, it is also possible that this edge is not traversable for the current state, + * for example if it's a walk-only edge but the state is arriving in a car. In such a + * case an empty array is returned (see {@link org.opentripplanner.street.search.state.State#empty()}). + * The Astar algorithm won't then explore this state any further as the destination is not + * reachable through this state/edge combination. + *

+ * Lastly, there can also be cases where more than one state is returned: For example + * when a state is renting a free-floating vehicle but the edge is in a no-drop-off zone. + * In such a case two resulting states are possible: one where the state continues on the + * rental vehicle (as you may exit the no-drop-off zone later) and a second state where the + * vehicle is speculatively dropped off and the passenger continues on foot in case + * that the destination is inside the zone. + */ @Nonnull - State[] traverse(State u); + State[] traverse(State s0); } diff --git a/src/main/java/org/opentripplanner/framework/io/JsonDataListDownloader.java b/src/main/java/org/opentripplanner/framework/io/JsonDataListDownloader.java index 867c9de3e2e..aa8bd3d5f69 100644 --- a/src/main/java/org/opentripplanner/framework/io/JsonDataListDownloader.java +++ b/src/main/java/org/opentripplanner/framework/io/JsonDataListDownloader.java @@ -9,7 +9,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Function; +import javax.annotation.Nonnull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,26 +25,26 @@ public class JsonDataListDownloader { private final OtpHttpClient otpHttpClient; public JsonDataListDownloader( - String url, - String jsonParsePath, - Function elementParser, - Map headers + @Nonnull String url, + @Nonnull String jsonParsePath, + @Nonnull Function elementParser, + @Nonnull Map headers ) { this(url, jsonParsePath, elementParser, headers, new OtpHttpClient()); } public JsonDataListDownloader( - String url, - String jsonParsePath, - Function elementParser, - Map headers, - OtpHttpClient OtpHttpClient + @Nonnull String url, + @Nonnull String jsonParsePath, + @Nonnull Function elementParser, + @Nonnull Map headers, + @Nonnull OtpHttpClient OtpHttpClient ) { - this.url = url; - this.jsonParsePath = jsonParsePath; - this.headers = headers; - this.elementParser = elementParser; - this.otpHttpClient = OtpHttpClient; + this.url = Objects.requireNonNull(url); + this.jsonParsePath = Objects.requireNonNull(jsonParsePath); + this.headers = Objects.requireNonNull(headers); + this.elementParser = Objects.requireNonNull(elementParser); + this.otpHttpClient = Objects.requireNonNull(OtpHttpClient); } public List download() { diff --git a/src/main/java/org/opentripplanner/framework/lang/PredicateUtils.java b/src/main/java/org/opentripplanner/framework/lang/PredicateUtils.java new file mode 100644 index 00000000000..867019943c0 --- /dev/null +++ b/src/main/java/org/opentripplanner/framework/lang/PredicateUtils.java @@ -0,0 +1,26 @@ +package org.opentripplanner.framework.lang; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * Utility for building predicates to be used for filtering streams. + */ +public class PredicateUtils { + + /** + * Build a predicate that uses the {@code keyExtractor} to remove any key that has already + * been seen by this (stateful) predicate. + *

+ * This is useful for removing duplicates from a stream where the key to be compared is not the + * entity itself but a field of it. + *

+ * Note: Duplicate check is based on equality not identity. + */ + public static Predicate distinctByKey(Function keyExtractor) { + Map seen = new ConcurrentHashMap<>(); + return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; + } +} diff --git a/src/main/java/org/opentripplanner/graph_builder/issues/ConflictingBikeTags.java b/src/main/java/org/opentripplanner/graph_builder/issues/ConflictingBikeTags.java deleted file mode 100644 index b546db63509..00000000000 --- a/src/main/java/org/opentripplanner/graph_builder/issues/ConflictingBikeTags.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.opentripplanner.graph_builder.issues; - -import org.opentripplanner.graph_builder.issue.api.DataImportIssue; -import org.opentripplanner.openstreetmap.model.OSMWithTags; - -public record ConflictingBikeTags(OSMWithTags entity) implements DataImportIssue { - private static final String FMT = - "Conflicting tags bicycle:[yes|designated] and cycleway:dismount, assuming dismount"; - private static final String HTMLFMT = - "Conflicting tags bicycle:[yes|designated] and cycleway:dismount on way '%s', assuming dismount"; - - @Override - public String getMessage() { - return String.format(FMT); - } - - @Override - public String getHTMLMessage() { - return String.format(HTMLFMT, entity.getOpenStreetMapLink(), entity.getId()); - } -} diff --git a/src/main/java/org/opentripplanner/graph_builder/issues/DisconnectedOsmNode.java b/src/main/java/org/opentripplanner/graph_builder/issues/DisconnectedOsmNode.java index afd247c85d7..f1c341a46fe 100644 --- a/src/main/java/org/opentripplanner/graph_builder/issues/DisconnectedOsmNode.java +++ b/src/main/java/org/opentripplanner/graph_builder/issues/DisconnectedOsmNode.java @@ -21,11 +21,11 @@ public String getMessage() { public String getHTMLMessage() { return String.format( HTMLFMT, - node.getOpenStreetMapLink(), + node.url(), node.getId(), - way.getOpenStreetMapLink(), + way.url(), way.getId(), - area.getOpenStreetMapLink(), + area.url(), area.getId() ); } diff --git a/src/main/java/org/opentripplanner/graph_builder/issues/InvalidOsmGeometry.java b/src/main/java/org/opentripplanner/graph_builder/issues/InvalidOsmGeometry.java index 72dea76fe95..7f84453a6d8 100644 --- a/src/main/java/org/opentripplanner/graph_builder/issues/InvalidOsmGeometry.java +++ b/src/main/java/org/opentripplanner/graph_builder/issues/InvalidOsmGeometry.java @@ -14,6 +14,6 @@ public String getMessage() { @Override public String getHTMLMessage() { - return String.format(HTMLFMT, entity.getOpenStreetMapLink(), entity.getId()); + return String.format(HTMLFMT, entity.url(), entity.getId()); } } diff --git a/src/main/java/org/opentripplanner/graph_builder/issues/InvalidVehicleParkingCapacity.java b/src/main/java/org/opentripplanner/graph_builder/issues/InvalidVehicleParkingCapacity.java index f50f0390009..c4f5498a769 100644 --- a/src/main/java/org/opentripplanner/graph_builder/issues/InvalidVehicleParkingCapacity.java +++ b/src/main/java/org/opentripplanner/graph_builder/issues/InvalidVehicleParkingCapacity.java @@ -17,6 +17,6 @@ public String getMessage() { @Override public String getHTMLMessage() { - return String.format(HTMLFMT, entity.getOpenStreetMapLink(), entity.getId(), capacityValue); + return String.format(HTMLFMT, entity.url(), entity.getId(), capacityValue); } } diff --git a/src/main/java/org/opentripplanner/graph_builder/issues/LevelAmbiguous.java b/src/main/java/org/opentripplanner/graph_builder/issues/LevelAmbiguous.java index 0497f17797b..650c8b299ac 100644 --- a/src/main/java/org/opentripplanner/graph_builder/issues/LevelAmbiguous.java +++ b/src/main/java/org/opentripplanner/graph_builder/issues/LevelAmbiguous.java @@ -20,6 +20,6 @@ public String getMessage() { @Override public String getHTMLMessage() { - return String.format(HTMLFMT, entity.getOpenStreetMapLink(), layerName, entity.getId()); + return String.format(HTMLFMT, entity.url(), layerName, entity.getId()); } } diff --git a/src/main/java/org/opentripplanner/graph_builder/issues/ParkAndRideUnlinked.java b/src/main/java/org/opentripplanner/graph_builder/issues/ParkAndRideUnlinked.java index aed7a9456e8..7da16a2891f 100644 --- a/src/main/java/org/opentripplanner/graph_builder/issues/ParkAndRideUnlinked.java +++ b/src/main/java/org/opentripplanner/graph_builder/issues/ParkAndRideUnlinked.java @@ -16,6 +16,6 @@ public String getMessage() { @Override public String getHTMLMessage() { - return String.format(HTMLFMT, entity.getOpenStreetMapLink(), name, entity); + return String.format(HTMLFMT, entity.url(), name, entity); } } diff --git a/src/main/java/org/opentripplanner/graph_builder/issues/StreetCarSpeedZero.java b/src/main/java/org/opentripplanner/graph_builder/issues/StreetCarSpeedZero.java index f0888821f0e..1107486af97 100644 --- a/src/main/java/org/opentripplanner/graph_builder/issues/StreetCarSpeedZero.java +++ b/src/main/java/org/opentripplanner/graph_builder/issues/StreetCarSpeedZero.java @@ -14,6 +14,6 @@ public String getMessage() { @Override public String getHTMLMessage() { - return String.format(HTMLFMT, entity.getOpenStreetMapLink(), entity.getId()); + return String.format(HTMLFMT, entity.url(), entity.getId()); } } diff --git a/src/main/java/org/opentripplanner/graph_builder/issues/TooManyAreasInRelation.java b/src/main/java/org/opentripplanner/graph_builder/issues/TooManyAreasInRelation.java deleted file mode 100644 index 25efd1f9b0f..00000000000 --- a/src/main/java/org/opentripplanner/graph_builder/issues/TooManyAreasInRelation.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.opentripplanner.graph_builder.issues; - -import org.opentripplanner.graph_builder.issue.api.DataImportIssue; -import org.opentripplanner.openstreetmap.model.OSMWithTags; - -public record TooManyAreasInRelation(OSMWithTags entity) implements DataImportIssue { - private static final String FMT = "Too many areas in relation %s"; - private static final String HTMLFMT = "Too many areas in relation '%s'"; - - @Override - public String getHTMLMessage() { - return String.format(HTMLFMT, entity.getOpenStreetMapLink(), entity.getId()); - } - - @Override - public String getMessage() { - return String.format(FMT, entity.getId()); - } -} diff --git a/src/main/java/org/opentripplanner/graph_builder/issues/TurnRestrictionUnknown.java b/src/main/java/org/opentripplanner/graph_builder/issues/TurnRestrictionUnknown.java index c0f2b749558..36e913cba6a 100644 --- a/src/main/java/org/opentripplanner/graph_builder/issues/TurnRestrictionUnknown.java +++ b/src/main/java/org/opentripplanner/graph_builder/issues/TurnRestrictionUnknown.java @@ -14,6 +14,6 @@ public String getMessage() { @Override public String getHTMLMessage() { - return String.format(HTMLFMT, tagval, entity.getOpenStreetMapLink(), entity.getId()); + return String.format(HTMLFMT, tagval, entity.url(), entity.getId()); } } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java b/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java index 9b8852977f3..98ec013aea0 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/configure/GraphBuilderModules.java @@ -73,8 +73,6 @@ static OsmModule provideOpenStreetMapModule( .withPlatformEntriesLinking(config.platformEntriesLinking) .withStaticParkAndRide(config.staticParkAndRide) .withStaticBikeParkAndRide(config.staticBikeParkAndRide) - .withBanDiscouragedWalking(config.banDiscouragedWalking) - .withBanDiscouragedBiking(config.banDiscouragedBiking) .withMaxAreaNodes(config.maxAreaNodes) .withBoardingAreaRefTags(config.boardingLocationTags) .withIssueStore(issueStore) diff --git a/src/main/java/org/opentripplanner/graph_builder/module/geometry/GeometryProcessor.java b/src/main/java/org/opentripplanner/graph_builder/module/geometry/GeometryProcessor.java index 810ef2bac20..360ad9f5865 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/geometry/GeometryProcessor.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/geometry/GeometryProcessor.java @@ -123,7 +123,7 @@ private LineString[] createGeometry(FeedScopedId shapeId, List stopTim // this trip has a shape_id, but no such shape exists, and no shape_dist in stop_times // create straight line segments between stops for each hop issueStore.add(new MissingShapeGeometry(stopTimes.get(0).getTrip().getId(), shapeId)); - return createStraightLineHopeGeometries(stopTimes, shapeId); + return createStraightLineHopGeometries(stopTimes); } List locations = getLinearLocations(stopTimes, shapeLineString); @@ -132,7 +132,7 @@ private LineString[] createGeometry(FeedScopedId shapeId, List stopTim // their stop sequence. So we'll fall back to trivial stop-to-stop // linking, even though theoretically we could do better. issueStore.add(new ShapeGeometryTooFar(stopTimes.get(0).getTrip().getId(), shapeId)); - return createStraightLineHopeGeometries(stopTimes, shapeId); + return createStraightLineHopGeometries(stopTimes); } return getGeometriesByShape(stopTimes, shapeId, shapeLineString, locations); @@ -267,10 +267,7 @@ private List getLinearLocations(List stopTimes, LineSt return getStopLocations(possibleSegmentsForStop, stopTimes, 0, -1); } - private LineString[] createStraightLineHopeGeometries( - List stopTimes, - FeedScopedId shapeId - ) { + private LineString[] createStraightLineHopGeometries(List stopTimes) { LineString[] geoms = new LineString[stopTimes.size() - 1]; StopTime st0; for (int i = 0; i < stopTimes.size() - 1; ++i) { diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/Area.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/Area.java index 5fb736b5ad6..f0a61e94c5a 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/Area.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/Area.java @@ -25,7 +25,7 @@ class Area { final List outermostRings; // This is the way or relation that has the relevant tags for the area - OSMWithTags parent; + final OSMWithTags parent; public MultiPolygon jtsMultiPolygon; Area( diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/AreaTooComplicated.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/AreaTooComplicated.java index c27c5bed732..65d03ba84ce 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/AreaTooComplicated.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/AreaTooComplicated.java @@ -17,13 +17,7 @@ public String getMessage() { @Override public String getHTMLMessage() { OSMWithTags entity = areaGroup.getSomeOSMObject(); - return String.format( - HTMLFMT, - entity.getOpenStreetMapLink(), - entity.getId(), - nbNodes, - maxAreaNodes - ); + return String.format(HTMLFMT, entity.url(), entity.getId(), nbNodes, maxAreaNodes); } @Override diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/ElevatorProcessor.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/ElevatorProcessor.java index 3a453c9e67e..3a0a9fa404b 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/ElevatorProcessor.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/ElevatorProcessor.java @@ -34,7 +34,7 @@ /** * Contains the logic for extracting elevator data from OSM and converting it to edges. *

- * I depends heavily on the idiosyncratic processing of the OSM data in {@link OsmModule} + * It depends heavily on the idiosyncratic processing of the OSM data in {@link OsmModule} * which is the reason this is not a public class. */ class ElevatorProcessor { @@ -208,12 +208,10 @@ private static void createElevatorHopEdges( } private boolean isElevatorWay(OSMWay way) { - if (!way.hasTag("highway")) { - return false; - } - if (!"elevator".equals(way.getTag("highway"))) { + if (!way.isElevator()) { return false; } + if (osmdb.isAreaWay(way.getId())) { return false; } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java index 0a4713c228c..599c66ef585 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java @@ -19,7 +19,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.stream.Collectors; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; @@ -34,14 +33,12 @@ import org.opentripplanner.graph_builder.issues.DisconnectedOsmNode; import org.opentripplanner.graph_builder.issues.InvalidOsmGeometry; import org.opentripplanner.graph_builder.issues.LevelAmbiguous; -import org.opentripplanner.graph_builder.issues.TooManyAreasInRelation; import org.opentripplanner.graph_builder.issues.TurnRestrictionBad; import org.opentripplanner.graph_builder.issues.TurnRestrictionException; import org.opentripplanner.graph_builder.issues.TurnRestrictionUnknown; import org.opentripplanner.graph_builder.module.osm.TurnRestrictionTag.Direction; import org.opentripplanner.openstreetmap.model.OSMLevel; import org.opentripplanner.openstreetmap.model.OSMLevel.Source; -import org.opentripplanner.openstreetmap.model.OSMMemberType; import org.opentripplanner.openstreetmap.model.OSMNode; import org.opentripplanner.openstreetmap.model.OSMRelation; import org.opentripplanner.openstreetmap.model.OSMRelationMember; @@ -132,11 +129,9 @@ public class OsmDatabase { * the United States. This does not affect floor names from level maps. */ public boolean noZeroLevels = true; - private final Set boardingAreaRefTags; - public OsmDatabase(DataImportIssueStore issueStore, Set boardingAreaRefTags) { + public OsmDatabase(DataImportIssueStore issueStore) { this.issueStore = issueStore; - this.boardingAreaRefTags = boardingAreaRefTags; } public OSMNode getNode(Long nodeId) { @@ -251,12 +246,7 @@ public void addWay(OSMWay way) { /* filter out ways that are not relevant for routing */ if ( - !( - OsmFilter.isWayRoutable(way) || - way.isParkAndRide() || - way.isBikeParking() || - way.isBoardingLocation() - ) + !(way.isRoutable() || way.isParkAndRide() || way.isBikeParking() || way.isBoardingLocation()) ) { return; } @@ -265,12 +255,7 @@ public void addWay(OSMWay way) { /* An area can be specified as such, or be one by default as an amenity */ if ( - ( - way.isTag("area", "yes") || - way.isTag("amenity", "parking") || - way.isTag("amenity", "bicycle_parking") || - way.isBoardingArea() - ) && + (way.isArea() || way.isParkAndRide() || way.isBikeParking() || way.isBoardingArea()) && way.getNodeRefs().size() > 2 ) { // this is an area that's a simple polygon. So we can just add it straight @@ -298,17 +283,15 @@ public void addRelation(OSMRelation relation) { } if ( - relation.isTag("type", "multipolygon") && - (OsmFilter.isOsmEntityRoutable(relation) || relation.isParkAndRide()) || + relation.isMultiPolygon() && + (relation.isRoutable() || relation.isParkAndRide()) || relation.isBikeParking() ) { // OSM MultiPolygons are ferociously complicated, and in fact cannot be processed // without reference to the ways that compose them. Accordingly, we will merely // mark the ways for preservation here, and deal with the details once we have // the ways loaded. - if ( - !OsmFilter.isWayRoutable(relation) && !relation.isParkAndRide() && !relation.isBikeParking() - ) { + if (!relation.isRoutable() && !relation.isParkAndRide() && !relation.isBikeParking()) { return; } for (OSMRelationMember member : relation.getMembers()) { @@ -316,18 +299,12 @@ public void addRelation(OSMRelation relation) { } applyLevelsForWay(relation); } else if ( - !(relation.isTag("type", "restriction")) && - !(relation.isTag("type", "route") && relation.isTag("route", "road")) && - !(relation.isTag("type", "multipolygon") && OsmFilter.isOsmEntityRoutable(relation)) && - !(relation.isTag("type", "level_map")) && - !( - relation.isTag("type", "public_transport") && - relation.isTag("public_transport", "stop_area") - ) && - !( - relation.isTag("type", "route") && - (relation.isTag("route", "road") || relation.isTag("route", "bicycle")) - ) + !relation.isRestriction() && + !relation.isRoadRoute() && + !(relation.isMultiPolygon() && relation.isRoutable()) && + !relation.isLevelMap() && + !relation.isStopArea() && + !(relation.isRoadRoute() || relation.isBicycleRoute()) ) { return; } @@ -740,12 +717,8 @@ private void processMultipolygonRelations() { } if ( !( - relation.isTag("type", "multipolygon") && - ( - OsmFilter.isOsmEntityRoutable(relation) || - relation.isParkAndRide() || - relation.isBikeParking() - ) + relation.isMultiPolygon() && + (relation.isRoutable() || relation.isParkAndRide() || relation.isBikeParking()) ) ) { continue; @@ -754,7 +727,6 @@ private void processMultipolygonRelations() { ArrayList innerWays = new ArrayList<>(); ArrayList outerWays = new ArrayList<>(); for (OSMRelationMember member : relation.getMembers()) { - String role = member.getRole(); OSMWay way = areaWaysById.get(member.getRef()); if (way == null) { // relation includes way which does not exist in the data. Skip. @@ -771,12 +743,12 @@ private void processMultipolygonRelations() { continue RELATION; } } - if (role.equals("inner")) { + if (member.hasRoleInner()) { innerWays.add(way); - } else if (role.equals("outer")) { + } else if (member.hasRoleOuter()) { outerWays.add(way); } else { - LOG.warn("Unexpected role {} in multipolygon", role); + LOG.warn("Unexpected role {} in multipolygon", member.getRole()); } } processedAreas.add(relation); @@ -789,7 +761,7 @@ private void processMultipolygonRelations() { for (OSMRelationMember member : relation.getMembers()) { // multipolygons for attribute mapping - if (!(member.hasType(OSMMemberType.WAY) && waysById.containsKey(member.getRef()))) { + if (!(member.hasTypeWay() && waysById.containsKey(member.getRef()))) { continue; } @@ -803,7 +775,7 @@ private void processMultipolygonRelations() { way.addTag(tag, relation.getTag(tag)); } } - if (relation.isTag("railway", "platform") && !way.hasTag("railway")) { + if (relation.isRailwayPlatform() && !way.hasTag("railway")) { way.addTag("railway", "platform"); } if (relation.isPlatform() && !way.hasTag("public_transport")) { @@ -817,13 +789,10 @@ private void processMultipolygonRelations() { * Handler for a new Area (single way area or multipolygon relations) */ private void newArea(Area area) { - StreetTraversalPermission permissions = OsmFilter.getPermissionsForEntity( - area.parent, + StreetTraversalPermission permissions = area.parent.overridePermissions( StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE ); - if ( - OsmFilter.isOsmEntityRoutable(area.parent) && permissions != StreetTraversalPermission.NONE - ) { + if (area.parent.isRoutable() && permissions != StreetTraversalPermission.NONE) { walkableAreas.add(area); } // Please note: the same area can be both car P+R AND bike park. @@ -842,14 +811,13 @@ private void processRelations() { LOG.debug("Processing relations..."); for (OSMRelation relation : relationsById.valueCollection()) { - if (relation.isTag("type", "restriction")) { + if (relation.isRestriction()) { processRestriction(relation); - } else if (relation.isTag("type", "level_map")) { + } else if (relation.isLevelMap()) { processLevelMap(relation); - } else if (relation.isTag("type", "route")) { - processRoad(relation); - processBicycleRoute(relation); - } else if (relation.isTag("type", "public_transport")) { + } else if (relation.isRoute()) { + processRoute(relation); + } else if (relation.isPublicTransport()) { processPublicTransportStopArea(relation); } } @@ -861,28 +829,10 @@ private void processRelations() { * @see "https://wiki.openstreetmap.org/wiki/Tag:route%3Dbicycle" */ private void processBicycleRoute(OSMRelation relation) { - if (relation.isTag("route", "bicycle")) { - var network = relation.getTag("network"); - - if (network == null) network = "lcn"; - switch (network) { - case "lcn": - setNetworkForAllMembers(relation, "lcn"); - break; - case "rcn": - setNetworkForAllMembers(relation, "rcn"); - break; - case "ncn": - setNetworkForAllMembers(relation, "ncn"); - break; - case "icn": - setNetworkForAllMembers(relation, "icn"); - break; - // we treat networks without known network type like local networks - default: - setNetworkForAllMembers(relation, "lcn"); - break; - } + if (relation.isBicycleRoute()) { + // we treat networks without known network type like local networks + var network = relation.getTagOpt("network").orElse("lcn"); + setNetworkForAllMembers(relation, network); } } @@ -890,9 +840,9 @@ private void setNetworkForAllMembers(OSMRelation relation, String key) { relation .getMembers() .forEach(member -> { - var isOsmWay = member.hasType(OSMMemberType.WAY); + var isOsmWay = member.hasTypeWay(); var way = waysById.get(member.getRef()); - // if it is an OSM way (rather than a node) and it it doesn't already contain the tag + // if it is an OSM way (rather than a node) and it doesn't already contain the tag // we add it if (way != null && isOsmWay && !way.hasTag(key)) { way.addTag(key, "yes"); @@ -1033,7 +983,7 @@ private void processLevelMap(OSMRelation relation) { issueStore ); for (OSMRelationMember member : relation.getMembers()) { - if (member.hasType(OSMMemberType.WAY) && waysById.containsKey(member.getRef())) { + if (member.hasTypeWay() && waysById.containsKey(member.getRef())) { OSMWay way = waysById.get(member.getRef()); if (way != null) { String role = member.getRole(); @@ -1052,11 +1002,11 @@ private void processLevelMap(OSMRelation relation) { } /** - * Handle route=road relations. + * Handle route=road and route=bicycle relations. */ - private void processRoad(OSMRelation relation) { + private void processRoute(OSMRelation relation) { for (OSMRelationMember member : relation.getMembers()) { - if (!(member.hasType(OSMMemberType.WAY) && waysById.containsKey(member.getRef()))) { + if (!(member.hasTypeWay() && waysById.containsKey(member.getRef()))) { continue; } @@ -1086,6 +1036,7 @@ private void processRoad(OSMRelation relation) { } } } + processBicycleRoute(relation); } /** @@ -1102,25 +1053,24 @@ private void processRoad(OSMRelation relation) { private void processPublicTransportStopArea(OSMRelation relation) { Set platformAreas = new HashSet<>(); Set platformNodes = new HashSet<>(); - boolean skipped = false; for (OSMRelationMember member : relation.getMembers()) { switch (member.getType()) { - case NODE: + case NODE -> { var node = nodesById.get(member.getRef()); if (node != null && (node.isEntrance() || node.isBoardingLocation())) { platformNodes.add(node); } - break; - case WAY: - if ("platform".equals(member.getRole()) && areaWayIds.contains(member.getRef())) { + } + case WAY -> { + if (member.hasRolePlatform() && areaWayIds.contains(member.getRef())) { platformAreas.add(areaWaysById.get(member.getRef())); } - break; - case RELATION: - if ("platform".equals(member.getRole()) && relationsById.containsKey(member.getRef())) { + } + case RELATION -> { + if (member.hasRolePlatform() && relationsById.containsKey(member.getRef())) { platformAreas.add(relationsById.get(member.getRef())); } - break; + } } } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmFilter.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmFilter.java index 5901266b401..e69de29bb2d 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmFilter.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmFilter.java @@ -1,264 +0,0 @@ -package org.opentripplanner.graph_builder.module.osm; - -import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; -import org.opentripplanner.graph_builder.issues.ConflictingBikeTags; -import org.opentripplanner.openstreetmap.model.OSMWay; -import org.opentripplanner.openstreetmap.model.OSMWithTags; -import org.opentripplanner.street.model.StreetTraversalPermission; - -/** - * - */ -public class OsmFilter { - - /** - * Determines whether this OSM way is considered routable. The majority of routable ways are those - * with a highway= tag (which includes everything from motorways to hiking trails). Anything with - * a public_transport=platform or railway=platform tag is also considered routable even if it - * doesn't have a highway tag. Platforms are however filtered out if they are marked - * usage=tourism. This prevents miniature tourist railways like the one in Portland's Zoo from - * receiving a better score and pulling search endpoints away from real transit stops. - */ - public static boolean isOsmEntityRoutable(OSMWithTags osmEntity) { - if (osmEntity.hasTag("highway")) { - return true; - } - if (osmEntity.isPlatform()) { - return !("tourism".equals(osmEntity.getTag("usage"))); - } - return false; - } - - public static StreetTraversalPermission getPermissionsForEntity( - OSMWithTags entity, - StreetTraversalPermission def - ) { - StreetTraversalPermission permission = null; - - /* - * Only a few tags are examined here, because we only care about modes supported by OTP - * (wheelchairs are not of concern here) - * - * Only a few values are checked for, all other values are presumed to be permissive (=> - * This may not be perfect, but is closer to reality, since most people don't follow the - * rules perfectly ;-) - */ - if (entity.isGeneralAccessDenied()) { - // this can actually be overridden - permission = StreetTraversalPermission.NONE; - } else { - permission = def; - } - - if (entity.isVehicleExplicitlyDenied()) { - permission = permission.remove(StreetTraversalPermission.BICYCLE_AND_CAR); - } else if (entity.isVehicleExplicitlyAllowed()) { - permission = permission.add(StreetTraversalPermission.BICYCLE_AND_CAR); - } - - if (entity.isMotorcarExplicitlyDenied() || entity.isMotorVehicleExplicitlyDenied()) { - permission = permission.remove(StreetTraversalPermission.CAR); - } else if (entity.isMotorcarExplicitlyAllowed() || entity.isMotorVehicleExplicitlyAllowed()) { - permission = permission.add(StreetTraversalPermission.CAR); - } - - if (entity.isBicycleExplicitlyDenied()) { - permission = permission.remove(StreetTraversalPermission.BICYCLE); - } else if (entity.isBicycleExplicitlyAllowed()) { - permission = permission.add(StreetTraversalPermission.BICYCLE); - } - - if (entity.isPedestrianExplicitlyDenied()) { - permission = permission.remove(StreetTraversalPermission.PEDESTRIAN); - } else if (entity.isPedestrianExplicitlyAllowed()) { - permission = permission.add(StreetTraversalPermission.PEDESTRIAN); - } - - if (entity.isUnderConstruction()) { - permission = StreetTraversalPermission.NONE; - } - - if (permission == null) { - return def; - } - return permission; - } - - /** - * Computes permissions for an OSMWay. - */ - public static StreetTraversalPermission getPermissionsForWay( - OSMWay way, - StreetTraversalPermission def, - boolean banDiscouragedWalking, - boolean banDiscouragedBiking, - DataImportIssueStore issueStore - ) { - StreetTraversalPermission permissions = getPermissionsForEntity(way, def); - - /* - * pedestrian rules: everything is two-way (assuming pedestrians are allowed at all) bicycle - * rules: default: permissions; - * - * cycleway=dismount means walk your bike -- the engine will automatically try walking bikes - * any time it is forbidden to ride them, so the only thing to do here is to remove bike - * permissions - * - * oneway=... sets permissions for cars and bikes oneway:bicycle overwrites these - * permissions for bikes only - * - * now, cycleway=opposite_lane, opposite, opposite_track can allow once oneway has been set - * by oneway:bicycle, but should give a warning if it conflicts with oneway:bicycle - * - * bicycle:backward=yes works like oneway:bicycle=no bicycle:backwards=no works like - * oneway:bicycle=yes - */ - - // Compute pedestrian permissions. - if (way.isPedestrianExplicitlyAllowed()) { - permissions = permissions.add(StreetTraversalPermission.PEDESTRIAN); - } else if (way.isPedestrianExplicitlyDenied()) { - permissions = permissions.remove(StreetTraversalPermission.PEDESTRIAN); - } - - // Check for foot=discouraged, if applicable - if (banDiscouragedWalking && way.hasTag("foot") && way.getTag("foot").equals("discouraged")) { - permissions = permissions.remove(StreetTraversalPermission.PEDESTRIAN); - } - - // Compute bike permissions, check consistency. - boolean forceBikes = false; - if (way.isBicycleExplicitlyAllowed()) { - permissions = permissions.add(StreetTraversalPermission.BICYCLE); - forceBikes = true; - } - - if ( - way.isBicycleDismountForced() || - (banDiscouragedBiking && way.hasTag("bicycle") && way.getTag("bicycle").equals("discouraged")) - ) { - permissions = permissions.remove(StreetTraversalPermission.BICYCLE); - if (forceBikes) { - issueStore.add(new ConflictingBikeTags(way)); - } - } - - return permissions; - } - - public static StreetTraversalPermission getPermissionsForWay( - OSMWay way, - StreetTraversalPermission def - ) { - return getPermissionsForWay(way, def, false, false, DataImportIssueStore.NOOP); - } - - /** - * Check OSM tags for various one-way and one-way-by-mode tags and return a pair of permissions - * for travel along and against the way. - */ - public static StreetTraversalPermissionPair getPermissions( - StreetTraversalPermission permissions, - OSMWay way - ) { - StreetTraversalPermission permissionsFront = permissions; - StreetTraversalPermission permissionsBack = permissions; - - // Check driving direction restrictions. - if (way.isOneWayForwardDriving() || way.isRoundabout()) { - permissionsBack = permissionsBack.remove(StreetTraversalPermission.BICYCLE_AND_CAR); - } - if (way.isOneWayReverseDriving()) { - permissionsFront = permissionsFront.remove(StreetTraversalPermission.BICYCLE_AND_CAR); - } - - // Check bike direction restrictions. - if (way.isOneWayForwardBicycle()) { - permissionsBack = permissionsBack.remove(StreetTraversalPermission.BICYCLE); - } - if (way.isOneWayReverseBicycle()) { - permissionsFront = permissionsFront.remove(StreetTraversalPermission.BICYCLE); - } - - // TODO(flamholz): figure out what this is for. - String oneWayBicycle = way.getTag("oneway:bicycle"); - if (OSMWithTags.isFalse(oneWayBicycle) || way.isTagTrue("bicycle:backwards")) { - if (permissions.allows(StreetTraversalPermission.BICYCLE)) { - permissionsFront = permissionsFront.add(StreetTraversalPermission.BICYCLE); - permissionsBack = permissionsBack.add(StreetTraversalPermission.BICYCLE); - } - } - - //This needs to be after adding permissions for oneway:bicycle=no - //removes bicycle permission when bicycles need to use sidepath - //TAG: bicycle:forward=use_sidepath - if (way.isForwardDirectionSidepath()) { - permissionsFront = permissionsFront.remove(StreetTraversalPermission.BICYCLE); - } - - //TAG bicycle:backward=use_sidepath - if (way.isReverseDirectionSidepath()) { - permissionsBack = permissionsBack.remove(StreetTraversalPermission.BICYCLE); - } - - if (way.isOpposableCycleway()) { - permissionsBack = permissionsBack.add(StreetTraversalPermission.BICYCLE); - } - return new StreetTraversalPermissionPair(permissionsFront, permissionsBack); - } - - public static boolean isLink(OSMWithTags way) { - String highway = way.getTag("highway"); - return highway != null && highway.endsWith(("_link")); - } - - /** - * Determine whether any mode can or should ever traverse the given way. If not, we leave the way - * out of the OTP graph. Potentially routable ways are those that have the tags : highway=* - * public_transport=platform railway=platform - *

- * But not conveyers, proposed highways/roads or those still under construction, and raceways (as - * well as ways where all access is specifically forbidden to the public). - * http://wiki.openstreetmap.org/wiki/Tag:highway%3Dproposed - *

- * A whitelist for highway tags is an alternative to a blacklist. - */ - static boolean isWayRoutable(OSMWithTags way) { - if (!isOsmEntityRoutable(way)) { - return false; - } - - String highway = way.getTag("highway"); - if (highway != null) { - if ( - highway.equals("proposed") || - highway.equals("planned") || - highway.equals("construction") || - highway.equals("razed") || - highway.equals("raceway") || - highway.equals("abandoned") || - highway.equals("historic") || - highway.equals("no") || - highway.equals("emergency_bay") || - highway.equals("rest_area") || - highway.equals("services") || - highway.equals("bus_guideway") || - highway.equals("escape") - ) { - return false; - } - } - - if (way.isGeneralAccessDenied()) { - // There are exceptions. - return ( - way.isMotorcarExplicitlyAllowed() || - way.isBicycleExplicitlyAllowed() || - way.isPedestrianExplicitlyAllowed() || - way.isMotorVehicleExplicitlyAllowed() || - way.isVehicleExplicitlyAllowed() - ); - } - return true; - } -} diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java index bfab1097ad7..1f54820a912 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java @@ -65,7 +65,7 @@ public class OsmModule implements GraphBuilderModule { this.graph = graph; this.issueStore = issueStore; this.params = params; - this.osmdb = new OsmDatabase(issueStore, params.boardingAreaRefTags()); + this.osmdb = new OsmDatabase(issueStore); this.vertexGenerator = new VertexGenerator(osmdb, graph, params.boardingAreaRefTags()); this.normalizer = new SafetyValueNormalizer(graph, issueStore); } @@ -247,14 +247,10 @@ private void buildBasicGraph() { WAY:for (OSMWay way : osmdb.getWays()) { WayProperties wayData = way.getOsmProvider().getWayPropertySet().getDataForWay(way); setWayName(way); - StreetTraversalPermission permissions = OsmFilter.getPermissionsForWay( - way, - wayData.getPermission(), - params.banDiscouragedWalking(), - params.banDiscouragedBiking(), - issueStore - ); - if (!OsmFilter.isWayRoutable(way) || permissions.allowsNothing()) { + + var permissions = wayData.getPermission(); + + if (!way.isRoutable() || permissions.allowsNothing()) { continue; } @@ -468,7 +464,7 @@ private StreetEdgePair getEdgesForStreet( StreetEdge backStreet = null; double length = getGeometryLengthMeters(geometry); - var permissionPair = OsmFilter.getPermissions(permissions, way); + var permissionPair = way.splitPermissions(permissions); var permissionsFront = permissionPair.main(); var permissionsBack = permissionPair.back(); @@ -528,18 +524,15 @@ private StreetEdge getEdgeForStreet( .withPermission(permissions) .withBack(back) .withCarSpeed(carSpeed) - .withLink(OsmFilter.isLink(way)) + .withLink(way.isLink()) .withRoundabout(way.isRoundabout()) .withSlopeOverride(way.getOsmProvider().getWayPropertySet().getSlopeOverride(way)) - .withStairs(way.isSteps()); + .withStairs(way.isSteps()) + .withWheelchairAccessible(way.isWheelchairAccessible()); if (!way.hasTag("name") && !way.hasTag("ref")) { seb.withBogusName(true); } - /* TODO: This should probably generalized somehow? */ - if ((way.isTagFalse("wheelchair") || (way.isSteps() && !way.isTagTrue("wheelchair")))) { - seb.withWheelchairAccessible(false); - } // < 0.04: account for if (carSpeed < 0.04) { diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModuleBuilder.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModuleBuilder.java index e73db9eb318..a99752ca2b9 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModuleBuilder.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModuleBuilder.java @@ -23,8 +23,6 @@ public class OsmModuleBuilder { private boolean platformEntriesLinking = false; private boolean staticParkAndRide = false; private boolean staticBikeParkAndRide = false; - private boolean banDiscouragedWalking = false; - private boolean banDiscouragedBiking = false; private int maxAreaNodes; OsmModuleBuilder(Collection providers, Graph graph) { @@ -67,16 +65,6 @@ public OsmModuleBuilder withStaticBikeParkAndRide(boolean staticBikeParkAndRide) return this; } - public OsmModuleBuilder withBanDiscouragedWalking(boolean banDiscouragedWalking) { - this.banDiscouragedWalking = banDiscouragedWalking; - return this; - } - - public OsmModuleBuilder withBanDiscouragedBiking(boolean banDiscouragedBiking) { - this.banDiscouragedBiking = banDiscouragedBiking; - return this; - } - public OsmModuleBuilder withMaxAreaNodes(int maxAreaNodes) { this.maxAreaNodes = maxAreaNodes; return this; @@ -94,9 +82,7 @@ public OsmModule build() { areaVisibility, platformEntriesLinking, staticParkAndRide, - staticBikeParkAndRide, - banDiscouragedWalking, - banDiscouragedBiking + staticBikeParkAndRide ) ); } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/ParkingProcessor.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/ParkingProcessor.java index a78e735d587..940ffa68198 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/ParkingProcessor.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/ParkingProcessor.java @@ -135,7 +135,7 @@ private OHCalendar parseOpeningHours(OSMWithTags entity) { if (openingHoursTag != null) { final ZoneId zoneId = entity.getOsmProvider().getZoneId(); final var id = entity.getId(); - final var link = entity.getOpenStreetMapLink(); + final var link = entity.url(); try { return osmOpeningHoursParser.parseOpeningHours( openingHoursTag, @@ -293,7 +293,7 @@ private List createArtificialEntra ) { LOG.debug( "Creating an artificial entrance for {} as it's not linked to the street network", - entity.getOpenStreetMapLink() + entity.url() ); return List.of(builder -> builder diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/SafetyValueNormalizer.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/SafetyValueNormalizer.java index c8acb8990d6..a9603d59417 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/SafetyValueNormalizer.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/SafetyValueNormalizer.java @@ -90,12 +90,12 @@ void applyWayProperties( boolean walkNoThrough = tagMapperForWay.isWalkNoThroughTrafficExplicitlyDisallowed(way); if (street != null) { - double bicycleSafety = wayData.getBicycleSafetyFeatures().forward(); + double bicycleSafety = wayData.bicycleSafety().forward(); street.setBicycleSafetyFactor((float) bicycleSafety); if (bicycleSafety < bestBikeSafety) { bestBikeSafety = (float) bicycleSafety; } - double walkSafety = wayData.getWalkSafetyFeatures().forward(); + double walkSafety = wayData.walkSafety().forward(); street.setWalkSafetyFactor((float) walkSafety); if (walkSafety < bestWalkSafety) { bestWalkSafety = (float) walkSafety; @@ -111,12 +111,12 @@ void applyWayProperties( } if (backStreet != null) { - double bicycleSafety = wayData.getBicycleSafetyFeatures().back(); + double bicycleSafety = wayData.bicycleSafety().back(); if (bicycleSafety < bestBikeSafety) { bestBikeSafety = (float) bicycleSafety; } backStreet.setBicycleSafetyFactor((float) bicycleSafety); - double walkSafety = wayData.getWalkSafetyFeatures().back(); + double walkSafety = wayData.walkSafety().back(); if (walkSafety < bestWalkSafety) { bestWalkSafety = (float) walkSafety; } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/UnconnectedArea.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/UnconnectedArea.java index 07999b0c72d..5dd5f851ac2 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/UnconnectedArea.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/UnconnectedArea.java @@ -16,7 +16,7 @@ public String getMessage() { @Override public String getHTMLMessage() { - return String.format(HTMLFMT, areaGroup.getSomeOSMObject().getOpenStreetMapLink(), idList()); + return String.format(HTMLFMT, areaGroup.getSomeOSMObject().url(), idList()); } @Override diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java index 405743ccf71..14489777dd4 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/VertexGenerator.java @@ -91,9 +91,7 @@ IntersectionVertex getVertexForOsmNode(OSMNode node, OSMWithTags way) { if (node.isBarrier()) { BarrierVertex bv = vertexFactory.barrier(nid, coordinate); - bv.setBarrierPermissions( - OsmFilter.getPermissionsForEntity(node, BarrierVertex.defaultBarrierPermissions) - ); + bv.setBarrierPermissions(node.overridePermissions(BarrierVertex.defaultBarrierPermissions)); iv = bv; } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilder.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilder.java index a0a3d33de44..af17593d36a 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilder.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilder.java @@ -139,7 +139,6 @@ public void buildWithoutVisibility(AreaGroup group) { // the points corresponding to concave or hole vertices // or those linked to ways HashSet alreadyAddedEdges = new HashSet<>(); - // we also want to fill in the edges of this area anyway, because we can, // and to avoid the numerical problems that they tend to cause for (Area area : group.areas) { @@ -245,7 +244,7 @@ public void buildWithVisibility(AreaGroup group) { // variable to indicate if some additional entrance points have been added to area boolean linkPointsAdded = !entrances.isEmpty(); // Add unconnected entries to area if platformEntriesLinking parameter is true - if (platformEntriesLinking && "platform".equals(area.parent.getTag("public_transport"))) { + if (platformEntriesLinking && area.parent.isPlatform()) { List endpointsWithin = platformLinkingEndpoints .stream() .filter(t -> @@ -491,7 +490,7 @@ private Set createSegments( intersects.add(area); } } - if (intersects.size() == 0) { + if (intersects.isEmpty()) { // apparently our intersection here was bogus return Set.of(); } @@ -500,8 +499,7 @@ private Set createSegments( Area area = intersects.get(0); OSMWithTags areaEntity = area.parent; - StreetTraversalPermission areaPermissions = OsmFilter.getPermissionsForEntity( - areaEntity, + StreetTraversalPermission areaPermissions = areaEntity.overridePermissions( StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE ); @@ -539,11 +537,9 @@ private Set createSegments( streetEdgeBuilder.withBogusName(true); } - if (areaEntity.isTagFalse("wheelchair")) { - streetEdgeBuilder.withWheelchairAccessible(false); - } + streetEdgeBuilder.withWheelchairAccessible(areaEntity.isWheelchairAccessible()); - streetEdgeBuilder.withLink(OsmFilter.isLink(areaEntity)); + streetEdgeBuilder.withLink(areaEntity.isLink()); label = "way (area) " + @@ -569,11 +565,9 @@ private Set createSegments( backStreetEdgeBuilder.withBogusName(true); } - if (areaEntity.isTagFalse("wheelchair")) { - backStreetEdgeBuilder.withWheelchairAccessible(false); - } + backStreetEdgeBuilder.withWheelchairAccessible(areaEntity.isWheelchairAccessible()); - backStreetEdgeBuilder.withLink(OsmFilter.isLink(areaEntity)); + backStreetEdgeBuilder.withLink(areaEntity.isLink()); if (!wayPropertiesCache.containsKey(areaEntity)) { WayProperties wayData = areaEntity @@ -606,8 +600,7 @@ private Set createSegments( if (lineParts.getLength() > 0.000001) { Coordinate edgeCoordinate = null; // this is either a LineString or a MultiLineString (we hope) - if (lineParts instanceof MultiLineString) { - MultiLineString mls = (MultiLineString) lineParts; + if (lineParts instanceof MultiLineString mls) { boolean found = false; for (int i = 0; i < mls.getNumGeometries(); ++i) { LineString segment = (LineString) mls.getGeometryN(i); @@ -623,8 +616,8 @@ private Set createSegments( } } } - } else if (lineParts instanceof LineString) { - edgeCoordinate = ((LineString) lineParts).getEndPoint().getCoordinate(); + } else if (lineParts instanceof LineString lineString) { + edgeCoordinate = lineString.getEndPoint().getCoordinate(); } else { continue; } @@ -665,19 +658,15 @@ private void createNamedAreas(AreaEdgeList edgeList, Ring ring, Collection wayPropertiesCache.put(areaEntity, wayData); } - double bicycleSafety = wayPropertiesCache - .get(areaEntity) - .getBicycleSafetyFeatures() - .forward(); + double bicycleSafety = wayPropertiesCache.get(areaEntity).bicycleSafety().forward(); namedArea.setBicycleSafetyMultiplier(bicycleSafety); - double walkSafety = wayPropertiesCache.get(areaEntity).getWalkSafetyFeatures().forward(); + double walkSafety = wayPropertiesCache.get(areaEntity).walkSafety().forward(); namedArea.setWalkSafetyMultiplier(walkSafety); namedArea.setOriginalEdges(intersection); - StreetTraversalPermission permission = OsmFilter.getPermissionsForEntity( - areaEntity, + StreetTraversalPermission permission = areaEntity.overridePermissions( StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE ); namedArea.setPermission(permission); @@ -690,8 +679,7 @@ private boolean isPlatformLinkingEndpoint(OsmVertex osmVertex) { boolean isCandidate = false; Vertex start = null; for (Edge e : osmVertex.getIncoming()) { - if (e instanceof StreetEdge && !(e instanceof AreaEdge)) { - StreetEdge se = (StreetEdge) e; + if (e instanceof StreetEdge se && !(e instanceof AreaEdge)) { if (Arrays.asList(1, 2, 3).contains(se.getPermission().code)) { isCandidate = true; start = se.getFromVertex(); diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmProcessingParameters.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmProcessingParameters.java index 7f8b87c96df..52bf8d65314 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmProcessingParameters.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/parameters/OsmProcessingParameters.java @@ -13,10 +13,6 @@ * @param platformEntriesLinking Whether platform entries should be linked * @param staticParkAndRide Whether we should create car P+R stations from OSM data. * @param staticBikeParkAndRide Whether we should create bike P+R stations from OSM data. - * @param banDiscouragedWalking Whether ways tagged foot=discouraged should be marked as - * inaccessible. - * @param banDiscouragedBiking Whether ways tagged bicycle=discouraged should be marked as - * inaccessible. */ public record OsmProcessingParameters( Set boardingAreaRefTags, @@ -25,9 +21,7 @@ public record OsmProcessingParameters( boolean areaVisibility, boolean platformEntriesLinking, boolean staticParkAndRide, - boolean staticBikeParkAndRide, - boolean banDiscouragedWalking, - boolean banDiscouragedBiking + boolean staticBikeParkAndRide ) { public OsmProcessingParameters { boardingAreaRefTags = Set.copyOf(Objects.requireNonNull(boardingAreaRefTags)); diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/StationMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/StationMapper.java index 546894f5728..1308994c414 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/StationMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/StationMapper.java @@ -5,6 +5,7 @@ import java.time.ZoneId; import java.util.HashMap; import java.util.Map; +import org.onebusaway.gtfs.model.Stop; import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.site.StationBuilder; import org.opentripplanner.transit.model.site.StopTransferPriority; @@ -21,7 +22,7 @@ class StationMapper { /** @see StationMapper (this class JavaDoc) for way we need this. */ - private final Map mappedStops = new HashMap<>(); + private final Map mappedStops = new HashMap<>(); private final TranslationHelper translationHelper; private final StopTransferPriority stationTransferPreference; @@ -35,17 +36,18 @@ class StationMapper { } /** Map from GTFS to OTP model, {@code null} safe. */ - Station map(org.onebusaway.gtfs.model.Stop orginal) { + Station map(Stop orginal) { return orginal == null ? null : mappedStops.computeIfAbsent(orginal, this::doMap); } - private Station doMap(org.onebusaway.gtfs.model.Stop rhs) { - if (rhs.getLocationType() != org.onebusaway.gtfs.model.Stop.LOCATION_TYPE_STATION) { + private Station doMap(Stop rhs) { + if (rhs.getLocationType() != Stop.LOCATION_TYPE_STATION) { throw new IllegalArgumentException( - "Expected type " + - org.onebusaway.gtfs.model.Stop.LOCATION_TYPE_STATION + - ", but got " + - rhs.getLocationType() + "Expected location_type %s, but got %s for stops.txt entry %s".formatted( + Stop.LOCATION_TYPE_STATION, + rhs.getLocationType(), + rhs + ) ); } StationBuilder builder = Station @@ -54,30 +56,15 @@ private Station doMap(org.onebusaway.gtfs.model.Stop rhs) { .withCode(rhs.getCode()); builder.withName( - translationHelper.getTranslation( - org.onebusaway.gtfs.model.Stop.class, - "name", - rhs.getId().getId(), - rhs.getName() - ) + translationHelper.getTranslation(Stop.class, "name", rhs.getId().getId(), rhs.getName()) ); builder.withDescription( - translationHelper.getTranslation( - org.onebusaway.gtfs.model.Stop.class, - "desc", - rhs.getId().getId(), - rhs.getDesc() - ) + translationHelper.getTranslation(Stop.class, "desc", rhs.getId().getId(), rhs.getDesc()) ); builder.withUrl( - translationHelper.getTranslation( - org.onebusaway.gtfs.model.Stop.class, - "url", - rhs.getId().getId(), - rhs.getUrl() - ) + translationHelper.getTranslation(Stop.class, "url", rhs.getId().getId(), rhs.getUrl()) ); builder.withPriority(stationTransferPreference); diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/StopMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/StopMapper.java index 5fa2a6f3d1c..a779a3631df 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/StopMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/StopMapper.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.Map; import java.util.function.Function; +import org.onebusaway.gtfs.model.Stop; import org.opentripplanner.framework.collection.MapUtils; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.site.FareZone; @@ -37,12 +38,11 @@ RegularStop map(org.onebusaway.gtfs.model.Stop orginal) { private RegularStop doMap(org.onebusaway.gtfs.model.Stop gtfsStop) { if (gtfsStop.getLocationType() != org.onebusaway.gtfs.model.Stop.LOCATION_TYPE_STOP) { throw new IllegalArgumentException( - "Expected type " + - org.onebusaway.gtfs.model.Stop.LOCATION_TYPE_STOP + - ", but got " + - gtfsStop.getLocationType() + - " from stop " + - gtfsStop + "Expected location_type %s, but got %s for stops.txt entry %s".formatted( + Stop.LOCATION_TYPE_STOP, + gtfsStop.getLocationType(), + gtfsStop + ) ); } diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/TransferMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/TransferMapper.java index 3ebe4b16233..7eabbbb3c78 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/TransferMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/TransferMapper.java @@ -297,11 +297,4 @@ private int alightStopPosition(Trip trip, RegularStop stop, Station station) { } return -1; } - - private boolean sameBlockId(Trip a, Trip b) { - if (a == null || b == null) { - return false; - } - return a.getGtfsBlockId() != null && a.getGtfsBlockId().equals(b.getGtfsBlockId()); - } } diff --git a/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorSerializer.java b/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorSerializer.java index dbbdc56af64..b3a25182678 100644 --- a/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorSerializer.java +++ b/src/main/java/org/opentripplanner/model/plan/pagecursor/PageCursorSerializer.java @@ -108,14 +108,6 @@ private static Duration readDuration(ObjectInputStream in) throws IOException { return Duration.ofSeconds(in.readInt()); } - private static void writeBoolean(boolean value, ObjectOutputStream out) throws IOException { - out.writeBoolean(value); - } - - private static boolean readBoolean(ObjectInputStream in) throws IOException { - return in.readBoolean(); - } - private static > void writeEnum(T value, ObjectOutputStream out) throws IOException { out.writeUTF(value.name()); diff --git a/src/main/java/org/opentripplanner/netex/mapping/NoticeAssignmentMapper.java b/src/main/java/org/opentripplanner/netex/mapping/NoticeAssignmentMapper.java index 02cced2b3f3..fc8cb3804f6 100644 --- a/src/main/java/org/opentripplanner/netex/mapping/NoticeAssignmentMapper.java +++ b/src/main/java/org/opentripplanner/netex/mapping/NoticeAssignmentMapper.java @@ -116,10 +116,6 @@ Multimap map(NoticeAssignment noticeAssignment) { return noticiesByEntity; } - private StopTimeKey lookupStopTimeKey(String timeTablePassingTimeId) { - return stopTimesByNetexId.get(timeTablePassingTimeId).getId(); - } - @Nullable private Notice getOrMapNotice(NoticeAssignment assignment) { org.rutebanken.netex.model.Notice notice = assignment.getNotice() != null diff --git a/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java b/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java index a4dd4554342..53a47298dae 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java +++ b/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java @@ -15,18 +15,6 @@ public Coordinate getCoordinate() { return new Coordinate(this.lon, this.lat); } - /** - * Returns the capacity of this node if defined, or 0. - */ - public int getCapacity() throws NumberFormatException { - String capacity = getTag("capacity"); - if (capacity == null) { - return 0; - } - - return Integer.parseInt(getTag("capacity")); - } - /** * Is this a multi-level node that should be decomposed to multiple coincident nodes? Currently * returns true only for elevators. @@ -35,7 +23,7 @@ public int getCapacity() throws NumberFormatException { * @author mattwigway */ public boolean isMultiLevel() { - return hasTag("highway") && "elevator".equals(getTag("highway")); + return isElevator(); } public boolean hasHighwayTrafficLight() { @@ -72,7 +60,7 @@ public boolean isBarrier() { } @Override - public String getOpenStreetMapLink() { + public String url() { return String.format("https://www.openstreetmap.org/node/%d", getId()); } } diff --git a/src/main/java/org/opentripplanner/openstreetmap/model/OSMRelation.java b/src/main/java/org/opentripplanner/openstreetmap/model/OSMRelation.java index e0c0e8750c8..e181cf97713 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/model/OSMRelation.java +++ b/src/main/java/org/opentripplanner/openstreetmap/model/OSMRelation.java @@ -20,7 +20,43 @@ public String toString() { } @Override - public String getOpenStreetMapLink() { - return String.format("http://www.openstreetmap.org/relation/%d", getId()); + public String url() { + return String.format("https://www.openstreetmap.org/relation/%d", getId()); + } + + public boolean isBicycleRoute() { + return isRoute() && isTag("route", "bicycle"); + } + + public boolean isRoute() { + return isType("route"); + } + + public boolean isRoadRoute() { + return isRoute() && isTag("route", "road"); + } + + public boolean isLevelMap() { + return isType("level_map"); + } + + public boolean isRestriction() { + return isType("restriction"); + } + + public boolean isPublicTransport() { + return isType("public_transport"); + } + + public boolean isMultiPolygon() { + return isType("multipolygon"); + } + + public boolean isStopArea() { + return isPublicTransport() && isTag("public_transport", "stop_area"); + } + + private boolean isType(String type) { + return isTag("type", type); } } diff --git a/src/main/java/org/opentripplanner/openstreetmap/model/OSMRelationMember.java b/src/main/java/org/opentripplanner/openstreetmap/model/OSMRelationMember.java index 8174a458d9b..e2ad2ff9691 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/model/OSMRelationMember.java +++ b/src/main/java/org/opentripplanner/openstreetmap/model/OSMRelationMember.java @@ -1,5 +1,7 @@ package org.opentripplanner.openstreetmap.model; +import static org.opentripplanner.openstreetmap.model.OSMMemberType.WAY; + public class OSMRelationMember { private OSMMemberType type; @@ -16,10 +18,6 @@ public void setType(OSMMemberType type) { this.type = type; } - public boolean hasType(OSMMemberType type) { - return this.type == type; - } - public long getRef() { return ref; } @@ -36,6 +34,22 @@ public void setRole(String role) { this.role = role; } + public boolean hasRoleOuter() { + return "outer".equals(role); + } + + public boolean hasRoleInner() { + return "inner".equals(role); + } + + public boolean hasRolePlatform() { + return "platform".equals(role); + } + + public boolean hasTypeWay() { + return type == WAY; + } + @Override public String toString() { return "osm rel " + type + ":" + role + ":" + ref; diff --git a/src/main/java/org/opentripplanner/openstreetmap/model/OSMWay.java b/src/main/java/org/opentripplanner/openstreetmap/model/OSMWay.java index 69c9c84c2b8..1ce6c698f22 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/model/OSMWay.java +++ b/src/main/java/org/opentripplanner/openstreetmap/model/OSMWay.java @@ -3,9 +3,17 @@ import gnu.trove.list.TLongList; import gnu.trove.list.array.TLongArrayList; import java.util.Set; +import org.opentripplanner.graph_builder.module.osm.StreetTraversalPermissionPair; +import org.opentripplanner.street.model.StreetTraversalPermission; public class OSMWay extends OSMWithTags { + private static final Set ESCALATOR_CONVEYING_TAGS = Set.of( + "yes", + "forward", + "backward", + "reversible" + ); private final TLongList nodes = new TLongArrayList(); public void addNodeRef(long nodeRef) { @@ -45,19 +53,19 @@ public boolean isBoardingArea() { return isBoardingLocation() && isClosed(); } - /** - * Returns true if bicycle dismounts are forced. - */ - public boolean isBicycleDismountForced() { - String bicycle = getTag("bicycle"); - return isTag("cycleway", "dismount") || "dismount".equals(bicycle); - } - /** * Returns true if these are steps. */ public boolean isSteps() { - return "steps".equals(getTag("highway")); + return isTag("highway", "steps"); + } + + public boolean isWheelchairAccessible() { + if (isSteps()) { + return isTagTrue("wheelchair"); + } else { + return super.isWheelchairAccessible(); + } } /** @@ -128,11 +136,7 @@ public boolean isOpposableCycleway() { } public boolean isEscalator() { - return ( - "steps".equals(this.getTag("highway")) && - this.getTag("conveying") != null && - Set.of("yes", "forward", "backward", "reversible").contains(this.getTag("conveying")) - ); + return (isTag("highway", "steps") && isOneOfTags("conveying", ESCALATOR_CONVEYING_TAGS)); } public boolean isForwardEscalator() { @@ -143,8 +147,63 @@ public boolean isBackwardEscalator() { return isEscalator() && "backward".equals(this.getTag("conveying")); } + public boolean isArea() { + return isTag("area", "yes"); + } + + /** + * Given a set of {@code permissions} check if it can really be applied to both directions + * of the way and return the permissions for both cases. + */ + public StreetTraversalPermissionPair splitPermissions(StreetTraversalPermission permissions) { + StreetTraversalPermission permissionsFront = permissions; + StreetTraversalPermission permissionsBack = permissions; + + // Check driving direction restrictions. + if (isOneWayForwardDriving() || isRoundabout()) { + permissionsBack = permissionsBack.remove(StreetTraversalPermission.BICYCLE_AND_CAR); + } + if (isOneWayReverseDriving()) { + permissionsFront = permissionsFront.remove(StreetTraversalPermission.BICYCLE_AND_CAR); + } + + // Check bike direction restrictions. + if (isOneWayForwardBicycle()) { + permissionsBack = permissionsBack.remove(StreetTraversalPermission.BICYCLE); + } + if (isOneWayReverseBicycle()) { + permissionsFront = permissionsFront.remove(StreetTraversalPermission.BICYCLE); + } + + // TODO(flamholz): figure out what this is for. + String oneWayBicycle = getTag("oneway:bicycle"); + if (isFalse(oneWayBicycle) || isTagTrue("bicycle:backwards")) { + if (permissions.allows(StreetTraversalPermission.BICYCLE)) { + permissionsFront = permissionsFront.add(StreetTraversalPermission.BICYCLE); + permissionsBack = permissionsBack.add(StreetTraversalPermission.BICYCLE); + } + } + + //This needs to be after adding permissions for oneway:bicycle=no + //removes bicycle permission when bicycles need to use sidepath + //TAG: bicycle:forward=use_sidepath + if (isForwardDirectionSidepath()) { + permissionsFront = permissionsFront.remove(StreetTraversalPermission.BICYCLE); + } + + //TAG bicycle:backward=use_sidepath + if (isReverseDirectionSidepath()) { + permissionsBack = permissionsBack.remove(StreetTraversalPermission.BICYCLE); + } + + if (isOpposableCycleway()) { + permissionsBack = permissionsBack.add(StreetTraversalPermission.BICYCLE); + } + return new StreetTraversalPermissionPair(permissionsFront, permissionsBack); + } + @Override - public String getOpenStreetMapLink() { + public String url() { return String.format("https://www.openstreetmap.org/way/%d", getId()); } } diff --git a/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java b/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java index 2c2f3167ab1..ed8819303d3 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java +++ b/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java @@ -2,29 +2,50 @@ import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.OptionalInt; import java.util.Set; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.framework.i18n.TranslatedString; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.graph_builder.module.osm.OsmModule; import org.opentripplanner.openstreetmap.OsmProvider; +import org.opentripplanner.street.model.StreetTraversalPermission; import org.opentripplanner.transit.model.basic.Accessibility; /** * A base class for OSM entities containing common methods. */ - public class OSMWithTags { + /** + * highway=* values that we don't want to even consider when building the graph. + */ + public static final Set NON_ROUTABLE_HIGHWAYS = Set.of( + "proposed", + "planned", + "construction", + "razed", + "raceway", + "abandoned", + "historic", + "no", + "emergency_bay", + "rest_area", + "services", + "bus_guideway", + "escape" + ); + /* To save memory this is only created when an entity actually has tags. */ private Map tags; @@ -70,19 +91,24 @@ public void addTag(OSMTag tag) { /** * Adds a tag. */ - public void addTag(String key, String value) { - if (key == null || value == null) return; + public OSMWithTags addTag(String key, String value) { + if (key == null || value == null) { + return this; + } - if (tags == null) tags = new HashMap<>(); + if (tags == null) { + tags = new HashMap<>(); + } tags.put(key.toLowerCase(), value); + return this; } /** * The tags of an entity. */ public Map getTags() { - return tags; + return Objects.requireNonNullElse(tags, Map.of()); } /** @@ -130,7 +156,14 @@ public boolean isTagTrue(String tag) { return isTrue(getTag(tag)); } - public boolean doesTagAllowAccess(String tag) { + /** + * Returns true if bicycle dismounts are forced. + */ + public boolean isBicycleDismountForced() { + return isTag("bicycle", "dismount"); + } + + protected boolean doesTagAllowAccess(String tag) { if (tags == null) { return false; } @@ -148,6 +181,7 @@ public boolean doesTagAllowAccess(String tag) { } /** @return a tag's value, converted to lower case. */ + @Nullable public String getTag(String tag) { tag = tag.toLowerCase(); if (tags != null && tags.containsKey(tag)) { @@ -157,10 +191,12 @@ public String getTag(String tag) { } /** - * Returns true if both key and value matches. + * + * @return A tags value converted to lower case. An empty Optional if tags is not present. */ - public boolean matchesKeyValue(String key, String value) { - return hasTag(key) && getTag(key).equals(value); + @Nonnull + public Optional getTagOpt(String network) { + return Optional.ofNullable(getTag(network)); } /** @@ -182,7 +218,7 @@ public OptionalInt getTagAsInt(String tag, Consumer errorHandler) { /** * Checks is a tag contains the specified value. */ - public Boolean isTag(String tag, String value) { + public boolean isTag(String tag, String value) { tag = tag.toLowerCase(); if (tags != null && tags.containsKey(tag) && value != null) { return value.equals(tags.get(tag)); @@ -191,6 +227,13 @@ public Boolean isTag(String tag, String value) { return false; } + /** + * Takes a tag key and checks if the value is any of those in {@code oneOfTags}. + */ + public boolean isOneOfTags(String key, Set oneOfTags) { + return oneOfTags.stream().anyMatch(value -> isTag(key, value)); + } + /** * Returns a name-like value for an entity (if one exists). The otp: namespaced tags are created * by {@link OsmModule} @@ -405,18 +448,30 @@ public boolean isParkAndRide() { */ public boolean isBoardingLocation() { return ( - "bus_stop".equals(getTag("highway")) || - "tram_stop".equals(getTag("railway")) || - "station".equals(getTag("railway")) || - "halt".equals(getTag("railway")) || - "bus_station".equals(getTag("amenity")) || - "ferry_terminal".equals(getTag("amenity")) || + isTag("highway", "bus_stop") || + isTag("railway", "tram_stop") || + isTag("railway", "station") || + isTag("railway", "halt") || + isTag("amenity", "bus_station") || + isTag("amenity", "ferry_terminal") || isPlatform() ); } + /** + * Determines if an entity is a platform. + *

+ * However, they are filtered out if they are tagged usage=tourism. This prevents miniature tourist + * railways like the one in Portland's Zoo (https://www.openstreetmap.org/way/119108622) + * from being linked to transit stops that are underneath it. + **/ public boolean isPlatform() { - return "platform".equals(getTag("public_transport")) || "platform".equals(getTag("railway")); + var isPlatform = isTag("public_transport", "platform") || isRailwayPlatform(); + return isPlatform && !isTag("usage", "tourism"); + } + + public boolean isRailwayPlatform() { + return isTag("railway", "platform"); } /** @@ -448,7 +503,7 @@ public void setCreativeName(I18NString creativeName) { this.creativeName = creativeName; } - public String getOpenStreetMapLink() { + public String url() { return null; } @@ -457,6 +512,7 @@ public String getOpenStreetMapLink() { *

* Values are split by semicolons. */ + @Nonnull public Set getMultiTagValues(Set refTags) { return refTags .stream() @@ -465,7 +521,7 @@ public Set getMultiTagValues(Set refTags) { .flatMap(v -> Arrays.stream(v.split(";"))) .map(String::strip) .filter(v -> !v.isBlank()) - .collect(Collectors.toSet()); + .collect(Collectors.toUnmodifiableSet()); } public OsmProvider getOsmProvider() { @@ -476,6 +532,50 @@ public void setOsmProvider(OsmProvider provider) { this.osmProvider = provider; } + /** + * Determines whether this OSM way is considered routable. The majority of routable ways are those + * with a highway= tag (which includes everything from motorways to hiking trails). Anything with + * a public_transport=platform or railway=platform tag is also considered routable even if it + * doesn't have a highway tag. + */ + public boolean isRoutable() { + if (isOneOfTags("highway", NON_ROUTABLE_HIGHWAYS)) { + return false; + } else if (hasTag("highway") || isPlatform()) { + if (isGeneralAccessDenied()) { + // There are exceptions. + return ( + isMotorcarExplicitlyAllowed() || + isBicycleExplicitlyAllowed() || + isPedestrianExplicitlyAllowed() || + isMotorVehicleExplicitlyAllowed() || + isVehicleExplicitlyAllowed() + ); + } + return true; + } + + return false; + } + + public boolean isLink() { + String highway = getTag("highway"); + return highway != null && highway.endsWith(("_link")); + } + + public boolean isElevator() { + return isTag("highway", "elevator"); + } + + /** + * @return true if there is no explicit tag that makes this unsuitable for wheelchair use. + * In other words: we assume that something is wheelchair-accessible in the absence + * of other information. + */ + public boolean isWheelchairAccessible() { + return !isTagFalse("wheelchair"); + } + /** * Returns true if this tag is explicitly access to this entity. */ @@ -497,6 +597,89 @@ public Set getLevels() { return levels; } + /** + * Given an assumed traversal permissions, check if there are explicit additional tags, like bicycle=no + * or bicycle=yes that override them. + */ + public StreetTraversalPermission overridePermissions(StreetTraversalPermission def) { + StreetTraversalPermission permission; + + /* + * Only a few tags are examined here, because we only care about modes supported by OTP + * (wheelchairs are not of concern here) + * + * Only a few values are checked for, all other values are presumed to be permissive (=> + * This may not be perfect, but is closer to reality, since most people don't follow the + * rules perfectly ;-) + */ + if (isGeneralAccessDenied()) { + // this can actually be overridden + permission = StreetTraversalPermission.NONE; + } else { + permission = def; + } + + if (isVehicleExplicitlyDenied()) { + permission = permission.remove(StreetTraversalPermission.BICYCLE_AND_CAR); + } else if (isVehicleExplicitlyAllowed()) { + permission = permission.add(StreetTraversalPermission.BICYCLE_AND_CAR); + } + + if (isMotorcarExplicitlyDenied() || isMotorVehicleExplicitlyDenied()) { + permission = permission.remove(StreetTraversalPermission.CAR); + } else if (isMotorcarExplicitlyAllowed() || isMotorVehicleExplicitlyAllowed()) { + permission = permission.add(StreetTraversalPermission.CAR); + } + + if (isBicycleExplicitlyDenied()) { + permission = permission.remove(StreetTraversalPermission.BICYCLE); + } else if (isBicycleExplicitlyAllowed()) { + permission = permission.add(StreetTraversalPermission.BICYCLE); + } + + if (isPedestrianExplicitlyDenied()) { + permission = permission.remove(StreetTraversalPermission.PEDESTRIAN); + } else if (isPedestrianExplicitlyAllowed()) { + permission = permission.add(StreetTraversalPermission.PEDESTRIAN); + } + + if (isUnderConstruction()) { + permission = StreetTraversalPermission.NONE; + } + + if (permission == null) { + return def; + } + + /* + * pedestrian rules: everything is two-way (assuming pedestrians are allowed at all) bicycle + * rules: default: permissions; + * + * cycleway=dismount means walk your bike -- the engine will automatically try walking bikes + * any time it is forbidden to ride them, so the only thing to do here is to remove bike + * permissions + * + * oneway=... sets permissions for cars and bikes oneway:bicycle overwrites these + * permissions for bikes only + * + * now, cycleway=opposite_lane, opposite, opposite_track can allow once oneway has been set + * by oneway:bicycle, but should give a warning if it conflicts with oneway:bicycle + * + * bicycle:backward=yes works like oneway:bicycle=no bicycle:backwards=no works like + * oneway:bicycle=yes + */ + + // Compute bike permissions, check consistency. + if (isBicycleExplicitlyAllowed()) { + permission = permission.add(StreetTraversalPermission.BICYCLE); + } + + if (isBicycleDismountForced()) { + permission = permission.remove(StreetTraversalPermission.BICYCLE); + } + return permission; + } + @Override public String toString() { return ToStringBuilder.of(this.getClass()).addObj("tags", tags).toString(); diff --git a/src/main/java/org/opentripplanner/openstreetmap/tagmapping/DefaultMapper.java b/src/main/java/org/opentripplanner/openstreetmap/tagmapping/DefaultMapper.java index 9185f1ccf3b..01cf213b03b 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/tagmapping/DefaultMapper.java +++ b/src/main/java/org/opentripplanner/openstreetmap/tagmapping/DefaultMapper.java @@ -1,6 +1,7 @@ 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.WayPropertiesBuilder.withModes; import static org.opentripplanner.street.model.StreetTraversalPermission.ALL; import static org.opentripplanner.street.model.StreetTraversalPermission.BICYCLE_AND_CAR; @@ -618,6 +619,9 @@ public void populateProperties(WayPropertySet props) { 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)); + populateNotesAndNames(props); // slope overrides diff --git a/src/main/java/org/opentripplanner/openstreetmap/tagmapping/FinlandMapper.java b/src/main/java/org/opentripplanner/openstreetmap/tagmapping/FinlandMapper.java index fb30309eb09..253bb385aa3 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/tagmapping/FinlandMapper.java +++ b/src/main/java/org/opentripplanner/openstreetmap/tagmapping/FinlandMapper.java @@ -83,6 +83,11 @@ else if (speedLimit <= 16.65f) { props.setProperties("highway=service;access=private", withModes(NONE)); props.setProperties("highway=trail", withModes(NONE)); + // Remove ice/winter roads + props.setProperties("highway=*;seasonal=winter", withModes(NONE)); + props.setProperties("highway=*;ice_road=yes", withModes(NONE)); + props.setProperties("highway=*;winter_road=yes", withModes(NONE)); + // No biking on designated footways/sidewalks props.setProperties("highway=footway", withModes(PEDESTRIAN)); props.setProperties("footway=sidewalk;highway=footway", withModes(PEDESTRIAN)); diff --git a/src/main/java/org/opentripplanner/openstreetmap/tagmapping/NorwayMapper.java b/src/main/java/org/opentripplanner/openstreetmap/tagmapping/NorwayMapper.java index 1b9ec441e39..d2cd2f3cf96 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/tagmapping/NorwayMapper.java +++ b/src/main/java/org/opentripplanner/openstreetmap/tagmapping/NorwayMapper.java @@ -4,7 +4,6 @@ import static org.opentripplanner.openstreetmap.wayproperty.MixinPropertiesBuilder.ofWalkSafety; import static org.opentripplanner.openstreetmap.wayproperty.WayPropertiesBuilder.withModes; import static org.opentripplanner.street.model.StreetTraversalPermission.ALL; -import static org.opentripplanner.street.model.StreetTraversalPermission.BICYCLE_AND_CAR; import static org.opentripplanner.street.model.StreetTraversalPermission.CAR; import static org.opentripplanner.street.model.StreetTraversalPermission.NONE; import static org.opentripplanner.street.model.StreetTraversalPermission.PEDESTRIAN; diff --git a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/MixinPropertiesBuilder.java b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/MixinPropertiesBuilder.java index dcc007caef7..dd16a28e3ec 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/MixinPropertiesBuilder.java +++ b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/MixinPropertiesBuilder.java @@ -8,8 +8,8 @@ */ public class MixinPropertiesBuilder { - private SafetyFeatures bicycleSafety = new SafetyFeatures(1, 1); - private SafetyFeatures walkSafety = new SafetyFeatures(1, 1); + private SafetyFeatures bicycleSafety = SafetyFeatures.DEFAULT; + private SafetyFeatures walkSafety = SafetyFeatures.DEFAULT; public static MixinPropertiesBuilder ofWalkSafety(double safety) { return new MixinPropertiesBuilder().walkSafety(safety); diff --git a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/SafetyFeatures.java b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/SafetyFeatures.java index 9dd78266618..fc166a69927 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/SafetyFeatures.java +++ b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/SafetyFeatures.java @@ -3,4 +3,6 @@ /** * Record that holds forward and back safety factors for cycling or walking. */ -public record SafetyFeatures(double forward, double back) {} +public record SafetyFeatures(double forward, double back) { + public static final SafetyFeatures DEFAULT = new SafetyFeatures(1, 1); +} diff --git a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/WayProperties.java b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/WayProperties.java index 8ceb063dc5a..4facfd041c8 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/WayProperties.java +++ b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/WayProperties.java @@ -1,42 +1,76 @@ package org.opentripplanner.openstreetmap.wayproperty; import java.util.Objects; +import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.opentripplanner.street.model.StreetTraversalPermission; /** * Parameters applied to OSM ways, usually based on their tags: - Which modes can traverse it - * Dangerousness on a bicycle in both directions (OSM ways can be bidirectional). * - * @author novalis */ public class WayProperties { + @Nonnull private final StreetTraversalPermission permission; + + @Nullable private final SafetyFeatures bicycleSafetyFeatures; + + @Nullable private final SafetyFeatures walkSafetyFeatures; - public WayProperties(WayPropertiesBuilder wayPropertiesBuilder) { - permission = wayPropertiesBuilder.getPermission(); - bicycleSafetyFeatures = wayPropertiesBuilder.getBicycleSafetyFeatures(); - walkSafetyFeatures = wayPropertiesBuilder.getWalkSafetyFeatures(); + WayProperties(WayPropertiesBuilder wayPropertiesBuilder) { + permission = Objects.requireNonNull(wayPropertiesBuilder.getPermission()); + bicycleSafetyFeatures = wayPropertiesBuilder.bicycleSafety(); + walkSafetyFeatures = wayPropertiesBuilder.walkSafety(); } - public SafetyFeatures getBicycleSafetyFeatures() { - return bicycleSafetyFeatures; + /** + * The value for the bicycle safety. If none has been set a default value of 1 is returned. + */ + @Nonnull + public SafetyFeatures bicycleSafety() { + return Objects.requireNonNullElse(bicycleSafetyFeatures, SafetyFeatures.DEFAULT); } - public SafetyFeatures getWalkSafetyFeatures() { - return walkSafetyFeatures; + /** + * The value for the walk safety. If none has been set a default value of 1 is returned. + */ + @Nonnull + public SafetyFeatures walkSafety() { + return Objects.requireNonNullElse(walkSafetyFeatures, SafetyFeatures.DEFAULT); } + @Nonnull public StreetTraversalPermission getPermission() { return permission; } + /** + * An optional value for the walk safety. If none has been set an empty Optional is returned. + */ + @Nonnull + protected Optional walkSafetyOpt() { + return Optional.ofNullable(walkSafetyFeatures); + } + + /** + * An optional value for the bicycle safety. If none has been set an empty Optional is returned. + */ + @Nonnull + protected Optional bicycleSafetyOpt() { + return Optional.ofNullable(bicycleSafetyFeatures); + } + + @Override public int hashCode() { return Objects.hash(bicycleSafetyFeatures, walkSafetyFeatures, permission); } + @Override public boolean equals(Object o) { if (o instanceof WayProperties other) { return ( diff --git a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/WayPropertiesBuilder.java b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/WayPropertiesBuilder.java index 30403250ad0..b91d1040a7d 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/WayPropertiesBuilder.java +++ b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/WayPropertiesBuilder.java @@ -1,5 +1,6 @@ package org.opentripplanner.openstreetmap.wayproperty; +import javax.annotation.Nullable; import org.opentripplanner.street.model.StreetTraversalPermission; /** @@ -8,7 +9,7 @@ */ public class WayPropertiesBuilder { - private final StreetTraversalPermission permission; + private StreetTraversalPermission permission; private SafetyFeatures bicycleSafetyFeatures = null; private SafetyFeatures walkSafetyFeatures = null; @@ -16,10 +17,10 @@ public WayPropertiesBuilder(StreetTraversalPermission permission) { this.permission = permission; } - public WayPropertiesBuilder(WayProperties defaultProperties) { + WayPropertiesBuilder(WayProperties defaultProperties) { this.permission = defaultProperties.getPermission(); - this.bicycleSafetyFeatures = defaultProperties.getBicycleSafetyFeatures(); - this.walkSafetyFeatures = defaultProperties.getWalkSafetyFeatures(); + this.bicycleSafetyFeatures = defaultProperties.bicycleSafety(); + this.walkSafetyFeatures = defaultProperties.walkSafety(); } /** @@ -62,15 +63,22 @@ public WayPropertiesBuilder walkSafety(double walkSafety, double walkSafetyBack) return this; } + public WayPropertiesBuilder withPermission(StreetTraversalPermission permission) { + this.permission = permission; + return this; + } + public StreetTraversalPermission getPermission() { return permission; } - public SafetyFeatures getBicycleSafetyFeatures() { + @Nullable + protected SafetyFeatures bicycleSafety() { return bicycleSafetyFeatures; } - public SafetyFeatures getWalkSafetyFeatures() { + @Nullable + protected SafetyFeatures walkSafety() { return walkSafetyFeatures; } diff --git a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/WayPropertySet.java b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/WayPropertySet.java index e7a1bd445ee..06d5f895777 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/WayPropertySet.java +++ b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/WayPropertySet.java @@ -26,7 +26,6 @@ /** * Information given to the GraphBuilder about how to assign permissions, safety values, names, etc. * to edges based on OSM tags. - * TODO rename so that the connection with OSM tags is obvious *

* WayPropertyPickers, CreativeNamePickers, SlopeOverridePickers, and SpeedPickers are applied to ways based on how well * their OSMSpecifiers match a given OSM way. Generally one OSMSpecifier will win out over all the others based on the @@ -119,39 +118,48 @@ public WayProperties getDataForWay(OSMWithTags way) { float forwardSpeed = getCarSpeedForWay(way, false); float backSpeed = getCarSpeedForWay(way, true); - StreetTraversalPermission permission = forwardResult.getPermission(); - StreetTraversalPermission backwardPermission = backwardResult.getPermission(); + + var permission = way.overridePermissions(forwardResult.getPermission()); + + var backwardPermission = way.overridePermissions(backwardResult.getPermission()); WayProperties result = forwardResult .mutate() + .withPermission(permission) .bicycleSafety( - forwardResult.getBicycleSafetyFeatures() != null - ? forwardResult.getBicycleSafetyFeatures().forward() - : defaultBicycleSafetyForPermission.apply(permission, forwardSpeed, way), - backwardResult.getBicycleSafetyFeatures() != null - ? backwardResult.getBicycleSafetyFeatures().back() - : defaultBicycleSafetyForPermission.apply(backwardPermission, backSpeed, way) + forwardResult + .bicycleSafetyOpt() + .map(SafetyFeatures::forward) + .orElseGet(() -> defaultBicycleSafetyForPermission.apply(permission, forwardSpeed, way)), + backwardResult + .bicycleSafetyOpt() + .map(SafetyFeatures::back) + .orElseGet(() -> + defaultBicycleSafetyForPermission.apply(backwardPermission, backSpeed, way) + ) ) .walkSafety( - forwardResult.getWalkSafetyFeatures() != null - ? forwardResult.getWalkSafetyFeatures().forward() - : defaultWalkSafetyForPermission.apply(permission, forwardSpeed, way), - backwardResult.getWalkSafetyFeatures() != null - ? backwardResult.getWalkSafetyFeatures().back() - : defaultWalkSafetyForPermission.apply(backwardPermission, backSpeed, way) + forwardResult + .walkSafetyOpt() + .map(SafetyFeatures::forward) + .orElseGet(() -> defaultWalkSafetyForPermission.apply(permission, forwardSpeed, way)), + backwardResult + .walkSafetyOpt() + .map(SafetyFeatures::back) + .orElseGet(() -> defaultWalkSafetyForPermission.apply(backwardPermission, backSpeed, way)) ) .build(); /* apply mixins */ - if (backwardMixins.size() > 0) { + if (!backwardMixins.isEmpty()) { result = applyMixins(result, backwardMixins, true); } - if (forwardMixins.size() > 0) { + if (!forwardMixins.isEmpty()) { result = applyMixins(result, forwardMixins, false); } if ( (bestBackwardScore == 0 || bestForwardScore == 0) && - (backwardMixins.size() == 0 || forwardMixins.size() == 0) + (backwardMixins.isEmpty() || forwardMixins.isEmpty()) ) { String all_tags = dumpTags(way); LOG.debug("Used default permissions: {}", all_tags); @@ -250,9 +258,6 @@ public Set getNoteForWay(OSMWithTags way) { out.add(noteProperties.generateNote(way)); } } - if (out.size() == 0) { - return null; - } return out; } @@ -459,10 +464,10 @@ private WayProperties applyMixins( List mixins, boolean backward ) { - SafetyFeatures bicycleSafetyFeatures = result.getBicycleSafetyFeatures(); + SafetyFeatures bicycleSafetyFeatures = result.bicycleSafety(); double forwardBicycle = bicycleSafetyFeatures.forward(); double backBicycle = bicycleSafetyFeatures.back(); - SafetyFeatures walkSafetyFeatures = result.getWalkSafetyFeatures(); + SafetyFeatures walkSafetyFeatures = result.walkSafety(); double forwardWalk = walkSafetyFeatures.forward(); double backWalk = walkSafetyFeatures.back(); for (var mixin : mixins) { 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 dcc155b6623..4d8381963b3 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/Condition.java +++ b/src/main/java/org/opentripplanner/openstreetmap/wayproperty/specifier/Condition.java @@ -110,7 +110,7 @@ enum MatchResult { record Equals(String key, String value) implements Condition { @Override public boolean isExtendedKeyMatch(OSMWithTags way, String exKey) { - return way.hasTag(exKey) && way.matchesKeyValue(exKey, value); + return way.hasTag(exKey) && way.isTag(exKey, value); } } @@ -165,7 +165,7 @@ public boolean isExtendedKeyMatch(OSMWithTags way, String exKey) { record EqualsAnyIn(String key, String... values) implements Condition { @Override public boolean isExtendedKeyMatch(OSMWithTags way, String exKey) { - return Arrays.stream(values).anyMatch(value -> way.matchesKeyValue(exKey, value)); + return Arrays.stream(values).anyMatch(value -> way.isTag(exKey, value)); } } @@ -178,8 +178,7 @@ public EqualsAnyInOrAbsent(String key) { @Override public boolean isExtendedKeyMatch(OSMWithTags way, String exKey) { return ( - !way.hasTag(exKey) || - Arrays.stream(values).anyMatch(value -> way.matchesKeyValue(exKey, value)) + !way.hasTag(exKey) || Arrays.stream(values).anyMatch(value -> way.isTag(exKey, value)) ); } } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/RaptorSearchWindowCalculator.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/RaptorSearchWindowCalculator.java index 2fd9de738be..31608622bc5 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/RaptorSearchWindowCalculator.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/RaptorSearchWindowCalculator.java @@ -92,7 +92,9 @@ public RaptorSearchWindowCalculator calculate() { int roundUpToNearestMinute(int minTravelTimeInSeconds) { if (minTravelTimeInSeconds < 0) { - throw new IllegalArgumentException("This operation is not defined for negative numbers."); + throw new IllegalArgumentException( + "This operation is not defined for negative numbers: " + minTravelTimeInSeconds + ); } // See the UnitTest for verification of this: // We want: 0 -> 0, 1 -> 60, 59 -> 60 ... diff --git a/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java b/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java index 56bd353e25a..98127555e56 100644 --- a/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java +++ b/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java @@ -6,6 +6,7 @@ import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -61,12 +62,12 @@ public static TransitAlertBuilder of(FeedScopedId id) { return new TransitAlertBuilder(id); } - public I18NString headerText() { - return headerText; + public Optional headerText() { + return Optional.ofNullable(headerText); } - public I18NString descriptionText() { - return descriptionText; + public Optional descriptionText() { + return Optional.ofNullable(descriptionText); } public I18NString detailText() { @@ -77,9 +78,8 @@ public I18NString adviceText() { return adviceText; } - @Nullable - public I18NString url() { - return url; + public Optional url() { + return Optional.ofNullable(url); } public List siriUrls() { diff --git a/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlertBuilder.java b/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlertBuilder.java index 4d9378cbaa6..fda6b571585 100644 --- a/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlertBuilder.java +++ b/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlertBuilder.java @@ -36,11 +36,11 @@ public class TransitAlertBuilder extends AbstractEntityBuilder rideHailingServices() { - if (request.journey().modes().contains(StreetMode.CAR_HAILING)) { - return serverContext.rideHailingServices(); - } else { - return List.of(); - } - } - private static AdditionalSearchDays createAdditionalSearchDays( RaptorTuningParameters raptorTuningParameters, ZoneId zoneId, diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java index ede94b8ef6a..a0f03427e64 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java @@ -23,6 +23,7 @@ import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveItinerariesWithShortStreetLeg; import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveParkAndRideWithMostlyWalkingFilter; import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveTransitIfStreetOnlyIsBetterFilter; +import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveTransitIfWalkingIsBetterFilter; import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveWalkOnlyFilter; import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.TransitGeneralizedCostFilter; import org.opentripplanner.routing.algorithm.filterchain.filter.DeletionFlaggingFilter; @@ -56,7 +57,7 @@ public class ItineraryListFilterChainBuilder { private ItineraryFilterDebugProfile debug = ItineraryFilterDebugProfile.OFF; private int maxNumberOfItineraries = NOT_SET; private ListSection maxNumberOfItinerariesCrop = ListSection.TAIL; - private boolean removeTransitWithHigherCostThanBestOnStreetOnly = true; + private CostLinearFunction removeTransitWithHigherCostThanBestOnStreetOnly; private boolean removeWalkAllTheWayResults; private boolean sameFirstOrLastTripFilter; private TransitGeneralizedCostFilterParams transitGeneralizedCostFilterParams; @@ -72,6 +73,7 @@ public class ItineraryListFilterChainBuilder { private Function getMultiModalStation; private boolean removeItinerariesWithSameRoutesAndStops; private double minBikeParkingDistance; + private boolean removeTransitIfWalkingIsBetter = true; @Sandbox private ItineraryListFilter rideHailingFilter; @@ -184,21 +186,32 @@ public ItineraryListFilterChainBuilder withParkAndRideDurationRatio(double value /** * The direct street search(walk, bicycle, car) is not pruning the transit search, so in some * cases we get "silly" transit itineraries that is marginally better on travel-duration compared - * with a on-street-all-the-way itinerary. Use this method to turn this filter on/off. + * with a on-street-all-the-way itinerary. Use this method to filter worse enough itineraries. *

- * The filter remove all itineraries with a generalized-cost that is higher than the best + * The filter removes all itineraries with a generalized-cost that is higher than the best * on-street-all-the-way itinerary. *

* This filter only have an effect, if an on-street-all-the-way(WALK, BICYCLE, CAR) itinerary * exist. */ public ItineraryListFilterChainBuilder withRemoveTransitWithHigherCostThanBestOnStreetOnly( - boolean value + CostLinearFunction value ) { this.removeTransitWithHigherCostThanBestOnStreetOnly = value; return this; } + /** + * A transit itinerary with higher generalized-cost than a walk-only itinerary is silly. This filter removes such + * itineraries. + *

+ * This filter only have an effect, if an walk-all-the-way itinerary exist. + */ + public ItineraryListFilterChainBuilder withRemoveTransitIfWalkingIsBetter(boolean value) { + this.removeTransitIfWalkingIsBetter = value; + return this; + } + /** * This will NOT delete itineraries, but tag them as deleted using the {@link * Itinerary#getSystemNotices()}. @@ -350,8 +363,19 @@ public ItineraryListFilterChain build() { // is worse). B is removed by the {@link LatestDepartureTimeFilter} below. This is exactly // what we want, since both itineraries are none optimal. { - if (removeTransitWithHigherCostThanBestOnStreetOnly) { - filters.add(new DeletionFlaggingFilter(new RemoveTransitIfStreetOnlyIsBetterFilter())); + // Filter transit itineraries by comparing against non-transit using generalized-cost + if (removeTransitWithHigherCostThanBestOnStreetOnly != null) { + filters.add( + new DeletionFlaggingFilter( + new RemoveTransitIfStreetOnlyIsBetterFilter( + removeTransitWithHigherCostThanBestOnStreetOnly + ) + ) + ); + } + + if (removeTransitIfWalkingIsBetter) { + filters.add(new DeletionFlaggingFilter(new RemoveTransitIfWalkingIsBetterFilter())); } if (removeWalkAllTheWayResults) { diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/RoutingErrorsAttacher.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/RoutingErrorsAttacher.java index f09d601b04f..c7433f93a22 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/RoutingErrorsAttacher.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/RoutingErrorsAttacher.java @@ -11,6 +11,7 @@ import org.opentripplanner.model.plan.StreetLeg; import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.OutsideSearchWindowFilter; import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveTransitIfStreetOnlyIsBetterFilter; +import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveTransitIfWalkingIsBetterFilter; import org.opentripplanner.routing.api.response.RoutingError; /** @@ -50,7 +51,16 @@ static List computeErrors( .getSystemNotices() .stream() .anyMatch(notice -> notice.tag.equals(RemoveTransitIfStreetOnlyIsBetterFilter.TAG)); - if (filteredItineraries.stream().allMatch(isOnStreetAllTheWay.or(isWorseThanStreet))) { + Predicate isWorseThanWalking = it -> + it + .getSystemNotices() + .stream() + .anyMatch(notice -> notice.tag.equals(RemoveTransitIfWalkingIsBetterFilter.TAG)); + if ( + filteredItineraries + .stream() + .allMatch(isOnStreetAllTheWay.or(isWorseThanStreet).or(isWorseThanWalking)) + ) { var nonTransitIsWalking = filteredItineraries .stream() .flatMap(Itinerary::getStreetLegs) diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/RemoveTransitIfStreetOnlyIsBetterFilter.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/RemoveTransitIfStreetOnlyIsBetterFilter.java index 7296f9b22de..08307c16ade 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/RemoveTransitIfStreetOnlyIsBetterFilter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/RemoveTransitIfStreetOnlyIsBetterFilter.java @@ -2,17 +2,27 @@ import java.util.Comparator; import java.util.List; -import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; import java.util.stream.Collectors; +import org.opentripplanner.framework.model.Cost; import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.model.plan.Leg; +import org.opentripplanner.routing.api.request.framework.CostLinearFunction; /** * Filter itineraries based on generalizedCost, compared with a on-street-all-the-way itinerary(if - * it exist). If an itinerary is slower than the best all-the-way-on-street itinerary, then the + * it exist). If an itinerary cost exceeds the limit computed from the best all-the-way-on-street itinerary, then the * transit itinerary is removed. */ public class RemoveTransitIfStreetOnlyIsBetterFilter implements ItineraryDeletionFlagger { + private final CostLinearFunction costLimitFunction; + + public RemoveTransitIfStreetOnlyIsBetterFilter(CostLinearFunction costLimitFunction) { + this.costLimitFunction = costLimitFunction; + } + /** * Required for {@link org.opentripplanner.routing.algorithm.filterchain.ItineraryListFilterChain}, * to know which filters removed @@ -26,19 +36,22 @@ public String name() { @Override public List flagForRemoval(List itineraries) { - // Find the best walk-all-the-way option - Optional bestStreetOp = itineraries + // Find the best street-all-the-way option + OptionalInt minStreetCost = itineraries .stream() .filter(Itinerary::isOnStreetAllTheWay) - .min(Comparator.comparingInt(Itinerary::getGeneralizedCost)); + .mapToInt(Itinerary::getGeneralizedCost) + .min(); - if (bestStreetOp.isEmpty()) { + if (minStreetCost.isEmpty()) { return List.of(); } - final long limit = bestStreetOp.get().getGeneralizedCost(); + var limit = costLimitFunction + .calculate(Cost.costOfSeconds(minStreetCost.getAsInt())) + .toSeconds(); - // Filter away itineraries that have higher cost than the best non-transit option. + // Filter away itineraries that have higher cost than limit cost computed above return itineraries .stream() .filter(it -> !it.isOnStreetAllTheWay() && it.getGeneralizedCost() >= limit) diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/RemoveTransitIfWalkingIsBetterFilter.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/RemoveTransitIfWalkingIsBetterFilter.java new file mode 100644 index 00000000000..4b645e45a79 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/RemoveTransitIfWalkingIsBetterFilter.java @@ -0,0 +1,53 @@ +package org.opentripplanner.routing.algorithm.filterchain.deletionflagger; + +import java.util.Comparator; +import java.util.List; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.stream.Collectors; +import org.opentripplanner.framework.model.Cost; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.model.plan.Leg; +import org.opentripplanner.routing.api.request.framework.CostLinearFunction; + +/** + * Filter itineraries which have a higher generalized-cost than a pure walk itinerary. + */ +public class RemoveTransitIfWalkingIsBetterFilter implements ItineraryDeletionFlagger { + + /** + * Required for {@link org.opentripplanner.routing.algorithm.filterchain.ItineraryListFilterChain}, + * to know which filters removed + */ + public static final String TAG = "transit-vs-walk-filter"; + + @Override + public String name() { + return TAG; + } + + @Override + public List flagForRemoval(List itineraries) { + OptionalInt minWalkCost = itineraries + .stream() + .filter(Itinerary::isWalkingAllTheWay) + .mapToInt(Itinerary::getGeneralizedCost) + .min(); + + if (minWalkCost.isEmpty()) { + return List.of(); + } + + var limit = minWalkCost.getAsInt(); + + return itineraries + .stream() + .filter(it -> !it.isOnStreetAllTheWay() && it.getGeneralizedCost() >= limit) + .collect(Collectors.toList()); + } + + @Override + public boolean skipAlreadyFlaggedItineraries() { + return false; + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java index d2926532abd..e8a3f226d8a 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java @@ -65,6 +65,9 @@ public static ItineraryListFilterChain createFilterChain( .withBikeRentalDistanceRatio(params.bikeRentalDistanceRatio()) .withParkAndRideDurationRatio(params.parkAndRideDurationRatio()) .withNonTransitGeneralizedCostLimit(params.nonTransitGeneralizedCostLimit()) + .withRemoveTransitWithHigherCostThanBestOnStreetOnly( + params.removeTransitWithHigherCostThanBestOnStreetOnly() + ) .withSameFirstOrLastTripFilter(params.filterItinerariesWithSameFirstOrLastTrip()) .withAccessibilityScore( params.useAccessibilityScore() && request.wheelchair(), @@ -79,10 +82,10 @@ public static ItineraryListFilterChain createFilterChain( context.transitService().getTransitAlertService(), context.transitService()::getMultiModalStationForStation ) - .withRemoveTransitWithHigherCostThanBestOnStreetOnly(true) .withLatestDepartureTimeLimit(filterOnLatestDepartureTime) .withMaxLimitReachedSubscriber(maxLimitReachedSubscriber) .withRemoveWalkAllTheWayResults(removeWalkAllTheWayResults) + .withRemoveTransitIfWalkingIsBetter(true) .withDebugEnabled(params.debug()); if (!context.rideHailingServices().isEmpty()) { diff --git a/src/main/java/org/opentripplanner/routing/api/request/StreetMode.java b/src/main/java/org/opentripplanner/routing/api/request/StreetMode.java index fcdefa35de8..0f5d65305b3 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/StreetMode.java +++ b/src/main/java/org/opentripplanner/routing/api/request/StreetMode.java @@ -137,7 +137,7 @@ public String typeDescription() { private static String GBFS_PREREQ = """ - _Prerequisite:_ Vehicle or station location need to be added to OTP from dynamic data feeds. + _Prerequisite:_ Vehicle or station locations need to be added to OTP from dynamic data feeds. See [Configuring GBFS](UpdaterConfig.md#gbfs-vehicle-rental-systems) on how to add one. """; @@ -148,8 +148,9 @@ public String enumValueDescription() { case WALK -> "Walking some or all of the way of the route."; case BIKE -> "Cycling for the entirety of the route or taking a bicycle onto the public transport and cycling from the arrival station to the destination."; case BIKE_TO_PARK -> """ - Leaving the bicycle at the departure station and walking from the arrival station to the destination. - This mode needs to be combined with at least one transit mode otherwise it behaves like an ordinary bicycle journey. + Leaving the bicycle at the departure station and walking from the arrival station to the destination. + This mode needs to be combined with at least one transit mode otherwise it behaves like an ordinary bicycle journey. + _Prerequisite:_ Bicycle parking stations present in the OSM file and visible to OTP by enabling the property `staticBikeParkAndRide` during graph build. """; case BIKE_RENTAL -> """ @@ -182,7 +183,7 @@ This means that the car is not parked in a permanent parking area but rather the See [the sandbox documentation](sandbox/RideHailing.md) on how to configure it. """; - case FLEXIBLE -> "Encompasses all types of on-demand and flexible transportation for example GTFS Flex or NeTex Flexible Stop Places."; + case FLEXIBLE -> "Encompasses all types of on-demand and flexible transportation for example GTFS Flex or NeTEx Flexible Stop Places."; }; } } diff --git a/src/main/java/org/opentripplanner/routing/api/request/preference/ItineraryFilterPreferences.java b/src/main/java/org/opentripplanner/routing/api/request/preference/ItineraryFilterPreferences.java index 731bfd3981b..91f3071d4ed 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/preference/ItineraryFilterPreferences.java +++ b/src/main/java/org/opentripplanner/routing/api/request/preference/ItineraryFilterPreferences.java @@ -30,6 +30,8 @@ public final class ItineraryFilterPreferences { private final double parkAndRideDurationRatio; private final boolean removeItinerariesWithSameRoutesAndStops; private final TransitGeneralizedCostFilterParams transitGeneralizedCostLimit; + private final CostLinearFunction removeTransitWithHigherCostThanBestOnStreetOnly; + private final boolean removeTransitIfWalkingIsBetter; private ItineraryFilterPreferences() { this.accessibilityScore = false; @@ -48,6 +50,9 @@ private ItineraryFilterPreferences() { CostLinearFunction.of(Duration.ofMinutes(15), 1.5), 0.4 ); + this.removeTransitWithHigherCostThanBestOnStreetOnly = + CostLinearFunction.of(Duration.ofMinutes(1), 1.3); + this.removeTransitIfWalkingIsBetter = false; } private ItineraryFilterPreferences(Builder builder) { @@ -66,6 +71,9 @@ private ItineraryFilterPreferences(Builder builder) { this.parkAndRideDurationRatio = Units.ratio(builder.parkAndRideDurationRatio); this.removeItinerariesWithSameRoutesAndStops = builder.removeItinerariesWithSameRoutesAndStops; this.transitGeneralizedCostLimit = Objects.requireNonNull(builder.transitGeneralizedCostLimit); + this.removeTransitWithHigherCostThanBestOnStreetOnly = + Objects.requireNonNull(builder.removeTransitWithHigherCostThanBestOnStreetOnly); + this.removeTransitIfWalkingIsBetter = builder.removeTransitIfWalkingIsBetter; } public static Builder of() { @@ -124,6 +132,14 @@ public TransitGeneralizedCostFilterParams transitGeneralizedCostLimit() { return transitGeneralizedCostLimit; } + public CostLinearFunction removeTransitWithHigherCostThanBestOnStreetOnly() { + return removeTransitWithHigherCostThanBestOnStreetOnly; + } + + public boolean removeTransitIfWalkingIsBetter() { + return removeTransitIfWalkingIsBetter; + } + @Override public String toString() { return ToStringBuilder @@ -162,10 +178,16 @@ public String toString() { transitGeneralizedCostLimit, DEFAULT.transitGeneralizedCostLimit ) + .addObj( + "removeTransitWithHigherCostThanBestOnStreetOnly", + removeTransitWithHigherCostThanBestOnStreetOnly, + DEFAULT.removeTransitWithHigherCostThanBestOnStreetOnly + ) .addBoolIfTrue( "removeItinerariesWithSameRoutesAndStops", removeItinerariesWithSameRoutesAndStops ) + .addBoolIfTrue("removeTransitIfWalkingIsBetter", removeTransitIfWalkingIsBetter) .toString(); } @@ -178,6 +200,7 @@ public boolean equals(Object o) { accessibilityScore == that.accessibilityScore && Double.compare(that.bikeRentalDistanceRatio, bikeRentalDistanceRatio) == 0 && debug == that.debug && + removeTransitIfWalkingIsBetter == that.removeTransitIfWalkingIsBetter && filterItinerariesWithSameFirstOrLastTrip == that.filterItinerariesWithSameFirstOrLastTrip && Double.compare( that.groupedOtherThanSameLegsMaxCostMultiplier, @@ -190,6 +213,10 @@ public boolean equals(Object o) { Double.compare(that.parkAndRideDurationRatio, parkAndRideDurationRatio) == 0 && removeItinerariesWithSameRoutesAndStops == that.removeItinerariesWithSameRoutesAndStops && Objects.equals(nonTransitGeneralizedCostLimit, that.nonTransitGeneralizedCostLimit) && + Objects.equals( + removeTransitWithHigherCostThanBestOnStreetOnly, + that.removeTransitWithHigherCostThanBestOnStreetOnly + ) && Objects.equals(transitGeneralizedCostLimit, that.transitGeneralizedCostLimit) ); } @@ -208,7 +235,9 @@ public int hashCode() { nonTransitGeneralizedCostLimit, parkAndRideDurationRatio, removeItinerariesWithSameRoutesAndStops, - transitGeneralizedCostLimit + transitGeneralizedCostLimit, + removeTransitWithHigherCostThanBestOnStreetOnly, + removeTransitIfWalkingIsBetter ); } @@ -227,6 +256,8 @@ public static class Builder { private double parkAndRideDurationRatio; private boolean removeItinerariesWithSameRoutesAndStops; private TransitGeneralizedCostFilterParams transitGeneralizedCostLimit; + private CostLinearFunction removeTransitWithHigherCostThanBestOnStreetOnly; + private boolean removeTransitIfWalkingIsBetter; public ItineraryFilterPreferences original() { return original; @@ -302,6 +333,19 @@ public Builder withTransitGeneralizedCostLimit( return this; } + public Builder withRemoveTransitWithHigherCostThanBestOnStreetOnly( + CostLinearFunction removeTransitWithHigherCostThanBestOnStreetOnly + ) { + this.removeTransitWithHigherCostThanBestOnStreetOnly = + removeTransitWithHigherCostThanBestOnStreetOnly; + return this; + } + + public Builder withRemoveTransitIfWalkingIsBetter(boolean removeTransitIfWalkingIsBetter) { + this.removeTransitIfWalkingIsBetter = removeTransitIfWalkingIsBetter; + return this; + } + public Builder(ItineraryFilterPreferences original) { this.original = original; this.accessibilityScore = original.accessibilityScore; @@ -319,6 +363,9 @@ public Builder(ItineraryFilterPreferences original) { this.removeItinerariesWithSameRoutesAndStops = original.removeItinerariesWithSameRoutesAndStops; this.transitGeneralizedCostLimit = original.transitGeneralizedCostLimit; + this.removeTransitWithHigherCostThanBestOnStreetOnly = + original.removeTransitWithHigherCostThanBestOnStreetOnly; + this.removeTransitIfWalkingIsBetter = original.removeTransitIfWalkingIsBetter; } public Builder apply(Consumer body) { diff --git a/src/main/java/org/opentripplanner/routing/graphfinder/StopFinderTraverseVisitor.java b/src/main/java/org/opentripplanner/routing/graphfinder/StopFinderTraverseVisitor.java index 64d0fcee6d7..631f527494a 100644 --- a/src/main/java/org/opentripplanner/routing/graphfinder/StopFinderTraverseVisitor.java +++ b/src/main/java/org/opentripplanner/routing/graphfinder/StopFinderTraverseVisitor.java @@ -4,21 +4,21 @@ import java.util.List; import org.opentripplanner.astar.spi.SkipEdgeStrategy; import org.opentripplanner.astar.spi.TraverseVisitor; +import org.opentripplanner.framework.lang.PredicateUtils; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.vertex.TransitStopVertex; import org.opentripplanner.street.model.vertex.Vertex; import org.opentripplanner.street.search.state.State; -// TODO Seems like this should be merged with the PlaceFinderTraverseVisitor - /** * A TraverseVisitor used in finding stops while walking the street graph. */ public class StopFinderTraverseVisitor implements TraverseVisitor { private final double radiusMeters; + /** A list of closest stops found while walking the graph */ - public final List stopsFound = new ArrayList<>(); + private final List stopsFound = new ArrayList<>(); public StopFinderTraverseVisitor(double radiusMeters) { this.radiusMeters = radiusMeters; @@ -27,18 +27,24 @@ public StopFinderTraverseVisitor(double radiusMeters) { @Override public void visitEdge(Edge edge) {} - // Accumulate stops into ret as the search runs. @Override public void visitVertex(State state) { Vertex vertex = state.getVertex(); - if (vertex instanceof TransitStopVertex) { - stopsFound.add(NearbyStop.nearbyStopForState(state, ((TransitStopVertex) vertex).getStop())); + if (vertex instanceof TransitStopVertex tsv) { + stopsFound.add(NearbyStop.nearbyStopForState(state, tsv.getStop())); } } @Override public void visitEnqueue() {} + /** + * @return A de-duplicated list of nearby stops found by this visitor. + */ + public List stopsFound() { + return stopsFound.stream().filter(PredicateUtils.distinctByKey(ns -> ns.stop)).toList(); + } + /** * @return A SkipEdgeStrategy that will stop exploring edges after the distance radius has been * reached. diff --git a/src/main/java/org/opentripplanner/routing/graphfinder/StreetGraphFinder.java b/src/main/java/org/opentripplanner/routing/graphfinder/StreetGraphFinder.java index a324cc7a4ef..06472c9ad97 100644 --- a/src/main/java/org/opentripplanner/routing/graphfinder/StreetGraphFinder.java +++ b/src/main/java/org/opentripplanner/routing/graphfinder/StreetGraphFinder.java @@ -41,7 +41,7 @@ public List findClosestStops(Coordinate coordinate, double radiusMet visitor, visitor.getSkipEdgeStrategy() ); - return visitor.stopsFound; + return visitor.stopsFound(); } @Override diff --git a/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java b/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java index b70fec35420..65d1a412331 100644 --- a/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/BuildConfig.java @@ -148,8 +148,6 @@ public class BuildConfig implements OtpDataStoreConfig { /** See {@link IslandPruningConfig}. */ public final IslandPruningConfig islandPruning; - public final boolean banDiscouragedWalking; - public final boolean banDiscouragedBiking; public final Duration maxTransferDuration; public final NetexFeedParameters netexDefaults; public final GtfsFeedParameters gtfsDefaults; @@ -211,18 +209,6 @@ public BuildConfig(NodeAdapter root, boolean logUnusedParams) { """ ) .asBoolean(false); - banDiscouragedWalking = - root - .of("banDiscouragedWalking") - .since(V2_0) - .summary("Should walking be allowed on OSM ways tagged with `foot=discouraged`") - .asBoolean(false); - banDiscouragedBiking = - root - .of("banDiscouragedBiking") - .since(V2_0) - .summary("Should biking be allowed on OSM ways tagged with `bicycle=discouraged`") - .asBoolean(false); configVersion = root .of("configVersion") diff --git a/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java b/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java index a3aa9afb82c..3bc66ce6ea5 100644 --- a/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java +++ b/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java @@ -647,14 +647,6 @@ private Duration parseDuration(String value) { } } - private Duration parseDuration(String value, ChronoUnit unit) { - try { - return DurationUtils.duration(value, unit); - } catch (DateTimeParseException e) { - throw error("The parameter value '%s' is not a duration.".formatted(value), e); - } - } - /** * Somehow Java do not provide a parse method for parsing Locale on the standard form * {@code [_[_]]}, so this little utility method does that. diff --git a/src/main/java/org/opentripplanner/standalone/config/routerequest/ItineraryFiltersConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerequest/ItineraryFiltersConfig.java index 1f78b96c391..82e2c0839f9 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerequest/ItineraryFiltersConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerequest/ItineraryFiltersConfig.java @@ -5,6 +5,7 @@ import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_1; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_2; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_3; +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_4; import org.opentripplanner.routing.algorithm.filterchain.api.TransitGeneralizedCostFilterParams; import org.opentripplanner.routing.api.request.preference.ItineraryFilterDebugProfile; @@ -153,6 +154,26 @@ public static void mapItineraryFilterParams( ) .asCostLinearFunction(dft.nonTransitGeneralizedCostLimit()) ) + .withRemoveTransitWithHigherCostThanBestOnStreetOnly( + c + .of("removeTransitWithHigherCostThanBestOnStreetOnly") + .since(V2_4) + .summary( + "Limit function for generalized-cost computed from street-only itineries applied to transit itineraries." + ) + .description( + """ +The max-limit is applied to itineraries with transit *legs*, and only itineraries +without transit legs are considered when calculating the minimum cost. The smallest +generalized-cost value is used as input to the function. The function is used to calculate a +*max-limit*. The max-limit is then used to filter *transit* itineraries by +*generalized-cost*. Itineraries with a cost higher than the max-limit are dropped from the result +set. Walking is handled with a different logic: if a transit itinerary has higher cost than +a plain walk itinerary, it will be removed even if the cost limit function would keep it. +""" + ) + .asCostLinearFunction(dft.removeTransitWithHigherCostThanBestOnStreetOnly()) + ) .withBikeRentalDistanceRatio( c .of("bikeRentalDistanceRatio") diff --git a/src/main/java/org/opentripplanner/street/model/StreetTraversalPermission.java b/src/main/java/org/opentripplanner/street/model/StreetTraversalPermission.java index c7ed7ffff41..98f7f2c3c91 100644 --- a/src/main/java/org/opentripplanner/street/model/StreetTraversalPermission.java +++ b/src/main/java/org/opentripplanner/street/model/StreetTraversalPermission.java @@ -65,10 +65,7 @@ public boolean allows(TraverseModeSet modes) { return true; } else if (modes.getBicycle() && allows(StreetTraversalPermission.BICYCLE)) { return true; - } else if (modes.getCar() && allows(StreetTraversalPermission.CAR)) { - return true; - } - return false; + } else return modes.getCar() && allows(StreetTraversalPermission.CAR); } /** @@ -79,10 +76,7 @@ public boolean allows(TraverseMode mode) { return true; } else if (mode.isCyclingIsh() && allows(StreetTraversalPermission.BICYCLE)) { return true; - } else if (mode == TraverseMode.CAR && allows(StreetTraversalPermission.CAR)) { - return true; - } - return false; + } else return mode == TraverseMode.CAR && allows(StreetTraversalPermission.CAR); } /** @@ -93,10 +87,9 @@ public boolean allowsAnything() { } /** - * Returns true if there no modes are by this permission. + * Returns true if this permission allows nothing to traverse */ public boolean allowsNothing() { - // TODO(flamholz): what about CROSSHATCHED? return this == StreetTraversalPermission.NONE; } } diff --git a/src/main/java/org/opentripplanner/street/model/edge/StreetTransitEntityLink.java b/src/main/java/org/opentripplanner/street/model/edge/StreetTransitEntityLink.java index 2c3a7681ebb..4fe66735532 100644 --- a/src/main/java/org/opentripplanner/street/model/edge/StreetTransitEntityLink.java +++ b/src/main/java/org/opentripplanner/street/model/edge/StreetTransitEntityLink.java @@ -86,11 +86,11 @@ public State[] traverse(State s0) { } } - switch (s0.currentMode()) { - case BICYCLE: + return switch (s0.currentMode()) { + case BICYCLE, SCOOTER -> { // Forbid taking your own bike in the station if bike P+R activated. if (s0.getRequest().mode().includesParking() && !s0.isVehicleParked()) { - return State.empty(); + yield State.empty(); } // Forbid taking a (station) rental vehicle in the station. This allows taking along // floating rental vehicles. @@ -101,32 +101,38 @@ else if ( s0.getRequest().rental().allowArrivingInRentedVehicleAtDestination() ) ) { - return State.empty(); + yield State.empty(); } - // Allow taking an owned bike in the station - break; - case CAR: + yield buildState(s0, s1, pref); + } + // Allow taking an owned bike in the station + case CAR -> { // Forbid taking your own car in the station if bike P+R activated. if (s0.getRequest().mode().includesParking() && !s0.isVehicleParked()) { - return State.empty(); + yield State.empty(); } // For Kiss & Ride allow dropping of the passenger before entering the station if (s0.getCarPickupState() != null) { if (canDropOffAfterDriving(s0) && isLeavingStreetNetwork(s0.getRequest().arriveBy())) { dropOffAfterDriving(s0, s1); } else { - return State.empty(); + yield State.empty(); } } - // If Kiss & Ride (Taxi) mode is not enabled allow car traversal so that the Stop - // may be reached by car - break; - case WALK: - break; - default: - return State.empty(); - } + if (s0.isRentingVehicleFromStation()) { + yield State.empty(); + } + yield buildState(s0, s1, pref); + } + // If Kiss & Ride (Taxi) mode is not enabled allow car traversal so that the Stop + // may be reached by car + case WALK -> buildState(s0, s1, pref); + case FLEX -> State.empty(); + }; + } + @Nonnull + private State[] buildState(State s0, StateEditor s1, RoutingPreferences pref) { if ( s0.isRentingVehicleFromStation() && s0.mayKeepRentedVehicleAtDestination() && diff --git a/src/main/java/org/opentripplanner/street/search/state/State.java b/src/main/java/org/opentripplanner/street/search/state/State.java index 591ade0a33d..37c3e81c097 100644 --- a/src/main/java/org/opentripplanner/street/search/state/State.java +++ b/src/main/java/org/opentripplanner/street/search/state/State.java @@ -206,7 +206,7 @@ public boolean isRentingVehicle() { ); } - public boolean vehicleRentalIsFinished() { + private boolean vehicleRentalIsFinished() { return ( stateData.vehicleRentalState == VehicleRentalState.HAVE_RENTED || ( @@ -221,7 +221,7 @@ public boolean vehicleRentalIsFinished() { ); } - public boolean vehicleRentalNotStarted() { + private boolean vehicleRentalNotStarted() { return stateData.vehicleRentalState == VehicleRentalState.BEFORE_RENTING; } @@ -471,6 +471,12 @@ public String toString() { .addNum("weight", weight) .addObj("vertex", vertex) .addBoolIfTrue("VEHICLE_RENT", isRentingVehicle()) + .addEnum("formFactor", vehicleRentalFormFactor()) + .addBoolIfTrue("RENTING_FROM_STATION", isRentingVehicleFromStation()) + .addBoolIfTrue( + "RENTING_FREE_FLOATING", + isRentingFloatingVehicle() && !isRentingVehicleFromStation() + ) .addBoolIfTrue("VEHICLE_PARKED", isVehicleParked()) .toString(); } diff --git a/src/main/java/org/opentripplanner/transit/service/TransitModel.java b/src/main/java/org/opentripplanner/transit/service/TransitModel.java index e845aa27dfb..c2262fafbc4 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitModel.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitModel.java @@ -375,13 +375,6 @@ public Map getTripOnServiceDates() { return tripOnServiceDates; } - /** - * Return a transit stop, a flex stop location or flex stop location group. - */ - public StopLocation getStopLocationById(FeedScopedId id) { - return stopModel.getStopLocation(id); - } - /** * Map from GTFS ServiceIds to integers close to 0. Allows using BitSets instead of * {@code Set}. An empty Map is created before the Graph is built to allow registering IDs diff --git a/src/test/java/org/opentripplanner/_support/text/I18NStrings.java b/src/test/java/org/opentripplanner/_support/text/I18NStrings.java new file mode 100644 index 00000000000..245d1f48473 --- /dev/null +++ b/src/test/java/org/opentripplanner/_support/text/I18NStrings.java @@ -0,0 +1,22 @@ +package org.opentripplanner._support.text; + +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.framework.i18n.TranslatedString; + +public class I18NStrings { + + public static final I18NString TRANSLATED_STRING_1 = TranslatedString.getI18NString( + "First string", + "de", + "Erste Zeichenabfolge", + "fi", + "Minulla ei ole aavistustakaan kuinka puhua suomea" + ); + public static final I18NString TRANSLATED_STRING_2 = TranslatedString.getI18NString( + "Second string", + "de", + "Zweite Zeichenabfolge", + "fi", + "Etkö ole varma, mitä tämä tarkoittaa" + ); +} diff --git a/src/test/java/org/opentripplanner/framework/lang/PredicateUtilsTest.java b/src/test/java/org/opentripplanner/framework/lang/PredicateUtilsTest.java new file mode 100644 index 00000000000..27beab35c06 --- /dev/null +++ b/src/test/java/org/opentripplanner/framework/lang/PredicateUtilsTest.java @@ -0,0 +1,27 @@ +package org.opentripplanner.framework.lang; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; + +class PredicateUtilsTest { + + private static String makeHello() { + return new String("HELLO"); + } + + @Test + void distinctByKey() { + var first = new Wrapper(10, makeHello()); + var last = new Wrapper(20, "HI"); + var stream = Stream.of(first, new Wrapper(20, makeHello()), last); + + var deduplicated = stream.filter(PredicateUtils.distinctByKey(w -> w.string)).toList(); + + assertEquals(List.of(first, last), deduplicated); + } + + private record Wrapper(int i, String string) {} +} diff --git a/src/test/java/org/opentripplanner/graph_builder/module/GtfsModuleTest.java b/src/test/java/org/opentripplanner/graph_builder/module/GtfsModuleTest.java index c120b63c08a..253e9882fc4 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/GtfsModuleTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/GtfsModuleTest.java @@ -11,12 +11,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.opentripplanner.ConstantsForTests; import org.opentripplanner.gtfs.graphbuilder.GtfsBundle; import org.opentripplanner.gtfs.graphbuilder.GtfsModule; import org.opentripplanner.model.calendar.ServiceDateInterval; import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.test.support.VariableSource; import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.service.StopModel; import org.opentripplanner.transit.service.TransitModel; @@ -73,18 +73,20 @@ static GtfsBundle bundle(String feedId) { return b; } - static Stream interliningCases = Stream.of( - Arguments.of(List.of(bundle("A")), 2), - Arguments.of(List.of(bundle("A"), bundle("B")), 4), - Arguments.of(List.of(bundle("A"), bundle("B"), bundle("C")), 6) - ); + static List interliningCases() { + return List.of( + Arguments.of(List.of(bundle("A")), 2), + Arguments.of(List.of(bundle("A"), bundle("B")), 4), + Arguments.of(List.of(bundle("A"), bundle("B"), bundle("C")), 6) + ); + } /** * We test that the number of stay-seated transfers grows linearly (not exponentially) with the * number of GTFS input feeds. */ @ParameterizedTest(name = "Bundles {0} should generate {1} stay-seated transfers") - @VariableSource("interliningCases") + @MethodSource("interliningCases") public void interline(List bundles, int expectedTransfers) { var model = buildTestModel(); diff --git a/src/test/java/org/opentripplanner/graph_builder/module/osm/OpenStreetMapParserTest.java b/src/test/java/org/opentripplanner/graph_builder/module/osm/OpenStreetMapParserTest.java index 2fdb2b4be1f..f0a3b8254f0 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/osm/OpenStreetMapParserTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/osm/OpenStreetMapParserTest.java @@ -7,7 +7,6 @@ import java.io.File; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.util.Set; import org.junit.jupiter.api.Test; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.openstreetmap.OsmProvider; @@ -22,7 +21,7 @@ public void testBinaryParser() { URLDecoder.decode(getClass().getResource("map.osm.pbf").getPath(), StandardCharsets.UTF_8) ); OsmProvider pr = new OsmProvider(osmFile, true); - OsmDatabase osmdb = new OsmDatabase(DataImportIssueStore.NOOP, Set.of()); + OsmDatabase osmdb = new OsmDatabase(DataImportIssueStore.NOOP); pr.readOSM(osmdb); diff --git a/src/test/java/org/opentripplanner/graph_builder/module/osm/OsmModuleTest.java b/src/test/java/org/opentripplanner/graph_builder/module/osm/OsmModuleTest.java index b8016cb1a29..c0c7a68cc37 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/osm/OsmModuleTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/osm/OsmModuleTest.java @@ -168,7 +168,6 @@ public void testWayDataSet() { OSMWithTags way = new OSMWay(); way.addTag("highway", "footway"); way.addTag("cycleway", "lane"); - way.addTag("access", "no"); way.addTag("surface", "gravel"); WayPropertySet wayPropertySet = new WayPropertySet(); @@ -176,10 +175,10 @@ public void testWayDataSet() { // where there are no way specifiers, the default is used WayProperties wayData = wayPropertySet.getDataForWay(way); assertEquals(wayData.getPermission(), ALL); - assertEquals(wayData.getWalkSafetyFeatures().forward(), 1.0); - assertEquals(wayData.getWalkSafetyFeatures().back(), 1.0); - assertEquals(wayData.getBicycleSafetyFeatures().forward(), 1.0); - assertEquals(wayData.getBicycleSafetyFeatures().back(), 1.0); + assertEquals(wayData.walkSafety().forward(), 1.0); + assertEquals(wayData.walkSafety().back(), 1.0); + assertEquals(wayData.bicycleSafety().forward(), 1.0); + assertEquals(wayData.bicycleSafety().back(), 1.0); // add two equal matches: lane only... OsmSpecifier lane_only = new BestMatchSpecifier("cycleway=lane"); @@ -217,7 +216,7 @@ public void testWayDataSet() { wayPropertySet.setMixinProperties(gravel, gravel_is_dangerous); dataForWay = wayPropertySet.getDataForWay(way); - assertEquals(dataForWay.getBicycleSafetyFeatures().forward(), 1.5); + assertEquals(dataForWay.bicycleSafety().forward(), 1.5); // test a left-right distinction way = new OSMWay(); @@ -234,9 +233,9 @@ public void testWayDataSet() { wayPropertySet.addProperties(track_only, track_is_safest); dataForWay = wayPropertySet.getDataForWay(way); // right (with traffic) comes from track - assertEquals(0.25, dataForWay.getBicycleSafetyFeatures().forward()); + assertEquals(0.25, dataForWay.bicycleSafety().forward()); // left comes from lane - assertEquals(0.75, dataForWay.getBicycleSafetyFeatures().back()); + assertEquals(0.75, dataForWay.bicycleSafety().back()); way = new OSMWay(); way.addTag("highway", "footway"); diff --git a/src/test/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilderTest.java b/src/test/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilderTest.java index 4e8b90863f0..8786fbdf304 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilderTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/osm/WalkableAreaBuilderTest.java @@ -17,8 +17,6 @@ import java.util.function.Consumer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; -import org.junit.jupiter.api.parallel.Execution; -import org.junit.jupiter.api.parallel.ExecutionMode; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.graph_builder.module.osm.naming.DefaultNamer; import org.opentripplanner.openstreetmap.OsmProvider; @@ -39,7 +37,7 @@ public Graph buildGraph(final TestInfo testInfo) { final boolean platformEntriesLinking = true; final Set boardingAreaRefTags = Set.of(); - final OsmDatabase osmdb = new OsmDatabase(DataImportIssueStore.NOOP, boardingAreaRefTags); + final OsmDatabase osmdb = new OsmDatabase(DataImportIssueStore.NOOP); final File file = new File(testInfo.getTestClass().get().getResource(osmFile).getFile()); assertTrue(file.exists()); @@ -78,7 +76,7 @@ public Graph buildGraph(final TestInfo testInfo) { @OsmFile("lund-station-sweden.osm.pbf") @Visibility(true) @MaxAreaNodes(5) - public void testCalculateVerticesArea(TestInfo testInfo) { + void testCalculateVerticesArea(TestInfo testInfo) { var graph = buildGraph(testInfo); var areas = graph .getEdgesOfType(AreaEdge.class) @@ -95,7 +93,7 @@ public void testCalculateVerticesArea(TestInfo testInfo) { @OsmFile("lund-station-sweden.osm.pbf") @Visibility(false) @MaxAreaNodes(5) - public void testSetupCalculateVerticesAreaWithoutVisibility(TestInfo testInfo) { + void testSetupCalculateVerticesAreaWithoutVisibility(TestInfo testInfo) { var graph = buildGraph(testInfo); var areas = graph .getEdgesOfType(AreaEdge.class) @@ -114,7 +112,7 @@ public void testSetupCalculateVerticesAreaWithoutVisibility(TestInfo testInfo) { @OsmFile("stopareas.pbf") @Visibility(true) @MaxAreaNodes(50) - public void testEntranceStopAreaLinking(TestInfo testInfo) { + void testEntranceStopAreaLinking(TestInfo testInfo) { var graph = buildGraph(testInfo); // first platform contains isolated node tagged as highway=bus_stop. Those are linked if level matches. var busStopConnection = graph @@ -181,6 +179,23 @@ public void testEntranceStopAreaLinking(TestInfo testInfo) { assertEquals(1, elevatorConnection.size()); } + @Test + @OsmFile("wendlingen-bahnhof.osm.pbf") + @Visibility(true) + @MaxAreaNodes(50) + void testSeveralIntersections(TestInfo testInfo) { + var graph = buildGraph(testInfo); + var areas = graph + .getEdgesOfType(AreaEdge.class) + .stream() + .filter(a -> a.getToVertex().getLabel().equals(VertexLabel.osm(2522105666L))) + .map(AreaEdge::getArea) + .distinct() + .toList(); + assertEquals(1, areas.size()); + assertFalse(areas.get(0).getAreas().isEmpty()); + } + private static boolean hasNodeId(AreaEdge a, long nodeId) { return ( a.getToVertex().getLabel() instanceof OsmNodeOnLevelLabel label && label.nodeId() == nodeId diff --git a/src/test/java/org/opentripplanner/openstreetmap/model/BicycleNetworkRelationsTest.java b/src/test/java/org/opentripplanner/openstreetmap/model/BicycleNetworkRelationsTest.java index 96b8a13ebf8..be8fca3864d 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/model/BicycleNetworkRelationsTest.java +++ b/src/test/java/org/opentripplanner/openstreetmap/model/BicycleNetworkRelationsTest.java @@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import java.io.File; -import java.util.Set; import org.junit.jupiter.api.Test; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.graph_builder.module.osm.OsmDatabase; @@ -19,7 +18,7 @@ public class BicycleNetworkRelationsTest { @Test public void testBicycleRouteRelations() { var issueStore = DataImportIssueStore.NOOP; - var osmdb = new OsmDatabase(issueStore, Set.of()); + var osmdb = new OsmDatabase(issueStore); var provider = new OsmProvider( new File("src/test/resources/germany/ehningen-minimal.osm.pbf"), true diff --git a/src/test/java/org/opentripplanner/openstreetmap/model/OSMNodeTest.java b/src/test/java/org/opentripplanner/openstreetmap/model/OSMNodeTest.java index 26dadab5c7d..a4579dec71e 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/model/OSMNodeTest.java +++ b/src/test/java/org/opentripplanner/openstreetmap/model/OSMNodeTest.java @@ -1,9 +1,7 @@ package org.opentripplanner.openstreetmap.model; -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 static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.Test; @@ -20,23 +18,4 @@ public void testIsMultiLevel() { node.addTag("highway", "elevator"); assertTrue(node.isMultiLevel()); } - - @Test - public void testGetCapacity() { - OSMNode node = new OSMNode(); - assertFalse(node.hasTag("capacity")); - assertEquals(0, node.getCapacity()); - - try { - node.addTag("capacity", "foobie"); - node.getCapacity(); - - // Above should fail. - fail(); - } catch (NumberFormatException e) {} - - node.addTag("capacity", "10"); - assertTrue(node.hasTag("capacity")); - assertEquals(10, node.getCapacity()); - } } diff --git a/src/test/java/org/opentripplanner/openstreetmap/model/OSMWayTest.java b/src/test/java/org/opentripplanner/openstreetmap/model/OSMWayTest.java index 8ec384601f0..c316793ad8c 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/model/OSMWayTest.java +++ b/src/test/java/org/opentripplanner/openstreetmap/model/OSMWayTest.java @@ -4,16 +4,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; -import org.opentripplanner.graph_builder.module.osm.OsmFilter; -import org.opentripplanner.graph_builder.module.osm.StreetTraversalPermissionPair; -import org.opentripplanner.openstreetmap.wayproperty.WayProperties; -import org.opentripplanner.openstreetmap.wayproperty.WayPropertySet; -import org.opentripplanner.street.model.StreetTraversalPermission; +import org.opentripplanner.openstreetmap.wayproperty.specifier.WayTestData; public class OSMWayTest { @Test - public void testIsBicycleDismountForced() { + void testIsBicycleDismountForced() { OSMWay way = new OSMWay(); assertFalse(way.isBicycleDismountForced()); @@ -22,7 +18,7 @@ public void testIsBicycleDismountForced() { } @Test - public void testIsSteps() { + void testIsSteps() { OSMWay way = new OSMWay(); assertFalse(way.isSteps()); @@ -34,7 +30,20 @@ public void testIsSteps() { } @Test - public void testIsRoundabout() { + void wheelchairAccessibleStairs() { + var osm1 = new OSMWay(); + osm1.addTag("highway", "steps"); + assertFalse(osm1.isWheelchairAccessible()); + + // explicitly suitable for wheelchair users, perhaps because of a ramp + var osm2 = new OSMWay(); + osm2.addTag("highway", "steps"); + osm2.addTag("wheelchair", "yes"); + assertTrue(osm2.isWheelchairAccessible()); + } + + @Test + void testIsRoundabout() { OSMWay way = new OSMWay(); assertFalse(way.isRoundabout()); @@ -46,7 +55,7 @@ public void testIsRoundabout() { } @Test - public void testIsOneWayDriving() { + void testIsOneWayDriving() { OSMWay way = new OSMWay(); assertFalse(way.isOneWayForwardDriving()); assertFalse(way.isOneWayReverseDriving()); @@ -65,7 +74,7 @@ public void testIsOneWayDriving() { } @Test - public void testIsOneWayBicycle() { + void testIsOneWayBicycle() { OSMWay way = new OSMWay(); assertFalse(way.isOneWayForwardBicycle()); assertFalse(way.isOneWayReverseBicycle()); @@ -84,7 +93,7 @@ public void testIsOneWayBicycle() { } @Test - public void testIsOneDirectionSidepath() { + void testIsOneDirectionSidepath() { OSMWay way = new OSMWay(); assertFalse(way.isForwardDirectionSidepath()); assertFalse(way.isReverseDirectionSidepath()); @@ -99,7 +108,7 @@ public void testIsOneDirectionSidepath() { } @Test - public void testIsOpposableCycleway() { + void testIsOpposableCycleway() { OSMWay way = new OSMWay(); assertFalse(way.isOpposableCycleway()); @@ -117,223 +126,18 @@ public void testIsOpposableCycleway() { assertTrue(way.isOpposableCycleway()); } - /** - * Tests if cars can drive on unclassified highways with bicycleDesignated - *

- * Check for bug #1878 and PR #1880 - */ - @Test - public void testCarPermission() { - OSMWay way = new OSMWay(); - way.addTag("highway", "unclassified"); - - var permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); - - way.addTag("bicycle", "designated"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); - } - - /** - * Tests that motorcar/bicycle/foot private don't add permissions but yes add permission if access - * is no - */ - @Test - public void testMotorCarTagAllowedPermissions() { - OSMWay way = new OSMWay(); - way.addTag("highway", "residential"); - var permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); - - way.addTag("access", "no"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allowsNothing()); - - way.addTag("motorcar", "private"); - way.addTag("bicycle", "private"); - way.addTag("foot", "private"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allowsNothing()); - - way.addTag("motorcar", "yes"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.CAR)); - - way.addTag("bicycle", "yes"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.BICYCLE_AND_CAR)); - - way.addTag("foot", "yes"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); - } - - /** - * Tests that motorcar/bicycle/foot private don't add permissions but no remove permission if - * access is yes - */ @Test - public void testMotorCarTagDeniedPermissions() { - OSMWay way = new OSMWay(); - way.addTag("highway", "residential"); - var permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); - - way.addTag("motorcar", "no"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE)); - - way.addTag("bicycle", "no"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.PEDESTRIAN)); - - way.addTag("foot", "no"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allowsNothing()); - //normal road with specific mode of transport private only is doubtful - /*way.addTag("motorcar", "private"); - way.addTag("bicycle", "private"); - way.addTag("foot", "private"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allowsNothing());*/ - } + void escalator() { + assertFalse(WayTestData.cycleway().isEscalator()); - /** - * Tests that motor_vehicle/bicycle/foot private don't add permissions but yes add permission if - * access is no - *

- * Support for motor_vehicle was added in #1881 - */ - @Test - public void testMotorVehicleTagAllowedPermissions() { - OSMWay way = new OSMWay(); - way.addTag("highway", "residential"); - var permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); - - way.addTag("access", "no"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allowsNothing()); - - way.addTag("motor_vehicle", "private"); - way.addTag("bicycle", "private"); - way.addTag("foot", "private"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allowsNothing()); - - way.addTag("motor_vehicle", "yes"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.CAR)); - - way.addTag("bicycle", "yes"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.BICYCLE_AND_CAR)); - - way.addTag("foot", "yes"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); - } - - /** - * Tests that motor_vehicle/bicycle/foot private don't add permissions but no remove permission if - * access is yes - *

- * Support for motor_vehicle was added in #1881 - */ - @Test - public void testMotorVehicleTagDeniedPermissions() { - OSMWay way = new OSMWay(); - way.addTag("highway", "residential"); - var permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); - - way.addTag("motor_vehicle", "no"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE)); - - way.addTag("bicycle", "no"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allows(StreetTraversalPermission.PEDESTRIAN)); - - way.addTag("foot", "no"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allowsNothing()); - //normal road with specific mode of transport private only is doubtful - /*way.addTag("motor_vehicle", "private"); - way.addTag("bicycle", "private"); - way.addTag("foot", "private"); - permissionPair = getWayProperties(way); - assertTrue(permissionPair.main().allowsNothing());*/ - } - - @Test - public void testSidepathPermissions() { - OSMWay way = new OSMWay(); - way.addTag("bicycle", "use_sidepath"); - way.addTag("highway", "primary"); - way.addTag("lanes", "2"); - way.addTag("maxspeed", "70"); - way.addTag("oneway", "yes"); - var permissionPair = getWayProperties(way); - - assertFalse(permissionPair.main().allows(StreetTraversalPermission.BICYCLE)); - assertFalse(permissionPair.back().allows(StreetTraversalPermission.BICYCLE)); - - assertTrue(permissionPair.main().allows(StreetTraversalPermission.CAR)); - assertFalse(permissionPair.back().allows(StreetTraversalPermission.CAR)); - - way = new OSMWay(); - way.addTag("bicycle:forward", "use_sidepath"); - way.addTag("highway", "tertiary"); - permissionPair = getWayProperties(way); - - assertFalse(permissionPair.main().allows(StreetTraversalPermission.BICYCLE)); - assertTrue(permissionPair.back().allows(StreetTraversalPermission.BICYCLE)); - - assertTrue(permissionPair.main().allows(StreetTraversalPermission.CAR)); - assertTrue(permissionPair.back().allows(StreetTraversalPermission.CAR)); - - way = new OSMWay(); - way.addTag("bicycle:backward", "use_sidepath"); - way.addTag("highway", "tertiary"); - permissionPair = getWayProperties(way); - - assertTrue(permissionPair.main().allows(StreetTraversalPermission.BICYCLE)); - assertFalse(permissionPair.back().allows(StreetTraversalPermission.BICYCLE)); - - assertTrue(permissionPair.main().allows(StreetTraversalPermission.CAR)); - assertTrue(permissionPair.back().allows(StreetTraversalPermission.CAR)); - - way = new OSMWay(); - way.addTag("highway", "tertiary"); - way.addTag("oneway", "yes"); - way.addTag("oneway:bicycle", "no"); - permissionPair = getWayProperties(way); - - assertTrue(permissionPair.main().allows(StreetTraversalPermission.BICYCLE)); - assertTrue(permissionPair.back().allows(StreetTraversalPermission.BICYCLE)); - - assertTrue(permissionPair.main().allows(StreetTraversalPermission.CAR)); - assertFalse(permissionPair.back().allows(StreetTraversalPermission.CAR)); - - way.addTag("bicycle:forward", "use_sidepath"); - permissionPair = getWayProperties(way); - assertFalse(permissionPair.main().allows(StreetTraversalPermission.BICYCLE)); - assertTrue(permissionPair.back().allows(StreetTraversalPermission.BICYCLE)); - - assertTrue(permissionPair.main().allows(StreetTraversalPermission.CAR)); - assertFalse(permissionPair.back().allows(StreetTraversalPermission.CAR)); - } + var escalator = new OSMWay(); + escalator.addTag("highway", "steps"); + assertFalse(escalator.isEscalator()); - private StreetTraversalPermissionPair getWayProperties(OSMWay way) { - WayPropertySet wayPropertySet = new WayPropertySet(); - WayProperties wayData = wayPropertySet.getDataForWay(way); + escalator.addTag("conveying", "yes"); + assertTrue(escalator.isEscalator()); - StreetTraversalPermission permissions = OsmFilter.getPermissionsForWay( - way, - wayData.getPermission() - ); - return OsmFilter.getPermissions(permissions, way); + escalator.addTag("conveying", "whoknows?"); + assertFalse(escalator.isEscalator()); } } diff --git a/src/test/java/org/opentripplanner/openstreetmap/model/OSMWithTagsTest.java b/src/test/java/org/opentripplanner/openstreetmap/model/OSMWithTagsTest.java index d2f6b4d3196..ca5db77df12 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/model/OSMWithTagsTest.java +++ b/src/test/java/org/opentripplanner/openstreetmap/model/OSMWithTagsTest.java @@ -10,11 +10,12 @@ import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Test; +import org.opentripplanner.openstreetmap.wayproperty.specifier.WayTestData; public class OSMWithTagsTest { @Test - public void testHasTag() { + void testHasTag() { OSMWithTags o = new OSMWithTags(); assertFalse(o.hasTag("foo")); assertFalse(o.hasTag("FOO")); @@ -25,7 +26,7 @@ public void testHasTag() { } @Test - public void testGetTag() { + void testGetTag() { OSMWithTags o = new OSMWithTags(); assertNull(o.getTag("foo")); assertNull(o.getTag("FOO")); @@ -36,7 +37,7 @@ public void testGetTag() { } @Test - public void testIsFalse() { + void testIsFalse() { assertTrue(OSMWithTags.isFalse("no")); assertTrue(OSMWithTags.isFalse("0")); assertTrue(OSMWithTags.isFalse("false")); @@ -50,7 +51,7 @@ public void testIsFalse() { } @Test - public void testIsTrue() { + void testIsTrue() { assertTrue(OSMWithTags.isTrue("yes")); assertTrue(OSMWithTags.isTrue("1")); assertTrue(OSMWithTags.isTrue("true")); @@ -64,7 +65,7 @@ public void testIsTrue() { } @Test - public void testIsTagFalseOrTrue() { + void testIsTagFalseOrTrue() { OSMWithTags o = new OSMWithTags(); assertFalse(o.isTagFalse("foo")); assertFalse(o.isTagFalse("FOO")); @@ -85,7 +86,18 @@ public void testIsTagFalseOrTrue() { } @Test - public void testDoesAllowTagAccess() { + void isTag() { + var name = "Brendan"; + var osm = new OSMWithTags(); + osm.addTag("NAME", name); + + assertTrue(osm.isTag("name", name)); + assertTrue(osm.isTag("NAME", name)); + assertFalse(osm.isTag("NAMEE", name)); + } + + @Test + void testDoesAllowTagAccess() { OSMWithTags o = new OSMWithTags(); assertFalse(o.doesTagAllowAccess("foo")); @@ -100,7 +112,7 @@ public void testDoesAllowTagAccess() { } @Test - public void testIsGeneralAccessDenied() { + void testIsGeneralAccessDenied() { OSMWithTags o = new OSMWithTags(); assertFalse(o.isGeneralAccessDenied()); @@ -115,7 +127,7 @@ public void testIsGeneralAccessDenied() { } @Test - public void testBicycleDenied() { + void testBicycleDenied() { OSMWithTags tags = new OSMWithTags(); assertFalse(tags.isBicycleExplicitlyDenied()); @@ -131,7 +143,7 @@ public void testBicycleDenied() { } @Test - public void getReferenceTags() { + void getReferenceTags() { var osm = new OSMWithTags(); osm.addTag("ref", "A"); @@ -140,7 +152,7 @@ public void getReferenceTags() { } @Test - public void getEmptyRefList() { + void getEmptyRefList() { var osm = new OSMWithTags(); osm.addTag("ref", "A"); @@ -148,7 +160,7 @@ public void getEmptyRefList() { } @Test - public void ignoreRefCase() { + void ignoreRefCase() { var osm = new OSMWithTags(); osm.addTag("ref:IFOPT", "A"); @@ -156,7 +168,7 @@ public void ignoreRefCase() { } @Test - public void readSemicolonSeparated() { + void readSemicolonSeparated() { var osm = new OSMWithTags(); osm.addTag("ref:A", "A;A;B"); @@ -164,7 +176,7 @@ public void readSemicolonSeparated() { } @Test - public void removeBlankRef() { + void removeBlankRef() { var osm = new OSMWithTags(); osm.addTag("ref1", " "); osm.addTag("ref2", ""); @@ -174,7 +186,7 @@ public void removeBlankRef() { } @Test - public void shouldNotReturnNull() { + void shouldNotReturnNull() { var osm = new OSMWithTags(); osm.addTag("ref1", " "); osm.addTag("ref2", ""); @@ -184,7 +196,31 @@ public void shouldNotReturnNull() { } @Test - public void testGenerateI18NForPattern() { + void isWheelchairAccessible() { + var osm1 = new OSMWithTags(); + assertTrue(osm1.isWheelchairAccessible()); + + var osm2 = new OSMWithTags(); + osm2.addTag("wheelchair", "no"); + assertFalse(osm2.isWheelchairAccessible()); + + var osm3 = new OSMWithTags(); + osm3.addTag("wheelchair", "yes"); + assertTrue(osm3.isWheelchairAccessible()); + } + + @Test + void isRoutable() { + assertFalse(WayTestData.zooPlatform().isRoutable()); + } + + @Test + void isPlatform() { + assertFalse(WayTestData.zooPlatform().isPlatform()); + } + + @Test + void testGenerateI18NForPattern() { OSMWithTags osmTags = new OSMWithTags(); osmTags.addTag("note", "Note EN"); osmTags.addTag("description:fr", "Description FR"); diff --git a/src/test/java/org/opentripplanner/openstreetmap/tagmapping/DefaultMapperTest.java b/src/test/java/org/opentripplanner/openstreetmap/tagmapping/DefaultMapperTest.java index cdb4e5518ac..cbaccb87f84 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/tagmapping/DefaultMapperTest.java +++ b/src/test/java/org/opentripplanner/openstreetmap/tagmapping/DefaultMapperTest.java @@ -2,8 +2,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.street.model.StreetTraversalPermission.ALL; import static org.opentripplanner.street.model.StreetTraversalPermission.PEDESTRIAN; +import static org.opentripplanner.street.model.StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.opentripplanner.openstreetmap.model.OSMWithTags; import org.opentripplanner.openstreetmap.wayproperty.SpeedPicker; @@ -13,12 +16,15 @@ public class DefaultMapperTest { - static WayPropertySet wps = new WayPropertySet(); + private WayPropertySet wps; float epsilon = 0.01f; - static { + @BeforeEach + public void setup() { + var wps = new WayPropertySet(); DefaultMapper source = new DefaultMapper(); source.populateProperties(wps); + this.wps = wps; } /** @@ -111,6 +117,32 @@ void stairs() { assertEquals(PEDESTRIAN, props.getPermission()); } + @Test + void footDiscouraged() { + var regular = WayTestData.pedestrianTunnel(); + var props = wps.getDataForWay(regular); + assertEquals(PEDESTRIAN_AND_BICYCLE, props.getPermission()); + assertEquals(1, props.walkSafety().forward()); + + var discouraged = WayTestData.pedestrianTunnel().addTag("foot", "discouraged"); + var discouragedProps = wps.getDataForWay(discouraged); + assertEquals(PEDESTRIAN_AND_BICYCLE, discouragedProps.getPermission()); + assertEquals(3, discouragedProps.walkSafety().forward()); + } + + @Test + void bicycleDiscouraged() { + var regular = WayTestData.southeastLaBonitaWay(); + var props = wps.getDataForWay(regular); + assertEquals(ALL, props.getPermission()); + assertEquals(.98, props.bicycleSafety().forward()); + + var discouraged = WayTestData.southeastLaBonitaWay().addTag("bicycle", "discouraged"); + var discouragedProps = wps.getDataForWay(discouraged); + assertEquals(ALL, discouragedProps.getPermission()); + assertEquals(2.94, discouragedProps.bicycleSafety().forward(), epsilon); + } + /** * Test that two values are within epsilon of each other. */ diff --git a/src/test/java/org/opentripplanner/openstreetmap/tagmapping/FinlandMapperTest.java b/src/test/java/org/opentripplanner/openstreetmap/tagmapping/FinlandMapperTest.java index 088acbb3b23..a2f84873f20 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/tagmapping/FinlandMapperTest.java +++ b/src/test/java/org/opentripplanner/openstreetmap/tagmapping/FinlandMapperTest.java @@ -1,9 +1,12 @@ package org.opentripplanner.openstreetmap.tagmapping; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.street.model.StreetTraversalPermission.NONE; import org.junit.jupiter.api.Test; +import org.opentripplanner.openstreetmap.model.OSMWay; import org.opentripplanner.openstreetmap.model.OSMWithTags; +import org.opentripplanner.openstreetmap.wayproperty.WayProperties; import org.opentripplanner.openstreetmap.wayproperty.WayPropertySet; public class FinlandMapperTest { @@ -79,79 +82,53 @@ public void testSafety() { cyclewaySegregatedFootwayCrossingWithTrafficLights.addTag("segregated", "yes"); cyclewaySegregatedFootwayCrossingWithTrafficLights.addTag("highway", "cycleway"); cyclewaySegregatedFootwayCrossingWithTrafficLights.addTag("crossing", "traffic_signals"); - assertEquals(2.06, wps.getDataForWay(primaryWay).getBicycleSafetyFeatures().forward(), epsilon); + assertEquals(2.06, wps.getDataForWay(primaryWay).bicycleSafety().forward(), epsilon); // way with high speed limit, has higher walk safety factor - assertEquals(1.8, wps.getDataForWay(primaryWay).getWalkSafetyFeatures().forward(), epsilon); - assertEquals(1.8, wps.getDataForWay(primaryWay).getWalkSafetyFeatures().back(), epsilon); + assertEquals(1.8, wps.getDataForWay(primaryWay).walkSafety().forward(), epsilon); + assertEquals(1.8, wps.getDataForWay(primaryWay).walkSafety().back(), epsilon); // way with low speed limit, has lower walk safety factor - assertEquals( - 1.45, - wps.getDataForWay(livingStreetWay).getWalkSafetyFeatures().forward(), - epsilon - ); - assertEquals(1.1, wps.getDataForWay(footway).getWalkSafetyFeatures().forward(), epsilon); - assertEquals(1.1, wps.getDataForWay(sidewalk).getWalkSafetyFeatures().forward(), epsilon); - assertEquals( - 1.1, - wps.getDataForWay(segregatedCycleway).getWalkSafetyFeatures().forward(), - epsilon - ); - assertEquals(1.0, wps.getDataForWay(tunnel).getWalkSafetyFeatures().forward(), epsilon); - assertEquals(1.0, wps.getDataForWay(bridge).getWalkSafetyFeatures().forward(), epsilon); - assertEquals( - 1.2, - wps.getDataForWay(footwayCrossing).getWalkSafetyFeatures().forward(), - epsilon - ); + assertEquals(1.45, wps.getDataForWay(livingStreetWay).walkSafety().forward(), epsilon); + assertEquals(1.1, wps.getDataForWay(footway).walkSafety().forward(), epsilon); + assertEquals(1.1, wps.getDataForWay(sidewalk).walkSafety().forward(), epsilon); + assertEquals(1.1, wps.getDataForWay(segregatedCycleway).walkSafety().forward(), epsilon); + assertEquals(1.0, wps.getDataForWay(tunnel).walkSafety().forward(), epsilon); + assertEquals(1.0, wps.getDataForWay(bridge).walkSafety().forward(), epsilon); + assertEquals(1.2, wps.getDataForWay(footwayCrossing).walkSafety().forward(), epsilon); assertEquals( 1.1, - wps.getDataForWay(footwayCrossingWithTrafficLights).getWalkSafetyFeatures().forward(), - epsilon - ); - assertEquals( - 1.25, - wps.getDataForWay(cyclewayCrossing).getWalkSafetyFeatures().forward(), - epsilon - ); - assertEquals( - 1.25, - wps.getDataForWay(cyclewayFootwayCrossing).getWalkSafetyFeatures().forward(), + wps.getDataForWay(footwayCrossingWithTrafficLights).walkSafety().forward(), epsilon ); + assertEquals(1.25, wps.getDataForWay(cyclewayCrossing).walkSafety().forward(), epsilon); + assertEquals(1.25, wps.getDataForWay(cyclewayFootwayCrossing).walkSafety().forward(), epsilon); assertEquals( 1.15, - wps.getDataForWay(cyclewayCrossingWithTrafficLights).getWalkSafetyFeatures().forward(), + wps.getDataForWay(cyclewayCrossingWithTrafficLights).walkSafety().forward(), epsilon ); assertEquals( 1.15, - wps.getDataForWay(cyclewayFootwayCrossingWithTrafficLights).getWalkSafetyFeatures().forward(), + wps.getDataForWay(cyclewayFootwayCrossingWithTrafficLights).walkSafety().forward(), epsilon ); assertEquals( 1.2, - wps.getDataForWay(cyclewaySegregatedCrossing).getWalkSafetyFeatures().forward(), + wps.getDataForWay(cyclewaySegregatedCrossing).walkSafety().forward(), epsilon ); assertEquals( 1.2, - wps.getDataForWay(cyclewaySegregatedFootwayCrossing).getWalkSafetyFeatures().forward(), + wps.getDataForWay(cyclewaySegregatedFootwayCrossing).walkSafety().forward(), epsilon ); assertEquals( 1.1, - wps - .getDataForWay(cyclewaySegregatedCrossingWithTrafficLights) - .getWalkSafetyFeatures() - .forward(), + wps.getDataForWay(cyclewaySegregatedCrossingWithTrafficLights).walkSafety().forward(), epsilon ); assertEquals( 1.1, - wps - .getDataForWay(cyclewaySegregatedFootwayCrossingWithTrafficLights) - .getWalkSafetyFeatures() - .forward(), + wps.getDataForWay(cyclewaySegregatedFootwayCrossingWithTrafficLights).walkSafety().forward(), epsilon ); } @@ -164,13 +141,9 @@ public void testSafetyWithMixins() { // surface has mixin bicycle safety of 1.3 but no walk safety wayWithMixins.addTag("surface", "metal"); // 1.0 * 1.3 = 1.3 - assertEquals( - 1.3, - wps.getDataForWay(wayWithMixins).getBicycleSafetyFeatures().forward(), - epsilon - ); + assertEquals(1.3, wps.getDataForWay(wayWithMixins).bicycleSafety().forward(), epsilon); // 1.6 is the default walk safety for a way with ALL permissions and speed limit > 35 and <= 60 kph - assertEquals(1.6, wps.getDataForWay(wayWithMixins).getWalkSafetyFeatures().forward(), epsilon); + assertEquals(1.6, wps.getDataForWay(wayWithMixins).walkSafety().forward(), epsilon); OSMWithTags wayWithMixinsAndCustomSafety = new OSMWithTags(); // highway=service has custom bicycle safety of 1.1 but no custom walk safety @@ -180,14 +153,38 @@ public void testSafetyWithMixins() { // 1.1 * 1.3 = 1.43 assertEquals( 1.43, - wps.getDataForWay(wayWithMixinsAndCustomSafety).getBicycleSafetyFeatures().forward(), + wps.getDataForWay(wayWithMixinsAndCustomSafety).bicycleSafety().forward(), epsilon ); // 1.6 is the default walk safety for a way with ALL permissions and speed limit <= 35 kph assertEquals( 1.45, - wps.getDataForWay(wayWithMixinsAndCustomSafety).getWalkSafetyFeatures().forward(), + wps.getDataForWay(wayWithMixinsAndCustomSafety).walkSafety().forward(), epsilon ); } + + @Test + public void testTagMapping() { + OSMWithTags way; + WayProperties wayData; + + way = new OSMWay(); + way.addTag("highway", "unclassified"); + way.addTag("seasonal", "winter"); + wayData = wps.getDataForWay(way); + assertEquals(wayData.getPermission(), NONE); + + way = new OSMWay(); + way.addTag("highway", "trunk"); + way.addTag("ice_road", "yes"); + wayData = wps.getDataForWay(way); + assertEquals(wayData.getPermission(), NONE); + + way = new OSMWay(); + way.addTag("highway", "track"); + way.addTag("winter_road", "yes"); + wayData = wps.getDataForWay(way); + assertEquals(wayData.getPermission(), NONE); + } } diff --git a/src/test/java/org/opentripplanner/openstreetmap/tagmapping/GermanyMapperTest.java b/src/test/java/org/opentripplanner/openstreetmap/tagmapping/GermanyMapperTest.java index 2fc0e699344..00b1049bdda 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/tagmapping/GermanyMapperTest.java +++ b/src/test/java/org/opentripplanner/openstreetmap/tagmapping/GermanyMapperTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.opentripplanner.openstreetmap.model.OSMWithTags; import org.opentripplanner.openstreetmap.wayproperty.WayPropertySet; @@ -20,70 +21,83 @@ public class GermanyMapperTest { /** * Test that bike safety factors are calculated accurately */ - @Test - public void testBikeSafety() { - OSMWithTags way; - - // way 361961158 - way = new OSMWithTags(); - way.addTag("bicycle", "yes"); - way.addTag("foot", "designated"); - way.addTag("footway", "sidewalk"); - way.addTag("highway", "footway"); - way.addTag("lit", "yes"); - way.addTag("oneway", "no"); - way.addTag("traffic_sign", "DE:239,1022-10"); - assertEquals(1.2, wps.getDataForWay(way).getBicycleSafetyFeatures().forward(), epsilon); - - way = new OSMWithTags(); - way.addTag("cycleway", "opposite"); - way.addTag("highway", "residential"); - way.addTag("lit", "yes"); - way.addTag("maxspeed", "30"); - way.addTag("name", "Freibadstraße"); - way.addTag("oneway", "yes"); - way.addTag("oneway:bicycle", "no"); - way.addTag("parking:lane:left", "parallel"); - way.addTag("parking:lane:right", "no_parking"); - way.addTag("sidewalk", "both"); - way.addTag("source:maxspeed", "DE:zone:30"); - way.addTag("surface", "asphalt"); - way.addTag("width", "6.5"); - way.addTag("zone:traffic", "DE:urban"); - assertEquals(0.9, wps.getDataForWay(way).getBicycleSafetyFeatures().forward(), epsilon); - // walk safety should be default - assertEquals(1, wps.getDataForWay(way).getWalkSafetyFeatures().forward(), epsilon); - - // way332589799 (Radschnellweg BW1) - way = new OSMWithTags(); - way.addTag("bicycle", "designated"); - way.addTag("class:bicycle", "2"); - way.addTag("class:bicycle:roadcycling", "1"); - way.addTag("highway", "track"); - way.addTag("horse", "forestry"); - way.addTag("lcn", "yes"); - way.addTag("lit", "yes"); - way.addTag("maxspeed", "30"); - way.addTag("motor_vehicle", "forestry"); - way.addTag("name", "Römerstraße"); - way.addTag("smoothness", "excellent"); - way.addTag("source:maxspeed", "sign"); - way.addTag("surface", "asphalt"); - way.addTag("tracktype", "grade1"); - assertEquals(0.693, wps.getDataForWay(way).getBicycleSafetyFeatures().forward(), epsilon); - - way = new OSMWithTags(); - way.addTag("highway", "track"); - way.addTag("motor_vehicle", "agricultural"); - way.addTag("surface", "asphalt"); - way.addTag("tracktype", "grade1"); - way.addTag("traffic_sign", "DE:260,1026-36"); - way.addTag("width", "2.5"); - assertEquals(1.0, wps.getDataForWay(way).getBicycleSafetyFeatures().forward(), epsilon); + @Nested + class BikeSafety { + + @Test + void testBikeSafety() { + OSMWithTags way; + + // way 361961158 + way = new OSMWithTags(); + way.addTag("bicycle", "yes"); + way.addTag("foot", "designated"); + way.addTag("footway", "sidewalk"); + way.addTag("highway", "footway"); + way.addTag("lit", "yes"); + way.addTag("oneway", "no"); + way.addTag("traffic_sign", "DE:239,1022-10"); + assertEquals(1.2, wps.getDataForWay(way).bicycleSafety().forward(), epsilon); + } + + @Test + void cyclewayOpposite() { + var way = new OSMWithTags(); + way.addTag("cycleway", "opposite"); + way.addTag("highway", "residential"); + way.addTag("lit", "yes"); + way.addTag("maxspeed", "30"); + way.addTag("name", "Freibadstraße"); + way.addTag("oneway", "yes"); + way.addTag("oneway:bicycle", "no"); + way.addTag("parking:lane:left", "parallel"); + way.addTag("parking:lane:right", "no_parking"); + way.addTag("sidewalk", "both"); + way.addTag("source:maxspeed", "DE:zone:30"); + way.addTag("surface", "asphalt"); + way.addTag("width", "6.5"); + way.addTag("zone:traffic", "DE:urban"); + assertEquals(0.9, wps.getDataForWay(way).bicycleSafety().forward(), epsilon); + // walk safety should be default + assertEquals(1, wps.getDataForWay(way).walkSafety().forward(), epsilon); + } + + @Test + void bikePath() { + // way332589799 (Radschnellweg BW1) + var way = new OSMWithTags(); + way.addTag("bicycle", "designated"); + way.addTag("class:bicycle", "2"); + way.addTag("class:bicycle:roadcycling", "1"); + way.addTag("highway", "track"); + way.addTag("horse", "forestry"); + way.addTag("lcn", "yes"); + way.addTag("lit", "yes"); + way.addTag("maxspeed", "30"); + way.addTag("motor_vehicle", "forestry"); + way.addTag("name", "Römerstraße"); + way.addTag("smoothness", "excellent"); + way.addTag("source:maxspeed", "sign"); + way.addTag("surface", "asphalt"); + way.addTag("tracktype", "grade1"); + assertEquals(0.693, wps.getDataForWay(way).bicycleSafety().forward(), epsilon); + } + + @Test + void track() { + var way = new OSMWithTags(); + way.addTag("highway", "track"); + way.addTag("motor_vehicle", "agricultural"); + way.addTag("surface", "asphalt"); + way.addTag("tracktype", "grade1"); + way.addTag("traffic_sign", "DE:260,1026-36"); + way.addTag("width", "2.5"); + assertEquals(1.0, wps.getDataForWay(way).bicycleSafety().forward(), epsilon); + } } @Test - public void testPermissions() { + void testPermissions() { // https://www.openstreetmap.org/way/124263424 var way = new OSMWithTags(); way.addTag("highway", "track"); @@ -120,99 +134,95 @@ public void testPermissions() { assertEquals(wps.getDataForWay(way).getPermission(), StreetTraversalPermission.ALL); } - @Test - public void lcnAndRcnShouldNotBeAddedUp() { - // https://www.openstreetmap.org/way/26443041 is part of both an lcn and rcn but that shouldn't mean that - // it is to be more heavily favoured than other ways that are part of just one. - - var both = new OSMWithTags(); - both.addTag("highway", "residential"); - both.addTag("rcn", "yes"); - both.addTag("lcn", "yes"); - - var justLcn = new OSMWithTags(); - justLcn.addTag("lcn", "yes"); - justLcn.addTag("highway", "residential"); - - var residential = new OSMWithTags(); - residential.addTag("highway", "residential"); - - assertEquals( - wps.getDataForWay(both).getBicycleSafetyFeatures().forward(), - wps.getDataForWay(justLcn).getBicycleSafetyFeatures().forward(), - epsilon - ); - - assertEquals(wps.getDataForWay(both).getBicycleSafetyFeatures().forward(), 0.6859, epsilon); - - assertEquals( - wps.getDataForWay(residential).getBicycleSafetyFeatures().forward(), - 0.98, - epsilon - ); - } - - @Test - public void bicycleRoadAndLcnShouldNotBeAddedUp() { - // https://www.openstreetmap.org/way/22201321 was tagged as bicycle_road without lcn - // make it so all ways tagged as some kind of cyclestreets are considered as equally safe - - var both = new OSMWithTags(); - both.addTag("highway", "residential"); - both.addTag("bicycle_road", "yes"); - both.addTag("cyclestreet", "yes"); - both.addTag("lcn", "yes"); - - var justBicycleRoad = new OSMWithTags(); - justBicycleRoad.addTag("bicycle_road", "yes"); - justBicycleRoad.addTag("highway", "residential"); - - var justCyclestreet = new OSMWithTags(); - justCyclestreet.addTag("cyclestreet", "yes"); - justCyclestreet.addTag("highway", "residential"); - - var justLcn = new OSMWithTags(); - justLcn.addTag("lcn", "yes"); - justLcn.addTag("highway", "residential"); - - var residential = new OSMWithTags(); - residential.addTag("highway", "residential"); - - assertEquals( - wps.getDataForWay(justCyclestreet).getBicycleSafetyFeatures().forward(), - wps.getDataForWay(justLcn).getBicycleSafetyFeatures().forward(), - epsilon - ); - - assertEquals( - wps.getDataForWay(both).getBicycleSafetyFeatures().forward(), - wps.getDataForWay(justBicycleRoad).getBicycleSafetyFeatures().forward(), - epsilon - ); - - assertEquals( - wps.getDataForWay(both).getBicycleSafetyFeatures().forward(), - wps.getDataForWay(justCyclestreet).getBicycleSafetyFeatures().forward(), - epsilon - ); - - assertEquals( - wps.getDataForWay(both).getBicycleSafetyFeatures().forward(), - wps.getDataForWay(justLcn).getBicycleSafetyFeatures().forward(), - epsilon - ); - - assertEquals(wps.getDataForWay(both).getBicycleSafetyFeatures().forward(), 0.6859, epsilon); - - assertEquals( - wps.getDataForWay(residential).getBicycleSafetyFeatures().forward(), - 0.98, - epsilon - ); + @Nested + class BikeRouteNetworks { + + @Test + void lcnAndRcnShouldNotBeAddedUp() { + // https://www.openstreetmap.org/way/26443041 is part of both an lcn and rcn but that shouldn't mean that + // it is to be more heavily favoured than other ways that are part of just one. + + var both = new OSMWithTags(); + both.addTag("highway", "residential"); + both.addTag("rcn", "yes"); + both.addTag("lcn", "yes"); + + var justLcn = new OSMWithTags(); + justLcn.addTag("lcn", "yes"); + justLcn.addTag("highway", "residential"); + + var residential = new OSMWithTags(); + residential.addTag("highway", "residential"); + + assertEquals( + wps.getDataForWay(both).bicycleSafety().forward(), + wps.getDataForWay(justLcn).bicycleSafety().forward(), + epsilon + ); + + assertEquals(wps.getDataForWay(both).bicycleSafety().forward(), 0.6859, epsilon); + + assertEquals(wps.getDataForWay(residential).bicycleSafety().forward(), 0.98, epsilon); + } + + @Test + void bicycleRoadAndLcnShouldNotBeAddedUp() { + // https://www.openstreetmap.org/way/22201321 was tagged as bicycle_road without lcn + // make it so all ways tagged as some kind of cyclestreets are considered as equally safe + + var both = new OSMWithTags(); + both.addTag("highway", "residential"); + both.addTag("bicycle_road", "yes"); + both.addTag("cyclestreet", "yes"); + both.addTag("lcn", "yes"); + + var justBicycleRoad = new OSMWithTags(); + justBicycleRoad.addTag("bicycle_road", "yes"); + justBicycleRoad.addTag("highway", "residential"); + + var justCyclestreet = new OSMWithTags(); + justCyclestreet.addTag("cyclestreet", "yes"); + justCyclestreet.addTag("highway", "residential"); + + var justLcn = new OSMWithTags(); + justLcn.addTag("lcn", "yes"); + justLcn.addTag("highway", "residential"); + + var residential = new OSMWithTags(); + residential.addTag("highway", "residential"); + + assertEquals( + wps.getDataForWay(justCyclestreet).bicycleSafety().forward(), + wps.getDataForWay(justLcn).bicycleSafety().forward(), + epsilon + ); + + assertEquals( + wps.getDataForWay(both).bicycleSafety().forward(), + wps.getDataForWay(justBicycleRoad).bicycleSafety().forward(), + epsilon + ); + + assertEquals( + wps.getDataForWay(both).bicycleSafety().forward(), + wps.getDataForWay(justCyclestreet).bicycleSafety().forward(), + epsilon + ); + + assertEquals( + wps.getDataForWay(both).bicycleSafety().forward(), + wps.getDataForWay(justLcn).bicycleSafety().forward(), + epsilon + ); + + assertEquals(wps.getDataForWay(both).bicycleSafety().forward(), 0.6859, epsilon); + + assertEquals(wps.getDataForWay(residential).bicycleSafety().forward(), 0.98, epsilon); + } } @Test - public void setCorrectPermissionsForRoundabouts() { + void setCorrectPermissionsForRoundabouts() { // https://www.openstreetmap.org/way/184185551 var residential = new OSMWithTags(); residential.addTag("highway", "residential"); @@ -230,7 +240,7 @@ public void setCorrectPermissionsForRoundabouts() { } @Test - public void setCorrectBikeSafetyValuesForBothDirections() { + void setCorrectBikeSafetyValuesForBothDirections() { // https://www.openstreetmap.org/way/13420871 var residential = new OSMWithTags(); residential.addTag("highway", "residential"); @@ -239,14 +249,14 @@ public void setCorrectBikeSafetyValuesForBothDirections() { residential.addTag("name", "Auf der Heide"); residential.addTag("surface", "asphalt"); assertEquals( - wps.getDataForWay(residential).getBicycleSafetyFeatures().forward(), - wps.getDataForWay(residential).getBicycleSafetyFeatures().back(), + wps.getDataForWay(residential).bicycleSafety().forward(), + wps.getDataForWay(residential).bicycleSafety().back(), epsilon ); } @Test - public void setCorrectPermissionsForSteps() { + void setCorrectPermissionsForSteps() { // https://www.openstreetmap.org/way/64359102 var steps = new OSMWithTags(); steps.addTag("highway", "steps"); @@ -254,7 +264,7 @@ public void setCorrectPermissionsForSteps() { } @Test - public void testGermanAutobahnSpeed() { + void testGermanAutobahnSpeed() { // https://www.openstreetmap.org/way/10879847 var alzentalstr = new OSMWithTags(); alzentalstr.addTag("highway", "residential"); diff --git a/src/test/java/org/opentripplanner/openstreetmap/tagmapping/NorwayMapperTest.java b/src/test/java/org/opentripplanner/openstreetmap/tagmapping/NorwayMapperTest.java index d659c89a187..cce2bf85cb4 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/tagmapping/NorwayMapperTest.java +++ b/src/test/java/org/opentripplanner/openstreetmap/tagmapping/NorwayMapperTest.java @@ -93,7 +93,7 @@ static List createLinkRoadLikeMainCases() { @ParameterizedTest(name = "{0} should have a score of {1}") @MethodSource("createExpectedBicycleSafetyForMaxspeedCases") public void testBicycleSafetyForMaxspeed(OSMWithTags way, Double expected) { - var result = wps.getDataForWay(way).getBicycleSafetyFeatures(); + var result = wps.getDataForWay(way).bicycleSafety(); var expectedSafetyFeatures = new SafetyFeatures(expected, expected); assertEquals(expectedSafetyFeatures, result); } @@ -101,7 +101,7 @@ public void testBicycleSafetyForMaxspeed(OSMWithTags way, Double expected) { @ParameterizedTest @MethodSource("createBicycleSafetyWithoutExplicitMaxspeed") public void testBicycleSafetyWithoutMaxspeed(OSMWithTags way, Double expected) { - var result = wps.getDataForWay(way).getBicycleSafetyFeatures(); + var result = wps.getDataForWay(way).bicycleSafety(); var expectedSafetyFeatures = new SafetyFeatures(expected, expected); assertEquals(expectedSafetyFeatures, result); } @@ -109,8 +109,8 @@ public void testBicycleSafetyWithoutMaxspeed(OSMWithTags way, Double expected) { @ParameterizedTest @MethodSource("createLinkRoadLikeMainCases") public void testBicycleSafetyLikeLinkRoad(OSMWithTags mainRoad, OSMWithTags linkRoad) { - var resultMain = wps.getDataForWay(mainRoad).getBicycleSafetyFeatures(); - var resultLink = wps.getDataForWay(linkRoad).getBicycleSafetyFeatures(); + var resultMain = wps.getDataForWay(mainRoad).bicycleSafety(); + var resultLink = wps.getDataForWay(linkRoad).bicycleSafety(); assertEquals(resultMain, resultLink); } diff --git a/src/test/java/org/opentripplanner/openstreetmap/tagmapping/OsmTagMapperTest.java b/src/test/java/org/opentripplanner/openstreetmap/tagmapping/OsmTagMapperTest.java index d3a947b217c..164faa644c2 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/tagmapping/OsmTagMapperTest.java +++ b/src/test/java/org/opentripplanner/openstreetmap/tagmapping/OsmTagMapperTest.java @@ -82,15 +82,16 @@ public void mixin() { var withoutFoo = new OSMWithTags(); withoutFoo.addTag("tag", "imaginary"); - assertEquals(2, wps.getDataForWay(withoutFoo).getBicycleSafetyFeatures().back()); + assertEquals(2, wps.getDataForWay(withoutFoo).bicycleSafety().back()); // the mixin for foo=bar reduces the bike safety factor var withFoo = new OSMWithTags(); withFoo.addTag("tag", "imaginary"); withFoo.addTag("foo", "bar"); - assertEquals(1, wps.getDataForWay(withFoo).getBicycleSafetyFeatures().back()); + assertEquals(1, wps.getDataForWay(withFoo).bicycleSafety().back()); } + @Test public void testAccessNo() { OSMWithTags tags = new OSMWithTags(); OsmTagMapper osmTagMapper = new DefaultMapper(); diff --git a/src/test/java/org/opentripplanner/openstreetmap/tagmapping/PortlandMapperTest.java b/src/test/java/org/opentripplanner/openstreetmap/tagmapping/PortlandMapperTest.java index 3abf5f73dc7..06369baddc8 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/tagmapping/PortlandMapperTest.java +++ b/src/test/java/org/opentripplanner/openstreetmap/tagmapping/PortlandMapperTest.java @@ -53,7 +53,7 @@ public class PortlandMapperTest { void walkSafety(OSMWithTags way, double expected) { var score = wps.getDataForWay(way); - var ws = score.getWalkSafetyFeatures(); + var ws = score.walkSafety(); assertEquals(expected, ws.forward(), delta); assertEquals(expected, ws.back(), delta); } diff --git a/src/test/java/org/opentripplanner/openstreetmap/wayproperty/WayPropertySetTest.java b/src/test/java/org/opentripplanner/openstreetmap/wayproperty/WayPropertySetTest.java index f26964efb60..c5ae8b86f7d 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/wayproperty/WayPropertySetTest.java +++ b/src/test/java/org/opentripplanner/openstreetmap/wayproperty/WayPropertySetTest.java @@ -1,57 +1,288 @@ package org.opentripplanner.openstreetmap.wayproperty; 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 static org.opentripplanner.openstreetmap.wayproperty.MixinPropertiesBuilder.ofBicycleSafety; import static org.opentripplanner.openstreetmap.wayproperty.WayPropertiesBuilder.withModes; import static org.opentripplanner.street.model.StreetTraversalPermission.CAR; import static org.opentripplanner.street.model.StreetTraversalPermission.NONE; import javax.annotation.Nonnull; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.opentripplanner.graph_builder.module.osm.StreetTraversalPermissionPair; +import org.opentripplanner.openstreetmap.model.OSMWay; import org.opentripplanner.openstreetmap.model.OSMWithTags; import org.opentripplanner.openstreetmap.tagmapping.OsmTagMapper; import org.opentripplanner.openstreetmap.wayproperty.specifier.ExactMatchSpecifier; import org.opentripplanner.openstreetmap.wayproperty.specifier.WayTestData; +import org.opentripplanner.street.model.StreetTraversalPermission; class WayPropertySetTest { - @Test - public void carTunnel() { - OSMWithTags tunnel = WayTestData.carTunnel(); - WayPropertySet wps = wps(); - assertEquals(CAR, wps.getDataForWay(tunnel).getPermission()); - } + @Nested + class ConditionSpecificity { - @Test - void pedestrianTunnelSpecificity() { - var tunnel = WayTestData.pedestrianTunnel(); - WayPropertySet wps = wps(); - assertEquals(NONE, wps.getDataForWay(tunnel).getPermission()); - } + @Test + public void carTunnel() { + OSMWithTags tunnel = WayTestData.carTunnel(); + WayPropertySet wps = wps(); + assertEquals(CAR, wps.getDataForWay(tunnel).getPermission()); + } + + @Test + void pedestrianTunnelSpecificity() { + var tunnel = WayTestData.pedestrianTunnel(); + WayPropertySet wps = wps(); + assertEquals(NONE, wps.getDataForWay(tunnel).getPermission()); + } - @Test - void mixinLeftSide() { - var cycleway = WayTestData.cyclewayLeft(); - WayPropertySet wps = wps(); - SafetyFeatures expected = new SafetyFeatures(1, 5); - assertEquals(expected, wps.getDataForWay(cycleway).getBicycleSafetyFeatures()); + @Test + void mixinLeftSide() { + var cycleway = WayTestData.cyclewayLeft(); + WayPropertySet wps = wps(); + SafetyFeatures expected = new SafetyFeatures(1, 5); + assertEquals(expected, wps.getDataForWay(cycleway).bicycleSafety()); + } + + @Nonnull + private static WayPropertySet wps() { + var wps = new WayPropertySet(); + var source = new OsmTagMapper() { + @Override + public void populateProperties(WayPropertySet props) { + props.setProperties("highway=primary", withModes(CAR)); + props.setProperties( + new ExactMatchSpecifier("highway=footway;layer=-1;tunnel=yes;indoor=yes"), + withModes(NONE) + ); + props.setMixinProperties("cycleway=lane", ofBicycleSafety(5)); + } + }; + source.populateProperties(wps); + return wps; + } } - @Nonnull - private static WayPropertySet wps() { - var wps = new WayPropertySet(); - var source = new OsmTagMapper() { - @Override - public void populateProperties(WayPropertySet props) { - props.setProperties("highway=primary", withModes(CAR)); - props.setProperties( - new ExactMatchSpecifier("highway=footway;layer=-1;tunnel=yes;indoor=yes"), - withModes(NONE) - ); - props.setMixinProperties("cycleway=lane", ofBicycleSafety(5)); - } - }; - source.populateProperties(wps); - return wps; + @Nested + class NoMapper { + + /** + * Tests if cars can drive on unclassified highways with bicycleDesignated + *

+ * Check for bug #1878 and PR #1880 + */ + @Test + void testCarPermission() { + OSMWay way = new OSMWay(); + way.addTag("highway", "unclassified"); + + var permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); + + way.addTag("bicycle", "designated"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); + } + + /** + * Tests that motorcar/bicycle/foot private don't add permissions but yes add permission if access + * is no + */ + @Test + void testMotorCarTagAllowedPermissions() { + OSMWay way = new OSMWay(); + way.addTag("highway", "residential"); + var permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); + + way.addTag("access", "no"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allowsNothing()); + + way.addTag("motorcar", "private"); + way.addTag("bicycle", "private"); + way.addTag("foot", "private"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allowsNothing()); + + way.addTag("motorcar", "yes"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.CAR)); + + way.addTag("bicycle", "yes"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.BICYCLE_AND_CAR)); + + way.addTag("foot", "yes"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); + } + + /** + * Tests that motorcar/bicycle/foot private don't add permissions but no remove permission if + * access is yes + */ + @Test + void testMotorCarTagDeniedPermissions() { + OSMWay way = new OSMWay(); + way.addTag("highway", "residential"); + var permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); + + way.addTag("motorcar", "no"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE)); + + way.addTag("bicycle", "no"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.PEDESTRIAN)); + + way.addTag("foot", "no"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allowsNothing()); + //normal road with specific mode of transport private only is doubtful + /*way.addTag("motorcar", "private"); + way.addTag("bicycle", "private"); + way.addTag("foot", "private"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allowsNothing());*/ + } + + /** + * Tests that motor_vehicle/bicycle/foot private don't add permissions but yes add permission if + * access is no + *

+ * Support for motor_vehicle was added in #1881 + */ + @Test + void testMotorVehicleTagAllowedPermissions() { + OSMWay way = new OSMWay(); + way.addTag("highway", "residential"); + var permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); + + way.addTag("access", "no"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allowsNothing()); + + way.addTag("motor_vehicle", "private"); + way.addTag("bicycle", "private"); + way.addTag("foot", "private"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allowsNothing()); + + way.addTag("motor_vehicle", "yes"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.CAR)); + + way.addTag("bicycle", "yes"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.BICYCLE_AND_CAR)); + + way.addTag("foot", "yes"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); + } + + /** + * Tests that motor_vehicle/bicycle/foot private don't add permissions but no remove permission if + * access is yes + *

+ * Support for motor_vehicle was added in #1881 + */ + @Test + void testMotorVehicleTagDeniedPermissions() { + OSMWay way = new OSMWay(); + way.addTag("highway", "residential"); + var permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.ALL)); + + way.addTag("motor_vehicle", "no"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE)); + + way.addTag("bicycle", "no"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allows(StreetTraversalPermission.PEDESTRIAN)); + + way.addTag("foot", "no"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allowsNothing()); + //normal road with specific mode of transport private only is doubtful + /*way.addTag("motor_vehicle", "private"); + way.addTag("bicycle", "private"); + way.addTag("foot", "private"); + permissionPair = getWayProperties(way); + assertTrue(permissionPair.main().allowsNothing());*/ + } + + @Test + void testSidepathPermissions() { + OSMWay way = new OSMWay(); + way.addTag("bicycle", "use_sidepath"); + way.addTag("highway", "primary"); + way.addTag("lanes", "2"); + way.addTag("maxspeed", "70"); + way.addTag("oneway", "yes"); + var permissionPair = getWayProperties(way); + + assertFalse(permissionPair.main().allows(StreetTraversalPermission.BICYCLE)); + assertFalse(permissionPair.back().allows(StreetTraversalPermission.BICYCLE)); + + assertTrue(permissionPair.main().allows(StreetTraversalPermission.CAR)); + assertFalse(permissionPair.back().allows(StreetTraversalPermission.CAR)); + + way = new OSMWay(); + way.addTag("bicycle:forward", "use_sidepath"); + way.addTag("highway", "tertiary"); + permissionPair = getWayProperties(way); + + assertFalse(permissionPair.main().allows(StreetTraversalPermission.BICYCLE)); + assertTrue(permissionPair.back().allows(StreetTraversalPermission.BICYCLE)); + + assertTrue(permissionPair.main().allows(StreetTraversalPermission.CAR)); + assertTrue(permissionPair.back().allows(StreetTraversalPermission.CAR)); + + way = new OSMWay(); + way.addTag("bicycle:backward", "use_sidepath"); + way.addTag("highway", "tertiary"); + permissionPair = getWayProperties(way); + + assertTrue(permissionPair.main().allows(StreetTraversalPermission.BICYCLE)); + assertFalse(permissionPair.back().allows(StreetTraversalPermission.BICYCLE)); + + assertTrue(permissionPair.main().allows(StreetTraversalPermission.CAR)); + assertTrue(permissionPair.back().allows(StreetTraversalPermission.CAR)); + + way = new OSMWay(); + way.addTag("highway", "tertiary"); + way.addTag("oneway", "yes"); + way.addTag("oneway:bicycle", "no"); + permissionPair = getWayProperties(way); + + assertTrue(permissionPair.main().allows(StreetTraversalPermission.BICYCLE)); + assertTrue(permissionPair.back().allows(StreetTraversalPermission.BICYCLE)); + + assertTrue(permissionPair.main().allows(StreetTraversalPermission.CAR)); + assertFalse(permissionPair.back().allows(StreetTraversalPermission.CAR)); + + way.addTag("bicycle:forward", "use_sidepath"); + permissionPair = getWayProperties(way); + assertFalse(permissionPair.main().allows(StreetTraversalPermission.BICYCLE)); + assertTrue(permissionPair.back().allows(StreetTraversalPermission.BICYCLE)); + + assertTrue(permissionPair.main().allows(StreetTraversalPermission.CAR)); + assertFalse(permissionPair.back().allows(StreetTraversalPermission.CAR)); + } + + private StreetTraversalPermissionPair getWayProperties(OSMWay way) { + WayPropertySet wayPropertySet = new WayPropertySet(); + WayProperties wayData = wayPropertySet.getDataForWay(way); + + StreetTraversalPermission def = wayData.getPermission(); + return way.splitPermissions(def); + } } } diff --git a/src/test/java/org/opentripplanner/openstreetmap/wayproperty/specifier/WayTestData.java b/src/test/java/org/opentripplanner/openstreetmap/wayproperty/specifier/WayTestData.java index f7ec49b925a..20dbcbb5a78 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/wayproperty/specifier/WayTestData.java +++ b/src/test/java/org/opentripplanner/openstreetmap/wayproperty/specifier/WayTestData.java @@ -1,5 +1,6 @@ package org.opentripplanner.openstreetmap.wayproperty.specifier; +import org.opentripplanner.openstreetmap.model.OSMWay; import org.opentripplanner.openstreetmap.model.OSMWithTags; public class WayTestData { @@ -99,8 +100,8 @@ public static OSMWithTags threeLanes() { return way; } - public static OSMWithTags cycleway() { - var way = new OSMWithTags(); + public static OSMWay cycleway() { + var way = new OSMWay(); way.addTag("highway", "residential"); way.addTag("cycleway", "lane"); return way; @@ -208,4 +209,12 @@ public static OSMWithTags excellentSmoothness() { way.addTag("smoothness", "excellent"); return way; } + + public static OSMWithTags zooPlatform() { + // https://www.openstreetmap.org/way/119108622 + var way = new OSMWithTags(); + way.addTag("public_transport", "platform"); + way.addTag("usage", "tourism"); + return way; + } } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainTest.java b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainTest.java index c7fe995ba26..2bbb5a25e3b 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainTest.java @@ -11,6 +11,7 @@ import static org.opentripplanner.model.plan.TestItineraryBuilder.newTime; import static org.opentripplanner.routing.api.request.preference.ItineraryFilterDebugProfile.ofDebugEnabled; +import java.time.Duration; import java.time.Instant; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -21,6 +22,7 @@ import org.opentripplanner.model.plan.PlanTestConstants; import org.opentripplanner.model.plan.TestItineraryBuilder; import org.opentripplanner.routing.alertpatch.StopCondition; +import org.opentripplanner.routing.api.request.framework.CostLinearFunction; import org.opentripplanner.routing.api.response.RoutingError; import org.opentripplanner.routing.api.response.RoutingErrorCode; import org.opentripplanner.routing.services.TransitAlertService; @@ -162,7 +164,9 @@ public void testSameFirstOrLastTripFilter() { void testRoutingErrorsOriginDestinationTooCloseTest() { ItineraryListFilterChain chain = createBuilder(false, false, 20) .withRemoveWalkAllTheWayResults(true) - .withRemoveTransitWithHigherCostThanBestOnStreetOnly(true) + .withRemoveTransitWithHigherCostThanBestOnStreetOnly( + CostLinearFunction.of(Duration.ofSeconds(0), 1.0) + ) .build(); Itinerary walk = newItinerary(A, T11_06).walk(D10m, E).build(); @@ -243,7 +247,9 @@ private ItineraryListFilterChainBuilder createBuilder( var sortOrder = arriveBy ? STREET_AND_DEPARTURE_TIME : STREET_AND_ARRIVAL_TIME; return new ItineraryListFilterChainBuilder(sortOrder) .withMaxNumberOfItineraries(numOfItineraries) - .withRemoveTransitWithHigherCostThanBestOnStreetOnly(true) + .withRemoveTransitWithHigherCostThanBestOnStreetOnly( + CostLinearFunction.of(Duration.ofSeconds(0), 1.0) + ) .withDebugEnabled(ofDebugEnabled(debug)); } @@ -297,9 +303,10 @@ public void setUpItineraries() { @Test public void removeTransitWithHigherCostThanBestOnStreetOnlyDisabled() { - // Disable filter and allow none optimal bus itinerary pass through + // Allow non-optimal bus itinerary pass through ItineraryListFilterChain chain = builder - .withRemoveTransitWithHigherCostThanBestOnStreetOnly(false) + .withRemoveTransitWithHigherCostThanBestOnStreetOnly(null) + .withRemoveTransitIfWalkingIsBetter(false) .build(); assertEquals(toStr(List.of(walk, bus)), toStr(chain.filter(List.of(walk, bus)))); } @@ -308,7 +315,9 @@ public void removeTransitWithHigherCostThanBestOnStreetOnlyDisabled() { public void removeTransitWithHigherCostThanBestOnStreetOnlyEnabled() { // Enable filter and remove bus itinerary ItineraryListFilterChain chain = builder - .withRemoveTransitWithHigherCostThanBestOnStreetOnly(true) + .withRemoveTransitWithHigherCostThanBestOnStreetOnly( + CostLinearFunction.of(Duration.ofSeconds(0), 1.0) + ) .build(); assertEquals(toStr(List.of(walk)), toStr(chain.filter(List.of(walk, bus)))); } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/RemoveTransitIfStreetOnlyIsBetterFilterTest.java b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/RemoveTransitIfStreetOnlyIsBetterFilterTest.java index 5598c97cd57..931953e013f 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/RemoveTransitIfStreetOnlyIsBetterFilterTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/RemoveTransitIfStreetOnlyIsBetterFilterTest.java @@ -4,10 +4,12 @@ import static org.opentripplanner.model.plan.Itinerary.toStr; import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary; +import java.time.Duration; import java.util.List; import org.junit.jupiter.api.Test; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.model.plan.PlanTestConstants; +import org.opentripplanner.routing.api.request.framework.CostLinearFunction; public class RemoveTransitIfStreetOnlyIsBetterFilterTest implements PlanTestConstants { @@ -20,7 +22,9 @@ public void filterAwayNothingIfNoWalking() { // When: List result = DeletionFlaggerTestHelper.process( List.of(i1, i2), - new RemoveTransitIfStreetOnlyIsBetterFilter() + new RemoveTransitIfStreetOnlyIsBetterFilter( + CostLinearFunction.of(Duration.ofSeconds(200), 1.2) + ) ); // Then: @@ -33,20 +37,24 @@ public void filterAwayLongTravelTimeWithoutWaitTime() { Itinerary walk = newItinerary(A, 6).walk(1, E).build(); walk.setGeneralizedCost(300); - // Given: a bicycle itinerary with low cost - transit with higher cost is removed + // Given: a bicycle itinerary with low cost - transit with clearly higher cost are removed Itinerary bicycle = newItinerary(A).bicycle(6, 8, E).build(); bicycle.setGeneralizedCost(200); + // transit with almost equal cost should not be dropped Itinerary i1 = newItinerary(A).bus(21, 6, 8, E).build(); - i1.setGeneralizedCost(199); + i1.setGeneralizedCost(220); + // transit with considerably higher cost will be dropped Itinerary i2 = newItinerary(A).bus(31, 6, 8, E).build(); - i2.setGeneralizedCost(200); + i2.setGeneralizedCost(360); // When: List result = DeletionFlaggerTestHelper.process( List.of(i2, bicycle, walk, i1), - new RemoveTransitIfStreetOnlyIsBetterFilter() + new RemoveTransitIfStreetOnlyIsBetterFilter( + CostLinearFunction.of(Duration.ofSeconds(60), 1.2) + ) ); // Then: diff --git a/src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/RemoveTransitIfWalkingIsBetterTest.java b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/RemoveTransitIfWalkingIsBetterTest.java new file mode 100644 index 00000000000..b68ac9c46a2 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/deletionflagger/RemoveTransitIfWalkingIsBetterTest.java @@ -0,0 +1,52 @@ +package org.opentripplanner.routing.algorithm.filterchain.deletionflagger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.model.plan.Itinerary.toStr; +import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary; + +import java.time.Duration; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.model.plan.PlanTestConstants; + +public class RemoveTransitIfWalkingIsBetterTest implements PlanTestConstants { + + @Test + public void filterAwayNothingIfNoWalking() { + // Given: + Itinerary i1 = newItinerary(A).bus(21, 6, 7, E).build(); + Itinerary i2 = newItinerary(A).rail(110, 6, 9, E).build(); + + // When: + List result = DeletionFlaggerTestHelper.process( + List.of(i1, i2), + new RemoveTransitIfWalkingIsBetterFilter() + ); + + // Then: + assertEquals(toStr(List.of(i1, i2)), toStr(result)); + } + + @Test + public void filterAwayTransitWithLongerWalk() { + // a walk itinerary + Itinerary walk = newItinerary(A, 6).walk(D2m, E).build(); + + // a bicycle itinerary will not be filtered + Itinerary bicycle = newItinerary(A).bicycle(6, 9, E).build(); + + // transit which has more walking as plain walk should be dropped + Itinerary i1 = newItinerary(A, 6).walk(D3m, D).bus(1, 9, 10, E).build(); + + // transit which has less walking than plain walk should be kept + Itinerary i2 = newItinerary(A, 6).walk(D1m, B).bus(2, 7, 10, E).build(); + + List result = DeletionFlaggerTestHelper.process( + List.of(i1, i2, bicycle, walk), + new RemoveTransitIfWalkingIsBetterFilter() + ); + + assertEquals(toStr(List.of(i2, bicycle, walk)), toStr(result)); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressPenaltyDecoratorTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressPenaltyDecoratorTest.java index 3e2cf4be2d8..23dfc03b252 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressPenaltyDecoratorTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressPenaltyDecoratorTest.java @@ -44,7 +44,7 @@ class AccessEgressPenaltyDecoratorTest { static void verifyTestSetup() { assertEquals("Walk 15m38s $238035 w/penalty(13m23s $1606) ~ 1", EXP_WALK_W_PENALTY.toString()); assertEquals( - "Walk 11m53s $237886 w/penalty(11m8s $1336) ~ 1", + "Walk 11m53s $237887 w/penalty(11m8s $1336) ~ 1", EXP_CAR_RENTAL_W_PENALTY.toString() ); } @@ -114,7 +114,7 @@ void filterEgress() {} private static DefaultAccessEgress ofCarRental(int duration) { return ofAccessEgress( duration, - TestStateBuilder.ofCarRental().streetEdge().pickUpCar().build() + TestStateBuilder.ofCarRental().streetEdge().pickUpCarFromStation().build() ); } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgressTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgressTest.java index 066bd9178d6..f19a19d5060 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgressTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgressTest.java @@ -90,7 +90,7 @@ void containsModeWalkOnly() { var subject = new DefaultAccessEgress(0, stateWalk); assertTrue(subject.isWalkOnly()); - var carRentalState = TestStateBuilder.ofCarRental().streetEdge().pickUpCar().build(); + var carRentalState = TestStateBuilder.ofCarRental().streetEdge().pickUpCarFromStation().build(); subject = new DefaultAccessEgress(0, carRentalState); assertFalse(subject.isWalkOnly()); } diff --git a/src/test/java/org/opentripplanner/routing/api/request/preference/ItineraryFilterPreferencesTest.java b/src/test/java/org/opentripplanner/routing/api/request/preference/ItineraryFilterPreferencesTest.java index c10443800cc..f59586b0af9 100644 --- a/src/test/java/org/opentripplanner/routing/api/request/preference/ItineraryFilterPreferencesTest.java +++ b/src/test/java/org/opentripplanner/routing/api/request/preference/ItineraryFilterPreferencesTest.java @@ -28,6 +28,10 @@ class ItineraryFilterPreferencesTest { CostLinearFunction.of(Duration.ofSeconds(4), 5.0), 3.0 ); + private static final CostLinearFunction TRANSIT_BEST_STREET_COST_LIMIT = CostLinearFunction.of( + Duration.ofSeconds(30), + 1.3 + ); private final ItineraryFilterPreferences subject = ItineraryFilterPreferences .of() @@ -42,6 +46,7 @@ class ItineraryFilterPreferencesTest { .withNonTransitGeneralizedCostLimit(NON_TRANSIT_GENERALIZED_COST_LIMIT) .withParkAndRideDurationRatio(PARK_AND_RIDE_DURATION_RATIO) .withTransitGeneralizedCostLimit(TRANSIT_GENERALIZED_COST_LIMIT) + .withRemoveTransitWithHigherCostThanBestOnStreetOnly(TRANSIT_BEST_STREET_COST_LIMIT) .build(); @Test @@ -105,6 +110,14 @@ void transitGeneralizedCostLimit() { assertEquals(TRANSIT_GENERALIZED_COST_LIMIT, subject.transitGeneralizedCostLimit()); } + @Test + void removeTransitWithHigherCostThanBestOnStreetOnly() { + assertEquals( + TRANSIT_BEST_STREET_COST_LIMIT, + subject.removeTransitWithHigherCostThanBestOnStreetOnly() + ); + } + @Test void testCopyOfEqualsAndHashCode() { // Return same object if no value is set @@ -133,7 +146,8 @@ void testToString() { "minBikeParkingDistance: 2,000.0, " + "nonTransitGeneralizedCostLimit: 4s + 5.0 t, " + "parkAndRideDurationRatio: 0.44, " + - "transitGeneralizedCostLimit: TransitGeneralizedCostFilterParams[costLimitFunction=4s + 5.0 t, intervalRelaxFactor=3.0]" + + "transitGeneralizedCostLimit: TransitGeneralizedCostFilterParams[costLimitFunction=4s + 5.0 t, intervalRelaxFactor=3.0], " + + "removeTransitWithHigherCostThanBestOnStreetOnly: 30s + 1.30 t" + "}", subject.toString() ); diff --git a/src/test/java/org/opentripplanner/routing/graphfinder/StopFinderTraverseVisitorTest.java b/src/test/java/org/opentripplanner/routing/graphfinder/StopFinderTraverseVisitorTest.java new file mode 100644 index 00000000000..1bdf76d1507 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/graphfinder/StopFinderTraverseVisitorTest.java @@ -0,0 +1,44 @@ +package org.opentripplanner.routing.graphfinder; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.street.model.vertex.TransitStopVertex; +import org.opentripplanner.street.search.state.TestStateBuilder; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.site.RegularStop; + +class StopFinderTraverseVisitorTest { + + static final RegularStop STOP = TransitModelForTest.stopForTest("a-stop", 1, 1); + + @Test + void deduplicateStops() { + var visitor = new StopFinderTraverseVisitor(1000); + + assertEquals(List.of(), visitor.stopsFound()); + var state1 = TestStateBuilder.ofWalking().streetEdge().stop(STOP).build(); + + visitor.visitVertex(state1); + + var transitStopVertex = (TransitStopVertex) state1.getVertex(); + final NearbyStop nearbyStop = NearbyStop.nearbyStopForState( + state1, + transitStopVertex.getStop() + ); + + assertEquals(List.of(nearbyStop), visitor.stopsFound()); + + visitor.visitVertex(state1); + + assertEquals(List.of(nearbyStop), visitor.stopsFound()); + + // taking a different path to the same stop + var state2 = TestStateBuilder.ofWalking().streetEdge().streetEdge().stop(STOP).build(); + + visitor.visitVertex(state2); + + assertEquals(List.of(nearbyStop), visitor.stopsFound()); + } +} diff --git a/src/test/java/org/opentripplanner/service/vehiclerental/model/TestFreeFloatingRentalVehicleBuilder.java b/src/test/java/org/opentripplanner/service/vehiclerental/model/TestFreeFloatingRentalVehicleBuilder.java new file mode 100644 index 00000000000..a6fe3da2f4a --- /dev/null +++ b/src/test/java/org/opentripplanner/service/vehiclerental/model/TestFreeFloatingRentalVehicleBuilder.java @@ -0,0 +1,53 @@ +package org.opentripplanner.service.vehiclerental.model; + +import javax.annotation.Nonnull; +import org.opentripplanner.framework.i18n.NonLocalizedString; +import org.opentripplanner.street.model.RentalFormFactor; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +public class TestFreeFloatingRentalVehicleBuilder { + + public static final String NETWORK_1 = "Network-1"; + + private RentalVehicleType vehicleType = RentalVehicleType.getDefaultType(NETWORK_1); + + public static TestFreeFloatingRentalVehicleBuilder of() { + return new TestFreeFloatingRentalVehicleBuilder(); + } + + public TestFreeFloatingRentalVehicleBuilder withVehicleScooter() { + return buildVehicleType(RentalFormFactor.SCOOTER); + } + + public TestFreeFloatingRentalVehicleBuilder withVehicleBicycle() { + return buildVehicleType(RentalFormFactor.BICYCLE); + } + + public TestFreeFloatingRentalVehicleBuilder withVehicleCar() { + return buildVehicleType(RentalFormFactor.CAR); + } + + @Nonnull + private TestFreeFloatingRentalVehicleBuilder buildVehicleType(RentalFormFactor rentalFormFactor) { + this.vehicleType = + new RentalVehicleType( + new FeedScopedId(TestFreeFloatingRentalVehicleBuilder.NETWORK_1, rentalFormFactor.name()), + rentalFormFactor.name(), + rentalFormFactor, + RentalVehicleType.PropulsionType.ELECTRIC, + 100000d + ); + return this; + } + + public VehicleRentalVehicle build() { + var vehicle = new VehicleRentalVehicle(); + var stationName = "free-floating-" + vehicleType.formFactor.name().toLowerCase(); + vehicle.id = new FeedScopedId(NETWORK_1, stationName); + vehicle.name = new NonLocalizedString(stationName); + vehicle.latitude = 47.510; + vehicle.longitude = 18.99; + vehicle.vehicleType = vehicleType; + 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 ea8d98b1fc1..6e3d302c219 100644 --- a/src/test/java/org/opentripplanner/service/vehiclerental/model/TestVehicleRentalStationBuilder.java +++ b/src/test/java/org/opentripplanner/service/vehiclerental/model/TestVehicleRentalStationBuilder.java @@ -1,6 +1,7 @@ package org.opentripplanner.service.vehiclerental.model; import java.util.Map; +import javax.annotation.Nonnull; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.street.model.RentalFormFactor; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -39,12 +40,21 @@ public TestVehicleRentalStationBuilder withStationOn(boolean stationOn) { return this; } + public TestVehicleRentalStationBuilder withVehicleTypeBicycle() { + return buildVehicleType(RentalFormFactor.BICYCLE); + } + public TestVehicleRentalStationBuilder withVehicleTypeCar() { + return buildVehicleType(RentalFormFactor.CAR); + } + + @Nonnull + private TestVehicleRentalStationBuilder buildVehicleType(RentalFormFactor rentalFormFactor) { this.vehicleType = new RentalVehicleType( - new FeedScopedId(TestVehicleRentalStationBuilder.NETWORK_1, "car"), - "car", - RentalFormFactor.CAR, + new FeedScopedId(TestVehicleRentalStationBuilder.NETWORK_1, rentalFormFactor.name()), + rentalFormFactor.name(), + rentalFormFactor, RentalVehicleType.PropulsionType.ELECTRIC, 100000d ); diff --git a/src/test/java/org/opentripplanner/street/model/edge/StreetTransitEntityLinkTest.java b/src/test/java/org/opentripplanner/street/model/edge/StreetTransitEntityLinkTest.java index 9a3c8a9c2dd..c996a6fc00a 100644 --- a/src/test/java/org/opentripplanner/street/model/edge/StreetTransitEntityLinkTest.java +++ b/src/test/java/org/opentripplanner/street/model/edge/StreetTransitEntityLinkTest.java @@ -1,5 +1,6 @@ package org.opentripplanner.street.model.edge; +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 static org.opentripplanner.transit.model.basic.Accessibility.NOT_POSSIBLE; @@ -7,29 +8,28 @@ import static org.opentripplanner.transit.model.basic.Accessibility.POSSIBLE; import java.util.Set; +import java.util.stream.Stream; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.api.request.preference.AccessibilityPreferences; import org.opentripplanner.routing.api.request.preference.WheelchairPreferences; import org.opentripplanner.street.model._data.StreetModelForTest; +import org.opentripplanner.street.model.vertex.StreetVertex; import org.opentripplanner.street.model.vertex.TransitStopVertexBuilder; import org.opentripplanner.street.search.request.StreetSearchRequest; import org.opentripplanner.street.search.state.State; +import org.opentripplanner.street.search.state.TestStateBuilder; +import org.opentripplanner.test.support.VariableSource; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.site.RegularStop; class StreetTransitEntityLinkTest { - RegularStop inaccessibleStop = TransitModelForTest.stopForTest( - "A:inaccessible", - "wheelchair inaccessible stop", - 10.001, - 10.001, - null, - NOT_POSSIBLE - ); - RegularStop accessibleStop = TransitModelForTest.stopForTest( + static final RegularStop accessibleStop = TransitModelForTest.stopForTest( "A:accessible", "wheelchair accessible stop", 10.001, @@ -38,68 +38,131 @@ class StreetTransitEntityLinkTest { POSSIBLE ); - RegularStop unknownStop = TransitModelForTest.stopForTest( - "A:unknown", - "unknown", - 10.001, - 10.001, - null, - NO_INFORMATION - ); + @Nested + class WheelchairAccessibility { - @Test - void disallowInaccessibleStop() { - var afterTraversal = traverse(inaccessibleStop, true); - assertTrue(State.isEmpty(afterTraversal)); - } + static final RegularStop inaccessibleStop = TransitModelForTest.stopForTest( + "A:inaccessible", + "wheelchair inaccessible stop", + 10.001, + 10.001, + null, + NOT_POSSIBLE + ); - @Test - void allowAccessibleStop() { - var afterTraversal = traverse(accessibleStop, true); + static final RegularStop unknownStop = TransitModelForTest.stopForTest( + "A:unknown", + "unknown", + 10.001, + 10.001, + null, + NO_INFORMATION + ); - assertFalse(State.isEmpty(afterTraversal)); - } + @Test + void disallowInaccessibleStop() { + var afterTraversal = traverse(inaccessibleStop, true); + assertTrue(State.isEmpty(afterTraversal)); + } + + @Test + void allowAccessibleStop() { + var afterTraversal = traverse(accessibleStop, true); - @Test - void unknownStop() { - var afterTraversal = traverse(unknownStop, false); - assertFalse(State.isEmpty(afterTraversal)); + assertFalse(State.isEmpty(afterTraversal)); + } - var afterStrictTraversal = traverse(unknownStop, true); - assertTrue(State.isEmpty(afterStrictTraversal)); + @Test + void unknownStop() { + var afterTraversal = traverse(unknownStop, false); + assertFalse(State.isEmpty(afterTraversal)); + + var afterStrictTraversal = traverse(unknownStop, true); + assertTrue(State.isEmpty(afterStrictTraversal)); + } + + private State[] traverse(RegularStop stop, boolean onlyAccessible) { + var from = StreetModelForTest.intersectionVertex("A", 10, 10); + var to = new TransitStopVertexBuilder() + .withStop(stop) + .withModes(Set.of(TransitMode.RAIL)) + .build(); + + var req = StreetSearchRequest.of().withMode(StreetMode.BIKE); + AccessibilityPreferences feature; + if (onlyAccessible) { + feature = AccessibilityPreferences.ofOnlyAccessible(); + } else { + feature = AccessibilityPreferences.ofCost(100, 100); + } + req.withWheelchair(true); + req.withPreferences(p -> + p.withWheelchair( + WheelchairPreferences + .of() + .withTrip(feature) + .withStop(feature) + .withElevator(feature) + .withInaccessibleStreetReluctance(25) + .withMaxSlope(0.045) + .withSlopeExceededReluctance(10) + .withStairsReluctance(25) + .build() + ) + ); + + var edge = StreetTransitStopLink.createStreetTransitStopLink(from, to); + return edge.traverse(new State(from, req.build())); + } } - private State[] traverse(RegularStop stop, boolean onlyAccessible) { - var from = StreetModelForTest.intersectionVertex("A", 10, 10); - var to = new TransitStopVertexBuilder() - .withStop(stop) - .withModes(Set.of(TransitMode.RAIL)) - .build(); - - var req = StreetSearchRequest.of().withMode(StreetMode.BIKE); - AccessibilityPreferences feature; - if (onlyAccessible) { - feature = AccessibilityPreferences.ofOnlyAccessible(); - } else { - feature = AccessibilityPreferences.ofCost(100, 100); + @Nested + class Rental { + + static Stream allowedStates = Stream + .of( + TestStateBuilder.ofScooterRental().pickUpFreeFloatingScooter(), + TestStateBuilder.ofBikeRental().pickUpFreeFloatingBike(), + // allowing cars into stations is a bit questionable but the alternatives would be quite + // computationally expensive + TestStateBuilder.ofCarRental().pickUpFreeFloatingCar(), + TestStateBuilder.ofWalking(), + TestStateBuilder.ofCycling() + ) + .map(TestStateBuilder::build) + .map(Arguments::of); + + @ParameterizedTest + @VariableSource("allowedStates") + void freeFloatingVehiclesAreAllowedIntoStops(State state) { + testTraversalWithState(state, true); } - req.withWheelchair(true); - req.withPreferences(p -> - p.withWheelchair( - WheelchairPreferences - .of() - .withTrip(feature) - .withStop(feature) - .withElevator(feature) - .withInaccessibleStreetReluctance(25) - .withMaxSlope(0.045) - .withSlopeExceededReluctance(10) - .withStairsReluctance(25) - .build() + + static Stream notAllowedStates = Stream + .of( + TestStateBuilder.ofBikeRental().pickUpBikeFromStation(), + TestStateBuilder.ofCarRental().pickUpCarFromStation(), + // for bike and ride you need to drop the bike at a parking facility first + TestStateBuilder.ofBikeAndRide().streetEdge(), + TestStateBuilder.parkAndRide().streetEdge() ) - ); + .map(TestStateBuilder::build) + .map(Arguments::of); - var edge = StreetTransitStopLink.createStreetTransitStopLink(from, to); - return edge.traverse(new State(from, req.build())); + @ParameterizedTest + @VariableSource("notAllowedStates") + void stationBasedVehiclesAreNotAllowedIntoStops(State state) { + testTraversalWithState(state, false); + } + + private void testTraversalWithState(State state, boolean canTraverse) { + var transitStopVertex = new TransitStopVertexBuilder().withStop(accessibleStop).build(); + var edge = StreetTransitStopLink.createStreetTransitStopLink( + (StreetVertex) state.getVertex(), + transitStopVertex + ); + var result = edge.traverse(state); + assertEquals(canTraverse, result.length > 0); + } } } diff --git a/src/test/java/org/opentripplanner/street/model/vertex/BarrierVertexTest.java b/src/test/java/org/opentripplanner/street/model/vertex/BarrierVertexTest.java index ab488bdb82a..127f34e17ca 100644 --- a/src/test/java/org/opentripplanner/street/model/vertex/BarrierVertexTest.java +++ b/src/test/java/org/opentripplanner/street/model/vertex/BarrierVertexTest.java @@ -8,7 +8,6 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.LineString; import org.opentripplanner.framework.geometry.GeometryUtils; -import org.opentripplanner.graph_builder.module.osm.OsmFilter; import org.opentripplanner.openstreetmap.model.OSMNode; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.street.model.StreetTraversalPermission; @@ -32,35 +31,35 @@ public void testBarrierPermissions() { String label = "simpleBarrier"; BarrierVertex bv = new BarrierVertex(simpleBarier.lon, simpleBarier.lat, 0); bv.setBarrierPermissions( - OsmFilter.getPermissionsForEntity(simpleBarier, BarrierVertex.defaultBarrierPermissions) + simpleBarier.overridePermissions(BarrierVertex.defaultBarrierPermissions) ); assertEquals(StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE, bv.getBarrierPermissions()); simpleBarier.addTag("foot", "yes"); bv.setBarrierPermissions( - OsmFilter.getPermissionsForEntity(simpleBarier, BarrierVertex.defaultBarrierPermissions) + simpleBarier.overridePermissions(BarrierVertex.defaultBarrierPermissions) ); assertEquals(StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE, bv.getBarrierPermissions()); simpleBarier.addTag("bicycle", "yes"); bv.setBarrierPermissions( - OsmFilter.getPermissionsForEntity(simpleBarier, BarrierVertex.defaultBarrierPermissions) + simpleBarier.overridePermissions(BarrierVertex.defaultBarrierPermissions) ); assertEquals(StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE, bv.getBarrierPermissions()); simpleBarier.addTag("access", "no"); bv.setBarrierPermissions( - OsmFilter.getPermissionsForEntity(simpleBarier, BarrierVertex.defaultBarrierPermissions) + simpleBarier.overridePermissions(BarrierVertex.defaultBarrierPermissions) ); assertEquals(StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE, bv.getBarrierPermissions()); simpleBarier.addTag("motor_vehicle", "no"); bv.setBarrierPermissions( - OsmFilter.getPermissionsForEntity(simpleBarier, BarrierVertex.defaultBarrierPermissions) + simpleBarier.overridePermissions(BarrierVertex.defaultBarrierPermissions) ); assertEquals(StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE, bv.getBarrierPermissions()); simpleBarier.addTag("bicycle", "no"); bv.setBarrierPermissions( - OsmFilter.getPermissionsForEntity(simpleBarier, BarrierVertex.defaultBarrierPermissions) + simpleBarier.overridePermissions(BarrierVertex.defaultBarrierPermissions) ); assertEquals(StreetTraversalPermission.PEDESTRIAN, bv.getBarrierPermissions()); @@ -69,7 +68,7 @@ public void testBarrierPermissions() { complexBarrier.addTag("access", "no"); bv.setBarrierPermissions( - OsmFilter.getPermissionsForEntity(complexBarrier, BarrierVertex.defaultBarrierPermissions) + complexBarrier.overridePermissions(BarrierVertex.defaultBarrierPermissions) ); assertEquals(StreetTraversalPermission.NONE, bv.getBarrierPermissions()); @@ -78,7 +77,7 @@ public void testBarrierPermissions() { noBikeBollard.addTag("bicycle", "no"); bv.setBarrierPermissions( - OsmFilter.getPermissionsForEntity(noBikeBollard, BarrierVertex.defaultBarrierPermissions) + noBikeBollard.overridePermissions(BarrierVertex.defaultBarrierPermissions) ); assertEquals(StreetTraversalPermission.PEDESTRIAN, bv.getBarrierPermissions()); @@ -87,7 +86,7 @@ public void testBarrierPermissions() { accessBarrier.addTag("access", "no"); bv.setBarrierPermissions( - OsmFilter.getPermissionsForEntity(accessBarrier, BarrierVertex.defaultBarrierPermissions) + accessBarrier.overridePermissions(BarrierVertex.defaultBarrierPermissions) ); assertEquals(StreetTraversalPermission.NONE, bv.getBarrierPermissions()); } diff --git a/src/test/java/org/opentripplanner/street/search/state/StateTest.java b/src/test/java/org/opentripplanner/street/search/state/StateTest.java index 43827e876d2..34d3af11b68 100644 --- a/src/test/java/org/opentripplanner/street/search/state/StateTest.java +++ b/src/test/java/org/opentripplanner/street/search/state/StateTest.java @@ -125,7 +125,7 @@ void walkingOnly() { ); assertFalse( - ofCarRental().streetEdge().pickUpCar().build().containsOnlyWalkMode(), + ofCarRental().streetEdge().pickUpCarFromStation().build().containsOnlyWalkMode(), "Walk + CAR" ); } 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 c40e131a056..9a65e670370 100644 --- a/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java +++ b/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java @@ -1,5 +1,6 @@ package org.opentripplanner.street.search.state; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.opentripplanner.transit.model.site.PathwayMode.WALKWAY; import java.time.Instant; @@ -10,7 +11,9 @@ import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.routing.api.request.StreetMode; +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.street.StreetVehicleRentalLink; import org.opentripplanner.service.vehiclerental.street.VehicleRentalEdge; import org.opentripplanner.service.vehiclerental.street.VehicleRentalPlaceVertex; @@ -31,6 +34,7 @@ import org.opentripplanner.street.search.request.StreetSearchRequest; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.basic.Accessibility; +import org.opentripplanner.transit.model.site.RegularStop; /** * Builds up a state chain for use in tests. @@ -70,6 +74,26 @@ public static TestStateBuilder ofCarRental() { return new TestStateBuilder(StreetMode.CAR_RENTAL); } + public static TestStateBuilder ofScooterRental() { + return new TestStateBuilder(StreetMode.SCOOTER_RENTAL); + } + + public static TestStateBuilder ofBikeRental() { + return new TestStateBuilder(StreetMode.BIKE_RENTAL); + } + + public static TestStateBuilder ofCycling() { + return new TestStateBuilder(StreetMode.BIKE); + } + + public static TestStateBuilder ofBikeAndRide() { + return new TestStateBuilder(StreetMode.BIKE_TO_PARK); + } + + public static TestStateBuilder parkAndRide() { + return new TestStateBuilder(StreetMode.CAR_TO_PARK); + } + /** * Traverse a very plain street edge with no special characteristics. */ @@ -90,25 +114,39 @@ public TestStateBuilder streetEdge() { /** * Traverse a street edge and switch to Car mode */ - public TestStateBuilder pickUpCar() { - count++; - - var station = TestVehicleRentalStationBuilder.of().withVehicleTypeCar().build(); + public TestStateBuilder pickUpCarFromStation() { + return pickUpRentalVehicle( + RentalFormFactor.CAR, + TestVehicleRentalStationBuilder.of().withVehicleTypeCar().build() + ); + } - VehicleRentalPlaceVertex vertex = new VehicleRentalPlaceVertex(station); - var link = StreetVehicleRentalLink.createStreetVehicleRentalLink( - (StreetVertex) currentState.vertex, - vertex + public TestStateBuilder pickUpFreeFloatingCar() { + return pickUpRentalVehicle( + RentalFormFactor.CAR, + TestFreeFloatingRentalVehicleBuilder.of().withVehicleCar().build() ); - currentState = link.traverse(currentState)[0]; + } - var edge = VehicleRentalEdge.createVehicleRentalEdge(vertex, RentalFormFactor.CAR); + public TestStateBuilder pickUpFreeFloatingScooter() { + return pickUpRentalVehicle( + RentalFormFactor.SCOOTER, + TestFreeFloatingRentalVehicleBuilder.of().withVehicleScooter().build() + ); + } - State[] traverse = edge.traverse(currentState); - currentState = - Arrays.stream(traverse).filter(it -> it.currentMode() == TraverseMode.CAR).findFirst().get(); + public TestStateBuilder pickUpBikeFromStation() { + return pickUpRentalVehicle( + RentalFormFactor.BICYCLE, + TestVehicleRentalStationBuilder.of().withVehicleTypeBicycle().build() + ); + } - return this; + public TestStateBuilder pickUpFreeFloatingBike() { + return pickUpRentalVehicle( + RentalFormFactor.BICYCLE, + TestFreeFloatingRentalVehicleBuilder.of().withVehicleBicycle().build() + ); } /** @@ -147,23 +185,16 @@ public TestStateBuilder elevator() { return this; } + public TestStateBuilder stop(RegularStop stop) { + return arriveAtStop(stop); + } + /** * Add a state that arrives at a transit stop. */ public TestStateBuilder stop() { count++; - var from = (StreetVertex) currentState.vertex; - var to = new TransitStopVertexBuilder() - .withStop(TransitModelForTest.stopForTest("stop", count, count)) - .build(); - - var edge = StreetTransitStopLink.createStreetTransitStopLink(from, to); - var states = edge.traverse(currentState); - if (states.length != 1) { - throw new IllegalStateException("Only single state transitions are supported."); - } - currentState = states[0]; - return this; + return arriveAtStop(TransitModelForTest.stopForTest("stop", count, count)); } public TestStateBuilder enterStation(String id) { @@ -206,8 +237,21 @@ public TestStateBuilder pathway(String s) { true, WALKWAY ); - var state = edge.traverse(currentState)[0]; - currentState = state; + currentState = edge.traverse(currentState)[0]; + return this; + } + + @Nonnull + private TestStateBuilder arriveAtStop(RegularStop stop) { + var from = (StreetVertex) currentState.vertex; + var to = new TransitStopVertexBuilder().withStop(stop).build(); + + var edge = StreetTransitStopLink.createStreetTransitStopLink(from, to); + var states = edge.traverse(currentState); + if (states.length != 1) { + throw new IllegalStateException("Only single state transitions are supported."); + } + currentState = states[0]; return this; } @@ -229,6 +273,35 @@ private static ElevatorOnboardVertex elevatorOnBoard(int count, String suffix) { ); } + private TestStateBuilder pickUpRentalVehicle( + RentalFormFactor rentalFormFactor, + VehicleRentalPlace place + ) { + count++; + VehicleRentalPlaceVertex vertex = new VehicleRentalPlaceVertex(place); + var link = StreetVehicleRentalLink.createStreetVehicleRentalLink( + (StreetVertex) currentState.vertex, + vertex + ); + currentState = link.traverse(currentState)[0]; + + var edge = VehicleRentalEdge.createVehicleRentalEdge(vertex, rentalFormFactor); + + State[] traverse = edge.traverse(currentState); + currentState = + Arrays.stream(traverse).filter(it -> it.currentMode() != TraverseMode.WALK).findFirst().get(); + + assertTrue(currentState.isRentingVehicle()); + + var linkBack = StreetVehicleRentalLink.createStreetVehicleRentalLink( + (VehicleRentalPlaceVertex) currentState.vertex, + StreetModelForTest.intersectionVertex(count, count) + ); + currentState = linkBack.traverse(currentState)[0]; + + return this; + } + public State build() { return currentState; } diff --git a/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java b/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java index dc4dda2a21f..089d33b98e9 100644 --- a/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java +++ b/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java @@ -54,6 +54,12 @@ public class TransitModelForTest { .withTimezone(TIME_ZONE_ID) .withUrl("https://www.otheragency.com") .build(); + public static final Agency OTHER_FEED_AGENCY = Agency + .of(FeedScopedId.ofNullable("F2", "other.feed-agency")) + .withName("Other feed agency") + .withTimezone(TIME_ZONE_ID) + .withUrl("https:/www.otherfeedagency.com") + .build(); public static FeedScopedId id(String id) { return new FeedScopedId(FEED_ID, id); @@ -89,6 +95,10 @@ public static TripBuilder trip(String id) { return Trip.of(id(id)).withRoute(route("R" + id).build()); } + public static TripBuilder trip(String feedId, String tripId) { + return Trip.of(FeedScopedId.ofNullable(feedId, tripId)).withRoute(route("R" + tripId).build()); + } + public static RegularStop stopForTest( String idAndName, Accessibility wheelchair, diff --git a/src/test/java/org/opentripplanner/updater/alert/AlertsUpdateHandlerTest.java b/src/test/java/org/opentripplanner/updater/alert/AlertsUpdateHandlerTest.java index 5d440447ff6..0474c328162 100644 --- a/src/test/java/org/opentripplanner/updater/alert/AlertsUpdateHandlerTest.java +++ b/src/test/java/org/opentripplanner/updater/alert/AlertsUpdateHandlerTest.java @@ -14,6 +14,7 @@ import java.util.Collection; import java.util.List; import java.util.Map.Entry; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.opentripplanner.framework.i18n.TranslatedString; @@ -83,7 +84,7 @@ public void testWithoutUrl() { .addInformedEntity(GtfsRealtime.EntitySelector.newBuilder().setAgencyId("1")) .build(); TransitAlert transitAlert = processOneAlert(alert); - assertNull(transitAlert.url()); + assertEquals(Optional.empty(), transitAlert.url()); } @Test @@ -102,7 +103,7 @@ public void testWithoutUrlTranslations() { ) .build(); TransitAlert transitAlert = processOneAlert(alert); - assertEquals("https://www.opentripplanner.org/", transitAlert.url().toString()); + assertEquals("https://www.opentripplanner.org/", transitAlert.url().get().toString()); } @Test @@ -135,7 +136,7 @@ public void testWithUrlTranslations() { TransitAlert transitAlert = processOneAlert(alert); List> translations = - ((TranslatedString) transitAlert.url()).getTranslations(); + ((TranslatedString) transitAlert.url().get()).getTranslations(); assertEquals(2, translations.size()); assertEquals("en", translations.get(0).getKey()); assertEquals("https://www.opentripplanner.org/", translations.get(0).getValue()); @@ -156,7 +157,7 @@ public void testWithoutHeaderTranslations() { ) .build(); TransitAlert transitAlert = processOneAlert(alert); - assertEquals("Title", transitAlert.headerText().toString()); + assertEquals("Title", transitAlert.headerText().get().toString()); } @Test @@ -175,7 +176,7 @@ public void testWithHeaderTranslations() { TransitAlert transitAlert = processOneAlert(alert); List> translations = - ((TranslatedString) transitAlert.headerText()).getTranslations(); + ((TranslatedString) transitAlert.headerText().get()).getTranslations(); assertEquals(2, translations.size()); assertEquals("en", translations.get(0).getKey()); assertEquals("Title", translations.get(0).getValue()); @@ -196,7 +197,7 @@ public void testWithoutDescriptionTranslations() { ) .build(); TransitAlert transitAlert = processOneAlert(alert); - assertEquals("Description", transitAlert.descriptionText().toString()); + assertEquals("Description", transitAlert.descriptionText().get().toString()); } @Test @@ -221,7 +222,7 @@ public void testWithDescriptionTranslations() { TransitAlert transitAlert = processOneAlert(alert); List> translations = - ((TranslatedString) transitAlert.descriptionText()).getTranslations(); + ((TranslatedString) transitAlert.descriptionText().get()).getTranslations(); assertEquals(2, translations.size()); assertEquals("en", translations.get(0).getKey()); assertEquals("Description", translations.get(0).getValue()); diff --git a/src/test/resources/org/opentripplanner/graph_builder/module/osm/wendlingen-bahnhof.osm.pbf b/src/test/resources/org/opentripplanner/graph_builder/module/osm/wendlingen-bahnhof.osm.pbf new file mode 100644 index 00000000000..13bcd69211f Binary files /dev/null and b/src/test/resources/org/opentripplanner/graph_builder/module/osm/wendlingen-bahnhof.osm.pbf differ diff --git a/src/test/resources/standalone/config/router-config.json b/src/test/resources/standalone/config/router-config.json index 390714b9107..ddfafff3eea 100644 --- a/src/test/resources/standalone/config/router-config.json +++ b/src/test/resources/standalone/config/router-config.json @@ -1,5 +1,5 @@ { - "configVersion": "v2.3.0-EN000121", + "configVersion": "v2.4.0-EN000121", "server": { "apiProcessingTimeout": "7s", "traceParameters": [ @@ -67,6 +67,8 @@ "costLimitFunction": "15m + 1.5 x", "intervalRelaxFactor": 0.4 }, + "nonTransitGeneralizedCostLimit": "400 + 1.5x", + "removeTransitWithHigherCostThanBestOnStreetOnly": "60 + 1.3x", "bikeRentalDistanceRatio": 0.3, "accessibilityScore": true, "minBikeParkingDistance": 300, diff --git a/test/performance/norway/build-config.json b/test/performance/norway/build-config.json index 50ea45afc2f..5fa7f03ed58 100644 --- a/test/performance/norway/build-config.json +++ b/test/performance/norway/build-config.json @@ -14,8 +14,6 @@ "islandWithoutStopsMaxSize": 5, "islandWithStopsMaxSize": 5 }, - "banDiscouragedWalking": false, - "banDiscouragedBiking": false, "distanceBetweenElevationSamples": 25, "multiThreadElevationCalculations": true, "boardingLocationTags": [],