diff --git a/README.md b/README.md index 4a5fa77c7ee..129771f9bdf 100644 --- a/README.md +++ b/README.md @@ -60,16 +60,7 @@ the world. ## Getting in touch -The fastest way to get help is to use our [Gitter chat room](https://gitter.im/opentripplanner/OpenTripPlanner) -where most of the core developers are. You can also send questions and comments to the -[mailing list](http://groups.google.com/group/opentripplanner-users). - -Changes and extensions to OTP are debated in issues on [GitHub](https://github.com/opentripplanner/OpenTripPlanner/issues) -and in the [Gitter chat room](https://gitter.im/opentripplanner/OpenTripPlanner). More general -questions and announcements of interest to non-developer OTP users should be directed to -the [opentripplanner-users](https://groups.google.com/forum/#!forum/opentripplanner-users) list. -Other details of [project governance](http://docs.opentripplanner.org/en/dev-2.x/Governance/) can be -found in the main documentation. +The fastest way to get help is to use our [Gitter chat room](https://gitter.im/opentripplanner/OpenTripPlanner) where most of the core developers are. Bug reports may be filed via the Github [issue tracker](https://github.com/openplans/OpenTripPlanner/issues). The OpenTripPlanner [mailing list](http://groups.google.com/group/opentripplanner-users) is used almost exclusively for project announcements. The mailing list and issue tracker are not intended for support questions or discussions. Please use the chat for this purpose. Other details of [project governance](http://docs.opentripplanner.org/en/dev-2.x/Governance/) can be found in the main documentation. ## OTP Ecosystem diff --git a/client-next/package-lock.json b/client-next/package-lock.json index abe2c5d3ed6..83f19bf3c48 100644 --- a/client-next/package-lock.json +++ b/client-next/package-lock.json @@ -12,7 +12,7 @@ "bootstrap": "5.3.3", "graphql": "16.8.1", "graphql-request": "6.1.0", - "maplibre-gl": "4.1.3", + "maplibre-gl": "4.2.0", "react": "18.3.1", "react-bootstrap": "2.10.2", "react-dom": "18.3.1", @@ -23,13 +23,13 @@ "@graphql-codegen/client-preset": "4.2.5", "@graphql-codegen/introspection": "4.0.3", "@parcel/watcher": "2.4.1", - "@testing-library/react": "15.0.6", + "@testing-library/react": "15.0.7", "@types/react": "18.3.1", "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "7.8.0", "@typescript-eslint/parser": "7.8.0", "@vitejs/plugin-react": "4.2.1", - "@vitest/coverage-v8": "1.5.3", + "@vitest/coverage-v8": "1.6.0", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.29.1", @@ -41,7 +41,7 @@ "prettier": "3.2.5", "typescript": "5.4.5", "vite": "5.2.11", - "vitest": "1.5.3" + "vitest": "1.6.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2737,16 +2737,18 @@ } }, "node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.1.1.tgz", - "integrity": "sha512-z85ARNPCBI2Cs5cPOS3DSbraTN+ue8zrcYVoSWBuNrD/mA+2SKAJ+hIzI22uN7gac6jBMnCdpPKRxS/V0KSZVQ==", + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.2.0.tgz", + "integrity": "sha512-BTw6/3ysowky22QMtNDjElp+YLwwvBDh3xxnq1izDFjTtUERm5nYSihlNZ6QaxXb+6lX2T2t0hBEjheAI+kBEQ==", "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", + "quickselect": "^2.0.0", "rw": "^1.3.3", - "sort-object": "^3.0.3" + "sort-object": "^3.0.3", + "tinyqueue": "^2.0.3" }, "bin": { "gl-style-format": "dist/gl-style-format.mjs", @@ -3372,9 +3374,9 @@ } }, "node_modules/@testing-library/react": { - "version": "15.0.6", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-15.0.6.tgz", - "integrity": "sha512-UlbazRtEpQClFOiYp+1BapMT+xyqWMnE+hh9tn5DQ6gmlE7AIZWcGpzZukmDZuFk3By01oiqOf8lRedLS4k6xQ==", + "version": "15.0.7", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-15.0.7.tgz", + "integrity": "sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -3485,6 +3487,11 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/junit-report-builder": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz", + "integrity": "sha512-R5M+SYhMbwBeQcNXYWNCZkl09vkVfAtcPIaCGdzIkkbeaTrVbGQ7HVgi4s+EmM/M1K4ZuWQH0jGcvMvNePfxYA==" + }, "node_modules/@types/mapbox__point-geometry": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", @@ -3896,9 +3903,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.5.3.tgz", - "integrity": "sha512-DPyGSu/fPHOJuPxzFSQoT4N/Fu/2aJfZRtEpEp8GI7NHsXBGE94CQ+pbEGBUMFjatsHPDJw/+TAF9r4ens2CNw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", + "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.1", @@ -3919,17 +3926,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.5.3" + "vitest": "1.6.0" } }, "node_modules/@vitest/expect": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.3.tgz", - "integrity": "sha512-y+waPz31pOFr3rD7vWTbwiLe5+MgsMm40jTZbQE8p8/qXyBX3CQsIXRx9XK12IbY7q/t5a5aM/ckt33b4PxK2g==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", + "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", "dev": true, "dependencies": { - "@vitest/spy": "1.5.3", - "@vitest/utils": "1.5.3", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", "chai": "^4.3.10" }, "funding": { @@ -3937,12 +3944,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.3.tgz", - "integrity": "sha512-7PlfuReN8692IKQIdCxwir1AOaP5THfNkp0Uc4BKr2na+9lALNit7ub9l3/R7MP8aV61+mHKRGiqEKRIwu6iiQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", + "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", "dev": true, "dependencies": { - "@vitest/utils": "1.5.3", + "@vitest/utils": "1.6.0", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -3978,9 +3985,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.3.tgz", - "integrity": "sha512-K3mvIsjyKYBhNIDujMD2gfQEzddLe51nNOAf45yKRt/QFJcUIeTQd2trRvv6M6oCBHNVnZwFWbQ4yj96ibiDsA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", + "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -4024,9 +4031,9 @@ "dev": true }, "node_modules/@vitest/spy": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.3.tgz", - "integrity": "sha512-Llj7Jgs6lbnL55WoshJUUacdJfjU2honvGcAJBxhra5TPEzTJH8ZuhI3p/JwqqfnTr4PmP7nDmOXP53MS7GJlg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", + "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -4036,9 +4043,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.3.tgz", - "integrity": "sha512-rE9DTN1BRhzkzqNQO+kw8ZgfeEBCLXiHJwetk668shmNBpSagQxneT5eSqEBLP+cqSiAeecvQmbpFfdMyLcIQA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", + "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", @@ -8339,9 +8346,9 @@ } }, "node_modules/maplibre-gl": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.1.3.tgz", - "integrity": "sha512-nMy5h0kzq9Z66C6AIb3p2BvLIVHz75dGGQow22x+h9/VOihr0IPQI26ylAi6lHqvEy2VqjiRmKAMlFwt0xFKfQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.2.0.tgz", + "integrity": "sha512-x5GgYyKKn5UDvbUZFK7ng3Pq829/uYWDSVN/itZoP2slWSzKbjIXKi/Qhz5FnYiMXwpRgM08UIcVjtn1PLK9Tg==", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -8350,9 +8357,10 @@ "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", - "@maplibre/maplibre-gl-style-spec": "^20.1.1", + "@maplibre/maplibre-gl-style-spec": "^20.2.0", "@types/geojson": "^7946.0.14", "@types/geojson-vt": "3.2.5", + "@types/junit-report-builder": "^3.0.2", "@types/mapbox__point-geometry": "^0.1.4", "@types/mapbox__vector-tile": "^1.3.4", "@types/pbf": "^3.0.5", @@ -10903,9 +10911,9 @@ } }, "node_modules/vite-node": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.3.tgz", - "integrity": "sha512-axFo00qiCpU/JLd8N1gu9iEYL3xTbMbMrbe5nDp9GL0nb6gurIdZLkkFogZXWnE8Oyy5kfSLwNVIcVsnhE7lgQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", + "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -10925,16 +10933,16 @@ } }, "node_modules/vitest": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.3.tgz", - "integrity": "sha512-2oM7nLXylw3mQlW6GXnRriw+7YvZFk/YNV8AxIC3Z3MfFbuziLGWP9GPxxu/7nRlXhqyxBikpamr+lEEj1sUEw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", + "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", "dev": true, "dependencies": { - "@vitest/expect": "1.5.3", - "@vitest/runner": "1.5.3", - "@vitest/snapshot": "1.5.3", - "@vitest/spy": "1.5.3", - "@vitest/utils": "1.5.3", + "@vitest/expect": "1.6.0", + "@vitest/runner": "1.6.0", + "@vitest/snapshot": "1.6.0", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", @@ -10948,7 +10956,7 @@ "tinybench": "^2.5.1", "tinypool": "^0.8.3", "vite": "^5.0.0", - "vite-node": "1.5.3", + "vite-node": "1.6.0", "why-is-node-running": "^2.2.2" }, "bin": { @@ -10963,8 +10971,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.5.3", - "@vitest/ui": "1.5.3", + "@vitest/browser": "1.6.0", + "@vitest/ui": "1.6.0", "happy-dom": "*", "jsdom": "*" }, diff --git a/client-next/package.json b/client-next/package.json index 1677c6a3b1f..5ff988bdc87 100644 --- a/client-next/package.json +++ b/client-next/package.json @@ -21,7 +21,7 @@ "bootstrap": "5.3.3", "graphql": "16.8.1", "graphql-request": "6.1.0", - "maplibre-gl": "4.1.3", + "maplibre-gl": "4.2.0", "react": "18.3.1", "react-bootstrap": "2.10.2", "react-dom": "18.3.1", @@ -32,13 +32,13 @@ "@graphql-codegen/client-preset": "4.2.5", "@graphql-codegen/introspection": "4.0.3", "@parcel/watcher": "2.4.1", - "@testing-library/react": "15.0.6", + "@testing-library/react": "15.0.7", "@types/react": "18.3.1", "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "7.8.0", "@typescript-eslint/parser": "7.8.0", "@vitejs/plugin-react": "4.2.1", - "@vitest/coverage-v8": "1.5.3", + "@vitest/coverage-v8": "1.6.0", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.29.1", @@ -50,6 +50,6 @@ "prettier": "3.2.5", "typescript": "5.4.5", "vite": "5.2.11", - "vitest": "1.5.3" + "vitest": "1.6.0" } } diff --git a/doc-templates/BuildConfiguration.md b/doc-templates/BuildConfiguration.md index 756593a57a3..43a29e1fb79 100644 --- a/doc-templates/BuildConfiguration.md +++ b/doc-templates/BuildConfiguration.md @@ -140,22 +140,6 @@ The mechanism is that for any two identical tags, OTP will use the first one. } ``` -### Custom naming - -You can define a custom naming scheme for elements drawn from OSM by defining an `osmNaming` field -in `build-config.json`, such as: - -```JSON -// build-config.json -{ - "osmNaming": "portland" -} -``` - -There is currently only one custom naming module called `portland` (which has no parameters). - - - ## Elevation data OpenTripPlanner can "drape" the OSM street network over a digital elevation model (DEM). This allows diff --git a/doc-templates/Flex.md b/doc-templates/Flex.md index 2015e898cae..0e79ecdbffc 100644 --- a/doc-templates/Flex.md +++ b/doc-templates/Flex.md @@ -10,8 +10,19 @@ To enable this turn on `FlexRouting` as a feature in `otp-config.json`. -The GTFS feeds should conform to the -[GTFS-Flex v2 draft PR](https://github.com/google/transit/pull/388) +The GTFS feeds must conform to the final, approved version of the draft which has been +merged into the [mainline specification](https://gtfs.org/schedule/reference/) in March 2024. + +### Experimental features + +This sandbox feature also has experimental support for the following fields: + +- `safe_duration_factor` +- `safe_duration_offset` + +These features are currently [undergoing specification](https://github.com/MobilityData/gtfs-flex/pull/79) +and their definition might change. OTP's implementation will be also be changed so be careful +when relying on this feature. ## Configuration diff --git a/doc-templates/sandbox/siri/SiriAzureUpdater.md b/doc-templates/sandbox/siri/SiriAzureUpdater.md index eb092ddd08d..85e22e30bda 100644 --- a/doc-templates/sandbox/siri/SiriAzureUpdater.md +++ b/doc-templates/sandbox/siri/SiriAzureUpdater.md @@ -1,7 +1,8 @@ # Siri Azure Updater -It is sandbox extension developed by Skånetrafiken that allows OTP to fetch Siri ET & SX messages through *Azure Service Bus*. -IT also OTP to download historical data from en HTTP endpoint on startup. +This is a sandbox extension developed by Skånetrafiken that allows OTP to fetch Siri ET & SX messages +through *Azure Service Bus*. +It also enables OTP to download historical data from en HTTP endpoint on startup. ## Contact Info diff --git a/docs/Accessibility.md b/docs/Accessibility.md index cc45a56d776..49373c522c2 100644 --- a/docs/Accessibility.md +++ b/docs/Accessibility.md @@ -2,10 +2,16 @@ ## Preamble -GTFS and Netex define accessibility primarily in terms of binary access for wheelchair users: it's -either on or off. Whilst it is the desire of the OTP developers to broaden the scope of -accessibility the lack of data limits us to use this definition in the implementation and in this -document. +In this document and in OTP, the term "accessibility" is used with its most common +meaning: design of products, devices, services, vehicles, or environments to ensure they are usable by +people with disabilities. If you have reached this page looking for cumulative opportunities +accessibility indicators (access to opportunities metrics) please see the [Analysis](Analysis.md) page. + +While accessibility is a complex subject, at this point GTFS and Netex mostly represent it very +simply as a yes/no possibility of wheelchair use. While OTP developers hope to broaden the +scope and nuance of accessibility support in OTP, the lack of detailed data from data producers +currently limits implementation and discussion in this document to this binary +"wheelchair accessible" definition. ## Unknown data diff --git a/docs/Analysis.md b/docs/Analysis.md new file mode 100644 index 00000000000..867e90869c4 --- /dev/null +++ b/docs/Analysis.md @@ -0,0 +1,21 @@ +# Travel Time Analysis + +## Background + +Since the beginning of the project, many OTP contributors and users have been primarily interested in research, spatial analysis, and urban planning use cases. They have prototyped many ideas within or on top of the OTP codebase, including one-to-many searches producing travel time grids, isochrones, and access-to-opportunities indicators (see Terminology Note below). This has historically been a major area of application for OpenTripPlanner and has helped popularize cumulative opportunities metrics in urban planning. For example, the University of Minnesota Accessibility Observatory used OpenTripPlanner for [Access Across America](https://www.cts.umn.edu/programs/ao/aaa). + +Although we consider these use cases quite important, most work of this kind has long since shifted to separate projects focused on urban planning and analytics. As of version 2, OTP has chosen to focus entirely on passenger information rather than analytics. + +## Travel Time Analysis in OTP1 + +Much of the analysis code present in the v1.x legacy branch of OTP is essentially an unmaintained and unsupported early prototype for later projects, specifically [R5](https://github.com/conveyal/r5/) and the [Conveyal Analysis](https://conveyal.com/learn) system built upon it. OTP1 seems to have gained popularity for analysis uses due to the existence of documentation and an active user community, but has significant technical shortcomings. One of these is simply speed: OTP1 can be orders of magnitude slower (and more memory-intensive) than the approaches used in R5. The other is the requirement to search at a single specific time. Travel times and especially wait times on scheduled transit vary greatly depending on when you depart. Accounting for variation over a time window requires repeated independent searches at each possible departure time, which is very inefficient. R5 is highly optimized to capture variations in travel time across time windows and account for uncertainty in waiting times on frequency-based routes. + +## Travel Time Analysis in OTP2 + +OTP2's new transit router is quite similar to R5 (indeed it was directly influenced by R5) and would not face the same technical problems. Nonetheless, we have decided not to port the OTP1 analysis features over to OTP2 since it would broaden the scope of OTP2 away from passenger information and draw the finite amount of available attention and resources away from existing open source analytics tools. If you would like to apply the routing innovations present in OTP2 in analytics situations, we recommend taking a look at projects like R5 or the R and Python language wrappers for it created by the community. + +Some analytics features may still be available as optional "sandbox" features in OTP2 (such as [Travel Time](sandbox/TravelTime.md)), but these do not work in the same way as the features you may have used or read about in OTP1. They are unmaintained and unsupported, and may be removed in the near future. + +## Terminology Note + +In OpenTripPlanner, we usually use the term "accessibility" with its most common meaning: design of products, devices, services, vehicles, or environments to ensure they are usable by people with disabilities. The term "accessibility" has a completely separate, unrelated definition in the fields of spatial analysis, urban transportation planning, and associated social sciences, where it refers to quantitative indicators of how well-connected a particular location is to people or opportunities. OTP has been widely used in research and planning settings for the calculation of such indicators. Although this meaning of the term dates back many decades, it is less well known and has become a source of confusion, so the academic and planning communities are gradually shifting to the expression "access to opportunities", often shortened to "access". diff --git a/docs/BuildConfiguration.md b/docs/BuildConfiguration.md index 3a3a6ff7ee4..7d2d90ea41b 100644 --- a/docs/BuildConfiguration.md +++ b/docs/BuildConfiguration.md @@ -35,7 +35,7 @@ Sections follow that describe particular settings in more depth. | maxTransferDuration | `duration` | Transfers up to this duration with the default walk speed value will be pre-calculated and included in the Graph. | *Optional* | `"PT30M"` | 2.1 | | [multiThreadElevationCalculations](#multiThreadElevationCalculations) | `boolean` | Configuring multi-threading during elevation calculations. | *Optional* | `false` | 2.0 | | [osmCacheDataInMem](#osmCacheDataInMem) | `boolean` | If OSM data should be cached in memory during processing. | *Optional* | `false` | 2.0 | -| osmNaming | `string` | A custom OSM namer to use. | *Optional* | | 2.0 | +| [osmNaming](#osmNaming) | `enum` | A custom OSM namer to use. | *Optional* | `"default"` | 1.5 | | platformEntriesLinking | `boolean` | Link unconnected entries to public transport platforms. | *Optional* | `false` | 2.0 | | [readCachedElevations](#readCachedElevations) | `boolean` | Whether to read cached elevation data. | *Optional* | `true` | 2.0 | | staticBikeParkAndRide | `boolean` | Whether we should create bike P+R stations from OSM data. | *Optional* | `false` | 1.5 | @@ -238,22 +238,6 @@ The mechanism is that for any two identical tags, OTP will use the first one. } ``` -### Custom naming - -You can define a custom naming scheme for elements drawn from OSM by defining an `osmNaming` field -in `build-config.json`, such as: - -```JSON -// build-config.json -{ - "osmNaming": "portland" -} -``` - -There is currently only one custom naming module called `portland` (which has no parameters). - - - ## Elevation data OpenTripPlanner can "drape" the OSM street network over a digital elevation model (DEM). This allows @@ -536,6 +520,14 @@ deployment depending on your infrastructure. Set the parameter to `true` to cach data, and to `false` to read the stream from the source each time. +

osmNaming

+ +**Since version:** `1.5` ∙ **Type:** `enum` ∙ **Cardinality:** `Optional` ∙ **Default value:** `"default"` +**Path:** / +**Enum values:** `default` | `portland` | `sidewalks` + +A custom OSM namer to use. +

readCachedElevations

**Since version:** `2.0` ∙ **Type:** `boolean` ∙ **Cardinality:** `Optional` ∙ **Default value:** `true` @@ -744,7 +736,7 @@ but we want to calculate the transfers always from OSM data. Should there be some preference or aversion for transfers at stops that are part of a station. This parameter sets the generic level of preference. What is the actual cost can be changed -with the `stopTransferCost` parameter in the router configuration. +with the `stopBoardAlightDuringTransferCost` parameter in the router configuration.

adaptivePruningDistance

@@ -993,7 +985,7 @@ but we want to calculate the transfers always from OSM data. Should there be some preference or aversion for transfers at stops that are part of a station. Overrides the value specified in `gtfsDefaults`. This parameter sets the generic level of preference. What is the actual cost can be changed -with the `stopTransferCost` parameter in the router configuration. +with the `stopBoardAlightDuringTransferCost` parameter in the router configuration.

groupFilePattern

