diff --git a/client/.env.development b/client/.env.development index 1cb7d9235e3..e3b3585a5eb 100644 --- a/client/.env.development +++ b/client/.env.development @@ -1,3 +1,3 @@ VITE_API_URL=http://localhost:8080/otp/transmodel/v3 VITE_DEBUG_STYLE_URL=http://localhost:8080/otp/routers/default/inspector/vectortile/style.json -VITE_GRAPHIQL_URL=http://localhost:8080/graphiql?flavor=transmodel \ No newline at end of file +VITE_GRAPHIQL_URL=http://localhost:8080/graphiql?flavor=transmodel diff --git a/client/package-lock.json b/client/package-lock.json index 4632899b38f..00490ce16d6 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,10 +9,11 @@ "version": "0.0.0", "dependencies": { "@googlemaps/polyline-codec": "1.0.28", + "@js-temporal/polyfill": "0.4.4", "bootstrap": "5.3.3", "graphql": "16.9.0", "graphql-request": "7.1.0", - "maplibre-gl": "4.6.0", + "maplibre-gl": "4.7.0", "react": "18.3.1", "react-bootstrap": "2.10.4", "react-dom": "18.3.1", @@ -24,24 +25,24 @@ "@graphql-codegen/introspection": "4.0.3", "@parcel/watcher": "2.4.1", "@testing-library/react": "16.0.1", - "@types/react": "18.3.5", + "@types/react": "18.3.7", "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "7.18.0", "@typescript-eslint/parser": "7.18.0", "@vitejs/plugin-react": "4.3.1", - "@vitest/coverage-v8": "2.0.5", - "eslint": "8.57.0", + "@vitest/coverage-v8": "2.1.1", + "eslint": "8.57.1", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.30.0", - "eslint-plugin-jsx-a11y": "6.9.0", - "eslint-plugin-react": "7.35.1", + "eslint-plugin-jsx-a11y": "6.10.0", + "eslint-plugin-react": "7.36.1", "eslint-plugin-react-hooks": "4.6.2", - "eslint-plugin-react-refresh": "0.4.11", + "eslint-plugin-react-refresh": "0.4.12", "jsdom": "25.0.0", "prettier": "3.3.3", - "typescript": "5.5.4", - "vite": "5.4.2", - "vitest": "2.0.5" + "typescript": "5.6.2", + "vite": "5.4.6", + "vitest": "2.1.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1775,10 +1776,11 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -2745,12 +2747,14 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -2794,10 +2798,12 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -2937,10 +2943,11 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -2952,6 +2959,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-temporal/polyfill": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.4.4.tgz", + "integrity": "sha512-2X6bvghJ/JAoZO52lbgyAPFj8uCflhTo2g7nkFzEQdXd/D8rEeD4HtmTEpmtGCva260fcd66YNXBOYdnmHqSOg==", + "dependencies": { + "jsbi": "^4.3.0", + "tslib": "^2.4.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@kamilkisiela/fast-url-parser": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@kamilkisiela/fast-url-parser/-/fast-url-parser-1.1.4.tgz", @@ -3951,9 +3970,9 @@ "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/react": { - "version": "18.3.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", - "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.7.tgz", + "integrity": "sha512-KUnDCJF5+AiZd8owLIeVHqmW9yM4sqmDVf2JRJiBMFkGvkoZ4/WyV2lL4zVsoinmRS/W3FeEdZLEWFRofnT2FQ==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -4232,20 +4251,20 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", - "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz", + "integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.5", + "debug": "^4.3.6", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.10", + "magic-string": "^0.30.11", "magicast": "^0.3.4", "std-env": "^3.7.0", "test-exclude": "^7.0.1", @@ -4255,18 +4274,24 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "2.0.5" + "@vitest/browser": "2.1.1", + "vitest": "2.1.1" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, "node_modules/@vitest/expect": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", - "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", + "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.0.5", - "@vitest/utils": "2.0.5", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", "chai": "^5.1.1", "tinyrainbow": "^1.2.0" }, @@ -4274,10 +4299,38 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/mocker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", + "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "^2.1.0-beta.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/spy": "2.1.1", + "msw": "^2.3.5", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "node_modules/@vitest/pretty-format": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", - "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz", + "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4288,13 +4341,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", - "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", + "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.0.5", + "@vitest/utils": "2.1.1", "pathe": "^1.1.2" }, "funding": { @@ -4302,14 +4355,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", - "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", + "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.0.5", - "magic-string": "^0.30.10", + "@vitest/pretty-format": "2.1.1", + "magic-string": "^0.30.11", "pathe": "^1.1.2" }, "funding": { @@ -4317,9 +4370,9 @@ } }, "node_modules/@vitest/spy": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", - "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", + "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", "dev": true, "license": "MIT", "dependencies": { @@ -4330,14 +4383,13 @@ } }, "node_modules/@vitest/utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", - "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", + "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.0.5", - "estree-walker": "^3.0.3", + "@vitest/pretty-format": "2.1.1", "loupe": "^3.1.1", "tinyrainbow": "^1.2.0" }, @@ -4761,15 +4813,25 @@ } }, "node_modules/axe-core": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.9.1.tgz", - "integrity": "sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.0.tgz", + "integrity": "sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==", "dev": true, "license": "MPL-2.0", "engines": { "node": ">=4" } }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/babel-plugin-syntax-trailing-function-commas": { "version": "7.0.0-beta.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz", @@ -5491,13 +5553,13 @@ "dev": true }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -6045,16 +6107,17 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -6239,9 +6302,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz", - "integrity": "sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz", + "integrity": "sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==", "dev": true, "license": "MIT", "dependencies": { @@ -6249,8 +6312,8 @@ "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", - "axe-core": "^4.9.1", - "axobject-query": "~3.1.1", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "es-iterator-helpers": "^1.0.19", @@ -6266,7 +6329,7 @@ "node": ">=4.0" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { @@ -6279,16 +6342,6 @@ "deep-equal": "^2.0.5" } }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/axobject-query": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", - "integrity": "sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6314,9 +6367,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.35.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.1.tgz", - "integrity": "sha512-B5ok2JgbaaWn/zXbKCGgKDNL2tsID3Pd/c/yvjcpsd9HQDwyYc/TQv3AZMmOvrJgCs3AnYNUHRCQEMMQAYJ7Yg==", + "version": "7.36.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.36.1.tgz", + "integrity": "sha512-/qwbqNXZoq+VP30s1d4Nc1C5GTxjJQjk4Jzs4Wq2qzxFM7dSmuG2UkIjg2USMLh3A/aVcUNrK7v0J5U1XEGGwA==", "dev": true, "license": "MIT", "dependencies": { @@ -6359,9 +6412,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.11.tgz", - "integrity": "sha512-wrAKxMbVr8qhXTtIKfXqAn5SAtRZt0aXxe5P23Fh4pUAdC6XEsybGLB8P0PI4j1yYqOgUEUlzKAGDfo7rJOjcw==", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.12.tgz", + "integrity": "sha512-9neVjoGv20FwYtCP6CB1dzR1vr57ZDNOXst21wd2xJ/cTlM2xLq0GWVlSNTdMn/4BtP6cHYBMCSp1wFBJ9jBsg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -6456,6 +6509,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6466,6 +6520,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -6481,6 +6536,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6493,6 +6549,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -6569,80 +6626,6 @@ "node": ">=0.10.0" } }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/execa/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/execa/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", @@ -7486,15 +7469,6 @@ "node": ">= 14" } }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -8037,18 +8011,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -8320,6 +8282,11 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz", + "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==" + }, "node_modules/jsdom": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.0.tgz", @@ -8740,13 +8707,13 @@ } }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/magicast": { @@ -8819,9 +8786,9 @@ } }, "node_modules/maplibre-gl": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.6.0.tgz", - "integrity": "sha512-zobZK+fE+XM+7K81fk5pSBYWZlTGjGT0P96y2fR4DV2ry35ZBfAd0uWNatll69EgYeE+uOhN1MvEk+z1PCuyOQ==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.0.tgz", + "integrity": "sha512-hkt7je7NxiMQE8EpCxLWP8t6tkK6SkrMe0hIBjYd4Ar/Q7BOCILxthGmGnU993Mwmkvs2mGiXnVUSOK12DeCzg==", "license": "BSD-3-Clause", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", @@ -8865,12 +8832,6 @@ "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", "license": "ISC" }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -8975,10 +8936,11 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/murmurhash-js": { "version": "1.0.0", @@ -9100,33 +9062,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -9601,9 +9536,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true, "license": "ISC" }, @@ -9629,9 +9564,9 @@ } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -9650,8 +9585,8 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -10547,10 +10482,11 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -10864,18 +10800,6 @@ "node": ">=4" } }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -10984,9 +10908,16 @@ "dev": true }, "node_modules/tinybench": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", - "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", "dev": true, "license": "MIT" }, @@ -11017,9 +10948,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", - "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "dev": true, "license": "MIT", "engines": { @@ -11265,9 +11196,9 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -11491,14 +11422,14 @@ } }, "node_modules/vite": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", - "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.41", + "postcss": "^8.4.43", "rollup": "^4.20.0" }, "bin": { @@ -11551,16 +11482,15 @@ } }, "node_modules/vite-node": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", - "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", + "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.5", + "debug": "^4.3.6", "pathe": "^1.1.2", - "tinyrainbow": "^1.2.0", "vite": "^5.0.0" }, "bin": { @@ -11574,30 +11504,30 @@ } }, "node_modules/vitest": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", - "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", + "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@vitest/expect": "2.0.5", - "@vitest/pretty-format": "^2.0.5", - "@vitest/runner": "2.0.5", - "@vitest/snapshot": "2.0.5", - "@vitest/spy": "2.0.5", - "@vitest/utils": "2.0.5", + "@vitest/expect": "2.1.1", + "@vitest/mocker": "2.1.1", + "@vitest/pretty-format": "^2.1.1", + "@vitest/runner": "2.1.1", + "@vitest/snapshot": "2.1.1", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", "chai": "^5.1.1", - "debug": "^4.3.5", - "execa": "^8.0.1", - "magic-string": "^0.30.10", + "debug": "^4.3.6", + "magic-string": "^0.30.11", "pathe": "^1.1.2", "std-env": "^3.7.0", - "tinybench": "^2.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.0", "tinypool": "^1.0.0", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.0.5", + "vite-node": "2.1.1", "why-is-node-running": "^2.3.0" }, "bin": { @@ -11612,8 +11542,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.0.5", - "@vitest/ui": "2.0.5", + "@vitest/browser": "2.1.1", + "@vitest/ui": "2.1.1", "happy-dom": "*", "jsdom": "*" }, diff --git a/client/package.json b/client/package.json index 5b26215fe5f..0e822b4a641 100644 --- a/client/package.json +++ b/client/package.json @@ -18,10 +18,11 @@ }, "dependencies": { "@googlemaps/polyline-codec": "1.0.28", + "@js-temporal/polyfill": "0.4.4", "bootstrap": "5.3.3", "graphql": "16.9.0", "graphql-request": "7.1.0", - "maplibre-gl": "4.6.0", + "maplibre-gl": "4.7.0", "react": "18.3.1", "react-bootstrap": "2.10.4", "react-dom": "18.3.1", @@ -33,23 +34,23 @@ "@graphql-codegen/introspection": "4.0.3", "@parcel/watcher": "2.4.1", "@testing-library/react": "16.0.1", - "@types/react": "18.3.5", + "@types/react": "18.3.7", "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "7.18.0", "@typescript-eslint/parser": "7.18.0", "@vitejs/plugin-react": "4.3.1", - "@vitest/coverage-v8": "2.0.5", - "eslint": "8.57.0", + "@vitest/coverage-v8": "2.1.1", + "eslint": "8.57.1", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.30.0", - "eslint-plugin-jsx-a11y": "6.9.0", - "eslint-plugin-react": "7.35.1", + "eslint-plugin-jsx-a11y": "6.10.0", + "eslint-plugin-react": "7.36.1", "eslint-plugin-react-hooks": "4.6.2", - "eslint-plugin-react-refresh": "0.4.11", + "eslint-plugin-react-refresh": "0.4.12", "jsdom": "25.0.0", "prettier": "3.3.3", - "typescript": "5.5.4", - "vite": "5.4.2", - "vitest": "2.0.5" + "typescript": "5.6.2", + "vite": "5.4.6", + "vitest": "2.1.1" } } diff --git a/client/src/components/ItineraryList/ItineraryHeaderContent.tsx b/client/src/components/ItineraryList/ItineraryHeaderContent.tsx index 419b7a2ebb9..fdfea81e7e4 100644 --- a/client/src/components/ItineraryList/ItineraryHeaderContent.tsx +++ b/client/src/components/ItineraryList/ItineraryHeaderContent.tsx @@ -1,8 +1,9 @@ import { TripPattern } from '../../gql/graphql.ts'; import { TIME_BOX_WIDTH, useHeaderContentStyleCalculations } from './useHeaderContentStyleCalculations.ts'; import { ItineraryHeaderLegContent } from './ItineraryHeaderLegContent.tsx'; -import { useMemo } from 'react'; +import { useContext, useMemo } from 'react'; import { formatTime } from '../../util/formatTime.ts'; +import { TimeZoneContext } from '../../hooks/TimeZoneContext.ts'; export function ItineraryHeaderContent({ tripPattern, @@ -24,14 +25,16 @@ export function ItineraryHeaderContent({ latestEndTime, ); + const timeZone = useContext(TimeZoneContext); + const formattedStartTime = useMemo( - () => formatTime(tripPattern.expectedStartTime, 'short'), - [tripPattern.expectedStartTime], + () => formatTime(tripPattern.expectedStartTime, timeZone, 'short'), + [tripPattern.expectedStartTime, timeZone], ); const formattedEndTime = useMemo( - () => formatTime(tripPattern.expectedEndTime, 'short'), - [tripPattern.expectedEndTime], + () => formatTime(tripPattern.expectedEndTime, timeZone, 'short'), + [tripPattern.expectedEndTime, timeZone], ); return ( @@ -45,6 +48,7 @@ export function ItineraryHeaderContent({ }} />
@@ -56,6 +59,9 @@ export function ItineraryListContainer({ ))} +
+ All times in {timeZone} +
); } diff --git a/client/src/components/ItineraryList/ItineraryPaginationControl.tsx b/client/src/components/ItineraryList/ItineraryPaginationControl.tsx index ecc1ffd45db..bf74c83fbca 100644 --- a/client/src/components/ItineraryList/ItineraryPaginationControl.tsx +++ b/client/src/components/ItineraryList/ItineraryPaginationControl.tsx @@ -14,6 +14,8 @@ export function ItineraryPaginationControl({ return (
{' '} -
+ ); } + export default GraphiQLRouteButton; diff --git a/client/src/components/SearchBar/ItineraryFilterDebugSelect.tsx b/client/src/components/SearchBar/ItineraryFilterDebugSelect.tsx index 636ba551541..6f479290947 100644 --- a/client/src/components/SearchBar/ItineraryFilterDebugSelect.tsx +++ b/client/src/components/SearchBar/ItineraryFilterDebugSelect.tsx @@ -11,11 +11,12 @@ export function ItineraryFilterDebugSelect({ return ( - Itinerary filter debug + Filter debug { setTripQueryVariables({ ...tripQueryVariables, diff --git a/client/src/components/SearchBar/LocationInputField.tsx b/client/src/components/SearchBar/LocationInputField.tsx index ffa66702e81..bfa707776f1 100644 --- a/client/src/components/SearchBar/LocationInputField.tsx +++ b/client/src/components/SearchBar/LocationInputField.tsx @@ -13,6 +13,7 @@ export function LocationInputField({ location, id, label }: { location: Location id={id} size="sm" placeholder="[Click in map]" + className="input-medium" // Intentionally empty for now, but needed because of // https://react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable onChange={() => {}} diff --git a/client/src/components/SearchBar/NumTripPatternsInput.tsx b/client/src/components/SearchBar/NumTripPatternsInput.tsx index b77e70adb81..360ce1c2c73 100644 --- a/client/src/components/SearchBar/NumTripPatternsInput.tsx +++ b/client/src/components/SearchBar/NumTripPatternsInput.tsx @@ -11,7 +11,7 @@ export function NumTripPatternsInput({ return ( - Number of trip patterns + Num. results setTripQueryVariables({ diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index dfcbc6ac36e..7b1ee58b902 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -1,9 +1,8 @@ -import { Button, Spinner } from 'react-bootstrap'; +import { Button, ButtonGroup, Spinner } from 'react-bootstrap'; import { ServerInfo, TripQueryVariables } from '../../gql/graphql.ts'; import { LocationInputField } from './LocationInputField.tsx'; import { DepartureArrivalSelect } from './DepartureArrivalSelect.tsx'; -import { TimeInputField } from './TimeInputField.tsx'; -import { DateInputField } from './DateInputField.tsx'; +import { DateTimeInputField } from './DateTimeInputField.tsx'; import { SearchWindowInput } from './SearchWindowInput.tsx'; import { AccessSelect } from './AccessSelect.tsx'; import { EgressSelect } from './EgressSelect.tsx'; @@ -16,6 +15,7 @@ import { ServerInfoTooltip } from './ServerInfoTooltip.tsx'; import { useRef, useState } from 'react'; import logo from '../../static/img/otp-logo.svg'; import GraphiQLRouteButton from './GraphiQLRouteButton.tsx'; +import WheelchairAccessibleCheckBox from './WheelchairAccessibleCheckBox.tsx'; type SearchBarProps = { onRoute: () => void; @@ -40,8 +40,7 @@ export function SearchBar({ onRoute, tripQueryVariables, setTripQueryVariables, - - + @@ -52,17 +51,24 @@ export function SearchBar({ onRoute, tripQueryVariables, setTripQueryVariables, tripQueryVariables={tripQueryVariables} setTripQueryVariables={setTripQueryVariables} /> + +
- + + + +
-
); } diff --git a/client/src/components/SearchBar/SearchWindowInput.tsx b/client/src/components/SearchBar/SearchWindowInput.tsx index 5442784de8e..a04a08bed04 100644 --- a/client/src/components/SearchBar/SearchWindowInput.tsx +++ b/client/src/components/SearchBar/SearchWindowInput.tsx @@ -19,6 +19,7 @@ export function SearchWindowInput({ size="sm" placeholder="(in minutes)" min={1} + className="input-small" value={tripQueryVariables.searchWindow || ''} onChange={(event) => setTripQueryVariables({ diff --git a/client/src/components/SearchBar/TimeInputField.tsx b/client/src/components/SearchBar/TimeInputField.tsx deleted file mode 100644 index 71bb7325340..00000000000 --- a/client/src/components/SearchBar/TimeInputField.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Form } from 'react-bootstrap'; -import { TripQueryVariables } from '../../gql/graphql.ts'; -import { ChangeEvent, useCallback, useMemo } from 'react'; - -export function TimeInputField({ - tripQueryVariables, - setTripQueryVariables, -}: { - tripQueryVariables: TripQueryVariables; - setTripQueryVariables: (tripQueryVariables: TripQueryVariables) => void; -}) { - const current = useMemo( - () => new Date(tripQueryVariables.dateTime).toTimeString().split(' ')[0], - [tripQueryVariables.dateTime], - ); - - const onChange = useCallback( - (event: ChangeEvent) => { - const timeComponents = event.target.value.split(':'); - const newDate = new Date(tripQueryVariables.dateTime); - newDate.setHours(Number(timeComponents[0]), Number(timeComponents[1]), Number(timeComponents[2])); - - setTripQueryVariables({ - ...tripQueryVariables, - dateTime: newDate.toISOString(), - }); - }, - [tripQueryVariables, setTripQueryVariables], - ); - - return ( - - - Time - - - - ); -} diff --git a/client/src/components/SearchBar/WheelchairAccessibleCheckBox.tsx b/client/src/components/SearchBar/WheelchairAccessibleCheckBox.tsx new file mode 100644 index 00000000000..b677e19049f --- /dev/null +++ b/client/src/components/SearchBar/WheelchairAccessibleCheckBox.tsx @@ -0,0 +1,35 @@ +import { Form } from 'react-bootstrap'; +import wheelchairIcon from '../../static/img/wheelchair.svg'; +import { TripQueryVariables } from '../../gql/graphql.ts'; + +export default function WheelchairAccessibleCheckBox({ + tripQueryVariables, + setTripQueryVariables, +}: { + tripQueryVariables: TripQueryVariables; + setTripQueryVariables: (tripQueryVariables: TripQueryVariables) => void; +}) { + return ( + + + Wheelchair Accessible Trip + + { + setTripQueryVariables({ + ...tripQueryVariables, + wheelchairAccessible: e.target.checked, + }); + }} + > + + ); +} diff --git a/client/src/hooks/TimeZoneContext.ts b/client/src/hooks/TimeZoneContext.ts new file mode 100644 index 00000000000..6a40921ebae --- /dev/null +++ b/client/src/hooks/TimeZoneContext.ts @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export const TimeZoneContext = createContext('UTC'); diff --git a/client/src/hooks/useServerInfo.ts b/client/src/hooks/useServerInfo.ts index 23ee23fc283..117c1357360 100644 --- a/client/src/hooks/useServerInfo.ts +++ b/client/src/hooks/useServerInfo.ts @@ -13,6 +13,7 @@ const query = graphql(` routerConfigVersion gitCommit gitBranch + internalTransitModelTimeZone } } `); diff --git a/client/src/screens/App.tsx b/client/src/screens/App.tsx index 3e5744e5ad6..1b6b86b7a81 100644 --- a/client/src/screens/App.tsx +++ b/client/src/screens/App.tsx @@ -6,39 +6,43 @@ import { useState } from 'react'; import { useTripQuery } from '../hooks/useTripQuery.ts'; import { useServerInfo } from '../hooks/useServerInfo.ts'; import { useTripQueryVariables } from '../hooks/useTripQueryVariables.ts'; +import { TimeZoneContext } from '../hooks/TimeZoneContext.ts'; export function App() { + const serverInfo = useServerInfo(); const { tripQueryVariables, setTripQueryVariables } = useTripQueryVariables(); const [tripQueryResult, loading, callback] = useTripQuery(tripQueryVariables); - const serverInfo = useServerInfo(); const [selectedTripPatternIndex, setSelectedTripPatternIndex] = useState(0); + const timeZone = serverInfo?.internalTransitModelTimeZone || Intl.DateTimeFormat().resolvedOptions().timeZone; return (
- - - - - + - + + + + + +
); } diff --git a/client/src/static/img/graphql-solid.svg b/client/src/static/img/graphql-solid.svg new file mode 100644 index 00000000000..32d6e5e0f00 --- /dev/null +++ b/client/src/static/img/graphql-solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/static/img/graphql.svg b/client/src/static/img/graphql.svg new file mode 100644 index 00000000000..ef85915ffaa --- /dev/null +++ b/client/src/static/img/graphql.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/static/img/wheelchair.svg b/client/src/static/img/wheelchair.svg new file mode 100644 index 00000000000..d8cf96ca995 --- /dev/null +++ b/client/src/static/img/wheelchair.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/src/static/query/tripQuery.tsx b/client/src/static/query/tripQuery.tsx index ccb19dc745e..57cca5d4056 100644 --- a/client/src/static/query/tripQuery.tsx +++ b/client/src/static/query/tripQuery.tsx @@ -11,6 +11,7 @@ export const query = graphql(` $searchWindow: Int $modes: Modes $itineraryFiltersDebug: ItineraryFilterDebugProfile + $wheelchairAccessible: Boolean $pageCursor: String ) { trip( @@ -22,6 +23,7 @@ export const query = graphql(` searchWindow: $searchWindow modes: $modes itineraryFilters: { debug: $itineraryFiltersDebug } + wheelchairAccessible: $wheelchairAccessible pageCursor: $pageCursor ) { previousPageCursor @@ -64,6 +66,9 @@ export const query = graphql(` publicCode name id + presentation { + colour + } } authority { name diff --git a/client/src/style.css b/client/src/style.css index 1a24ac2c072..86310fd857d 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -7,7 +7,7 @@ margin-right: 14px; } -@media (min-width: 2160px) { +@media (min-width: 1895px) { .top-content { height: 75px; } @@ -17,7 +17,7 @@ } } -@media (max-width: 2159px) { +@media (max-width: 1896px) { .top-content { height: 150px; } @@ -50,16 +50,34 @@ margin-right: 1rem; } +.search-bar input.input-small { + max-width: 100px; +} + +.search-bar input.input-medium { + max-width: 130px; +} + .search-bar-route-button-wrapper { height: 5rem; padding-top: 25px; } +.search-bar-route-button-wrapper a.btn img { + margin-top: -2px; +} + .itinerary-list-container { width: 36rem; overflow-y: auto; } +.itinerary-list-container .time-zone-info { + margin: 10px 20px; + font-size: 12px; + text-align: right; +} + .itinerary-header-wrapper { position: relative; background: #0a53be; diff --git a/client/src/util/formatTime.ts b/client/src/util/formatTime.ts index 1849640fe3f..1818ced5cd1 100644 --- a/client/src/util/formatTime.ts +++ b/client/src/util/formatTime.ts @@ -4,10 +4,11 @@ * If style argument is provided formatted with ('medium') or without ('short') seconds, * otherwise seconds are shown if not 0. */ -export function formatTime(dateTime: string, style?: 'short' | 'medium') { +export function formatTime(dateTime: string, timeZone: string, style?: 'short' | 'medium') { const parsed = new Date(dateTime); return parsed.toLocaleTimeString('en-US', { timeStyle: style ? style : parsed.getSeconds() === 0 ? 'short' : 'medium', hourCycle: 'h24', + timeZone: timeZone, }); } diff --git a/client/src/util/getColorForMode.ts b/client/src/util/getColorForLeg.ts similarity index 64% rename from client/src/util/getColorForMode.ts rename to client/src/util/getColorForLeg.ts index cb1ad8b6981..ecb8cbc1676 100644 --- a/client/src/util/getColorForMode.ts +++ b/client/src/util/getColorForLeg.ts @@ -1,6 +1,6 @@ -import { Mode } from '../gql/graphql.ts'; +import { Leg, Mode } from '../gql/graphql.ts'; -export const getColorForMode = function (mode: Mode) { +const getColorForMode = function (mode: Mode) { if (mode === Mode.Foot) return '#191616'; if (mode === Mode.Bicycle) return '#5076D9'; if (mode === Mode.Scooter) return '#253664'; @@ -19,3 +19,14 @@ export const getColorForMode = function (mode: Mode) { if (mode === Mode.Taxi) return '#81304C'; return '#aaa'; }; + +/** + * Extract a line color from a leg. If there isn't one given by its line, this method returns a fallback color. + */ +export const getColorForLeg = function (leg: Leg) { + if (leg.line?.presentation?.colour) { + return `#${leg.line.presentation.colour}`; + } else { + return getColorForMode(leg.mode); + } +}; diff --git a/doc/templates/BuildConfiguration.md b/doc/templates/BuildConfiguration.md index 7e768e30eb1..77b03dae500 100644 --- a/doc/templates/BuildConfiguration.md +++ b/doc/templates/BuildConfiguration.md @@ -2,7 +2,7 @@ NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc. - Template directory is: /doc/templates - - Generated directory is: /docs + - Generated directory is: /doc/user --> # Graph Build Configuration diff --git a/doc/templates/Configuration.md b/doc/templates/Configuration.md index 6f6f4fc9960..45b2c36c67b 100644 --- a/doc/templates/Configuration.md +++ b/doc/templates/Configuration.md @@ -2,7 +2,7 @@ NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc. - Template directory is: /doc/templates - - Generated directory is: /docs + - Generated directory is: /doc/user --> # Configuring OpenTripPlanner diff --git a/doc/templates/GraphQL-Tutorial.md b/doc/templates/GraphQL-Tutorial.md index 11a2e304119..2a78be65cc2 100644 --- a/doc/templates/GraphQL-Tutorial.md +++ b/doc/templates/GraphQL-Tutorial.md @@ -2,7 +2,7 @@ NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc. - Template directory is: /doc/templates - - Generated directory is: /docs + - Generated directory is: /doc/user --> # GraphQL tutorial diff --git a/doc/templates/RouteRequest.md b/doc/templates/RouteRequest.md index a452e1d1480..9b7cd6fd58f 100644 --- a/doc/templates/RouteRequest.md +++ b/doc/templates/RouteRequest.md @@ -2,7 +2,7 @@ NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc. - Template directory is: /doc/templates - - Generated directory is: /docs + - Generated directory is: /doc/user --> # Route Request diff --git a/doc/templates/RouterConfiguration.md b/doc/templates/RouterConfiguration.md index b6c6ccf9c4b..87e4c1693cc 100644 --- a/doc/templates/RouterConfiguration.md +++ b/doc/templates/RouterConfiguration.md @@ -2,7 +2,7 @@ NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc. - Template directory is: /doc/templates - - Generated directory is: /docs + - Generated directory is: /doc/user --> # Router configuration diff --git a/doc/templates/StopConsolidation.md b/doc/templates/StopConsolidation.md index 6817ee47d4c..70866882bd1 100644 --- a/doc/templates/StopConsolidation.md +++ b/doc/templates/StopConsolidation.md @@ -2,7 +2,7 @@ NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc. - Template directory is: /doc/templates - - Generated directory is: /docs + - Generated directory is: /doc/user --> # Stop consolidation diff --git a/doc/templates/UpdaterConfig.md b/doc/templates/UpdaterConfig.md index 440cd96f733..aab5631e6e2 100644 --- a/doc/templates/UpdaterConfig.md +++ b/doc/templates/UpdaterConfig.md @@ -2,7 +2,7 @@ NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc. - Template directory is: /doc/templates - - Generated directory is: /docs + - Generated directory is: /doc/user --> diff --git a/doc/user/BuildConfiguration.md b/doc/user/BuildConfiguration.md index b311991120e..0bf4a3c29ec 100644 --- a/doc/user/BuildConfiguration.md +++ b/doc/user/BuildConfiguration.md @@ -2,7 +2,7 @@ NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc. - Template directory is: /doc/templates - - Generated directory is: /docs + - Generated directory is: /doc/user --> # Graph Build Configuration diff --git a/doc/user/Changelog.md b/doc/user/Changelog.md index 2bdee72c6e0..800cdd1bb69 100644 --- a/doc/user/Changelog.md +++ b/doc/user/Changelog.md @@ -3,6 +3,15 @@ The changelog lists most feature changes between each release. The list is automatically created based on merged pull requests. Search GitHub issues and pull requests for smaller issues. +## 2.7.0-SNAPSHOT (under development) + +- Extra leg when transferring at the same stop [#5984](https://github.com/opentripplanner/OpenTripPlanner/pull/5984) +- Filter vector tiles stops by current service week [#6003](https://github.com/opentripplanner/OpenTripPlanner/pull/6003) +- Add a matcher API for filters in the transit service used for datedServiceJourneyQuery [#5713](https://github.com/opentripplanner/OpenTripPlanner/pull/5713) +- Refetch transit leg with a leg query of GTFS GraphQL API [#6045](https://github.com/opentripplanner/OpenTripPlanner/pull/6045) +- Remove deprecated support for GTFS flex stop areas [#6074](https://github.com/opentripplanner/OpenTripPlanner/pull/6074) +[](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) + ## 2.6.0 (2024-09-18) ### Notable Changes diff --git a/doc/user/Configuration.md b/doc/user/Configuration.md index 832784d977b..bca974f8617 100644 --- a/doc/user/Configuration.md +++ b/doc/user/Configuration.md @@ -2,7 +2,7 @@ NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc. - Template directory is: /doc/templates - - Generated directory is: /docs + - Generated directory is: /doc/user --> # Configuring OpenTripPlanner @@ -226,6 +226,7 @@ Here is a list of all features which can be toggled on/off and their default val | `APIUpdaterStatus` | Enable endpoint for graph updaters status. | ✓️ | | | `ConsiderPatternsForDirectTransfers` | Enable limiting transfers so that there is only a single transfer to each pattern. | ✓️ | | | `DebugUi` | Enable the debug GraphQL client and web UI and located at the root of the web server as well as the debug map tiles it uses. Be aware that the map tiles are not a stable API and can change without notice. Use the [vector tiles feature if](sandbox/MapboxVectorTilesApi.md) you want a stable map tiles API. | ✓️ | | +| `ExtraTransferLegOnSameStop` | Should there be a transfer leg when transferring on the very same stop. Note that for in-seat/interlined transfers no transfer leg will be generated. | | | | `FloatingBike` | Enable floating bike routing. | ✓️ | | | `GtfsGraphQlApi` | Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md). | ✓️ | | | `GtfsGraphQlApiRentalStationFuzzyMatching` | Does vehicleRentalStation query also allow ids that are not feed scoped. | | | diff --git a/doc/user/Developers-Guide.md b/doc/user/Developers-Guide.md index 368a12edb62..ee0fe1eab9a 100644 --- a/doc/user/Developers-Guide.md +++ b/doc/user/Developers-Guide.md @@ -56,7 +56,7 @@ There are several ways to get involved: * Join the [Gitter chat room](https://gitter.im/opentripplanner/OpenTripPlanner) and the [user mailing list](http://groups.google.com/group/opentripplanner-users). -* Fix typos and improve the documentation within the `/docs` directory of the project (details +* Fix typos and improve the documentation within the `/doc/user` directory of the project (details below). * [File a bug or new feature request](http://github.com/openplans/OpenTripPlanner/issues/new). @@ -133,7 +133,7 @@ control to be applied to documentation as well as program source code. All pull how OTP is used or configured should include changes to the documentation alongside code modifications. -The documentation files are in Markdown format and are in the `/docs` directory under the root of +The documentation files are in Markdown format and are in the `/doc/user` directory under the root of the project. On every push to the `dev-2.x` branch the documentation will be rebuilt and deployed as static pages to our subdomain of [Github Pages](https://github.com/opentripplanner/docs). MkDocs is a Python program and should run on any major platform. @@ -143,7 +143,7 @@ how to generate a live local preview of the documentation while you're writing i In short: ``` -$ pip install -r docs/requirements.txt +$ pip install -r doc/user/requirements.txt $ mkdocs serve ``` diff --git a/doc/user/RouteRequest.md b/doc/user/RouteRequest.md index 674ab238888..c00502b2726 100644 --- a/doc/user/RouteRequest.md +++ b/doc/user/RouteRequest.md @@ -2,7 +2,7 @@ NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc. - Template directory is: /doc/templates - - Generated directory is: /docs + - Generated directory is: /doc/user --> # Route Request diff --git a/doc/user/RouterConfiguration.md b/doc/user/RouterConfiguration.md index 4e565cfe17d..2c42bea6d74 100644 --- a/doc/user/RouterConfiguration.md +++ b/doc/user/RouterConfiguration.md @@ -2,7 +2,7 @@ NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc. - Template directory is: /doc/templates - - Generated directory is: /docs + - Generated directory is: /doc/user --> # Router configuration diff --git a/doc/user/UpdaterConfig.md b/doc/user/UpdaterConfig.md index f3a0d982e68..3d03cb0a5ae 100644 --- a/doc/user/UpdaterConfig.md +++ b/doc/user/UpdaterConfig.md @@ -2,7 +2,7 @@ NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc. - Template directory is: /doc/templates - - Generated directory is: /docs + - Generated directory is: /doc/user --> diff --git a/doc/user/apis/GraphQL-Tutorial.md b/doc/user/apis/GraphQL-Tutorial.md index d65fbc144ba..3d365de5862 100644 --- a/doc/user/apis/GraphQL-Tutorial.md +++ b/doc/user/apis/GraphQL-Tutorial.md @@ -2,7 +2,7 @@ NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc. - Template directory is: /doc/templates - - Generated directory is: /docs + - Generated directory is: /doc/user --> # GraphQL tutorial diff --git a/doc/user/examples/ibi/portland/router-config.json b/doc/user/examples/ibi/portland/router-config.json index 0bf3547dbfd..acbcbc2e0a0 100644 --- a/doc/user/examples/ibi/portland/router-config.json +++ b/doc/user/examples/ibi/portland/router-config.json @@ -59,6 +59,62 @@ "url": "https://gbfs.spin.pm/api/gbfs/v2/portland" } ], + "vectorTiles": { + "basePath": "/rtp/routers/default/vectorTiles", + "attribution": "Regional Partners", + "layers": [ + { + "name": "stops", + "type": "Stop", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600, + "filter": "sunday-to-sunday-service-week" + }, + { + "name": "areaStops", + "type": "AreaStop", + "mapper": "OTPRR", + "maxZoom": 30, + "minZoom": 8, + "cacheMaxSeconds": 600 + }, + { + "name": "stations", + "type": "Station", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 2, + "cacheMaxSeconds": 600 + }, + { + "name": "rentalVehicles", + "type": "VehicleRentalVehicle", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 2, + "cacheMaxSeconds": 60 + }, + { + "name": "rentalStations", + "type": "VehicleRentalStation", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 2, + "cacheMaxSeconds": 600 + }, + { + "name": "vehicleParking", + "type": "VehicleParking", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 10, + "cacheMaxSeconds": 60, + "expansionFactor": 0.25 + } + ] + }, "rideHailingServices": [ { "type": "uber-car-hailing", diff --git a/doc/user/sandbox/MapboxVectorTilesApi.md b/doc/user/sandbox/MapboxVectorTilesApi.md index 62f3bd36c38..4430a398a5d 100644 --- a/doc/user/sandbox/MapboxVectorTilesApi.md +++ b/doc/user/sandbox/MapboxVectorTilesApi.md @@ -173,6 +173,7 @@ For each layer, the configuration includes: |       type = "stop" | `enum` | Type of the layer. | *Required* | | 2.0 | |       [cacheMaxSeconds](#vectorTiles_layers_0_cacheMaxSeconds) | `integer` | Sets the cache header in the response. | *Optional* | `-1` | 2.0 | |       [expansionFactor](#vectorTiles_layers_0_expansionFactor) | `double` | How far outside its boundaries should the tile contain information. | *Optional* | `0.25` | 2.0 | +|       [filter](#vectorTiles_layers_0_filter) | `enum` | Reduce the result set of a layer further by a specific filter. | *Optional* | `"none"` | 2.6 | |       [mapper](#vectorTiles_layers_0_mapper) | `string` | Describes the mapper converting from the OTP model entities to the vector tile properties. | *Required* | | 2.0 | |       maxZoom | `integer` | Maximum zoom levels the layer is active for. | *Optional* | `20` | 2.0 | |       minZoom | `integer` | Minimum zoom levels the layer is active for. | *Optional* | `9` | 2.0 | @@ -245,6 +246,18 @@ How far outside its boundaries should the tile contain information. The value is a fraction of the tile size. If you are having problem with icons and shapes being clipped at tile edges, then increase this number. +

filter

+ +**Since version:** `2.6` ∙ **Type:** `enum` ∙ **Cardinality:** `Optional` ∙ **Default value:** `"none"` +**Path:** /vectorTiles/layers/[0] +**Enum values:** `none` | `sunday-to-sunday-service-week` + +Reduce the result set of a layer further by a specific filter. + +This is useful for when the schema of a layer, say stops, should remain unchanged but some +elements should not be included in the result. + +

mapper

**Since version:** `2.0` ∙ **Type:** `string` ∙ **Cardinality:** `Required` diff --git a/doc/user/sandbox/StopConsolidation.md b/doc/user/sandbox/StopConsolidation.md index b36429b1f60..d0e18a9ce30 100644 --- a/doc/user/sandbox/StopConsolidation.md +++ b/doc/user/sandbox/StopConsolidation.md @@ -2,7 +2,7 @@ NOTE! Part of this document is generated. Make sure you edit the template, not the generated doc. - Template directory is: /doc/templates - - Generated directory is: /docs + - Generated directory is: /doc/user --> # Stop consolidation diff --git a/pom.xml b/pom.xml index 710270dad16..1f5bb3ed8a3 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ https://opentripplanner.org org.opentripplanner otp - 2.6.0 + 2.7.0-SNAPSHOT jar @@ -56,9 +56,9 @@ - 157 + 158 - 31.3 + 32.0 2.52 2.17.2 3.1.8 @@ -858,7 +858,7 @@ org.apache.httpcomponents.client5 httpclient5 - 5.3.1 + 5.4 commons-cli @@ -989,7 +989,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.5 + 3.2.6 sign-artifacts diff --git a/renovate.json5 b/renovate.json5 index b672797d48f..045bbe07d22 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -150,6 +150,14 @@ "com.google.dagger:" ], "minimumReleaseAge": "1 week" + }, + { + "description": "Geotools takes a while to publish a changelog and since it pulls in JTS it can change the serialization of the graph", + "matchPackagePrefixes": [ + "org.geotools:" + ], + "minimumReleaseAge": "1 week", + "labels": ["skip changelog", "bump serialization id"] } ], "timezone": "Europe/Berlin" diff --git a/src/client/index.html b/src/client/index.html index 5b969aed83e..88764c84a17 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -5,8 +5,8 @@ OTP Debug Client - - + +
diff --git a/src/ext-test/java/org/opentripplanner/ext/fares/FareRuleSetTest.java b/src/ext-test/java/org/opentripplanner/ext/fares/FareRuleSetTest.java new file mode 100644 index 00000000000..13d4f713634 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/fares/FareRuleSetTest.java @@ -0,0 +1,222 @@ +package org.opentripplanner.ext.fares; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.Duration; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentripplanner.ext.fares.model.FareAttribute; +import org.opentripplanner.ext.fares.model.FareRuleSet; +import org.opentripplanner.transit.model.basic.Money; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +class FareRuleSetTest { + + private FareRuleSet fareRuleSet; + static final Money TWO_FIFTY = Money.usDollars(2.50f); + + @BeforeEach + void setUp() { + FeedScopedId id = new FeedScopedId("feed", "fare1"); + FareAttribute fareAttribute = FareAttribute + .of(id) + .setPrice(TWO_FIFTY) + .setPaymentMethod(1) + .setTransfers(1) + .setTransferDuration(7200) + .build(); + fareRuleSet = new FareRuleSet(fareAttribute); + } + + @Test + void testHasNoRules() { + assertFalse(fareRuleSet.hasRules()); + } + + @Test + void testAddOriginDestination() { + fareRuleSet.addOriginDestination("A", "B"); + assertTrue(fareRuleSet.hasRules()); + } + + @Test + void testAddRouteOriginDestination() { + fareRuleSet.addRouteOriginDestination("Route1", "A", "B"); + assertTrue(fareRuleSet.hasRules()); + assertEquals(1, fareRuleSet.getRouteOriginDestinations().size()); + } + + @Test + void testAddContains() { + fareRuleSet.addContains("Zone1"); + assertTrue(fareRuleSet.hasRules()); + assertEquals(1, fareRuleSet.getContains().size()); + } + + @Test + void testAddRoute() { + FeedScopedId routeId = new FeedScopedId("feed", "route1"); + fareRuleSet.addRoute(routeId); + assertTrue(fareRuleSet.hasRules()); + assertEquals(1, fareRuleSet.getRoutes().size()); + } + + @Test + void testMatchesWithNoRules() { + var routes = Set.of(new FeedScopedId("feed", "route1")); + var trips = Set.of(new FeedScopedId("feed", "trip1")); + var zones = Set.of("zone1"); + assertTrue( + fareRuleSet.matches("A", "B", Set.of(), Set.of(), Set.of(), 0, Duration.ZERO, Duration.ZERO) + ); + assertTrue( + fareRuleSet.matches( + "A", + "B", + zones, + routes, + trips, + 0, + Duration.ofMinutes(100), + Duration.ofMinutes(100) + ) + ); + } + + @Test + void testMatchesWithOriginDestination() { + fareRuleSet.addOriginDestination("A", "B"); + assertTrue( + fareRuleSet.matches("A", "B", Set.of(), Set.of(), Set.of(), 0, Duration.ZERO, Duration.ZERO) + ); + assertFalse( + fareRuleSet.matches("B", "C", Set.of(), Set.of(), Set.of(), 0, Duration.ZERO, Duration.ZERO) + ); + } + + @Test + void testMatchesWithContains() { + Set zones = new HashSet<>(); + zones.add("Zone1"); + zones.add("Zone2"); + fareRuleSet.addContains("Zone1"); + fareRuleSet.addContains("Zone2"); + assertTrue( + fareRuleSet.matches("A", "B", zones, Set.of(), Set.of(), 0, Duration.ZERO, Duration.ZERO) + ); + assertFalse( + fareRuleSet.matches("A", "B", Set.of(), Set.of(), Set.of(), 0, Duration.ZERO, Duration.ZERO) + ); + } + + @Test + void testMatchesWithRoutes() { + Set routes = new HashSet<>(); + FeedScopedId routeId = new FeedScopedId("feed", "route1"); + FeedScopedId otherRouteId = new FeedScopedId("feed", "route2"); + routes.add(routeId); + fareRuleSet.addRoute(routeId); + assertTrue( + fareRuleSet.matches("A", "B", Set.of(), routes, Set.of(), 0, Duration.ZERO, Duration.ZERO) + ); + assertFalse( + fareRuleSet.matches( + "A", + "B", + Set.of(), + Set.of(otherRouteId), + Set.of(), + 0, + Duration.ZERO, + Duration.ZERO + ) + ); + } + + @Test + void testMatchesWithTransfers() { + assertTrue( + fareRuleSet.matches("A", "B", Set.of(), Set.of(), Set.of(), 1, Duration.ZERO, Duration.ZERO) + ); + assertFalse( + fareRuleSet.matches("A", "B", Set.of(), Set.of(), Set.of(), 2, Duration.ZERO, Duration.ZERO) + ); + } + + @Test + void testMatchesWithTransferDuration() { + assertTrue( + fareRuleSet.matches( + "A", + "B", + Set.of(), + Set.of(), + Set.of(), + 0, + Duration.ofSeconds(7000), + Duration.ZERO + ) + ); + assertFalse( + fareRuleSet.matches( + "A", + "B", + Set.of(), + Set.of(), + Set.of(), + 0, + Duration.ofSeconds(8000), + Duration.ZERO + ) + ); + } + + @Test + void testMatchesWithJourneyDuration() { + FareAttribute journeyFare = FareAttribute + .of(new FeedScopedId("feed", "journey")) + .setPrice(Money.usDollars(3.00f)) + .setPaymentMethod(1) + .setJourneyDuration(7200) + .build(); + FareRuleSet journeyRuleSet = new FareRuleSet(journeyFare); + + assertTrue( + journeyRuleSet.matches( + "A", + "B", + Set.of(), + Set.of(), + Set.of(), + 0, + Duration.ZERO, + Duration.ofSeconds(7000) + ) + ); + assertFalse( + journeyRuleSet.matches( + "A", + "B", + Set.of(), + Set.of(), + Set.of(), + 0, + Duration.ZERO, + Duration.ofSeconds(8000) + ) + ); + } + + @Test + void testAgencyMethods() { + assertFalse(fareRuleSet.hasAgencyDefined()); + assertNull(fareRuleSet.getAgency()); + + FeedScopedId agencyId = new FeedScopedId("feed", "agency1"); + fareRuleSet.setAgency(agencyId); + assertTrue(fareRuleSet.hasAgencyDefined()); + assertEquals(agencyId, fareRuleSet.getAgency()); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/realtimeresolver/RealtimeResolverTest.java b/src/ext-test/java/org/opentripplanner/ext/realtimeresolver/RealtimeResolverTest.java index 21d5a7f1696..e5b842a474a 100644 --- a/src/ext-test/java/org/opentripplanner/ext/realtimeresolver/RealtimeResolverTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/realtimeresolver/RealtimeResolverTest.java @@ -129,9 +129,12 @@ void testPopulateLegsWithRealtimeKeepStaySeated() { private static TripPattern delay(TripPattern pattern1, int seconds) { var originalTimeTable = pattern1.getScheduledTimetable(); - var delayedTimetable = new Timetable(pattern1); var delayedTripTimes = delay(originalTimeTable.getTripTimes(0), seconds); - delayedTimetable.addTripTimes(delayedTripTimes); + var delayedTimetable = Timetable + .of() + .withTripPattern(pattern1) + .addTripTimes(delayedTripTimes) + .build(); return pattern1.copy().withScheduledTimeTable(delayedTimetable).build(); } diff --git a/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java b/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java index cf29eec8f49..d10dbc05090 100644 --- a/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java @@ -2,12 +2,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertFailure; -import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.SERVICE_DATE; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.timetable.RealTimeState; @@ -15,33 +14,48 @@ import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.updater.spi.UpdateError; +import org.opentripplanner.updater.trip.RealtimeTestConstants; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; -import uk.org.siri.siri20.EstimatedTimetableDeliveryStructure; +import org.opentripplanner.updater.trip.TripInput; -class SiriTimetableSnapshotSourceTest { +class SiriTimetableSnapshotSourceTest implements RealtimeTestConstants { + + private static final TripInput TRIP_1_INPUT = TripInput + .of(TRIP_1_ID) + .withRoute(ROUTE_1.copy().withOperator(OPERATOR1).build()) + .addStop(STOP_A1, "0:00:10", "0:00:11") + .addStop(STOP_B1, "0:00:20", "0:00:21") + .build(); + + private static final TripInput TRIP_2_INPUT = TripInput + .of(TRIP_2_ID) + .addStop(STOP_A1, "0:01:00", "0:01:01") + .addStop(STOP_B1, "0:01:10", "0:01:11") + .addStop(STOP_C1, "0:01:20", "0:01:21") + .build(); @Test void testCancelTrip() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); - assertEquals(RealTimeState.SCHEDULED, env.getTripTimesForTrip(env.trip1).getRealTimeState()); + assertEquals(RealTimeState.SCHEDULED, env.getTripTimesForTrip(TRIP_1_ID).getRealTimeState()); var updates = new SiriEtBuilder(env.getDateTimeHelper()) - .withDatedVehicleJourneyRef(env.trip1.getId().getId()) + .withDatedVehicleJourneyRef(TRIP_1_ID) .withCancellation(true) .buildEstimatedTimetableDeliveries(); var result = env.applyEstimatedTimetable(updates); assertEquals(1, result.successful()); - assertEquals(RealTimeState.CANCELED, env.getTripTimesForTrip(env.trip1).getRealTimeState()); + assertEquals(RealTimeState.CANCELED, env.getTripTimesForTrip(TRIP_1_ID).getRealTimeState()); } @Test void testAddJourneyWithExistingRoute() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); - Route route = env.getTransitService().getRouteForId(env.route1Id); + Route route = ROUTE_1; int numPatternForRoute = env.getTransitService().getPatternsForRoute(route).size(); String newJourneyId = "newJourney"; @@ -55,7 +69,7 @@ void testAddJourneyWithExistingRoute() { "SCHEDULED | C1 0:01 0:01 | D1 0:03 0:03", env.getScheduledTimetable(newJourneyId) ); - FeedScopedId tripId = TransitModelForTest.id(newJourneyId); + FeedScopedId tripId = id(newJourneyId); TransitService transitService = env.getTransitService(); Trip trip = transitService.getTripForId(tripId); assertNotNull(trip); @@ -75,7 +89,8 @@ void testAddJourneyWithExistingRoute() { @Test void testAddJourneyWithNewRoute() { - var env = RealtimeTestEnvironment.siri(); + // we actually don't need the trip, but it's the only way to add a route to the index + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); String newRouteRef = "new route ref"; var updates = createValidAddedJourney(env) @@ -93,7 +108,7 @@ void testAddJourneyWithNewRoute() { ); TransitService transitService = env.getTransitService(); assertEquals(numRoutes + 1, transitService.getAllRoutes().size()); - FeedScopedId newRouteId = TransitModelForTest.id(newRouteRef); + FeedScopedId newRouteId = id(newRouteRef); Route newRoute = transitService.getRouteForId(newRouteId); assertNotNull(newRoute); assertEquals(1, transitService.getPatternsForRoute(newRoute).size()); @@ -101,7 +116,8 @@ void testAddJourneyWithNewRoute() { @Test void testAddJourneyMultipleTimes() { - var env = RealtimeTestEnvironment.siri(); + // we actually don't need the trip, but it's the only way to add a route to the index + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); var updates = createValidAddedJourney(env).buildEstimatedTimetableDeliveries(); int numTrips = env.getTransitService().getAllTrips().size(); @@ -115,20 +131,21 @@ void testAddJourneyMultipleTimes() { @Test void testAddedJourneyWithInvalidScheduledData() { - var env = RealtimeTestEnvironment.siri(); + // we actually don't need the trip, but it's the only way to add a route to the index + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); // Create an extra journey with invalid planned data (travel back in time) // and valid real time data var createExtraJourney = new SiriEtBuilder(env.getDateTimeHelper()) .withEstimatedVehicleJourneyCode("newJourney") .withIsExtraJourney(true) - .withOperatorRef(env.operator1Id.getId()) - .withLineRef(env.route1Id.getId()) + .withOperatorRef(OPERATOR_1_ID) + .withLineRef(ROUTE_1_ID) .withEstimatedCalls(builder -> builder - .call(env.stopA1) + .call(STOP_A1) .departAimedExpected("10:58", "10:48") - .call(env.stopB1) + .call(STOP_B1) .arriveAimedExpected("10:08", "10:58") ) .buildEstimatedTimetableDeliveries(); @@ -140,7 +157,7 @@ void testAddedJourneyWithInvalidScheduledData() { @Test void testAddedJourneyWithUnresolvableAgency() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().build(); // Create an extra journey with unknown line and operator var createExtraJourney = new SiriEtBuilder(env.getDateTimeHelper()) @@ -150,9 +167,9 @@ void testAddedJourneyWithUnresolvableAgency() { .withLineRef("unknown line") .withEstimatedCalls(builder -> builder - .call(env.stopA1) + .call(STOP_A1) .departAimedExpected("10:58", "10:48") - .call(env.stopB1) + .call(STOP_B1) .arriveAimedExpected("10:08", "10:58") ) .buildEstimatedTimetableDeliveries(); @@ -164,17 +181,17 @@ void testAddedJourneyWithUnresolvableAgency() { @Test void testReplaceJourney() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) .withEstimatedVehicleJourneyCode("newJourney") .withIsExtraJourney(true) // replace trip1 - .withVehicleJourneyRef(env.trip1.getId().getId()) - .withOperatorRef(env.operator1Id.getId()) - .withLineRef(env.route1Id.getId()) - .withRecordedCalls(builder -> builder.call(env.stopA1).departAimedActual("00:01", "00:02")) - .withEstimatedCalls(builder -> builder.call(env.stopC1).arriveAimedExpected("00:03", "00:04")) + .withVehicleJourneyRef(TRIP_1_ID) + .withOperatorRef(OPERATOR_1_ID) + .withLineRef(ROUTE_1_ID) + .withRecordedCalls(builder -> builder.call(STOP_A1).departAimedActual("00:01", "00:02")) + .withEstimatedCalls(builder -> builder.call(STOP_C1).arriveAimedExpected("00:03", "00:04")) .buildEstimatedTimetableDeliveries(); var result = env.applyEstimatedTimetable(updates); @@ -188,7 +205,7 @@ void testReplaceJourney() { ); // Original trip should not get canceled - var originalTripTimes = env.getTripTimesForTrip(env.trip1); + var originalTripTimes = env.getTripTimesForTrip(TRIP_1_ID); assertEquals(RealTimeState.SCHEDULED, originalTripTimes.getRealTimeState()); } @@ -197,17 +214,17 @@ void testReplaceJourney() { */ @Test void testUpdateJourneyWithDatedVehicleJourneyRef() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); var updates = updatedJourneyBuilder(env) - .withDatedVehicleJourneyRef(env.trip1.getId().getId()) + .withDatedVehicleJourneyRef(TRIP_1_ID) .buildEstimatedTimetableDeliveries(); var result = env.applyEstimatedTimetable(updates); assertEquals(1, result.successful()); assertTripUpdated(env); assertEquals( "UPDATED | A1 0:00:15 0:00:15 | B1 0:00:25 0:00:25", - env.getRealtimeTimetable(env.trip1) + env.getRealtimeTimetable(TRIP_1_ID) ); } @@ -216,11 +233,11 @@ void testUpdateJourneyWithDatedVehicleJourneyRef() { */ @Test void testUpdateJourneyWithFramedVehicleJourneyRef() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); var updates = updatedJourneyBuilder(env) .withFramedVehicleJourneyRef(builder -> - builder.withServiceDate(SERVICE_DATE).withVehicleJourneyRef(env.trip1.getId().getId()) + builder.withServiceDate(SERVICE_DATE).withVehicleJourneyRef(TRIP_1_ID) ) .buildEstimatedTimetableDeliveries(); var result = env.applyEstimatedTimetable(updates); @@ -233,7 +250,7 @@ void testUpdateJourneyWithFramedVehicleJourneyRef() { */ @Test void testUpdateJourneyWithoutJourneyRef() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); var updates = updatedJourneyBuilder(env).buildEstimatedTimetableDeliveries(); var result = env.applyEstimatedTimetable(updates); @@ -246,7 +263,7 @@ void testUpdateJourneyWithoutJourneyRef() { */ @Test void testUpdateJourneyWithFuzzyMatching() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); var updates = updatedJourneyBuilder(env).buildEstimatedTimetableDeliveries(); var result = env.applyEstimatedTimetableWithFuzzyMatcher(updates); @@ -260,17 +277,17 @@ void testUpdateJourneyWithFuzzyMatching() { */ @Test void testUpdateJourneyWithFuzzyMatchingAndMissingAimedDepartureTime() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) .withFramedVehicleJourneyRef(builder -> - builder.withServiceDate(RealtimeTestEnvironment.SERVICE_DATE).withVehicleJourneyRef("XXX") + builder.withServiceDate(SERVICE_DATE).withVehicleJourneyRef("XXX") ) .withEstimatedCalls(builder -> builder - .call(env.stopA1) + .call(STOP_A1) .departAimedExpected(null, "00:00:12") - .call(env.stopB1) + .call(STOP_B1) .arriveAimedExpected("00:00:20", "00:00:22") ) .buildEstimatedTimetableDeliveries(); @@ -285,15 +302,13 @@ void testUpdateJourneyWithFuzzyMatchingAndMissingAimedDepartureTime() { */ @Test void testChangeQuay() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) - .withDatedVehicleJourneyRef(env.trip1.getId().getId()) - .withRecordedCalls(builder -> - builder.call(env.stopA1).departAimedActual("00:00:11", "00:00:15") - ) + .withDatedVehicleJourneyRef(TRIP_1_ID) + .withRecordedCalls(builder -> builder.call(STOP_A1).departAimedActual("00:00:11", "00:00:15")) .withEstimatedCalls(builder -> - builder.call(env.stopB2).arriveAimedExpected("00:00:20", "00:00:33") + builder.call(STOP_B2).arriveAimedExpected("00:00:20", "00:00:33") ) .buildEstimatedTimetableDeliveries(); @@ -302,23 +317,23 @@ void testChangeQuay() { assertEquals(1, result.successful()); assertEquals( "MODIFIED | A1 [R] 0:00:15 0:00:15 | B2 0:00:33 0:00:33", - env.getRealtimeTimetable(env.trip1) + env.getRealtimeTimetable(TRIP_1_ID) ); } @Test void testCancelStop() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_2_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) - .withDatedVehicleJourneyRef(env.trip2.getId().getId()) + .withDatedVehicleJourneyRef(TRIP_2_ID) .withEstimatedCalls(builder -> builder - .call(env.stopA1) + .call(STOP_A1) .departAimedExpected("00:01:01", "00:01:01") - .call(env.stopB1) + .call(STOP_B1) .withIsCancellation(true) - .call(env.stopC1) + .call(STOP_C1) .arriveAimedExpected("00:01:30", "00:01:30") ) .buildEstimatedTimetableDeliveries(); @@ -328,7 +343,7 @@ void testCancelStop() { assertEquals(1, result.successful()); assertEquals( "MODIFIED | A1 0:01:01 0:01:01 | B1 [C] 0:01:10 0:01:11 | C1 0:01:30 0:01:30", - env.getRealtimeTimetable(env.trip2) + env.getRealtimeTimetable(TRIP_2_ID) ); } @@ -336,20 +351,18 @@ void testCancelStop() { @Test @Disabled("Not supported yet") void testAddStop() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) - .withDatedVehicleJourneyRef(env.trip1.getId().getId()) - .withRecordedCalls(builder -> - builder.call(env.stopA1).departAimedActual("00:00:11", "00:00:15") - ) + .withDatedVehicleJourneyRef(TRIP_1_ID) + .withRecordedCalls(builder -> builder.call(STOP_A1).departAimedActual("00:00:11", "00:00:15")) .withEstimatedCalls(builder -> builder - .call(env.stopD1) + .call(STOP_D1) .withIsExtraCall(true) .arriveAimedExpected("00:00:19", "00:00:20") .departAimedExpected("00:00:24", "00:00:25") - .call(env.stopB1) + .call(STOP_B1) .arriveAimedExpected("00:00:20", "00:00:33") ) .buildEstimatedTimetableDeliveries(); @@ -359,7 +372,7 @@ void testAddStop() { assertEquals(1, result.successful()); assertEquals( "MODIFIED | A1 0:00:15 0:00:15 | D1 [C] 0:00:20 0:00:25 | B1 0:00:33 0:00:33", - env.getRealtimeTimetable(env.trip1) + env.getRealtimeTimetable(TRIP_1_ID) ); } @@ -369,7 +382,7 @@ void testAddStop() { @Test void testNotMonitored() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) .withMonitored(false) @@ -382,19 +395,19 @@ void testNotMonitored() { @Test void testReplaceJourneyWithoutEstimatedVehicleJourneyCode() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) .withDatedVehicleJourneyRef("newJourney") .withIsExtraJourney(true) - .withVehicleJourneyRef(env.trip1.getId().getId()) - .withOperatorRef(env.operator1Id.getId()) - .withLineRef(env.route1Id.getId()) + .withVehicleJourneyRef(TRIP_1_ID) + .withOperatorRef(OPERATOR_1_ID) + .withLineRef(ROUTE_1_ID) .withEstimatedCalls(builder -> builder - .call(env.stopA1) + .call(STOP_A1) .departAimedExpected("00:01", "00:02") - .call(env.stopC1) + .call(STOP_C1) .arriveAimedExpected("00:03", "00:04") ) .buildEstimatedTimetableDeliveries(); @@ -407,15 +420,15 @@ void testReplaceJourneyWithoutEstimatedVehicleJourneyCode() { @Test void testNegativeHopTime() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) - .withDatedVehicleJourneyRef(env.trip1.getId().getId()) + .withDatedVehicleJourneyRef(TRIP_1_ID) .withRecordedCalls(builder -> builder - .call(env.stopA1) + .call(STOP_A1) .departAimedActual("00:00:11", "00:00:15") - .call(env.stopB1) + .call(STOP_B1) .arriveAimedActual("00:00:20", "00:00:14") ) .buildEstimatedTimetableDeliveries(); @@ -427,18 +440,18 @@ void testNegativeHopTime() { @Test void testNegativeDwellTime() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_2_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) - .withDatedVehicleJourneyRef(env.trip2.getId().getId()) + .withDatedVehicleJourneyRef(TRIP_2_ID) .withRecordedCalls(builder -> builder - .call(env.stopA1) + .call(STOP_A1) .departAimedActual("00:01:01", "00:01:01") - .call(env.stopB1) + .call(STOP_B1) .arriveAimedActual("00:01:10", "00:01:13") .departAimedActual("00:01:11", "00:01:12") - .call(env.stopB1) + .call(STOP_B1) .arriveAimedActual("00:01:20", "00:01:20") ) .buildEstimatedTimetableDeliveries(); @@ -452,19 +465,19 @@ void testNegativeDwellTime() { @Test @Disabled("Not supported yet") void testExtraUnknownStop() { - var env = RealtimeTestEnvironment.siri(); + var env = RealtimeTestEnvironment.siri().addTrip(TRIP_1_INPUT).build(); var updates = new SiriEtBuilder(env.getDateTimeHelper()) - .withDatedVehicleJourneyRef(env.trip1.getId().getId()) + .withDatedVehicleJourneyRef(TRIP_1_ID) .withEstimatedCalls(builder -> builder - .call(env.stopA1) + .call(STOP_A1) .departAimedExpected("00:00:11", "00:00:15") // Unexpected extra stop without isExtraCall flag - .call(env.stopD1) + .call(STOP_D1) .arriveAimedExpected("00:00:19", "00:00:20") .departAimedExpected("00:00:24", "00:00:25") - .call(env.stopB1) + .call(STOP_B1) .arriveAimedExpected("00:00:20", "00:00:33") ) .buildEstimatedTimetableDeliveries(); @@ -478,20 +491,19 @@ private static SiriEtBuilder createValidAddedJourney(RealtimeTestEnvironment env return new SiriEtBuilder(env.getDateTimeHelper()) .withEstimatedVehicleJourneyCode("newJourney") .withIsExtraJourney(true) - .withOperatorRef(env.operator1Id.getId()) - .withLineRef(env.route1Id.getId()) - .withRecordedCalls(builder -> builder.call(env.stopC1).departAimedActual("00:01", "00:02")) - .withEstimatedCalls(builder -> builder.call(env.stopD1).arriveAimedExpected("00:03", "00:04") - ); + .withOperatorRef(OPERATOR_1_ID) + .withLineRef(ROUTE_1_ID) + .withRecordedCalls(builder -> builder.call(STOP_C1).departAimedActual("00:01", "00:02")) + .withEstimatedCalls(builder -> builder.call(STOP_D1).arriveAimedExpected("00:03", "00:04")); } private static SiriEtBuilder updatedJourneyBuilder(RealtimeTestEnvironment env) { return new SiriEtBuilder(env.getDateTimeHelper()) .withEstimatedCalls(builder -> builder - .call(env.stopA1) + .call(STOP_A1) .departAimedExpected("00:00:11", "00:00:15") - .call(env.stopB1) + .call(STOP_B1) .arriveAimedExpected("00:00:20", "00:00:25") ); } @@ -499,7 +511,7 @@ private static SiriEtBuilder updatedJourneyBuilder(RealtimeTestEnvironment env) private static void assertTripUpdated(RealtimeTestEnvironment env) { assertEquals( "UPDATED | A1 0:00:15 0:00:15 | B1 0:00:25 0:00:25", - env.getRealtimeTimetable(env.trip1) + env.getRealtimeTimetable(TRIP_1_ID) ); } } diff --git a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/LayerFiltersTest.java b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/LayerFiltersTest.java new file mode 100644 index 00000000000..f12d43b62cf --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/LayerFiltersTest.java @@ -0,0 +1,42 @@ +package org.opentripplanner.ext.vectortiles.layers; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.model._data.PatternTestModel; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.RegularStop; + +class LayerFiltersTest { + + private static final RegularStop STOP = TransitModelForTest.of().stop("1").build(); + private static final LocalDate DATE = LocalDate.of(2024, 9, 5); + private static final TripPattern PATTERN = PatternTestModel.pattern(); + + @Test + void includeStopWithinServiceWeek() { + var predicate = LayerFilters.buildCurrentServiceWeekPredicate( + s -> List.of(PATTERN), + trip -> List.of(DATE), + () -> DATE + ); + + assertTrue(predicate.test(STOP)); + } + + @Test + void excludeOutsideServiceWeek() { + var inThreeWeeks = DATE.plusDays(21); + var predicate = LayerFilters.buildCurrentServiceWeekPredicate( + s -> List.of(PATTERN), + trip -> List.of(inThreeWeeks), + () -> DATE + ); + + assertFalse(predicate.test(STOP)); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/fares/impl/DefaultFareServiceFactory.java b/src/ext/java/org/opentripplanner/ext/fares/impl/DefaultFareServiceFactory.java index ee4c87924ea..d7a3a0425ec 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/impl/DefaultFareServiceFactory.java +++ b/src/ext/java/org/opentripplanner/ext/fares/impl/DefaultFareServiceFactory.java @@ -92,7 +92,7 @@ protected void fillFareRules( FareRuleSet fareRule = fareRuleSet.get(id); if (fareRule == null) { // Should never happen by design - LOG.error("Inexistant fare ID in fare rule: " + id); + LOG.error("Nonexistent fare ID in fare rule: " + id); continue; } String contains = rule.getContainsId(); diff --git a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareFactory.java b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareFactory.java index 34a03c1fc06..d48cad48450 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareFactory.java +++ b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareFactory.java @@ -24,6 +24,8 @@ public FareService makeFareService() { @Override public void processGtfs(FareRulesData fareRuleService, OtpTransitService transitService) { fillFareRules(fareRuleService.fareAttributes(), fareRuleService.fareRules(), regularFareRules); + // ORCA agencies don't rely on fare attributes without rules, so let's remove them. + regularFareRules.entrySet().removeIf(entry -> !entry.getValue().hasRules()); } /** diff --git a/src/ext/java/org/opentripplanner/ext/fares/model/FareRuleSet.java b/src/ext/java/org/opentripplanner/ext/fares/model/FareRuleSet.java index 30119631216..b8fbb0f1fe9 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/model/FareRuleSet.java +++ b/src/ext/java/org/opentripplanner/ext/fares/model/FareRuleSet.java @@ -40,6 +40,19 @@ public Set getRouteOriginDestinations() { return routeOriginDestinations; } + /** + * Determine whether the FareRuleSet has any rules added. + * @return True if any rules have been added. + */ + public boolean hasRules() { + return ( + !routes.isEmpty() || + !originDestinations.isEmpty() || + !routeOriginDestinations.isEmpty() || + !contains.isEmpty() + ); + } + public void addContains(String containsId) { contains.add(containsId); } @@ -60,6 +73,19 @@ public FareAttribute getFareAttribute() { return fareAttribute; } + /** + * Determines whether the FareRuleSet matches against a set of itinerary parameters + * based on the added rules and fare attribute + * @param startZone Origin zone + * @param endZone End zone + * @param zonesVisited A set containing the names of zones visited on the fare + * @param routesVisited A set containing the route IDs visited + * @param tripsVisited [Not implemented] A set containing the trip IDs visited + * @param transfersUsed Number of transfers already used + * @param tripTime Time from beginning of first leg to beginning of current leg to be evaluated + * @param journeyTime Total journey time from beginning of first leg to end of current leg + * @return True if this FareAttribute should apply to this leg + */ public boolean matches( String startZone, String endZone, diff --git a/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java b/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java index cdbaa8f3d4c..ed2378a839d 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java +++ b/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java @@ -210,13 +210,6 @@ Result build() { // TODO: We always create a new TripPattern to be able to modify its scheduled timetable StopPattern stopPattern = new StopPattern(aimedStopTimes); - TripPattern pattern = TripPattern - .of(getTripPatternId.apply(trip)) - .withRoute(trip.getRoute()) - .withMode(trip.getMode()) - .withNetexSubmode(trip.getNetexSubMode()) - .withStopPattern(stopPattern) - .build(); RealTimeTripTimes tripTimes = TripTimesFactory.tripTimes( trip, @@ -229,7 +222,16 @@ Result build() { // therefore they must be valid tripTimes.validateNonIncreasingTimes(); tripTimes.setServiceCode(transitService.getServiceCodeForId(trip.getServiceId())); - pattern.add(tripTimes); + + TripPattern pattern = TripPattern + .of(getTripPatternId.apply(trip)) + .withRoute(trip.getRoute()) + .withMode(trip.getMode()) + .withNetexSubmode(trip.getNetexSubMode()) + .withStopPattern(stopPattern) + .withScheduledTimeTableBuilder(builder -> builder.addTripTimes(tripTimes)) + .build(); + RealTimeTripTimes updatedTripTimes = tripTimes.copyScheduledTimes(); // Loop through calls again and apply updates diff --git a/src/ext/java/org/opentripplanner/ext/vectortiles/layers/LayerFilters.java b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/LayerFilters.java new file mode 100644 index 00000000000..7d63bbe5398 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/LayerFilters.java @@ -0,0 +1,73 @@ +package org.opentripplanner.ext.vectortiles.layers; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import org.opentripplanner.apis.gtfs.model.LocalDateRange; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.service.PatternByServiceDatesFilter; +import org.opentripplanner.transit.service.TransitService; + +/** + * Predicates for filtering elements of vector tile layers. Currently only contains predicates + * for {@link RegularStop}. Once more types need to be filtered, this may need some refactoring. + */ +public class LayerFilters { + + /** + * No filter is applied: all stops are included in the result. + */ + public static final Predicate NO_FILTER = x -> true; + + /** + * Returns a predicate which only includes stop which are visited by a pattern that is in the current + * "service week", which lasts from Sunday to Sunday. + */ + public static Predicate buildCurrentServiceWeekPredicate( + Function> getPatternsForStop, + Function> getServiceDatesForTrip, + Supplier nowSupplier + ) { + var serviceDate = nowSupplier.get(); + var lastSunday = serviceDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY)); + var nextSundayPlusOne = serviceDate.with(TemporalAdjusters.next(DayOfWeek.SUNDAY)).plusDays(1); + + var filter = new PatternByServiceDatesFilter( + // reminder, the end of the date range is exclusive so it's the next Sunday plus one day + new LocalDateRange(lastSunday, nextSundayPlusOne), + // not used + route -> List.of(), + getServiceDatesForTrip + ); + + return regularStop -> { + var patterns = getPatternsForStop.apply(regularStop); + var patternsInCurrentWeek = filter.filterPatterns(patterns); + return !patternsInCurrentWeek.isEmpty(); + }; + } + + public static Predicate forType(FilterType type, TransitService transitService) { + return switch (type) { + case NONE -> NO_FILTER; + case SUNDAY_TO_SUNDAY_SERVICE_WEEK -> buildCurrentServiceWeekPredicate( + transitService::getPatternsForStop, + trip -> + transitService.getCalendarService().getServiceDatesForServiceId(trip.getServiceId()), + () -> LocalDate.now(transitService.getTimeZone()) + ); + }; + } + + public enum FilterType { + NONE, + SUNDAY_TO_SUNDAY_SERVICE_WEEK, + } +} diff --git a/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerBuilder.java b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerBuilder.java index aa664497728..141157f8f3e 100644 --- a/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerBuilder.java +++ b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerBuilder.java @@ -5,24 +5,20 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.function.BiFunction; -import java.util.stream.Collectors; +import java.util.function.Predicate; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; -import org.opentripplanner.apis.support.mapping.PropertyMapper; import org.opentripplanner.ext.vectortiles.VectorTilesResource; +import org.opentripplanner.ext.vectortiles.layers.LayerFilters; import org.opentripplanner.inspector.vector.LayerBuilder; import org.opentripplanner.inspector.vector.LayerParameters; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.service.TransitService; -public class StopsLayerBuilder extends LayerBuilder { +public class StopsLayerBuilder extends LayerBuilder { - static Map>> mappers = Map.of( - MapperType.Digitransit, - DigitransitStopPropertyMapper::create - ); private final TransitService transitService; + private final Predicate filter; public StopsLayerBuilder( TransitService transitService, @@ -30,7 +26,7 @@ public StopsLayerBuilder( Locale locale ) { super( - (PropertyMapper) Map + Map .ofEntries( entry(MapperType.Digitransit, new DigitransitStopPropertyMapper(transitService, locale)), entry( @@ -43,12 +39,14 @@ public StopsLayerBuilder( layerParameters.expansionFactor() ); this.transitService = transitService; + this.filter = LayerFilters.forType(layerParameters.filterType(), transitService); } protected List getGeometries(Envelope query) { return transitService .findRegularStops(query) .stream() + .filter(filter) .map(stop -> { Geometry point = stop.getGeometry(); @@ -56,7 +54,7 @@ protected List getGeometries(Envelope query) { return point; }) - .collect(Collectors.toList()); + .toList(); } enum MapperType { diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java index 9ec83a4bf67..950e3d9bba5 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java @@ -23,6 +23,7 @@ import org.opentripplanner.model.plan.StreetLeg; import org.opentripplanner.model.plan.TransitLeg; import org.opentripplanner.model.plan.WalkStep; +import org.opentripplanner.model.plan.legreference.LegReferenceSerializer; import org.opentripplanner.routing.alertpatch.TransitAlert; import org.opentripplanner.routing.alternativelegs.AlternativeLegs; import org.opentripplanner.routing.alternativelegs.AlternativeLegsFilter; @@ -189,10 +190,12 @@ public DataFetcher realTime() { return environment -> getSource(environment).getRealTime(); } - // TODO @Override public DataFetcher realtimeState() { - return environment -> null; + return environment -> { + var state = getSource(environment).getRealTimeState(); + return (state != null) ? state.name() : null; + }; } @Override @@ -324,4 +327,15 @@ public DataFetcher> nextLegs() { public DataFetcher accessibilityScore() { return environment -> NumberMapper.toDouble(getSource(environment).accessibilityScore()); } + + @Override + public DataFetcher id() { + return environment -> { + var ref = getSource(environment).getLegReference(); + if (ref == null) { + return null; + } + return LegReferenceSerializer.encode(ref); + }; + } } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java index 0e70c13074b..95984ba6dd0 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java @@ -27,12 +27,12 @@ import org.locationtech.jts.geom.Envelope; import org.opentripplanner.apis.gtfs.GraphQLRequestContext; import org.opentripplanner.apis.gtfs.GraphQLUtils; -import org.opentripplanner.apis.gtfs.PatternByServiceDatesFilter; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLQueryTypeStopsByRadiusArgs; import org.opentripplanner.apis.gtfs.mapping.routerequest.LegacyRouteRequestMapper; import org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapper; +import org.opentripplanner.apis.gtfs.support.filter.PatternByDateFilterUtil; import org.opentripplanner.apis.gtfs.support.time.LocalDateRangeUtil; import org.opentripplanner.ext.fares.impl.DefaultFareService; import org.opentripplanner.ext.fares.impl.GtfsFaresService; @@ -42,6 +42,9 @@ import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.gtfs.mapping.DirectionMapper; import org.opentripplanner.model.TripTimeOnDate; +import org.opentripplanner.model.plan.Leg; +import org.opentripplanner.model.plan.legreference.LegReference; +import org.opentripplanner.model.plan.legreference.LegReferenceSerializer; import org.opentripplanner.routing.alertpatch.EntitySelector; import org.opentripplanner.routing.alertpatch.TransitAlert; import org.opentripplanner.routing.api.request.RouteRequest; @@ -362,6 +365,20 @@ public DataFetcher> nearest() { }; } + @Override + public DataFetcher leg() { + return environment -> { + TransitService transitService = getTransitService(environment); + var args = new GraphQLTypes.GraphQLQueryTypeLegArgs(environment.getArguments()); + String id = args.getGraphQLId(); + LegReference ref = LegReferenceSerializer.decode(id); + if (ref == null) { + return null; + } + return ref.getLeg(transitService); + }; + } + @Override public DataFetcher node() { return environment -> { @@ -615,8 +632,11 @@ public DataFetcher> routes() { } if (LocalDateRangeUtil.hasServiceDateFilter(args.getGraphQLServiceDates())) { - var filter = new PatternByServiceDatesFilter(args.getGraphQLServiceDates(), transitService); - routeStream = filter.filterRoutes(routeStream).stream(); + var filter = PatternByDateFilterUtil.ofGraphQL( + args.getGraphQLServiceDates(), + transitService + ); + routeStream = filter.filterRoutes(routeStream.toList()).stream(); } return routeStream.toList(); }; diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RouteImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RouteImpl.java index a3f557951f0..ae6acb0a297 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RouteImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RouteImpl.java @@ -9,12 +9,12 @@ import java.util.stream.Collectors; import org.opentripplanner.apis.gtfs.GraphQLRequestContext; import org.opentripplanner.apis.gtfs.GraphQLUtils; -import org.opentripplanner.apis.gtfs.PatternByServiceDatesFilter; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLBikesAllowed; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLTransitMode; import org.opentripplanner.apis.gtfs.mapping.BikesAllowedMapper; +import org.opentripplanner.apis.gtfs.support.filter.PatternByDateFilterUtil; import org.opentripplanner.apis.gtfs.support.time.LocalDateRangeUtil; import org.opentripplanner.routing.alertpatch.EntitySelector; import org.opentripplanner.routing.alertpatch.TransitAlert; @@ -183,7 +183,10 @@ public DataFetcher> patterns() { var args = new GraphQLTypes.GraphQLRoutePatternsArgs(environment.getArguments()); if (LocalDateRangeUtil.hasServiceDateFilter(args.getGraphQLServiceDates())) { - var filter = new PatternByServiceDatesFilter(args.getGraphQLServiceDates(), transitService); + var filter = PatternByDateFilterUtil.ofGraphQL( + args.getGraphQLServiceDates(), + transitService + ); return filter.filterPatterns(patterns); } else { return patterns; diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java index 0730d2fbc91..503899ba21a 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java @@ -18,6 +18,8 @@ import org.opentripplanner.apis.gtfs.GraphQLUtils; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.apis.gtfs.support.filter.PatternByDateFilterUtil; +import org.opentripplanner.apis.gtfs.support.time.LocalDateRangeUtil; import org.opentripplanner.framework.time.ServiceDateUtils; import org.opentripplanner.model.StopTimesInPattern; import org.opentripplanner.model.TripTimeOnDate; @@ -243,7 +245,19 @@ public DataFetcher platformCode() { @Override public DataFetcher> routes() { - return this::getRoutes; + return env -> { + var args = new GraphQLTypes.GraphQLStopRoutesArgs(env.getArguments()); + var routes = getRoutes(env); + if (LocalDateRangeUtil.hasServiceDateFilter(args.getGraphQLServiceDates())) { + var filter = PatternByDateFilterUtil.ofGraphQL( + args.getGraphQLServiceDates(), + getTransitService(env) + ); + return filter.filterRoutes(routes); + } else { + return routes; + } + }; } @Override diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java index 67944543580..2dbcf7d1998 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java @@ -482,6 +482,8 @@ public interface GraphQLLeg { public DataFetcher headsign(); + public DataFetcher id(); + public DataFetcher interlineWithPreviousLeg(); public DataFetcher intermediatePlace(); @@ -779,6 +781,8 @@ public interface GraphQLQueryType { public DataFetcher fuzzyTrip(); + public DataFetcher leg(); + public DataFetcher> nearest(); public DataFetcher node(); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java index 67051444cdf..3cd98b15652 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java @@ -2432,6 +2432,25 @@ public void setGraphQLTime(Integer time) { } } + public static class GraphQLQueryTypeLegArgs { + + private String id; + + public GraphQLQueryTypeLegArgs(Map args) { + if (args != null) { + this.id = (String) args.get("id"); + } + } + + public String getGraphQLId() { + return this.id; + } + + public void setGraphQLId(String id) { + this.id = id; + } + } + public static class GraphQLQueryTypeNearestArgs { private String after; @@ -4231,6 +4250,26 @@ public void setGraphQLLanguage(String language) { } } + public static class GraphQLStopRoutesArgs { + + private GraphQLLocalDateRangeInput serviceDates; + + public GraphQLStopRoutesArgs(Map args) { + if (args != null) { + this.serviceDates = + new GraphQLLocalDateRangeInput((Map) args.get("serviceDates")); + } + } + + public GraphQLLocalDateRangeInput getGraphQLServiceDates() { + return this.serviceDates; + } + + public void setGraphQLServiceDates(GraphQLLocalDateRangeInput serviceDates) { + this.serviceDates = serviceDates; + } + } + public static class GraphQLStopStopTimesForPatternArgs { private String id; diff --git a/src/main/java/org/opentripplanner/apis/gtfs/support/filter/PatternByDateFilterUtil.java b/src/main/java/org/opentripplanner/apis/gtfs/support/filter/PatternByDateFilterUtil.java new file mode 100644 index 00000000000..f3c0d6d0352 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/support/filter/PatternByDateFilterUtil.java @@ -0,0 +1,23 @@ +package org.opentripplanner.apis.gtfs.support.filter; + +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.apis.gtfs.model.LocalDateRange; +import org.opentripplanner.transit.service.PatternByServiceDatesFilter; +import org.opentripplanner.transit.service.TransitService; + +/** + * Utility methods for instantiating a {@link PatternByServiceDatesFilter}. + */ +public class PatternByDateFilterUtil { + + public static PatternByServiceDatesFilter ofGraphQL( + GraphQLTypes.GraphQLLocalDateRangeInput range, + TransitService transitService + ) { + return new PatternByServiceDatesFilter( + new LocalDateRange(range.getGraphQLStart(), range.getGraphQLEnd()), + transitService::getPatternsForRoute, + trip -> transitService.getCalendarService().getServiceDatesForServiceId(trip.getServiceId()) + ); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java index 9ad43606420..c3b59070903 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java @@ -1596,7 +1596,7 @@ private GraphQLSchema create() { GraphQLFieldDefinition .newFieldDefinition() .name("leg") - .description("Refetch a single leg based on its id") + .description("Refetch a single transit leg based on its id") .withDirective(gqlUtil.timingData) .type(LegType.REF) .argument( @@ -1623,7 +1623,9 @@ private GraphQLSchema create() { GraphQLFieldDefinition .newFieldDefinition() .name("serverInfo") - .description("Get OTP server information") + .description( + "Get OTP deployment information. This is only useful for developers of OTP itself not regular API users." + ) .withDirective(gqlUtil.timingData) .type(new GraphQLNonNull(serverInfoType)) .dataFetcher(e -> projectInfo()) diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/framework/ServerInfoType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/ServerInfoType.java index ae6889ab033..8f679cafda3 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/framework/ServerInfoType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/ServerInfoType.java @@ -9,6 +9,7 @@ import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLOutputType; +import org.opentripplanner.apis.transmodel.support.GqlUtil; public class ServerInfoType { @@ -16,6 +17,13 @@ public static GraphQLOutputType create() { return GraphQLObjectType .newObject() .name("ServerInfo") + .description( + """ + Information about the deployment. This is only useful to developers of OTP itself. + It is not recommended for regular API consumers to use this type as it has no + stability guarantees. + """ + ) .field( GraphQLFieldDefinition .newFieldDefinition() @@ -99,6 +107,21 @@ public static GraphQLOutputType create() { .dataFetcher(e -> projectInfo().getOtpSerializationVersionId()) .build() ) + .field( + GraphQLFieldDefinition + .newFieldDefinition() + .name("internalTransitModelTimeZone") + .description( + """ + The internal time zone of the transit data. + + Note: The input data can be in several time zones, but OTP internally operates on a single one. + """ + ) + .type(Scalars.GraphQLString) + .dataFetcher(e -> GqlUtil.getTransitService(e).getTimeZone()) + .build() + ) .build(); } } diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/LegType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/LegType.java index 867d08e3933..e4a0b350d3d 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/LegType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/LegType.java @@ -63,7 +63,9 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("id") - .description("An identifier for the leg, which can be used to re-fetch the information.") + .description( + "An identifier for the leg, which can be used to re-fetch transit leg information." + ) .type(Scalars.GraphQLID) .dataFetcher(env -> LegReferenceSerializer.encode(leg(env).getLegReference())) .build() diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyQuery.java b/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyQuery.java index c3c8ba420e4..e3fbf90a35d 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyQuery.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyQuery.java @@ -10,13 +10,13 @@ import graphql.schema.GraphQLOutputType; import java.time.LocalDate; import java.util.List; -import java.util.stream.Stream; import org.opentripplanner.apis.transmodel.mapping.TransitIdMapper; import org.opentripplanner.apis.transmodel.model.EnumTypes; import org.opentripplanner.apis.transmodel.support.GqlUtil; +import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; +import org.opentripplanner.transit.api.request.TripOnServiceDateRequestBuilder; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.timetable.TripAlteration; -import org.opentripplanner.transit.model.timetable.TripOnServiceDate; /** * A GraphQL query for retrieving data on DatedServiceJourneys @@ -93,72 +93,34 @@ public static GraphQLFieldDefinition createQuery( .type(new GraphQLList(new GraphQLNonNull(Scalars.GraphQLString))) ) .dataFetcher(environment -> { - Stream stream = GqlUtil - .getTransitService(environment) - .getAllTripOnServiceDates() - .stream(); - + // The null safety checks are not needed here - they are taken care of by the request + // object, but reuse let's use the mapping method and leave this improvement until all APIs + // are pushing this check into the domain request. + var authorities = mapIDsToDomainNullSafe(environment.getArgument("authorities")); var lines = mapIDsToDomainNullSafe(environment.getArgument("lines")); var serviceJourneys = mapIDsToDomainNullSafe(environment.getArgument("serviceJourneys")); + var replacementFor = mapIDsToDomainNullSafe(environment.getArgument("replacementFor")); var privateCodes = environment.>getArgument("privateCodes"); var operatingDays = environment.>getArgument("operatingDays"); var alterations = environment.>getArgument("alterations"); - var authorities = mapIDsToDomainNullSafe(environment.getArgument("authorities")); - var replacementFor = mapIDsToDomainNullSafe(environment.getArgument("replacementFor")); - if (!lines.isEmpty()) { - stream = - stream.filter(tripOnServiceDate -> - lines.contains(tripOnServiceDate.getTrip().getRoute().getId()) - ); - } + TripOnServiceDateRequestBuilder tripOnServiceDateRequestBuilder = TripOnServiceDateRequest + .of() + .withOperatingDays(operatingDays) + .withAuthorities(authorities) + .withLines(lines) + .withServiceJourneys(serviceJourneys) + .withReplacementFor(replacementFor); - if (!serviceJourneys.isEmpty()) { - stream = - stream.filter(tripOnServiceDate -> - serviceJourneys.contains(tripOnServiceDate.getTrip().getId()) - ); - } + tripOnServiceDateRequestBuilder = + tripOnServiceDateRequestBuilder.withPrivateCodes(privateCodes); - if (privateCodes != null && !privateCodes.isEmpty()) { - stream = - stream.filter(tripOnServiceDate -> - privateCodes.contains(tripOnServiceDate.getTrip().getNetexInternalPlanningCode()) - ); - } + tripOnServiceDateRequestBuilder = + tripOnServiceDateRequestBuilder.withAlterations(alterations); - // At least one operationg day is required - var days = operatingDays.stream().toList(); - - stream = - stream.filter(tripOnServiceDate -> days.contains(tripOnServiceDate.getServiceDate())); - - if (alterations != null && !alterations.isEmpty()) { - stream = - stream.filter(tripOnServiceDate -> - alterations.contains(tripOnServiceDate.getTripAlteration()) - ); - } - - if (!authorities.isEmpty()) { - stream = - stream.filter(tripOnServiceDate -> - authorities.contains(tripOnServiceDate.getTrip().getRoute().getAgency().getId()) - ); - } - - if (!replacementFor.isEmpty()) { - stream = - stream.filter(tripOnServiceDate -> - !tripOnServiceDate.getReplacementFor().isEmpty() && - tripOnServiceDate - .getReplacementFor() - .stream() - .anyMatch(replacement -> replacementFor.contains(replacement.getId())) - ); - } - - return stream.toList(); + return GqlUtil + .getTransitService(environment) + .getTripOnServiceDates(tripOnServiceDateRequestBuilder.build()); }) .build(); } diff --git a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java index 749772fae12..29c2a452448 100644 --- a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java +++ b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java @@ -31,6 +31,11 @@ public enum OTPFeature { Use the [vector tiles feature if](sandbox/MapboxVectorTilesApi.md) you want a stable map tiles API. """ ), + ExtraTransferLegOnSameStop( + false, + false, + "Should there be a transfer leg when transferring on the very same stop. Note that for in-seat/interlined transfers no transfer leg will be generated." + ), FloatingBike(true, false, "Enable floating bike routing."), GtfsGraphQlApi(true, false, "Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md)."), GtfsGraphQlApiRentalStationFuzzyMatching( diff --git a/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java b/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java index 1e86f49770f..63f1df1aad5 100644 --- a/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java +++ b/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java @@ -37,6 +37,9 @@ public static String toString(@Nullable Collection c, String nullText) { /** * A null-safe version of isEmpty() for a collection. *

+ * The main strategy handling collections in OTP is to avoid nullable collection fields and use empty + * collections instead. So, before using this method check if the variable/field is indeed `@Nullable`. + *

* If the collection is {@code null} then {@code true} is returned. *

* If the collection is empty then {@code true} is returned. diff --git a/src/main/java/org/opentripplanner/framework/collection/ListUtils.java b/src/main/java/org/opentripplanner/framework/collection/ListUtils.java index 5964a1674e3..35b7e083695 100644 --- a/src/main/java/org/opentripplanner/framework/collection/ListUtils.java +++ b/src/main/java/org/opentripplanner/framework/collection/ListUtils.java @@ -69,4 +69,13 @@ public static List ofNullable(T input) { return List.of(input); } } + + /** + * This method converts the given collection to an instance of a List. If the input is + * {@code null} an empty collection is returned. If not the {@link List#copyOf(Collection)} is + * called. + */ + public static List nullSafeImmutableList(Collection c) { + return (c == null) ? List.of() : List.copyOf(c); + } } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/TimeZoneAdjusterModule.java b/src/main/java/org/opentripplanner/graph_builder/module/TimeZoneAdjusterModule.java index eb3674bf76a..7168066afe3 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/TimeZoneAdjusterModule.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/TimeZoneAdjusterModule.java @@ -7,6 +7,7 @@ import java.util.HashMap; import java.util.Map; import org.opentripplanner.graph_builder.model.GraphBuilderModule; +import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.service.TransitModel; /** @@ -45,9 +46,15 @@ public void buildGraph() { return; } - pattern - .getScheduledTimetable() - .updateAllTripTimes(it -> it.adjustTimesToGraphTimeZone(timeShift)); + TripPattern updatedPattern = pattern + .copy() + .withScheduledTimeTableBuilder(builder -> + builder.updateAllTripTimes(tt -> tt.adjustTimesToGraphTimeZone(timeShift)) + ) + .build(); + // replace the original pattern with the updated pattern in the transit model + transitModel.addTripPattern(updatedPattern.getId(), updatedPattern); }); + transitModel.index(); } } diff --git a/src/main/java/org/opentripplanner/gtfs/GenerateTripPatternsOperation.java b/src/main/java/org/opentripplanner/gtfs/GenerateTripPatternsOperation.java index bf26f9c6d7b..08ad300fa9a 100644 --- a/src/main/java/org/opentripplanner/gtfs/GenerateTripPatternsOperation.java +++ b/src/main/java/org/opentripplanner/gtfs/GenerateTripPatternsOperation.java @@ -3,6 +3,7 @@ import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -23,6 +24,7 @@ import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.network.TripPatternBuilder; import org.opentripplanner.transit.model.timetable.Direction; import org.opentripplanner.transit.model.timetable.FrequencyEntry; import org.opentripplanner.transit.model.timetable.Trip; @@ -40,13 +42,18 @@ public class GenerateTripPatternsOperation { private final Map tripPatternIdCounters = new HashMap<>(); - private final OtpTransitServiceBuilder transitDaoBuilder; + private final OtpTransitServiceBuilder transitServiceBuilder; private final DataImportIssueStore issueStore; private final Deduplicator deduplicator; private final Set calendarServiceIds; private final GeometryProcessor geometryProcessor; - private final Multimap tripPatterns; + // TODO the linked hashset configuration ensures that TripPatterns are created in the same order + // as Trips are imported, as a workaround for issue #6067 + private final Multimap tripPatternBuilders = MultimapBuilder + .linkedHashKeys() + .linkedHashSetValues() + .build(); private final ListMultimap frequenciesForTrip = ArrayListMultimap.create(); private int freqCount = 0; @@ -59,18 +66,17 @@ public GenerateTripPatternsOperation( Set calendarServiceIds, GeometryProcessor geometryProcessor ) { - this.transitDaoBuilder = builder; + this.transitServiceBuilder = builder; this.issueStore = issueStore; this.deduplicator = deduplicator; this.calendarServiceIds = calendarServiceIds; this.geometryProcessor = geometryProcessor; - this.tripPatterns = transitDaoBuilder.getTripPatterns(); } public void run() { collectFrequencyByTrip(); - final Collection trips = transitDaoBuilder.getTripsById().values(); + final Collection trips = transitServiceBuilder.getTripsById().values(); var progressLogger = ProgressTracker.track("build trip patterns", 50_000, trips.size()); LOG.info(progressLogger.startMessage()); @@ -85,6 +91,14 @@ public void run() { } } + tripPatternBuilders + .values() + .stream() + .map(TripPatternBuilder::build) + .forEach(tripPattern -> + transitServiceBuilder.getTripPatterns().put(tripPattern.getStopPattern(), tripPattern) + ); + LOG.info(progressLogger.completeMessage()); LOG.info( "Added {} frequency-based and {} single-trip timetable entries.", @@ -107,7 +121,7 @@ public boolean hasScheduledTrips() { * the same trip can be added at once to the same Timetable/TripPattern. */ private void collectFrequencyByTrip() { - for (Frequency freq : transitDaoBuilder.getFrequencies()) { + for (Frequency freq : transitServiceBuilder.getFrequencies()) { frequenciesForTrip.put(freq.getTrip(), freq); } } @@ -119,7 +133,7 @@ private void buildTripPatternForTrip(Trip trip) { return; // Invalid trip, skip it, it will break later } - List stopTimes = transitDaoBuilder.getStopTimesSortedByTrip().get(trip); + List stopTimes = transitServiceBuilder.getStopTimesSortedByTrip().get(trip); // If after filtering this trip does not contain at least 2 stoptimes, it does not serve any purpose. var staticTripWithFewerThan2Stops = @@ -134,8 +148,7 @@ private void buildTripPatternForTrip(Trip trip) { // Get the existing TripPattern for this filtered StopPattern, or create one. StopPattern stopPattern = new StopPattern(stopTimes); - Direction direction = trip.getDirection(); - TripPattern tripPattern = findOrCreateTripPattern(stopPattern, trip, direction); + TripPatternBuilder tripPatternBuilder = findOrCreateTripPattern(stopPattern, trip); // Create a TripTimes object for this list of stoptimes, which form one trip. TripTimes tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, deduplicator); @@ -144,44 +157,42 @@ private void buildTripPatternForTrip(Trip trip) { List frequencies = frequenciesForTrip.get(trip); if (!frequencies.isEmpty()) { for (Frequency freq : frequencies) { - tripPattern.add(new FrequencyEntry(freq, tripTimes)); + tripPatternBuilder.withScheduledTimeTableBuilder(builder -> + builder.addFrequencyEntry(new FrequencyEntry(freq, tripTimes)) + ); freqCount++; } } // This trip was not frequency-based. Add the TripTimes directly to the TripPattern's scheduled timetable. else { - tripPattern.add(tripTimes); + tripPatternBuilder.withScheduledTimeTableBuilder(builder -> builder.addTripTimes(tripTimes)); scheduledCount++; } } - private TripPattern findOrCreateTripPattern( - StopPattern stopPattern, - Trip trip, - Direction direction - ) { + private TripPatternBuilder findOrCreateTripPattern(StopPattern stopPattern, Trip trip) { Route route = trip.getRoute(); - for (TripPattern tripPattern : tripPatterns.get(stopPattern)) { + Direction direction = trip.getDirection(); + for (TripPatternBuilder tripPatternBuilder : tripPatternBuilders.get(stopPattern)) { if ( - tripPattern.getRoute().equals(route) && - tripPattern.getDirection().equals(direction) && - tripPattern.getMode().equals(trip.getMode()) && - tripPattern.getNetexSubmode().equals(trip.getNetexSubMode()) + tripPatternBuilder.getRoute().equals(route) && + tripPatternBuilder.getDirection().equals(direction) && + tripPatternBuilder.getMode().equals(trip.getMode()) && + tripPatternBuilder.getNetexSubmode().equals(trip.getNetexSubMode()) ) { - return tripPattern; + return tripPatternBuilder; } } FeedScopedId patternId = generateUniqueIdForTripPattern(route, direction); - TripPattern tripPattern = TripPattern + TripPatternBuilder tripPatternBuilder = TripPattern .of(patternId) .withRoute(route) .withStopPattern(stopPattern) .withMode(trip.getMode()) .withNetexSubmode(trip.getNetexSubMode()) - .withHopGeometries(geometryProcessor.createHopGeometries(trip)) - .build(); - tripPatterns.put(stopPattern, tripPattern); - return tripPattern; + .withHopGeometries(geometryProcessor.createHopGeometries(trip)); + tripPatternBuilders.put(stopPattern, tripPatternBuilder); + return tripPatternBuilder; } /** diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java index d58410741f9..b7aea1c2e2f 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java @@ -72,7 +72,6 @@ public class GTFSToOtpTransitServiceMapper { private final FareTransferRuleMapper fareTransferRuleMapper; - private final StopAreaMapper stopAreaMapper; private final DirectionMapper directionMapper; private final DataImportIssueStore issueStore; @@ -112,9 +111,6 @@ public GTFSToOtpTransitServiceMapper( boardingAreaMapper = new BoardingAreaMapper(translationHelper, stopLookup); locationMapper = new LocationMapper(builder.stopModel(), issueStore); locationGroupMapper = new LocationGroupMapper(stopMapper, locationMapper, builder.stopModel()); - // the use of stop areas were reverted in the spec - // this code will go away, please migrate now! - stopAreaMapper = new StopAreaMapper(stopMapper, locationMapper, builder.stopModel()); pathwayMapper = new PathwayMapper(stopMapper, entranceMapper, pathwayNodeMapper, boardingAreaMapper); routeMapper = new RouteMapper(agencyMapper, issueStore, translationHelper); @@ -126,7 +122,6 @@ public GTFSToOtpTransitServiceMapper( stopMapper, locationMapper, locationGroupMapper, - stopAreaMapper, tripMapper, bookingRuleMapper, translationHelper @@ -166,7 +161,6 @@ public void mapStopTripAndRouteDataIntoBuilder() { // Stop areas and Stop groups are only used in FLEX routes builder.stopModel().withAreaStops(locationMapper.map(data.getAllLocations())); builder.stopModel().withGroupStops(locationGroupMapper.map(data.getAllLocationGroups())); - builder.stopModel().withGroupStops(stopAreaMapper.map(data.getAllStopAreas())); } builder.getPathways().addAll(pathwayMapper.map(data.getAllPathways())); diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/StopAreaMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/StopAreaMapper.java deleted file mode 100644 index 55c836aa458..00000000000 --- a/src/main/java/org/opentripplanner/gtfs/mapping/StopAreaMapper.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.opentripplanner.gtfs.mapping; - -import static org.opentripplanner.gtfs.mapping.AgencyAndIdMapper.mapAgencyAndId; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import org.onebusaway.gtfs.model.Location; -import org.onebusaway.gtfs.model.Stop; -import org.opentripplanner.framework.collection.MapUtils; -import org.opentripplanner.framework.i18n.NonLocalizedString; -import org.opentripplanner.transit.model.site.GroupStop; -import org.opentripplanner.transit.model.site.GroupStopBuilder; -import org.opentripplanner.transit.service.StopModelBuilder; - -/** - * For a while GTFS Flex location groups were replaced by GTFS Fares v2 stop areas. After a few - * months, this decision was reverted and a new style of location groups were re-added to the Flex - * spec. - * @deprecated Arcadis tooling still produces stop areas and for a while we will support both. Please don't rely - * on this as the class will be removed in the future! - */ -@Deprecated -public class StopAreaMapper { - - private final StopMapper stopMapper; - - private final LocationMapper locationMapper; - - private final Map mappedStopAreas = new HashMap<>(); - private final StopModelBuilder stopModel; - - public StopAreaMapper( - StopMapper stopMapper, - LocationMapper locationMapper, - StopModelBuilder stopModel - ) { - this.stopMapper = stopMapper; - this.locationMapper = locationMapper; - this.stopModel = stopModel; - } - - Collection map(Collection allAreas) { - return MapUtils.mapToList(allAreas, this::map); - } - - /** Map from GTFS to OTP model, {@code null} safe. */ - GroupStop map(org.onebusaway.gtfs.model.StopArea original) { - return original == null ? null : mappedStopAreas.computeIfAbsent(original, this::doMap); - } - - private GroupStop doMap(org.onebusaway.gtfs.model.StopArea element) { - GroupStopBuilder groupStopBuilder = stopModel - .groupStop(mapAgencyAndId(element.getId())) - .withName(new NonLocalizedString(element.getName())); - - for (org.onebusaway.gtfs.model.StopLocation location : element.getLocations()) { - switch (location) { - case Stop stop -> groupStopBuilder.addLocation(stopMapper.map(stop)); - case Location loc -> groupStopBuilder.addLocation(locationMapper.map(loc)); - case org.onebusaway.gtfs.model.StopArea ignored -> throw new RuntimeException( - "Nested GroupStops are not allowed" - ); - case null, default -> throw new RuntimeException( - "Unknown location type: " + location.getClass().getSimpleName() - ); - } - } - - return groupStopBuilder.build(); - } -} diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/StopTimeMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/StopTimeMapper.java index 67b250c5061..5f20b6e0224 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/StopTimeMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/StopTimeMapper.java @@ -7,7 +7,6 @@ import org.onebusaway.gtfs.model.Location; import org.onebusaway.gtfs.model.LocationGroup; import org.onebusaway.gtfs.model.Stop; -import org.onebusaway.gtfs.model.StopArea; import org.opentripplanner.framework.collection.MapUtils; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.model.StopTime; @@ -22,7 +21,6 @@ class StopTimeMapper { private final LocationMapper locationMapper; private final LocationGroupMapper locationGroupMapper; - private final StopAreaMapper stopAreaMapper; private final TripMapper tripMapper; private final BookingRuleMapper bookingRuleMapper; @@ -35,7 +33,6 @@ class StopTimeMapper { StopMapper stopMapper, LocationMapper locationMapper, LocationGroupMapper locationGroupMapper, - StopAreaMapper stopAreaMapper, TripMapper tripMapper, BookingRuleMapper bookingRuleMapper, TranslationHelper translationHelper @@ -43,7 +40,6 @@ class StopTimeMapper { this.stopMapper = stopMapper; this.locationMapper = locationMapper; this.locationGroupMapper = locationGroupMapper; - this.stopAreaMapper = stopAreaMapper; this.tripMapper = tripMapper; this.bookingRuleMapper = bookingRuleMapper; this.translationHelper = translationHelper; @@ -71,8 +67,6 @@ private StopTime doMap(org.onebusaway.gtfs.model.StopTime rhs) { case Stop stop -> lhs.setStop(stopMapper.map(stop)); case Location location -> lhs.setStop(locationMapper.map(location)); case LocationGroup locGroup -> lhs.setStop(locationGroupMapper.map(locGroup)); - // TODO: only here for backwards compatibility, this will be removed in the future - case StopArea area -> lhs.setStop(stopAreaMapper.map(area)); default -> throw new IllegalArgumentException( "Unknown location type: %s".formatted(stopLocation) ); diff --git a/src/main/java/org/opentripplanner/inspector/vector/LayerParameters.java b/src/main/java/org/opentripplanner/inspector/vector/LayerParameters.java index cb849e0f0ac..70cb5a2ce43 100644 --- a/src/main/java/org/opentripplanner/inspector/vector/LayerParameters.java +++ b/src/main/java/org/opentripplanner/inspector/vector/LayerParameters.java @@ -1,6 +1,7 @@ package org.opentripplanner.inspector.vector; import org.opentripplanner.apis.support.mapping.PropertyMapper; +import org.opentripplanner.ext.vectortiles.layers.LayerFilters; /** * Configuration options for a single vector tile layer. @@ -53,4 +54,8 @@ default int cacheMaxSeconds() { default double expansionFactor() { return EXPANSION_FACTOR; } + + default LayerFilters.FilterType filterType() { + return LayerFilters.FilterType.NONE; + } } diff --git a/src/main/java/org/opentripplanner/model/Timetable.java b/src/main/java/org/opentripplanner/model/Timetable.java index 10c384e6211..6740e2cc99d 100644 --- a/src/main/java/org/opentripplanner/model/Timetable.java +++ b/src/main/java/org/opentripplanner/model/Timetable.java @@ -16,13 +16,11 @@ import java.time.LocalDate; import java.time.ZoneId; import java.util.ArrayList; +import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; -import java.util.function.UnaryOperator; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.opentripplanner.framework.time.ServiceDateUtils; import org.opentripplanner.transit.model.framework.DataValidationException; @@ -48,8 +46,7 @@ * one Timetable when stop time updates are being applied: one for the scheduled stop times, one for * each snapshot of updated stop times, another for a working buffer of updated stop times, etc. *

- * TODO OTP2 - Move this to package: org.opentripplanner.model after as Entur NeTEx PRs are merged. - * Also consider moving its dependencies into package org.opentripplanner.routing. The NEW + * TODO OTP2 consider moving dependencies into package org.opentripplanner.routing. The NEW * Timetable should not have any dependencies to [?] */ public class Timetable implements Serializable { @@ -58,28 +55,30 @@ public class Timetable implements Serializable { private final TripPattern pattern; - private final List tripTimes = new ArrayList<>(); + private final List tripTimes; - private final List frequencyEntries = new ArrayList<>(); + private final List frequencyEntries; @Nullable private final LocalDate serviceDate; + Timetable(TimetableBuilder timetableBuilder) { + this.pattern = timetableBuilder.getPattern(); + this.serviceDate = timetableBuilder.getServiceDate(); + this.tripTimes = timetableBuilder.createImmutableOrderedListOfTripTimes(); + this.frequencyEntries = List.copyOf(timetableBuilder.getFrequencies()); + } + /** Construct an empty Timetable. */ - public Timetable(TripPattern pattern) { - this.pattern = pattern; - this.serviceDate = null; + public static TimetableBuilder of() { + return new TimetableBuilder(); } /** - * Copy constructor: create an un-indexed Timetable with the same TripTimes as the specified - * timetable. + * Copy timetable into a builder witch can be used to modify the timetable. */ - Timetable(Timetable tt, @Nonnull LocalDate serviceDate) { - Objects.requireNonNull(serviceDate); - tripTimes.addAll(tt.tripTimes); - this.serviceDate = serviceDate; - this.pattern = tt.pattern; + public TimetableBuilder copyOf() { + return new TimetableBuilder(this); } /** @return the index of TripTimes for this trip ID in this particular Timetable */ @@ -134,17 +133,6 @@ public TripTimes getTripTimes(FeedScopedId tripId) { return null; } - /** - * Set new trip times for trip given a trip index - * - * @param tripIndex trip index of trip - * @param tt new trip times for trip - * @return old trip times of trip - */ - public TripTimes setTripTimes(int tripIndex, TripTimes tt) { - return tripTimes.set(tripIndex, tt); - } - /** * Apply the TripUpdate to the appropriate TripTimes from this Timetable. The existing TripTimes * must not be modified directly because they may be shared with the underlying @@ -386,44 +374,6 @@ public Result createUpdatedTripTimesFromGTFSRT( return Result.success(new TripTimesPatch(newTimes, skippedStopIndices)); } - /** - * Add a trip to this Timetable. The Timetable must be analyzed, compacted, and indexed any time - * trips are added, but this is not done automatically because it is time consuming and should - * only be done once after an entire batch of trips are added. Note that the trip is not added to - * the enclosing pattern here, but in the pattern's wrapper function. Here we don't know if it's a - * scheduled trip or a realtime-added trip. - */ - public void addTripTimes(TripTimes tt) { - tripTimes.add(tt); - } - - /** - * Apply the same update to all trip-times inculuding scheduled and frequency based - * trip times. - *

- * THIS IS NOT THREAD-SAFE - ONLY USE THIS METHOD DURING GRAPH-BUILD! - */ - public void updateAllTripTimes(UnaryOperator update) { - tripTimes.replaceAll(update); - frequencyEntries.replaceAll(it -> - new FrequencyEntry( - it.startTime, - it.endTime, - it.headway, - it.exactTimes, - update.apply(it.tripTimes) - ) - ); - } - - /** - * Add a frequency entry to this Timetable. See addTripTimes method. Maybe Frequency Entries - * should just be TripTimes for simplicity. - */ - public void addFrequencyEntry(FrequencyEntry freq) { - frequencyEntries.add(freq); - } - public boolean isValidFor(LocalDate serviceDate) { return this.serviceDate == null || this.serviceDate.equals(serviceDate); } @@ -473,21 +423,61 @@ public LocalDate getServiceDate() { } /** - * The direction for all the trips in this pattern. + * Return the direction for all the trips in this timetable. + * By construction, all trips in a timetable have the same direction. */ public Direction getDirection() { + return getDirection(tripTimes, frequencyEntries); + } + + /** + * Return an arbitrary TripTimes in this Timetable. + * Return a scheduled trip times if it exists, otherwise return a frequency-based trip times. + */ + public TripTimes getRepresentativeTripTimes() { + return getRepresentativeTripTimes(tripTimes, frequencyEntries); + } + + /** + * @return true if the timetable was created by a real-time update, false if this + * timetable is based on scheduled data. + * Only real-time timetables have a service date. + */ + public boolean isCreatedByRealTimeUpdater() { + return serviceDate != null; + } + + /** + * The direction for the given collections of trip times. + * The method assumes that all trip times have the same directions and picks up one arbitrarily. + * @param scheduledTripTimes all the scheduled-based trip times in a timetable. + * @param frequencies all the frequency-based trip times in a timetable. + */ + static Direction getDirection( + Collection scheduledTripTimes, + Collection frequencies + ) { return Optional - .ofNullable(getRepresentativeTripTimes()) + .ofNullable(getRepresentativeTripTimes(scheduledTripTimes, frequencies)) .map(TripTimes::getTrip) .map(Trip::getDirection) .orElse(Direction.UNKNOWN); } - public TripTimes getRepresentativeTripTimes() { - if (!getTripTimes().isEmpty()) { - return getTripTimes(0); - } else if (!getFrequencyEntries().isEmpty()) { - return getFrequencyEntries().get(0).tripTimes; + /** + * Return an arbitrary TripTimes. + * @param scheduledTripTimes all the scheduled-based trip times in a timetable. + * @param frequencies all the frequency-based trip times in a timetable. + * + */ + private static TripTimes getRepresentativeTripTimes( + Collection scheduledTripTimes, + Collection frequencies + ) { + if (!scheduledTripTimes.isEmpty()) { + return scheduledTripTimes.iterator().next(); + } else if (!frequencies.isEmpty()) { + return frequencies.iterator().next().tripTimes; } else { // Pattern is created only for real-time updates return null; diff --git a/src/main/java/org/opentripplanner/model/TimetableBuilder.java b/src/main/java/org/opentripplanner/model/TimetableBuilder.java new file mode 100644 index 00000000000..74e89d0d973 --- /dev/null +++ b/src/main/java/org/opentripplanner/model/TimetableBuilder.java @@ -0,0 +1,136 @@ +package org.opentripplanner.model; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.Direction; +import org.opentripplanner.transit.model.timetable.FrequencyEntry; +import org.opentripplanner.transit.model.timetable.TripTimes; + +public class TimetableBuilder { + + private TripPattern pattern; + private LocalDate serviceDate; + private final Map tripTimes = new HashMap<>(); + private final List frequencies = new ArrayList<>(); + + TimetableBuilder() {} + + TimetableBuilder(Timetable tt) { + pattern = tt.getPattern(); + serviceDate = tt.getServiceDate(); + frequencies.addAll(tt.getFrequencyEntries()); + addAllTripTimes(tt.getTripTimes()); + } + + public TimetableBuilder withTripPattern(TripPattern tripPattern) { + this.pattern = tripPattern; + return this; + } + + public TimetableBuilder withServiceDate(LocalDate serviceDate) { + this.serviceDate = serviceDate; + return this; + } + + /** + * Add a new trip-times to the timetable. If the associated trip already exists, an exception is + * thrown. This is considered a programming error. Use {@link #addOrUpdateTripTimes(TripTimes)} + * if you want to replace an existing trip. + */ + public TimetableBuilder addTripTimes(TripTimes tripTimes) { + var trip = tripTimes.getTrip(); + if (this.tripTimes.containsKey(trip.getId())) { + throw new IllegalStateException( + "Error! TripTimes for the same trip is added twice. Trip: " + trip + ); + } + return addOrUpdateTripTimes(tripTimes); + } + + /** + * Add or update the trip-times. If the trip has an associated trip-times, then the trip-times + * are replaced. If not, the trip-times it is added. Consider using + * {@link #addTripTimes(TripTimes)}. + */ + public TimetableBuilder addOrUpdateTripTimes(TripTimes tripTimes) { + this.tripTimes.put(tripTimes.getTrip().getId(), tripTimes); + return this; + } + + public TimetableBuilder addAllTripTimes(List tripTimes) { + for (TripTimes it : tripTimes) { + addTripTimes(it); + } + return this; + } + + public TimetableBuilder removeTripTimes(TripTimes tripTimesToRemove) { + tripTimes.remove(tripTimesToRemove.getTrip().getId()); + return this; + } + + public TimetableBuilder removeAllTripTimes(Collection tripTimesToBeRemoved) { + for (TripTimes it : tripTimesToBeRemoved) { + tripTimes.remove(it.getTrip().getId()); + } + return this; + } + + /** + * Apply the same update to all trip-times including scheduled and frequency based + * trip times. + *

+ */ + public TimetableBuilder updateAllTripTimes(UnaryOperator update) { + tripTimes.replaceAll((t, tt) -> update.apply(tt)); + frequencies.replaceAll(it -> + new FrequencyEntry( + it.startTime, + it.endTime, + it.headway, + it.exactTimes, + update.apply(it.tripTimes) + ) + ); + return this; + } + + public TimetableBuilder addFrequencyEntry(FrequencyEntry frequencyEntry) { + this.frequencies.add(frequencyEntry); + return this; + } + + /** + * The direction for all the trips in this timetable. + */ + public Direction getDirection() { + return Timetable.getDirection(tripTimes.values(), frequencies); + } + + public Timetable build() { + return new Timetable(this); + } + + List createImmutableOrderedListOfTripTimes() { + return tripTimes.values().stream().sorted().toList(); + } + + TripPattern getPattern() { + return pattern; + } + + LocalDate getServiceDate() { + return serviceDate; + } + + List getFrequencies() { + return frequencies; + } +} diff --git a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java index d076bf9f1f0..8cd0707d655 100644 --- a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java +++ b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java @@ -298,22 +298,15 @@ public Result update(RealTimeTripUpdate realTimeTrip } Timetable tt = resolve(pattern, serviceDate); - // we need to perform the copy of Timetable here rather than in Timetable.update() - // to avoid repeatedly copying in case several updates are applied to the same timetable - tt = copyTimetable(pattern, serviceDate, tt); + TimetableBuilder ttb = tt.copyOf().withServiceDate(serviceDate); // Assume all trips in a pattern are from the same feed, which should be the case. - // Find trip index - Trip trip = updatedTripTimes.getTrip(); - int tripIndex = tt.getTripIndex(trip.getId()); - if (tripIndex == -1) { - // Trip not found, add it - tt.addTripTimes(updatedTripTimes); - } else { - // Set updated trip times of trip - tt.setTripTimes(tripIndex, updatedTripTimes); - } + ttb.addOrUpdateTripTimes(updatedTripTimes); + + Timetable updated = ttb.build(); + swapTimetable(pattern, tt, updated); + Trip trip = updatedTripTimes.getTrip(); if (pattern.isCreatedByRealtimeUpdater()) { // Remember this pattern for the added trip id and service date FeedScopedId tripId = trip.getId(); @@ -459,8 +452,11 @@ public boolean revertTripToScheduledTripPattern(FeedScopedId tripId, LocalDate s if (tripTimesToRemove != null) { for (Timetable originalTimetable : sortedTimetables) { if (originalTimetable.getTripTimes().contains(tripTimesToRemove)) { - Timetable updatedTimetable = copyTimetable(pattern, serviceDate, originalTimetable); - updatedTimetable.getTripTimes().remove(tripTimesToRemove); + Timetable updatedTimetable = originalTimetable + .copyOf() + .removeTripTimes(tripTimesToRemove) + .build(); + swapTimetable(pattern, originalTimetable, updatedTimetable); } } } @@ -579,36 +575,33 @@ private void addPatternToIndex(TripPattern tripPattern) { } /** - * Make a copy of the given timetable for a given pattern and service date. - * If the timetable was already copied-on write in this snapshot, the same instance will be - * returned. The SortedSet that holds the collection of Timetables for that pattern + * Replace the original Timetable by the updated one in the timetable index. + * The SortedSet that holds the collection of Timetables for that pattern * (sorted by service date) is shared between multiple snapshots and must be copied as well.
* Note on performance: if multiple Timetables are modified in a SortedSet, the SortedSet will be * copied multiple times. The impact on memory/garbage collection is assumed to be minimal * since the collection is small. * The SortedSet is made immutable to prevent change after snapshot publication. */ - private Timetable copyTimetable(TripPattern pattern, LocalDate serviceDate, Timetable tt) { - if (!dirtyTimetables.contains(tt)) { - Timetable old = tt; - tt = new Timetable(tt, serviceDate); - SortedSet sortedTimetables = timetables.get(pattern); - if (sortedTimetables == null) { - sortedTimetables = new TreeSet<>(new SortedTimetableComparator()); - } else { - SortedSet temp = new TreeSet<>(new SortedTimetableComparator()); - temp.addAll(sortedTimetables); - sortedTimetables = temp; - } - if (old.getServiceDate() != null) { - sortedTimetables.remove(old); - } - sortedTimetables.add(tt); - timetables.put(pattern, ImmutableSortedSet.copyOfSorted(sortedTimetables)); - dirtyTimetables.add(tt); - dirty = true; + private void swapTimetable(TripPattern pattern, Timetable original, Timetable updated) { + SortedSet sortedTimetables = timetables.get(pattern); + if (sortedTimetables == null) { + sortedTimetables = new TreeSet<>(new SortedTimetableComparator()); + } else { + SortedSet temp = new TreeSet<>(new SortedTimetableComparator()); + temp.addAll(sortedTimetables); + sortedTimetables = temp; + } + // This is a minor optimization: + // Since sortedTimetables contains only timetables created in real-time, no need to try to + // remove the original if it was not created by real-time. + if (original.isCreatedByRealTimeUpdater()) { + sortedTimetables.remove(original); } - return tt; + sortedTimetables.add(updated); + timetables.put(pattern, ImmutableSortedSet.copyOfSorted(sortedTimetables)); + dirtyTimetables.add(updated); + dirty = true; } protected static class SortedTimetableComparator implements Comparator { diff --git a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java index 373b99f0bc6..d62d0331a34 100644 --- a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java +++ b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.opentripplanner.ext.flex.trip.FlexTrip; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.gtfs.mapping.StaySeatedNotAllowed; @@ -15,6 +16,7 @@ import org.opentripplanner.model.Frequency; import org.opentripplanner.model.OtpTransitService; import org.opentripplanner.model.ShapePoint; +import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TripStopTimes; import org.opentripplanner.model.calendar.CalendarServiceData; import org.opentripplanner.model.calendar.ServiceCalendar; @@ -51,6 +53,7 @@ import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; +import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.transit.service.StopModel; import org.opentripplanner.transit.service.StopModelBuilder; import org.slf4j.Logger; @@ -375,21 +378,40 @@ private void removeStopTimesForNoneExistingTrips() { private void fixOrRemovePatternsWhichReferenceNoneExistingTrips() { int orgSize = tripPatterns.size(); List> removePatterns = new ArrayList<>(); + List updatedPatterns = new ArrayList<>(); for (Map.Entry e : tripPatterns.entries()) { TripPattern ptn = e.getValue(); - ptn.removeTrips(t -> !tripsById.containsKey(t.getId())); - if (ptn.scheduledTripsAsStream().findAny().isEmpty()) { + Set tripTimesToBeRemoved = ptn + .getScheduledTimetable() + .getTripTimes() + .stream() + .filter(tripTimes -> !tripsById.containsKey(tripTimes.getTrip().getId())) + .collect(Collectors.toUnmodifiableSet()); + if (!tripTimesToBeRemoved.isEmpty()) { removePatterns.add(e); + Timetable updatedTimetable = ptn + .getScheduledTimetable() + .copyOf() + .removeAllTripTimes(tripTimesToBeRemoved) + .build(); + TripPattern updatedPattern = ptn.copy().withScheduledTimeTable(updatedTimetable).build(); + if (!updatedTimetable.getTripTimes().isEmpty()) { + updatedPatterns.add(updatedPattern); + } else { + issueStore.add( + "RemovedEmptyTripPattern", + "Removed trip pattern %s as it contains no trips", + updatedPattern.getId() + ); + } } } for (Map.Entry it : removePatterns) { tripPatterns.remove(it.getKey(), it.getValue()); - issueStore.add( - "RemovedEmptyTripPattern", - "Removed trip pattern %s as it contains no trips", - it.getValue().getId() - ); + } + for (TripPattern tripPattern : updatedPatterns) { + tripPatterns.put(tripPattern.getStopPattern(), tripPattern); } logRemove("TripPattern", orgSize, tripPatterns.size(), "No trips for pattern exist."); } diff --git a/src/main/java/org/opentripplanner/model/plan/Leg.java b/src/main/java/org/opentripplanner/model/plan/Leg.java index 2a0b6726560..d9e3a4589d8 100644 --- a/src/main/java/org/opentripplanner/model/plan/Leg.java +++ b/src/main/java/org/opentripplanner/model/plan/Leg.java @@ -22,6 +22,7 @@ import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.organization.Operator; import org.opentripplanner.transit.model.site.FareZone; +import org.opentripplanner.transit.model.timetable.RealTimeState; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.booking.BookingInfo; @@ -244,6 +245,10 @@ default boolean getRealTime() { return false; } + default RealTimeState getRealTimeState() { + return null; + } + /** * Whether this Leg describes a flexible trip. The reason we need this is that FlexTrip does not * inherit from Trip, so that the information that the Trip is flexible would be lost when diff --git a/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java index d94ec1895c2..e774c9e0dbc 100644 --- a/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java +++ b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java @@ -34,6 +34,7 @@ import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.organization.Operator; import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.RealTimeState; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; @@ -227,6 +228,11 @@ public boolean getRealTime() { ); } + @Override + public RealTimeState getRealTimeState() { + return tripTimes.getRealTimeState(); + } + @Override public double getDistanceMeters() { return distanceMeters; diff --git a/src/main/java/org/opentripplanner/netex/mapping/TripPatternMapper.java b/src/main/java/org/opentripplanner/netex/mapping/TripPatternMapper.java index 740224b4489..c7b85e72af8 100644 --- a/src/main/java/org/opentripplanner/netex/mapping/TripPatternMapper.java +++ b/src/main/java/org/opentripplanner/netex/mapping/TripPatternMapper.java @@ -255,8 +255,10 @@ Optional mapTripPattern(JourneyPattern_VersionStructure .withHopGeometries( serviceLinkMapper.getGeometriesByJourneyPattern(journeyPattern, stopPattern) ) + .withScheduledTimeTableBuilder(builder -> + builder.addAllTripTimes(createTripTimes(trips, tripStopTimes)) + ) .build(); - createTripTimes(trips, tripStopTimes).forEach(tripPattern::add); return Optional.of( new TripPatternMapperResult( diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java index d7098c20661..155394376ae 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Objects; import org.opentripplanner.astar.model.GraphPath; +import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.geometry.GeometryUtils; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.model.GenericLocation; @@ -105,9 +106,16 @@ public Itinerary createItinerary(RaptorPath path) { Leg transitLeg = null; + PathLeg previousLeg = null; while (!pathLeg.isEgressLeg()) { // Map transit leg if (pathLeg.isTransitLeg()) { + if ( + OTPFeature.ExtraTransferLegOnSameStop.isOn() && + isPathTransferAtSameStop(previousLeg, pathLeg) + ) { + legs.add(createTransferLegAtSameStop(previousLeg, pathLeg)); + } transitLeg = mapTransitLeg(transitLeg, pathLeg.asTransitLeg()); legs.add(transitLeg); } @@ -123,6 +131,7 @@ else if (pathLeg.isTransferLeg()) { } } + previousLeg = pathLeg; pathLeg = pathLeg.nextLeg(); } @@ -164,6 +173,19 @@ else if (pathLeg.isTransferLeg()) { return itinerary; } + private static boolean isPathTransferAtSameStop( + PathLeg previousLeg, + PathLeg currentLeg + ) { + return ( + previousLeg != null && + previousLeg.isTransitLeg() && + currentLeg.isTransitLeg() && + !previousLeg.asTransitLeg().isStaySeatedOntoNextLeg() && + (previousLeg.asTransitLeg().toStop() == currentLeg.asTransitLeg().fromStop()) + ); + } + private List mapAccessLeg(AccessPathLeg accessPathLeg) { if (accessPathLeg.access().isFree()) { return List.of(); @@ -264,6 +286,27 @@ private boolean isFree(EgressPathLeg egressPathLeg) { return egressPathLeg.egress().isFree(); } + /** + * If a routing result transfers at the very same stop, RAPTOR doesn't add a path leg. However, + * sometimes we want to create a zero distance leg so a UI can show a transfer. Since it would + * be considered backwards-incompatible, this is an opt-in feature. + */ + private Leg createTransferLegAtSameStop(PathLeg previousLeg, PathLeg nextLeg) { + var transferStop = Place.forStop(transitLayer.getStopByIndex(previousLeg.toStop())); + return StreetLeg + .create() + .withMode(TraverseMode.WALK) + .withStartTime(createZonedDateTime(previousLeg.toTime())) + .withEndTime(createZonedDateTime(nextLeg.fromTime())) + .withFrom(transferStop) + .withTo(transferStop) + .withDistanceMeters(0) + .withGeneralizedCost(0) + .withGeometry(GeometryUtils.makeLineString(transferStop.coordinate, transferStop.coordinate)) + .withWalkSteps(List.of()) + .build(); + } + private List mapTransferLeg(TransferPathLeg pathLeg, TraverseMode transferMode) { var transferFromStop = transitLayer.getStopByIndex(pathLeg.fromStop()); var transferToStop = transitLayer.getStopByIndex(pathLeg.toStop()); 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 d375b4c546c..13f8facef3d 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 @@ -7,15 +7,12 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; 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; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitTuningParameters; @@ -26,7 +23,6 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.RaptorRequestTransferCache; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.StopTransferPriority; -import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.StopModel; import org.opentripplanner.transit.service.TransitModel; @@ -64,16 +60,6 @@ public static TransitLayer map( return new TransitLayerMapper(transitModel).map(tuningParameters); } - // TODO We could save time by either pre-sorting these, or by using a sorting algorithm that is - // optimized for sorting nearly-sorted lists. - static List getSortedTripTimes(Timetable timetable) { - return timetable - .getTripTimes() - .stream() - .sorted(Comparator.comparing(TripTimes::sortIndex)) - .collect(Collectors.toList()); - } - private TransitLayer map(TransitTuningParameters tuningParameters) { HashMap> tripPatternsByStopByDate; List> transferByStopIndex; diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapper.java index cd00b9356dc..256a268e2c7 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapper.java @@ -3,12 +3,9 @@ import gnu.trove.set.TIntSet; import java.time.LocalDate; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.opentripplanner.model.Timetable; @@ -33,8 +30,6 @@ public class TripPatternForDateMapper { private static final Logger LOG = LoggerFactory.getLogger(TripPatternForDateMapper.class); - private final ConcurrentMap> sortedTripTimesForTimetable = new ConcurrentHashMap<>(); - private final Map serviceCodesRunningForDate; /** @@ -69,19 +64,7 @@ public TripPatternForDate map(Timetable timetable, LocalDate serviceDate) { List times = new ArrayList<>(); - // The TripTimes are not sorted by departure time in the source timetable because - // OTP1 performs a simple/ linear search. Raptor results depend on trips being - // sorted. We reuse the same timetables many times on different days, so cache the - // sorted versions to avoid repeated compute-intensive sorting. Anecdotally this - // reduces mapping time by more than half, but it is still rather slow. NL Mapping - // takes 32 seconds sorting every timetable, 9 seconds with cached sorting, and 6 - // seconds with no timetable sorting at all. - List sortedTripTimes = sortedTripTimesForTimetable.computeIfAbsent( - timetable, - TransitLayerMapper::getSortedTripTimes - ); - - for (TripTimes tripTimes : sortedTripTimes) { + for (TripTimes tripTimes : timetable.getTripTimes()) { if (!serviceCodesRunning.contains(tripTimes.getServiceCode())) { continue; } diff --git a/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java index 96cd49b72bb..26d56dc0dba 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java @@ -6,20 +6,22 @@ import static org.opentripplanner.inspector.vector.LayerParameters.MIN_ZOOM; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_0; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_5; +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_6; import java.util.Collection; import java.util.List; import java.util.Optional; import javax.annotation.Nullable; import org.opentripplanner.ext.vectortiles.VectorTilesResource; +import org.opentripplanner.ext.vectortiles.VectorTilesResource.LayerType; +import org.opentripplanner.ext.vectortiles.layers.LayerFilters; import org.opentripplanner.inspector.vector.LayerParameters; import org.opentripplanner.standalone.config.framework.json.NodeAdapter; -public class VectorTileConfig - implements VectorTilesResource.LayersParameters { +public class VectorTileConfig implements VectorTilesResource.LayersParameters { public static final VectorTileConfig DEFAULT = new VectorTileConfig(List.of(), null, null); - private final List> layers; + private final List> layers; @Nullable private final String basePath; @@ -28,7 +30,7 @@ public class VectorTileConfig private final String attribution; VectorTileConfig( - Collection> layers, + Collection> layers, @Nullable String basePath, @Nullable String attribution ) { @@ -38,7 +40,7 @@ public class VectorTileConfig } @Override - public List> layers() { + public List> layers() { return layers; } @@ -144,7 +146,18 @@ public static Layer mapLayer(NodeAdapter node) { "The value is a fraction of the tile size. If you are having problem with icons and " + "shapes being clipped at tile edges, then increase this number." ) - .asDouble(EXPANSION_FACTOR) + .asDouble(EXPANSION_FACTOR), + node + .of("filter") + .since(V2_6) + .summary("Reduce the result set of a layer further by a specific filter.") + .description( + """ + This is useful for when the schema of a layer, say stops, should remain unchanged but some + elements should not be included in the result. + """ + ) + .asEnum(LayerFilters.FilterType.NONE) ); } @@ -155,7 +168,8 @@ record Layer( int maxZoom, int minZoom, int cacheMaxSeconds, - double expansionFactor + double expansionFactor, + LayerFilters.FilterType filterType ) implements LayerParameters {} } diff --git a/src/main/java/org/opentripplanner/standalone/config/routerequest/ItineraryFiltersConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerequest/ItineraryFiltersConfig.java index 82e2c0839f9..3a58bfe9bcd 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerequest/ItineraryFiltersConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerequest/ItineraryFiltersConfig.java @@ -11,13 +11,9 @@ import org.opentripplanner.routing.api.request.preference.ItineraryFilterDebugProfile; import org.opentripplanner.routing.api.request.preference.ItineraryFilterPreferences; import org.opentripplanner.standalone.config.framework.json.NodeAdapter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class ItineraryFiltersConfig { - private static final Logger LOG = LoggerFactory.getLogger(ItineraryFiltersConfig.class); - public static void mapItineraryFilterParams( String parameterName, NodeAdapter root, diff --git a/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java new file mode 100644 index 00000000000..6735dc1db29 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequest.java @@ -0,0 +1,77 @@ +package org.opentripplanner.transit.api.request; + +import java.time.LocalDate; +import java.util.List; +import org.opentripplanner.framework.collection.ListUtils; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.timetable.TripAlteration; + +/* + * A request for trips on a specific service date. + * + * This request is used to retrieve TripsOnServiceDates that match the provided criteria. + * At least one operatingDay must be provided. + */ +public class TripOnServiceDateRequest { + + private final List operatingDays; + private final List authorities; + private final List lines; + private final List serviceJourneys; + private final List replacementFor; + private final List privateCodes; + private final List alterations; + + protected TripOnServiceDateRequest( + List operatingDays, + List authorities, + List lines, + List serviceJourneys, + List replacementFor, + List privateCodes, + List alterations + ) { + if (operatingDays == null || operatingDays.isEmpty()) { + throw new IllegalArgumentException("operatingDays must have at least one date"); + } + this.operatingDays = ListUtils.nullSafeImmutableList(operatingDays); + this.authorities = ListUtils.nullSafeImmutableList(authorities); + this.lines = ListUtils.nullSafeImmutableList(lines); + this.serviceJourneys = ListUtils.nullSafeImmutableList(serviceJourneys); + this.replacementFor = ListUtils.nullSafeImmutableList(replacementFor); + this.privateCodes = ListUtils.nullSafeImmutableList(privateCodes); + this.alterations = ListUtils.nullSafeImmutableList(alterations); + } + + public static TripOnServiceDateRequestBuilder of() { + return new TripOnServiceDateRequestBuilder(); + } + + public List authorities() { + return authorities; + } + + public List lines() { + return lines; + } + + public List serviceJourneys() { + return serviceJourneys; + } + + public List replacementFor() { + return replacementFor; + } + + public List privateCodes() { + return privateCodes; + } + + public List alterations() { + return alterations; + } + + public List operatingDays() { + return operatingDays; + } +} diff --git a/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java new file mode 100644 index 00000000000..7aa2644fdc9 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/api/request/TripOnServiceDateRequestBuilder.java @@ -0,0 +1,66 @@ +package org.opentripplanner.transit.api.request; + +import java.time.LocalDate; +import java.util.List; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.timetable.TripAlteration; + +public class TripOnServiceDateRequestBuilder { + + private List authorities; + private List lines; + private List serviceJourneys; + private List replacementFor; + private List privateCodes; + private List alterations; + private List operatingDays; + + protected TripOnServiceDateRequestBuilder() {} + + public TripOnServiceDateRequestBuilder withOperatingDays(List operatingDays) { + this.operatingDays = operatingDays; + return this; + } + + public TripOnServiceDateRequestBuilder withAuthorities(List authorities) { + this.authorities = authorities; + return this; + } + + public TripOnServiceDateRequestBuilder withLines(List lines) { + this.lines = lines; + return this; + } + + public TripOnServiceDateRequestBuilder withServiceJourneys(List serviceJourneys) { + this.serviceJourneys = serviceJourneys; + return this; + } + + public TripOnServiceDateRequestBuilder withReplacementFor(List replacementFor) { + this.replacementFor = replacementFor; + return this; + } + + public TripOnServiceDateRequestBuilder withPrivateCodes(List privateCodes) { + this.privateCodes = privateCodes; + return this; + } + + public TripOnServiceDateRequestBuilder withAlterations(List alterations) { + this.alterations = alterations; + return this; + } + + public TripOnServiceDateRequest build() { + return new TripOnServiceDateRequest( + operatingDays, + authorities, + lines, + serviceJourneys, + replacementFor, + privateCodes, + alterations + ); + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/expr/AndMatcher.java b/src/main/java/org/opentripplanner/transit/model/filter/expr/AndMatcher.java new file mode 100644 index 00000000000..74f38efa8b7 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/AndMatcher.java @@ -0,0 +1,43 @@ +package org.opentripplanner.transit.model.filter.expr; + +import static org.opentripplanner.transit.model.filter.expr.BinaryOperator.AND; + +import java.util.List; + +/** + * Takes a list of matchers and provides a single interface. All matchers in the list must match for + * the composite matcher to return a match. + * + * @param The entity type the AndMatcher matches. + */ +public final class AndMatcher implements Matcher { + + private final Matcher[] matchers; + + private AndMatcher(List> matchers) { + this.matchers = matchers.toArray(Matcher[]::new); + } + + public static Matcher of(List> matchers) { + // simplify a list of one element + if (matchers.size() == 1) { + return matchers.get(0); + } + return new AndMatcher<>(matchers); + } + + @Override + public boolean match(T entity) { + for (var m : matchers) { + if (!m.match(entity)) { + return false; + } + } + return true; + } + + @Override + public String toString() { + return "(" + AND.arrayToString(matchers) + ')'; + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/expr/BinaryOperator.java b/src/main/java/org/opentripplanner/transit/model/filter/expr/BinaryOperator.java new file mode 100644 index 00000000000..62f3fa30f27 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/BinaryOperator.java @@ -0,0 +1,37 @@ +package org.opentripplanner.transit.model.filter.expr; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Used to concatenate matches with either the logical "AND" or "OR" operator. + */ +enum BinaryOperator { + AND("&"), + OR("|"); + + private final String token; + + BinaryOperator(String token) { + this.token = token; + } + + @Override + public String toString() { + return token; + } + + String arrayToString(T[] values) { + return colToString(Arrays.asList(values)); + } + + String colToString(Collection values) { + return values.stream().map(Objects::toString).collect(Collectors.joining(" " + token + " ")); + } + + String toString(T a, T b) { + return a.toString() + " " + token + " " + b.toString(); + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcher.java b/src/main/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcher.java new file mode 100644 index 00000000000..ed3731897ec --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcher.java @@ -0,0 +1,55 @@ +package org.opentripplanner.transit.model.filter.expr; + +import java.util.function.Function; + +/** + * A matcher that applies a provided matcher to an iterable of child entities returned from the main + * entity that this matcher is for. + *

+ * If any of the iterable entities match the valueMatcher, then the match method returns true. In + * this way it is similar to an OR. + *

+ * @param The main entity type this matcher is applied to. + * @param The type of the child entities, for which there is a mapping from S to T. + */ +public class ContainsMatcher implements Matcher { + + private final String relationshipName; + private final Function> valuesProvider; + private final Matcher valueMatcher; + + /** + * @param relationshipName The name of the type of relationship between the main entity and the + * entity matched by the valueMatcher. + * @param valuesProvider The function that maps the entity being matched by this matcher (S) to + * the iterable of items being matched by valueMatcher. + * @param valueMatcher The matcher that is applied each of the iterable entities returned from the + * valuesProvider function. + */ + public ContainsMatcher( + String relationshipName, + Function> valuesProvider, + Matcher valueMatcher + ) { + this.relationshipName = relationshipName; + this.valuesProvider = valuesProvider; + this.valueMatcher = valueMatcher; + } + + public boolean match(S entity) { + if (valuesProvider.apply(entity) == null) { + return false; + } + for (T it : valuesProvider.apply(entity)) { + if (valueMatcher.match(it)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return "ContainsMatcher: " + relationshipName + ": " + valueMatcher.toString(); + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcher.java b/src/main/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcher.java new file mode 100644 index 00000000000..1380131e07a --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcher.java @@ -0,0 +1,40 @@ +package org.opentripplanner.transit.model.filter.expr; + +import java.util.function.Function; + +/** + * A matcher that checks if a value is equal to another value derived from the matched entities. + *

+ * The derived entity value is provided by a function that takes the entity being matched as an argument. + *

+ * @param The type of the entity being matched. + * @param The type of the value that the matcher will test equality for. + */ +public class EqualityMatcher implements Matcher { + + private final String typeName; + private final V value; + private final Function valueProvider; + + /** + * @param typeName The typeName appears in the toString for easier debugging. + * @param value The value that this matcher will check equality for. + * @param valueProvider The function that maps the entity being matched by this matcher (T) to + * the value being matched by this matcher. + */ + public EqualityMatcher(String typeName, V value, Function valueProvider) { + this.typeName = typeName; + this.value = value; + this.valueProvider = valueProvider; + } + + @Override + public boolean match(T entity) { + return value.equals(valueProvider.apply(entity)); + } + + @Override + public String toString() { + return typeName + "==" + value; + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/expr/ExpressionBuilder.java b/src/main/java/org/opentripplanner/transit/model/filter/expr/ExpressionBuilder.java new file mode 100644 index 00000000000..b1b4d5be322 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/ExpressionBuilder.java @@ -0,0 +1,37 @@ +package org.opentripplanner.transit.model.filter.expr; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +/** + * A builder for creating complex matchers composed of other matchers. + *

+ * This builder contains convenience methods for creating complex matchers from simpler ones. The + * resulting matcher "ands" together all the matchers it has built up. This supports the common + * pattern of narrowing results with multiple filters. + * + * @param The type of entity to match in the expression. + */ +public class ExpressionBuilder { + + private final List> matchers = new ArrayList<>(); + + public static ExpressionBuilder of() { + return new ExpressionBuilder<>(); + } + + public ExpressionBuilder or(Collection values, Function> valueProvider) { + if (values.isEmpty()) { + return this; + } + + matchers.add(OrMatcher.of(values.stream().map(valueProvider).toList())); + return this; + } + + public Matcher build() { + return AndMatcher.of(matchers); + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/expr/Matcher.java b/src/main/java/org/opentripplanner/transit/model/filter/expr/Matcher.java new file mode 100644 index 00000000000..db3c02296b9 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/Matcher.java @@ -0,0 +1,19 @@ +package org.opentripplanner.transit.model.filter.expr; + +/** + * Generic matcher interface - this is the root of the matcher type hierarchy. + *

+ * @param Domain type to match. + */ +@FunctionalInterface +public interface Matcher { + boolean match(T entity); + + static Matcher everything() { + return e -> true; + } + + static Matcher nothing() { + return e -> false; + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/expr/OrMatcher.java b/src/main/java/org/opentripplanner/transit/model/filter/expr/OrMatcher.java new file mode 100644 index 00000000000..62da7af63f4 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/expr/OrMatcher.java @@ -0,0 +1,58 @@ +package org.opentripplanner.transit.model.filter.expr; + +import static org.opentripplanner.transit.model.filter.expr.BinaryOperator.OR; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Takes a list of matchers and provides a single interface. At least one of the matchers in the + * list must match for the composite matcher to return a match. + *

+ * @param The entity type the OrMatcher matches. + */ +public final class OrMatcher implements Matcher { + + private final Matcher[] matchers; + + private OrMatcher(List> matchers) { + this.matchers = matchers.toArray(Matcher[]::new); + } + + public static Matcher of(Matcher a, Matcher b) { + return of(List.of(a, b)); + } + + public static Matcher of(List> matchers) { + // Simplify if there is just one matcher in the list + if (matchers.size() == 1) { + return matchers.get(0); + } + // Collapse nested or matchers + var expr = new ArrayList>(); + for (Matcher it : matchers) { + if (it instanceof OrMatcher orMatcher) { + expr.addAll(Arrays.asList(orMatcher.matchers)); + } else { + expr.add(it); + } + } + return new OrMatcher<>(expr); + } + + @Override + public boolean match(T entity) { + for (var m : matchers) { + if (m.match(entity)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return "(" + OR.arrayToString(matchers) + ')'; + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java b/src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java new file mode 100644 index 00000000000..f86e7a1ff77 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactory.java @@ -0,0 +1,70 @@ +package org.opentripplanner.transit.model.filter.transit; + +import java.time.LocalDate; +import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; +import org.opentripplanner.transit.model.filter.expr.ContainsMatcher; +import org.opentripplanner.transit.model.filter.expr.EqualityMatcher; +import org.opentripplanner.transit.model.filter.expr.ExpressionBuilder; +import org.opentripplanner.transit.model.filter.expr.Matcher; +import org.opentripplanner.transit.model.framework.AbstractTransitEntity; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.timetable.TripAlteration; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; + +/** + * A factory for creating matchers for TripOnServiceDate objects. + *

+ * This factory is used to create matchers for TripOnServiceDate objects based on a request. The + * resulting matcher can be used to filter a list of TripOnServiceDate objects. + */ +public class TripOnServiceDateMatcherFactory { + + public static Matcher of(TripOnServiceDateRequest request) { + ExpressionBuilder expr = ExpressionBuilder.of(); + + expr.or(request.operatingDays(), TripOnServiceDateMatcherFactory::operatingDay); + expr.or(request.authorities(), TripOnServiceDateMatcherFactory::authorityId); + expr.or(request.lines(), TripOnServiceDateMatcherFactory::routeId); + expr.or(request.serviceJourneys(), TripOnServiceDateMatcherFactory::serviceJourneyId); + expr.or(request.replacementFor(), TripOnServiceDateMatcherFactory::replacementFor); + expr.or(request.privateCodes(), TripOnServiceDateMatcherFactory::privateCode); + expr.or(request.alterations(), TripOnServiceDateMatcherFactory::alteration); + return expr.build(); + } + + static Matcher authorityId(FeedScopedId id) { + return new EqualityMatcher<>("agency", id, t -> t.getTrip().getRoute().getAgency().getId()); + } + + static Matcher routeId(FeedScopedId id) { + return new EqualityMatcher<>("route", id, t -> t.getTrip().getRoute().getId()); + } + + static Matcher serviceJourneyId(FeedScopedId id) { + return new EqualityMatcher<>("serviceJourney", id, t -> t.getTrip().getId()); + } + + static Matcher replacementFor(FeedScopedId id) { + return new ContainsMatcher<>( + "replacementForContains", + t -> t.getReplacementFor().stream().map(AbstractTransitEntity::getId).toList(), + new EqualityMatcher<>("replacementForIdEquals", id, (idToMatch -> idToMatch)) + ); + } + + static Matcher privateCode(String code) { + return new EqualityMatcher<>( + "privateCode", + code, + t -> t.getTrip().getNetexInternalPlanningCode() + ); + } + + static Matcher operatingDay(LocalDate date) { + return new EqualityMatcher<>("operatingDay", date, TripOnServiceDate::getServiceDate); + } + + static Matcher alteration(TripAlteration alteration) { + return new EqualityMatcher<>("alteration", alteration, TripOnServiceDate::getTripAlteration); + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java index 57c71d06113..e7d7af45b6d 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java @@ -1,14 +1,12 @@ package org.opentripplanner.transit.model.network; import static java.util.Objects.requireNonNull; -import static java.util.Objects.requireNonNullElseGet; import static org.opentripplanner.framework.lang.ObjectUtils.requireNotInitialized; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; -import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nonnull; @@ -28,7 +26,6 @@ import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Direction; -import org.opentripplanner.transit.model.timetable.FrequencyEntry; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; import org.slf4j.Logger; @@ -122,20 +119,27 @@ public final class TripPattern private final RoutingTripPattern routingTripPattern; - public TripPattern(TripPatternBuilder builder) { + TripPattern(TripPatternBuilder builder) { super(builder.getId()); this.name = builder.getName(); this.route = builder.getRoute(); this.stopPattern = requireNonNull(builder.getStopPattern()); this.createdByRealtimeUpdater = builder.isCreatedByRealtimeUpdate(); - this.mode = requireNonNullElseGet(builder.getMode(), route::getMode); - this.netexSubMode = requireNonNullElseGet(builder.getNetexSubmode(), route::getNetexSubmode); + this.mode = requireNonNull(builder.getMode()); + this.netexSubMode = requireNonNull(builder.getNetexSubmode()); this.containsMultipleModes = builder.getContainsMultipleModes(); - this.scheduledTimetable = - builder.getScheduledTimetable() != null - ? builder.getScheduledTimetable() - : new Timetable(this); + if (builder.getScheduledTimetable() != null) { + if (builder.getScheduledTimetableBuilder() != null) { + throw new IllegalArgumentException( + "Cannot provide both scheduled timetable and scheduled timetable builder" + ); + } + this.scheduledTimetable = builder.getScheduledTimetable(); + } else { + this.scheduledTimetable = + builder.getScheduledTimetableBuilder().withTripPattern(this).build(); + } this.originalTripPattern = builder.getOriginalTripPattern(); @@ -331,56 +335,6 @@ public boolean isBoardAndAlightAt(int stopIndex, PickDrop value) { /* METHODS THAT DELEGATE TO THE SCHEDULED TIMETABLE */ - // TODO OTP2 this method modifies the state, it will be refactored in a subsequent step - /** - * Add the given tripTimes to this pattern's scheduled timetable, recording the corresponding trip - * as one of the scheduled trips on this pattern. - */ - public void add(TripTimes tt) { - // Only scheduled trips (added at graph build time, rather than directly to the timetable - // via updates) are in this list. - scheduledTimetable.addTripTimes(tt); - - // Check that all trips added to this pattern are on the initially declared route. - // Identity equality is valid on GTFS entity objects. - if (this.route != tt.getTrip().getRoute()) { - LOG.warn( - "The trip {} is on route {} but its stop pattern is on route {}.", - tt.getTrip(), - tt.getTrip().getRoute(), - route - ); - } - } - - // TODO OTP2 this method modifies the state, it will be refactored in a subsequent step - /** - * Add the given FrequencyEntry to this pattern's scheduled timetable, recording the corresponding - * trip as one of the scheduled trips on this pattern. - * TODO possible improvements: combine freq entries and TripTimes. Do not keep trips list in TripPattern - * since it is redundant. - */ - public void add(FrequencyEntry freq) { - scheduledTimetable.addFrequencyEntry(freq); - if (this.getRoute() != freq.tripTimes.getTrip().getRoute()) { - LOG.warn( - "The trip {} is on a different route than its stop pattern, which is on {}.", - freq.tripTimes.getTrip(), - route - ); - } - } - - // TODO OTP2 this method modifies the state, it will be refactored in a subsequent step - /** - * Remove all trips matching the given predicate. - * - * @param removeTrip it the predicate returns true - */ - public void removeTrips(Predicate removeTrip) { - scheduledTimetable.getTripTimes().removeIf(tt -> removeTrip.test(tt.getTrip())); - } - /** * Checks that this is TripPattern is based of the provided TripPattern and contains same stops * (but not necessarily with same pickup and dropoff values). @@ -394,7 +348,11 @@ public boolean isModifiedFromTripPatternWithEqualStops(TripPattern other) { } /** - * The direction for all the trips in this pattern. + * Return the direction for all the trips in this pattern. + * By construction, all trips in a pattern have the same direction: + * - trips derived from NeTEx data belong to a ServiceJourney that belongs to a JourneyPattern + * that belongs to a NeTEx Route that specifies a single direction. + * - trips derived from GTFS data are grouped by direction in a trip pattern, during graph build. */ public Direction getDirection() { return scheduledTimetable.getDirection(); diff --git a/src/main/java/org/opentripplanner/transit/model/network/TripPatternBuilder.java b/src/main/java/org/opentripplanner/transit/model/network/TripPatternBuilder.java index f34d206922b..ec2451ea66d 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/TripPatternBuilder.java +++ b/src/main/java/org/opentripplanner/transit/model/network/TripPatternBuilder.java @@ -1,18 +1,23 @@ package org.opentripplanner.transit.model.network; +import static java.util.Objects.requireNonNullElseGet; + import java.util.ArrayList; import java.util.List; +import java.util.function.UnaryOperator; import java.util.stream.IntStream; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.LineString; import org.opentripplanner.framework.geometry.CompactLineStringUtils; import org.opentripplanner.framework.geometry.GeometryUtils; import org.opentripplanner.model.Timetable; +import org.opentripplanner.model.TimetableBuilder; import org.opentripplanner.routing.algorithm.raptoradapter.api.SlackProvider; import org.opentripplanner.transit.model.basic.SubMode; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.AbstractEntityBuilder; import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.timetable.Direction; @SuppressWarnings("UnusedReturnValue") public final class TripPatternBuilder @@ -24,6 +29,7 @@ public final class TripPatternBuilder private boolean containsMultipleModes; private StopPattern stopPattern; private Timetable scheduledTimetable; + private TimetableBuilder scheduledTimetableBuilder; private String name; private boolean createdByRealtimeUpdate; @@ -33,6 +39,7 @@ public final class TripPatternBuilder TripPatternBuilder(FeedScopedId id) { super(id); + this.scheduledTimetableBuilder = Timetable.of(); } TripPatternBuilder(TripPattern original) { @@ -86,10 +93,28 @@ public TripPatternBuilder withStopPattern(StopPattern stopPattern) { } public TripPatternBuilder withScheduledTimeTable(Timetable scheduledTimetable) { + if (scheduledTimetableBuilder != null) { + throw new IllegalStateException( + "Cannot set scheduled Timetable after scheduled Timetable builder is created" + ); + } this.scheduledTimetable = scheduledTimetable; return this; } + public TripPatternBuilder withScheduledTimeTableBuilder( + UnaryOperator producer + ) { + // create a builder for the scheduled timetable only if it needs to be modified. + // otherwise reuse the existing timetable + if (scheduledTimetableBuilder == null) { + scheduledTimetableBuilder = scheduledTimetable.copyOf(); + scheduledTimetable = null; + } + producer.apply(scheduledTimetableBuilder); + return this; + } + public TripPatternBuilder withCreatedByRealtimeUpdater(boolean createdByRealtimeUpdate) { this.createdByRealtimeUpdate = createdByRealtimeUpdate; return this; @@ -115,6 +140,13 @@ public int transitReluctanceFactorIndex() { return route.getMode().ordinal(); } + public Direction getDirection() { + if (scheduledTimetable != null) { + return scheduledTimetable.getDirection(); + } + return scheduledTimetableBuilder.getDirection(); + } + @Override protected TripPattern buildFromValues() { return new TripPattern(this); @@ -125,11 +157,11 @@ public Route getRoute() { } public TransitMode getMode() { - return mode; + return mode != null ? mode : route.getMode(); } public SubMode getNetexSubmode() { - return netexSubMode; + return netexSubMode != null ? netexSubMode : route.getNetexSubmode(); } public boolean getContainsMultipleModes() { @@ -144,6 +176,10 @@ public Timetable getScheduledTimetable() { return scheduledTimetable; } + public TimetableBuilder getScheduledTimetableBuilder() { + return scheduledTimetableBuilder; + } + public String getName() { return name; } diff --git a/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java b/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java index 8fa18443bab..1738c66bab0 100644 --- a/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java @@ -35,8 +35,11 @@ import org.opentripplanner.routing.services.TransitAlertService; import org.opentripplanner.routing.stoptimes.ArrivalDeparture; import org.opentripplanner.routing.stoptimes.StopTimesHelper; +import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; import org.opentripplanner.transit.model.basic.Notice; import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.filter.expr.Matcher; +import org.opentripplanner.transit.model.filter.transit.TripOnServiceDateMatcherFactory; import org.opentripplanner.transit.model.framework.AbstractTransitEntity; import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -591,6 +594,23 @@ public TripOnServiceDate getTripOnServiceDateForTripAndDay( return transitModelIndex.getTripOnServiceDateForTripAndDay().get(tripIdAndServiceDate); } + /** + * Returns a list of TripOnServiceDates that match the filtering defined in the request. + * + * @param request - A TripOnServiceDateRequest object with filtering defined. + * @return - A list of TripOnServiceDates + */ + @Override + public List getTripOnServiceDates(TripOnServiceDateRequest request) { + Matcher matcher = TripOnServiceDateMatcherFactory.of(request); + return transitModelIndex + .getTripOnServiceDateForTripAndDay() + .values() + .stream() + .filter(matcher::match) + .collect(Collectors.toList()); + } + /** * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix * this when doing the issue #3030. diff --git a/src/main/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilter.java b/src/main/java/org/opentripplanner/transit/service/PatternByServiceDatesFilter.java similarity index 79% rename from src/main/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilter.java rename to src/main/java/org/opentripplanner/transit/service/PatternByServiceDatesFilter.java index 8eecfe6273b..21890975acf 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilter.java +++ b/src/main/java/org/opentripplanner/transit/service/PatternByServiceDatesFilter.java @@ -1,16 +1,13 @@ -package org.opentripplanner.apis.gtfs; +package org.opentripplanner.transit.service; import java.time.LocalDate; import java.util.Collection; import java.util.Objects; import java.util.function.Function; -import java.util.stream.Stream; -import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; import org.opentripplanner.apis.gtfs.model.LocalDateRange; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.service.TransitService; /** * Encapsulates the logic to filter patterns by the service dates that they operate on. It also @@ -29,7 +26,7 @@ public class PatternByServiceDatesFilter { * This method is not private to enable unit testing. *

*/ - PatternByServiceDatesFilter( + public PatternByServiceDatesFilter( LocalDateRange range, Function> getPatternsForRoute, Function> getServiceDatesForTrip @@ -45,17 +42,6 @@ public class PatternByServiceDatesFilter { } } - public PatternByServiceDatesFilter( - GraphQLTypes.GraphQLLocalDateRangeInput filterInput, - TransitService transitService - ) { - this( - new LocalDateRange(filterInput.getGraphQLStart(), filterInput.getGraphQLEnd()), - transitService::getPatternsForRoute, - trip -> transitService.getCalendarService().getServiceDatesForServiceId(trip.getServiceId()) - ); - } - /** * Filter the patterns by the service dates that it operates on. */ @@ -67,8 +53,9 @@ public Collection filterPatterns(Collection tripPatter * Filter the routes by listing all their patterns' service dates and checking if they * operate on the specified dates. */ - public Collection filterRoutes(Stream routeStream) { + public Collection filterRoutes(Collection routeStream) { return routeStream + .stream() .filter(r -> { var patterns = getPatternsForRoute.apply(r); return !this.filterPatterns(patterns).isEmpty(); diff --git a/src/main/java/org/opentripplanner/transit/service/TransitService.java b/src/main/java/org/opentripplanner/transit/service/TransitService.java index 1836b5612d2..1e7c4ff8397 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitService.java @@ -24,6 +24,7 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; import org.opentripplanner.routing.services.TransitAlertService; import org.opentripplanner.routing.stoptimes.ArrivalDeparture; +import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; import org.opentripplanner.transit.model.basic.Notice; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.AbstractTransitEntity; @@ -307,4 +308,12 @@ List stopTimesForPatternAtStop( Set getAllServiceCodes(); Map getServiceCodesRunningForDate(); + + /** + * Returns a list of TripOnServiceDates that match the filtering defined in the request. + * + * @param request - A TripOnServiceDateRequest object with filtering defined. + * @return - A list of TripOnServiceDates + */ + List getTripOnServiceDates(TripOnServiceDateRequest request); } diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index a9a8e9af9ec..e39a212dba5 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -670,6 +670,11 @@ type Leg { """ headsign: String """ + An identifier for the leg, which can be used to re-fetch transit leg information. + Re-fetching fails when the underlying transit data no longer exists. + """ + id: String + """ Interlines with previous leg. This is true when the same vehicle is used for the previous leg as for this leg and passenger can stay inside the vehicle. @@ -1166,6 +1171,11 @@ type QueryType { time: Int! ): Trip """ + Try refetching the current state of a transit leg using its id. + This fails when the underlying transit data (mostly IDs) has changed or are no longer available. + """ + leg(id: String!): Leg + """ Get all places (stops, stations, etc. with coordinates) within the specified radius from a location. The returned type is a Relay connection (see https://facebook.github.io/relay/graphql/connections.htm). The placeAtDistance @@ -2001,7 +2011,16 @@ type Stop implements Node & PlaceInterface { "Identifier of the platform, usually a number. This value is only present for stops that are part of a station" platformCode: String "Routes which pass through this stop" - routes: [Route!] + routes( + """ + Only include routes which are operational on at least one service date specified by this filter. + + **Note**: A service date is a technical term useful for transit planning purposes and might not + correspond to a how a passenger thinks of a calendar date. For example, a night bus running + on Sunday morning at 1am to 3am, might have the previous Saturday's service date. + """ + serviceDates: LocalDateRangeInput + ): [Route!] "Returns timetable of the specified pattern at this stop" stopTimesForPattern( "Id of the pattern" diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index 0d2bf71dcc6..a3d0c013955 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -325,7 +325,7 @@ type Leg { fromPlace: Place! "Generalized cost or weight of the leg. Used for debugging." generalizedCost: Int - "An identifier for the leg, which can be used to re-fetch the information." + "An identifier for the leg, which can be used to re-fetch transit leg information." id: ID interchangeFrom: Interchange interchangeTo: Interchange @@ -647,7 +647,7 @@ type QueryType { groupOfLines(id: String!): GroupOfLines "Get all groups of lines" groupsOfLines: [GroupOfLines!]! - "Refetch a single leg based on its id" + "Refetch a single transit leg based on its id" leg(id: ID!): Leg @timingData "Get a single line based on its id" line(id: ID!): Line @timingData @@ -730,7 +730,7 @@ type QueryType { ): quayAtDistanceConnection @timingData "Get default routing parameters." routingParameters: RoutingParameters @timingData - "Get OTP server information" + "Get OTP deployment information. This is only useful for developers of OTP itself not regular API users." serverInfo: ServerInfo! @timingData "Get a single service journey based on its id" serviceJourney(id: String!): ServiceJourney @timingData @@ -1067,6 +1067,11 @@ type RoutingParameters { wheelChairAccessible: Boolean } +""" +Information about the deployment. This is only useful to developers of OTP itself. +It is not recommended for regular API consumers to use this type as it has no +stability guarantees. +""" type ServerInfo { "The 'configVersion' of the build-config.json file." buildConfigVersion: String @@ -1075,6 +1080,12 @@ type ServerInfo { gitBranch: String gitCommit: String gitCommitTime: String + """ + The internal time zone of the transit data. + + Note: The input data can be in several time zones, but OTP internally operates on a single one. + """ + internalTransitModelTimeZone: String "The 'configVersion' of the otp-config.json file." otpConfigVersion: String "The otp-serialization-version-id used to check graphs for compatibility with current version of OTP." diff --git a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index 79590ca2775..7cd9a0910b1 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -28,6 +28,7 @@ import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.stream.Stream; import javax.annotation.Nonnull; import org.glassfish.jersey.message.internal.OutboundJaxrsResponse; @@ -86,6 +87,7 @@ import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.BikeAccess; +import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.site.RegularStop; @@ -114,6 +116,7 @@ class GraphQLIntegrationTest { .of(A, B, C, D, E, F, G, H) .map(p -> (RegularStop) p.stop) .toList(); + private static final Route ROUTE = TransitModelForTest.route("a-route").build(); private static VehicleRentalStation VEHICLE_RENTAL_STATION = new TestVehicleRentalStationBuilder() .withVehicles(10) @@ -160,11 +163,13 @@ static void setup() { var model = stopModel.build(); var transitModel = new TransitModel(model, DEDUPLICATOR); - final TripPattern pattern = TEST_MODEL.pattern(BUS).build(); var trip = TransitModelForTest.trip("123").withHeadsign(I18NString.of("Trip Headsign")).build(); var stopTimes = TEST_MODEL.stopTimesEvery5Minutes(3, trip, T11_00); var tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, DEDUPLICATOR); - pattern.add(tripTimes); + final TripPattern pattern = TEST_MODEL + .pattern(BUS) + .withScheduledTimeTableBuilder(builder -> builder.addTripTimes(tripTimes)) + .build(); transitModel.addTripPattern(id("pattern-1"), pattern); @@ -209,6 +214,11 @@ public List getModesOfStopLocation(StopLocation stop) { public TransitAlertService getTransitAlertService() { return alertService; } + + @Override + public Set getRoutesForStop(StopLocation stop) { + return Set.of(ROUTE); + } }; routes.forEach(transitService::addRoutes); diff --git a/src/test/java/org/opentripplanner/framework/collection/CollectionUtilsTest.java b/src/test/java/org/opentripplanner/framework/collection/CollectionUtilsTest.java index ae33e493bcc..e2eaab520bc 100644 --- a/src/test/java/org/opentripplanner/framework/collection/CollectionUtilsTest.java +++ b/src/test/java/org/opentripplanner/framework/collection/CollectionUtilsTest.java @@ -1,6 +1,8 @@ package org.opentripplanner.framework.collection; 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 com.google.type.Month; import java.time.Duration; @@ -15,6 +17,13 @@ class CollectionUtilsTest { public static final String NULL_STRING = ""; + @Test + void testIsEmpty() { + assertTrue(CollectionUtils.isEmpty(null)); + assertTrue(CollectionUtils.isEmpty(List.of())); + assertFalse(CollectionUtils.isEmpty(List.of(1))); + } + @Test void testToString() { assertEquals("", CollectionUtils.toString(null, NULL_STRING)); diff --git a/src/test/java/org/opentripplanner/generate/doc/framework/DocsTestConstants.java b/src/test/java/org/opentripplanner/generate/doc/framework/DocsTestConstants.java index a7f7afa806a..cc306c133ac 100644 --- a/src/test/java/org/opentripplanner/generate/doc/framework/DocsTestConstants.java +++ b/src/test/java/org/opentripplanner/generate/doc/framework/DocsTestConstants.java @@ -14,19 +14,20 @@ public interface DocsTestConstants { File USER_DOC_PATH = new File(DOC_ROOT, "user"); /** - * This method return {@code true} if the /docs directory is available. If not, a warning is + * This method return {@code true} if both the /doc/user and /doc/templates directories are available. If not, a warning is * logged and the method returns {@code false}. This is used by the {@link GeneratesDocumentation} * annotation. */ static boolean docsExistOrWarn() { - if (USER_DOC_PATH.exists()) { + if (USER_DOC_PATH.exists() && TEMPLATE_PATH.exists()) { return true; } + LOG.warn( """ - SKIP TEST - '/docs' NOT FOUND + SKIP TEST - '/doc/user' or '/doc/templates' NOT FOUND - The doc/templates directory might not be available if you run the tests outside the + The /doc/user and /doc/templates directories might not be available if you run the tests outside the root of the projects. This may happen if the project root is not the working directory, if you run tests using jar files or in a Maven multi-module project. diff --git a/src/test/java/org/opentripplanner/generate/doc/framework/GeneratesDocumentation.java b/src/test/java/org/opentripplanner/generate/doc/framework/GeneratesDocumentation.java index d8ce7a529d2..2dc099b2a0b 100644 --- a/src/test/java/org/opentripplanner/generate/doc/framework/GeneratesDocumentation.java +++ b/src/test/java/org/opentripplanner/generate/doc/framework/GeneratesDocumentation.java @@ -8,7 +8,7 @@ import org.junit.jupiter.api.condition.EnabledIf; /** - * Use this annotation on tests that generate(access) the /docs directory outside the + * Use this annotation on tests that generate(access) the /doc directory outside the * source/resource. *

* All tests annotated with this annotation is tagged with "docs". You may include or exclude diff --git a/src/test/java/org/opentripplanner/gtfs/GenerateTripPatternsOperationTest.java b/src/test/java/org/opentripplanner/gtfs/GenerateTripPatternsOperationTest.java new file mode 100644 index 00000000000..74819deabc2 --- /dev/null +++ b/src/test/java/org/opentripplanner/gtfs/GenerateTripPatternsOperationTest.java @@ -0,0 +1,305 @@ +package org.opentripplanner.gtfs; + +import static org.opentripplanner.transit.model._data.TransitModelForTest.trip; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +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; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.graph_builder.issue.service.DefaultDataImportIssueStore; +import org.opentripplanner.graph_builder.issues.TripDegenerate; +import org.opentripplanner.graph_builder.issues.TripUndefinedService; +import org.opentripplanner.graph_builder.module.geometry.GeometryProcessor; +import org.opentripplanner.model.StopTime; +import org.opentripplanner.model.impl.OtpTransitServiceBuilder; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.framework.Deduplicator; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.timetable.Direction; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.service.StopModel; + +class GenerateTripPatternsOperationTest { + + private static StopModel stopModel; + private static RegularStop stopA; + private static RegularStop stopB; + private static RegularStop stopC; + private static Trip trip1; + private static Trip trip2; + private static Trip trip3; + private static Trip trip4; + private static Trip trip5; + private static StopTime stopTimeA; + private static StopTime stopTimeB; + private static StopTime stopTimeC; + + private Deduplicator deduplicator; + private DataImportIssueStore issueStore; + private OtpTransitServiceBuilder transitServiceBuilder; + private GeometryProcessor geometryProcessor; + + @BeforeAll + static void setupClass() { + TransitModelForTest transitModelForTest = TransitModelForTest.of(); + stopA = transitModelForTest.stop("stopA").build(); + stopB = transitModelForTest.stop("stopB").build(); + stopC = transitModelForTest.stop("stopC").build(); + stopModel = + transitModelForTest + .stopModelBuilder() + .withRegularStop(stopA) + .withRegularStop(stopB) + .withRegularStop(stopC) + .build(); + + stopTimeA = new StopTime(); + stopTimeA.setStop(stopA); + stopTimeB = new StopTime(); + stopTimeB.setStop(stopB); + stopTimeC = new StopTime(); + stopTimeC.setStop(stopC); + + FeedScopedId serviceId1 = TransitModelForTest.id("SERVICE_ID_1"); + trip1 = + trip("TRIP_ID_1") + .withServiceId(serviceId1) + .withMode(TransitMode.RAIL) + .withNetexSubmode("SUBMODE_1") + .withDirection(Direction.INBOUND) + .build(); + + // same route, mode, submode and direction as trip1 + FeedScopedId serviceId2 = TransitModelForTest.id("SERVICE_ID_2"); + trip2 = + trip("TRIP_ID_2") + .withServiceId(serviceId2) + .withRoute(trip1.getRoute()) + .withMode(trip1.getMode()) + .withNetexSubmode(trip1.getNetexSubMode().name()) + .withDirection(trip1.getDirection()) + .build(); + + // same route, direction as trip1, different mode + FeedScopedId serviceId3 = TransitModelForTest.id("SERVICE_ID_3"); + trip3 = + trip("TRIP_ID_3") + .withServiceId(serviceId3) + .withRoute(trip1.getRoute()) + .withMode(TransitMode.BUS) + .withDirection(trip1.getDirection()) + .build(); + + // same route, mode, direction as trip1, different submode + FeedScopedId serviceId4 = TransitModelForTest.id("SERVICE_ID_4"); + trip4 = + trip("TRIP_ID_4") + .withServiceId(serviceId4) + .withRoute(trip1.getRoute()) + .withMode(trip1.getMode()) + .withNetexSubmode("SUMODE_2") + .withDirection(trip1.getDirection()) + .build(); + + // same route, mode as trip1, different direction + FeedScopedId serviceId5 = TransitModelForTest.id("SERVICE_ID_5"); + trip5 = + trip("TRIP_ID_5") + .withServiceId(serviceId5) + .withRoute(trip1.getRoute()) + .withMode(trip1.getMode()) + .withNetexSubmode(trip1.getNetexSubMode().name()) + .withDirection(Direction.OUTBOUND) + .build(); + } + + @BeforeEach + void setup() { + deduplicator = new Deduplicator(); + issueStore = new DefaultDataImportIssueStore(); + transitServiceBuilder = new OtpTransitServiceBuilder(stopModel, issueStore); + double maxStopToShapeSnapDistance = 100; + geometryProcessor = + new GeometryProcessor(transitServiceBuilder, maxStopToShapeSnapDistance, issueStore); + } + + @Test + void testGenerateTripPatternsNoTrip() { + Set calendarServiceIds = Set.of(); + GenerateTripPatternsOperation generateTripPatternsOperation = new GenerateTripPatternsOperation( + transitServiceBuilder, + issueStore, + deduplicator, + calendarServiceIds, + geometryProcessor + ); + generateTripPatternsOperation.run(); + + Assertions.assertTrue(transitServiceBuilder.getTripPatterns().isEmpty()); + Assertions.assertTrue(issueStore.listIssues().isEmpty()); + } + + @Test + void testGenerateTripPatternsTripWithUndefinedService() { + transitServiceBuilder.getTripsById().computeIfAbsent(trip1.getId(), feedScopedId -> trip1); + Set calendarServiceIds = Set.of(); + + GenerateTripPatternsOperation generateTripPatternsOperation = new GenerateTripPatternsOperation( + transitServiceBuilder, + issueStore, + deduplicator, + calendarServiceIds, + geometryProcessor + ); + generateTripPatternsOperation.run(); + + Assertions.assertTrue(transitServiceBuilder.getTripPatterns().isEmpty()); + Assertions.assertFalse(issueStore.listIssues().isEmpty()); + Assertions.assertInstanceOf(TripUndefinedService.class, issueStore.listIssues().getFirst()); + } + + @Test + void testGenerateTripPatternsDegeneratedTrip() { + transitServiceBuilder.getTripsById().computeIfAbsent(trip1.getId(), feedScopedId -> trip1); + Set calendarServiceIds = Set.of(trip1.getServiceId()); + + GenerateTripPatternsOperation generateTripPatternsOperation = new GenerateTripPatternsOperation( + transitServiceBuilder, + issueStore, + deduplicator, + calendarServiceIds, + geometryProcessor + ); + generateTripPatternsOperation.run(); + + Assertions.assertTrue(transitServiceBuilder.getTripPatterns().isEmpty()); + Assertions.assertFalse(issueStore.listIssues().isEmpty()); + Assertions.assertInstanceOf(TripDegenerate.class, issueStore.listIssues().getFirst()); + } + + @Test + void testGenerateTripPatterns() { + transitServiceBuilder.getTripsById().computeIfAbsent(trip1.getId(), feedScopedId -> trip1); + Collection stopTimes = List.of(stopTimeA, stopTimeB); + transitServiceBuilder.getStopTimesSortedByTrip().put(trip1, stopTimes); + Set calendarServiceIds = Set.of(trip1.getServiceId()); + + GenerateTripPatternsOperation generateTripPatternsOperation = new GenerateTripPatternsOperation( + transitServiceBuilder, + issueStore, + deduplicator, + calendarServiceIds, + geometryProcessor + ); + generateTripPatternsOperation.run(); + + Assertions.assertEquals(1, transitServiceBuilder.getTripPatterns().size()); + Assertions.assertTrue(issueStore.listIssues().isEmpty()); + } + + @Test + void testGenerateTripPatterns2TripsSameStops() { + transitServiceBuilder.getTripsById().computeIfAbsent(trip1.getId(), feedScopedId -> trip1); + transitServiceBuilder.getTripsById().computeIfAbsent(trip2.getId(), feedScopedId -> trip2); + Collection stopTimes = List.of(stopTimeA, stopTimeB); + + transitServiceBuilder.getStopTimesSortedByTrip().put(trip1, stopTimes); + transitServiceBuilder.getStopTimesSortedByTrip().put(trip2, stopTimes); + + Set calendarServiceIds = Set.of(trip1.getServiceId(), trip2.getServiceId()); + + GenerateTripPatternsOperation generateTripPatternsOperation = new GenerateTripPatternsOperation( + transitServiceBuilder, + issueStore, + deduplicator, + calendarServiceIds, + geometryProcessor + ); + generateTripPatternsOperation.run(); + + Assertions.assertEquals(1, transitServiceBuilder.getTripPatterns().size()); + Assertions.assertEquals( + 2, + transitServiceBuilder + .getTripPatterns() + .values() + .stream() + .findFirst() + .orElseThrow() + .getScheduledTimetable() + .getTripTimes() + .size() + ); + Assertions.assertTrue(issueStore.listIssues().isEmpty()); + } + + @Test + void testGenerateTripPatterns2TripsDifferentStops() { + transitServiceBuilder.getTripsById().computeIfAbsent(trip1.getId(), feedScopedId -> trip1); + transitServiceBuilder.getTripsById().computeIfAbsent(trip2.getId(), feedScopedId -> trip2); + Collection stopTimesTrip1 = List.of(stopTimeA, stopTimeB); + Collection stopTimesTrip2 = List.of(stopTimeA, stopTimeC); + + transitServiceBuilder.getStopTimesSortedByTrip().put(trip1, stopTimesTrip1); + transitServiceBuilder.getStopTimesSortedByTrip().put(trip2, stopTimesTrip2); + + Set calendarServiceIds = Set.of(trip1.getServiceId(), trip2.getServiceId()); + + GenerateTripPatternsOperation generateTripPatternsOperation = new GenerateTripPatternsOperation( + transitServiceBuilder, + issueStore, + deduplicator, + calendarServiceIds, + geometryProcessor + ); + generateTripPatternsOperation.run(); + + Assertions.assertEquals(2, transitServiceBuilder.getTripPatterns().size()); + Assertions.assertTrue(issueStore.listIssues().isEmpty()); + } + + static List testCases() { + return List.of( + // trips with different modes + Arguments.of(trip1, trip3), + // trips with different sub-modes + Arguments.of(trip1, trip4), + // trips with different directions + Arguments.of(trip1, trip5) + ); + } + + @ParameterizedTest + @MethodSource("testCases") + void testGenerateDifferentTripPatterns(Trip t1, Trip t2) { + transitServiceBuilder.getTripsById().computeIfAbsent(t1.getId(), feedScopedId -> t1); + transitServiceBuilder.getTripsById().computeIfAbsent(t2.getId(), feedScopedId -> t2); + Collection stopTimes = List.of(stopTimeA, stopTimeB); + + transitServiceBuilder.getStopTimesSortedByTrip().put(t1, stopTimes); + transitServiceBuilder.getStopTimesSortedByTrip().put(t2, stopTimes); + + Set calendarServiceIds = Set.of(t1.getServiceId(), t2.getServiceId()); + + GenerateTripPatternsOperation generateTripPatternsOperation = new GenerateTripPatternsOperation( + transitServiceBuilder, + issueStore, + deduplicator, + calendarServiceIds, + geometryProcessor + ); + generateTripPatternsOperation.run(); + + Assertions.assertEquals(2, transitServiceBuilder.getTripPatterns().size()); + Assertions.assertTrue(issueStore.listIssues().isEmpty()); + } +} diff --git a/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java b/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java index a1aa4f9d753..e14a33cd276 100644 --- a/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java +++ b/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java @@ -163,13 +163,12 @@ private static TripPattern tripPattern(String tripId, String blockId, String ser ); var stopPattern = new StopPattern(stopTimes); - var tp = TripPattern + var tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); + return TripPattern .of(TransitModelForTest.id(tripId)) .withRoute(trip.getRoute()) .withStopPattern(stopPattern) + .withScheduledTimeTableBuilder(builder -> builder.addTripTimes(tripTimes)) .build(); - var tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); - tp.add(tripTimes); - return tp; } } diff --git a/src/test/java/org/opentripplanner/gtfs/mapping/StopAreaMapperTest.java b/src/test/java/org/opentripplanner/gtfs/mapping/StopAreaMapperTest.java deleted file mode 100644 index d58bcfcdd31..00000000000 --- a/src/test/java/org/opentripplanner/gtfs/mapping/StopAreaMapperTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.opentripplanner.gtfs.mapping; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.opentripplanner.graph_builder.issue.api.DataImportIssueStore.NOOP; - -import java.util.Set; -import java.util.stream.Collectors; -import javax.annotation.Nonnull; -import org.junit.jupiter.api.Test; -import org.onebusaway.gtfs.model.AgencyAndId; -import org.onebusaway.gtfs.model.Area; -import org.onebusaway.gtfs.model.Location; -import org.onebusaway.gtfs.model.Stop; -import org.onebusaway.gtfs.model.StopArea; -import org.opentripplanner._support.geometry.Polygons; -import org.opentripplanner.transit.service.StopModel; - -class StopAreaMapperTest { - - private static final String NAME = "Floxjam"; - private static final AgencyAndId AREA_ID = agencyAndId("flox"); - - @Test - void map() { - var stopModel = StopModel.of(); - var stopMapper = new StopMapper(new TranslationHelper(), ignored -> null, stopModel); - var locationMapper = new LocationMapper(stopModel, NOOP); - var mapper = new StopAreaMapper(stopMapper, locationMapper, stopModel); - - var area = new Area(); - area.setId(AREA_ID); - area.setName(NAME); - - var stop1 = stop("stop1"); - var stop2 = stop("stop2"); - var location = location("location"); - - var stopArea = new StopArea(); - stopArea.setArea(area); - stopArea.addLocation(stop1); - stopArea.addLocation(stop2); - stopArea.addLocation(location); - var areaStop = mapper.map(stopArea); - - assertEquals(NAME, areaStop.getName().toString()); - var stopIds = areaStop - .getChildLocations() - .stream() - .map(l -> l.getId().toString()) - .collect(Collectors.toSet()); - assertEquals(Set.of("1:location", "1:stop1", "1:stop2"), stopIds); - } - - private static Stop stop(String id) { - var stop = new Stop(); - stop.setId(agencyAndId(id)); - stop.setLat(1); - stop.setLon(2); - stop.setName("A stop"); - return stop; - } - - private static Location location(String id) { - var stop = new Location(); - stop.setId(agencyAndId(id)); - stop.setName("A stop"); - stop.setGeometry(Polygons.toGeoJson(Polygons.BERLIN)); - return stop; - } - - @Nonnull - private static AgencyAndId agencyAndId(String id) { - return new AgencyAndId("1", id); - } -} diff --git a/src/test/java/org/opentripplanner/gtfs/mapping/StopTimeMapperTest.java b/src/test/java/org/opentripplanner/gtfs/mapping/StopTimeMapperTest.java index e45f7a8ff0d..d17c22c4abe 100644 --- a/src/test/java/org/opentripplanner/gtfs/mapping/StopTimeMapperTest.java +++ b/src/test/java/org/opentripplanner/gtfs/mapping/StopTimeMapperTest.java @@ -85,17 +85,11 @@ public class StopTimeMapperTest { locationMapper, stopModelBuilder ); - private final StopAreaMapper stopAreaMapper = new StopAreaMapper( - stopMapper, - locationMapper, - stopModelBuilder - ); private final TranslationHelper translationHelper = new TranslationHelper(); private final StopTimeMapper subject = new StopTimeMapper( stopMapper, locationMapper, locationGroupMapper, - stopAreaMapper, new TripMapper( new RouteMapper(new AgencyMapper(FEED_ID), ISSUE_STORE, translationHelper), new DirectionMapper(ISSUE_STORE), diff --git a/src/test/java/org/opentripplanner/gtfs/mapping/TransferMapperTest.java b/src/test/java/org/opentripplanner/gtfs/mapping/TransferMapperTest.java index 3581cca9ac8..3c5a1eff42a 100644 --- a/src/test/java/org/opentripplanner/gtfs/mapping/TransferMapperTest.java +++ b/src/test/java/org/opentripplanner/gtfs/mapping/TransferMapperTest.java @@ -56,11 +56,6 @@ public class TransferMapperTest { LOCATION_MAPPER, STOP_MODEL_BUILDER ); - private static final StopAreaMapper STOP_AREA_MAPPER = new StopAreaMapper( - STOP_MAPPER, - LOCATION_MAPPER, - STOP_MODEL_BUILDER - ); private static StopTimeMapper STOP_TIME_MAPPER; private static final Integer ID = 45; @@ -99,7 +94,6 @@ void prepare() { STOP_MAPPER, LOCATION_MAPPER, LOCATION_GROUP_MAPPER, - STOP_AREA_MAPPER, new TripMapper( new RouteMapper(new AgencyMapper(FEED_ID), ISSUE_STORE, TRANSLATION_HELPER), new DirectionMapper(ISSUE_STORE), diff --git a/src/test/java/org/opentripplanner/model/TimetableSnapshotTest.java b/src/test/java/org/opentripplanner/model/TimetableSnapshotTest.java index 0a737630a5b..ffdfc027b76 100644 --- a/src/test/java/org/opentripplanner/model/TimetableSnapshotTest.java +++ b/src/test/java/org/opentripplanner/model/TimetableSnapshotTest.java @@ -2,6 +2,7 @@ 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.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; @@ -52,9 +53,9 @@ public static void setUp() throws Exception { @Test public void testCompare() { - Timetable orig = new Timetable(null); - Timetable a = new Timetable(orig, LocalDate.now(timeZone).minusDays(1)); - Timetable b = new Timetable(orig, LocalDate.now(timeZone)); + Timetable orig = Timetable.of().build(); + Timetable a = orig.copyOf().withServiceDate(LocalDate.now(timeZone).minusDays(1)).build(); + Timetable b = orig.copyOf().withServiceDate(LocalDate.now(timeZone)).build(); assertTrue(new TimetableSnapshot.SortedTimetableComparator().compare(a, b) < 0); } @@ -106,52 +107,50 @@ public void testResolve() { @Test public void testUpdate() { - assertThrows( - ConcurrentModificationException.class, - () -> { - LocalDate today = LocalDate.now(timeZone); - LocalDate yesterday = today.minusDays(1); - TripPattern pattern = patternIndex.get(new FeedScopedId(feedId, "1.1")); + LocalDate today = LocalDate.now(timeZone); + LocalDate yesterday = today.minusDays(1); + TripPattern pattern = patternIndex.get(new FeedScopedId(feedId, "1.1")); - TimetableSnapshot resolver = new TimetableSnapshot(); - Timetable origNow = resolver.resolve(pattern, today); + TimetableSnapshot resolver = new TimetableSnapshot(); + Timetable origNow = resolver.resolve(pattern, today); - TripDescriptor.Builder tripDescriptorBuilder = TripDescriptor.newBuilder(); + TripDescriptor.Builder tripDescriptorBuilder = TripDescriptor.newBuilder(); - tripDescriptorBuilder.setTripId("1.1"); - tripDescriptorBuilder.setScheduleRelationship(ScheduleRelationship.SCHEDULED); + tripDescriptorBuilder.setTripId("1.1"); + tripDescriptorBuilder.setScheduleRelationship(ScheduleRelationship.SCHEDULED); - TripUpdate.Builder tripUpdateBuilder = TripUpdate.newBuilder(); + TripUpdate.Builder tripUpdateBuilder = TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); + tripUpdateBuilder.setTrip(tripDescriptorBuilder); - var stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(2); - stopTimeUpdateBuilder.setScheduleRelationship( - TripUpdate.StopTimeUpdate.ScheduleRelationship.SCHEDULED - ); - stopTimeUpdateBuilder.setDeparture( - TripUpdate.StopTimeEvent.newBuilder().setDelay(5).build() - ); + var stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); + stopTimeUpdateBuilder.setStopSequence(2); + stopTimeUpdateBuilder.setScheduleRelationship( + TripUpdate.StopTimeUpdate.ScheduleRelationship.SCHEDULED + ); + stopTimeUpdateBuilder.setDeparture(TripUpdate.StopTimeEvent.newBuilder().setDelay(5).build()); - TripUpdate tripUpdate = tripUpdateBuilder.build(); + TripUpdate tripUpdate = tripUpdateBuilder.build(); - // new timetable for today - updateResolver(resolver, pattern, tripUpdate, today); - Timetable updatedNow = resolver.resolve(pattern, today); - assertNotSame(origNow, updatedNow); + // new timetable for today + updateResolver(resolver, pattern, tripUpdate, today); + Timetable updatedNow = resolver.resolve(pattern, today); + assertNotSame(origNow, updatedNow); - // reuse timetable for today - updateResolver(resolver, pattern, tripUpdate, today); - assertEquals(updatedNow, resolver.resolve(pattern, today)); + // a new timetable instance is created for today + updateResolver(resolver, pattern, tripUpdate, today); + assertNotEquals(updatedNow, resolver.resolve(pattern, today)); - // create new timetable for tomorrow - updateResolver(resolver, pattern, tripUpdate, yesterday); - assertNotSame(origNow, resolver.resolve(pattern, yesterday)); - assertNotSame(updatedNow, resolver.resolve(pattern, yesterday)); + // create new timetable for tomorrow + updateResolver(resolver, pattern, tripUpdate, yesterday); + assertNotSame(origNow, resolver.resolve(pattern, yesterday)); + assertNotSame(updatedNow, resolver.resolve(pattern, yesterday)); - // exception if we try to modify a snapshot - TimetableSnapshot snapshot = resolver.commit(); + // exception if we try to modify a snapshot + TimetableSnapshot snapshot = resolver.commit(); + assertThrows( + ConcurrentModificationException.class, + () -> { updateResolver(snapshot, pattern, tripUpdate, yesterday); } ); diff --git a/src/test/java/org/opentripplanner/model/TimetableTest.java b/src/test/java/org/opentripplanner/model/TimetableTest.java index 9e6a7467dc5..9135abd96ee 100644 --- a/src/test/java/org/opentripplanner/model/TimetableTest.java +++ b/src/test/java/org/opentripplanner/model/TimetableTest.java @@ -190,7 +190,7 @@ public void update() { result.ifSuccess(p -> { var updatedTripTimes = p.getTripTimes(); assertNotNull(updatedTripTimes); - timetable.setTripTimes(trip_1_1_index, updatedTripTimes); + timetable = timetable.copyOf().addOrUpdateTripTimes(updatedTripTimes).build(); assertEquals(20 * 60 + 120, timetable.getTripTimes(trip_1_1_index).getArrivalTime(2)); }); @@ -217,7 +217,7 @@ public void update() { result.ifSuccess(p -> { var updatedTripTimes = p.getTripTimes(); assertNotNull(updatedTripTimes); - timetable.setTripTimes(trip_1_1_index, updatedTripTimes); + timetable = timetable.copyOf().addOrUpdateTripTimes(updatedTripTimes).build(); }); // update trip arrival time only @@ -246,7 +246,7 @@ public void update() { result.ifSuccess(p -> { var updatedTripTimes = p.getTripTimes(); assertNotNull(updatedTripTimes); - timetable.setTripTimes(trip_1_1_index, updatedTripTimes); + timetable = timetable.copyOf().addOrUpdateTripTimes(updatedTripTimes).build(); }); // update trip departure time only @@ -273,7 +273,7 @@ public void update() { result.ifSuccess(p -> { var updatedTripTimes = p.getTripTimes(); assertNotNull(updatedTripTimes); - timetable.setTripTimes(trip_1_1_index, updatedTripTimes); + timetable = timetable.copyOf().addOrUpdateTripTimes(updatedTripTimes).build(); }); // update trip using stop id @@ -299,7 +299,7 @@ public void update() { result.ifSuccess(p -> { var updatedTripTimes = p.getTripTimes(); assertNotNull(updatedTripTimes); - timetable.setTripTimes(trip_1_1_index, updatedTripTimes); + timetable = timetable.copyOf().addOrUpdateTripTimes(updatedTripTimes).build(); }); } diff --git a/src/test/java/org/opentripplanner/model/impl/OtpTransitServiceBuilderLimitPeriodTest.java b/src/test/java/org/opentripplanner/model/impl/OtpTransitServiceBuilderLimitPeriodTest.java index 5d1fef21271..27e6e56c645 100644 --- a/src/test/java/org/opentripplanner/model/impl/OtpTransitServiceBuilderLimitPeriodTest.java +++ b/src/test/java/org/opentripplanner/model/impl/OtpTransitServiceBuilderLimitPeriodTest.java @@ -1,6 +1,7 @@ package org.opentripplanner.model.impl; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.LocalDate; @@ -22,6 +23,7 @@ import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.network.TripPatternBuilder; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.timetable.Direction; import org.opentripplanner.transit.model.timetable.Trip; @@ -141,16 +143,27 @@ public void testLimitPeriod() { assertTrue(patterns.contains(patternInT1), patterns.toString()); assertTrue(patterns.contains(patternInT2), patterns.toString()); - // Verify trips in pattern (one trip is removed from patternInT1) - assertEquals(1, patternInT1.scheduledTripsAsStream().count()); - assertEquals(tripCSIn, patternInT1.scheduledTripsAsStream().findFirst().orElseThrow()); - - // Verify trips in pattern is unchanged (one trip) + // Verify patternInT1 is replaced by a copy that contains one less trip + TripPattern copyOfTripPattern1 = subject + .getTripPatterns() + .values() + .stream() + .filter(p -> p.getId().equals(patternInT1.getId())) + .findFirst() + .orElseThrow(); + assertNotSame(patternInT1, copyOfTripPattern1); + assertEquals(1, copyOfTripPattern1.scheduledTripsAsStream().count()); + assertEquals(tripCSIn, copyOfTripPattern1.scheduledTripsAsStream().findFirst().orElseThrow()); + + // Verify trips in patternInT2 is unchanged (one trip) assertEquals(1, patternInT2.scheduledTripsAsStream().count()); - // Verify scheduledTimetable trips (one trip is removed from patternInT1) - assertEquals(1, patternInT1.getScheduledTimetable().getTripTimes().size()); - assertEquals(tripCSIn, patternInT1.getScheduledTimetable().getTripTimes().get(0).getTrip()); + // Verify scheduledTimetable trips (one trip is removed from the copy of patternInT1) + assertEquals(1, copyOfTripPattern1.getScheduledTimetable().getTripTimes().size()); + assertEquals( + tripCSIn, + copyOfTripPattern1.getScheduledTimetable().getTripTimes().get(0).getTrip() + ); // Verify scheduledTimetable trips in pattern is unchanged (one trip) assertEquals(1, patternInT2.getScheduledTimetable().getTripTimes().size()); @@ -186,16 +199,17 @@ private TripPattern createTripPattern(Collection trips) { FeedScopedId patternId = TransitModelForTest.id( trips.stream().map(t -> t.getId().getId()).collect(Collectors.joining(":")) ); - TripPattern p = TripPattern + TripPatternBuilder tpb = TripPattern .of(patternId) .withRoute(route) - .withStopPattern(STOP_PATTERN) - .build(); + .withStopPattern(STOP_PATTERN); for (Trip trip : trips) { - p.add(TripTimesFactory.tripTimes(trip, STOP_TIMES, DEDUPLICATOR)); + tpb.withScheduledTimeTableBuilder(builder -> + builder.addTripTimes(TripTimesFactory.tripTimes(trip, STOP_TIMES, DEDUPLICATOR)) + ); } - return p; + return tpb.build(); } private Trip createTrip(String id, FeedScopedId serviceId) { diff --git a/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java b/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java index edaafabd753..ec04d9f6d96 100644 --- a/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java +++ b/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java @@ -482,13 +482,13 @@ public TestItineraryBuilder transit( stopTimes.add(toStopTime); StopPattern stopPattern = new StopPattern(stopTimes); + final TripTimes tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); TripPattern tripPattern = TripPattern .of(route.getId()) .withRoute(route) .withStopPattern(stopPattern) + .withScheduledTimeTableBuilder(builder -> builder.addTripTimes(tripTimes)) .build(); - final TripTimes tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); - tripPattern.add(tripTimes); ScheduledTransitLeg leg; 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 480639e5bda..315b12dfefc 100644 --- a/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java +++ b/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java @@ -11,7 +11,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; -import org.opentripplanner.model.Timetable; import org.opentripplanner.model.calendar.CalendarServiceData; import org.opentripplanner.model.plan.PlanTestConstants; import org.opentripplanner.model.plan.ScheduledTransitLeg; @@ -54,23 +53,23 @@ static void buildTransitService() { stop4 = TEST_MODEL.stop("STOP4", 0, 0).withParentStation(parentStation).build(); // build transit data + Trip trip = TransitModelForTest.trip("1").build(); + var tripTimes = TripTimesFactory.tripTimes( + trip, + TEST_MODEL.stopTimesEvery5Minutes(5, trip, PlanTestConstants.T11_00), + new Deduplicator() + ); + tripTimes.setServiceCode(SERVICE_CODE); TripPattern tripPattern = TransitModelForTest .tripPattern("1", TransitModelForTest.route(id("1")).build()) .withStopPattern(TransitModelForTest.stopPattern(stop1, stop2, stop3)) + .withScheduledTimeTableBuilder(builder -> builder.addTripTimes(tripTimes)) .build(); - Timetable timetable = tripPattern.getScheduledTimetable(); - Trip trip = TransitModelForTest.trip("1").build(); + tripId = trip.getId(); stopIdAtPosition0 = tripPattern.getStop(0).getId(); stopIdAtPosition1 = tripPattern.getStop(1).getId(); stopIdAtPosition2 = tripPattern.getStop(2).getId(); - var tripTimes = TripTimesFactory.tripTimes( - trip, - TEST_MODEL.stopTimesEvery5Minutes(5, trip, PlanTestConstants.T11_00), - new Deduplicator() - ); - tripTimes.setServiceCode(SERVICE_CODE); - timetable.addTripTimes(tripTimes); // build transit model StopModel stopModel = TEST_MODEL diff --git a/src/test/java/org/opentripplanner/netex/NetexEpipBundleSmokeTest.java b/src/test/java/org/opentripplanner/netex/NetexEpipBundleSmokeTest.java index b89e6883951..72e750c055f 100644 --- a/src/test/java/org/opentripplanner/netex/NetexEpipBundleSmokeTest.java +++ b/src/test/java/org/opentripplanner/netex/NetexEpipBundleSmokeTest.java @@ -84,7 +84,7 @@ private static FeedScopedId fId(String id) { private void assertAgencies(Collection agencies) { assertEquals(3, agencies.size()); - Agency a = list(agencies).get(0); + Agency a = list(agencies).getFirst(); assertEquals("DE::Authority:41::", a.getId().getId()); assertEquals("HOCHBAHN, Bus", a.getName()); assertNull(a.getUrl()); @@ -146,7 +146,7 @@ private void assertTripPatterns(Collection patterns) { p.getStops().toString() ); List trips = p.scheduledTripsAsStream().toList(); - assertEquals("Trip{HH:DE::ServiceJourney:36439031_0:: X86}", trips.get(0).toString()); + assertEquals("Trip{HH:DE::ServiceJourney:36439062_0:: X86}", trips.getFirst().toString()); assertEquals(55, trips.size()); assertEquals(4, patterns.size()); } @@ -176,12 +176,12 @@ private void assertServiceIds(Collection trips, Collection s private void assetServiceCalendar(CalendarServiceData cal) { ArrayList sIds = new ArrayList<>(cal.getServiceIds()); assertEquals(2, sIds.size()); - FeedScopedId serviceId1 = sIds.get(0); + FeedScopedId serviceId1 = sIds.getFirst(); List dates = cal.getServiceDatesForServiceId(serviceId1); - assertEquals("2023-02-02", dates.get(0).toString()); - assertEquals("2023-12-08", dates.get(dates.size() - 1).toString()); + assertEquals("2023-02-02", dates.getFirst().toString()); + assertEquals("2023-12-08", dates.getLast().toString()); assertEquals(214, dates.size()); } } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java b/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java index 8f5d1a0208e..7b877abcf3a 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java @@ -1,5 +1,6 @@ package org.opentripplanner.routing.algorithm.mapping; +import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -11,6 +12,7 @@ import java.time.Month; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -18,11 +20,15 @@ import org.opentripplanner._support.time.ZoneIds; import org.opentripplanner.ext.flex.FlexAccessEgress; import org.opentripplanner.ext.flex.FlexPathDurations; +import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.model.Cost; import org.opentripplanner.framework.model.TimeAndCost; import org.opentripplanner.framework.time.TimeUtils; import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.StopTime; +import org.opentripplanner.model.plan.Leg; +import org.opentripplanner.model.plan.ScheduledTransitLeg; +import org.opentripplanner.model.plan.StreetLeg; import org.opentripplanner.raptor._data.api.TestPathBuilder; import org.opentripplanner.raptor._data.transit.TestAccessEgress; import org.opentripplanner.raptor._data.transit.TestRoute; @@ -51,12 +57,10 @@ import org.opentripplanner.street.search.state.State; import org.opentripplanner.street.search.state.TestStateBuilder; import org.opentripplanner.transit.model._data.TransitModelForTest; -import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo; import org.opentripplanner.transit.service.DefaultTransitService; @@ -73,8 +77,7 @@ public class RaptorPathToItineraryMapperTest { private static final int TRANSIT_START = TimeUtils.time("10:00"); private static final int TRANSIT_END = TimeUtils.time("11:00"); - - private final TestTransitData data = new TestTransitData(); + private static final Route ROUTE = TransitModelForTest.route("route").build(); public static final RaptorCostCalculator COST_CALCULATOR = new DefaultCostCalculator<>( BOARD_COST_SEC, @@ -84,9 +87,9 @@ public class RaptorPathToItineraryMapperTest { STOP_COSTS ); - private static final RegularStop S1 = TEST_MODEL.stop("STOP1", 0.0, 0.0).build(); - - private static final RegularStop S2 = TEST_MODEL.stop("STOP2", 1.0, 1.0).build(); + private static final RegularStop S1 = TEST_MODEL.stop("STOP1").build(); + private static final RegularStop S2 = TEST_MODEL.stop("STOP2").build(); + private static final RegularStop S3 = TEST_MODEL.stop("STOP3").build(); @ParameterizedTest @ValueSource(ints = { 0, 3000, -3000 }) @@ -113,6 +116,35 @@ void createItineraryTestZeroDurationEgress(int lastLegCost) { ); } + @Test + void noExtraLegWhenTransferringAtSameStop() { + var mapper = getRaptorPathToItineraryMapper(); + + var path = transferAtSameStopPath(); + var itinerary = mapper.createItinerary(path); + assertThat(itinerary.getLegs().stream().map(Object::getClass)).doesNotContain(StreetLeg.class); + } + + @Test + void extraLegWhenTransferringAtSameStop() { + RaptorPathToItineraryMapper mapper = getRaptorPathToItineraryMapper(); + + var schedule = getTestTripSchedule2(); + var path = new TestPathBuilder(COST_CALCULATOR) + .access(TRANSIT_START - BOARD_SLACK, 1) + .bus(schedule, 2) + .bus(schedule, 1) + .egress(TestAccessEgress.free(1, RaptorCostConverter.toRaptorCost(100))); + + OTPFeature.ExtraTransferLegOnSameStop.testOn(() -> { + var itinerary = mapper.createItinerary(path); + assertEquals( + List.of(ScheduledTransitLeg.class, StreetLeg.class, ScheduledTransitLeg.class), + itinerary.getLegs().stream().map(Leg::getClass).toList() + ); + }); + } + @Test @Disabled("Need to write a general test framework to enable this.") void penalty() { @@ -184,6 +216,36 @@ void createItineraryWithOnBoardFlexAccess() { assertEquals(3, itinerary.getLegs().size(), "The wrong number of legs was returned"); } + private RaptorPath transferAtSameStopPath() { + var schedule = transferAtSameStopSchedule(); + return new TestPathBuilder(COST_CALCULATOR) + .access(TRANSIT_START, 1) + .bus(schedule, 2) + .bus(schedule, 1) + .egress(TestAccessEgress.free(1, RaptorCostConverter.toRaptorCost(100))); + } + + private TestTripSchedule transferAtSameStopSchedule() { + TestTransitData data = new TestTransitData(); + var pattern = TestTripPattern.pattern("TestPattern", 1, 2, 3, 2, 1).withRoute(ROUTE); + + var timetable = new TestTripSchedule.Builder() + .times( + TimeUtils.time("10:00"), + TimeUtils.time("10:05"), + TimeUtils.time("10:10"), + TimeUtils.time("10:15"), + TimeUtils.time("10:20") + ) + .pattern(pattern) + .originalPattern(getOriginalPattern(pattern)) + .build(); + + data.withRoutes(TestRoute.route("TransferAtSameStop", 1, 2, 3, 2, 1).withTimetable(timetable)); + + return data.getRoute(0).getTripSchedule(0); + } + private TripPattern getOriginalPattern(TestTripPattern pattern) { var stopModelBuilder = TEST_MODEL.stopModelBuilder(); ArrayList stopTimes = new ArrayList<>(); @@ -233,7 +295,12 @@ private static TransitLayer getTransitLayer() { new HashMap<>(), null, null, - TEST_MODEL.stopModelBuilder().withRegularStop(S1).withRegularStop(S2).build(), + TEST_MODEL + .stopModelBuilder() + .withRegularStop(S1) + .withRegularStop(S2) + .withRegularStop(S3) + .build(), null, null, null, @@ -241,21 +308,30 @@ private static TransitLayer getTransitLayer() { ); } - private TestTripSchedule getTestTripSchedule() { - var agency = Agency - .of(new FeedScopedId("TestFeed", "Auth_1")) - .withName("Test_Agency") - .withTimezone("Europe/Stockholm") - .build(); + private TestTripSchedule getTestTripSchedule2() { + TestTransitData data = new TestTransitData(); + var pattern = TestTripPattern.pattern("TestPattern", 1, 2, 3, 2, 1).withRoute(ROUTE); - var route = Route - .of(new FeedScopedId("TestFeed", "Line_1")) - .withAgency(agency) - .withMode(TransitMode.BUS) - .withShortName("Test_Bus") + var timetable = new TestTripSchedule.Builder() + .times( + TimeUtils.time("10:00"), + TimeUtils.time("10:05"), + TimeUtils.time("10:10"), + TimeUtils.time("10:15"), + TimeUtils.time("10:20") + ) + .pattern(pattern) + .originalPattern(getOriginalPattern(pattern)) .build(); - var pattern = TestTripPattern.pattern("TestPattern", 1, 2).withRoute(route); + data.withRoutes(TestRoute.route("TestRoute_1", 1, 2, 3, 2, 1).withTimetable(timetable)); + + return data.getRoute(0).getTripSchedule(0); + } + + private TestTripSchedule getTestTripSchedule() { + TestTransitData data = new TestTransitData(); + var pattern = TestTripPattern.pattern("TestPattern", 1, 2).withRoute(ROUTE); var timetable = new TestTripSchedule.Builder() .times(TRANSIT_START, TRANSIT_END) diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapperTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapperTest.java index 020dc6305a4..27f80062555 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapperTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapperTest.java @@ -36,7 +36,6 @@ public class TripPatternForDateMapperTest { @BeforeAll public static void setUp() throws Exception { var pattern = TEST_MODEL.pattern(BUS).build(); - timetable = new Timetable(pattern); var trip = TransitModelForTest.trip("1").build(); var tripTimes = TripTimesFactory.tripTimes( trip, @@ -44,7 +43,7 @@ public static void setUp() throws Exception { new Deduplicator() ); tripTimes.setServiceCode(SERVICE_CODE); - timetable.addTripTimes(tripTimes); + timetable = Timetable.of().withTripPattern(pattern).addTripTimes(tripTimes).build(); } /** diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TestRouteData.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TestRouteData.java index f57f03705fd..8648ea20324 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TestRouteData.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TestRouteData.java @@ -62,8 +62,8 @@ public TestRouteData(Route route, List stops, List times) { .of(TransitModelForTest.id("TP:" + route)) .withRoute(this.route) .withStopPattern(new StopPattern(stopTimesFistTrip)) + .withScheduledTimeTableBuilder(builder -> builder.addAllTripTimes(tripTimes)) .build(); - tripTimes.forEach(tripPattern::add); RoutingTripPattern routingTripPattern = tripPattern.getRoutingTripPattern(); diff --git a/src/test/java/org/opentripplanner/routing/stoptimes/StopTimesHelperTest.java b/src/test/java/org/opentripplanner/routing/stoptimes/StopTimesHelperTest.java index 1432c68fd49..61fffd7a905 100644 --- a/src/test/java/org/opentripplanner/routing/stoptimes/StopTimesHelperTest.java +++ b/src/test/java/org/opentripplanner/routing/stoptimes/StopTimesHelperTest.java @@ -33,14 +33,21 @@ public static void setUp() throws Exception { transitService = new DefaultTransitService(transitModel); feedId = transitModel.getFeedIds().iterator().next(); stopId = new FeedScopedId(feedId, "J"); - pattern = - transitService.getPatternForTrip( - transitService.getTripForId(new FeedScopedId(feedId, "5.1")) - ); - var tt = transitService.getTimetableForTripPattern(pattern, LocalDate.now()); + var originalPattern = transitService.getPatternForTrip( + transitService.getTripForId(new FeedScopedId(feedId, "5.1")) + ); + var tt = originalPattern.getScheduledTimetable(); var newTripTimes = tt.getTripTimes(0).copyScheduledTimes(); newTripTimes.cancelTrip(); - tt.setTripTimes(0, newTripTimes); + pattern = + originalPattern + .copy() + .withScheduledTimeTableBuilder(builder -> builder.addOrUpdateTripTimes(newTripTimes)) + .build(); + // replace the original pattern by the updated pattern in the transit model + transitModel.addTripPattern(pattern.getId(), pattern); + transitModel.index(); + transitService = new DefaultTransitService(transitModel); } /** diff --git a/src/test/java/org/opentripplanner/transit/model/_data/PatternTestModel.java b/src/test/java/org/opentripplanner/transit/model/_data/PatternTestModel.java new file mode 100644 index 00000000000..0b149ae4ee4 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/_data/PatternTestModel.java @@ -0,0 +1,45 @@ +package org.opentripplanner.transit.model._data; + +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; + +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.network.StopPattern; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.service.StopModel; + +public class PatternTestModel { + + public static final Route ROUTE_1 = TransitModelForTest.route("1").build(); + + private static final FeedScopedId SERVICE_ID = id("service"); + private static final Trip TRIP = TransitModelForTest + .trip("t1") + .withRoute(ROUTE_1) + .withServiceId(SERVICE_ID) + .build(); + private static final TransitModelForTest MODEL = new TransitModelForTest(StopModel.of()); + private static final RegularStop STOP_1 = MODEL.stop("1").build(); + private static final StopPattern STOP_PATTERN = TransitModelForTest.stopPattern(STOP_1, STOP_1); + + /** + * Creates a trip pattern that has a stop pattern, trip times and a trip with a service id. + */ + public static TripPattern pattern() { + var tt = ScheduledTripTimes + .of() + .withTrip(TRIP) + .withArrivalTimes("10:00 10:05") + .withDepartureTimes("10:00 10:05") + .build(); + + return TransitModelForTest + .tripPattern("1", ROUTE_1) + .withStopPattern(STOP_PATTERN) + .withScheduledTimeTableBuilder(builder -> builder.addTripTimes(tt)) + .build(); + } +} diff --git a/src/test/java/org/opentripplanner/transit/model/filter/expr/AndMatcherTest.java b/src/test/java/org/opentripplanner/transit/model/filter/expr/AndMatcherTest.java new file mode 100644 index 00000000000..c79260193ff --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/filter/expr/AndMatcherTest.java @@ -0,0 +1,39 @@ +package org.opentripplanner.transit.model.filter.expr; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class AndMatcherTest { + + @Test + void testMatchSingleMatcher() { + var matcher = AndMatcher.of(List.of(new EqualityMatcher<>("int", 42, i -> i))); + assertTrue(matcher.match(42)); + assertFalse(matcher.match(43)); + } + + @Test + void testMatchMultiple() { + var matcher = AndMatcher.of( + List.of(new EqualityMatcher<>("int", 42, i -> i), new EqualityMatcher<>("int", 43, i -> i)) + ); + assertFalse(matcher.match(42)); + assertFalse(matcher.match(43)); + assertFalse(matcher.match(44)); + } + + @Test + void testMatchComposites() { + var matcher = AndMatcher.of( + List.of( + OrMatcher.of(List.of(new EqualityMatcher<>("int", 42, i -> i))), + OrMatcher.of(List.of(new EqualityMatcher<>("int", 43, i -> i))) + ) + ); + assertFalse(matcher.match(42)); + assertFalse(matcher.match(43)); + assertFalse(matcher.match(44)); + } +} diff --git a/src/test/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcherTest.java b/src/test/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcherTest.java new file mode 100644 index 00000000000..1709fc7bf86 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/filter/expr/ContainsMatcherTest.java @@ -0,0 +1,33 @@ +package org.opentripplanner.transit.model.filter.expr; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ContainsMatcherTest { + + private static final Map> integerListMap = Map.of( + 1, + List.of("foo"), + 2, + List.of("bar"), + 3, + List.of("foo", "bar") + ); + + @Test + void testMatch() { + var matcher = new ContainsMatcher<>( + "contains", + integerListMap::get, + new EqualityMatcher<>("string", "foo", s -> s) + ); + + assertTrue(matcher.match(1)); + assertFalse(matcher.match(2)); + assertTrue(matcher.match(3)); + assertFalse(matcher.match(4)); + } +} diff --git a/src/test/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcherTest.java b/src/test/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcherTest.java new file mode 100644 index 00000000000..31d208a768a --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/filter/expr/EqualityMatcherTest.java @@ -0,0 +1,22 @@ +package org.opentripplanner.transit.model.filter.expr; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class EqualityMatcherTest { + + @Test + void testMatchesPrimitive() { + var matcher = new EqualityMatcher<>("int", 42, i -> i); + assertTrue(matcher.match(42)); + assertFalse(matcher.match(43)); + } + + @Test + void testMatchesObject() { + var matcher = new EqualityMatcher<>("string", "foo", s -> s); + assertTrue(matcher.match("foo")); + assertFalse(matcher.match("bar")); + } +} diff --git a/src/test/java/org/opentripplanner/transit/model/filter/expr/OrMatcherTest.java b/src/test/java/org/opentripplanner/transit/model/filter/expr/OrMatcherTest.java new file mode 100644 index 00000000000..415f64e40ed --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/filter/expr/OrMatcherTest.java @@ -0,0 +1,31 @@ +package org.opentripplanner.transit.model.filter.expr; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class OrMatcherTest { + + @Test + void testMatch() { + var matcher = OrMatcher.of( + new EqualityMatcher<>("int", 42, i -> i), + new EqualityMatcher<>("int", 43, i -> i) + ); + assertTrue(matcher.match(42)); + assertTrue(matcher.match(43)); + assertFalse(matcher.match(44)); + } + + @Test + void testMatchComposites() { + var matcher = OrMatcher.of( + AndMatcher.of(List.of(new EqualityMatcher<>("int", 42, i -> i))), + AndMatcher.of(List.of(new EqualityMatcher<>("int", 43, i -> i))) + ); + assertTrue(matcher.match(42)); + assertTrue(matcher.match(43)); + assertFalse(matcher.match(44)); + } +} diff --git a/src/test/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactoryTest.java b/src/test/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactoryTest.java new file mode 100644 index 00000000000..b7cb7aa6698 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/filter/transit/TripOnServiceDateMatcherFactoryTest.java @@ -0,0 +1,151 @@ +package org.opentripplanner.transit.model.filter.transit; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.filter.expr.Matcher; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.organization.Agency; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; + +class TripOnServiceDateMatcherFactoryTest { + + private TripOnServiceDate tripOnServiceDateRut; + private TripOnServiceDate tripOnServiceDateRut2; + private TripOnServiceDate tripOnServiceDateAkt; + + @BeforeEach + void setup() { + tripOnServiceDateRut = + TripOnServiceDate + .of(new FeedScopedId("RUT:route:trip:date", "123")) + .withTrip( + Trip + .of(new FeedScopedId("RUT:route:trip", "1")) + .withRoute( + Route + .of(new FeedScopedId("RUT:route", "2")) + .withAgency( + Agency + .of(new FeedScopedId("RUT", "3")) + .withName("RUT") + .withTimezone("Europe/Oslo") + .build() + ) + .withMode(TransitMode.BUS) + .withShortName("BUS") + .build() + ) + .build() + ) + .withServiceDate(LocalDate.of(2024, 2, 22)) + .build(); + + tripOnServiceDateRut2 = + TripOnServiceDate + .of(new FeedScopedId("RUT:route:trip:date", "123")) + .withTrip( + Trip + .of(new FeedScopedId("RUT:route:trip2", "1")) + .withRoute( + Route + .of(new FeedScopedId("RUT:route", "2")) + .withAgency( + Agency + .of(new FeedScopedId("RUT", "3")) + .withName("RUT") + .withTimezone("Europe/Oslo") + .build() + ) + .withMode(TransitMode.BUS) + .withShortName("BUS") + .build() + ) + .build() + ) + .withServiceDate(LocalDate.of(2024, 2, 22)) + .build(); + + tripOnServiceDateAkt = + TripOnServiceDate + .of(new FeedScopedId("AKT:route:trip:date", "123")) + .withTrip( + Trip + .of(new FeedScopedId("AKT:route:trip", "1")) + .withRoute( + Route + .of(new FeedScopedId("AKT:route", "2")) + .withAgency( + Agency + .of(new FeedScopedId("AKT", "3")) + .withName("AKT") + .withTimezone("Europe/Oslo") + .build() + ) + .withMode(TransitMode.BUS) + .withShortName("BUS") + .build() + ) + .build() + ) + .withServiceDate(LocalDate.of(2024, 2, 22)) + .build(); + } + + @Test + void testMatchOperatingDays() { + TripOnServiceDateRequest request = TripOnServiceDateRequest + .of() + .withOperatingDays(List.of(LocalDate.of(2024, 2, 22))) + .build(); + + Matcher matcher = TripOnServiceDateMatcherFactory.of(request); + + assertTrue(matcher.match(tripOnServiceDateRut)); + assertTrue(matcher.match(tripOnServiceDateRut2)); + assertTrue(matcher.match(tripOnServiceDateAkt)); + } + + @Test + void testMatchMultiple() { + TripOnServiceDateRequest request = TripOnServiceDateRequest + .of() + .withOperatingDays(List.of(LocalDate.of(2024, 2, 22))) + .withAuthorities(List.of(new FeedScopedId("RUT", "3"))) + .withLines(List.of(new FeedScopedId("RUT:route", "2"))) + .withServiceJourneys(List.of(new FeedScopedId("RUT:route:trip", "1"))) + .build(); + + Matcher matcher = TripOnServiceDateMatcherFactory.of(request); + + assertTrue(matcher.match(tripOnServiceDateRut)); + assertFalse(matcher.match(tripOnServiceDateRut2)); + assertFalse(matcher.match(tripOnServiceDateAkt)); + } + + @Test + void testMatchMultipleServiceJourneyMatchers() { + TripOnServiceDateRequest request = TripOnServiceDateRequest + .of() + .withOperatingDays(List.of(LocalDate.of(2024, 2, 22))) + .withAuthorities(List.of(new FeedScopedId("RUT", "3"))) + .withLines(List.of(new FeedScopedId("RUT:route", "2"))) + .withServiceJourneys( + List.of(new FeedScopedId("RUT:route:trip", "1"), new FeedScopedId("RUT:route:trip2", "1")) + ) + .build(); + + Matcher matcher = TripOnServiceDateMatcherFactory.of(request); + + assertTrue(matcher.match(tripOnServiceDateRut)); + assertTrue(matcher.match(tripOnServiceDateRut2)); + assertFalse(matcher.match(tripOnServiceDateAkt)); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilterTest.java b/src/test/java/org/opentripplanner/transit/service/PatternByServiceDatesFilterTest.java similarity index 66% rename from src/test/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilterTest.java rename to src/test/java/org/opentripplanner/transit/service/PatternByServiceDatesFilterTest.java index f01bac12006..93f50ce5b1e 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilterTest.java +++ b/src/test/java/org/opentripplanner/transit/service/PatternByServiceDatesFilterTest.java @@ -1,12 +1,12 @@ -package org.opentripplanner.apis.gtfs; +package org.opentripplanner.transit.service; import static java.time.LocalDate.parse; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.opentripplanner.apis.gtfs.PatternByServiceDatesFilterTest.FilterExpectation.NOT_REMOVED; -import static org.opentripplanner.apis.gtfs.PatternByServiceDatesFilterTest.FilterExpectation.REMOVED; -import static org.opentripplanner.transit.model._data.TransitModelForTest.id; +import static org.opentripplanner.transit.model._data.PatternTestModel.ROUTE_1; +import static org.opentripplanner.transit.service.PatternByServiceDatesFilterTest.FilterExpectation.NOT_REMOVED; +import static org.opentripplanner.transit.service.PatternByServiceDatesFilterTest.FilterExpectation.REMOVED; import java.time.LocalDate; import java.util.List; @@ -14,51 +14,18 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.opentripplanner.apis.gtfs.model.LocalDateRange; -import org.opentripplanner.transit.model._data.TransitModelForTest; -import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.network.Route; -import org.opentripplanner.transit.model.network.StopPattern; +import org.opentripplanner.transit.model._data.PatternTestModel; import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.site.RegularStop; -import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; -import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.service.StopModel; class PatternByServiceDatesFilterTest { - private static final Route ROUTE_1 = TransitModelForTest.route("1").build(); - private static final FeedScopedId SERVICE_ID = id("service"); - private static final Trip TRIP = TransitModelForTest - .trip("t1") - .withRoute(ROUTE_1) - .withServiceId(SERVICE_ID) - .build(); - private static final TransitModelForTest MODEL = new TransitModelForTest(StopModel.of()); - private static final RegularStop STOP_1 = MODEL.stop("1").build(); - private static final StopPattern STOP_PATTERN = TransitModelForTest.stopPattern(STOP_1, STOP_1); - private static final TripPattern PATTERN_1 = pattern(); + private static final TripPattern PATTERN_1 = PatternTestModel.pattern(); enum FilterExpectation { REMOVED, NOT_REMOVED, } - private static TripPattern pattern() { - var pattern = TransitModelForTest - .tripPattern("1", ROUTE_1) - .withStopPattern(STOP_PATTERN) - .build(); - - var tt = ScheduledTripTimes - .of() - .withTrip(TRIP) - .withArrivalTimes("10:00 10:05") - .withDepartureTimes("10:00 10:05") - .build(); - pattern.add(tt); - return pattern; - } - static List invalidRangeCases() { return List.of( Arguments.of(null, null), @@ -140,7 +107,7 @@ void filterRoutes(LocalDate start, LocalDate end, FilterExpectation expectation) var filter = defaultFilter(start, end); var filterInput = List.of(ROUTE_1); - var filterOutput = filter.filterRoutes(filterInput.stream()); + var filterOutput = filter.filterRoutes(filterInput); if (expectation == NOT_REMOVED) { assertEquals(filterOutput, filterInput); diff --git a/src/test/java/org/opentripplanner/transit/speed_test/SpeedIntegrationTest.java b/src/test/java/org/opentripplanner/transit/speed_test/SpeedIntegrationTest.java index e5006d949b6..61a23b10ff6 100644 --- a/src/test/java/org/opentripplanner/transit/speed_test/SpeedIntegrationTest.java +++ b/src/test/java/org/opentripplanner/transit/speed_test/SpeedIntegrationTest.java @@ -24,14 +24,14 @@ import org.opentripplanner.transit.speed_test.options.SpeedTestConfig; /** - * This test run the SpeedTest on the Portland dataset. It tests all SpeedTest + * This test runs the SpeedTest on the Portland dataset. It tests all SpeedTest * profiles. This is also a good integration-test for the OTP routing query. */ public class SpeedIntegrationTest { /** - * We need to use a relative path here, because the test will update the results files in case - * the results differ. This make it easy to maintain the test. + * We need to use a relative path here, because the test will update the result files in case + * the results differ. This makes it easy to maintain the test. */ private static final File BASE_DIR = Path.of("src", "test", "resources", "speedtest").toFile(); diff --git a/src/test/java/org/opentripplanner/updater/trip/RealtimeTestConstants.java b/src/test/java/org/opentripplanner/updater/trip/RealtimeTestConstants.java new file mode 100644 index 00000000000..bad0c1982e9 --- /dev/null +++ b/src/test/java/org/opentripplanner/updater/trip/RealtimeTestConstants.java @@ -0,0 +1,48 @@ +package org.opentripplanner.updater.trip; + +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; + +import java.time.LocalDate; +import java.time.ZoneId; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.organization.Operator; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.Station; +import org.opentripplanner.transit.service.StopModel; + +public interface RealtimeTestConstants { + LocalDate SERVICE_DATE = LocalDate.of(2024, 5, 8); + FeedScopedId SERVICE_ID = TransitModelForTest.id("CAL_1"); + String STOP_A1_ID = "A1"; + String STOP_B1_ID = "B1"; + String STOP_C1_ID = "C1"; + String TRIP_1_ID = "TestTrip1"; + String TRIP_2_ID = "TestTrip2"; + String OPERATOR_1_ID = "TestOperator1"; + Operator OPERATOR1 = Operator.of(id(OPERATOR_1_ID)).withName(OPERATOR_1_ID).build(); + String ROUTE_1_ID = "TestRoute1"; + + TransitModelForTest TEST_MODEL = TransitModelForTest.of(); + ZoneId TIME_ZONE = ZoneId.of(TransitModelForTest.TIME_ZONE_ID); + Station STATION_A = TEST_MODEL.station("A").build(); + Station STATION_B = TEST_MODEL.station("B").build(); + Station STATION_C = TEST_MODEL.station("C").build(); + Station STATION_D = TEST_MODEL.station("D").build(); + RegularStop STOP_A1 = TEST_MODEL.stop(STOP_A1_ID).withParentStation(STATION_A).build(); + RegularStop STOP_B1 = TEST_MODEL.stop(STOP_B1_ID).withParentStation(STATION_B).build(); + RegularStop STOP_B2 = TEST_MODEL.stop("B2").withParentStation(STATION_B).build(); + RegularStop STOP_C1 = TEST_MODEL.stop(STOP_C1_ID).withParentStation(STATION_C).build(); + RegularStop STOP_D1 = TEST_MODEL.stop("D1").withParentStation(STATION_D).build(); + StopModel STOP_MODEL = TEST_MODEL + .stopModelBuilder() + .withRegularStop(STOP_A1) + .withRegularStop(STOP_B1) + .withRegularStop(STOP_B2) + .withRegularStop(STOP_C1) + .withRegularStop(STOP_D1) + .build(); + + Route ROUTE_1 = TransitModelForTest.route(ROUTE_1_ID).build(); +} diff --git a/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java b/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java index a40bd8bd797..682bff038b6 100644 --- a/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java +++ b/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java @@ -1,41 +1,25 @@ package org.opentripplanner.updater.trip; +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; import static org.opentripplanner.updater.trip.UpdateIncrementality.DIFFERENTIAL; import static org.opentripplanner.updater.trip.UpdateIncrementality.FULL_DATASET; import com.google.transit.realtime.GtfsRealtime; import java.time.Duration; import java.time.LocalDate; -import java.time.ZoneId; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.IntStream; import org.opentripplanner.DateTimeHelper; import org.opentripplanner.ext.siri.SiriTimetableSnapshotSource; import org.opentripplanner.ext.siri.updater.EstimatedTimetableHandler; -import org.opentripplanner.framework.i18n.I18NString; -import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; -import org.opentripplanner.model.StopTime; import org.opentripplanner.model.TimetableSnapshot; -import org.opentripplanner.model.calendar.CalendarServiceData; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.transit.model._data.TransitModelForTest; -import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.organization.Operator; -import org.opentripplanner.transit.model.site.RegularStop; -import org.opentripplanner.transit.model.site.Station; -import org.opentripplanner.transit.model.site.StopLocation; -import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; -import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.opentripplanner.transit.model.timetable.TripTimesStringBuilder; import org.opentripplanner.transit.service.DefaultTransitService; -import org.opentripplanner.transit.service.StopModel; import org.opentripplanner.transit.service.TransitModel; import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.updater.DefaultRealTimeUpdateContext; @@ -50,47 +34,20 @@ *

* It is however a goal to change that and then these two can be combined. */ -public final class RealtimeTestEnvironment { +public final class RealtimeTestEnvironment implements RealtimeTestConstants { + // static constants private static final TimetableSnapshotSourceParameters PARAMETERS = new TimetableSnapshotSourceParameters( Duration.ZERO, false ); - public static final LocalDate SERVICE_DATE = LocalDate.of(2024, 5, 8); - public static final FeedScopedId SERVICE_ID = TransitModelForTest.id("CAL_1"); - public static final String STOP_A1_ID = "A1"; - public static final String STOP_B1_ID = "B1"; - public static final String STOP_C1_ID = "C1"; - private final TransitModelForTest testModel = TransitModelForTest.of(); - public final ZoneId timeZone = ZoneId.of(TransitModelForTest.TIME_ZONE_ID); - public final Station stationA = testModel.station("A").build(); - public final Station stationB = testModel.station("B").build(); - public final Station stationC = testModel.station("C").build(); - public final Station stationD = testModel.station("D").build(); - public final RegularStop stopA1 = testModel.stop(STOP_A1_ID).withParentStation(stationA).build(); - public final RegularStop stopB1 = testModel.stop(STOP_B1_ID).withParentStation(stationB).build(); - public final RegularStop stopB2 = testModel.stop("B2").withParentStation(stationB).build(); - public final RegularStop stopC1 = testModel.stop(STOP_C1_ID).withParentStation(stationC).build(); - public final RegularStop stopD1 = testModel.stop("D1").withParentStation(stationD).build(); - public final StopModel stopModel = testModel - .stopModelBuilder() - .withRegularStop(stopA1) - .withRegularStop(stopB1) - .withRegularStop(stopB2) - .withRegularStop(stopC1) - .withRegularStop(stopD1) - .build(); - public final FeedScopedId operator1Id = TransitModelForTest.id("TestOperator1"); - public final FeedScopedId route1Id = TransitModelForTest.id("TestRoute1"); - public final Trip trip1; - public final Trip trip2; - public final Operator operator1; + public final TransitModel transitModel; private final SiriTimetableSnapshotSource siriSource; private final TimetableSnapshotSource gtfsSource; private final DateTimeHelper dateTimeHelper; - private enum SourceType { + enum SourceType { GTFS_RT, SIRI, } @@ -98,54 +55,22 @@ private enum SourceType { /** * Siri and GTFS-RT cannot be run at the same time, so you need to decide. */ - public static RealtimeTestEnvironment siri() { - return new RealtimeTestEnvironment(SourceType.SIRI); + public static RealtimeTestEnvironmentBuilder siri() { + return new RealtimeTestEnvironmentBuilder().withSourceType(SourceType.SIRI); } /** * Siri and GTFS-RT cannot be run at the same time, so you need to decide. */ - public static RealtimeTestEnvironment gtfs() { - return new RealtimeTestEnvironment(SourceType.GTFS_RT); + public static RealtimeTestEnvironmentBuilder gtfs() { + return new RealtimeTestEnvironmentBuilder().withSourceType(SourceType.GTFS_RT); } - private RealtimeTestEnvironment(SourceType sourceType) { - transitModel = new TransitModel(stopModel, new Deduplicator()); - transitModel.initTimeZone(timeZone); - transitModel.addAgency(TransitModelForTest.AGENCY); - - operator1 = Operator.of(operator1Id).withName("Operator 1").build(); - transitModel.getOperators().add(operator1); - - Route route1 = TransitModelForTest.route(route1Id).withOperator(operator1).build(); - - trip1 = - createTrip( - "TestTrip1", - route1, - List.of(new StopCall(stopA1, 10, 11), new StopCall(stopB1, 20, 21)) - ); - trip2 = - createTrip( - "TestTrip2", - route1, - List.of( - new StopCall(stopA1, 60, 61), - new StopCall(stopB1, 70, 71), - new StopCall(stopC1, 80, 81) - ) - ); - - CalendarServiceData calendarServiceData = new CalendarServiceData(); - calendarServiceData.putServiceDatesForServiceId( - SERVICE_ID, - List.of(SERVICE_DATE.minusDays(1), SERVICE_DATE, SERVICE_DATE.plusDays(1)) - ); - transitModel.getServiceCodes().put(SERVICE_ID, 0); - transitModel.updateCalendarServiceData(true, calendarServiceData, DataImportIssueStore.NOOP); - - transitModel.index(); + RealtimeTestEnvironment(SourceType sourceType, TransitModel transitModel) { + Objects.requireNonNull(sourceType); + this.transitModel = transitModel; + this.transitModel.index(); // SIRI and GTFS-RT cannot be registered with the transit model at the same time // we are actively refactoring to remove this restriction // for the time being you cannot run a SIRI and GTFS-RT test at the same time @@ -156,11 +81,7 @@ private RealtimeTestEnvironment(SourceType sourceType) { gtfsSource = new TimetableSnapshotSource(PARAMETERS, transitModel); siriSource = null; } - dateTimeHelper = new DateTimeHelper(timeZone, RealtimeTestEnvironment.SERVICE_DATE); - } - - public static FeedScopedId id(String id) { - return TransitModelForTest.id(id); + dateTimeHelper = new DateTimeHelper(TIME_ZONE, SERVICE_DATE); } /** @@ -190,7 +111,11 @@ private EstimatedTimetableHandler getEstimatedTimetableHandler(boolean fuzzyMatc } public TripPattern getPatternForTrip(FeedScopedId tripId) { - return getPatternForTrip(tripId, RealtimeTestEnvironment.SERVICE_DATE); + return getPatternForTrip(tripId, SERVICE_DATE); + } + + public TripPattern getPatternForTrip(String id) { + return getPatternForTrip(id(id)); } public TripPattern getPatternForTrip(FeedScopedId tripId, LocalDate serviceDate) { @@ -199,13 +124,6 @@ public TripPattern getPatternForTrip(FeedScopedId tripId, LocalDate serviceDate) return transitService.getPatternForTrip(trip.getTrip(), serviceDate); } - /** - * Find the current TripTimes for a trip id on the default serviceDate - */ - public TripTimes getTripTimesForTrip(Trip trip) { - return getTripTimesForTrip(trip.getId(), SERVICE_DATE); - } - /** * Find the current TripTimes for a trip id on the default serviceDate */ @@ -217,10 +135,6 @@ public DateTimeHelper getDateTimeHelper() { return dateTimeHelper; } - public TripPattern getPatternForTrip(Trip trip) { - return getTransitService().getPatternForTrip(trip); - } - public TimetableSnapshot getTimetableSnapshot() { if (siriSource != null) { return siriSource.getTimetableSnapshot(); @@ -233,10 +147,6 @@ public String getRealtimeTimetable(String tripId) { return getRealtimeTimetable(id(tripId), SERVICE_DATE); } - public String getRealtimeTimetable(Trip trip) { - return getRealtimeTimetable(trip.getId(), SERVICE_DATE); - } - public String getRealtimeTimetable(FeedScopedId tripId, LocalDate serviceDate) { var tt = getTripTimesForTrip(tripId, serviceDate); var pattern = getPatternForTrip(tripId); @@ -325,59 +235,4 @@ private void commitTimetableSnapshot() { gtfsSource.flushBuffer(); } } - - private Trip createTrip(String id, Route route, List stops) { - var trip = Trip - .of(id(id)) - .withRoute(route) - .withHeadsign(I18NString.of("Headsign of %s".formatted(id))) - .withServiceId(SERVICE_ID) - .build(); - - var tripOnServiceDate = TripOnServiceDate - .of(trip.getId()) - .withTrip(trip) - .withServiceDate(SERVICE_DATE) - .build(); - - transitModel.addTripOnServiceDate(tripOnServiceDate.getId(), tripOnServiceDate); - - var stopTimes = IntStream - .range(0, stops.size()) - .mapToObj(i -> { - var stop = stops.get(i); - return createStopTime(trip, i, stop.stop(), stop.arrivalTime(), stop.departureTime()); - }) - .collect(Collectors.toList()); - - TripTimes tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, null); - - final TripPattern pattern = TransitModelForTest - .tripPattern(id + "Pattern", route) - .withStopPattern(TransitModelForTest.stopPattern(stops.stream().map(StopCall::stop).toList())) - .build(); - pattern.add(tripTimes); - - transitModel.addTripPattern(pattern.getId(), pattern); - - return trip; - } - - private StopTime createStopTime( - Trip trip, - int stopSequence, - StopLocation stop, - int arrivalTime, - int departureTime - ) { - var st = new StopTime(); - st.setTrip(trip); - st.setStopSequence(stopSequence); - st.setStop(stop); - st.setArrivalTime(arrivalTime); - st.setDepartureTime(departureTime); - return st; - } - - private record StopCall(RegularStop stop, int arrivalTime, int departureTime) {} } diff --git a/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironmentBuilder.java b/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironmentBuilder.java new file mode 100644 index 00000000000..88f4bf41012 --- /dev/null +++ b/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironmentBuilder.java @@ -0,0 +1,114 @@ +package org.opentripplanner.updater.trip; + +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; + +import java.util.List; +import java.util.Objects; +import java.util.stream.IntStream; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.model.StopTime; +import org.opentripplanner.model.calendar.CalendarServiceData; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.framework.Deduplicator; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; +import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; +import org.opentripplanner.transit.service.TransitModel; + +public class RealtimeTestEnvironmentBuilder implements RealtimeTestConstants { + + private RealtimeTestEnvironment.SourceType sourceType; + private final TransitModel transitModel = new TransitModel(STOP_MODEL, new Deduplicator()); + + RealtimeTestEnvironmentBuilder withSourceType(RealtimeTestEnvironment.SourceType sourceType) { + this.sourceType = sourceType; + return this; + } + + public RealtimeTestEnvironmentBuilder addTrip(TripInput trip) { + createTrip(trip); + transitModel.index(); + return this; + } + + public RealtimeTestEnvironment build() { + Objects.requireNonNull(sourceType, "sourceType cannot be null"); + transitModel.initTimeZone(TIME_ZONE); + transitModel.addAgency(TransitModelForTest.AGENCY); + + CalendarServiceData calendarServiceData = new CalendarServiceData(); + calendarServiceData.putServiceDatesForServiceId( + SERVICE_ID, + List.of(SERVICE_DATE.minusDays(1), SERVICE_DATE, SERVICE_DATE.plusDays(1)) + ); + transitModel.getServiceCodes().put(SERVICE_ID, 0); + transitModel.updateCalendarServiceData(true, calendarServiceData, DataImportIssueStore.NOOP); + + return new RealtimeTestEnvironment(sourceType, transitModel); + } + + private Trip createTrip(TripInput tripInput) { + var trip = Trip + .of(id(tripInput.id())) + .withRoute(tripInput.route()) + .withHeadsign(I18NString.of("Headsign of %s".formatted(tripInput.id()))) + .withServiceId(SERVICE_ID) + .build(); + + var tripOnServiceDate = TripOnServiceDate + .of(trip.getId()) + .withTrip(trip) + .withServiceDate(SERVICE_DATE) + .build(); + + transitModel.addTripOnServiceDate(tripOnServiceDate.getId(), tripOnServiceDate); + + if (tripInput.route().getOperator() != null) { + transitModel.getOperators().add(tripInput.route().getOperator()); + } + + var stopTimes = IntStream + .range(0, tripInput.stops().size()) + .mapToObj(i -> { + var stop = tripInput.stops().get(i); + return createStopTime(trip, i, stop.stop(), stop.arrivalTime(), stop.departureTime()); + }) + .toList(); + + TripTimes tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, null); + + final TripPattern pattern = TransitModelForTest + .tripPattern(tripInput.id() + "Pattern", tripInput.route()) + .withStopPattern( + TransitModelForTest.stopPattern( + tripInput.stops().stream().map(TripInput.StopCall::stop).toList() + ) + ) + .withScheduledTimeTableBuilder(builder -> builder.addTripTimes(tripTimes)) + .build(); + + transitModel.addTripPattern(pattern.getId(), pattern); + + return trip; + } + + private static StopTime createStopTime( + Trip trip, + int stopSequence, + StopLocation stop, + int arrivalTime, + int departureTime + ) { + var st = new StopTime(); + st.setTrip(trip); + st.setStopSequence(stopSequence); + st.setStop(stop); + st.setArrivalTime(arrivalTime); + st.setDepartureTime(departureTime); + return st; + } +} diff --git a/src/test/java/org/opentripplanner/updater/trip/TripInput.java b/src/test/java/org/opentripplanner/updater/trip/TripInput.java new file mode 100644 index 00000000000..e4d9309061a --- /dev/null +++ b/src/test/java/org/opentripplanner/updater/trip/TripInput.java @@ -0,0 +1,47 @@ +package org.opentripplanner.updater.trip; + +import java.util.ArrayList; +import java.util.List; +import org.opentripplanner.framework.time.TimeUtils; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.site.RegularStop; + +/** + * A simple data structure that is used by the {@link RealtimeTestEnvironment} to create + * trips, trips on date and patterns. + */ +public record TripInput(String id, Route route, List stops) { + public static TripInputBuilder of(String id) { + return new TripInputBuilder(id); + } + + public static class TripInputBuilder implements RealtimeTestConstants { + + private final String id; + private final List stops = new ArrayList<>(); + // can be made configurable if needed + private Route route = ROUTE_1; + + TripInputBuilder(String id) { + this.id = id; + } + + public TripInputBuilder addStop(RegularStop stopId, String arrivalTime, String departureTime) { + this.stops.add( + new StopCall(stopId, TimeUtils.time(arrivalTime), TimeUtils.time(departureTime)) + ); + return this; + } + + public TripInput build() { + return new TripInput(id, route, stops); + } + + public TripInputBuilder withRoute(Route route) { + this.route = route; + return this; + } + } + + record StopCall(RegularStop stop, int arrivalTime, int departureTime) {} +} diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/addition/AddedTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/addition/AddedTest.java index 2e0b9d3d88e..6b662e8c8f3 100644 --- a/src/test/java/org/opentripplanner/updater/trip/moduletests/addition/AddedTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/addition/AddedTest.java @@ -7,10 +7,6 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess; -import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.SERVICE_DATE; -import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.STOP_A1_ID; -import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.STOP_B1_ID; -import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.STOP_C1_ID; import de.mfdz.MfdzRealtimeExtensions.StopTimePropertiesExtension.DropOffPickupType; import java.util.List; @@ -23,18 +19,19 @@ import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.updater.spi.UpdateSuccess; +import org.opentripplanner.updater.trip.RealtimeTestConstants; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; import org.opentripplanner.updater.trip.TripUpdateBuilder; -class AddedTest { +class AddedTest implements RealtimeTestConstants { final String ADDED_TRIP_ID = "added_trip"; @Test void addedTrip() { - var env = RealtimeTestEnvironment.gtfs(); + var env = RealtimeTestEnvironment.gtfs().build(); - var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, env.timeZone) + var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, TIME_ZONE) .addStopTime(STOP_A1_ID, 30) .addStopTime(STOP_B1_ID, 40) .addStopTime(STOP_C1_ID, 55) @@ -46,8 +43,8 @@ void addedTrip() { @Test void addedTripWithNewRoute() { - var env = RealtimeTestEnvironment.gtfs(); - var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, env.timeZone) + var env = RealtimeTestEnvironment.gtfs().build(); + var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, TIME_ZONE) .addTripExtension() .addStopTime(STOP_A1_ID, 30, DropOffPickupType.PHONE_AGENCY) .addStopTime(STOP_B1_ID, 40, DropOffPickupType.COORDINATE_WITH_DRIVER) @@ -81,8 +78,8 @@ void addedTripWithNewRoute() { @Test void addedWithUnknownStop() { - var env = RealtimeTestEnvironment.gtfs(); - var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, env.timeZone) + var env = RealtimeTestEnvironment.gtfs().build(); + var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, TIME_ZONE) // add extension to set route name, url, mode .addTripExtension() .addStopTime(STOP_A1_ID, 30, DropOffPickupType.PHONE_AGENCY) @@ -105,8 +102,8 @@ void addedWithUnknownStop() { @Test void repeatedlyAddedTripWithNewRoute() { - var env = RealtimeTestEnvironment.gtfs(); - var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, env.timeZone) + var env = RealtimeTestEnvironment.gtfs().build(); + var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, TIME_ZONE) // add extension to set route name, url, mode .addTripExtension() .addStopTime(STOP_A1_ID, 30, DropOffPickupType.PHONE_AGENCY) @@ -135,7 +132,7 @@ private TripPattern assertAddedTrip(String tripId, RealtimeTestEnvironment env) assertNotNull(trip); assertNotNull(transitService.getPatternForTrip(trip)); - var stopA = env.transitModel.getStopModel().getRegularStop(env.stopA1.getId()); + var stopA = env.transitModel.getStopModel().getRegularStop(STOP_A1.getId()); // Get the trip pattern of the added trip which goes through stopA var patternsAtA = env.getTimetableSnapshot().getPatternsForStop(stopA); diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java index c85225b7828..b8bd5c4574b 100644 --- a/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess; import static org.opentripplanner.updater.trip.UpdateIncrementality.DIFFERENTIAL; @@ -13,14 +14,16 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.opentripplanner.transit.model.timetable.RealTimeState; +import org.opentripplanner.updater.trip.RealtimeTestConstants; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; +import org.opentripplanner.updater.trip.TripInput; import org.opentripplanner.updater.trip.TripUpdateBuilder; /** * Cancellations and deletions should end up in the internal data model and make trips unavailable * for routing. */ -public class CancellationDeletionTest { +public class CancellationDeletionTest implements RealtimeTestConstants { static List cases() { return List.of( @@ -32,22 +35,25 @@ static List cases() { @ParameterizedTest @MethodSource("cases") void cancelledTrip(ScheduleRelationship relationship, RealTimeState state) { - var env = RealtimeTestEnvironment.gtfs(); - var pattern1 = env.getPatternForTrip(env.trip1); + var env = RealtimeTestEnvironment + .gtfs() + .addTrip( + TripInput + .of(TRIP_1_ID) + .addStop(STOP_A1, "0:00:10", "0:00:11") + .addStop(STOP_B1, "0:00:20", "0:00:21") + .build() + ) + .build(); + var pattern1 = env.getPatternForTrip(TRIP_1_ID); - final int tripIndex1 = pattern1.getScheduledTimetable().getTripIndex(env.trip1.getId()); + final int tripIndex1 = pattern1.getScheduledTimetable().getTripIndex(id(TRIP_1_ID)); - var update = new TripUpdateBuilder( - env.trip1.getId().getId(), - RealtimeTestEnvironment.SERVICE_DATE, - relationship, - env.timeZone - ) - .build(); + var update = new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE, relationship, TIME_ZONE).build(); assertSuccess(env.applyTripUpdate(update)); var snapshot = env.getTimetableSnapshot(); - var forToday = snapshot.resolve(pattern1, RealtimeTestEnvironment.SERVICE_DATE); + var forToday = snapshot.resolve(pattern1, SERVICE_DATE); var schedule = snapshot.resolve(pattern1, null); assertNotSame(forToday, schedule); assertNotSame(forToday.getTripTimes(tripIndex1), schedule.getTripTimes(tripIndex1)); @@ -71,41 +77,34 @@ void cancelledTrip(ScheduleRelationship relationship, RealTimeState state) { @ParameterizedTest @MethodSource("cases") void cancelingAddedTrip(ScheduleRelationship relationship, RealTimeState state) { - var env = RealtimeTestEnvironment.gtfs(); + var env = RealtimeTestEnvironment.gtfs().build(); var addedTripId = "added-trip"; // First add ADDED trip var update = new TripUpdateBuilder( addedTripId, - RealtimeTestEnvironment.SERVICE_DATE, + SERVICE_DATE, ScheduleRelationship.ADDED, - env.timeZone + TIME_ZONE ) - .addStopTime(env.stopA1.getId().getId(), 30) - .addStopTime(env.stopB1.getId().getId(), 40) - .addStopTime(env.stopC1.getId().getId(), 55) + .addStopTime(STOP_A1_ID, 30) + .addStopTime(STOP_B1_ID, 40) + .addStopTime(STOP_C1_ID, 55) .build(); assertSuccess(env.applyTripUpdate(update, DIFFERENTIAL)); // Cancel or delete the added trip - update = - new TripUpdateBuilder( - addedTripId, - RealtimeTestEnvironment.SERVICE_DATE, - relationship, - env.timeZone - ) - .build(); + update = new TripUpdateBuilder(addedTripId, SERVICE_DATE, relationship, TIME_ZONE).build(); assertSuccess(env.applyTripUpdate(update, DIFFERENTIAL)); var snapshot = env.getTimetableSnapshot(); // Get the trip pattern of the added trip which goes through stopA - var patternsAtA = snapshot.getPatternsForStop(env.stopA1); + var patternsAtA = snapshot.getPatternsForStop(STOP_A1); assertNotNull(patternsAtA, "Added trip pattern should be found"); var tripPattern = patternsAtA.stream().findFirst().get(); - var forToday = snapshot.resolve(tripPattern, RealtimeTestEnvironment.SERVICE_DATE); + var forToday = snapshot.resolve(tripPattern, SERVICE_DATE); var schedule = snapshot.resolve(tripPattern, null); assertNotSame(forToday, schedule); diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java index 5298853f36d..f45a82b9ba0 100644 --- a/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java @@ -5,34 +5,34 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess; -import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.SERVICE_DATE; import org.junit.jupiter.api.Test; -import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.RealTimeState; -import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.updater.trip.RealtimeTestConstants; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; +import org.opentripplanner.updater.trip.TripInput; import org.opentripplanner.updater.trip.TripUpdateBuilder; /** * Delays should be applied to the first trip but should leave the second trip untouched. */ -class DelayedTest { +class DelayedTest implements RealtimeTestConstants { private static final int DELAY = 1; private static final int STOP_SEQUENCE = 1; @Test void singleStopDelay() { - var env = RealtimeTestEnvironment.gtfs(); - - var tripUpdate = new TripUpdateBuilder( - env.trip1.getId().getId(), - RealtimeTestEnvironment.SERVICE_DATE, - SCHEDULED, - env.timeZone - ) + var TRIP_INPUT = TripInput + .of(TRIP_1_ID) + .addStop(STOP_A1, "0:00:10", "0:00:11") + .addStop(STOP_B1, "0:00:20", "0:00:21") + .build(); + var env = RealtimeTestEnvironment.gtfs().addTrip(TRIP_INPUT).build(); + + var tripUpdate = new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) .addDelayedStopTime(STOP_SEQUENCE, DELAY) .build(); @@ -40,11 +40,11 @@ void singleStopDelay() { assertEquals(1, result.successful()); - var pattern1 = env.getPatternForTrip(env.trip1); - int trip1Index = pattern1.getScheduledTimetable().getTripIndex(env.trip1.getId()); + var pattern1 = env.getPatternForTrip(TRIP_1_ID); + int trip1Index = pattern1.getScheduledTimetable().getTripIndex(id(TRIP_1_ID)); var snapshot = env.getTimetableSnapshot(); - var trip1Realtime = snapshot.resolve(pattern1, RealtimeTestEnvironment.SERVICE_DATE); + var trip1Realtime = snapshot.resolve(pattern1, SERVICE_DATE); var trip1Scheduled = snapshot.resolve(pattern1, null); assertNotSame(trip1Realtime, trip1Scheduled); @@ -59,11 +59,11 @@ void singleStopDelay() { assertEquals( "SCHEDULED | A1 0:00:10 0:00:11 | B1 0:00:20 0:00:21", - env.getScheduledTimetable(env.trip1.getId()) + env.getScheduledTimetable(TRIP_1_ID) ); assertEquals( "UPDATED | A1 [ND] 0:00:10 0:00:11 | B1 0:00:21 0:00:22", - env.getRealtimeTimetable(env.trip1.getId().getId()) + env.getRealtimeTimetable(TRIP_1_ID) ); } @@ -72,11 +72,15 @@ void singleStopDelay() { */ @Test void complexDelay() { - var env = RealtimeTestEnvironment.gtfs(); - - var tripId = env.trip2.getId().getId(); + var tripInput = TripInput + .of(TRIP_2_ID) + .addStop(STOP_A1, "0:01:00", "0:01:01") + .addStop(STOP_B1, "0:01:10", "0:01:11") + .addStop(STOP_C1, "0:01:20", "0:01:21") + .build(); + var env = RealtimeTestEnvironment.gtfs().addTrip(tripInput).build(); - var tripUpdate = new TripUpdateBuilder(tripId, SERVICE_DATE, SCHEDULED, env.timeZone) + var tripUpdate = new TripUpdateBuilder(TRIP_2_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) .addDelayedStopTime(0, 0) .addDelayedStopTime(1, 60, 80) .addDelayedStopTime(2, 90, 90) @@ -86,19 +90,20 @@ void complexDelay() { var snapshot = env.getTimetableSnapshot(); - final TripPattern originalTripPattern = env.getTransitService().getPatternForTrip(env.trip2); + var trip2 = env.getTransitService().getTripForId(id(TRIP_2_ID)); + var originalTripPattern = env.getTransitService().getPatternForTrip(trip2); var originalTimetableForToday = snapshot.resolve(originalTripPattern, SERVICE_DATE); var originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); assertNotSame(originalTimetableForToday, originalTimetableScheduled); - final int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(tripId); + final int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(TRIP_2_ID); assertTrue( originalTripIndexScheduled > -1, "Original trip should be found in scheduled time table" ); - final TripTimes originalTripTimesScheduled = originalTimetableScheduled.getTripTimes( + var originalTripTimesScheduled = originalTimetableScheduled.getTripTimes( originalTripIndexScheduled ); assertFalse( @@ -107,7 +112,7 @@ void complexDelay() { ); assertEquals(RealTimeState.SCHEDULED, originalTripTimesScheduled.getRealTimeState()); - final int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); + final int originalTripIndexForToday = originalTimetableForToday.getTripIndex(TRIP_2_ID); assertTrue( originalTripIndexForToday > -1, "Original trip should be found in time table for service date" @@ -115,11 +120,11 @@ void complexDelay() { assertEquals( "SCHEDULED | A1 0:01 0:01:01 | B1 0:01:10 0:01:11 | C1 0:01:20 0:01:21", - env.getScheduledTimetable(env.trip2.getId()) + env.getScheduledTimetable(TRIP_2_ID) ); assertEquals( "UPDATED | A1 0:01 0:01:01 | B1 0:02:10 0:02:31 | C1 0:02:50 0:02:51", - env.getRealtimeTimetable(env.trip2) + env.getRealtimeTimetable(TRIP_2_ID) ); } } diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java index de699324bb6..f9799ba6512 100644 --- a/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java @@ -6,28 +6,35 @@ import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess; -import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.SERVICE_DATE; import static org.opentripplanner.updater.trip.UpdateIncrementality.DIFFERENTIAL; import org.junit.jupiter.api.Test; -import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.timetable.RealTimeState; import org.opentripplanner.transit.model.timetable.TripTimesStringBuilder; +import org.opentripplanner.updater.trip.RealtimeTestConstants; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; +import org.opentripplanner.updater.trip.TripInput; import org.opentripplanner.updater.trip.TripUpdateBuilder; /** * A mixture of delayed and skipped stops should result in both delayed and cancelled stops. */ -public class SkippedTest { +class SkippedTest implements RealtimeTestConstants { + + private static final TripInput TRIP_INPUT = TripInput + .of(TRIP_2_ID) + .addStop(STOP_A1, "0:01:00", "0:01:01") + .addStop(STOP_B1, "0:01:10", "0:01:11") + .addStop(STOP_C1, "0:01:20", "0:01:21") + .build(); @Test void scheduledTripWithSkippedAndScheduled() { - var env = RealtimeTestEnvironment.gtfs(); - String scheduledTripId = env.trip2.getId().getId(); + var env = RealtimeTestEnvironment.gtfs().addTrip(TRIP_INPUT).build(); - var tripUpdate = new TripUpdateBuilder(scheduledTripId, SERVICE_DATE, SCHEDULED, env.timeZone) + var tripUpdate = new TripUpdateBuilder(TRIP_2_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) .addDelayedStopTime(0, 0) .addSkippedStop(1) .addDelayedStopTime(2, 90) @@ -35,13 +42,13 @@ void scheduledTripWithSkippedAndScheduled() { assertSuccess(env.applyTripUpdate(tripUpdate)); - assertOriginalTripPatternIsDeleted(env, env.trip2.getId()); + assertOriginalTripPatternIsDeleted(env, TRIP_2_ID); - assertNewTripTimesIsUpdated(env, env.trip2.getId()); + assertNewTripTimesIsUpdated(env, TRIP_2_ID); assertEquals( "UPDATED | A1 0:01 0:01:01 | B1 [C] 0:01:52 0:01:58 | C1 0:02:50 0:02:51", - env.getRealtimeTimetable(scheduledTripId) + env.getRealtimeTimetable(TRIP_2_ID) ); } @@ -56,10 +63,9 @@ void scheduledTripWithSkippedAndScheduled() { */ @Test void scheduledTripWithPreviouslySkipped() { - var env = RealtimeTestEnvironment.gtfs(); - var tripId = env.trip2.getId(); + var env = RealtimeTestEnvironment.gtfs().addTrip(TRIP_INPUT).build(); - var tripUpdate = new TripUpdateBuilder(tripId.getId(), SERVICE_DATE, SCHEDULED, env.timeZone) + var tripUpdate = new TripUpdateBuilder(TRIP_2_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) .addDelayedStopTime(0, 0) .addSkippedStop(1) .addDelayedStopTime(2, 90) @@ -68,12 +74,7 @@ void scheduledTripWithPreviouslySkipped() { assertSuccess(env.applyTripUpdate(tripUpdate, DIFFERENTIAL)); // Create update to the same trip but now the skipped stop is no longer skipped - var scheduledBuilder = new TripUpdateBuilder( - tripId.getId(), - SERVICE_DATE, - SCHEDULED, - env.timeZone - ) + var scheduledBuilder = new TripUpdateBuilder(TRIP_2_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) .addDelayedStopTime(0, 0) .addDelayedStopTime(1, 50) .addDelayedStopTime(2, 90); @@ -87,17 +88,17 @@ void scheduledTripWithPreviouslySkipped() { // stoptime updates have gone through var snapshot = env.getTimetableSnapshot(); - assertNull(snapshot.getRealtimeAddedTripPattern(tripId, SERVICE_DATE)); + assertNull(snapshot.getRealtimeAddedTripPattern(id(TRIP_2_ID), SERVICE_DATE)); - assertNewTripTimesIsUpdated(env, tripId); + assertNewTripTimesIsUpdated(env, TRIP_2_ID); assertEquals( "SCHEDULED | A1 0:01 0:01:01 | B1 0:01:10 0:01:11 | C1 0:01:20 0:01:21", - env.getScheduledTimetable(tripId) + env.getScheduledTimetable(TRIP_2_ID) ); assertEquals( "UPDATED | A1 0:01 0:01:01 | B1 0:02 0:02:01 | C1 0:02:50 0:02:51", - env.getRealtimeTimetable(tripId, SERVICE_DATE) + env.getRealtimeTimetable(id(TRIP_2_ID), SERVICE_DATE) ); } @@ -106,11 +107,11 @@ void scheduledTripWithPreviouslySkipped() { */ @Test void skippedNoData() { - var env = RealtimeTestEnvironment.gtfs(); + var env = RealtimeTestEnvironment.gtfs().addTrip(TRIP_INPUT).build(); - final FeedScopedId tripId = env.trip2.getId(); + String tripId = TRIP_2_ID; - var tripUpdate = new TripUpdateBuilder(tripId.getId(), SERVICE_DATE, SCHEDULED, env.timeZone) + var tripUpdate = new TripUpdateBuilder(tripId, SERVICE_DATE, SCHEDULED, TIME_ZONE) .addNoDataStop(0) .addSkippedStop(1) .addNoDataStop(2) @@ -124,15 +125,15 @@ void skippedNoData() { assertEquals( "UPDATED | A1 [ND] 0:01 0:01:01 | B1 [C] 0:01:10 0:01:11 | C1 [ND] 0:01:20 0:01:21", - env.getRealtimeTimetable(env.trip2) + env.getRealtimeTimetable(tripId) ); } private static void assertOriginalTripPatternIsDeleted( RealtimeTestEnvironment env, - FeedScopedId tripId + String tripId ) { - var trip = env.getTransitService().getTripForId(tripId); + var trip = env.getTransitService().getTripForId(id(tripId)); var originalTripPattern = env.getTransitService().getPatternForTrip(trip); var snapshot = env.getTimetableSnapshot(); var originalTimetableForToday = snapshot.resolve(originalTripPattern, SERVICE_DATE); @@ -174,11 +175,9 @@ private static void assertOriginalTripPatternIsDeleted( assertEquals(RealTimeState.DELETED, originalTripTimesForToday.getRealTimeState()); } - private static void assertNewTripTimesIsUpdated( - RealtimeTestEnvironment env, - FeedScopedId tripId - ) { - var originalTripPattern = env.getTransitService().getPatternForTrip(env.trip2); + private static void assertNewTripTimesIsUpdated(RealtimeTestEnvironment env, String tripId) { + var trip = env.getTransitService().getTripForId(id(tripId)); + var originalTripPattern = env.getTransitService().getPatternForTrip(trip); var snapshot = env.getTimetableSnapshot(); var originalTimetableForToday = snapshot.resolve(originalTripPattern, SERVICE_DATE); diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java index da362451753..2ba6749b4b0 100644 --- a/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java @@ -4,20 +4,21 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_SERVICE_ON_DATE; import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertFailure; -import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.SERVICE_DATE; import java.time.LocalDate; import java.util.List; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.updater.trip.RealtimeTestConstants; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; +import org.opentripplanner.updater.trip.TripInput; import org.opentripplanner.updater.trip.TripUpdateBuilder; /** * A trip with start date that is outside the service period shouldn't throw an exception and is * ignored instead. */ -class InvalidInputTest { +class InvalidInputTest implements RealtimeTestConstants { public static List cases() { return List.of(SERVICE_DATE.minusYears(10), SERVICE_DATE.plusYears(10)); @@ -26,9 +27,14 @@ public static List cases() { @ParameterizedTest @MethodSource("cases") void invalidTripDate(LocalDate date) { - var env = RealtimeTestEnvironment.gtfs(); + var tripInput = TripInput + .of(TRIP_1_ID) + .addStop(STOP_A1, "0:00:10", "0:00:11") + .addStop(STOP_B1, "0:00:20", "0:00:21") + .build(); + var env = RealtimeTestEnvironment.gtfs().addTrip(tripInput).build(); - var update = new TripUpdateBuilder(env.trip1.getId().getId(), date, SCHEDULED, env.timeZone) + var update = new TripUpdateBuilder(TRIP_1_ID, date, SCHEDULED, TIME_ZONE) .addDelayedStopTime(2, 60, 80) .build(); diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidTripIdTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidTripIdTest.java index 83c2547dbc7..699e8fe865c 100644 --- a/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidTripIdTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidTripIdTest.java @@ -22,7 +22,7 @@ static Stream invalidCases() { @ParameterizedTest(name = "tripId=\"{0}\"") @MethodSource("invalidCases") void invalidTripId(String tripId) { - var env = RealtimeTestEnvironment.gtfs(); + var env = RealtimeTestEnvironment.gtfs().build(); var tripDescriptorBuilder = GtfsRealtime.TripDescriptor.newBuilder(); if (tripId != null) { tripDescriptorBuilder.setTripId(tripId); diff --git a/src/test/java/org/opentripplanner/updater/vehicle_position/RealtimeVehicleMatcherTest.java b/src/test/java/org/opentripplanner/updater/vehicle_position/RealtimeVehicleMatcherTest.java index ea5d86cd0e1..4f10fb2d64b 100644 --- a/src/test/java/org/opentripplanner/updater/vehicle_position/RealtimeVehicleMatcherTest.java +++ b/src/test/java/org/opentripplanner/updater/vehicle_position/RealtimeVehicleMatcherTest.java @@ -383,10 +383,10 @@ private static TripPattern tripPattern(Trip trip, List stopTimes) { .of(trip.getId()) .withStopPattern(stopPattern) .withRoute(ROUTE) + .withScheduledTimeTableBuilder(builder -> + builder.addTripTimes(TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator())) + ) .build(); - pattern - .getScheduledTimetable() - .addTripTimes(TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator())); return pattern; } diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/plan-extended.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/plan-extended.json index ea58480be8e..c899606bd0b 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/plan-extended.json +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/plan-extended.json @@ -64,7 +64,9 @@ "intermediatePlaces" : null, "alerts" : [ ], "rideHailingEstimate" : null, - "accessibilityScore" : null + "accessibilityScore" : null, + "id": null, + "realtimeState": null }, { "mode" : "BUS", @@ -154,7 +156,9 @@ ], "alerts" : [ ], "rideHailingEstimate" : null, - "accessibilityScore" : null + "accessibilityScore" : null, + "id": "rO0ABXdBABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjMABUY6MTIyAAoyMDIwLTAyLTAyAAAABQAAAAcAA0Y6QgADRjpDAAA=", + "realtimeState": "UPDATED" }, { "mode" : "RAIL", @@ -264,7 +268,9 @@ } ], "rideHailingEstimate" : null, - "accessibilityScore" : null + "accessibilityScore" : null, + "id": "rO0ABXdBABhTQ0hFRFVMRURfVFJBTlNJVF9MRUdfVjMABUY6NDM5AAoyMDIwLTAyLTAyAAAABQAAAAcAA0Y6QwADRjpEAAA=", + "realtimeState": "UPDATED" }, { "mode" : "CAR", @@ -334,11 +340,13 @@ }, "arrival" : "PT10M" }, - "accessibilityScore" : null + "accessibilityScore" : null, + "id": null, + "realtimeState": null } ] } ] } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stops.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stops.json index 307b07b58aa..4e3d4c8a18c 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stops.json +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stops.json @@ -6,56 +6,120 @@ "lat" : 5.0, "lon" : 8.0, "name" : "A", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] }, { "gtfsId" : "F:B", "lat" : 6.0, "lon" : 8.5, "name" : "B", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] }, { "gtfsId" : "F:C", "lat" : 7.0, "lon" : 9.0, "name" : "C", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] }, { "gtfsId" : "F:D", "lat" : 8.0, "lon" : 9.5, "name" : "D", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] }, { "gtfsId" : "F:E", "lat" : 9.0, "lon" : 10.0, "name" : "E", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] }, { "gtfsId" : "F:F", "lat" : 9.0, "lon" : 10.5, "name" : "F", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] }, { "gtfsId" : "F:G", "lat" : 9.5, "lon" : 11.0, "name" : "G", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] }, { "gtfsId" : "F:H", "lat" : 10.0, "lon" : 11.5, "name" : "H", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] } ] } diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/plan-extended.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/plan-extended.graphql index 76bf8aa84e0..7823ae91bab 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/queries/plan-extended.graphql +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/plan-extended.graphql @@ -151,6 +151,8 @@ arrival } accessibilityScore + id + realtimeState } } } diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/stops.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/stops.graphql index 5f3df71f8e7..af4fd904096 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/queries/stops.graphql +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/stops.graphql @@ -5,5 +5,17 @@ lon name vehicleMode + allRoutes: routes { + gtfsId + longName + shortName + } + routesWithinRange: routes( + serviceDates: { start: "2024-09-10", end: "2024-09-10" } + ) { + gtfsId + longName + shortName + } } }