diff --git a/docs/Changelog.md b/docs/Changelog.md index cafc677ccc6..55b76823b79 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -16,6 +16,11 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Fix trip duplication in Graph Builder DSJ mapping [#5794](https://github.com/opentripplanner/OpenTripPlanner/pull/5794) - Optionally abort startup when unknown configuration parameters were detected [#5676](https://github.com/opentripplanner/OpenTripPlanner/pull/5676) - Fix bug in heuristics cost calculation for egress legs [#5783](https://github.com/opentripplanner/OpenTripPlanner/pull/5783) +- Fix handling of implicit access and egress mode parameters. [#5821](https://github.com/opentripplanner/OpenTripPlanner/pull/5821) +- Add prettier:write to docs and rephrase slightly [ci skip] [#5831](https://github.com/opentripplanner/OpenTripPlanner/pull/5831) +- Make naming of stopTransferCosts/stopBoardAlightCosts consistent [#5822](https://github.com/opentripplanner/OpenTripPlanner/pull/5822) +- Namer for applying street names to nearby sidewalks [#5774](https://github.com/opentripplanner/OpenTripPlanner/pull/5774) +- Implement GTFS Flex safe duration spec draft [#5796](https://github.com/opentripplanner/OpenTripPlanner/pull/5796) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.5.0 (2024-03-13) diff --git a/docs/Codestyle.md b/docs/Codestyle.md index d127ff21e5c..d468937adfa 100644 --- a/docs/Codestyle.md +++ b/docs/Codestyle.md @@ -14,14 +14,21 @@ it takes a bit of time, but reformat the entire codebase. Only code you have cha formatted, since the existing code is already formatted. The second way is to set up prettier and run it manually or hick it into your IDE, so it runs every time a file is changed. -### How to run Prittier with Maven +### How to run Prettier with Maven -Format all code is done in the validate phase (run before test, package, install) +Prettier will automatically format all code in the Maven "validate" phase, which runs before the test, package, and install phases. So formatting will happen for example when you run: ``` % mvn test ``` +You can manually run _only_ the formatting process with: + +``` +% mvn prettier:write + +``` + To skip the prettier formating use profile `prettierSkip`: ``` @@ -31,7 +38,7 @@ To skip the prettier formating use profile `prettierSkip`: The check for formatting errors use profile `prettierCheck`: ``` -% mvn test -P prettierSkip +% mvn test -P prettierCheck ``` The check is run by the CI server and will fail the build if the code is incorrectly formatted. diff --git a/docs/Product-Overview.md b/docs/Product-Overview.md index cbf9dc3a1b8..dc0cfcc25a1 100644 --- a/docs/Product-Overview.md +++ b/docs/Product-Overview.md @@ -3,8 +3,9 @@ ## OpenTripPlanner project OpenTripPlanner is a group of open source software applications that help individuals and organizations -calculate and deliver multimodal trip plans based on OpenStreetMap (OSM) and other standardized data -sources (e.g. GTFS, GBFS, NeTEx). +calculate and deliver multimodal trip plans based on a combination of open-standard data sources. +These include public transit services and schedules (GTFS and NeTEx) and OpenStreetMap (OSM), as +well as sources describing bicycle sharing or rental, ride hailing, and other services (e.g. GBFS). A community of dozens of individuals and organizations work on OpenTripPlanner collaboratively to improve multimodal trip planning best practices and to make it easier for public transit agencies and @@ -12,7 +13,7 @@ public transit riders to publish and access information about transit services. OpenTripPlanner deployments are locally managed in many different ways by many different types of organizations. OpenTripPlanner consistently and dependably delivers multimodal trip plans to millions of riders -everyday in dozens of countries around the globe. The project is actively maintained by the community, +every day in dozens of countries around the globe. The project is actively maintained by the community, with more than 50 commits most weeks during 2022, and 20 different developers having made 50 or more commits during the life of the project. diff --git a/docs/RouteRequest.md b/docs/RouteRequest.md index ad7dc366bcf..52fd177fbf6 100644 --- a/docs/RouteRequest.md +++ b/docs/RouteRequest.md @@ -947,7 +947,7 @@ The wait time is used to prevent *back-travel*, the `backTravelWaitTimeFactor` i Add an extra board- and alight-cost for prioritized stops. -A stopBoardAlightCosts is added to the generalized-cost during routing. But this cost +A stopBoardAlightTransferCosts is added to the generalized-cost during routing. But this cost cannot be too high, because that would add extra cost to the transfer, and favor other alternative paths. But, when optimizing transfers, we do not have to take other paths into consideration and can *boost* the stop-priority-cost to allow transfers to diff --git a/docs/RouterConfiguration.md b/docs/RouterConfiguration.md index 918717d439f..663c9dd5c8a 100644 --- a/docs/RouterConfiguration.md +++ b/docs/RouterConfiguration.md @@ -61,7 +61,7 @@ A full list of them can be found in the [RouteRequest](RouteRequest.md). |       [minWindow](#transit_dynamicSearchWindow_minWindow) | `duration` | The constant minimum duration for a raptor-search-window. | *Optional* | `"PT40M"` | 2.2 | |       [stepMinutes](#transit_dynamicSearchWindow_stepMinutes) | `integer` | Used to set the steps the search-window is rounded to. | *Optional* | `10` | 2.1 | |    [pagingSearchWindowAdjustments](#transit_pagingSearchWindowAdjustments) | `duration[]` | The provided array of durations is used to increase the search-window for the next/previous page. | *Optional* | | na | -|    [stopTransferCost](#transit_stopTransferCost) | `enum map of integer` | Use this to set a stop transfer cost for the given transfer priority | *Optional* | | 2.0 | +|    [stopBoardAlightDuringTransferCost](#transit_stopBoardAlightDuringTransferCost) | `enum map of integer` | Costs for boarding and alighting during transfers at stops with a given transfer priority. | *Optional* | | 2.0 | |    [transferCacheRequests](#transit_transferCacheRequests) | `object[]` | Routing requests to use for pre-filling the stop-to-stop transfer cache. | *Optional* | | 2.3 | | transmodelApi | `object` | Configuration for the Transmodel GraphQL API. | *Optional* | | 2.1 | |    [hideFeedId](#transmodelApi_hideFeedId) | `boolean` | Hide the FeedId in all API output, and add it to input. | *Optional* | `false` | na | @@ -358,19 +358,23 @@ previous page cursor. See JavaDoc for [TransitTuningParameters#pagingSearchWindo for more info." -

stopTransferCost

+

stopBoardAlightDuringTransferCost

**Since version:** `2.0` ∙ **Type:** `enum map of integer` ∙ **Cardinality:** `Optional` **Path:** /transit **Enum keys:** `discouraged` | `allowed` | `recommended` | `preferred` -Use this to set a stop transfer cost for the given transfer priority +Costs for boarding and alighting during transfers at stops with a given transfer priority. -The cost is applied to boarding and alighting at all stops. All stops have a transfer cost priority -set, the default is `allowed`. The `stopTransferCost` parameter is optional, but if listed all -values must be set. +This cost is applied **both to boarding and alighting** at stops during transfers. All stops have a +transfer cost priority set, the default is `allowed`. The `stopBoardAlightDuringTransferCost` +parameter is optional, but if listed all values must be set. -If not set the `stopTransferCost` is ignored. This is only available for NeTEx imported Stops. +When a transfer occurs at the same stop, the cost will be applied twice since the cost is both for +boarding and alighting, + +If not set the `stopBoardAlightDuringTransferCost` is ignored. This is only available for NeTEx +imported Stops. The cost is a scalar, but is equivalent to the felt cost of riding a transit trip for 1 second. @@ -382,7 +386,7 @@ The cost is a scalar, but is equivalent to the felt cost of riding a transit tri | `preferred` | The best place to do transfers. Should be set to `0`(zero). | int | Use values in a range from `0` to `100 000`. **All key/value pairs are required if the -`stopTransferCost` is listed.** +`stopBoardAlightDuringTransferCost` is listed.**

transferCacheRequests

@@ -608,7 +612,7 @@ Used to group requests when monitoring OTP. "minWindow" : "1h", "maxWindow" : "5h" }, - "stopTransferCost" : { + "stopBoardAlightDuringTransferCost" : { "DISCOURAGED" : 1500, "ALLOWED" : 75, "RECOMMENDED" : 30, diff --git a/docs/Version-Comparison.md b/docs/Version-Comparison.md index 454188d022e..bc4a7caf5d8 100644 --- a/docs/Version-Comparison.md +++ b/docs/Version-Comparison.md @@ -81,35 +81,18 @@ other projects where they are more actively developed. ### Analysis -Many OpenTripPlanner contributors have been primarily interested in transportation and urban -planning use cases. We consider these use cases quite important. This has been a major area of -application for OpenTripPlanner and has helped popularize cumulative opportunities accessibility -metrics. For example, the University of Minnesota Accessibility Observatory used OpenTripPlanner -for [Access Across America](http://access.umn.edu/research/america/). Nonetheless, the analysis code -in OTP1 is essentially an unmaintained and unsupported early prototype for later projects, -specifically Conveyal's R5 (and the Conveyal Analysis system built upon it). OTP1 seems to have -gained popularity for analysis uses due to the existence of documentation and an active user -community, but has significant technical shortcomings. One of these is simply speed: OTP1 can be -orders of magnitude slower (and more memory-intensive) than the approaches exemplified in R5. The -other is the requirement to search at a single specific time. Travel times and especially wait times -on scheduled transit vary greatly depending on when you depart. Accounting for variation over a time -window requires repeated independent searches at each possible departure time, which is very -inefficient. R5 is highly optimized to capture variations in travel time across time windows and -account for uncertainty in waiting times on frequency-based routes. - -Due to its similarity to the R5 approach, OTP2's transit router would not have these same problems. -Nonetheless, we have decided not to port the OTP1 analysis features over to OTP2 since it would -broaden the focus away from passenger information and draw finite attention away from existing -projects like R5 and Conveyal Analysis. - -Accordingly, we have made an effort to clean up and augment OTP1 analysis documentation for -researchers who will continue to need it. It should remain possible for people to continue using -OTP1 if they prefer. If you would instead like to apply the innovations present in OTP2, we -recommend looking into R5 or Conveyal Analysis. +From the beginning of the project, many OTP contributors and users have used OTP in research, +analysis, and planning applications. They have prototyped many ideas within the OTP codebase, +including one-to-many searches producing travel time grids, isochrones, and access-to-opportunities +metrics. While some of these features may still be available as optional "sandbox" features in OTP2, they are unsupported and may be removed in the near future. + +Most work of this kind moved over separate projects focused on urban planning and analytics. As of +version 2, OTP has chosen to focus entirely on passenger information rather than analytics +applications. See the [Analysis](Analysis.md) page for more details. ### Routers API and Hot Reloading -Via it's Routers API, OTP1 allows loading data and serving APIs for multiple separate geographic +Via its Routers API, OTP1 allows loading data and serving APIs for multiple separate geographic areas. This is functionally equivalent to running more than one OTP server with separate data sets. This system also allows reloading transportation network data when it changes, or even pushing new data over a network connection. diff --git a/docs/apis/GTFS-GraphQL-API.md b/docs/apis/GTFS-GraphQL-API.md index f2cc32e2888..cda512bc689 100644 --- a/docs/apis/GTFS-GraphQL-API.md +++ b/docs/apis/GTFS-GraphQL-API.md @@ -11,7 +11,7 @@ makes it easy to use this API in a Java application. ## URLs - GraphQL endpoint: [`http://localhost:8080/otp/gtfs/v1`](http://localhost:8080/otp/gtfs/v1) - HTML schema documentation: [https://docs.opentripplanner.org/api/dev-2.x/graphql-gtfs/](https://docs.opentripplanner.org/api/dev-2.x/graphql-gtfs/) - - Built-in visual GraphQL client: [http://localhost:8080/graphiql](http://localhost:8080/graphiql) + - Built-in visual GraphQL client: [http://localhost:8080/graphiql](http://localhost:8080/graphiql) (note the additional `i`!) ## Built-in API client @@ -43,4 +43,4 @@ If you want to disable it, do it in `otp-config.json`: "GtfsGraphQlApi": false } } -``` \ No newline at end of file +``` diff --git a/docs/examples/entur/router-config.json b/docs/examples/entur/router-config.json index 652f43f74bc..0cbded8bc85 100644 --- a/docs/examples/entur/router-config.json +++ b/docs/examples/entur/router-config.json @@ -91,7 +91,7 @@ "minWindow" : "1h", "maxWindow" : "5h" }, - "stopTransferCost" : { + "stopBoardAlightDuringTransferCost" : { "DISCOURAGED" : 1500, "ALLOWED" : 75, "RECOMMENDED" : 30, diff --git a/docs/examples/skanetrafiken/router-config.json b/docs/examples/skanetrafiken/router-config.json index 74042d68e31..6d782879a52 100644 --- a/docs/examples/skanetrafiken/router-config.json +++ b/docs/examples/skanetrafiken/router-config.json @@ -15,7 +15,7 @@ "24h", "0h" ], - "stopTransferCost": { + "stopBoardAlightDuringTransferCost": { "DISCOURAGED": 3000, "ALLOWED": 150, "RECOMMENDED": 60, diff --git a/docs/sandbox/Flex.md b/docs/sandbox/Flex.md index 61d15851a56..7b08fd7d05f 100644 --- a/docs/sandbox/Flex.md +++ b/docs/sandbox/Flex.md @@ -10,8 +10,19 @@ To enable this turn on `FlexRouting` as a feature in `otp-config.json`. -The GTFS feeds should conform to the -[GTFS-Flex v2 draft PR](https://github.com/google/transit/pull/388) +The GTFS feeds must conform to the final, approved version of the draft which has been +merged into the [mainline specification](https://gtfs.org/schedule/reference/) in March 2024. + +### Experimental features + +This sandbox feature also has experimental support for the following fields: + +- `safe_duration_factor` +- `safe_duration_offset` + +These features are currently [undergoing specification](https://github.com/MobilityData/gtfs-flex/pull/79) +and their definition might change. OTP's implementation will be also be changed so be careful +when relying on this feature. ## Configuration diff --git a/docs/sandbox/IBIAccessibilityScore.md b/docs/sandbox/IBIAccessibilityScore.md index e3c6ef16bbd..5b944eaf73b 100644 --- a/docs/sandbox/IBIAccessibilityScore.md +++ b/docs/sandbox/IBIAccessibilityScore.md @@ -1,14 +1,9 @@ -# IBI Group Accessibility Score - OTP Sandbox Extension +# IBI Group Accessibility Score ## Contact Info - IBI Group ([transitrealtime@ibigroup.com](mailto:transitrealtime@ibigroup.com)) -## Changelog - -- Create initial - implementation [#4221](https://github.com/opentripplanner/OpenTripPlanner/pull/4221) - ## Documentation This extension computes a numeric accessibility score between 0 and 1 and adds it to the itinerary @@ -32,4 +27,8 @@ To enable the feature add the following to `router-config.json`: } ``` -The score is only computed when you search for wheelchair-accessible routes. \ No newline at end of file +The score is only computed when you search for wheelchair-accessible routes. + +## Changelog + +- Create initial implementation [#4221](https://github.com/opentripplanner/OpenTripPlanner/pull/4221) diff --git a/docs/sandbox/TravelTime.md b/docs/sandbox/TravelTime.md index 335d454c529..817f71506f3 100644 --- a/docs/sandbox/TravelTime.md +++ b/docs/sandbox/TravelTime.md @@ -11,7 +11,11 @@ ## Documentation The API produces a snapshot of travel time form a single place to places around it. The results can -be fetched either as a set of isochrones or a raster map. +be fetched either as a set of isochrones or a raster map. Please note that as a sandbox feature this +functionality is UNSUPPORTED and neither maintained nor well-understood by most current OTP +developers, and may not be accurate or reliable. Travel time analytics work that began within OTP +has moved years ago to other projects, where it actively continues. See the +[Analysis](../Analysis.md) page for further details. ### Configuration diff --git a/docs/sandbox/siri/SiriAzureUpdater.md b/docs/sandbox/siri/SiriAzureUpdater.md index c6ddf9f3ebe..7b29e802f21 100644 --- a/docs/sandbox/siri/SiriAzureUpdater.md +++ b/docs/sandbox/siri/SiriAzureUpdater.md @@ -1,7 +1,8 @@ # Siri Azure Updater -It is sandbox extension developed by Skånetrafiken that allows OTP to fetch Siri ET & SX messages through *Azure Service Bus*. -IT also OTP to download historical data from en HTTP endpoint on startup. +This is a sandbox extension developed by Skånetrafiken that allows OTP to fetch Siri ET & SX messages +through *Azure Service Bus*. +It also enables OTP to download historical data from en HTTP endpoint on startup. ## Contact Info diff --git a/magidoc.mjs b/magidoc.mjs index a57b17a4308..4fea5e4e127 100644 --- a/magidoc.mjs +++ b/magidoc.mjs @@ -29,7 +29,7 @@ at http://localhost:8080/graphiql This API is activated by default. To learn how to deactivate it, read the -[documentation](https://docs.opentripplanner.org/en/dev-2.x/apis/GTFS-GraphQ-API/). +[documentation](https://docs.opentripplanner.org/en/dev-2.x/apis/GTFS-GraphQL-API/). `, }], appTitle: 'OTP GTFS GraphQL API', diff --git a/mkdocs.yml b/mkdocs.yml index c14e451a906..da976615d1b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -84,6 +84,7 @@ nav: - "Stop Area Relations": 'StopAreas.md' - "Street Graph Pruning": 'IslandPruning.md' - Accessibility: 'Accessibility.md' + - "Travel Time Analysis": 'Analysis.md' - "Logging": "Logging.md" - Development: - "Developers' Guide": 'Developers-Guide.md' diff --git a/pom.xml b/pom.xml index 5230cf472f2..2169a4bfadb 100644 --- a/pom.xml +++ b/pom.xml @@ -56,11 +56,11 @@ - 149 + 150 31.0 2.51.1 - 2.17.0 + 2.17.1 3.1.6 5.10.2 1.12.3 @@ -706,7 +706,7 @@ org.mockito mockito-core - 5.11.0 + 5.12.0 test @@ -831,7 +831,7 @@ org.onebusaway onebusaway-gtfs - 1.4.15 + 1.4.17 org.slf4j diff --git a/src/client/debug-client-preview/index.html b/src/client/debug-client-preview/index.html index 8968324b94d..974356161cb 100644 --- a/src/client/debug-client-preview/index.html +++ b/src/client/debug-client-preview/index.html @@ -5,8 +5,8 @@ OTP Debug Client - - + +
diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/FlexStopTimesForTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/FlexStopTimesForTest.java new file mode 100644 index 00000000000..d76382999a2 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/flex/FlexStopTimesForTest.java @@ -0,0 +1,49 @@ +package org.opentripplanner.ext.flex; + +import static org.opentripplanner.model.StopTime.MISSING_VALUE; + +import org.opentripplanner._support.geometry.Polygons; +import org.opentripplanner.framework.time.TimeUtils; +import org.opentripplanner.model.StopTime; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.StopLocation; + +public class FlexStopTimesForTest { + + private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); + private static final StopLocation AREA_STOP = TEST_MODEL.areaStopForTest("area", Polygons.BERLIN); + private static final RegularStop REGULAR_STOP = TEST_MODEL.stop("stop").build(); + + public static StopTime area(String startTime, String endTime) { + return area(AREA_STOP, endTime, startTime); + } + + public static StopTime area(StopLocation areaStop, String endTime, String startTime) { + var stopTime = new StopTime(); + stopTime.setStop(areaStop); + stopTime.setFlexWindowStart(TimeUtils.time(startTime)); + stopTime.setFlexWindowEnd(TimeUtils.time(endTime)); + return stopTime; + } + + public static StopTime regularArrival(String arrivalTime) { + return regularStopTime(TimeUtils.time(arrivalTime), MISSING_VALUE); + } + + public static StopTime regularStopTime(String arrivalTime, String departureTime) { + return regularStopTime(TimeUtils.time(arrivalTime), TimeUtils.time(departureTime)); + } + + public static StopTime regularStopTime(int arrivalTime, int departureTime) { + var stopTime = new StopTime(); + stopTime.setStop(REGULAR_STOP); + stopTime.setArrivalTime(arrivalTime); + stopTime.setDepartureTime(departureTime); + return stopTime; + } + + public static StopTime regularDeparture(String departureTime) { + return regularStopTime(MISSING_VALUE, TimeUtils.time(departureTime)); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPathTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPathTest.java new file mode 100644 index 00000000000..8bd3abee785 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPathTest.java @@ -0,0 +1,38 @@ +package org.opentripplanner.ext.flex.flexpathcalculator; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; +import java.util.List; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner._support.geometry.LineStrings; +import org.opentripplanner.routing.api.request.framework.TimePenalty; + +class FlexPathTest { + + private static final int THIRTY_MINS_IN_SECONDS = (int) Duration.ofMinutes(30).toSeconds(); + private static final FlexPath PATH = new FlexPath( + 10_000, + THIRTY_MINS_IN_SECONDS, + () -> LineStrings.SIMPLE + ); + + static List cases() { + return List.of( + Arguments.of(TimePenalty.ZERO, THIRTY_MINS_IN_SECONDS), + Arguments.of(TimePenalty.of(Duration.ofMinutes(10), 1), 2400), + Arguments.of(TimePenalty.of(Duration.ofMinutes(10), 1.5f), 3300), + Arguments.of(TimePenalty.of(Duration.ZERO, 3), 5400) + ); + } + + @ParameterizedTest + @MethodSource("cases") + void calculate(TimePenalty mod, int expectedSeconds) { + var modified = PATH.withDurationModifier(mod); + assertEquals(expectedSeconds, modified.durationSeconds); + assertEquals(LineStrings.SIMPLE, modified.getGeometry()); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculatorTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculatorTest.java new file mode 100644 index 00000000000..70f39af2420 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculatorTest.java @@ -0,0 +1,38 @@ +package org.opentripplanner.ext.flex.flexpathcalculator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.ext.flex.FlexStopTimesForTest.area; +import static org.opentripplanner.ext.flex.FlexStopTimesForTest.regularStopTime; +import static org.opentripplanner.street.model._data.StreetModelForTest.V1; +import static org.opentripplanner.street.model._data.StreetModelForTest.V2; +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; + +import java.time.Duration; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner._support.geometry.LineStrings; +import org.opentripplanner.ext.flex.trip.ScheduledDeviatedTrip; + +class ScheduledFlexPathCalculatorTest { + + private static final ScheduledDeviatedTrip TRIP = ScheduledDeviatedTrip + .of(id("123")) + .withStopTimes( + List.of( + regularStopTime("10:00", "10:01"), + area("10:10", "10:20"), + regularStopTime("10:25", "10:26"), + area("10:40", "10:50") + ) + ) + .build(); + + @Test + void calculateTime() { + var c = (FlexPathCalculator) (fromv, tov, fromStopIndex, toStopIndex) -> + new FlexPath(10_000, (int) Duration.ofMinutes(10).toSeconds(), () -> LineStrings.SIMPLE); + var calc = new ScheduledFlexPathCalculator(c, TRIP); + var path = calc.calculateFlexPath(V1, V2, 0, 1); + assertEquals(Duration.ofMinutes(19), Duration.ofSeconds(path.durationSeconds)); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculatorTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculatorTest.java new file mode 100644 index 00000000000..e504f8ac762 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculatorTest.java @@ -0,0 +1,35 @@ +package org.opentripplanner.ext.flex.flexpathcalculator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.time.Duration; +import org.junit.jupiter.api.Test; +import org.opentripplanner._support.geometry.LineStrings; +import org.opentripplanner.routing.api.request.framework.TimePenalty; +import org.opentripplanner.street.model._data.StreetModelForTest; + +class TimePenaltyCalculatorTest { + + private static final int THIRTY_MINS_IN_SECONDS = (int) Duration.ofMinutes(30).toSeconds(); + + @Test + void calculate() { + FlexPathCalculator delegate = (fromv, tov, fromStopIndex, toStopIndex) -> + new FlexPath(10_000, THIRTY_MINS_IN_SECONDS, () -> LineStrings.SIMPLE); + + var mod = TimePenalty.of(Duration.ofMinutes(10), 1.5f); + var calc = new TimePenaltyCalculator(delegate, mod); + var path = calc.calculateFlexPath(StreetModelForTest.V1, StreetModelForTest.V2, 0, 5); + assertEquals(3300, path.durationSeconds); + } + + @Test + void nullValue() { + FlexPathCalculator delegate = (fromv, tov, fromStopIndex, toStopIndex) -> null; + var mod = TimePenalty.of(Duration.ofMinutes(10), 1.5f); + var calc = new TimePenaltyCalculator(delegate, mod); + var path = calc.calculateFlexPath(StreetModelForTest.V1, StreetModelForTest.V2, 0, 5); + assertNull(path); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledDrivingDurationTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledDrivingDurationTest.java new file mode 100644 index 00000000000..a4b245b7de8 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledDrivingDurationTest.java @@ -0,0 +1,45 @@ +package org.opentripplanner.ext.flex.trip; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.street.model._data.StreetModelForTest.V1; +import static org.opentripplanner.street.model._data.StreetModelForTest.V2; +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; + +import java.time.Duration; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner._support.geometry.LineStrings; +import org.opentripplanner.ext.flex.FlexStopTimesForTest; +import org.opentripplanner.ext.flex.flexpathcalculator.FlexPath; +import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; +import org.opentripplanner.model.StopTime; +import org.opentripplanner.routing.api.request.framework.TimePenalty; + +class UnscheduledDrivingDurationTest { + + static final FlexPathCalculator STATIC_CALCULATOR = (fromv, tov, fromStopIndex, toStopIndex) -> + new FlexPath(10_000, (int) Duration.ofMinutes(10).toSeconds(), () -> LineStrings.SIMPLE); + private static final StopTime STOP_TIME = FlexStopTimesForTest.area("10:00", "18:00"); + + @Test + void noPenalty() { + var trip = UnscheduledTrip.of(id("1")).withStopTimes(List.of(STOP_TIME)).build(); + + var calculator = trip.flexPathCalculator(STATIC_CALCULATOR); + var path = calculator.calculateFlexPath(V1, V2, 0, 0); + assertEquals(600, path.durationSeconds); + } + + @Test + void withPenalty() { + var trip = UnscheduledTrip + .of(id("1")) + .withStopTimes(List.of(STOP_TIME)) + .withTimePenalty(TimePenalty.of(Duration.ofMinutes(2), 1.5f)) + .build(); + + var calculator = trip.flexPathCalculator(STATIC_CALCULATOR); + var path = calculator.calculateFlexPath(V1, V2, 0, 0); + assertEquals(1020, path.durationSeconds); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java index fabe534ff23..80bda31fabf 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java @@ -3,6 +3,9 @@ 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.ext.flex.FlexStopTimesForTest.area; +import static org.opentripplanner.ext.flex.FlexStopTimesForTest.regularArrival; +import static org.opentripplanner.ext.flex.FlexStopTimesForTest.regularDeparture; import static org.opentripplanner.ext.flex.trip.UnscheduledTrip.isUnscheduledTrip; import static org.opentripplanner.ext.flex.trip.UnscheduledTripTest.TestCase.tc; import static org.opentripplanner.model.PickDrop.NONE; @@ -48,11 +51,10 @@ class UnscheduledTripTest { private static final int T15_00 = TimeUtils.hm2time(15, 0); private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); + private static final StopLocation AREA_STOP = TEST_MODEL.areaStopForTest("area", Polygons.BERLIN); private static final RegularStop REGULAR_STOP = TEST_MODEL.stop("stop").build(); - private static final StopLocation AREA_STOP = TEST_MODEL.areaStopForTest("area", Polygons.BERLIN); - @Nested class IsUnscheduledTrip { @@ -661,35 +663,6 @@ private static String timeToString(int time) { return TimeUtils.timeToStrCompact(time, MISSING_VALUE, "MISSING_VALUE"); } - private static StopTime area(String startTime, String endTime) { - return area(AREA_STOP, endTime, startTime); - } - - @Nonnull - private static StopTime area(StopLocation areaStop, String endTime, String startTime) { - var stopTime = new StopTime(); - stopTime.setStop(areaStop); - stopTime.setFlexWindowStart(TimeUtils.time(startTime)); - stopTime.setFlexWindowEnd(TimeUtils.time(endTime)); - return stopTime; - } - - private static StopTime regularDeparture(String departureTime) { - return regularStopTime(MISSING_VALUE, TimeUtils.time(departureTime)); - } - - private static StopTime regularArrival(String arrivalTime) { - return regularStopTime(TimeUtils.time(arrivalTime), MISSING_VALUE); - } - - private static StopTime regularStopTime(int arrivalTime, int departureTime) { - var stopTime = new StopTime(); - stopTime.setStop(REGULAR_STOP); - stopTime.setArrivalTime(arrivalTime); - stopTime.setDepartureTime(departureTime); - return stopTime; - } - @Nonnull private static NearbyStop nearbyStop(AreaStop stop) { return new NearbyStop(stop, 1000, List.of(), null); diff --git a/src/ext/java/org/opentripplanner/ext/flex/FlexTripsMapper.java b/src/ext/java/org/opentripplanner/ext/flex/FlexTripsMapper.java index f5ab34cce49..c4167f2f9e1 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/FlexTripsMapper.java +++ b/src/ext/java/org/opentripplanner/ext/flex/FlexTripsMapper.java @@ -12,6 +12,7 @@ import org.opentripplanner.model.StopTime; import org.opentripplanner.model.TripStopTimes; import org.opentripplanner.model.impl.OtpTransitServiceBuilder; +import org.opentripplanner.routing.api.request.framework.TimePenalty; import org.opentripplanner.transit.model.timetable.Trip; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,12 +33,16 @@ public class FlexTripsMapper { ProgressTracker progress = ProgressTracker.track("Create flex trips", 500, tripSize); for (Trip trip : stopTimesByTrip.keys()) { - /* Fetch the stop times for this trip. Copy the list since it's immutable. */ - List stopTimes = new ArrayList<>(stopTimesByTrip.get(trip)); - + var stopTimes = stopTimesByTrip.get(trip); if (UnscheduledTrip.isUnscheduledTrip(stopTimes)) { + var timePenalty = builder.getFlexTimePenalty().getOrDefault(trip, TimePenalty.NONE); result.add( - UnscheduledTrip.of(trip.getId()).withTrip(trip).withStopTimes(stopTimes).build() + UnscheduledTrip + .of(trip.getId()) + .withTrip(trip) + .withStopTimes(stopTimes) + .withTimePenalty(timePenalty) + .build() ); } else if (ScheduledDeviatedTrip.isScheduledFlexTrip(stopTimes)) { result.add( diff --git a/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java b/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java index fac1118556f..b9e10e29214 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java +++ b/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java @@ -196,6 +196,7 @@ public int getGeneralizedCost() { return generalizedCost; } + @Override public void addAlert(TransitAlert alert) { transitAlerts.add(alert); } diff --git a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPath.java b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPath.java index 5c5557890d6..3a692dff40b 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPath.java +++ b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPath.java @@ -1,11 +1,16 @@ package org.opentripplanner.ext.flex.flexpathcalculator; +import java.time.Duration; import java.util.function.Supplier; +import javax.annotation.concurrent.Immutable; import org.locationtech.jts.geom.LineString; +import org.opentripplanner.framework.lang.IntUtils; +import org.opentripplanner.routing.api.request.framework.TimePenalty; /** * This class contains the results from a FlexPathCalculator. */ +@Immutable public class FlexPath { private final Supplier geometrySupplier; @@ -22,7 +27,7 @@ public class FlexPath { */ public FlexPath(int distanceMeters, int durationSeconds, Supplier geometrySupplier) { this.distanceMeters = distanceMeters; - this.durationSeconds = durationSeconds; + this.durationSeconds = IntUtils.requireNotNegative(durationSeconds); this.geometrySupplier = geometrySupplier; } @@ -32,4 +37,16 @@ public LineString getGeometry() { } return geometry; } + + /** + * Returns an (immutable) copy of this path with the duration modified. + */ + public FlexPath withDurationModifier(TimePenalty mod) { + if (mod.isZero()) { + return this; + } else { + int updatedDuration = (int) mod.calculate(Duration.ofSeconds(durationSeconds)).toSeconds(); + return new FlexPath(distanceMeters, updatedDuration, geometrySupplier); + } + } } diff --git a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculator.java b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculator.java index 7d953abc4fd..cd5228dada5 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculator.java +++ b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculator.java @@ -20,7 +20,7 @@ public ScheduledFlexPathCalculator(FlexPathCalculator flexPathCalculator, FlexTr @Override public FlexPath calculateFlexPath(Vertex fromv, Vertex tov, int fromStopIndex, int toStopIndex) { - FlexPath flexPath = flexPathCalculator.calculateFlexPath( + final var flexPath = flexPathCalculator.calculateFlexPath( fromv, tov, fromStopIndex, @@ -29,7 +29,6 @@ public FlexPath calculateFlexPath(Vertex fromv, Vertex tov, int fromStopIndex, i if (flexPath == null) { return null; } - int distance = flexPath.distanceMeters; int departureTime = trip.earliestDepartureTime( Integer.MIN_VALUE, fromStopIndex, @@ -50,6 +49,10 @@ public FlexPath calculateFlexPath(Vertex fromv, Vertex tov, int fromStopIndex, i if (departureTime >= arrivalTime) { return null; } - return new FlexPath(distance, arrivalTime - departureTime, flexPath::getGeometry); + return new FlexPath( + flexPath.distanceMeters, + arrivalTime - departureTime, + flexPath::getGeometry + ); } } diff --git a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculator.java b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculator.java new file mode 100644 index 00000000000..63b661f0f9a --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculator.java @@ -0,0 +1,32 @@ +package org.opentripplanner.ext.flex.flexpathcalculator; + +import javax.annotation.Nullable; +import org.opentripplanner.routing.api.request.framework.TimePenalty; +import org.opentripplanner.street.model.vertex.Vertex; + +/** + * A calculator to delegates the main computation to another instance and applies a duration + * modifier afterward. + */ +public class TimePenaltyCalculator implements FlexPathCalculator { + + private final FlexPathCalculator delegate; + private final TimePenalty factors; + + public TimePenaltyCalculator(FlexPathCalculator delegate, TimePenalty penalty) { + this.delegate = delegate; + this.factors = penalty; + } + + @Nullable + @Override + public FlexPath calculateFlexPath(Vertex fromv, Vertex tov, int fromStopIndex, int toStopIndex) { + var path = delegate.calculateFlexPath(fromv, tov, fromStopIndex, toStopIndex); + + if (path == null) { + return null; + } else { + return path.withDurationModifier(factors); + } + } +} diff --git a/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTrip.java b/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTrip.java index c5968507676..402d39e2aa7 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTrip.java +++ b/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTrip.java @@ -5,6 +5,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -15,12 +16,16 @@ import javax.annotation.Nonnull; import org.opentripplanner.ext.flex.FlexServiceDate; import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; +import org.opentripplanner.ext.flex.flexpathcalculator.TimePenaltyCalculator; import org.opentripplanner.ext.flex.template.FlexAccessTemplate; import org.opentripplanner.ext.flex.template.FlexEgressTemplate; +import org.opentripplanner.framework.lang.DoubleUtils; import org.opentripplanner.framework.lang.IntRange; +import org.opentripplanner.framework.time.DurationUtils; import org.opentripplanner.model.BookingInfo; import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.StopTime; +import org.opentripplanner.routing.api.request.framework.TimePenalty; import org.opentripplanner.routing.graphfinder.NearbyStop; import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -50,6 +55,8 @@ public class UnscheduledTrip extends FlexTrip stopTimes = builder.stopTimes(); @@ -64,9 +71,12 @@ public UnscheduledTrip(UnscheduledTripBuilder builder) { for (int i = 0; i < size; i++) { this.stopTimes[i] = new StopTimeWindow(stopTimes.get(i)); - this.dropOffBookingInfos[i] = stopTimes.get(0).getDropOffBookingInfo(); - this.pickupBookingInfos[i] = stopTimes.get(0).getPickupBookingInfo(); + this.dropOffBookingInfos[i] = stopTimes.get(i).getDropOffBookingInfo(); + this.pickupBookingInfos[i] = stopTimes.get(i).getPickupBookingInfo(); } + this.timePenalty = Objects.requireNonNull(builder.timePenalty()); + DurationUtils.requireNonNegative(timePenalty.constant()); + DoubleUtils.requireInRange(timePenalty.coefficient(), 0.05d, Double.MAX_VALUE); } public static UnscheduledTripBuilder of(FeedScopedId id) { @@ -81,8 +91,6 @@ public static UnscheduledTripBuilder of(FeedScopedId id) { * - One or more stop times with a flexible time window but no fixed stop in between them */ public static boolean isUnscheduledTrip(List stopTimes) { - Predicate hasFlexWindow = st -> - st.getFlexWindowStart() != MISSING_VALUE || st.getFlexWindowEnd() != MISSING_VALUE; Predicate hasContinuousStops = stopTime -> stopTime.getFlexContinuousDropOff() != NONE || stopTime.getFlexContinuousPickup() != NONE; if (stopTimes.isEmpty()) { @@ -90,9 +98,9 @@ public static boolean isUnscheduledTrip(List stopTimes) { } else if (stopTimes.stream().anyMatch(hasContinuousStops)) { return false; } else if (N_STOPS.contains(stopTimes.size())) { - return stopTimes.stream().anyMatch(hasFlexWindow); + return stopTimes.stream().anyMatch(StopTime::hasFlexWindow); } else { - return stopTimes.stream().allMatch(hasFlexWindow); + return stopTimes.stream().allMatch(StopTime::hasFlexWindow); } } @@ -120,6 +128,9 @@ public Stream getFlexAccessTemplates( } else { indices = IntStream.range(fromIndex + 1, lastIndexInTrip + 1); } + + final var updatedCalculator = flexPathCalculator(calculator); + // check for every stop after fromIndex if you can alight, if so return a template return indices // if you cannot alight at an index, the trip is not possible @@ -137,12 +148,24 @@ public Stream getFlexAccessTemplates( alightStop.index, alightStop.stop, date, - calculator, + updatedCalculator, config ) ); } + /** + * Get the correct {@link FlexPathCalculator} depending on the {@code timePenalty}. + * If the modifier doesn't actually modify, we return the regular calculator. + */ + protected FlexPathCalculator flexPathCalculator(FlexPathCalculator calculator) { + if (timePenalty.modifies()) { + return new TimePenaltyCalculator(calculator, timePenalty); + } else { + return calculator; + } + } + @Override public Stream getFlexEgressTemplates( NearbyStop egress, diff --git a/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTripBuilder.java b/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTripBuilder.java index 678b7fcce5e..1f8585f5ad0 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTripBuilder.java +++ b/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTripBuilder.java @@ -2,12 +2,14 @@ import java.util.List; import org.opentripplanner.model.StopTime; +import org.opentripplanner.routing.api.request.framework.TimePenalty; import org.opentripplanner.transit.model.framework.FeedScopedId; public class UnscheduledTripBuilder extends FlexTripBuilder { private List stopTimes; + private TimePenalty timePenalty = TimePenalty.NONE; UnscheduledTripBuilder(FeedScopedId id) { super(id); @@ -29,6 +31,15 @@ public List stopTimes() { return stopTimes; } + public UnscheduledTripBuilder withTimePenalty(TimePenalty factors) { + this.timePenalty = factors; + return this; + } + + public TimePenalty timePenalty() { + return timePenalty; + } + @Override UnscheduledTripBuilder instance() { return this; diff --git a/src/ext/java/org/opentripplanner/ext/restapi/mapping/TripMapper.java b/src/ext/java/org/opentripplanner/ext/restapi/mapping/TripMapper.java index f4894a4a7a3..1f7abd3897a 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/mapping/TripMapper.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/mapping/TripMapper.java @@ -31,7 +31,6 @@ public static ApiTrip mapToApi(Trip obj) { api.shapeId = FeedScopedIdMapper.mapToApi(obj.getShapeId()); api.wheelchairAccessible = WheelchairAccessibilityMapper.mapToApi(obj.getWheelchairBoarding()); api.bikesAllowed = BikeAccessMapper.mapToApi(obj.getBikesAllowed()); - api.fareId = obj.getGtfsFareId(); return api; } diff --git a/src/ext/java/org/opentripplanner/ext/ridehailing/service/uber/UberService.java b/src/ext/java/org/opentripplanner/ext/ridehailing/service/uber/UberService.java index bfc65566e6e..9b276fc053e 100644 --- a/src/ext/java/org/opentripplanner/ext/ridehailing/service/uber/UberService.java +++ b/src/ext/java/org/opentripplanner/ext/ridehailing/service/uber/UberService.java @@ -25,6 +25,7 @@ import org.opentripplanner.ext.ridehailing.service.oauth.UrlEncodedOAuthService; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.framework.json.ObjectMappers; import org.opentripplanner.transit.model.basic.Money; import org.slf4j.Logger; @@ -79,7 +80,7 @@ public UberService(RideHailingServiceParameters config) { this.timeEstimateUri = timeEstimateUri; this.bannedTypes = bannedTypes; this.wheelchairAccessibleProductId = wheelchairAccessibleProductId; - this.otpHttpClient = new OtpHttpClient(); + this.otpHttpClient = new OtpHttpClientFactory().create(LOG); } @Override diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETGooglePubsubUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETGooglePubsubUpdater.java index 4b5797f03de..9a1c5690c58 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETGooglePubsubUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETGooglePubsubUpdater.java @@ -28,7 +28,7 @@ import org.opentripplanner.ext.siri.SiriFuzzyTripMatcher; import org.opentripplanner.ext.siri.SiriTimetableSnapshotSource; import org.opentripplanner.framework.application.ApplicationShutdownSupport; -import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.framework.retry.OtpRetry; import org.opentripplanner.framework.retry.OtpRetryBuilder; import org.opentripplanner.framework.text.FileSizeToTextConverter; @@ -312,7 +312,8 @@ private void initializeData() { } private ByteString fetchInitialData() { - try (OtpHttpClient otpHttpClient = new OtpHttpClient()) { + try (OtpHttpClientFactory otpHttpClientFactory = new OtpHttpClientFactory()) { + var otpHttpClient = otpHttpClientFactory.create(LOG); return otpHttpClient.getAndMap( dataInitializationUrl, initialGetDataTimeout, diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriHttpLoader.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriHttpLoader.java index 7a094dc0863..daf3d0397cc 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriHttpLoader.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriHttpLoader.java @@ -4,6 +4,7 @@ import java.time.Duration; import java.util.Optional; import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.updater.spi.HttpHeaders; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,7 +36,7 @@ public SiriHttpLoader( this.timeout = timeout; this.requestHeaders = requestHeaders; this.previewInterval = previewInterval; - this.otpHttpClient = new OtpHttpClient(timeout, timeout); + this.otpHttpClient = new OtpHttpClientFactory(timeout, timeout).create(LOG); } /** diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java index 1915105960e..f211468cc66 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java @@ -25,7 +25,7 @@ import org.opentripplanner.ext.siri.EntityResolver; import org.opentripplanner.ext.siri.SiriFuzzyTripMatcher; import org.opentripplanner.framework.application.ApplicationShutdownSupport; -import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; import org.opentripplanner.transit.service.TransitService; @@ -206,7 +206,8 @@ public String getConfigRef() { protected Optional fetchInitialSiriData(URI uri) { var headers = HttpHeaders.of().acceptApplicationXML().build().asMap(); - try (OtpHttpClient otpHttpClient = new OtpHttpClient()) { + try (OtpHttpClientFactory otpHttpClientFactory = new OtpHttpClientFactory()) { + var otpHttpClient = otpHttpClientFactory.create(LOG); var t1 = System.currentTimeMillis(); var siriOptional = otpHttpClient.executeAndMapOptional( new HttpGet(uri), diff --git a/src/ext/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSource.java b/src/ext/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSource.java index abeaab42137..3876ff44651 100644 --- a/src/ext/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSource.java +++ b/src/ext/java/org/opentripplanner/ext/smoovebikerental/SmooveBikeRentalDataSource.java @@ -4,6 +4,7 @@ import java.util.Map; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; @@ -23,7 +24,7 @@ public class SmooveBikeRentalDataSource extends GenericJsonDataSource implements VehicleRentalDatasource { - private static final Logger log = LoggerFactory.getLogger(SmooveBikeRentalDataSource.class); + private static final Logger LOG = LoggerFactory.getLogger(SmooveBikeRentalDataSource.class); public static final String DEFAULT_NETWORK_NAME = "smoove"; @@ -33,14 +34,14 @@ public class SmooveBikeRentalDataSource private final RentalVehicleType vehicleType; public SmooveBikeRentalDataSource(SmooveBikeRentalDataSourceParameters config) { - this(config, new OtpHttpClient()); + this(config, new OtpHttpClientFactory()); } public SmooveBikeRentalDataSource( SmooveBikeRentalDataSourceParameters config, - OtpHttpClient otpHttpClient + OtpHttpClientFactory otpHttpClientFactory ) { - super(config.url(), "result", config.httpHeaders(), otpHttpClient); + super(config.url(), "result", config.httpHeaders(), otpHttpClientFactory.create(LOG)); networkName = config.getNetwork(DEFAULT_NETWORK_NAME); vehicleType = RentalVehicleType.getDefaultType(networkName); overloadingAllowed = config.overloadingAllowed(); @@ -76,7 +77,7 @@ protected VehicleRentalStation parseElement(JsonNode node) { station.longitude = Double.parseDouble(coordinates[1].trim()); } catch (NumberFormatException e) { // E.g. coordinates is empty - log.warn("Error parsing bike rental station {}", station.id, e); + LOG.warn("Error parsing bike rental station {}", station.id, e); return null; } if (!node.path("style").asText().equals("Station on")) { diff --git a/src/ext/java/org/opentripplanner/ext/vehicleparking/bikely/BikelyUpdater.java b/src/ext/java/org/opentripplanner/ext/vehicleparking/bikely/BikelyUpdater.java index 5758d5d99e1..2bed1121913 100644 --- a/src/ext/java/org/opentripplanner/ext/vehicleparking/bikely/BikelyUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/vehicleparking/bikely/BikelyUpdater.java @@ -15,6 +15,7 @@ import org.opentripplanner.framework.i18n.LocalizedString; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.framework.json.ObjectMappers; import org.opentripplanner.routing.vehicle_parking.VehicleParking; import org.opentripplanner.routing.vehicle_parking.VehicleParkingSpaces; @@ -44,7 +45,7 @@ public class BikelyUpdater implements DataSource { .put("lonMax", 0) .put("latMin", 0) .put("latMax", 0); - private final OtpHttpClient httpClient = new OtpHttpClient(); + private final OtpHttpClient httpClient = new OtpHttpClientFactory().create(LOG); private final BikelyUpdaterParameters parameters; private List lots; diff --git a/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslFacilitiesDownloader.java b/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslFacilitiesDownloader.java index 31664170a04..dcf4ffe29d9 100644 --- a/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslFacilitiesDownloader.java +++ b/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslFacilitiesDownloader.java @@ -12,6 +12,7 @@ import java.util.function.BiFunction; import org.opentripplanner.framework.io.OtpHttpClient; import org.opentripplanner.framework.io.OtpHttpClientException; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.routing.vehicle_parking.VehicleParking; import org.opentripplanner.routing.vehicle_parking.VehicleParkingGroup; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -20,7 +21,7 @@ public class HslFacilitiesDownloader { - private static final Logger log = LoggerFactory.getLogger(HslFacilitiesDownloader.class); + private static final Logger LOG = LoggerFactory.getLogger(HslFacilitiesDownloader.class); private static final ObjectMapper mapper = new ObjectMapper(); private final String jsonParsePath; @@ -31,19 +32,20 @@ public class HslFacilitiesDownloader { public HslFacilitiesDownloader( String url, String jsonParsePath, - BiFunction, VehicleParking> facilitiesParser + BiFunction, VehicleParking> facilitiesParser, + OtpHttpClientFactory otpHttpClientFactory ) { this.url = url; this.jsonParsePath = jsonParsePath; this.facilitiesParser = facilitiesParser; - this.otpHttpClient = new OtpHttpClient(); + this.otpHttpClient = otpHttpClientFactory.create(LOG); } public List downloadFacilities( Map hubForPark ) { if (url == null) { - log.warn("Cannot download updates, because url is null!"); + LOG.warn("Cannot download updates, because url is null!"); return null; } @@ -55,17 +57,17 @@ public List downloadFacilities( try { return parseJSON(is, hubForPark); } catch (IllegalArgumentException e) { - log.warn("Error parsing facilities from {}", url, e); + LOG.warn("Error parsing facilities from {}", url, e); } catch (JsonProcessingException e) { - log.warn("Error parsing facilities from {} (bad JSON of some sort)", url, e); + LOG.warn("Error parsing facilities from {} (bad JSON of some sort)", url, e); } catch (IOException e) { - log.warn("Error reading facilities from {}", url, e); + LOG.warn("Error reading facilities from {}", url, e); } return null; } ); } catch (OtpHttpClientException e) { - log.warn("Failed to get data from url {}", url); + LOG.warn("Failed to get data from url {}", url); return null; } } diff --git a/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslHubsDownloader.java b/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslHubsDownloader.java index c5712e2067e..f1f98b6139c 100644 --- a/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslHubsDownloader.java +++ b/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslHubsDownloader.java @@ -11,6 +11,7 @@ import java.util.function.Function; import org.opentripplanner.framework.io.OtpHttpClient; import org.opentripplanner.framework.io.OtpHttpClientException; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.routing.vehicle_parking.VehicleParkingGroup; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.slf4j.Logger; @@ -18,7 +19,7 @@ public class HslHubsDownloader { - private static final Logger log = LoggerFactory.getLogger(HslHubsDownloader.class); + private static final Logger LOG = LoggerFactory.getLogger(HslHubsDownloader.class); private static final ObjectMapper mapper = new ObjectMapper(); private final String jsonParsePath; @@ -29,17 +30,18 @@ public class HslHubsDownloader { public HslHubsDownloader( String url, String jsonParsePath, - Function> hubsParser + Function> hubsParser, + OtpHttpClientFactory otpHttpClientFactory ) { this.url = url; this.jsonParsePath = jsonParsePath; this.hubsParser = hubsParser; - otpHttpClient = new OtpHttpClient(); + otpHttpClient = otpHttpClientFactory.create(LOG); } public Map downloadHubs() { if (url == null) { - log.warn("Cannot download updates, because url is null!"); + LOG.warn("Cannot download updates, because url is null!"); return null; } try { @@ -50,17 +52,17 @@ public Map downloadHubs() { try { return parseJSON(is); } catch (IllegalArgumentException e) { - log.warn("Error parsing hubs from {}", url, e); + LOG.warn("Error parsing hubs from {}", url, e); } catch (JsonProcessingException e) { - log.warn("Error parsing hubs from {} (bad JSON of some sort)", url, e); + LOG.warn("Error parsing hubs from {} (bad JSON of some sort)", url, e); } catch (IOException e) { - log.warn("Error reading hubs from {}", url, e); + LOG.warn("Error reading hubs from {}", url, e); } return null; } ); } catch (OtpHttpClientException e) { - log.warn("Failed to get data from url {}", url); + LOG.warn("Failed to get data from url {}", url); return null; } } 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 e5086630941..f5c5b33ef29 100644 --- a/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslParkUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslParkUpdater.java @@ -4,6 +4,7 @@ import java.util.Map; import java.util.stream.Collectors; import org.opentripplanner.framework.io.JsonDataListDownloader; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.model.calendar.openinghours.OpeningHoursCalendarService; import org.opentripplanner.routing.vehicle_parking.VehicleParking; import org.opentripplanner.routing.vehicle_parking.VehicleParkingGroup; @@ -11,6 +12,8 @@ import org.opentripplanner.routing.vehicle_parking.VehicleParkingSpaces.VehicleParkingSpacesBuilder; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.updater.spi.DataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Vehicle parking updater class for https://github.com/HSLdevcom/parkandrideAPI format APIs. There @@ -19,6 +22,8 @@ */ public class HslParkUpdater implements DataSource { + private static final Logger LOG = LoggerFactory.getLogger(HslParkUpdater.class); + private static final String JSON_PARSE_PATH = "results"; private final HslFacilitiesDownloader facilitiesDownloader; @@ -43,24 +48,28 @@ public HslParkUpdater( new HslParkToVehicleParkingMapper(feedId, openingHoursCalendarService, parameters.timeZone()); vehicleParkingGroupMapper = new HslHubToVehicleParkingGroupMapper(feedId); parkPatchMapper = new HslParkUtilizationToPatchMapper(feedId); + var otpHttpClientFactory = new OtpHttpClientFactory(); facilitiesDownloader = new HslFacilitiesDownloader( parameters.facilitiesUrl(), JSON_PARSE_PATH, - vehicleParkingMapper::parsePark + vehicleParkingMapper::parsePark, + otpHttpClientFactory ); hubsDownloader = new HslHubsDownloader( parameters.hubsUrl(), JSON_PARSE_PATH, - vehicleParkingGroupMapper::parseHub + vehicleParkingGroupMapper::parseHub, + otpHttpClientFactory ); utilizationsDownloader = new JsonDataListDownloader<>( parameters.utilizationsUrl(), "", parkPatchMapper::parseUtilization, - Map.of() + Map.of(), + otpHttpClientFactory.create(LOG) ); this.facilitiesFrequencySec = parameters.facilitiesFrequencySec(); } diff --git a/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java b/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java index d51d0d70b6c..9b350dec345 100644 --- a/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java +++ b/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java @@ -10,8 +10,8 @@ import java.util.Map; import java.util.Optional; import org.opentripplanner.ext.vehiclerentalservicedirectory.api.VehicleRentalServiceDirectoryFetcherParameters; -import org.opentripplanner.framework.io.OtpHttpClient; import org.opentripplanner.framework.io.OtpHttpClientException; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.framework.json.JsonUtils; import org.opentripplanner.routing.linking.VertexLinker; import org.opentripplanner.service.vehiclerental.VehicleRentalRepository; @@ -35,16 +35,16 @@ public class VehicleRentalServiceDirectoryFetcher { private final VertexLinker vertexLinker; private final VehicleRentalRepository repository; - private final OtpHttpClient otpHttpClient; + private final OtpHttpClientFactory otpHttpClientFactory; public VehicleRentalServiceDirectoryFetcher( VertexLinker vertexLinker, VehicleRentalRepository repository, - OtpHttpClient otpHttpClient + OtpHttpClientFactory otpHttpClientFactory ) { this.vertexLinker = vertexLinker; this.repository = repository; - this.otpHttpClient = otpHttpClient; + this.otpHttpClientFactory = otpHttpClientFactory; } public static List createUpdatersFromEndpoint( @@ -61,12 +61,12 @@ public static List createUpdatersFromEndpoint( } int maxHttpConnections = sources.size(); - var otpHttpClient = new OtpHttpClient(maxHttpConnections); + var otpHttpClientFactory = new OtpHttpClientFactory(maxHttpConnections); var serviceDirectory = new VehicleRentalServiceDirectoryFetcher( vertexLinker, repository, - otpHttpClient + otpHttpClientFactory ); return serviceDirectory.createUpdatersFromEndpoint(parameters, sources); } @@ -146,7 +146,7 @@ private VehicleRentalUpdater fetchAndCreateUpdater( var dataSource = VehicleRentalDataSourceFactory.create( vehicleRentalParameters.sourceParameters(), - otpHttpClient + otpHttpClientFactory ); return new VehicleRentalUpdater(vehicleRentalParameters, dataSource, vertexLinker, repository); } @@ -154,7 +154,8 @@ private VehicleRentalUpdater fetchAndCreateUpdater( private static JsonNode listSources(VehicleRentalServiceDirectoryFetcherParameters parameters) { JsonNode node; URI url = parameters.getUrl(); - try (OtpHttpClient otpHttpClient = new OtpHttpClient()) { + try { + var otpHttpClient = new OtpHttpClientFactory().create(LOG); node = otpHttpClient.getAndMapAsJsonNode(url, Map.of(), new ObjectMapper()); } catch (OtpHttpClientException e) { LOG.warn("Error fetching list of vehicle rental endpoints from {}", url, e); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java b/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java index 42ffe992539..5ab24c89543 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java @@ -1,6 +1,5 @@ package org.opentripplanner.apis.gtfs; -import com.bedatadriven.jackson.datatype.jts.JtsModule; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import graphql.language.StringValue; @@ -16,15 +15,15 @@ import java.time.format.DateTimeFormatter; import javax.annotation.Nonnull; import org.locationtech.jts.geom.Geometry; -import org.opentripplanner.framework.geometry.GeometryUtils; import org.opentripplanner.framework.graphql.scalar.DurationScalarFactory; +import org.opentripplanner.framework.json.ObjectMappers; import org.opentripplanner.framework.model.Grams; import org.opentripplanner.framework.time.OffsetDateTimeParser; public class GraphQLScalars { - private static final ObjectMapper geoJsonMapper = new ObjectMapper() - .registerModule(new JtsModule(GeometryUtils.getGeometryFactory())); + private static final ObjectMapper geoJsonMapper = ObjectMappers.geoJson(); + public static GraphQLScalarType DURATION_SCALAR = DurationScalarFactory.createDurationScalar(); public static final GraphQLScalarType POLYLINE_SCALAR = GraphQLScalarType diff --git a/src/main/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapper.java b/src/main/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapper.java index e7a13dcacc9..974b8dd10c3 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapper.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapper.java @@ -1,12 +1,16 @@ package org.opentripplanner.apis.transmodel.mapping; import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; import org.opentripplanner.routing.api.request.RequestModes; import org.opentripplanner.routing.api.request.RequestModesBuilder; import org.opentripplanner.routing.api.request.StreetMode; class RequestModesMapper { + private static final Predicate IS_BIKE = m -> m == StreetMode.BIKE; private static final String accessModeKey = "accessMode"; private static final String egressModeKey = "egressMode"; private static final String directModeKey = "directMode"; @@ -15,22 +19,26 @@ class RequestModesMapper { * Maps GraphQL Modes input type to RequestModes. *

* This only maps access, egress, direct & transfer modes. Transport modes are set using filters. - * Default modes are WALK for access, egress & transfer. */ static RequestModes mapRequestModes(Map modesInput) { RequestModesBuilder mBuilder = RequestModes.of(); - if (modesInput.containsKey(accessModeKey)) { - StreetMode accessMode = (StreetMode) modesInput.get(accessModeKey); - mBuilder.withAccessMode(accessMode); - mBuilder.withTransferMode(accessMode == StreetMode.BIKE ? StreetMode.BIKE : StreetMode.WALK); - } - if (modesInput.containsKey(egressModeKey)) { - mBuilder.withEgressMode((StreetMode) modesInput.get(egressModeKey)); - } - // An unset directMode should overwrite the walk default, so we don't check for existence first. - mBuilder.withDirectMode((StreetMode) modesInput.get(directModeKey)); + final StreetMode accessMode = (StreetMode) modesInput.get(accessModeKey); + ensureValueAndSet(accessMode, mBuilder::withAccessMode); + ensureValueAndSet((StreetMode) modesInput.get(egressModeKey), mBuilder::withEgressMode); + ensureValueAndSet((StreetMode) modesInput.get(directModeKey), mBuilder::withDirectMode); + Optional.ofNullable(accessMode).filter(IS_BIKE).ifPresent(mBuilder::withTransferMode); return mBuilder.build(); } + + /** + * Use the provided consumer to apply the StreetMode if it's non-null, otherwise apply NOT_SET. + * + * @param streetMode + * @param consumer + */ + private static void ensureValueAndSet(StreetMode streetMode, Consumer consumer) { + consumer.accept(streetMode == null ? StreetMode.NOT_SET : streetMode); + } } diff --git a/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java b/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java index 8350a10d670..92d577480e2 100644 --- a/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java +++ b/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java @@ -13,6 +13,7 @@ import org.opentripplanner.service.vehiclerental.street.StreetVehicleRentalLink; import org.opentripplanner.street.model.edge.AreaEdge; import org.opentripplanner.street.model.edge.BoardingLocationToStopLink; +import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.edge.ElevatorHopEdge; import org.opentripplanner.street.model.edge.EscalatorEdge; import org.opentripplanner.street.model.edge.PathwayEdge; @@ -49,6 +50,15 @@ public class DebugStyleSpec { 1, List.of(new ZoomStop(15, 0.2f), new ZoomStop(MAX_ZOOM, 3)) ); + private static final Class[] EDGES_TO_DISPLAY = new Class[] { + StreetEdge.class, + AreaEdge.class, + EscalatorEdge.class, + PathwayEdge.class, + ElevatorHopEdge.class, + TemporaryPartialStreetEdge.class, + TemporaryFreeEdge.class, + }; static StyleSpec build( VectorSourceLayer regularStops, @@ -73,19 +83,20 @@ static StyleSpec build( .typeLine() .vectorSourceLayer(edges) .lineColor(MAGENTA) - .edgeFilter( - StreetEdge.class, - AreaEdge.class, - EscalatorEdge.class, - PathwayEdge.class, - ElevatorHopEdge.class, - TemporaryPartialStreetEdge.class, - TemporaryFreeEdge.class - ) + .edgeFilter(EDGES_TO_DISPLAY) .lineWidth(LINE_WIDTH) .minZoom(13) .maxZoom(MAX_ZOOM) .intiallyHidden(), + StyleBuilder + .ofId("edge-name") + .typeSymbol() + .lineText("name") + .vectorSourceLayer(edges) + .edgeFilter(EDGES_TO_DISPLAY) + .minZoom(17) + .maxZoom(MAX_ZOOM) + .intiallyHidden(), StyleBuilder .ofId("link") .typeLine() diff --git a/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java b/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java index 07efe376968..28c1e792fd1 100644 --- a/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java +++ b/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java @@ -8,6 +8,7 @@ import java.util.Map; import java.util.Objects; import java.util.stream.Stream; +import org.opentripplanner.apis.vectortiles.model.ZoomDependentNumber.ZoomStop; import org.opentripplanner.framework.collection.ListUtils; import org.opentripplanner.framework.json.ObjectMappers; import org.opentripplanner.street.model.edge.Edge; @@ -41,6 +42,7 @@ public enum LayerType { Line, Raster, Fill, + Symbol, } private StyleBuilder(String id) { @@ -94,11 +96,35 @@ public StyleBuilder typeFill() { return this; } + public StyleBuilder typeSymbol() { + type(LayerType.Symbol); + return this; + } + private StyleBuilder type(LayerType type) { props.put(TYPE, type.name().toLowerCase()); return this; } + public StyleBuilder lineText(String name) { + layout.put("symbol-placement", "line"); + layout.put("symbol-spacing", 500); + layout.put("text-field", "{%s}".formatted(name)); + layout.put("text-font", List.of("KlokanTech Noto Sans Regular")); + layout.put( + "text-size", + new ZoomDependentNumber(14, List.of(new ZoomStop(14, 12), new ZoomStop(20, 14))).toJson() + ); + layout.put("text-max-width", 5); + layout.put("text-keep-upright", true); + layout.put("text-rotation-alignment", "map"); + paint.put("text-color", "#000"); + paint.put("text-halo-color", "#fff"); + paint.put("text-halo-blur", 4); + paint.put("text-halo-width", 3); + return this; + } + public StyleBuilder circleColor(String color) { paint.put("circle-color", validateColor(color)); return this; diff --git a/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleSpec.java b/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleSpec.java index 64f680ed202..71e5df9d335 100644 --- a/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleSpec.java +++ b/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleSpec.java @@ -46,4 +46,9 @@ public Map sources() { public List layers() { return layers; } + + @JsonSerialize + public String glyphs() { + return "https://cdn.jsdelivr.net/gh/klokantech/klokantech-gl-fonts@master/{fontstack}/{range}.pbf"; + } } diff --git a/src/main/java/org/opentripplanner/datastore/https/HttpsDataSourceRepository.java b/src/main/java/org/opentripplanner/datastore/https/HttpsDataSourceRepository.java index bf087d0c0ad..56afc888c73 100644 --- a/src/main/java/org/opentripplanner/datastore/https/HttpsDataSourceRepository.java +++ b/src/main/java/org/opentripplanner/datastore/https/HttpsDataSourceRepository.java @@ -11,13 +11,17 @@ import org.opentripplanner.datastore.api.FileType; import org.opentripplanner.datastore.base.DataSourceRepository; import org.opentripplanner.datastore.file.ZipStreamDataSourceDecorator; -import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * This data store accesses files in read-only mode over HTTPS. */ public class HttpsDataSourceRepository implements DataSourceRepository { + private static final Logger LOG = LoggerFactory.getLogger(HttpsFileDataSource.class); + private static final Duration HTTP_HEAD_REQUEST_TIMEOUT = Duration.ofSeconds(20); @Override @@ -75,7 +79,8 @@ private CompositeDataSource createCompositeSource(URI uri, FileType type) { } protected List

getHttpHeaders(URI uri) { - try (OtpHttpClient otpHttpClient = new OtpHttpClient()) { + try (OtpHttpClientFactory otpHttpClientFactory = new OtpHttpClientFactory()) { + var otpHttpClient = otpHttpClientFactory.create(LOG); return otpHttpClient.getHeaders(uri, HTTP_HEAD_REQUEST_TIMEOUT, Map.of()); } } diff --git a/src/main/java/org/opentripplanner/datastore/https/HttpsFileDataSource.java b/src/main/java/org/opentripplanner/datastore/https/HttpsFileDataSource.java index 14b770036b8..f81f42f9c77 100644 --- a/src/main/java/org/opentripplanner/datastore/https/HttpsFileDataSource.java +++ b/src/main/java/org/opentripplanner/datastore/https/HttpsFileDataSource.java @@ -13,6 +13,9 @@ import org.opentripplanner.datastore.file.DirectoryDataSource; import org.opentripplanner.datastore.file.ZipFileDataSource; import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * This class is a wrapper around an HTTPS resource. @@ -22,6 +25,8 @@ */ final class HttpsFileDataSource implements DataSource { + private static final Logger LOG = LoggerFactory.getLogger(HttpsFileDataSource.class); + private static final Duration HTTP_GET_REQUEST_TIMEOUT = Duration.ofSeconds(20); private final URI uri; private final FileType type; @@ -35,7 +40,7 @@ final class HttpsFileDataSource implements DataSource { this.uri = uri; this.type = type; this.httpsDataSourceMetadata = httpsDataSourceMetadata; - otpHttpClient = new OtpHttpClient(); + otpHttpClient = new OtpHttpClientFactory().create(LOG); } /** diff --git a/src/main/java/org/opentripplanner/framework/geometry/GeometryUtils.java b/src/main/java/org/opentripplanner/framework/geometry/GeometryUtils.java index c02ff6151e2..85915cfeb48 100644 --- a/src/main/java/org/opentripplanner/framework/geometry/GeometryUtils.java +++ b/src/main/java/org/opentripplanner/framework/geometry/GeometryUtils.java @@ -244,7 +244,7 @@ public static Geometry convertGeoJsonToJtsGeometry(GeoJsonObject geoJsonGeom) } /** - * Extract individual line string from a mult-line string. + * Extract individual line strings from a multi-line string. */ public static List getLineStrings(MultiLineString mls) { var ret = new ArrayList(); diff --git a/src/main/java/org/opentripplanner/framework/geometry/SphericalDistanceLibrary.java b/src/main/java/org/opentripplanner/framework/geometry/SphericalDistanceLibrary.java index 479163aefd1..38c80e86d79 100644 --- a/src/main/java/org/opentripplanner/framework/geometry/SphericalDistanceLibrary.java +++ b/src/main/java/org/opentripplanner/framework/geometry/SphericalDistanceLibrary.java @@ -30,10 +30,16 @@ public static double distance(Coordinate from, Coordinate to) { return distance(from.y, from.x, to.y, to.x); } + /** + * @see SphericalDistanceLibrary#fastDistance(double, double, double, double) + */ public static double fastDistance(Coordinate from, Coordinate to) { return fastDistance(from.y, from.x, to.y, to.x); } + /** + * @see SphericalDistanceLibrary#fastDistance(double, double, double, double) + */ public static double fastDistance(Coordinate from, Coordinate to, double cosLat) { double dLat = toRadians(from.y - to.y); double dLon = toRadians(from.x - to.x) * cosLat; @@ -105,8 +111,8 @@ public static double distance(double lat1, double lon1, double lat2, double lon2 } /** - * Compute an (approximated) distance between two points, with a known cos(lat). Be careful, this - * is approximated and never check for the validity of input cos(lat). + * Compute an (approximated) distance in meters between two points, with a known cos(lat). + * Be careful, this is approximated and never checks for the validity of input cos(lat). */ public static double fastDistance(double lat1, double lon1, double lat2, double lon2) { return fastDistance(lat1, lon1, lat2, lon2, RADIUS_OF_EARTH_IN_M); @@ -131,8 +137,8 @@ public static double distance(double lat1, double lon1, double lat2, double lon2 } /** - * Approximated, fast and under-estimated equirectangular distance between two points. Works only - * for small delta lat/lon, fall-back on exact distance if not the case. See: + * Approximated, fast and under-estimated equirectangular distance in meters between two points. + * Works only for small delta lat/lon, fall-back on exact distance if not the case. See: * http://www.movable-type.co.uk/scripts/latlong.html */ public static double fastDistance( diff --git a/src/main/java/org/opentripplanner/framework/io/JsonDataListDownloader.java b/src/main/java/org/opentripplanner/framework/io/JsonDataListDownloader.java index bb602adb472..3affa734605 100644 --- a/src/main/java/org/opentripplanner/framework/io/JsonDataListDownloader.java +++ b/src/main/java/org/opentripplanner/framework/io/JsonDataListDownloader.java @@ -17,7 +17,7 @@ public class JsonDataListDownloader { - private static final Logger log = LoggerFactory.getLogger(JsonDataListDownloader.class); + private static final Logger LOG = LoggerFactory.getLogger(JsonDataListDownloader.class); private final String jsonParsePath; private final Map headers; private final Function elementParser; @@ -30,7 +30,7 @@ public JsonDataListDownloader( @Nonnull Function elementParser, @Nonnull Map headers ) { - this(url, jsonParsePath, elementParser, headers, new OtpHttpClient()); + this(url, jsonParsePath, elementParser, headers, new OtpHttpClientFactory().create(LOG)); } public JsonDataListDownloader( @@ -49,7 +49,7 @@ public JsonDataListDownloader( public List download() { if (url == null) { - log.warn("Cannot download updates, because url is null!"); + LOG.warn("Cannot download updates, because url is null!"); return null; } try { @@ -60,17 +60,17 @@ public List download() { try { return parseJSON(is); } catch (IllegalArgumentException e) { - log.warn("Error parsing bike rental feed from {}", url, e); + LOG.warn("Error parsing feed from {}", url, e); } catch (JsonProcessingException e) { - log.warn("Error parsing bike rental feed from {} (bad JSON of some sort)", url, e); + LOG.warn("Error parsing feed from {} (bad JSON of some sort)", url, e); } catch (IOException e) { - log.warn("Error reading bike rental feed from {}", url, e); + LOG.warn("Error reading feed from {}", url, e); } return null; } ); } catch (OtpHttpClientException e) { - log.warn("Failed to get data from url {}", url); + LOG.warn("Failed to get data from url {}", url); return null; } } @@ -111,7 +111,7 @@ private List parseJSON(InputStream dataStream) throws IllegalArgumentExceptio out.add(parsedElement); } } catch (Exception e) { - log.error("Could not process element in JSON list downloaded from {}", url, e); + LOG.error("Could not process element in JSON list downloaded from {}", url, e); } } return out; diff --git a/src/main/java/org/opentripplanner/framework/io/OtpHttpClient.java b/src/main/java/org/opentripplanner/framework/io/OtpHttpClient.java index 1ac8891553c..678eb4754bd 100644 --- a/src/main/java/org/opentripplanner/framework/io/OtpHttpClient.java +++ b/src/main/java/org/opentripplanner/framework/io/OtpHttpClient.java @@ -2,8 +2,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; @@ -14,17 +16,13 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; -import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.Header; @@ -32,14 +30,9 @@ import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.io.HttpClientResponseHandler; -import org.apache.hc.core5.http.io.SocketConfig; import org.apache.hc.core5.http.io.entity.StringEntity; -import org.apache.hc.core5.pool.PoolConcurrencyPolicy; -import org.apache.hc.core5.pool.PoolReusePolicy; -import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * HTTP client providing convenience methods to send HTTP requests and map HTTP responses to Java @@ -50,101 +43,27 @@ *

Exception management

* Exceptions thrown during network operations or response mapping are wrapped in * {@link OtpHttpClientException} - *

Timeout configuration

- * The same timeout value is applied to the following parameters: - *
    - *
  • Connection request timeout: the maximum waiting time for leasing a connection in the - * connection pool. - *
  • Connect timeout: the maximum waiting time for the first packet received from the server. - *
  • Socket timeout: the maximum waiting time between two packets received from the server. - *
- * The default timeout is set to 5 seconds. - *

Connection time-to-live

- * Maximum time an HTTP connection can stay in the connection pool before being closed. - * Note that HTTP 1.1 and HTTP/2 rely on persistent connections and the HTTP server is allowed to - * close idle connections at any time. - * The default connection time-to-live is set to 1 minute. *

Resource management

- * It is recommended to use the getAndMapXXX and postAndMapXXX methods - * in this class since they - * ensure that the underlying network resources are properly released. - * The method {@link #getAsInputStream} gives access to an input stream on the body response but - * requires the caller to close this stream. For most use cases, this method is not recommended . - *

Connection Pooling

- * The connection pool holds by default a maximum of 25 connections, with maximum 5 connections - * per host. + * It is recommended to use the getAndMapXXX and postAndMapXXX methods in + * this class since they ensure that the underlying network resources are properly released. The + * method {@link #getAsInputStream} gives access to an input stream on the body response but + * requires the caller to close this stream. For most use cases, this method is not recommended. * *

Thread-safety

* Instances of this class are thread-safe. */ -public class OtpHttpClient implements AutoCloseable { - - private static final Logger LOG = LoggerFactory.getLogger(OtpHttpClient.class); - private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5); - - private static final Duration DEFAULT_TTL = Duration.ofMinutes(1); - - /** - * see {@link PoolingHttpClientConnectionManager#DEFAULT_MAX_TOTAL_CONNECTIONS} - */ - public static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 25; +public class OtpHttpClient { private final CloseableHttpClient httpClient; - /** - * Creates an HTTP client with default timeout, default connection time-to-live and default max - * number of connections. - */ - public OtpHttpClient() { - this(DEFAULT_TIMEOUT, DEFAULT_TTL); - } - - /** - * Creates an HTTP client with default timeout, default connection time-to-live and the given max - * number of connections. - */ - public OtpHttpClient(int maxConnections) { - this(DEFAULT_TIMEOUT, DEFAULT_TTL, maxConnections); - } - - /** - * Creates an HTTP client the given timeout and connection time-to-live and the default max - * number of connections. - */ - public OtpHttpClient(Duration timeout, Duration connectionTtl) { - this(timeout, connectionTtl, DEFAULT_MAX_TOTAL_CONNECTIONS); - } + private final Logger log; /** * Creates an HTTP client with custom configuration. */ - private OtpHttpClient(Duration timeout, Duration connectionTtl, int maxConnections) { - Objects.requireNonNull(timeout); - Objects.requireNonNull(connectionTtl); - - PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder - .create() - .setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(Timeout.of(timeout)).build()) - .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT) - .setConnPoolPolicy(PoolReusePolicy.LIFO) - .setMaxConnTotal(maxConnections) - .setDefaultConnectionConfig( - ConnectionConfig - .custom() - .setSocketTimeout(Timeout.of(timeout)) - .setConnectTimeout(Timeout.of(timeout)) - .setTimeToLive(TimeValue.of(connectionTtl)) - .build() - ) - .build(); - - HttpClientBuilder httpClientBuilder = HttpClients - .custom() - .setUserAgent("OpenTripPlanner") - .setConnectionManager(connectionManager) - .setDefaultRequestConfig(requestConfig(timeout)); - - httpClient = httpClientBuilder.build(); + OtpHttpClient(CloseableHttpClient httpClient, Logger logger) { + this.httpClient = httpClient; + log = logger; } /** @@ -162,7 +81,8 @@ public List
getHeaders( requestHeaderValues, response -> { if (isFailedRequest(response)) { - LOG.warn( + logResponse(response); + log.warn( "Headers of resource {} unavailable. HTTP error code {}", sanitizeUri(uri), response.getCode() @@ -347,15 +267,6 @@ public Optional executeAndMapOptional( ); } - @Override - public void close() { - try { - httpClient.close(); - } catch (IOException e) { - throw new OtpHttpClientException(e); - } - } - /** * Executes an HTTP GET request and returns an input stream on the response body. The caller must * close the stream in order to release resources. Use preferably the provided getAndMapXXX @@ -416,6 +327,7 @@ protected T executeAndMapWithResponseHandler( private T mapResponse(ClassicHttpResponse response, ResponseMapper contentMapper) { if (isFailedRequest(response)) { + logResponse(response); throw new OtpHttpClientException( "HTTP request failed with status code " + response.getCode() ); @@ -485,6 +397,24 @@ private static String sanitizeUri(URI uri) { return uri.toString().replace('?' + uri.getQuery(), ""); } + private void logResponse(ClassicHttpResponse response) { + try { + if ( + log.isTraceEnabled() && + response.getEntity() != null && + response.getEntity().getContent() != null + ) { + var entity = response.getEntity(); + String content = new BufferedReader(new InputStreamReader(entity.getContent())) + .lines() + .collect(Collectors.joining("\n")); + log.trace("HTTP request failed with status code {}: \n{}", response.getCode(), content); + } + } catch (Exception e) { + log.debug(e.getMessage()); + } + } + @FunctionalInterface public interface ResponseMapper { /** diff --git a/src/main/java/org/opentripplanner/framework/io/OtpHttpClientFactory.java b/src/main/java/org/opentripplanner/framework/io/OtpHttpClientFactory.java new file mode 100644 index 00000000000..a6436168541 --- /dev/null +++ b/src/main/java/org/opentripplanner/framework/io/OtpHttpClientFactory.java @@ -0,0 +1,137 @@ +package org.opentripplanner.framework.io; + +import java.io.IOException; +import java.time.Duration; +import java.util.Objects; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.core5.http.io.SocketConfig; +import org.apache.hc.core5.pool.PoolConcurrencyPolicy; +import org.apache.hc.core5.pool.PoolReusePolicy; +import org.apache.hc.core5.util.TimeValue; +import org.apache.hc.core5.util.Timeout; +import org.slf4j.Logger; + +/** + * Factory for creating {@link OtpHttpClient} instances. These instances will share the same + * {@link CloseableHttpClient} instance. + * + *

Timeout configuration

+ * The same timeout value is applied to the following parameters: + *
    + *
  • Connection request timeout: the maximum waiting time for leasing a connection in the + * connection pool. + *
  • Connect timeout: the maximum waiting time for the first packet received from the server. + *
  • Socket timeout: the maximum waiting time between two packets received from the server. + *
+ * The default timeout is set to 5 seconds. + *

Connection time-to-live

+ * Maximum time an HTTP connection can stay in the connection pool before being closed. + * Note that HTTP 1.1 and HTTP/2 rely on persistent connections and the HTTP server is allowed to + * close idle connections at any time. + * The default connection time-to-live is set to 1 minute. + *

Connection Pooling

+ * The connection pool holds by default a maximum of 25 connections, with maximum 5 connections + * per host. + * + *

Thread-safety

+ * Instances of this class are thread-safe. + */ +public class OtpHttpClientFactory implements AutoCloseable { + + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5); + + private static final Duration DEFAULT_TTL = Duration.ofMinutes(1); + + /** + * see {@link PoolingHttpClientConnectionManager#DEFAULT_MAX_TOTAL_CONNECTIONS} + */ + public static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 25; + + private final CloseableHttpClient httpClient; + + /** + * Creates an HTTP client with default timeout, default connection time-to-live and default max + * number of connections. + */ + public OtpHttpClientFactory() { + this(DEFAULT_TIMEOUT, DEFAULT_TTL); + } + + /** + * Creates an HTTP client with default timeout, default connection time-to-live and the given max + * number of connections. + */ + public OtpHttpClientFactory(int maxConnections) { + this(DEFAULT_TIMEOUT, DEFAULT_TTL, maxConnections); + } + + /** + * Creates an HTTP client the given timeout and connection time-to-live and the default max + * number of connections. + */ + public OtpHttpClientFactory(Duration timeout, Duration connectionTtl) { + this(timeout, connectionTtl, DEFAULT_MAX_TOTAL_CONNECTIONS); + } + + /** + * Creates an HTTP client with custom configuration. + */ + private OtpHttpClientFactory(Duration timeout, Duration connectionTtl, int maxConnections) { + Objects.requireNonNull(timeout); + Objects.requireNonNull(connectionTtl); + + PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder + .create() + .setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(Timeout.of(timeout)).build()) + .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT) + .setConnPoolPolicy(PoolReusePolicy.LIFO) + .setMaxConnTotal(maxConnections) + .setDefaultConnectionConfig( + ConnectionConfig + .custom() + .setSocketTimeout(Timeout.of(timeout)) + .setConnectTimeout(Timeout.of(timeout)) + .setTimeToLive(TimeValue.of(connectionTtl)) + .build() + ) + .build(); + + HttpClientBuilder httpClientBuilder = HttpClients + .custom() + .setUserAgent("OpenTripPlanner") + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig(timeout)); + + httpClient = httpClientBuilder.build(); + } + + public OtpHttpClient create(Logger logger) { + return new OtpHttpClient(httpClient, logger); + } + + @Override + public void close() { + try { + httpClient.close(); + } catch (IOException e) { + throw new OtpHttpClientException(e); + } + } + + /** + * Configures the request with a custom timeout. + */ + private static RequestConfig requestConfig(Duration timeout) { + return RequestConfig + .custom() + .setResponseTimeout(Timeout.of(timeout)) + .setConnectionRequestTimeout(Timeout.of(timeout)) + .build(); + } +} diff --git a/src/main/java/org/opentripplanner/framework/json/ObjectMappers.java b/src/main/java/org/opentripplanner/framework/json/ObjectMappers.java index 8802db0fc41..1670ae94fb3 100644 --- a/src/main/java/org/opentripplanner/framework/json/ObjectMappers.java +++ b/src/main/java/org/opentripplanner/framework/json/ObjectMappers.java @@ -1,7 +1,9 @@ package org.opentripplanner.framework.json; +import com.bedatadriven.jackson.datatype.jts.JtsModule; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import org.opentripplanner.framework.geometry.GeometryUtils; public class ObjectMappers { @@ -13,4 +15,11 @@ public static ObjectMapper ignoringExtraFields() { mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); return mapper; } + + /** + * Returns a mapper that can serialize JTS geometries into GeoJSON. + */ + public static ObjectMapper geoJson() { + return new ObjectMapper().registerModule(new JtsModule(GeometryUtils.getGeometryFactory())); + } } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/DisjointSet.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/DisjointSet.java index a5b9179f696..15bfc3605b5 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/DisjointSet.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/DisjointSet.java @@ -12,7 +12,7 @@ import org.opentripplanner.framework.collection.MapUtils; /** Basic union-find data structure with path compression */ -public class DisjointSet { +class DisjointSet { TIntList sets = new TIntArrayList(); 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 e831d2ada47..10c215ee448 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 @@ -113,8 +113,6 @@ public Map elevationDataOutput() { return elevationData; } - private record StreetEdgePair(StreetEdge main, StreetEdge back) {} - private void build() { var parkingProcessor = new ParkingProcessor( graph, @@ -390,8 +388,10 @@ private void buildBasicGraph() { geometry ); - StreetEdge street = streets.main; - StreetEdge backStreet = streets.back; + params.edgeNamer().recordEdges(way, streets); + + StreetEdge street = streets.main(); + StreetEdge backStreet = streets.back(); normalizer.applyWayProperties(street, backStreet, wayData, way); applyEdgesToTurnRestrictions(way, startNode, endNode, street, backStreet); @@ -541,16 +541,10 @@ private StreetEdge getEdgeForStreet( .withRoundabout(way.isRoundabout()) .withSlopeOverride(way.getOsmProvider().getWayPropertySet().getSlopeOverride(way)) .withStairs(way.isSteps()) - .withWheelchairAccessible(way.isWheelchairAccessible()); - - if (!way.hasTag("name") && !way.hasTag("ref")) { - seb.withBogusName(true); - } - - StreetEdge street = seb.buildAndConnect(); - params.edgeNamer().recordEdge(way, street); + .withWheelchairAccessible(way.isWheelchairAccessible()) + .withBogusName(way.hasNoName()); - return street; + return seb.buildAndConnect(); } private float getMaxCarSpeed() { diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/Ring.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/Ring.java index 9804c2d646f..754bdbc36b2 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/Ring.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/Ring.java @@ -19,7 +19,7 @@ import org.opentripplanner.framework.geometry.GeometryUtils; import org.opentripplanner.openstreetmap.model.OSMNode; -public class Ring { +class Ring { private final LinearRing shell; private final List holes = new ArrayList<>(); diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/StreetEdgePair.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/StreetEdgePair.java new file mode 100644 index 00000000000..7e87838c4d5 --- /dev/null +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/StreetEdgePair.java @@ -0,0 +1,35 @@ +package org.opentripplanner.graph_builder.module.osm; + +import java.util.ArrayList; +import org.opentripplanner.street.model.edge.StreetEdge; + +public record StreetEdgePair(StreetEdge main, StreetEdge back) { + /** + * Return the non-null elements of this pair as an Iterable. + */ + public Iterable asIterable() { + var ret = new ArrayList(2); + if (main != null) { + ret.add(main); + } + if (back != null) { + ret.add(back); + } + return ret; + } + + /** + * Select one of the edges contained in this pair that is not null. No particular algorithm is + * guaranteed, and it may change in the future. + */ + public StreetEdge pickAny() { + if (main != null) { + return main; + } else if (back != null) { + return back; + } + throw new IllegalStateException( + "%s must not contain two null elements".formatted(getClass().getSimpleName()) + ); + } +} 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 5562f1df1a3..89b71403232 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 @@ -68,7 +68,7 @@ * number of edges for an area wouldn't be determined by the nodes. The current approach can lead * to an excessive number of edges, or to no edges at all if maxAreaNodes is surpassed. */ -public class WalkableAreaBuilder { +class WalkableAreaBuilder { private final DataImportIssueStore issueStore; @@ -400,7 +400,7 @@ private void pruneAreaEdges( Set edges, Set edgesToKeep ) { - if (edges.size() == 0) return; + if (edges.isEmpty()) return; StreetMode mode; StreetEdge firstEdge = (StreetEdge) edges.iterator().next(); @@ -496,7 +496,7 @@ private Set createSegments( } // do we need to recurse? if (intersects.size() == 1) { - Area area = intersects.get(0); + Area area = intersects.getFirst(); OSMWithTags areaEntity = area.parent; StreetTraversalPermission areaPermissions = areaEntity.overridePermissions( @@ -531,15 +531,10 @@ private Set createSegments( .withPermission(areaPermissions) .withBack(false) .withArea(edgeList) - .withCarSpeed(carSpeed); - - if (!areaEntity.hasTag("name") && !areaEntity.hasTag("ref")) { - streetEdgeBuilder.withBogusName(true); - } - - streetEdgeBuilder.withWheelchairAccessible(areaEntity.isWheelchairAccessible()); - - streetEdgeBuilder.withLink(areaEntity.isLink()); + .withCarSpeed(carSpeed) + .withBogusName(areaEntity.hasNoName()) + .withWheelchairAccessible(areaEntity.isWheelchairAccessible()) + .withLink(areaEntity.isLink()); label = "way (area) " + @@ -559,15 +554,10 @@ private Set createSegments( .withPermission(areaPermissions) .withBack(true) .withArea(edgeList) - .withCarSpeed(carSpeed); - - if (!areaEntity.hasTag("name") && !areaEntity.hasTag("ref")) { - backStreetEdgeBuilder.withBogusName(true); - } - - backStreetEdgeBuilder.withWheelchairAccessible(areaEntity.isWheelchairAccessible()); - - backStreetEdgeBuilder.withLink(areaEntity.isLink()); + .withCarSpeed(carSpeed) + .withBogusName(areaEntity.hasNoName()) + .withWheelchairAccessible(areaEntity.isWheelchairAccessible()) + .withLink(areaEntity.isLink()); if (!wayPropertiesCache.containsKey(areaEntity)) { WayProperties wayData = areaEntity diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/naming/DefaultNamer.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/naming/DefaultNamer.java index b1639608d4c..ff7b7d17666 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/naming/DefaultNamer.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/naming/DefaultNamer.java @@ -1,9 +1,9 @@ package org.opentripplanner.graph_builder.module.osm.naming; import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.graph_builder.module.osm.StreetEdgePair; import org.opentripplanner.graph_builder.services.osm.EdgeNamer; import org.opentripplanner.openstreetmap.model.OSMWithTags; -import org.opentripplanner.street.model.edge.StreetEdge; public class DefaultNamer implements EdgeNamer { @@ -13,7 +13,7 @@ public I18NString name(OSMWithTags way) { } @Override - public void recordEdge(OSMWithTags way, StreetEdge edge) {} + public void recordEdges(OSMWithTags way, StreetEdgePair edge) {} @Override public void postprocess() {} diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/naming/PortlandCustomNamer.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/naming/PortlandCustomNamer.java index 9d550d51ae6..c0ca595fbd7 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/osm/naming/PortlandCustomNamer.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/naming/PortlandCustomNamer.java @@ -3,6 +3,7 @@ import java.util.HashSet; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.NonLocalizedString; +import org.opentripplanner.graph_builder.module.osm.StreetEdgePair; import org.opentripplanner.graph_builder.services.osm.EdgeNamer; import org.opentripplanner.openstreetmap.model.OSMWithTags; import org.opentripplanner.street.model.edge.StreetEdge; @@ -69,28 +70,29 @@ public I18NString name(OSMWithTags way) { } @Override - public void recordEdge(OSMWithTags way, StreetEdge edge) { - if (!edge.hasBogusName()) { - return; // this edge already has a real name so there is nothing to do - } - String highway = way.getTag("highway"); - if ("motorway_link".equals(highway) || "trunk_link".equals(highway)) { - if (edge.isBack()) { - nameByDestination.add(edge); - } else { - nameByOrigin.add(edge); - } - } else if ( - "secondary_link".equals(highway) || - "primary_link".equals(highway) || - "tertiary_link".equals(highway) - ) { - if (edge.isBack()) { - nameByOrigin.add(edge); - } else { - nameByDestination.add(edge); - } - } + public void recordEdges(OSMWithTags way, StreetEdgePair edgePair) { + final boolean isHighwayLink = isHighwayLink(way); + final boolean isLowerLink = isLowerLink(way); + edgePair + .asIterable() + .forEach(edge -> { + if (!edge.hasBogusName()) { + return; // this edge already has a real name so there is nothing to do + } + if (isHighwayLink) { + if (edge.isBack()) { + nameByDestination.add(edge); + } else { + nameByOrigin.add(edge); + } + } else if (isLowerLink) { + if (edge.isBack()) { + nameByOrigin.add(edge); + } else { + nameByDestination.add(edge); + } + } + }); } @Override @@ -183,4 +185,18 @@ private static String nameAccordingToOrigin(StreetEdge e, int maxDepth) { } return null; } + + private static boolean isHighwayLink(OSMWithTags way) { + String highway = way.getTag("highway"); + return "motorway_link".equals(highway) || "trunk_link".equals(highway); + } + + private static boolean isLowerLink(OSMWithTags way) { + String highway = way.getTag("highway"); + return ( + "secondary_link".equals(highway) || + "primary_link".equals(highway) || + "tertiary_link".equals(highway) + ); + } } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/osm/naming/SidewalkNamer.java b/src/main/java/org/opentripplanner/graph_builder/module/osm/naming/SidewalkNamer.java new file mode 100644 index 00000000000..eafccc0da1b --- /dev/null +++ b/src/main/java/org/opentripplanner/graph_builder/module/osm/naming/SidewalkNamer.java @@ -0,0 +1,290 @@ +package org.opentripplanner.graph_builder.module.osm.naming; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.geotools.api.referencing.FactoryException; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; +import org.geotools.api.referencing.operation.MathTransform; +import org.geotools.api.referencing.operation.TransformException; +import org.geotools.geometry.jts.JTS; +import org.geotools.referencing.CRS; +import org.geotools.referencing.crs.DefaultGeographicCRS; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.operation.buffer.BufferParameters; +import org.opentripplanner.framework.geometry.GeometryUtils; +import org.opentripplanner.framework.geometry.HashGridSpatialIndex; +import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.framework.lang.DoubleUtils; +import org.opentripplanner.framework.logging.ProgressTracker; +import org.opentripplanner.graph_builder.module.osm.StreetEdgePair; +import org.opentripplanner.graph_builder.services.osm.EdgeNamer; +import org.opentripplanner.openstreetmap.model.OSMWithTags; +import org.opentripplanner.street.model.edge.StreetEdge; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A namer that assigns names of nearby streets to sidewalks if they meet certain + * geometric similarity criteria. + *

+ * The algorithm works as follows: + * - for each sidewalk we look up (named) street edges nearby + * - group those edges into groups where each edge has the same name + * - draw a flat-capped buffer around the sidewalk, like this: https://tinyurl.com/4fpe882h + * - check how much of a named edge group is inside the buffer + * - remove those groups which are below MIN_PERCENT_IN_BUFFER + * - take the group that has the highest percentage (as a proportion of the sidewalk length) inside + * the buffer and apply its name to the sidewalk. + *

+ * This works very well for OSM data where the sidewalk runs a parallel to the street and at each + * intersection the sidewalk is also split. It doesn't work well for sidewalks that go around + * the corner, like https://www.openstreetmap.org/way/1059101564. These cases are, however, detected + * by the above algorithm and the sidewalk name remains the same. + */ +public class SidewalkNamer implements EdgeNamer { + + private static final Logger LOG = LoggerFactory.getLogger(SidewalkNamer.class); + private static final double MIN_PERCENT_IN_BUFFER = .85; + private static final int BUFFER_METERS = 25; + + private HashGridSpatialIndex streetEdges = new HashGridSpatialIndex<>(); + private Collection unnamedSidewalks = new ArrayList<>(); + private PreciseBuffer preciseBuffer; + + @Override + public I18NString name(OSMWithTags way) { + return way.getAssumedName(); + } + + @Override + public void recordEdges(OSMWithTags way, StreetEdgePair pair) { + // This way is a sidewalk and hasn't been named yet (and is not explicitly unnamed) + if (way.isSidewalk() && way.hasNoName() && !way.isExplicitlyUnnamed()) { + pair + .asIterable() + .forEach(edge -> unnamedSidewalks.add(new EdgeOnLevel(edge, way.getLevels()))); + } + // The way is _not_ a sidewalk and does have a name + else if (way.isNamed() && !way.isLink()) { + // We generate two edges for each osm way: one there and one back. This spatial index only + // needs to contain one item for each road segment with a unique geometry and name, so we + // add only one of the two edges. + var edge = pair.pickAny(); + streetEdges.insert( + edge.getGeometry().getEnvelopeInternal(), + new EdgeOnLevel(edge, way.getLevels()) + ); + } + } + + @Override + public void postprocess() { + ProgressTracker progress = ProgressTracker.track( + "Assigning names to sidewalks", + 500, + unnamedSidewalks.size() + ); + + this.preciseBuffer = new PreciseBuffer(computeEnvelopeCenter(), BUFFER_METERS); + + final AtomicInteger namesApplied = new AtomicInteger(0); + unnamedSidewalks + .parallelStream() + .forEach(sidewalkOnLevel -> { + assignNameToSidewalk(sidewalkOnLevel, namesApplied); + + // Keep lambda! A method-ref would cause incorrect class and line number to be logged + // noinspection Convert2MethodRef + progress.step(m -> LOG.info(m)); + }); + + LOG.info( + "Assigned names to {} of {} of sidewalks ({}%)", + namesApplied.get(), + unnamedSidewalks.size(), + DoubleUtils.roundTo2Decimals((double) namesApplied.get() / unnamedSidewalks.size() * 100) + ); + + LOG.info(progress.completeMessage()); + + // Set the indices to null so they can be garbage-collected + streetEdges = null; + unnamedSidewalks = null; + } + + /** + * Compute the centroid of all sidewalk edges. + */ + private Coordinate computeEnvelopeCenter() { + var envelope = new Envelope(); + unnamedSidewalks.forEach(e -> { + envelope.expandToInclude(e.edge.getFromVertex().getCoordinate()); + envelope.expandToInclude(e.edge.getToVertex().getCoordinate()); + }); + return envelope.centre(); + } + + /** + * The actual worker method that runs the business logic on an individual sidewalk edge. + */ + private void assignNameToSidewalk(EdgeOnLevel sidewalkOnLevel, AtomicInteger namesApplied) { + var sidewalk = sidewalkOnLevel.edge; + var buffer = preciseBuffer.preciseBuffer(sidewalk.getGeometry()); + var sidewalkLength = SphericalDistanceLibrary.length(sidewalk.getGeometry()); + + var candidates = streetEdges.query(buffer.getEnvelopeInternal()); + + groupEdgesByName(candidates) + // Make sure we only compare sidewalks and streets that are on the same level + .filter(g -> g.levels.equals(sidewalkOnLevel.levels)) + .map(g -> computePercentInsideBuffer(g, buffer, sidewalkLength)) + // Remove those groups where less than a certain percentage is inside the buffer around + // the sidewalk. This is a safety mechanism for sidewalks that snake around the corner, + // like https://www.openstreetmap.org/way/1059101564. + .filter(group -> group.percentInBuffer > MIN_PERCENT_IN_BUFFER) + .max(Comparator.comparingDouble(NamedEdgeGroup::percentInBuffer)) + .ifPresent(group -> { + namesApplied.incrementAndGet(); + sidewalk.setName(Objects.requireNonNull(group.name)); + }); + } + + /** + * Compute the length of the group that is inside the buffer and return it as a percentage + * of the length of the sidewalk. + */ + private static NamedEdgeGroup computePercentInsideBuffer( + CandidateGroup g, + Geometry buffer, + double sidewalkLength + ) { + var lengthInsideBuffer = g.intersectionLength(buffer); + double percentInBuffer = lengthInsideBuffer / sidewalkLength; + return new NamedEdgeGroup(percentInBuffer, g.name); + } + + /** + * If a single street is split into several edges, each individual part of the street would potentially + * have a low similarity with the (longer) sidewalk. For that reason we combine them into a group + * and have a better basis for comparison. + */ + private static Stream groupEdgesByName(List candidates) { + return candidates + .stream() + .collect(Collectors.groupingBy(e -> e.edge.getName())) + .entrySet() + .stream() + .map(entry -> { + var levels = entry + .getValue() + .stream() + .flatMap(e -> e.levels.stream()) + .collect(Collectors.toSet()); + return new CandidateGroup( + entry.getKey(), + entry.getValue().stream().map(e -> e.edge).toList(), + levels + ); + }); + } + + private record NamedEdgeGroup(double percentInBuffer, I18NString name) { + NamedEdgeGroup { + Objects.requireNonNull(name); + } + } + + /** + * A group of edges that are near a sidewalk that have the same name. These groups are used + * to figure out if the name of the group can be applied to a nearby sidewalk. + */ + private record CandidateGroup(I18NString name, List edges, Set levels) { + /** + * How much of this group intersects with the given geometry, in meters. + */ + double intersectionLength(Geometry polygon) { + return edges + .stream() + .mapToDouble(edge -> { + var intersection = polygon.intersection(edge.getGeometry()); + return length(intersection); + }) + .sum(); + } + + private double length(Geometry intersection) { + return switch (intersection) { + case LineString ls -> SphericalDistanceLibrary.length(ls); + case MultiLineString mls -> GeometryUtils + .getLineStrings(mls) + .stream() + .mapToDouble(this::intersectionLength) + .sum(); + case Point ignored -> 0; + case Geometry g -> throw new IllegalStateException( + "Didn't expect geometry %s".formatted(g.getClass()) + ); + }; + } + } + + private record EdgeOnLevel(StreetEdge edge, Set levels) {} + + /** + * A class to cache the expensive construction of a Universal Traverse Mercator coordinate + * reference system. + * Re-using the same CRS for all edges might introduce tiny imprecisions for OTPs use cases + * but speeds up the processing enormously and is a price well worth paying. + */ + private static final class PreciseBuffer { + + private final double distanceInMeters; + private final MathTransform toTransform; + private final MathTransform fromTransform; + + private PreciseBuffer(Coordinate coordinate, double distanceInMeters) { + this.distanceInMeters = distanceInMeters; + String code = "AUTO:42001,%s,%s".formatted(coordinate.x, coordinate.y); + try { + CoordinateReferenceSystem auto = CRS.decode(code); + this.toTransform = CRS.findMathTransform(DefaultGeographicCRS.WGS84, auto); + this.fromTransform = CRS.findMathTransform(auto, DefaultGeographicCRS.WGS84); + } catch (FactoryException e) { + throw new RuntimeException(e); + } + } + + /** + * Add a buffer around a geometry that makes sure that the buffer is the same distance (in + * meters) anywhere on earth. + *

+ * Background: If you call the regular buffer() method on a JTS geometry that uses WGS84 as the + * coordinate reference system, the buffer will be accurate at the equator but will become more + * and more elongated the farther north/south you go. + *

+ * Taken from https://stackoverflow.com/questions/36455020 + */ + private Geometry preciseBuffer(Geometry geometry) { + try { + Geometry pGeom = JTS.transform(geometry, toTransform); + Geometry pBufferedGeom = pGeom.buffer(distanceInMeters, 4, BufferParameters.CAP_FLAT); + return JTS.transform(pBufferedGeom, fromTransform); + } catch (TransformException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/main/java/org/opentripplanner/graph_builder/services/osm/EdgeNamer.java b/src/main/java/org/opentripplanner/graph_builder/services/osm/EdgeNamer.java index d9e52a18465..60cf780f714 100644 --- a/src/main/java/org/opentripplanner/graph_builder/services/osm/EdgeNamer.java +++ b/src/main/java/org/opentripplanner/graph_builder/services/osm/EdgeNamer.java @@ -3,12 +3,13 @@ import javax.annotation.Nonnull; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.NonLocalizedString; +import org.opentripplanner.graph_builder.module.osm.StreetEdgePair; import org.opentripplanner.graph_builder.module.osm.naming.DefaultNamer; import org.opentripplanner.graph_builder.module.osm.naming.PortlandCustomNamer; +import org.opentripplanner.graph_builder.module.osm.naming.SidewalkNamer; import org.opentripplanner.openstreetmap.model.OSMWithTags; import org.opentripplanner.standalone.config.framework.json.NodeAdapter; import org.opentripplanner.standalone.config.framework.json.OtpVersion; -import org.opentripplanner.street.model.edge.StreetEdge; /** * Interface responsible for naming edges of the street graph. It allows you to write your own @@ -24,11 +25,11 @@ public interface EdgeNamer { * Callback function for each way/edge combination so that more complicated names can be built * in the post-processing step. */ - void recordEdge(OSMWithTags way, StreetEdge edge); + void recordEdges(OSMWithTags way, StreetEdgePair edge); /** * Called after each edge has been named to build a more complex name out of the relationships - * tracked in {@link EdgeNamer#recordEdge(OSMWithTags, StreetEdge)}. + * tracked in {@link EdgeNamer#recordEdges(OSMWithTags, StreetEdgePair)}. */ void postprocess(); @@ -50,25 +51,29 @@ public static EdgeNamer fromConfig(NodeAdapter root, String parameterName) { var osmNaming = root .of(parameterName) .summary("A custom OSM namer to use.") - .since(OtpVersion.V2_0) - .asString(null); + .since(OtpVersion.V1_5) + .asEnum(EdgeNamerType.DEFAULT); return fromConfig(osmNaming); } /** * Create a custom namer if needed, return null if not found / by default. */ - public static EdgeNamer fromConfig(String type) { + public static EdgeNamer fromConfig(EdgeNamerType type) { if (type == null) { return new DefaultNamer(); } - return switch (type) { - case "portland" -> new PortlandCustomNamer(); - default -> throw new IllegalArgumentException( - String.format("Unknown osmNaming type: '%s'", type) - ); + case PORTLAND -> new PortlandCustomNamer(); + case SIDEWALKS -> new SidewalkNamer(); + case DEFAULT -> new DefaultNamer(); }; } } + + enum EdgeNamerType { + DEFAULT, + PORTLAND, + SIDEWALKS, + } } diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java index 4d5fe6bd051..ce65d6b0820 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java @@ -171,6 +171,7 @@ public void mapStopTripAndRouteDataIntoBuilder() { builder.getPathways().addAll(pathwayMapper.map(data.getAllPathways())); builder.getStopTimesSortedByTrip().addAll(stopTimeMapper.map(data.getAllStopTimes())); + builder.getFlexTimePenalty().putAll(tripMapper.flexSafeDurationModifiers()); builder.getTripsById().addAll(tripMapper.map(data.getAllTrips())); fareRulesBuilder.fareAttributes().addAll(fareAttributeMapper.map(data.getAllFareAttributes())); diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/StopTimeMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/StopTimeMapper.java index fc75236f4e4..67b250c5061 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/StopTimeMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/StopTimeMapper.java @@ -102,6 +102,7 @@ private StopTime doMap(org.onebusaway.gtfs.model.StopTime rhs) { lhs.setFarePeriodId(rhs.getFarePeriodId()); lhs.setFlexWindowStart(rhs.getStartPickupDropOffWindow()); lhs.setFlexWindowEnd(rhs.getEndPickupDropOffWindow()); + lhs.setFlexContinuousPickup( PickDropMapper.mapFlexContinuousPickDrop(rhs.getContinuousPickup()) ); diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java index a80ae035ed1..ca221c1c6ca 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java @@ -1,10 +1,13 @@ package org.opentripplanner.gtfs.mapping; +import java.time.Duration; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import org.opentripplanner.framework.collection.MapUtils; import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.routing.api.request.framework.TimePenalty; import org.opentripplanner.transit.model.timetable.Trip; /** Responsible for mapping GTFS TripMapper into the OTP model. */ @@ -12,9 +15,10 @@ class TripMapper { private final RouteMapper routeMapper; private final DirectionMapper directionMapper; - private TranslationHelper translationHelper; + private final TranslationHelper translationHelper; private final Map mappedTrips = new HashMap<>(); + private final Map flexSafeDurationModifiers = new HashMap<>(); TripMapper( RouteMapper routeMapper, @@ -38,6 +42,13 @@ Collection getMappedTrips() { return mappedTrips.values(); } + /** + * The map of flex duration factors per flex trip. + */ + Map flexSafeDurationModifiers() { + return flexSafeDurationModifiers; + } + private Trip doMap(org.onebusaway.gtfs.model.Trip rhs) { var lhs = Trip.of(AgencyAndIdMapper.mapAgencyAndId(rhs.getId())); @@ -60,8 +71,18 @@ private Trip doMap(org.onebusaway.gtfs.model.Trip rhs) { lhs.withShapeId(AgencyAndIdMapper.mapAgencyAndId(rhs.getShapeId())); lhs.withWheelchairBoarding(WheelchairAccessibilityMapper.map(rhs.getWheelchairAccessible())); lhs.withBikesAllowed(BikeAccessMapper.mapForTrip(rhs)); - lhs.withGtfsFareId(rhs.getFareId()); - return lhs.build(); + var trip = lhs.build(); + mapSafeDurationModifier(rhs).ifPresent(f -> flexSafeDurationModifiers.put(trip, f)); + return trip; + } + + private Optional mapSafeDurationModifier(org.onebusaway.gtfs.model.Trip rhs) { + if (rhs.getSafeDurationFactor() == null && rhs.getSafeDurationOffset() == null) { + return Optional.empty(); + } else { + var offset = Duration.ofSeconds(rhs.getSafeDurationOffset().longValue()); + return Optional.of(TimePenalty.of(offset, rhs.getSafeDurationFactor().doubleValue())); + } } } diff --git a/src/main/java/org/opentripplanner/inspector/vector/edge/EdgePropertyMapper.java b/src/main/java/org/opentripplanner/inspector/vector/edge/EdgePropertyMapper.java index fb65d0b5d3b..d43a91d384d 100644 --- a/src/main/java/org/opentripplanner/inspector/vector/edge/EdgePropertyMapper.java +++ b/src/main/java/org/opentripplanner/inspector/vector/edge/EdgePropertyMapper.java @@ -3,6 +3,7 @@ import static org.opentripplanner.framework.lang.DoubleUtils.roundTo2Decimals; import static org.opentripplanner.inspector.vector.KeyValue.kv; +import com.google.common.collect.Lists; import java.util.Collection; import java.util.List; import org.opentripplanner.apis.support.mapping.PropertyMapper; @@ -16,16 +17,26 @@ public class EdgePropertyMapper extends PropertyMapper { @Override protected Collection map(Edge input) { - List baseProps = List.of(kv("class", input.getClass().getSimpleName())); + var baseProps = List.of(kv("class", input.getClass().getSimpleName())); List properties = switch (input) { - case StreetEdge e -> List.of( - kv("permission", e.getPermission().toString()), - kv("bicycleSafetyFactor", roundTo2Decimals(e.getBicycleSafetyFactor())) - ); + case StreetEdge e -> mapStreetEdge(e); case EscalatorEdge e -> List.of(kv("distance", e.getDistanceMeters())); default -> List.of(); }; return ListUtils.combine(baseProps, properties); } + + private static List mapStreetEdge(StreetEdge se) { + var props = Lists.newArrayList( + kv("permission", se.getPermission().toString()), + kv("bicycleSafetyFactor", roundTo2Decimals(se.getBicycleSafetyFactor())) + ); + if (se.hasBogusName()) { + props.addFirst(kv("name", "%s (generated)".formatted(se.getName().toString()))); + } else { + props.addFirst(kv("name", se.getName().toString())); + } + return props; + } } diff --git a/src/main/java/org/opentripplanner/model/StopTime.java b/src/main/java/org/opentripplanner/model/StopTime.java index 2ae04484426..e753b8d2885 100644 --- a/src/main/java/org/opentripplanner/model/StopTime.java +++ b/src/main/java/org/opentripplanner/model/StopTime.java @@ -62,28 +62,6 @@ public final class StopTime implements Comparable { public StopTime() {} - public StopTime(StopTime st) { - this.trip = st.trip; - this.stop = st.stop; - this.arrivalTime = st.arrivalTime; - this.departureTime = st.departureTime; - this.timepoint = st.timepoint; - this.stopSequence = st.stopSequence; - this.stopHeadsign = st.stopHeadsign; - this.routeShortName = st.routeShortName; - this.pickupType = st.pickupType; - this.dropOffType = st.dropOffType; - this.shapeDistTraveled = st.shapeDistTraveled; - this.farePeriodId = st.farePeriodId; - this.flexWindowStart = st.flexWindowStart; - this.flexWindowEnd = st.flexWindowEnd; - this.flexContinuousPickup = st.flexContinuousPickup; - this.flexContinuousDropOff = st.flexContinuousDropOff; - this.dropOffBookingInfo = st.dropOffBookingInfo; - this.pickupBookingInfo = st.pickupBookingInfo; - this.headsignVias = st.headsignVias; - } - /** * The id is used to navigate/link StopTime to other entities (Map from StopTime.id -> Entity.id). * There is no need to navigate in the opposite direction. The StopTime id is NOT stored in a @@ -319,4 +297,11 @@ private static int getAvailableTime(int... times) { return MISSING_VALUE; } + + /** + * Does this stop time define a flex window? + */ + public boolean hasFlexWindow() { + return flexWindowStart != MISSING_VALUE || flexWindowEnd != MISSING_VALUE; + } } diff --git a/src/main/java/org/opentripplanner/model/Timetable.java b/src/main/java/org/opentripplanner/model/Timetable.java index a8f0f1bbf44..031e95e8870 100644 --- a/src/main/java/org/opentripplanner/model/Timetable.java +++ b/src/main/java/org/opentripplanner/model/Timetable.java @@ -61,6 +61,7 @@ public class Timetable implements Serializable { private final List frequencyEntries = new ArrayList<>(); + @Nullable private final LocalDate serviceDate; /** Construct an empty Timetable. */ diff --git a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java index 382d6042a05..544ca29599d 100644 --- a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java +++ b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java @@ -3,6 +3,7 @@ import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -22,6 +23,7 @@ import org.opentripplanner.model.calendar.impl.CalendarServiceDataFactoryImpl; import org.opentripplanner.model.transfer.ConstrainedTransfer; import org.opentripplanner.model.transfer.TransferPoint; +import org.opentripplanner.routing.api.request.framework.TimePenalty; import org.opentripplanner.transit.model.basic.Notice; import org.opentripplanner.transit.model.framework.AbstractTransitEntity; import org.opentripplanner.transit.model.framework.DefaultEntityById; @@ -92,6 +94,8 @@ public class OtpTransitServiceBuilder { private final TripStopTimes stopTimesByTrip = new TripStopTimes(); + private final Map flexDurationFactors = new HashMap<>(); + private final EntityById fareZonesById = new DefaultEntityById<>(); private final List transfers = new ArrayList<>(); @@ -209,6 +213,10 @@ public TripStopTimes getStopTimesSortedByTrip() { return stopTimesByTrip; } + public Map getFlexTimePenalty() { + return flexDurationFactors; + } + public EntityById getFareZonesById() { return fareZonesById; } diff --git a/src/main/java/org/opentripplanner/netex/NetexModule.java b/src/main/java/org/opentripplanner/netex/NetexModule.java index 08904c947e4..b9a05d25b10 100644 --- a/src/main/java/org/opentripplanner/netex/NetexModule.java +++ b/src/main/java/org/opentripplanner/netex/NetexModule.java @@ -69,7 +69,7 @@ public void buildGraph() { ); transitBuilder.limitServiceDays(transitPeriodLimit); for (var tripOnServiceDate : transitBuilder.getTripOnServiceDates().values()) { - transitModel.getTripOnServiceDates().put(tripOnServiceDate.getId(), tripOnServiceDate); + transitModel.addTripOnServiceDate(tripOnServiceDate.getId(), tripOnServiceDate); } calendarServiceData.add(transitBuilder.buildCalendarServiceData()); diff --git a/src/main/java/org/opentripplanner/openstreetmap/model/OSMWay.java b/src/main/java/org/opentripplanner/openstreetmap/model/OSMWay.java index 6e610f99fa3..b1e90044bf2 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/model/OSMWay.java +++ b/src/main/java/org/opentripplanner/openstreetmap/model/OSMWay.java @@ -60,6 +60,10 @@ public boolean isSteps() { return isTag("highway", "steps"); } + /** + * Checks the wheelchair-accessibility of this way. Stairs are by default inaccessible but + * can be made accessible if they explicitly set wheelchair=true. + */ public boolean isWheelchairAccessible() { if (isSteps()) { return isTagTrue("wheelchair"); diff --git a/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java b/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java index e5feda76bc5..b53739bf6a1 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java +++ b/src/main/java/org/opentripplanner/openstreetmap/model/OSMWithTags.java @@ -30,7 +30,7 @@ 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( + private static final Set NON_ROUTABLE_HIGHWAYS = Set.of( "proposed", "planned", "construction", @@ -46,7 +46,8 @@ public class OSMWithTags { "escape" ); - static final Set LEVEL_TAGS = Set.of("level", "layer"); + private static final Set LEVEL_TAGS = Set.of("level", "layer"); + private static final Set DEFAULT_LEVEL = Set.of("0"); /* To save memory this is only created when an entity actually has tags. */ private Map tags; @@ -163,6 +164,10 @@ public boolean isBicycleDismountForced() { return isTag("bicycle", "dismount"); } + public boolean isSidewalk() { + return isTag("footway", "sidewalk") && isTag("highway", "footway"); + } + protected boolean doesTagAllowAccess(String tag) { if (tags == null) { return false; @@ -554,6 +559,9 @@ public boolean isRoutable() { return false; } + /** + * Is this a link to another road, like a highway ramp. + */ public boolean isLink() { String highway = getTag("highway"); return highway != null && highway.endsWith(("_link")); @@ -572,6 +580,36 @@ public boolean isWheelchairAccessible() { return !isTagFalse("wheelchair"); } + /** + * Does this entity have tags that allow extracting a name? + */ + public boolean isNamed() { + return hasTag("name") || hasTag("ref"); + } + + /** + * Is this entity unnamed? + *

+ * Perhaps this entity has a name that isn't in the source data, but it's also possible that + * it's explicitly tagged as not having one. + * + * @see OSMWithTags#isExplicitlyUnnamed() + */ + public boolean hasNoName() { + return !isNamed(); + } + + /** + * Whether this entity explicitly doesn't have a name. This is different to no name being + * set on the entity in OSM. + * + * @see OSMWithTags#isNamed() + * @see https://wiki.openstreetmap.org/wiki/Tag:noname%3Dyes + */ + public boolean isExplicitlyUnnamed() { + return isTagTrue("noname"); + } + /** * Returns true if this tag is explicitly access to this entity. */ @@ -589,7 +627,7 @@ public Set getLevels() { var levels = getMultiTagValues(LEVEL_TAGS); if (levels.isEmpty()) { // default - return Set.of("0"); + return DEFAULT_LEVEL; } return levels; } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java index b58af3c8b23..0a9e46d2fe3 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java @@ -146,7 +146,7 @@ private TransitRouterResult route() { requestTransitDataProvider.stopNameResolver(), serverContext.transitService().getTransferService(), requestTransitDataProvider, - transitLayer.getStopBoardAlightCosts(), + transitLayer.getStopBoardAlightTransferCosts(), request.preferences().transfer().optimization(), raptorRequest.multiCriteria() ); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java index 288f429a861..2dde108b97e 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java @@ -48,7 +48,8 @@ public class TransitLayer { private final TransferIndexGenerator transferIndexGenerator; - private final int[] stopBoardAlightCosts; + @Nullable + private final int[] stopBoardAlightTransferCosts; /** * Makes a shallow copy of the TransitLayer, except for the tripPatternsForDate, where a shallow @@ -65,7 +66,7 @@ public TransitLayer(TransitLayer transitLayer) { transitLayer.transferCache, transitLayer.constrainedTransfers, transitLayer.transferIndexGenerator, - transitLayer.stopBoardAlightCosts + transitLayer.stopBoardAlightTransferCosts ); } @@ -78,7 +79,7 @@ public TransitLayer( RaptorRequestTransferCache transferCache, ConstrainedTransfersForPatterns constrainedTransfers, TransferIndexGenerator transferIndexGenerator, - int[] stopBoardAlightCosts + @Nullable int[] stopBoardAlightTransferCosts ) { this.tripPatternsRunningOnDate = new HashMap<>(tripPatternsRunningOnDate); this.transfersByStopIndex = transfersByStopIndex; @@ -88,7 +89,7 @@ public TransitLayer( this.transferCache = transferCache; this.constrainedTransfers = constrainedTransfers; this.transferIndexGenerator = transferIndexGenerator; - this.stopBoardAlightCosts = stopBoardAlightCosts; + this.stopBoardAlightTransferCosts = stopBoardAlightTransferCosts; } @Nullable @@ -166,8 +167,13 @@ public TransferIndexGenerator getTransferIndexGenerator() { return transferIndexGenerator; } - public int[] getStopBoardAlightCosts() { - return stopBoardAlightCosts; + /** + * Costs for both boarding and alighting at a given stop during transfer. Note that this is in + * raptor centi-second units. + */ + @Nullable + public int[] getStopBoardAlightTransferCosts() { + return stopBoardAlightTransferCosts; } /** diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitTuningParameters.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitTuningParameters.java index 31c602b6133..b62459db679 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitTuningParameters.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitTuningParameters.java @@ -13,7 +13,7 @@ public interface TransitTuningParameters { * These tuning parameters are typically used in unit tests. The values are: *

    * enableStopTransferPriority : true
-   * stopTransferCost : {
+   * stopBoardAlightDuringTransferCost : {
    *   DISCOURAGED:  3600  (equivalent of 1 hour penalty)
    *   ALLOWED:        60  (60 seconds penalty)
    *   RECOMMENDED:    20  (20 seconds penalty)
@@ -28,7 +28,7 @@ public boolean enableStopTransferPriority() {
     }
 
     @Override
-    public Integer stopTransferCost(StopTransferPriority key) {
+    public Integer stopBoardAlightDuringTransferCost(StopTransferPriority key) {
       switch (key) {
         case DISCOURAGED:
           return 3600;
@@ -70,10 +70,11 @@ public List transferCacheRequests() {
   boolean enableStopTransferPriority();
 
   /**
-   * The stop transfer cost for the given {@link StopTransferPriority}. The cost applied to boarding
-   * and alighting all stops with the given priority.
+   * The stop board alight transfer cost for the given {@link StopTransferPriority}. The cost
+   * applied during transfers to both boarding and alighting of stops with the given
+   * priority.
    */
-  Integer stopTransferCost(StopTransferPriority key);
+  Integer stopBoardAlightDuringTransferCost(StopTransferPriority key);
 
   /**
    * The maximum number of transfer RouteRequests for which the pre-calculated transfers should be
diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/CostCalculatorFactory.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/CostCalculatorFactory.java
index fef7b3509e3..a10cf828b45 100644
--- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/CostCalculatorFactory.java
+++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/CostCalculatorFactory.java
@@ -1,16 +1,17 @@
 package org.opentripplanner.routing.algorithm.raptoradapter.transit.cost;
 
+import javax.annotation.Nullable;
 import org.opentripplanner.raptor.spi.RaptorCostCalculator;
 
 public class CostCalculatorFactory {
 
   public static  RaptorCostCalculator createCostCalculator(
     GeneralizedCostParameters generalizedCostParameters,
-    int[] stopBoardAlightCosts
+    @Nullable int[] stopBoardAlightTransferCosts
   ) {
     RaptorCostCalculator calculator = new DefaultCostCalculator<>(
       generalizedCostParameters,
-      stopBoardAlightCosts
+      stopBoardAlightTransferCosts
     );
 
     if (generalizedCostParameters.wheelchairEnabled()) {
diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java
index ee2f1282625..43faa3f0c40 100644
--- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java
+++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/DefaultCostCalculator.java
@@ -19,22 +19,28 @@ public final class DefaultCostCalculator
   private final int boardAndTransferCost;
   private final int waitFactor;
   private final FactorStrategy transitFactors;
-  private final int[] stopTransferCost;
+
+  /**
+   * Costs for boarding and alighting at a given stop during transfer.
+   * See TransitLayer.getStopBoardAlightTransferCosts()
+   */
+  @Nullable
+  private final int[] stopBoardAlightTransferCosts;
 
   /**
    * Cost unit: SECONDS - The unit for all input parameters are in the OTP TRANSIT model cost unit
    * (in Raptor the unit for cost is centi-seconds).
    *
-   * @param stopTransferCost Unit centi-seconds. This parameter is used "as-is" and not transformed
-   *                      into the Raptor cast unit to avoid the transformation for each request.
-   *                      Use {@code null} to ignore stop cost.
+   * @param stopBoardAlightTransferCosts Unit centi-seconds. This parameter is used "as-is" and not
+   *                      transformed into the Raptor cast unit to avoid the transformation for each
+   *                      request. Use {@code null} to ignore stop cost.
    */
   public DefaultCostCalculator(
     int boardCost,
     int transferCost,
     double waitReluctanceFactor,
     @Nullable double[] transitReluctanceFactors,
-    @Nullable int[] stopTransferCost
+    @Nullable int[] stopBoardAlightTransferCosts
   ) {
     this.boardCostOnly = RaptorCostConverter.toRaptorCost(boardCost);
     this.transferCostOnly = RaptorCostConverter.toRaptorCost(transferCost);
@@ -46,16 +52,19 @@ public DefaultCostCalculator(
         ? new SingleValueFactorStrategy(GeneralizedCostParameters.DEFAULT_TRANSIT_RELUCTANCE)
         : new IndexBasedFactorStrategy(transitReluctanceFactors);
 
-    this.stopTransferCost = stopTransferCost;
+    this.stopBoardAlightTransferCosts = stopBoardAlightTransferCosts;
   }
 
-  public DefaultCostCalculator(GeneralizedCostParameters params, int[] stopTransferCost) {
+  public DefaultCostCalculator(
+    GeneralizedCostParameters params,
+    @Nullable int[] stopBoardAlightTransferCosts
+  ) {
     this(
       params.boardCost(),
       params.transferCost(),
       params.waitReluctanceFactor(),
       params.transitReluctanceFactors(),
-      stopTransferCost
+      stopBoardAlightTransferCosts
     );
   }
 
@@ -108,8 +117,8 @@ public int transitArrivalCost(
 
     // Add transfer cost on all alighting events.
     // If it turns out to be the last one this cost will be removed during costEgress phase.
-    if (stopTransferCost != null) {
-      cost += stopTransferCost[toStop];
+    if (stopBoardAlightTransferCosts != null) {
+      cost += stopBoardAlightTransferCosts[toStop];
     }
 
     return cost;
@@ -134,7 +143,9 @@ public int calculateRemainingMinCost(int minTravelTime, int minNumTransfers, int
       // Remove cost that was added during alighting similar as we do in the costEgress() method
       int fixedCost = transitFactors.minFactor() * minTravelTime;
 
-      return stopTransferCost == null ? fixedCost : fixedCost - stopTransferCost[fromStop];
+      return stopBoardAlightTransferCosts == null
+        ? fixedCost
+        : fixedCost - stopBoardAlightTransferCosts[fromStop];
     }
   }
 
@@ -142,12 +153,12 @@ public int calculateRemainingMinCost(int minTravelTime, int minNumTransfers, int
   public int costEgress(RaptorAccessEgress egress) {
     if (egress.hasRides()) {
       return egress.c1() + transferCostOnly;
-    } else if (stopTransferCost != null) {
+    } else if (stopBoardAlightTransferCosts != null) {
       // Remove cost that was added during alighting.
       // We do not want to add this cost on last alighting since it should only be applied on transfers
       // It has to be done here because during alighting we do not know yet if it will be
       // a transfer or not.
-      return egress.c1() - stopTransferCost[egress.stop()];
+      return egress.c1() - stopBoardAlightTransferCosts[egress.stop()];
     } else {
       return egress.c1();
     }
@@ -171,8 +182,8 @@ public int boardingCostRegularTransfer(
     cost += firstBoarding ? boardCostOnly : boardAndTransferCost;
 
     // If it's first boarding event then it is not a transfer
-    if (stopTransferCost != null && !firstBoarding) {
-      cost += stopTransferCost[boardStop];
+    if (stopBoardAlightTransferCosts != null && !firstBoarding) {
+      cost += stopBoardAlightTransferCosts[boardStop];
     }
     return cost;
   }
@@ -210,8 +221,9 @@ private int boardingCostConstrainedTransfer(
       // For a guaranteed transfer we skip board- and transfer-cost
       final int boardWaitTime = boardTime - prevArrivalTime;
 
-      // StopTransferCost is NOT added to the cost here. This is because a trip-to-trip constrained transfer take
-      // precedence over stop-to-stop transfer priority (NeTEx station transfer priority).
+      // StopBoardAlightTransferCost is NOT added to the cost here. This is because a trip-to-trip
+      // constrained transfer take precedence over stop-to-stop transfer priority (NeTEx station
+      // transfer priority).
       return waitFactor * boardWaitTime;
     }
 
diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java
index d74e2fbcbdb..fe81d28c098 100644
--- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java
+++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java
@@ -13,6 +13,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import org.opentripplanner.framework.application.OTPFeature;
 import org.opentripplanner.model.Timetable;
 import org.opentripplanner.routing.algorithm.raptoradapter.transit.Transfer;
@@ -103,7 +104,7 @@ private TransitLayer map(TransitTuningParameters tuningParameters) {
       transferCache,
       constrainedTransfers,
       transferIndexGenerator,
-      createStopTransferCosts(stopModel, tuningParameters)
+      createStopBoardAlightTransferCosts(stopModel, tuningParameters)
     );
   }
 
@@ -179,19 +180,23 @@ private HashMap> keyByRunningPeriodDates(
   }
 
   /**
-   * Create static board/alight cost for Raptor to include for each stop.
+   * Create static board/alight cost for Raptor to apply during transfer
    */
-  static int[] createStopTransferCosts(StopModel stops, TransitTuningParameters tuningParams) {
+  @Nullable
+  static int[] createStopBoardAlightTransferCosts(
+    StopModel stops,
+    TransitTuningParameters tuningParams
+  ) {
     if (!tuningParams.enableStopTransferPriority()) {
       return null;
     }
-    int[] stopTransferCosts = new int[stops.stopIndexSize()];
+    int[] stopBoardAlightTransferCosts = new int[stops.stopIndexSize()];
 
     for (int i = 0; i < stops.stopIndexSize(); ++i) {
       StopTransferPriority priority = stops.stopByIndex(i).getPriority();
-      int domainCost = tuningParams.stopTransferCost(priority);
-      stopTransferCosts[i] = RaptorCostConverter.toRaptorCost(domainCost);
+      int domainCost = tuningParams.stopBoardAlightDuringTransferCost(priority);
+      stopBoardAlightTransferCosts[i] = RaptorCostConverter.toRaptorCost(domainCost);
     }
-    return stopTransferCosts;
+    return stopBoardAlightTransferCosts;
   }
 }
diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java
index 4338593d59d..8c310206a01 100644
--- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java
+++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java
@@ -104,7 +104,7 @@ public RaptorRoutingRequestTransitData(
     this.generalizedCostCalculator =
       CostCalculatorFactory.createCostCalculator(
         mcCostParams,
-        transitLayer.getStopBoardAlightCosts()
+        transitLayer.getStopBoardAlightTransferCosts()
       );
 
     this.slackProvider =
diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/api/TransferOptimizationParameters.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/api/TransferOptimizationParameters.java
index 554de3ddc6d..0f51dd83165 100644
--- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/api/TransferOptimizationParameters.java
+++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/api/TransferOptimizationParameters.java
@@ -35,9 +35,10 @@ public interface TransferOptimizationParameters {
   double minSafeWaitTimeFactor();
 
   /**
-   * Use this to add an extra board- and alight-cost for (none) prioritized stops. A {@code
-   * stopBoardAlightCosts} is added to the generalized-cost during routing. But, this cost cannot be
-   * too high, because that would add extra cost to the transfer, and favor other alternative paths.
+   * Use this to add an extra board- and alight-cost for (non) prioritized stops. A {@code
+   * stopBoardAlightTransferCosts} is added to the generalized-cost during routing. But, this cost
+   * cannot be too high, because that would add extra cost to the transfer, and favor other
+   * alternative paths.
    * But, when optimizing transfers, we do not have to take other paths into consideration and can
    * "boost" the stop-priority-cost to allow transfers to take place at a preferred stop.
    * 

diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/configure/TransferOptimizationServiceConfigurator.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/configure/TransferOptimizationServiceConfigurator.java index a733927f068..cb5f3d7fdab 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/configure/TransferOptimizationServiceConfigurator.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/configure/TransferOptimizationServiceConfigurator.java @@ -1,6 +1,7 @@ package org.opentripplanner.routing.algorithm.transferoptimization.configure; import java.util.function.IntFunction; +import javax.annotation.Nullable; import org.opentripplanner.model.transfer.TransferService; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; @@ -28,7 +29,10 @@ public class TransferOptimizationServiceConfigurator transitDataProvider; - private final int[] stopBoardAlightCosts; + + @Nullable + private final int[] stopBoardAlightTransferCosts; + private final TransferOptimizationParameters config; private final MultiCriteriaRequest multiCriteriaRequest; @@ -37,7 +41,7 @@ private TransferOptimizationServiceConfigurator( RaptorStopNameResolver stopNameResolver, TransferService transferService, RaptorTransitDataProvider transitDataProvider, - int[] stopBoardAlightCosts, + int[] stopBoardAlightTransferCosts, TransferOptimizationParameters config, MultiCriteriaRequest multiCriteriaRequest ) { @@ -45,7 +49,7 @@ private TransferOptimizationServiceConfigurator( this.stopNameResolver = stopNameResolver; this.transferService = transferService; this.transitDataProvider = transitDataProvider; - this.stopBoardAlightCosts = stopBoardAlightCosts; + this.stopBoardAlightTransferCosts = stopBoardAlightTransferCosts; this.config = config; this.multiCriteriaRequest = multiCriteriaRequest; } @@ -60,7 +64,7 @@ > OptimizeTransferService createOptimizeTransferService( RaptorStopNameResolver stopNameResolver, TransferService transferService, RaptorTransitDataProvider transitDataProvider, - int[] stopBoardAlightCosts, + @Nullable int[] stopBoardAlightTransferCosts, TransferOptimizationParameters config, MultiCriteriaRequest multiCriteriaRequest ) { @@ -69,7 +73,7 @@ > OptimizeTransferService createOptimizeTransferService( stopNameResolver, transferService, transitDataProvider, - stopBoardAlightCosts, + stopBoardAlightTransferCosts, config, multiCriteriaRequest ) @@ -113,7 +117,7 @@ private OptimizePathDomainService createOptimizePathService( costCalculator, transitDataProvider.slackProvider(), transferWaitTimeCostCalculator, - stopBoardAlightCosts, + stopBoardAlightTransferCosts, config.extraStopBoardAlightCostsFactor(), createFilter(), stopNameResolver diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTail.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTail.java index fa74648e8eb..8d18c461101 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTail.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTail.java @@ -33,6 +33,7 @@ public class OptimizedPathTail @Nullable private final TransferWaitTimeCostCalculator waitTimeCostCalculator; + @Nullable private final StopPriorityCostCalculator stopPriorityCostCalculator; private int transferPriorityCost = TransferConstraint.ZERO_COST; @@ -44,15 +45,18 @@ public OptimizedPathTail( RaptorCostCalculator costCalculator, int iterationDepartureTime, TransferWaitTimeCostCalculator waitTimeCostCalculator, - int[] stopBoardAlightCosts, + @Nullable int[] stopBoardAlightTransferCosts, double extraStopBoardAlightCostsFactor, RaptorStopNameResolver stopNameResolver ) { super(slackProvider, iterationDepartureTime, costCalculator, stopNameResolver, null); this.waitTimeCostCalculator = waitTimeCostCalculator; this.stopPriorityCostCalculator = - (stopBoardAlightCosts != null && extraStopBoardAlightCostsFactor > 0.01) - ? new StopPriorityCostCalculator(extraStopBoardAlightCostsFactor, stopBoardAlightCosts) + (stopBoardAlightTransferCosts != null && extraStopBoardAlightCostsFactor > 0.01) + ? new StopPriorityCostCalculator( + extraStopBoardAlightCostsFactor, + stopBoardAlightTransferCosts + ) : null; } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/StopPriorityCostCalculator.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/StopPriorityCostCalculator.java index 090dd1ebd5a..48a4b733b58 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/StopPriorityCostCalculator.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/StopPriorityCostCalculator.java @@ -1,22 +1,29 @@ package org.opentripplanner.routing.algorithm.transferoptimization.model; import org.opentripplanner.framework.lang.IntUtils; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; /** - * This calculator is used to give the stop-priority-cost a boost, by multiplying it with a {@code - * factor}. + * This class calculates an extra stop priority cost by using the stop-board-alight-transfer-cost + * and boosting it by multiplying it with a {@code factor}. */ public class StopPriorityCostCalculator { - private final int[] stopTransferCost; - private final double extraStopTransferCostFactor; + /** + * @see TransitLayer#getStopBoardAlightTransferCosts() + */ + private final int[] stopBoardAlightTransferCosts; + private final double extraStopBoardAlightCostFactor; - StopPriorityCostCalculator(double extraStopTransferCostFactor, int[] stopTransferCost) { - this.stopTransferCost = stopTransferCost; - this.extraStopTransferCostFactor = extraStopTransferCostFactor; + StopPriorityCostCalculator( + double extraStopBoardAlightCostFactor, + int[] stopBoardAlightTransferCosts + ) { + this.stopBoardAlightTransferCosts = stopBoardAlightTransferCosts; + this.extraStopBoardAlightCostFactor = extraStopBoardAlightCostFactor; } int extraStopPriorityCost(int stop) { - return IntUtils.round(stopTransferCost[stop] * extraStopTransferCostFactor); + return IntUtils.round(stopBoardAlightTransferCosts[stop] * extraStopBoardAlightCostFactor); } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java index bfe5c3de841..ca8f953ced5 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java @@ -8,7 +8,6 @@ import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; -import org.opentripplanner.raptor.api.model.RaptorConstants; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.RaptorPath; import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; @@ -16,6 +15,7 @@ import org.opentripplanner.raptor.api.path.TransitPathLeg; import org.opentripplanner.raptor.spi.RaptorCostCalculator; import org.opentripplanner.raptor.spi.RaptorSlackProvider; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; import org.opentripplanner.routing.algorithm.transferoptimization.api.OptimizedPath; import org.opentripplanner.routing.algorithm.transferoptimization.model.OptimizedPathTail; import org.opentripplanner.routing.algorithm.transferoptimization.model.PathTailFilter; @@ -78,8 +78,11 @@ public class OptimizePathDomainService { @Nullable private final TransferWaitTimeCostCalculator waitTimeCostCalculator; + /** + * @see TransitLayer#getStopBoardAlightTransferCosts() + */ @Nullable - private final int[] stopBoardAlightCosts; + private final int[] stopBoardAlightTransferCosts; private final double extraStopBoardAlightCostsFactor; @@ -88,7 +91,7 @@ public OptimizePathDomainService( RaptorCostCalculator costCalculator, RaptorSlackProvider slackProvider, @Nullable TransferWaitTimeCostCalculator waitTimeCostCalculator, - int[] stopBoardAlightCosts, + @Nullable int[] stopBoardAlightTransferCosts, double extraStopBoardAlightCostsFactor, PathTailFilter filter, RaptorStopNameResolver stopNameTranslator @@ -97,7 +100,7 @@ public OptimizePathDomainService( this.costCalculator = costCalculator; this.slackProvider = slackProvider; this.waitTimeCostCalculator = waitTimeCostCalculator; - this.stopBoardAlightCosts = stopBoardAlightCosts; + this.stopBoardAlightTransferCosts = stopBoardAlightTransferCosts; this.extraStopBoardAlightCostsFactor = extraStopBoardAlightCostsFactor; this.filter = filter; this.stopNameTranslator = stopNameTranslator; @@ -139,7 +142,7 @@ private Set> findBestTransferOption( costCalculator, iterationDepartureTime, waitTimeCostCalculator, - stopBoardAlightCosts, + stopBoardAlightTransferCosts, extraStopBoardAlightCostsFactor, stopNameTranslator ) diff --git a/src/main/java/org/opentripplanner/routing/api/request/framework/TimePenalty.java b/src/main/java/org/opentripplanner/routing/api/request/framework/TimePenalty.java index aaa81db5a09..0c6fdd96436 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/framework/TimePenalty.java +++ b/src/main/java/org/opentripplanner/routing/api/request/framework/TimePenalty.java @@ -7,6 +7,7 @@ public final class TimePenalty extends AbstractLinearFunction { public static final TimePenalty ZERO = new TimePenalty(Duration.ZERO, 0.0); + public static final TimePenalty NONE = new TimePenalty(Duration.ZERO, 1.0); private TimePenalty(Duration constant, double coefficient) { super(DurationUtils.requireNonNegative(constant), coefficient); @@ -31,6 +32,13 @@ protected boolean isZero(Duration value) { return value.isZero(); } + /** + * Does this penalty actually modify a duration or would it be returned unchanged? + */ + public boolean modifies() { + return !constant().isZero() && coefficient() != 1.0; + } + @Override protected Duration constantAsDuration() { return constant(); diff --git a/src/main/java/org/opentripplanner/standalone/config/buildconfig/GtfsConfig.java b/src/main/java/org/opentripplanner/standalone/config/buildconfig/GtfsConfig.java index a0a947e429a..e4a400dffe3 100644 --- a/src/main/java/org/opentripplanner/standalone/config/buildconfig/GtfsConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/buildconfig/GtfsConfig.java @@ -74,7 +74,7 @@ private static GtfsFeedParametersBuilder mapGenericParameters( .description( """ This parameter sets the generic level of preference. What is the actual cost can be changed - with the `stopTransferCost` parameter in the router configuration. + with the `stopBoardAlightDuringTransferCost` parameter in the router configuration. """ ) .docDefaultValue(docDefaults.stationTransferPreference()) diff --git a/src/main/java/org/opentripplanner/standalone/config/routerconfig/TransitRoutingConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerconfig/TransitRoutingConfig.java index d4c99004a59..34465cff27a 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerconfig/TransitRoutingConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerconfig/TransitRoutingConfig.java @@ -31,7 +31,7 @@ public final class TransitRoutingConfig implements RaptorTuningParameters, Trans private final List transferCacheRequests; private final List pagingSearchWindowAdjustments; - private final Map stopTransferCost; + private final Map stopBoardAlightDuringTransferCost; private final Duration maxSearchWindow; private final DynamicSearchWindowCoefficients dynamicSearchWindowCoefficients; @@ -114,18 +114,24 @@ public TransitRoutingConfig( ) .asInt(dft.searchThreadPoolSize()); // Dynamic Search Window - this.stopTransferCost = + this.stopBoardAlightDuringTransferCost = c - .of("stopTransferCost") + .of("stopBoardAlightDuringTransferCost") .since(V2_0) - .summary("Use this to set a stop transfer cost for the given transfer priority") + .summary( + "Costs for boarding and alighting during transfers at stops with a given transfer priority." + ) .description( """ -The cost is applied to boarding and alighting at all stops. All stops have a transfer cost priority -set, the default is `allowed`. The `stopTransferCost` parameter is optional, but if listed all -values must be set. +This cost is applied **both to boarding and alighting** at stops during transfers. All stops have a +transfer cost priority set, the default is `allowed`. The `stopBoardAlightDuringTransferCost` +parameter is optional, but if listed all values must be set. + +When a transfer occurs at the same stop, the cost will be applied twice since the cost is both for +boarding and alighting, -If not set the `stopTransferCost` is ignored. This is only available for NeTEx imported Stops. +If not set the `stopBoardAlightDuringTransferCost` is ignored. This is only available for NeTEx +imported Stops. The cost is a scalar, but is equivalent to the felt cost of riding a transit trip for 1 second. @@ -137,7 +143,7 @@ public TransitRoutingConfig( | `preferred` | The best place to do transfers. Should be set to `0`(zero). | int | Use values in a range from `0` to `100 000`. **All key/value pairs are required if the -`stopTransferCost` is listed.** +`stopBoardAlightDuringTransferCost` is listed.** """ ) .asEnumMapAllKeysRequired(StopTransferPriority.class, Integer.class); @@ -250,12 +256,12 @@ public DynamicSearchWindowCoefficients dynamicSearchWindowCoefficients() { @Override public boolean enableStopTransferPriority() { - return stopTransferCost != null; + return stopBoardAlightDuringTransferCost != null; } @Override - public Integer stopTransferCost(StopTransferPriority key) { - return stopTransferCost.get(key); + public Integer stopBoardAlightDuringTransferCost(StopTransferPriority key) { + return stopBoardAlightDuringTransferCost.get(key); } @Override diff --git a/src/main/java/org/opentripplanner/standalone/config/routerequest/TransferConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerequest/TransferConfig.java index 91665d56d38..365c4e89145 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerequest/TransferConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerequest/TransferConfig.java @@ -144,7 +144,7 @@ private static TransferOptimizationPreferences mapTransferOptimization(NodeAdapt .summary("Add an extra board- and alight-cost for prioritized stops.") .description( """ - A stopBoardAlightCosts is added to the generalized-cost during routing. But this cost + A stopBoardAlightTransferCosts is added to the generalized-cost during routing. But this cost cannot be too high, because that would add extra cost to the transfer, and favor other alternative paths. But, when optimizing transfers, we do not have to take other paths into consideration and can *boost* the stop-priority-cost to allow transfers to diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/Trip.java b/src/main/java/org/opentripplanner/transit/model/timetable/Trip.java index 9e6795e5140..5a1e9150e78 100644 --- a/src/main/java/org/opentripplanner/transit/model/timetable/Trip.java +++ b/src/main/java/org/opentripplanner/transit/model/timetable/Trip.java @@ -36,7 +36,6 @@ public final class Trip extends AbstractTransitEntity impleme private final Accessibility wheelchairBoarding; private final String gtfsBlockId; - private final String gtfsFareId; private final String netexInternalPlanningCode; private final TripAlteration netexAlteration; @@ -65,7 +64,6 @@ public final class Trip extends AbstractTransitEntity impleme this.headsign = builder.getHeadsign(); this.shapeId = builder.getShapeId(); this.gtfsBlockId = builder.getGtfsBlockId(); - this.gtfsFareId = builder.getGtfsFareId(); this.netexInternalPlanningCode = builder.getNetexInternalPlanningCode(); } @@ -149,12 +147,6 @@ public String getGtfsBlockId() { return gtfsBlockId; } - /** Custom extension for KCM to specify a fare per-trip */ - @Nullable - public String getGtfsFareId() { - return gtfsFareId; - } - /** * Internal code (non-public identifier) for the journey (e.g. train- or trip number from the * planners' tool). This is kept to ensure compatibility with legacy planning systems. In NeTEx @@ -209,8 +201,7 @@ public boolean sameAs(@Nonnull Trip other) { Objects.equals(this.direction, other.direction) && Objects.equals(this.bikesAllowed, other.bikesAllowed) && Objects.equals(this.wheelchairBoarding, other.wheelchairBoarding) && - Objects.equals(this.netexAlteration, other.netexAlteration) && - Objects.equals(this.gtfsFareId, other.gtfsFareId) + Objects.equals(this.netexAlteration, other.netexAlteration) ); } diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/TripBuilder.java b/src/main/java/org/opentripplanner/transit/model/timetable/TripBuilder.java index fd51b9035c7..063dfe10da2 100644 --- a/src/main/java/org/opentripplanner/transit/model/timetable/TripBuilder.java +++ b/src/main/java/org/opentripplanner/transit/model/timetable/TripBuilder.java @@ -23,7 +23,6 @@ public class TripBuilder extends AbstractEntityBuilder { private BikeAccess bikesAllowed; private Accessibility wheelchairBoarding; private String gtfsBlockId; - private String gtfsFareId; private String netexInternalPlanningCode; private TripAlteration netexAlteration; @@ -47,7 +46,6 @@ public class TripBuilder extends AbstractEntityBuilder { this.bikesAllowed = original.getBikesAllowed(); this.wheelchairBoarding = original.getWheelchairBoarding(); this.netexInternalPlanningCode = original.getNetexInternalPlanningCode(); - this.gtfsFareId = original.getGtfsFareId(); } public Operator getOperator() { @@ -176,15 +174,6 @@ public TripBuilder withNetexAlteration(TripAlteration netexAlteration) { return this; } - public String getGtfsFareId() { - return gtfsFareId; - } - - public TripBuilder withGtfsFareId(String gtfsFareId) { - this.gtfsFareId = gtfsFareId; - return this; - } - @Override protected Trip buildFromValues() { return new Trip(this); diff --git a/src/main/java/org/opentripplanner/transit/service/TransitModel.java b/src/main/java/org/opentripplanner/transit/service/TransitModel.java index 2f0162aa8b1..46e22a937be 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitModel.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitModel.java @@ -371,8 +371,9 @@ public TripPattern getTripPatternForId(FeedScopedId id) { return tripPatternForId.get(id); } - public Map getTripOnServiceDates() { - return tripOnServiceDates; + public void addTripOnServiceDate(FeedScopedId id, TripOnServiceDate tripOnServiceDate) { + invalidateIndex(); + tripOnServiceDates.put(id, tripOnServiceDate); } /** diff --git a/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java b/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java index 21f6c81bc67..364972531c2 100644 --- a/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java +++ b/src/main/java/org/opentripplanner/updater/alert/GtfsRealtimeAlertsUpdater.java @@ -3,6 +3,7 @@ import com.google.transit.realtime.GtfsRealtime.FeedMessage; import java.net.URI; import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.routing.impl.TransitAlertServiceImpl; import org.opentripplanner.routing.services.TransitAlertService; @@ -50,7 +51,7 @@ public GtfsRealtimeAlertsUpdater( this.updateHandler.setFeedId(config.feedId()); this.updateHandler.setTransitAlertService(transitAlertService); this.updateHandler.setFuzzyTripMatcher(fuzzyTripMatcher); - this.otpHttpClient = new OtpHttpClient(); + this.otpHttpClient = new OtpHttpClientFactory().create(LOG); LOG.info("Creating real-time alert updater running every {}: {}", pollingPeriod(), url); } diff --git a/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java b/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java index c755578da41..3b6f7f52279 100644 --- a/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java +++ b/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java @@ -10,7 +10,7 @@ import org.opentripplanner.ext.siri.updater.azure.SiriAzureSXUpdater; import org.opentripplanner.ext.vehiclerentalservicedirectory.VehicleRentalServiceDirectoryFetcher; import org.opentripplanner.ext.vehiclerentalservicedirectory.api.VehicleRentalServiceDirectoryFetcherParameters; -import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.model.calendar.openinghours.OpeningHoursCalendarService; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; @@ -28,6 +28,8 @@ import org.opentripplanner.updater.vehicle_position.PollingVehiclePositionUpdater; import org.opentripplanner.updater.vehicle_rental.VehicleRentalUpdater; import org.opentripplanner.updater.vehicle_rental.datasources.VehicleRentalDataSourceFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Sets up and starts all the graph updaters. @@ -38,6 +40,8 @@ */ public class UpdaterConfigurator { + private static final Logger LOG = LoggerFactory.getLogger(UpdaterConfigurator.class); + private final Graph graph; private final TransitModel transitModel; private final UpdatersParameters updatersParameters; @@ -137,11 +141,11 @@ private List createUpdatersFromConfig() { if (!updatersParameters.getVehicleRentalParameters().isEmpty()) { int maxHttpConnections = updatersParameters.getVehicleRentalParameters().size(); - OtpHttpClient otpHttpClient = new OtpHttpClient(maxHttpConnections); + var otpHttpClientFactory = new OtpHttpClientFactory(maxHttpConnections); for (var configItem : updatersParameters.getVehicleRentalParameters()) { var source = VehicleRentalDataSourceFactory.create( configItem.sourceParameters(), - otpHttpClient + otpHttpClientFactory ); updaters.add( new VehicleRentalUpdater(configItem, source, graph.getLinker(), vehicleRentalRepository) diff --git a/src/main/java/org/opentripplanner/updater/spi/GenericJsonDataSource.java b/src/main/java/org/opentripplanner/updater/spi/GenericJsonDataSource.java index df275073f72..894c0a0466f 100644 --- a/src/main/java/org/opentripplanner/updater/spi/GenericJsonDataSource.java +++ b/src/main/java/org/opentripplanner/updater/spi/GenericJsonDataSource.java @@ -4,6 +4,7 @@ import java.util.List; import org.opentripplanner.framework.io.JsonDataListDownloader; import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,7 +16,7 @@ public abstract class GenericJsonDataSource implements DataSource { protected List updates = List.of(); protected GenericJsonDataSource(String url, String jsonParsePath, HttpHeaders headers) { - this(url, jsonParsePath, headers, new OtpHttpClient()); + this(url, jsonParsePath, headers, new OtpHttpClientFactory().create(LOG)); } protected GenericJsonDataSource( diff --git a/src/main/java/org/opentripplanner/updater/trip/GtfsRealtimeTripUpdateSource.java b/src/main/java/org/opentripplanner/updater/trip/GtfsRealtimeTripUpdateSource.java index 765f6d11845..12a044424ca 100644 --- a/src/main/java/org/opentripplanner/updater/trip/GtfsRealtimeTripUpdateSource.java +++ b/src/main/java/org/opentripplanner/updater/trip/GtfsRealtimeTripUpdateSource.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.updater.spi.HttpHeaders; import org.slf4j.Logger; @@ -33,7 +34,7 @@ public GtfsRealtimeTripUpdateSource(PollingTripUpdaterParameters config) { this.url = config.url(); this.headers = HttpHeaders.of().acceptProtobuf().add(config.headers()).build(); MfdzRealtimeExtensions.registerAllExtensions(registry); - otpHttpClient = new OtpHttpClient(); + otpHttpClient = new OtpHttpClientFactory().create(LOG); } public List getUpdates() { diff --git a/src/main/java/org/opentripplanner/updater/vehicle_position/GtfsRealtimeHttpVehiclePositionSource.java b/src/main/java/org/opentripplanner/updater/vehicle_position/GtfsRealtimeHttpVehiclePositionSource.java index 5983eef7c40..ec0be6822e7 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_position/GtfsRealtimeHttpVehiclePositionSource.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_position/GtfsRealtimeHttpVehiclePositionSource.java @@ -9,6 +9,7 @@ import java.util.List; import org.opentripplanner.framework.io.OtpHttpClient; import org.opentripplanner.framework.io.OtpHttpClientException; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.updater.spi.HttpHeaders; import org.slf4j.Logger; @@ -33,7 +34,7 @@ public class GtfsRealtimeHttpVehiclePositionSource { public GtfsRealtimeHttpVehiclePositionSource(URI url, HttpHeaders headers) { this.url = url; this.headers = HttpHeaders.of().acceptProtobuf().add(headers).build(); - this.otpHttpClient = new OtpHttpClient(); + this.otpHttpClient = new OtpHttpClientFactory().create(LOG); } /** diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoader.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoader.java index 58a625192f9..d4e5cbd4952 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoader.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoader.java @@ -13,6 +13,7 @@ import org.entur.gbfs.v2_3.gbfs.GBFSFeeds; import org.opentripplanner.framework.io.OtpHttpClient; import org.opentripplanner.framework.io.OtpHttpClientException; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.updater.spi.HttpHeaders; import org.opentripplanner.updater.spi.UpdaterConstructionException; import org.slf4j.Logger; @@ -98,7 +99,7 @@ public GbfsFeedLoader( } GbfsFeedLoader(String url, HttpHeaders httpHeaders, String languageCode) { - this(url, httpHeaders, languageCode, new OtpHttpClient()); + this(url, httpHeaders, languageCode, new OtpHttpClientFactory().create(LOG)); } /** diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSource.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSource.java index 8cc8897f0de..aed214102ff 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSource.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSource.java @@ -16,6 +16,7 @@ import org.entur.gbfs.v2_3.vehicle_types.GBFSVehicleTypes; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.service.vehiclerental.model.GeofencingZone; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; @@ -46,10 +47,10 @@ class GbfsVehicleRentalDataSource implements VehicleRentalDatasource { public GbfsVehicleRentalDataSource( GbfsVehicleRentalDataSourceParameters parameters, - OtpHttpClient otpHttpClient + OtpHttpClientFactory otpHttpClientFactory ) { this.params = parameters; - this.otpHttpClient = otpHttpClient; + this.otpHttpClient = otpHttpClientFactory.create(LOG); } @Override diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/VehicleRentalDataSourceFactory.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/VehicleRentalDataSourceFactory.java index 50e26d82459..bd921a70edb 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/VehicleRentalDataSourceFactory.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/VehicleRentalDataSourceFactory.java @@ -2,7 +2,7 @@ import org.opentripplanner.ext.smoovebikerental.SmooveBikeRentalDataSource; import org.opentripplanner.ext.smoovebikerental.SmooveBikeRentalDataSourceParameters; -import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.updater.vehicle_rental.datasources.params.GbfsVehicleRentalDataSourceParameters; import org.opentripplanner.updater.vehicle_rental.datasources.params.VehicleRentalDataSourceParameters; @@ -10,7 +10,7 @@ public class VehicleRentalDataSourceFactory { public static VehicleRentalDatasource create( VehicleRentalDataSourceParameters source, - OtpHttpClient otpHttpClient + OtpHttpClientFactory otpHttpClientFactory ) { return switch (source.sourceType()) { // There used to be a lot more updaters and corresponding config variables here, but since @@ -20,11 +20,11 @@ public static VehicleRentalDatasource create( // and become the point of contact for the community. case GBFS -> new GbfsVehicleRentalDataSource( (GbfsVehicleRentalDataSourceParameters) source, - otpHttpClient + otpHttpClientFactory ); case SMOOVE -> new SmooveBikeRentalDataSource( (SmooveBikeRentalDataSourceParameters) source, - otpHttpClient + otpHttpClientFactory ); }; } diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 56f3528d6e3..0f863393d87 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -1082,7 +1082,7 @@ input InputBanned { banned for boarding and disembarking vehicles — it is possible to get an itinerary where a vehicle stops at one of these stops """ - stops: String + stops: String @deprecated(reason: "Not implemented in OTP2.") """ A comma-separated list of banned stop ids. Only itineraries where these stops @@ -1090,7 +1090,7 @@ input InputBanned { these stops, that route will not be used in the itinerary, even if the stop is not used for boarding or disembarking the vehicle. """ - stopsHard: String + stopsHard: String @deprecated(reason: "Not implemented in OTP2.") } input InputCoordinates { diff --git a/src/test/java/org/opentripplanner/_support/geometry/LineStrings.java b/src/test/java/org/opentripplanner/_support/geometry/LineStrings.java new file mode 100644 index 00000000000..515f161be92 --- /dev/null +++ b/src/test/java/org/opentripplanner/_support/geometry/LineStrings.java @@ -0,0 +1,9 @@ +package org.opentripplanner._support.geometry; + +import org.locationtech.jts.geom.LineString; +import org.opentripplanner.framework.geometry.GeometryUtils; + +public class LineStrings { + + public static final LineString SIMPLE = GeometryUtils.makeLineString(0, 0, 1, 1); +} diff --git a/src/test/java/org/opentripplanner/_support/geometry/Polygons.java b/src/test/java/org/opentripplanner/_support/geometry/Polygons.java index a386d8a27e1..ee110ab4f4f 100644 --- a/src/test/java/org/opentripplanner/_support/geometry/Polygons.java +++ b/src/test/java/org/opentripplanner/_support/geometry/Polygons.java @@ -20,7 +20,7 @@ public class Polygons { } ); - public static Polygon OSLO = FAC.createPolygon( + public static final Polygon OSLO = FAC.createPolygon( new Coordinate[] { Coordinates.of(59.961055202323195, 10.62535658370308), Coordinates.of(59.889009435700416, 10.62535658370308), @@ -29,7 +29,7 @@ public class Polygons { Coordinates.of(59.961055202323195, 10.62535658370308), } ); - public static Polygon OSLO_FROGNER_PARK = FAC.createPolygon( + public static final Polygon OSLO_FROGNER_PARK = FAC.createPolygon( new Coordinate[] { Coordinates.of(59.92939032560119, 10.69770054003061), Coordinates.of(59.929138466684975, 10.695210909925208), diff --git a/src/test/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapperTest.java b/src/test/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapperTest.java index b8d08c7d7f5..d897fc808c2 100644 --- a/src/test/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapperTest.java +++ b/src/test/java/org/opentripplanner/apis/transmodel/mapping/RequestModesMapperTest.java @@ -1,6 +1,6 @@ package org.opentripplanner.apis.transmodel.mapping; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.Map; import org.junit.jupiter.api.Test; @@ -9,11 +9,32 @@ class RequestModesMapperTest { + private static final RequestModes MODES_NOT_SET = RequestModes + .of() + .withAccessMode(StreetMode.NOT_SET) + .withEgressMode(StreetMode.NOT_SET) + .withDirectMode(StreetMode.NOT_SET) + .build(); + @Test void testMapRequestModesEmptyMapReturnsDefaults() { Map inputModes = Map.of(); - RequestModes wantModes = RequestModes.of().withDirectMode(null).build(); + RequestModes mappedModes = RequestModesMapper.mapRequestModes(inputModes); + + assertEquals(MODES_NOT_SET, mappedModes); + } + + @Test + void testMapRequestModesScooterRentalAccessSetReturnsDefaultsForOthers() { + Map inputModes = Map.of("accessMode", StreetMode.SCOOTER_RENTAL); + + RequestModes wantModes = MODES_NOT_SET + .copyOf() + .withAccessMode(StreetMode.SCOOTER_RENTAL) + .withTransferMode(StreetMode.WALK) + .withDirectMode(null) + .build(); RequestModes mappedModes = RequestModesMapper.mapRequestModes(inputModes); @@ -21,11 +42,11 @@ void testMapRequestModesEmptyMapReturnsDefaults() { } @Test - void testMapRequestModesAccessSetReturnsDefaultsForOthers() { + void testMapRequestModesBikeAccessSetReturnsDefaultsForOthers() { Map inputModes = Map.of("accessMode", StreetMode.BIKE); - RequestModes wantModes = RequestModes - .of() + RequestModes wantModes = MODES_NOT_SET + .copyOf() .withAccessMode(StreetMode.BIKE) .withTransferMode(StreetMode.BIKE) .withDirectMode(null) @@ -40,8 +61,8 @@ void testMapRequestModesAccessSetReturnsDefaultsForOthers() { void testMapRequestModesEgressSetReturnsDefaultsForOthers() { Map inputModes = Map.of("egressMode", StreetMode.CAR); - RequestModes wantModes = RequestModes - .of() + RequestModes wantModes = MODES_NOT_SET + .copyOf() .withEgressMode(StreetMode.CAR) .withDirectMode(null) .build(); @@ -55,7 +76,7 @@ void testMapRequestModesEgressSetReturnsDefaultsForOthers() { void testMapRequestModesDirectSetReturnsDefaultsForOthers() { Map inputModes = Map.of("directMode", StreetMode.CAR); - RequestModes wantModes = RequestModes.of().withDirectMode(StreetMode.CAR).build(); + RequestModes wantModes = MODES_NOT_SET.copyOf().withDirectMode(StreetMode.CAR).build(); RequestModes mappedModes = RequestModesMapper.mapRequestModes(inputModes); diff --git a/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java b/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java index e35f198d16e..654c52ce912 100644 --- a/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java +++ b/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java @@ -15,11 +15,15 @@ import io.micrometer.core.instrument.Metrics; import java.time.Duration; import java.time.LocalDate; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -64,7 +68,6 @@ public class TripRequestMapperTest implements PlanTestConstants { private static TransitModelForTest TEST_MODEL = TransitModelForTest.of(); - static final TransmodelRequestContext context; private static final Duration MAX_FLEXIBLE = Duration.ofMinutes(20); private static final Function STOP_TO_ID = s -> s.getId().toString(); @@ -76,8 +79,12 @@ public class TripRequestMapperTest implements PlanTestConstants { private static final RegularStop stop2 = TEST_MODEL.stop("ST:stop2", 2, 1).build(); private static final RegularStop stop3 = TEST_MODEL.stop("ST:stop3", 3, 1).build(); + private static final Graph graph = new Graph(); + private static final DefaultTransitService transitService; + + private TransmodelRequestContext context; + static { - var graph = new Graph(); var itinerary = newItinerary(Place.forStop(stop1), time("11:00")) .bus(route1, 1, time("11:05"), time("11:20"), Place.forStop(stop2)) .bus(route2, 2, time("11:20"), time("11:40"), Place.forStop(stop3)) @@ -103,8 +110,12 @@ public class TripRequestMapperTest implements PlanTestConstants { transitModel.updateCalendarServiceData(true, calendarServiceData, DataImportIssueStore.NOOP); transitModel.index(); - final var transitService = new DefaultTransitService(transitModel); - var defaultRequest = new RouteRequest(); + transitService = new DefaultTransitService(transitModel); + } + + @BeforeEach + void setup() { + final RouteRequest defaultRequest = new RouteRequest(); // Change defaults for FLEXIBLE to a lower value than the default 45m. This should restrict the // input to be less than 20m, not 45m. @@ -335,6 +346,74 @@ void testPassThroughPointsNoMatch() { assertEquals("No match for F:XX:NonExisting.", ex.getMessage()); } + @Test + public void testNoModes() { + var req = TripRequestMapper.createRequest(executionContext(Map.of())); + + assertEquals(StreetMode.WALK, req.journey().access().mode()); + assertEquals(StreetMode.WALK, req.journey().egress().mode()); + assertEquals(StreetMode.WALK, req.journey().direct().mode()); + assertEquals(StreetMode.WALK, req.journey().transfer().mode()); + } + + @Test + public void testEmptyModes() { + Map arguments = Map.of("modes", Map.of()); + var req = TripRequestMapper.createRequest(executionContext(arguments)); + + assertEquals(StreetMode.NOT_SET, req.journey().access().mode()); + assertEquals(StreetMode.NOT_SET, req.journey().egress().mode()); + assertEquals(StreetMode.NOT_SET, req.journey().direct().mode()); + assertEquals(StreetMode.WALK, req.journey().transfer().mode()); + } + + @Test + public void testNullModes() { + HashMap modes = new HashMap<>(); + modes.put("accessMode", null); + modes.put("egressMode", null); + modes.put("directMode", null); + Map arguments = Map.of("modes", modes); + var req = TripRequestMapper.createRequest(executionContext(arguments)); + + assertEquals(StreetMode.NOT_SET, req.journey().access().mode()); + assertEquals(StreetMode.NOT_SET, req.journey().egress().mode()); + assertEquals(StreetMode.NOT_SET, req.journey().direct().mode()); + assertEquals(StreetMode.WALK, req.journey().transfer().mode()); + } + + @Test + public void testExplicitModes() { + Map arguments = Map.of( + "modes", + Map.of( + "accessMode", + StreetMode.SCOOTER_RENTAL, + "egressMode", + StreetMode.BIKE_RENTAL, + "directMode", + StreetMode.BIKE_TO_PARK + ) + ); + var req = TripRequestMapper.createRequest(executionContext(arguments)); + + assertEquals(StreetMode.SCOOTER_RENTAL, req.journey().access().mode()); + assertEquals(StreetMode.BIKE_RENTAL, req.journey().egress().mode()); + assertEquals(StreetMode.BIKE_TO_PARK, req.journey().direct().mode()); + assertEquals(StreetMode.WALK, req.journey().transfer().mode()); + } + + @Test + public void testExplicitModesBikeAccess() { + Map arguments = Map.of("modes", Map.of("accessMode", StreetMode.BIKE)); + var req = TripRequestMapper.createRequest(executionContext(arguments)); + + assertEquals(StreetMode.BIKE, req.journey().access().mode()); + assertEquals(StreetMode.NOT_SET, req.journey().egress().mode()); + assertEquals(StreetMode.NOT_SET, req.journey().direct().mode()); + assertEquals(StreetMode.BIKE, req.journey().transfer().mode()); + } + private DataFetchingEnvironment executionContext(Map arguments) { ExecutionInput executionInput = ExecutionInput .newExecutionInput() diff --git a/src/test/java/org/opentripplanner/graph_builder/module/islandpruning/TestNamer.java b/src/test/java/org/opentripplanner/graph_builder/module/islandpruning/TestNamer.java index 0d2ad370541..4002da919d8 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/islandpruning/TestNamer.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/islandpruning/TestNamer.java @@ -2,9 +2,9 @@ import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.NonLocalizedString; +import org.opentripplanner.graph_builder.module.osm.StreetEdgePair; import org.opentripplanner.graph_builder.services.osm.EdgeNamer; import org.opentripplanner.openstreetmap.model.OSMWithTags; -import org.opentripplanner.street.model.edge.StreetEdge; class TestNamer implements EdgeNamer { @@ -14,7 +14,7 @@ public I18NString name(OSMWithTags way) { } @Override - public void recordEdge(OSMWithTags way, StreetEdge edge) {} + public void recordEdges(OSMWithTags way, StreetEdgePair edge) {} @Override public void postprocess() {} diff --git a/src/test/java/org/opentripplanner/graph_builder/module/osm/naming/SidewalkNamerTest.java b/src/test/java/org/opentripplanner/graph_builder/module/osm/naming/SidewalkNamerTest.java new file mode 100644 index 00000000000..c1c3d7e5e73 --- /dev/null +++ b/src/test/java/org/opentripplanner/graph_builder/module/osm/naming/SidewalkNamerTest.java @@ -0,0 +1,115 @@ +package org.opentripplanner.graph_builder.module.osm.naming; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.geometry.GeometryUtils; +import org.opentripplanner.framework.geometry.WgsCoordinate; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.graph_builder.module.osm.StreetEdgePair; +import org.opentripplanner.graph_builder.services.osm.EdgeNamer; +import org.opentripplanner.openstreetmap.model.OSMWay; +import org.opentripplanner.openstreetmap.wayproperty.specifier.WayTestData; +import org.opentripplanner.street.model.StreetTraversalPermission; +import org.opentripplanner.street.model._data.StreetModelForTest; +import org.opentripplanner.street.model.edge.StreetEdge; +import org.opentripplanner.street.model.edge.StreetEdgeBuilder; +import org.opentripplanner.test.support.GeoJsonIo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class SidewalkNamerTest { + + private static final I18NString SIDEWALK = I18NString.of("sidewalk"); + private static final Logger LOG = LoggerFactory.getLogger(SidewalkNamerTest.class); + + @Test + void postprocess() { + var builder = new ModelBuilder(); + var sidewalk = builder.addUnamedSidewalk( + new WgsCoordinate(33.75029, -84.39198), + new WgsCoordinate(33.74932, -84.39275) + ); + + LOG.info( + "Geometry of {}: {}", + sidewalk.edge.getName(), + GeoJsonIo.toUrl(sidewalk.edge.getGeometry()) + ); + + var pryorStreet = builder.addStreetEdge( + "Pryor Street", + new WgsCoordinate(33.75032, -84.39190), + new WgsCoordinate(33.74924, -84.39275) + ); + + LOG.info( + "Geometry of {}: {}", + pryorStreet.edge.getName(), + GeoJsonIo.toUrl(pryorStreet.edge.getGeometry()) + ); + + assertNotEquals(sidewalk.edge.getName(), pryorStreet.edge.getName()); + builder.postProcess(new SidewalkNamer()); + assertEquals(sidewalk.edge.getName(), pryorStreet.edge.getName()); + } + + private static class ModelBuilder { + + private final List pairs = new ArrayList<>(); + + EdgePair addUnamedSidewalk(WgsCoordinate... coordinates) { + var edge = edgeBuilder(coordinates) + .withName(SIDEWALK) + .withPermission(StreetTraversalPermission.PEDESTRIAN) + .withBogusName(true) + .buildAndConnect(); + + var way = WayTestData.footwaySidewalk(); + assertTrue(way.isSidewalk()); + var p = new EdgePair(way, edge); + pairs.add(p); + return p; + } + + EdgePair addStreetEdge(String name, WgsCoordinate... coordinates) { + var edge = edgeBuilder(coordinates) + .withName(I18NString.of(name)) + .withPermission(StreetTraversalPermission.ALL) + .buildAndConnect(); + var way = WayTestData.highwayTertiary(); + way.addTag("name", name); + assertFalse(way.isSidewalk()); + assertTrue(way.isNamed()); + var p = new EdgePair(way, edge); + pairs.add(p); + return p; + } + + void postProcess(EdgeNamer namer) { + pairs.forEach(p -> namer.recordEdges(p.way, new StreetEdgePair(p.edge, null))); + namer.postprocess(); + } + + private static StreetEdgeBuilder edgeBuilder(WgsCoordinate... c) { + var coordinates = Arrays.stream(c).toList(); + var ls = GeometryUtils.makeLineString(c); + return new StreetEdgeBuilder<>() + .withFromVertex( + StreetModelForTest.intersectionVertex(coordinates.getFirst().asJtsCoordinate()) + ) + .withToVertex( + StreetModelForTest.intersectionVertex(coordinates.getLast().asJtsCoordinate()) + ) + .withGeometry(ls); + } + } + + private record EdgePair(OSMWay way, StreetEdge edge) {} +} diff --git a/src/test/java/org/opentripplanner/graph_builder/services/osm/EdgeNamerTest.java b/src/test/java/org/opentripplanner/graph_builder/services/osm/EdgeNamerTest.java new file mode 100644 index 00000000000..ff471fbfbf6 --- /dev/null +++ b/src/test/java/org/opentripplanner/graph_builder/services/osm/EdgeNamerTest.java @@ -0,0 +1,19 @@ +package org.opentripplanner.graph_builder.services.osm; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import org.junit.jupiter.api.Test; +import org.opentripplanner.graph_builder.module.osm.naming.DefaultNamer; +import org.opentripplanner.graph_builder.services.osm.EdgeNamer.EdgeNamerType; + +class EdgeNamerTest { + + @Test + void nullType() { + assertInstanceOf(DefaultNamer.class, EdgeNamer.EdgeNamerFactory.fromConfig(null)); + assertInstanceOf( + DefaultNamer.class, + EdgeNamer.EdgeNamerFactory.fromConfig(EdgeNamerType.DEFAULT) + ); + } +} diff --git a/src/test/java/org/opentripplanner/gtfs/mapping/TripMapperTest.java b/src/test/java/org/opentripplanner/gtfs/mapping/TripMapperTest.java index 4f0c70f22d2..f24e405515d 100644 --- a/src/test/java/org/opentripplanner/gtfs/mapping/TripMapperTest.java +++ b/src/test/java/org/opentripplanner/gtfs/mapping/TripMapperTest.java @@ -23,7 +23,6 @@ public class TripMapperTest { private static final int BIKES_ALLOWED = 1; private static final String BLOCK_ID = "Block Id"; private static final int DIRECTION_ID = 1; - private static final String FARE_ID = "Fare Id"; private static final String TRIP_HEADSIGN = "Trip Headsign"; private static final String TRIP_SHORT_NAME = "Trip Short Name"; @@ -33,11 +32,15 @@ public class TripMapperTest { public static final DataImportIssueStore ISSUE_STORE = DataImportIssueStore.NOOP; - private final TripMapper subject = new TripMapper( - new RouteMapper(new AgencyMapper(FEED_ID), ISSUE_STORE, new TranslationHelper()), - new DirectionMapper(ISSUE_STORE), - new TranslationHelper() - ); + private final TripMapper subject = defaultTripMapper(); + + private static TripMapper defaultTripMapper() { + return new TripMapper( + new RouteMapper(new AgencyMapper(FEED_ID), ISSUE_STORE, new TranslationHelper()), + new DirectionMapper(ISSUE_STORE), + new TranslationHelper() + ); + } static { GtfsTestData data = new GtfsTestData(); @@ -46,7 +49,6 @@ public class TripMapperTest { TRIP.setBikesAllowed(BIKES_ALLOWED); TRIP.setBlockId(BLOCK_ID); TRIP.setDirectionId(Integer.toString(DIRECTION_ID)); - TRIP.setFareId(FARE_ID); TRIP.setRoute(data.route); TRIP.setServiceId(AGENCY_AND_ID); TRIP.setShapeId(AGENCY_AND_ID); @@ -56,20 +58,19 @@ public class TripMapperTest { } @Test - public void testMapCollection() throws Exception { + void testMapCollection() throws Exception { assertNull(subject.map((Collection) null)); assertTrue(subject.map(Collections.emptyList()).isEmpty()); assertEquals(1, subject.map(Collections.singleton(TRIP)).size()); } @Test - public void testMap() throws Exception { + void testMap() throws Exception { org.opentripplanner.transit.model.timetable.Trip result = subject.map(TRIP); assertEquals("A:1", result.getId().toString()); assertEquals(BLOCK_ID, result.getGtfsBlockId()); assertEquals(Direction.INBOUND, result.getDirection()); - assertEquals(FARE_ID, result.getGtfsFareId()); assertNotNull(result.getRoute()); assertEquals("A:1", result.getServiceId().toString()); assertEquals("A:1", result.getShapeId().toString()); @@ -80,7 +81,7 @@ public void testMap() throws Exception { } @Test - public void testMapWithNulls() throws Exception { + void testMapWithNulls() throws Exception { Trip input = new Trip(); input.setId(AGENCY_AND_ID); input.setRoute(new GtfsTestData().route); @@ -91,7 +92,6 @@ public void testMapWithNulls() throws Exception { assertNotNull(result.getRoute()); assertNull(result.getGtfsBlockId()); - assertNull(result.getGtfsFareId()); assertNull(result.getServiceId()); assertNull(result.getShapeId()); assertNull(result.getHeadsign()); @@ -101,12 +101,33 @@ public void testMapWithNulls() throws Exception { assertEquals(BikeAccess.UNKNOWN, result.getBikesAllowed()); } - /** Mapping the same object twice, should return the the same instance. */ + /** Mapping the same object twice, should return the same instance. */ @Test - public void testMapCache() throws Exception { + void testMapCache() throws Exception { org.opentripplanner.transit.model.timetable.Trip result1 = subject.map(TRIP); org.opentripplanner.transit.model.timetable.Trip result2 = subject.map(TRIP); assertSame(result1, result2); } + + @Test + void noFlexDurationModifier() { + var mapper = defaultTripMapper(); + mapper.map(TRIP); + assertTrue(mapper.flexSafeDurationModifiers().isEmpty()); + } + + @Test + void flexDurationModifier() { + var flexTrip = new Trip(); + flexTrip.setId(new AgencyAndId("1", "1")); + flexTrip.setSafeDurationFactor(1.5); + flexTrip.setSafeDurationOffset(600d); + flexTrip.setRoute(new GtfsTestData().route); + var mapper = defaultTripMapper(); + var mapped = mapper.map(flexTrip); + var mod = mapper.flexSafeDurationModifiers().get(mapped); + assertEquals(1.5f, mod.coefficient()); + assertEquals(600, mod.constant().toSeconds()); + } } diff --git a/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java b/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java index a3fab1838cb..480639e5bda 100644 --- a/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java +++ b/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java @@ -87,16 +87,14 @@ static void buildTransitService() { calendarServiceData.putServiceDatesForServiceId(tripPattern.getId(), List.of(SERVICE_DATE)); transitModel.updateCalendarServiceData(true, calendarServiceData, DataImportIssueStore.NOOP); - transitModel - .getTripOnServiceDates() - .put( - TRIP_ON_SERVICE_DATE_ID, - TripOnServiceDate - .of(TRIP_ON_SERVICE_DATE_ID) - .withTrip(trip) - .withServiceDate(SERVICE_DATE) - .build() - ); + transitModel.addTripOnServiceDate( + TRIP_ON_SERVICE_DATE_ID, + TripOnServiceDate + .of(TRIP_ON_SERVICE_DATE_ID) + .withTrip(trip) + .withServiceDate(SERVICE_DATE) + .build() + ); transitModel.index(); diff --git a/src/test/java/org/opentripplanner/openstreetmap/model/OSMWithTagsTest.java b/src/test/java/org/opentripplanner/openstreetmap/model/OSMWithTagsTest.java index aefd287cac8..3b50d0bff0d 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/model/OSMWithTagsTest.java +++ b/src/test/java/org/opentripplanner/openstreetmap/model/OSMWithTagsTest.java @@ -262,4 +262,13 @@ void testGenerateI18NForPattern() { osmTags.generateI18NForPattern("Note: {note}, {wheelchair:description}, {foobar:description}") ); } + + @Test + void fallbackName() { + var nameless = WayTestData.cycleway(); + assertTrue(nameless.hasNoName()); + + var namedTunnel = WayTestData.carTunnel(); + assertFalse(namedTunnel.hasNoName()); + } } 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 20dbcbb5a78..b09f690f794 100644 --- a/src/test/java/org/opentripplanner/openstreetmap/wayproperty/specifier/WayTestData.java +++ b/src/test/java/org/opentripplanner/openstreetmap/wayproperty/specifier/WayTestData.java @@ -121,9 +121,10 @@ public static OSMWithTags cyclewayBoth() { return way; } - public static OSMWithTags footwaySidewalk() { - var way = new OSMWithTags(); + public static OSMWay footwaySidewalk() { + var way = new OSMWay(); way.addTag("footway", "sidewalk"); + way.addTag("highway", "footway"); return way; } @@ -155,8 +156,8 @@ public static OSMWithTags highwayTrunk() { return way; } - public static OSMWithTags highwayTertiary() { - var way = new OSMWithTags(); + public static OSMWay highwayTertiary() { + var way = new OSMWay(); way.addTag("highway", "tertiary"); return way; } diff --git a/src/test/java/org/opentripplanner/raptor/_data/transit/TestTransitData.java b/src/test/java/org/opentripplanner/raptor/_data/transit/TestTransitData.java index 037c345bc23..97ad09e3bfd 100644 --- a/src/test/java/org/opentripplanner/raptor/_data/transit/TestTransitData.java +++ b/src/test/java/org/opentripplanner/raptor/_data/transit/TestTransitData.java @@ -66,7 +66,7 @@ public class TestTransitData private final List constrainedTransfers = new ArrayList<>(); private final GeneralizedCostParametersBuilder costParamsBuilder = GeneralizedCostParameters.of(); - private final int[] stopBoardAlightCosts = new int[NUM_STOPS]; + private final int[] stopBoardAlightTransferCosts = new int[NUM_STOPS]; private RaptorSlackProvider slackProvider = SLACK_PROVIDER; @@ -106,7 +106,7 @@ public int numberOfStops() { public RaptorCostCalculator multiCriteriaCostCalculator() { return CostCalculatorFactory.createCostCalculator( costParamsBuilder.build(), - stopBoardAlightCost() + stopBoardAlightTransferCosts() ); } @@ -302,8 +302,8 @@ public TestTransitData withConstrainedTransfer( return this; } - public TestTransitData withStopBoardAlightCost(int stop, int boardAlightCost) { - stopBoardAlightCosts[stop] = boardAlightCost; + public TestTransitData withStopBoardAlightTransferCost(int stop, int boardAlightTransferCost) { + stopBoardAlightTransferCosts[stop] = boardAlightTransferCost; return this; } @@ -357,9 +357,9 @@ public List getPatterns() { /* private methods */ - private int[] stopBoardAlightCost() { + private int[] stopBoardAlightTransferCosts() { // Not implemented, no test for this yet. - return stopBoardAlightCosts; + return stopBoardAlightTransferCosts; } private void expandNumOfStops(int stopIndex) { diff --git a/src/test/java/org/opentripplanner/raptor/moduletests/B05_EgressStopTransferCostTest.java b/src/test/java/org/opentripplanner/raptor/moduletests/B05_EgressStopBoardAlightTransferCostTest.java similarity index 86% rename from src/test/java/org/opentripplanner/raptor/moduletests/B05_EgressStopTransferCostTest.java rename to src/test/java/org/opentripplanner/raptor/moduletests/B05_EgressStopBoardAlightTransferCostTest.java index 21c1dd2a755..df38aabc483 100644 --- a/src/test/java/org/opentripplanner/raptor/moduletests/B05_EgressStopTransferCostTest.java +++ b/src/test/java/org/opentripplanner/raptor/moduletests/B05_EgressStopBoardAlightTransferCostTest.java @@ -22,10 +22,10 @@ /** * FEATURE UNDER TEST *

- * This verifies that the stopTransferCost is not applied for egress legs. If this is not correctly - * handled by the heuristics optimization, the cheapest journey could be discarded. + * This verifies that the stopBoardAlightTransferCost is not applied for egress legs. If this is not + * correctly handled by the heuristics optimization, the cheapest journey could be discarded. */ -public class B05_EgressStopTransferCostTest implements RaptorTestConstants { +public class B05_EgressStopBoardAlightTransferCostTest implements RaptorTestConstants { private final TestTransitData data = new TestTransitData(); private final RaptorRequestBuilder requestBuilder = new RaptorRequestBuilder<>(); @@ -40,7 +40,7 @@ void setup() { .withRoute(route("R2", STOP_C, STOP_D).withTimetable(schedule("0:18, 0:20"))); data.mcCostParamsBuilder().transferCost(0).boardCost(0); - data.withStopBoardAlightCost(STOP_D, 60000); + data.withStopBoardAlightTransferCost(STOP_D, 60000); requestBuilder .searchParams() @@ -61,7 +61,7 @@ static List testCases() { .add( multiCriteria(), // We should get both the fastest and the c1-cheapest results - // The stopTransferCost should not be applied to the egress leg from STOP_D + // The stopBoardAlightTransferCost should not be applied to the egress leg from STOP_D "B ~ BUS R1 0:10 0:14 ~ C ~ Walk 5m [0:10 0:19 9m Tₓ0 C₁840]", "B ~ BUS R1 0:10 0:14 ~ C ~ BUS R2 0:18 0:20 ~ D ~ Walk 20s [0:10 0:20:20 10m20s Tₓ1 C₁640]" ) diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapperTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapperTest.java index bebc11fd2c2..9296ee2730c 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapperTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapperTest.java @@ -42,8 +42,8 @@ class TransitLayerMapperTest { private final List STOPS = Arrays.asList(STOP_0, STOP_1, STOP_2, STOP_3, STOP_4); @Test - public void createStopTransferCosts() { - int[] result = TransitLayerMapper.createStopTransferCosts( + public void createStopBoardAlightTransferCosts() { + int[] result = TransitLayerMapper.createStopBoardAlightTransferCosts( new StopModelMock(STOPS), TransitTuningParameters.FOR_TEST ); diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTailTest.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTailTest.java index f9bdf0df273..8c82f68a8bc 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTailTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTailTest.java @@ -53,14 +53,14 @@ class OptimizedPathTailTest implements RaptorTestConstants { * Extra cost = (30.0 + 2 * 10.0) * 2.0 = $100.00 *

*/ - private final int[] stopBoardAlightCost = new int[] { 0, 0, 3000, 0, 1000, 0 }; + private final int[] stopBoardAlightTransferCosts = new int[] { 0, 0, 3000, 0, 1000, 0 }; private final OptimizedPathTail subject = new OptimizedPathTail<>( SLACK_PROVIDER, BasicPathTestCase.C1_CALCULATOR, 0, waitTimeCalc, - stopBoardAlightCost, + stopBoardAlightTransferCosts, 2.0, this::stopIndexToName ); diff --git a/src/test/java/org/opentripplanner/routing/api/request/framework/TimePenaltyTest.java b/src/test/java/org/opentripplanner/routing/api/request/framework/TimePenaltyTest.java index f5bffe1f415..087ffa1d637 100644 --- a/src/test/java/org/opentripplanner/routing/api/request/framework/TimePenaltyTest.java +++ b/src/test/java/org/opentripplanner/routing/api/request/framework/TimePenaltyTest.java @@ -64,4 +64,11 @@ void calculate() { var subject = TimePenalty.of(D2m, 0.5); assertEquals(120 + 150, subject.calculate(Duration.ofMinutes(5)).toSeconds()); } + + @Test + void modifies() { + var subject = TimePenalty.of(D2m, 0.5); + assertTrue(subject.modifies()); + assertFalse(TimePenalty.ZERO.modifies()); + } } diff --git a/src/test/java/org/opentripplanner/test/support/GeoJsonIo.java b/src/test/java/org/opentripplanner/test/support/GeoJsonIo.java new file mode 100644 index 00000000000..8e25407fca5 --- /dev/null +++ b/src/test/java/org/opentripplanner/test/support/GeoJsonIo.java @@ -0,0 +1,26 @@ +package org.opentripplanner.test.support; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import org.locationtech.jts.geom.Geometry; +import org.opentripplanner.framework.json.ObjectMappers; + +/** + * Helper class for generating URLs to geojson.io. + */ +public class GeoJsonIo { + + private static final ObjectMapper MAPPER = ObjectMappers.geoJson(); + + public static String toUrl(Geometry geometry) { + try { + var geoJson = MAPPER.writeValueAsString(geometry); + var encoded = URLEncoder.encode(geoJson, StandardCharsets.UTF_8); + return "http://geojson.io/#data=data:application/json,%s".formatted(encoded); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/org/opentripplanner/test/support/JsonAssertions.java b/src/test/java/org/opentripplanner/test/support/JsonAssertions.java index 2dab1e96190..1fe87268eee 100644 --- a/src/test/java/org/opentripplanner/test/support/JsonAssertions.java +++ b/src/test/java/org/opentripplanner/test/support/JsonAssertions.java @@ -27,8 +27,17 @@ public static void assertEqualJson(String expected, String actual) { */ public static void assertEqualJson(String expected, JsonNode actual) { try { + var actualNode = MAPPER.readTree(actual.toString()); var exp = MAPPER.readTree(expected); - assertEquals(JsonSupport.prettyPrint(exp), JsonSupport.prettyPrint(actual)); + assertEquals( + exp, + actualNode, + () -> + "Expected '%s' but actual was '%s'".formatted( + JsonSupport.prettyPrint(exp), + JsonSupport.prettyPrint(actualNode) + ) + ); } catch (JsonProcessingException e) { throw new RuntimeException(e); } diff --git a/src/test/java/org/opentripplanner/transit/model/timetable/TripTest.java b/src/test/java/org/opentripplanner/transit/model/timetable/TripTest.java index 098c1d0994a..3a3f35643fa 100644 --- a/src/test/java/org/opentripplanner/transit/model/timetable/TripTest.java +++ b/src/test/java/org/opentripplanner/transit/model/timetable/TripTest.java @@ -26,7 +26,6 @@ class TripTest { private static final BikeAccess BIKE_ACCESS = BikeAccess.ALLOWED; private static final TransitMode TRANSIT_MODE = TransitMode.BUS; private static final String BLOCK_ID = "blockId"; - private static final String FARE_ID = "fareId"; private static final TripAlteration TRIP_ALTERATION = TripAlteration.CANCELLATION; private static final String NETEX_SUBMODE_NAME = "submode"; private static final SubMode NETEX_SUBMODE = SubMode.of(NETEX_SUBMODE_NAME); @@ -46,7 +45,6 @@ class TripTest { .withBikesAllowed(BIKE_ACCESS) .withMode(TRANSIT_MODE) .withGtfsBlockId(BLOCK_ID) - .withGtfsFareId(FARE_ID) .withNetexAlteration(TRIP_ALTERATION) .withNetexSubmode(NETEX_SUBMODE_NAME) .withNetexInternalPlanningCode(NETEX_INTERNAL_PLANNING_CODE) @@ -88,7 +86,6 @@ void copy() { assertEquals(BIKE_ACCESS, copy.getBikesAllowed()); assertEquals(TRANSIT_MODE, copy.getMode()); assertEquals(BLOCK_ID, copy.getGtfsBlockId()); - assertEquals(FARE_ID, copy.getGtfsFareId()); assertEquals(TRIP_ALTERATION, copy.getNetexAlteration()); assertEquals(NETEX_SUBMODE, copy.getNetexSubMode()); assertEquals(NETEX_INTERNAL_PLANNING_CODE, copy.getNetexInternalPlanningCode()); @@ -115,7 +112,6 @@ void sameAs() { assertFalse(subject.sameAs(subject.copy().withBikesAllowed(BikeAccess.NOT_ALLOWED).build())); assertFalse(subject.sameAs(subject.copy().withMode(TransitMode.RAIL).build())); assertFalse(subject.sameAs(subject.copy().withGtfsBlockId("X").build())); - assertFalse(subject.sameAs(subject.copy().withGtfsFareId("X").build())); assertFalse( subject.sameAs(subject.copy().withNetexAlteration(TripAlteration.REPLACED).build()) ); diff --git a/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoaderTest.java b/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoaderTest.java index 5bb9a3a750c..9a0dbcdfe87 100644 --- a/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoaderTest.java +++ b/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoaderTest.java @@ -28,8 +28,9 @@ import org.entur.gbfs.v2_3.vehicle_types.GBFSVehicleTypes; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.updater.spi.HttpHeaders; +import org.slf4j.LoggerFactory; /** * This tests that {@link GbfsFeedLoader} handles loading of different versions of GBFS correctly, @@ -90,7 +91,10 @@ void getV10FeedWithExplicitLanguage() { @Test @Disabled void fetchAllPublicFeeds() { - try (OtpHttpClient otpHttpClient = new OtpHttpClient()) { + try (OtpHttpClientFactory otpHttpClientFactory = new OtpHttpClientFactory()) { + var otpHttpClient = otpHttpClientFactory.create( + LoggerFactory.getLogger(GbfsFeedLoaderTest.class) + ); List exceptions = otpHttpClient.getAndMap( URI.create("https://raw.githubusercontent.com/NABSA/gbfs/master/systems.csv"), Map.of(), diff --git a/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSourceTest.java b/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSourceTest.java index dba2e10510b..018a3d06e5b 100644 --- a/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSourceTest.java +++ b/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSourceTest.java @@ -10,7 +10,7 @@ import java.util.Map; import org.entur.gbfs.v2_3.vehicle_types.GBFSVehicleType; import org.junit.jupiter.api.Test; -import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.service.vehiclerental.model.GeofencingZone; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; @@ -34,7 +34,7 @@ void makeStationFromV22() { false, false ), - new OtpHttpClient() + new OtpHttpClientFactory() ); dataSource.setup(); @@ -125,7 +125,7 @@ void geofencing() { true, false ), - new OtpHttpClient() + new OtpHttpClientFactory() ); dataSource.setup(); @@ -167,7 +167,7 @@ void makeStationFromV10() { false, true ), - new OtpHttpClient() + new OtpHttpClientFactory() ); dataSource.setup(); diff --git a/src/test/resources/org/opentripplanner/apis/vectortiles/style.json b/src/test/resources/org/opentripplanner/apis/vectortiles/style.json index 7e611171409..c0e31a26f5d 100644 --- a/src/test/resources/org/opentripplanner/apis/vectortiles/style.json +++ b/src/test/resources/org/opentripplanner/apis/vectortiles/style.json @@ -63,6 +63,56 @@ "visibility" : "none" } }, + { + "id" : "edge-name", + "type" : "symbol", + "source" : "vectorSource", + "source-layer" : "edges", + "minzoom" : 17, + "maxzoom" : 23, + "paint" : { + "text-color" : "#000", + "text-halo-color" : "#fff", + "text-halo-blur" : 4, + "text-halo-width" : 3 + }, + "filter" : [ + "in", + "class", + "StreetEdge", + "AreaEdge", + "EscalatorEdge", + "PathwayEdge", + "ElevatorHopEdge", + "TemporaryPartialStreetEdge", + "TemporaryFreeEdge" + ], + "layout" : { + "symbol-placement" : "line", + "symbol-spacing" : 500, + "text-field" : "{name}", + "text-font" : [ + "KlokanTech Noto Sans Regular" + ], + "text-size" : { + "base" : 14.0, + "stops" : [ + [ + 14, + 12.0 + ], + [ + 20, + 14.0 + ] + ] + }, + "text-max-width" : 5, + "text-keep-upright" : true, + "text-rotation-alignment" : "map", + "visibility" : "none" + } + }, { "id" : "link", "type" : "line", @@ -194,5 +244,6 @@ } } ], + "glyphs" : "https://cdn.jsdelivr.net/gh/klokantech/klokantech-gl-fonts@master/{fontstack}/{range}.pbf", "version" : 8 -} +} \ No newline at end of file diff --git a/src/test/resources/standalone/config/router-config.json b/src/test/resources/standalone/config/router-config.json index 3b43ef1c5d2..cee86fafa2e 100644 --- a/src/test/resources/standalone/config/router-config.json +++ b/src/test/resources/standalone/config/router-config.json @@ -171,7 +171,7 @@ "minWindow": "1h", "maxWindow": "5h" }, - "stopTransferCost": { + "stopBoardAlightDuringTransferCost": { "DISCOURAGED": 1500, "ALLOWED": 75, "RECOMMENDED": 30, diff --git a/test/performance/norway/speed-test-config.json b/test/performance/norway/speed-test-config.json index e4ca99d7fe5..e8e33576769 100644 --- a/test/performance/norway/speed-test-config.json +++ b/test/performance/norway/speed-test-config.json @@ -31,7 +31,7 @@ // stepMinutes: 10 // stepMinutes: 10 }, - "stopTransferCost": { + "stopBoardAlightDuringTransferCost": { "DISCOURAGED": 86400, "ALLOWED": 3000, "RECOMMENDED": 300, diff --git a/test/performance/skanetrafiken/speed-test-config.json b/test/performance/skanetrafiken/speed-test-config.json index 66184ab124f..69a12755336 100644 --- a/test/performance/skanetrafiken/speed-test-config.json +++ b/test/performance/skanetrafiken/speed-test-config.json @@ -7,7 +7,7 @@ "dynamicSearchWindow": { "maxWindow": "24h" }, - "stopTransferCost": { + "stopBoardAlightDuringTransferCost": { "DISCOURAGED": 3000, "ALLOWED": 150, "RECOMMENDED": 60, diff --git a/test/performance/switzerland/speed-test-config.json b/test/performance/switzerland/speed-test-config.json index 19ac7a1358a..7c2e692d2cb 100644 --- a/test/performance/switzerland/speed-test-config.json +++ b/test/performance/switzerland/speed-test-config.json @@ -31,7 +31,7 @@ // stepMinutes: 10 // stepMinutes: 10 }, - "stopTransferCost": { + "stopBoardAlightDuringTransferCost": { "DISCOURAGED": 86400, "ALLOWED": 3000, "RECOMMENDED": 300,