diff --git a/client/.env b/client/.env index ca1d71c90e3..a2d09f3b312 100644 --- a/client/.env +++ b/client/.env @@ -1,2 +1,3 @@ VITE_API_URL=/otp/transmodel/v3 VITE_DEBUG_STYLE_URL=/otp/routers/default/inspector/vectortile/style.json + diff --git a/client/package-lock.json b/client/package-lock.json index 3c4520eb745..9e2798660ec 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,34 +14,34 @@ "graphql-request": "7.1.0", "maplibre-gl": "4.5.0", "react": "18.3.1", - "react-bootstrap": "2.10.3", + "react-bootstrap": "2.10.4", "react-dom": "18.3.1", "react-map-gl": "7.1.7" }, "devDependencies": { "@graphql-codegen/cli": "5.0.2", - "@graphql-codegen/client-preset": "4.3.1", + "@graphql-codegen/client-preset": "4.3.3", "@graphql-codegen/introspection": "4.0.3", "@parcel/watcher": "2.4.1", "@testing-library/react": "16.0.0", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", - "@typescript-eslint/eslint-plugin": "7.14.1", - "@typescript-eslint/parser": "7.14.1", + "@typescript-eslint/eslint-plugin": "7.18.0", + "@typescript-eslint/parser": "7.18.0", "@vitejs/plugin-react": "4.3.1", - "@vitest/coverage-v8": "1.6.0", + "@vitest/coverage-v8": "2.0.5", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.29.1", "eslint-plugin-jsx-a11y": "6.9.0", - "eslint-plugin-react": "7.34.3", + "eslint-plugin-react": "7.35.0", "eslint-plugin-react-hooks": "4.6.2", - "eslint-plugin-react-refresh": "0.4.7", - "jsdom": "24.1.0", - "prettier": "3.3.2", - "typescript": "5.5.2", - "vite": "5.3.2", - "vitest": "1.6.0" + "eslint-plugin-react-refresh": "0.4.9", + "jsdom": "24.1.1", + "prettier": "3.3.3", + "typescript": "5.5.4", + "vite": "5.3.5", + "vitest": "2.0.5" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1861,21 +1861,21 @@ } }, "node_modules/@graphql-codegen/client-preset": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-4.3.1.tgz", - "integrity": "sha512-FHszBKhubbJkrZHwzUNfMUp9IkzufCfn/riTpIy5yA84Wq0AJSPFL7nWkG+h3azFPeznLfqo3KJmfzRb+xeFEA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-4.3.3.tgz", + "integrity": "sha512-IrDsSVe8bkKtxgVfKPHzjL9tYlv7KEpA59R4gZLqx/t2WIJncW1i0OMvoz9tgoZsFEs8OKKgXZbnwPZ/Qf1kEw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/template": "^7.20.7", "@graphql-codegen/add": "^5.0.3", - "@graphql-codegen/gql-tag-operations": "4.0.8", + "@graphql-codegen/gql-tag-operations": "4.0.9", "@graphql-codegen/plugin-helpers": "^5.0.4", - "@graphql-codegen/typed-document-node": "^5.0.8", - "@graphql-codegen/typescript": "^4.0.8", - "@graphql-codegen/typescript-operations": "^4.2.2", - "@graphql-codegen/visitor-plugin-common": "^5.3.0", + "@graphql-codegen/typed-document-node": "^5.0.9", + "@graphql-codegen/typescript": "^4.0.9", + "@graphql-codegen/typescript-operations": "^4.2.3", + "@graphql-codegen/visitor-plugin-common": "^5.3.1", "@graphql-tools/documents": "^1.0.0", "@graphql-tools/utils": "^10.0.0", "@graphql-typed-document-node/core": "3.2.0", @@ -1901,14 +1901,14 @@ } }, "node_modules/@graphql-codegen/gql-tag-operations": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-4.0.8.tgz", - "integrity": "sha512-slCICQOFbMfdL7mAZ6XUiOhcJl0yOKfqHFiULIlQJKpo8ey6NHsrtc8Q02ZF417BfTfZ/Qj7rmXhkc/dwY94ag==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-4.0.9.tgz", + "integrity": "sha512-lVgu1HClel896HqZAEjynatlU6eJrYOw+rh05DPgM150xvmb7Gz5TnRHA2vfwlDNIXDaToAIpz5RFfkjjnYM1Q==", "dev": true, "license": "MIT", "dependencies": { "@graphql-codegen/plugin-helpers": "^5.0.4", - "@graphql-codegen/visitor-plugin-common": "5.3.0", + "@graphql-codegen/visitor-plugin-common": "5.3.1", "@graphql-tools/utils": "^10.0.0", "auto-bind": "~4.0.0", "tslib": "~2.6.0" @@ -1964,14 +1964,14 @@ } }, "node_modules/@graphql-codegen/typed-document-node": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-5.0.8.tgz", - "integrity": "sha512-ImJd1KwS0vYZiPVZzs8EOZ79V96zN0p1A1MJNpk/8CiJWpIi4FupLLfTMMYq5Rr0AZET+O/A+udw4LDjDrAWvg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-5.0.9.tgz", + "integrity": "sha512-Wx6fyA4vpfIbfNTMiWUECGnjqzKkJdEbZHxVMIegiCBPzBYPAJV4mZZcildLAfm2FtZcgW4YKtFoTbnbXqPB3w==", "dev": true, "license": "MIT", "dependencies": { "@graphql-codegen/plugin-helpers": "^5.0.4", - "@graphql-codegen/visitor-plugin-common": "5.3.0", + "@graphql-codegen/visitor-plugin-common": "5.3.1", "auto-bind": "~4.0.0", "change-case-all": "1.0.15", "tslib": "~2.6.0" @@ -1981,15 +1981,15 @@ } }, "node_modules/@graphql-codegen/typescript": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-4.0.8.tgz", - "integrity": "sha512-kYS3SjGNnC9vgFS8N3vaxzRFkdXX2umMi1SOpHjMFCPjMe8NR0uNdW4nP9T0YEq+DvWgj+XojjpFy2oyz9q12w==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-4.0.9.tgz", + "integrity": "sha512-0O35DMR4d/ctuHL1Zo6mRUUzp0BoszKfeWsa6sCm/g70+S98+hEfTwZNDkQHylLxapiyjssF9uw/F+sXqejqLw==", "dev": true, "license": "MIT", "dependencies": { "@graphql-codegen/plugin-helpers": "^5.0.4", "@graphql-codegen/schema-ast": "^4.0.2", - "@graphql-codegen/visitor-plugin-common": "5.3.0", + "@graphql-codegen/visitor-plugin-common": "5.3.1", "auto-bind": "~4.0.0", "tslib": "~2.6.0" }, @@ -1998,15 +1998,15 @@ } }, "node_modules/@graphql-codegen/typescript-operations": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-4.2.2.tgz", - "integrity": "sha512-8FJHIAubM4r9ElLuuDAKhdOjainSwRHEmGIrtEgEwHARKhMk1Ttj6bpOQDisYlbDl4ZTHWEJCdNa9o9rgcl+9g==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-4.2.3.tgz", + "integrity": "sha512-6z7avSSOr03l5SyKbeDs7MzRyGwnQFSCqQm8Om5wIuoIgXVu2gXRmcJAY/I7SLdAy9xbF4Sho7XNqieFM2CAFQ==", "dev": true, "license": "MIT", "dependencies": { "@graphql-codegen/plugin-helpers": "^5.0.4", - "@graphql-codegen/typescript": "^4.0.8", - "@graphql-codegen/visitor-plugin-common": "5.3.0", + "@graphql-codegen/typescript": "^4.0.9", + "@graphql-codegen/visitor-plugin-common": "5.3.1", "auto-bind": "~4.0.0", "tslib": "~2.6.0" }, @@ -2015,9 +2015,9 @@ } }, "node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-5.3.0.tgz", - "integrity": "sha512-+kUk7gRD/72Wfkjd7D96Lonh9k4lFw9d3O1+I07Jyja4QN9H42kdFEO0hM/b4Q9lLkI1yJ66Oym7lWz2Ikj3aw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-5.3.1.tgz", + "integrity": "sha512-MktoBdNZhSmugiDjmFl1z6rEUUaqyxtFJYWnDilE7onkPgyw//O0M+TuPBJPBWdyV6J2ond0Hdqtq+rkghgSIQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2799,25 +2799,109 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, "engines": { - "node": ">=8" + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.27.8" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" } }, "node_modules/@jridgewell/gen-mapping": { @@ -3372,6 +3456,17 @@ "node": ">=10.12.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -3610,12 +3705,6 @@ "win32" ] }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "node_modules/@swc/helpers": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.7.tgz", @@ -3854,17 +3943,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz", - "integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.14.1", - "@typescript-eslint/type-utils": "7.14.1", - "@typescript-eslint/utils": "7.14.1", - "@typescript-eslint/visitor-keys": "7.14.1", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -3888,16 +3977,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz", - "integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.14.1", - "@typescript-eslint/types": "7.14.1", - "@typescript-eslint/typescript-estree": "7.14.1", - "@typescript-eslint/visitor-keys": "7.14.1", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "engines": { @@ -3917,14 +4006,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz", - "integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.14.1", - "@typescript-eslint/visitor-keys": "7.14.1" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -3935,14 +4024,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz", - "integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.14.1", - "@typescript-eslint/utils": "7.14.1", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -3963,9 +4052,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz", - "integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "dev": true, "license": "MIT", "engines": { @@ -3977,14 +4066,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz", - "integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.14.1", - "@typescript-eslint/visitor-keys": "7.14.1", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -4006,9 +4095,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "license": "ISC", "bin": { @@ -4019,16 +4108,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz", - "integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.14.1", - "@typescript-eslint/types": "7.14.1", - "@typescript-eslint/typescript-estree": "7.14.1" + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -4042,13 +4131,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz", - "integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -4086,192 +4175,119 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", - "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", + "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==", "dev": true, + "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.1", + "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", + "debug": "^4.3.5", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.10", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.6.0" + "vitest": "2.0.5" } }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "chai": "^4.3.10" + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "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==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/utils": "1.6.0", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "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==", "dev": true, + "license": "MIT", "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", "dev": true, + "license": "MIT", "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/snapshot/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/snapshot/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", "dev": true, + "license": "MIT", "dependencies": { - "tinyspy": "^2.2.0" + "tinyspy": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", "dev": true, + "license": "MIT", "dependencies": { - "diff-sequences": "^29.6.3", + "@vitest/pretty-format": "2.0.5", "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/utils/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/utils/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/@whatwg-node/events": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.0.3.tgz", @@ -4325,15 +4341,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", @@ -4571,18 +4578,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.toreversed": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", - "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - } - }, "node_modules/array.prototype.tosorted": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", @@ -4643,12 +4638,13 @@ } }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, + "license": "MIT", "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/assign-symbols": { @@ -5020,21 +5016,20 @@ } }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", "dev": true, + "license": "MIT", "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, "node_modules/chalk": { @@ -5098,15 +5093,13 @@ "dev": true }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, + "license": "MIT", "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/classnames": { @@ -5441,10 +5434,11 @@ "dev": true }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.1.2" }, @@ -5473,13 +5467,11 @@ "dev": true }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, + "license": "MIT", "engines": { "node": ">=6" } @@ -5616,15 +5608,6 @@ "node": ">=0.10" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -5722,6 +5705,13 @@ "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.4.717", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.717.tgz", @@ -6259,36 +6249,36 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.34.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz", - "integrity": "sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA==", + "version": "7.35.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", + "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", "dev": true, "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.2", - "array.prototype.toreversed": "^1.1.2", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", - "object.hasown": "^1.1.4", "object.values": "^1.2.0", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11" + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "node_modules/eslint-plugin-react-hooks": { @@ -6304,9 +6294,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz", - "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==", + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.9.tgz", + "integrity": "sha512-QK49YrBAo5CLNLseZ7sZgvgTy21E6NEw22eZqc4teZfH8pxV3yXc9XXOYfUI6JNpw7mfHNkAeWtBxrTyykB6HA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -6500,6 +6490,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } @@ -6826,6 +6817,36 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/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, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -6925,6 +6946,7 @@ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -7372,10 +7394,11 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.0.2", "debug": "4" @@ -8118,10 +8141,11 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", - "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", @@ -8157,6 +8181,22 @@ "set-function-name": "^2.0.1" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", @@ -8193,9 +8233,9 @@ } }, "node_modules/jsdom": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz", - "integrity": "sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==", + "version": "24.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.1.tgz", + "integrity": "sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8205,11 +8245,11 @@ "form-data": "^4.0.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.4", + "https-proxy-agent": "^7.0.5", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.10", + "nwsapi": "^2.2.12", "parse5": "^7.1.2", - "rrweb-cssom": "^0.7.0", + "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^4.1.4", @@ -8218,7 +8258,7 @@ "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0", - "ws": "^8.17.0", + "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "engines": { @@ -8234,9 +8274,9 @@ } }, "node_modules/jsdom/node_modules/rrweb-cssom": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.0.tgz", - "integrity": "sha512-KlSv0pm9kgQSRxXEMgtivPJ4h826YHsuob8pSHcfSZsSXGtvpEAie8S0AnXuObEJ7nhikOb4ahwxDm0H2yW17g==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", "dev": true, "license": "MIT" }, @@ -8324,12 +8364,6 @@ "node": ">=6" } }, - "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true - }, "node_modules/jsonify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", @@ -8457,22 +8491,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8587,10 +8605,11 @@ } }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.1" } @@ -8633,26 +8652,25 @@ } }, "node_modules/magic-string": { - "version": "0.30.8", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", - "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" } }, "node_modules/magicast": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.3.tgz", - "integrity": "sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/parser": "^7.23.6", - "@babel/types": "^7.23.6", - "source-map-js": "^1.0.2" + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" } }, "node_modules/make-dir": { @@ -8853,16 +8871,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mlly": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz", - "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==", + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.0.3", - "ufo": "^1.3.2" + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/ms": { @@ -9025,9 +9041,9 @@ "dev": true }, "node_modules/nwsapi": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", - "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", + "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", "dev": true, "license": "MIT" }, @@ -9138,23 +9154,6 @@ "node": ">= 0.4" } }, - "node_modules/object.hasown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", - "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.values": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", @@ -9299,6 +9298,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -9439,6 +9445,30 @@ "node": ">=0.10.0" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -9455,12 +9485,13 @@ "dev": true }, "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, + "license": "MIT", "engines": { - "node": "*" + "node": ">= 14.16" } }, "node_modules/pbf": { @@ -9476,10 +9507,11 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -9493,17 +9525,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -9514,9 +9535,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", "dev": true, "funding": [ { @@ -9532,9 +9553,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "source-map-js": "^1.2.0" }, "engines": { @@ -9556,9 +9578,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", "bin": { @@ -9718,9 +9740,9 @@ } }, "node_modules/react-bootstrap": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.3.tgz", - "integrity": "sha512-cc1KAaQyj6Gr3AfA0eRRiUMSlRi3brDVcjc/o0E9y9XNW7ISo8TITrq8G8G3QTFe7VIhCiDt38k99AEFoLOolw==", + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.4.tgz", + "integrity": "sha512-W3398nBM2CBfmGP2evneEO3ZZwEMPtHs72q++eNw60uDGDAdiGn0f9yNys91eo7/y8CTF5Ke1C0QO8JFVPU40Q==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.24.7", @@ -10583,6 +10605,29 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -10626,6 +10671,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -10687,6 +10743,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -10720,24 +10790,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", - "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", - "dev": true, - "dependencies": { - "js-tokens": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", - "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", - "dev": true - }, "node_modules/supercluster": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", @@ -10786,39 +10838,39 @@ "dev": true }, "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, + "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node": ">=18" } }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/text-table": { @@ -10834,18 +10886,20 @@ "dev": true }, "node_modules/tinybench": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", - "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", - "dev": true + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", + "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", + "dev": true, + "license": "MIT" }, "node_modules/tinypool": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.3.tgz", - "integrity": "sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", + "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": "^18.0.0 || >=20.0.0" } }, "node_modules/tinyqueue": { @@ -10853,11 +10907,22 @@ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", + "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -11015,15 +11080,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", @@ -11110,9 +11166,9 @@ } }, "node_modules/typescript": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", - "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -11159,12 +11215,6 @@ "node": "*" } }, - "node_modules/ufo": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", - "dev": true - }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -11342,14 +11392,14 @@ } }, "node_modules/vite": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz", - "integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", + "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.38", + "postcss": "^8.4.39", "rollup": "^4.13.0" }, "bin": { @@ -11398,15 +11448,16 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", "dev": true, + "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0" }, "bin": { @@ -11420,31 +11471,31 @@ } }, "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", + "@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", + "chai": "^5.1.1", + "debug": "^4.3.5", "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" @@ -11458,8 +11509,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@vitest/browser": "2.0.5", + "@vitest/ui": "2.0.5", "happy-dom": "*", "jsdom": "*" }, @@ -11701,10 +11752,11 @@ } }, "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, + "license": "MIT", "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" @@ -11730,6 +11782,25 @@ "node": ">=8" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -11737,9 +11808,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "license": "MIT", "engines": { diff --git a/client/package.json b/client/package.json index 509c25752dd..fe8836e8d36 100644 --- a/client/package.json +++ b/client/package.json @@ -23,33 +23,33 @@ "graphql-request": "7.1.0", "maplibre-gl": "4.5.0", "react": "18.3.1", - "react-bootstrap": "2.10.3", + "react-bootstrap": "2.10.4", "react-dom": "18.3.1", "react-map-gl": "7.1.7" }, "devDependencies": { "@graphql-codegen/cli": "5.0.2", - "@graphql-codegen/client-preset": "4.3.1", + "@graphql-codegen/client-preset": "4.3.3", "@graphql-codegen/introspection": "4.0.3", "@parcel/watcher": "2.4.1", "@testing-library/react": "16.0.0", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", - "@typescript-eslint/eslint-plugin": "7.14.1", - "@typescript-eslint/parser": "7.14.1", + "@typescript-eslint/eslint-plugin": "7.18.0", + "@typescript-eslint/parser": "7.18.0", "@vitejs/plugin-react": "4.3.1", - "@vitest/coverage-v8": "1.6.0", + "@vitest/coverage-v8": "2.0.5", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.29.1", "eslint-plugin-jsx-a11y": "6.9.0", - "eslint-plugin-react": "7.34.3", + "eslint-plugin-react": "7.35.0", "eslint-plugin-react-hooks": "4.6.2", - "eslint-plugin-react-refresh": "0.4.7", - "jsdom": "24.1.0", - "prettier": "3.3.2", - "typescript": "5.5.2", - "vite": "5.3.2", - "vitest": "1.6.0" + "eslint-plugin-react-refresh": "0.4.9", + "jsdom": "24.1.1", + "prettier": "3.3.3", + "typescript": "5.5.4", + "vite": "5.3.5", + "vitest": "2.0.5" } } diff --git a/client/src/components/ItineraryList/InterchangeInfo.tsx b/client/src/components/ItineraryList/InterchangeInfo.tsx new file mode 100644 index 00000000000..b095564d8da --- /dev/null +++ b/client/src/components/ItineraryList/InterchangeInfo.tsx @@ -0,0 +1,22 @@ +import logo from '../../static/img/stay-seated.svg'; +import { Leg } from '../../gql/graphql.ts'; + +/** + * Displays an icon if a leg has a stay-seated transfer from the previous one. + */ +export function InterchangeInfo({ leg }: { leg: Leg }) { + if (leg.interchangeFrom?.staySeated) { + return ( + Stay-seated transfer + ); + } else { + return null; + } +} diff --git a/client/src/components/ItineraryList/ItineraryLegDetails.tsx b/client/src/components/ItineraryList/ItineraryLegDetails.tsx index 5f0e2b381aa..56fdf430388 100644 --- a/client/src/components/ItineraryList/ItineraryLegDetails.tsx +++ b/client/src/components/ItineraryList/ItineraryLegDetails.tsx @@ -2,6 +2,7 @@ import { Leg, Mode } from '../../gql/graphql.ts'; import { LegTime } from './LegTime.tsx'; import { formatDistance } from '../../util/formatDistance.ts'; import { formatDuration } from '../../util/formatDuration.ts'; +import { InterchangeInfo } from './InterchangeInfo.tsx'; export function ItineraryLegDetails({ leg, isLast }: { leg: Leg; isLast: boolean }) { return ( @@ -10,8 +11,12 @@ export function ItineraryLegDetails({ leg, isLast }: { leg: Leg; isLast: boolean {formatDistance(leg.distance)}, {formatDuration(leg.duration)}
- -{' '} - + + -
{leg.mode}{' '} @@ -24,7 +29,12 @@ export function ItineraryLegDetails({ leg, isLast }: { leg: Leg; isLast: boolean )}{' '}
- {leg.mode !== Mode.Foot && {leg.fromPlace.name}} {!isLast && → {leg.toPlace.name}} + {leg.mode !== Mode.Foot && ( + <> + {leg.fromPlace.name} →{' '} + + )}{' '} + {!isLast && {leg.toPlace.name}}
); diff --git a/client/src/components/MapView/MapView.tsx b/client/src/components/MapView/MapView.tsx index 36d98be79b6..9c3761bb530 100644 --- a/client/src/components/MapView/MapView.tsx +++ b/client/src/components/MapView/MapView.tsx @@ -75,7 +75,7 @@ export function MapView({ }} // it's unfortunate that you have to list these layers here. // maybe there is a way around it: https://github.com/visgl/react-map-gl/discussions/2343 - interactiveLayerIds={['regular-stop', 'area-stop', 'group-stop', 'vertex', 'edge', 'link']} + interactiveLayerIds={['regular-stop', 'area-stop', 'group-stop', 'parking-vertex', 'vertex', 'edge', 'link']} onClick={showFeaturePropPopup} // put lat/long in URL and pan to it on page reload hash={true} diff --git a/client/src/hooks/useTripQuery.ts b/client/src/hooks/useTripQuery.ts index c9672d0b6d2..e50e7019b66 100644 --- a/client/src/hooks/useTripQuery.ts +++ b/client/src/hooks/useTripQuery.ts @@ -52,9 +52,15 @@ const query = graphql(` duration fromPlace { name + quay { + id + } } toPlace { name + quay { + id + } } toEstimatedCall { destinationDisplay { @@ -71,6 +77,12 @@ const query = graphql(` pointsOnLink { points } + interchangeTo { + staySeated + } + interchangeFrom { + staySeated + } } systemNotices { tag diff --git a/client/src/static/img/stay-seated.svg b/client/src/static/img/stay-seated.svg new file mode 100644 index 00000000000..a0e451c497f --- /dev/null +++ b/client/src/static/img/stay-seated.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/client/src/util/getColorForMode.ts b/client/src/util/getColorForMode.ts index 79af525e826..cb1ad8b6981 100644 --- a/client/src/util/getColorForMode.ts +++ b/client/src/util/getColorForMode.ts @@ -1,10 +1,10 @@ import { Mode } from '../gql/graphql.ts'; export const getColorForMode = function (mode: Mode) { - if (mode === Mode.Foot) return '#444'; + if (mode === Mode.Foot) return '#191616'; if (mode === Mode.Bicycle) return '#5076D9'; if (mode === Mode.Scooter) return '#253664'; - if (mode === Mode.Car) return '#444'; + if (mode === Mode.Car) return '#7e7e7e'; if (mode === Mode.Rail) return '#86BF8B'; if (mode === Mode.Coach) return '#25642A'; if (mode === Mode.Metro) return '#D9B250'; diff --git a/docs/BuildConfiguration.md b/docs/BuildConfiguration.md index cefd8d2cf2f..6a6b42cd664 100644 --- a/docs/BuildConfiguration.md +++ b/docs/BuildConfiguration.md @@ -78,6 +78,7 @@ Sections follow that describe particular settings in more depth. |    [groupFilePattern](#nd_groupFilePattern) | `regexp` | Pattern for matching group NeTEx files. | *Optional* | `"(\w{3})-.*\.xml"` | 2.0 | |    ignoreFareFrame | `boolean` | Ignore contents of the FareFrame | *Optional* | `false` | 2.3 | |    [ignoreFilePattern](#nd_ignoreFilePattern) | `regexp` | Pattern for matching ignored files in a NeTEx bundle. | *Optional* | `"$^"` | 2.0 | +|    ignoreParking | `boolean` | Ignore Parking elements. | *Optional* | `true` | 2.6 | |    noTransfersOnIsolatedStops | `boolean` | Whether we should allow transfers to and from StopPlaces marked with LimitedUse.ISOLATED | *Optional* | `false` | 2.2 | |    [sharedFilePattern](#nd_sharedFilePattern) | `regexp` | Pattern for matching shared NeTEx files in a NeTEx bundle. | *Optional* | `"shared-data\.xml"` | 2.0 | |    [sharedGroupFilePattern](#nd_sharedGroupFilePattern) | `regexp` | Pattern for matching shared group NeTEx files in a NeTEx bundle. | *Optional* | `"(\w{3})-.*-shared\.xml"` | 2.0 | @@ -106,6 +107,7 @@ Sections follow that describe particular settings in more depth. |       [groupFilePattern](#tf_1_groupFilePattern) | `regexp` | Pattern for matching group NeTEx files. | *Optional* | `"(\w{3})-.*\.xml"` | 2.0 | |       ignoreFareFrame | `boolean` | Ignore contents of the FareFrame | *Optional* | `false` | 2.3 | |       [ignoreFilePattern](#tf_1_ignoreFilePattern) | `regexp` | Pattern for matching ignored files in a NeTEx bundle. | *Optional* | `"$^"` | 2.0 | +|       ignoreParking | `boolean` | Ignore Parking elements. | *Optional* | `true` | 2.6 | |       noTransfersOnIsolatedStops | `boolean` | Whether we should allow transfers to and from StopPlaces marked with LimitedUse.ISOLATED | *Optional* | `false` | 2.2 | |       [sharedFilePattern](#tf_1_sharedFilePattern) | `regexp` | Pattern for matching shared NeTEx files in a NeTEx bundle. | *Optional* | `"shared-data\.xml"` | 2.0 | |       [sharedGroupFilePattern](#tf_1_sharedGroupFilePattern) | `regexp` | Pattern for matching shared group NeTEx files in a NeTEx bundle. | *Optional* | `"(\w{3})-.*-shared\.xml"` | 2.0 | diff --git a/docs/Changelog.md b/docs/Changelog.md index 6d84b377de7..64840e36790 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -46,6 +46,13 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Fix copy-on-write in TimetableSnapshot [#5941](https://github.com/opentripplanner/OpenTripPlanner/pull/5941) - Generate documentation for OSM tag mappers [#5929](https://github.com/opentripplanner/OpenTripPlanner/pull/5929) - Disable Legacy REST API by default [#5948](https://github.com/opentripplanner/OpenTripPlanner/pull/5948) +- Enforce non-null coordinates on multimodal station [#5971](https://github.com/opentripplanner/OpenTripPlanner/pull/5971) +- Add car rental to Transmodel street mode options [#5977](https://github.com/opentripplanner/OpenTripPlanner/pull/5977) +- Add debug information for stop/quay ID and stay-seated transfers [#5962](https://github.com/opentripplanner/OpenTripPlanner/pull/5962) +- Handle NeTEx `any` version [#5983](https://github.com/opentripplanner/OpenTripPlanner/pull/5983) +- Keep at least one result for min-transfers and each transit-group in itinerary-group-filter [#5919](https://github.com/opentripplanner/OpenTripPlanner/pull/5919) +- Extract parking lots from NeTEx feeds [#5946](https://github.com/opentripplanner/OpenTripPlanner/pull/5946) +- Filter routes and patterns by service date in GTFS GraphQL API [#5869](https://github.com/opentripplanner/OpenTripPlanner/pull/5869) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.5.0 (2024-03-13) diff --git a/docs/Configuration.md b/docs/Configuration.md index a4e8b5100a4..7e48dab5344 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -219,36 +219,37 @@ Here is a list of all features which can be toggled on/off and their default val -| Feature | Description | Enabled by default | Sandbox | -|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------:|:-------:| -| `APIBikeRental` | Enable the bike rental endpoint. | ✓️ | | -| `APIServerInfo` | Enable the server info endpoint. | ✓️ | | -| `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. | ✓️ | | -| `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. | | | -| `MinimumTransferTimeIsDefinitive` | If the minimum transfer time is a lower bound (default) or the definitive time for the transfer. Set this to `true` if you want to set a transfer time lower than what OTP derives from OSM data. | | | -| `OptimizeTransfers` | OTP will inspect all itineraries found and optimize where (which stops) the transfer will happen. Waiting time, priority and guaranteed transfers are taken into account. | ✓️ | | -| `ParallelRouting` | Enable performing parts of the trip planning in parallel. | | | -| `TransferConstraints` | Enforce transfers to happen according to the _transfers.txt_ (GTFS) and Interchanges (NeTEx). Turning this _off_ will increase the routing performance a little. | ✓️ | | -| `TransmodelGraphQlApi` | Enable the [Transmodel (NeTEx) GraphQL API](apis/TransmodelApi.md). | ✓️ | ✓️ | -| `ActuatorAPI` | Endpoint for actuators (service health status). | | ✓️ | -| `AsyncGraphQLFetchers` | Whether the @async annotation in the GraphQL schema should lead to the fetch being executed asynchronously. This allows batch or alias queries to run in parallel at the cost of consuming extra threads. | | | -| `Co2Emissions` | Enable the emissions sandbox module. | | ✓️ | -| `DataOverlay` | Enable usage of data overlay when calculating costs for the street network. | | ✓️ | -| `FaresV2` | Enable import of GTFS-Fares v2 data. | | ✓️ | -| `FlexRouting` | Enable FLEX routing. | | ✓️ | -| `GoogleCloudStorage` | Enable Google Cloud Storage integration. | | ✓️ | -| `LegacyRestApi` | Enable legacy REST API. This API will be removed in the future. | | ✓️ | -| `RealtimeResolver` | When routing with ignoreRealtimeUpdates=true, add an extra step which populates results with real-time data | | ✓️ | -| `ReportApi` | Enable the report API. | | ✓️ | -| `RestAPIPassInDefaultConfigAsJson` | Enable a default RouteRequest to be passed in as JSON on the REST API - FOR DEBUGGING ONLY! | | | -| `SandboxAPIGeocoder` | Enable the Geocoder API. | | ✓️ | -| `SandboxAPIMapboxVectorTilesApi` | Enable Mapbox vector tiles API. | | ✓️ | -| `SandboxAPIParkAndRideApi` | Enable park-and-ride endpoint. | | ✓️ | -| `TransferAnalyzer` | Analyze transfers during graph build. | | ✓️ | +| Feature | Description | Enabled by default | Sandbox | +|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------:|:-------:| +| `APIBikeRental` | Enable the bike rental endpoint. | ✓️ | | +| `APIServerInfo` | Enable the server info endpoint. | ✓️ | | +| `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. | ✓️ | | +| `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. | | | +| `MinimumTransferTimeIsDefinitive` | If the minimum transfer time is a lower bound (default) or the definitive time for the transfer. Set this to `true` if you want to set a transfer time lower than what OTP derives from OSM data. | | | +| `OptimizeTransfers` | OTP will inspect all itineraries found and optimize where (which stops) the transfer will happen. Waiting time, priority and guaranteed transfers are taken into account. | ✓️ | | +| `ParallelRouting` | Enable performing parts of the trip planning in parallel. | | | +| `TransferConstraints` | Enforce transfers to happen according to the _transfers.txt_ (GTFS) and Interchanges (NeTEx). Turning this _off_ will increase the routing performance a little. | ✓️ | | +| `TransmodelGraphQlApi` | Enable the [Transmodel (NeTEx) GraphQL API](apis/TransmodelApi.md). | ✓️ | ✓️ | +| `ActuatorAPI` | Endpoint for actuators (service health status). | | ✓️ | +| `AsyncGraphQLFetchers` | Whether the @async annotation in the GraphQL schema should lead to the fetch being executed asynchronously. This allows batch or alias queries to run in parallel at the cost of consuming extra threads. | | | +| `Co2Emissions` | Enable the emissions sandbox module. | | ✓️ | +| `DataOverlay` | Enable usage of data overlay when calculating costs for the street network. | | ✓️ | +| `FaresV2` | Enable import of GTFS-Fares v2 data. | | ✓️ | +| `FlexRouting` | Enable FLEX routing. | | ✓️ | +| `GoogleCloudStorage` | Enable Google Cloud Storage integration. | | ✓️ | +| `LegacyRestApi` | Enable legacy REST API. This API will be removed in the future. | | ✓️ | +| `MultiCriteriaGroupMaxFilter` | Keep the best itinerary with respect to each criteria used in the transit-routing search. For example the itinerary with the lowest cost, fewest transfers, and each unique transit-group (transit-group-priority) is kept, even if the max-limit is exceeded. This is turned off by default for now, until this feature is well tested. | | | +| `RealtimeResolver` | When routing with ignoreRealtimeUpdates=true, add an extra step which populates results with real-time data | | ✓️ | +| `ReportApi` | Enable the report API. | | ✓️ | +| `RestAPIPassInDefaultConfigAsJson` | Enable a default RouteRequest to be passed in as JSON on the REST API - FOR DEBUGGING ONLY! | | | +| `SandboxAPIGeocoder` | Enable the Geocoder API. | | ✓️ | +| `SandboxAPIMapboxVectorTilesApi` | Enable Mapbox vector tiles API. | | ✓️ | +| `SandboxAPIParkAndRideApi` | Enable park-and-ride endpoint. | | ✓️ | +| `TransferAnalyzer` | Analyze transfers during graph build. | | ✓️ | diff --git a/docs/Container-Image.md b/docs/Container-Image.md index d8e6db6d614..ed2441f5ac1 100644 --- a/docs/Container-Image.md +++ b/docs/Container-Image.md @@ -4,7 +4,9 @@ The CI pipeline deploys container images for runtimes like Docker, Kubernetes or [Dockerhub](https://hub.docker.com/r/opentripplanner/opentripplanner/tags). The image assumes you use a volume to mount the input data (GTFS/NeTex, OSM) and config files into -`/var/opentripplanner/`. When serving a graph it's also expected to be in this directory. +`/var/opentripplanner/`. When serving a graph it's also expected to be in this directory. If a logback +extensions file needs to be used, it should be mounted to the root location `/logback-include-extensions.xml` +instead of the `/var/opentripplanner/` directory. ## Quick start diff --git a/docs/RouteRequest.md b/docs/RouteRequest.md index 14d937eeb63..a8baaf1603b 100644 --- a/docs/RouteRequest.md +++ b/docs/RouteRequest.md @@ -254,7 +254,7 @@ This is a performance limit and should therefore be set high. Results close to t guaranteed to be optimal. Use itinerary-filters to limit what is presented to the client. The duration can be set per mode(`maxDirectStreetDurationForMode`), because some street modes searches are much more resource intensive than others. A default value is applied if the mode specific value -do not exist." +does not exist."

maxJourneyDuration

@@ -403,7 +403,7 @@ This is a performance limit and should therefore be set high. Results close to t guaranteed to be optimal. Use itinerary-filters to limit what is presented to the client. The duration can be set per mode(`maxDurationForMode`), because some street modes searches are much more resource intensive than others. A default value is applied if the mode specific value -do not exist. +does not exist.

maxStopCount

diff --git a/docs/apis/Apis.md b/docs/apis/Apis.md index ab6b41a25cd..9e8f31f6eeb 100644 --- a/docs/apis/Apis.md +++ b/docs/apis/Apis.md @@ -25,5 +25,5 @@ The [Geocoder API](../sandbox/GeocoderAPI.md) allows you to geocode stop names a The OTP REST API used to power many apps and frontends. For years it was the only way to access OTP programmatically. -Over time it has been replaced by the GraphQL APIs and is scheduled to be disabled by default -and eventually removed completely. It's therefore not recommended to use it. +Over time it has been replaced by the GraphQL APIs and is now disabled by default +and will eventually be removed completely. It's therefore not recommended to use it. diff --git a/docs/examples/entur/build-config.json b/docs/examples/entur/build-config.json index e9351882774..2acea588234 100644 --- a/docs/examples/entur/build-config.json +++ b/docs/examples/entur/build-config.json @@ -33,7 +33,8 @@ { "type": "netex", "source": "gs://${OTP_GCS_BUCKET}/outbound/netex/rb_norway-aggregated-netex-otp2.zip", - "feedId": "EN" + "feedId": "EN", + "ignoreParking": true } ], "osm": [ diff --git a/magidoc.mjs b/magidoc.mjs index 4d9e8c98a7f..595ba25c0c0 100644 --- a/magidoc.mjs +++ b/magidoc.mjs @@ -37,6 +37,7 @@ To learn how to deactivate it, read the 'Polyline': '<>', 'GeoJson': '<>', 'OffsetDateTime': '2024-02-05T18:04:23+01:00', + 'LocalDate': '2024-05-24', 'Duration': 'PT10M', 'CoordinateValue': 19.24, 'Reluctance': 3.1, @@ -44,7 +45,6 @@ To learn how to deactivate it, read the 'Cost': 100, 'Ratio': 0.25, 'Locale': 'en' - }, } }, diff --git a/pom.xml b/pom.xml index 586f835c860..db6167578cd 100644 --- a/pom.xml +++ b/pom.xml @@ -56,15 +56,15 @@ - 154 + 156 - 31.2 + 31.3 2.51.1 2.17.2 3.1.7 5.10.3 1.13.2 - 5.5.3 + 5.6.0 1.5.6 9.11.1 2.0.13 @@ -252,7 +252,7 @@ me.fabriciorby maven-surefire-junit5-tree-reporter - 1.2.1 + 1.3.0 @@ -328,7 +328,7 @@ but we need the Maven project version as well, so we perform substitution. --> io.github.git-commit-id git-commit-id-maven-plugin - 9.0.0 + 9.0.1 @@ -698,6 +698,12 @@ ${junit.version} test + + com.google.truth + truth + 1.4.2 + test + com.tngtech.archunit archunit diff --git a/renovate.json5 b/renovate.json5 index 7b957c577e3..98ec1f24fbe 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -44,6 +44,23 @@ }, // some dependencies that we auto-merge release very often and even the auto-merges create a lot of // noise, so we slow it down a bit + { + "matchPackageNames": [ + "org.mockito:mockito-core", + "com.tngtech.archunit:archunit", + "org.apache.maven.plugins:maven-surefire-plugin", + "me.fabriciorby:maven-surefire-junit5-tree-reporter", + "com.google.truth:truth", + "org.jacoco:jacoco-maven-plugin", // coverage plugin + "org.apache.commons:commons-compress" // only used by tests + ], + "matchPackagePrefixes": [ + "org.junit.jupiter:", + ], + "groupName": "Test dependencies", + "automerge": true, + "schedule": "on the 17th day of the month" + }, { "matchPackageNames": [ "org.mobilitydata:gbfs-java-model" @@ -110,11 +127,6 @@ { "description": "automatically merge test, logging and build dependencies", "matchPackageNames": [ - "org.mockito:mockito-core", - "com.tngtech.archunit:archunit", - "org.apache.maven.plugins:maven-surefire-plugin", - "org.jacoco:jacoco-maven-plugin", // coverage plugin - "org.apache.commons:commons-compress", // only used by tests // maven plugins "org.codehaus.mojo:build-helper-maven-plugin", "org.apache.maven.plugins:maven-source-plugin", @@ -126,7 +138,6 @@ "org.sonatype.plugins:nexus-staging-maven-plugin" ], "matchPackagePrefixes": [ - "org.junit.jupiter:", "org.slf4j:" ], "automerge": true, diff --git a/src/client/index.html b/src/client/index.html index d406984f024..d1734898ca0 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/flex/trip/ScheduledDeviatedTripTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java index 3971d03ac6d..6466d7bf6d6 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java @@ -41,14 +41,15 @@ import org.opentripplanner.street.search.request.StreetSearchRequest; import org.opentripplanner.street.search.state.State; import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; import org.opentripplanner.transit.model.site.AreaStop; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; /** * This tests that the feed for the Cobb County Flex service is processed correctly. This service - * contains both flex zones but also scheduled stops. Inside the zone passengers can get on or off - * anywhere so there it works more like a taxi. + * contains both flex zones but also scheduled stops. Inside the zone, passengers can get on or off + * anywhere, so there it works more like a taxi. *

* Read about the details at: https://www.cobbcounty.org/transportation/cobblinc/routes-and-schedules/flex */ @@ -212,6 +213,7 @@ private static List getItineraries( var result = TransitRouter.route( request, serverContext, + TransitGroupPriorityService.empty(), transitStartOfTime, additionalSearchDays, new DebugTimingAggregator() diff --git a/src/ext-test/java/org/opentripplanner/ext/geocoder/LuceneIndexTest.java b/src/ext-test/java/org/opentripplanner/ext/geocoder/LuceneIndexTest.java index cee6cf3a2d8..3e6c2b15195 100644 --- a/src/ext-test/java/org/opentripplanner/ext/geocoder/LuceneIndexTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/geocoder/LuceneIndexTest.java @@ -18,6 +18,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationRepository; +import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationService; import org.opentripplanner.model.FeedInfo; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.basic.TransitMode; @@ -160,8 +162,12 @@ public FeedInfo getFeedInfo(String feedId) { ); } }; - index = new LuceneIndex(transitService); - mapper = new StopClusterMapper(transitService); + var stopConsolidationService = new DefaultStopConsolidationService( + new DefaultStopConsolidationRepository(), + transitModel + ); + index = new LuceneIndex(transitService, stopConsolidationService); + mapper = new StopClusterMapper(transitService, stopConsolidationService); } @Test diff --git a/src/ext-test/java/org/opentripplanner/ext/geocoder/StopClusterMapperTest.java b/src/ext-test/java/org/opentripplanner/ext/geocoder/StopClusterMapperTest.java new file mode 100644 index 00000000000..578f7d4118f --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/geocoder/StopClusterMapperTest.java @@ -0,0 +1,57 @@ +package org.opentripplanner.ext.geocoder; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationRepository; +import org.opentripplanner.ext.stopconsolidation.internal.DefaultStopConsolidationService; +import org.opentripplanner.ext.stopconsolidation.model.ConsolidatedStopGroup; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.framework.Deduplicator; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.service.DefaultTransitService; +import org.opentripplanner.transit.service.StopModel; +import org.opentripplanner.transit.service.TransitModel; + +class StopClusterMapperTest { + + private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); + private static final RegularStop STOP_A = TEST_MODEL.stop("A").build(); + private static final RegularStop STOP_B = TEST_MODEL.stop("B").build(); + private static final RegularStop STOP_C = TEST_MODEL.stop("C").build(); + private static final List STOPS = List.of(STOP_A, STOP_B, STOP_C); + private static final StopModel STOP_MODEL = TEST_MODEL + .stopModelBuilder() + .withRegularStops(STOPS) + .build(); + private static final TransitModel TRANSIT_MODEL = new TransitModel( + STOP_MODEL, + new Deduplicator() + ); + private static final List LOCATIONS = STOPS + .stream() + .map(StopLocation.class::cast) + .toList(); + + @Test + void clusterConsolidatedStops() { + var repo = new DefaultStopConsolidationRepository(); + repo.addGroups(List.of(new ConsolidatedStopGroup(STOP_A.getId(), List.of(STOP_B.getId())))); + + var service = new DefaultStopConsolidationService(repo, TRANSIT_MODEL); + var mapper = new StopClusterMapper(new DefaultTransitService(TRANSIT_MODEL), service); + + var clusters = mapper.generateStopClusters(LOCATIONS, List.of()); + + var expected = new LuceneStopCluster( + STOP_A.getId().toString(), + List.of(STOP_B.getId().toString()), + List.of(STOP_A.getName(), STOP_B.getName()), + List.of(STOP_A.getCode(), STOP_B.getCode()), + new StopCluster.Coordinate(STOP_A.getLat(), STOP_A.getLon()) + ); + assertThat(clusters).contains(expected); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/siri/AddedTripBuilderTest.java b/src/ext-test/java/org/opentripplanner/ext/siri/AddedTripBuilderTest.java index b7431c16091..59995062eb7 100644 --- a/src/ext-test/java/org/opentripplanner/ext/siri/AddedTripBuilderTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/siri/AddedTripBuilderTest.java @@ -35,6 +35,7 @@ import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.StopModel; +import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.transit.service.TransitModel; import org.opentripplanner.updater.spi.UpdateError; import uk.org.siri.siri20.VehicleModesEnumeration; @@ -77,6 +78,7 @@ class AddedTripBuilderTest { private final Deduplicator DEDUPLICATOR = new Deduplicator(); private final TransitModel TRANSIT_MODEL = new TransitModel(STOP_MODEL, DEDUPLICATOR); + private TransitEditorService transitService; private EntityResolver ENTITY_RESOLVER; @BeforeEach @@ -101,6 +103,7 @@ void setUp() { // Create transit model index TRANSIT_MODEL.index(); + transitService = new DefaultTransitService(TRANSIT_MODEL); // Create the entity resolver only after the model has been indexed ENTITY_RESOLVER = @@ -110,7 +113,7 @@ void setUp() { @Test void testAddedTrip() { var addedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, @@ -151,37 +154,33 @@ void testAddedTrip() { assertEquals(SubMode.of(SUB_MODE), route.getNetexSubmode(), "submode should be mapped"); assertNotEquals(REPLACED_ROUTE, route, "Should not re-use replaced route"); - // Assert transit model index - var transitModelIndex = TRANSIT_MODEL.getTransitModelIndex(); - assertNotNull(transitModelIndex); assertEquals( route, - transitModelIndex.getRouteForId(TransitModelForTest.id(LINE_REF)), + transitService.getRouteForId(TransitModelForTest.id(LINE_REF)), "Route should be added to transit index" ); assertEquals( trip, - transitModelIndex.getTripForId().get(TRIP_ID), + transitService.getTripForId(TRIP_ID), "Route should be added to transit index" ); - var pattern = transitModelIndex.getPatternForTrip().get(trip); + var pattern = transitService.getPatternForTrip(trip); assertNotNull(pattern); assertEquals(route, pattern.getRoute()); assertTrue( - transitModelIndex - .getServiceCodesRunningForDate() - .get(SERVICE_DATE) + transitService + .getServiceCodesRunningForDate(SERVICE_DATE) .contains(TRANSIT_MODEL.getServiceCodes().get(trip.getServiceId())), "serviceId should be running on service date" ); assertNotNull( - transitModelIndex.getTripOnServiceDateById().get(TRIP_ID), + transitService.getTripOnServiceDateById(TRIP_ID), "TripOnServiceDate should be added to transit index by id" ); assertNotNull( - transitModelIndex - .getTripOnServiceDateForTripAndDay() - .get(new TripIdAndServiceDate(TRIP_ID, SERVICE_DATE)), + transitService.getTripOnServiceDateForTripAndDay( + new TripIdAndServiceDate(TRIP_ID, SERVICE_DATE) + ), "TripOnServiceDate should be added to transit index for trip and day" ); @@ -239,7 +238,7 @@ void testAddedTrip() { @Test void testAddedTripOnAddedRoute() { var firstAddedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, @@ -265,7 +264,7 @@ void testAddedTripOnAddedRoute() { var tripId2 = TransitModelForTest.id("TRIP_ID_2"); var secondAddedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, tripId2, @@ -296,10 +295,7 @@ void testAddedTripOnAddedRoute() { Route route = secondTrip.getRoute(); assertSame(firstTrip.getRoute(), route, "route be reused from the first trip"); - // Assert transit model index - var transitModelIndex = TRANSIT_MODEL.getTransitModelIndex(); - assertNotNull(transitModelIndex); - assertEquals(2, transitModelIndex.getPatternsForRoute().get(route).size()); + assertEquals(2, transitService.getPatternsForRoute(route).size()); // Assert trip times var times = secondAddedTrip.successValue().tripTimes(); @@ -316,7 +312,7 @@ void testAddedTripOnAddedRoute() { @Test void testAddedTripOnExistingRoute() { var addedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, @@ -347,7 +343,7 @@ void testAddedTripOnExistingRoute() { @Test void testAddedTripWithoutReplacedRoute() { var addedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, @@ -390,7 +386,7 @@ void testAddedTripWithoutReplacedRoute() { @Test void testAddedTripFailOnMissingServiceId() { var addedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, @@ -445,7 +441,7 @@ void testAddedTripFailOnNonIncreasingDwellTime() { ); var addedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, @@ -484,7 +480,7 @@ void testAddedTripFailOnTooFewCalls() { .build() ); var addedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, @@ -531,7 +527,7 @@ void testAddedTripFailOnUnknownStop() { .build() ); var addedTrip = new AddedTripBuilder( - TRANSIT_MODEL, + transitService, ENTITY_RESOLVER, AbstractTransitEntity::getId, TRIP_ID, diff --git a/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java b/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java index 7eb44302fb9..45e7979b353 100644 --- a/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java @@ -1,13 +1,13 @@ package org.opentripplanner.ext.siri; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertFailure; import java.util.Set; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.opentripplanner.transit.model.timetable.RealTimeState; import org.opentripplanner.updater.spi.UpdateError; -import org.opentripplanner.updater.spi.UpdateResult; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; class SiriTimetableSnapshotSourceTest { @@ -415,10 +415,6 @@ void testExtraUnknownStop() { assertFailure(UpdateError.UpdateErrorType.INVALID_STOP_SEQUENCE, result); } - private void assertFailure(UpdateError.UpdateErrorType expectedError, UpdateResult result) { - assertEquals(Set.of(expectedError), result.failures().keySet()); - } - private static SiriEtBuilder updatedJourneyBuilder(RealtimeTestEnvironment env) { return new SiriEtBuilder(env.getDateTimeHelper()) .withEstimatedCalls(builder -> diff --git a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerTest.java b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerTest.java index 4c3e60701bd..7760eee13b8 100644 --- a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerTest.java @@ -1,56 +1,60 @@ package org.opentripplanner.ext.vectortiles.layers.stops; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; import java.util.HashMap; import java.util.Locale; import java.util.Map; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.opentripplanner.ext.vectortiles.layers.TestTransitService; +import org.opentripplanner.framework.geometry.WgsCoordinate; +import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.TranslatedString; 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.site.Station; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.StopModel; import org.opentripplanner.transit.service.TransitModel; public class StopsLayerTest { - private RegularStop stop; + private static final I18NString NAME_TRANSLATIONS = TranslatedString.getI18NString( + new HashMap<>() { + { + put(null, "name"); + put("de", "nameDE"); + } + }, + false, + false + ); + private static final I18NString DESC_TRANSLATIONS = TranslatedString.getI18NString( + new HashMap<>() { + { + put(null, "desc"); + put("de", "descDE"); + } + }, + false, + false + ); - @BeforeEach - public void setUp() { - var nameTranslations = TranslatedString.getI18NString( - new HashMap<>() { - { - put(null, "name"); - put("de", "nameDE"); - } - }, - false, - false - ); - var descTranslations = TranslatedString.getI18NString( - new HashMap<>() { - { - put(null, "desc"); - put("de", "descDE"); - } - }, - false, - false - ); - stop = - StopModel - .of() - .regularStop(new FeedScopedId("F", "name")) - .withName(nameTranslations) - .withDescription(descTranslations) - .withCoordinate(50, 10) - .build(); - } + private static final Station STATION = Station + .of(id("station1")) + .withCoordinate(WgsCoordinate.GREENWICH) + .withName(I18NString.of("A Station")) + .build(); + private static final RegularStop STOP = StopModel + .of() + .regularStop(new FeedScopedId("F", "name")) + .withName(NAME_TRANSLATIONS) + .withDescription(DESC_TRANSLATIONS) + .withCoordinate(50, 10) + .withParentStation(STATION) + .build(); @Test public void digitransitStopPropertyMapperTest() { @@ -65,12 +69,13 @@ public void digitransitStopPropertyMapperTest() { ); Map map = new HashMap<>(); - mapper.map(stop).forEach(o -> map.put(o.key(), o.value())); + mapper.map(STOP).forEach(o -> map.put(o.key(), o.value())); assertEquals("F:name", map.get("gtfsId")); assertEquals("name", map.get("name")); assertEquals("desc", map.get("desc")); assertEquals("[{\"gtfsType\":100}]", map.get("routes")); + assertEquals(STATION.getId().toString(), map.get("parentStation")); } @Test @@ -86,7 +91,7 @@ public void digitransitStopPropertyMapperTranslationTest() { ); Map map = new HashMap<>(); - mapper.map(stop).forEach(o -> map.put(o.key(), o.value())); + mapper.map(STOP).forEach(o -> map.put(o.key(), o.value())); assertEquals("nameDE", map.get("name")); assertEquals("descDE", map.get("desc")); diff --git a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingGroupsLayerTest.java b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingGroupsLayerTest.java index 60ba9100f43..1beca037457 100644 --- a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingGroupsLayerTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingGroupsLayerTest.java @@ -125,8 +125,8 @@ public void vehicleParkingGroupGeometryTest() { assertEquals("[POINT (1.1 1.9)]", geometries.toString()); assertEquals( - "VehicleParkingAndGroup[vehicleParkingGroup=VehicleParkingGroup{name: 'groupName', coordinate: (1.9, 1.1)}, vehicleParking=[VehicleParking{name: 'name', coordinate: (2.0, 1.0)}]]", - geometries.get(0).getUserData().toString() + "VehicleParkingAndGroup[vehicleParkingGroup=VehicleParkingGroup{name: 'groupName', coordinate: (1.9, 1.1)}, vehicleParking=[VehicleParking{id: 'F:id', name: 'name', coordinate: (2.0, 1.0)}]]", + geometries.getFirst().getUserData().toString() ); } diff --git a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java index fdb723b3dc7..14e96e2aa28 100644 --- a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java @@ -117,8 +117,8 @@ public void vehicleParkingGeometryTest() { assertEquals("[POINT (1 2)]", geometries.toString()); assertEquals( - "VehicleParking{name: 'default name', coordinate: (2.0, 1.0)}", - geometries.get(0).getUserData().toString() + "VehicleParking{id: 'F:id', name: 'default name', coordinate: (2.0, 1.0)}", + geometries.getFirst().getUserData().toString() ); } diff --git a/src/ext/java/org/opentripplanner/ext/geocoder/GeocoderResource.java b/src/ext/java/org/opentripplanner/ext/geocoder/GeocoderResource.java index f5d1f950632..304829843ae 100644 --- a/src/ext/java/org/opentripplanner/ext/geocoder/GeocoderResource.java +++ b/src/ext/java/org/opentripplanner/ext/geocoder/GeocoderResource.java @@ -25,10 +25,10 @@ @Produces(MediaType.APPLICATION_JSON) public class GeocoderResource { - private final OtpServerRequestContext serverContext; + private final LuceneIndex luceneIndex; public GeocoderResource(@Context OtpServerRequestContext requestContext) { - serverContext = requestContext; + luceneIndex = requestContext.lucenceIndex(); } /** @@ -71,7 +71,7 @@ public Response textSearch( @GET @Path("stopClusters") public Response stopClusters(@QueryParam("query") String query) { - var clusters = LuceneIndex.forServer(serverContext).queryStopClusters(query).toList(); + var clusters = luceneIndex.queryStopClusters(query).toList(); return Response.status(Response.Status.OK).entity(clusters).build(); } @@ -96,8 +96,7 @@ private List query( } private Collection queryStopLocations(String query, boolean autocomplete) { - return LuceneIndex - .forServer(serverContext) + return luceneIndex .queryStopLocations(query, autocomplete) .map(sl -> new SearchResult( @@ -111,8 +110,7 @@ private Collection queryStopLocations(String query, boolean autoco } private Collection queryStations(String query, boolean autocomplete) { - return LuceneIndex - .forServer(serverContext) + return luceneIndex .queryStopLocationGroups(query, autocomplete) .map(sc -> new SearchResult( diff --git a/src/ext/java/org/opentripplanner/ext/geocoder/LuceneIndex.java b/src/ext/java/org/opentripplanner/ext/geocoder/LuceneIndex.java index e0ea8ba36b9..fe7bef8ad13 100644 --- a/src/ext/java/org/opentripplanner/ext/geocoder/LuceneIndex.java +++ b/src/ext/java/org/opentripplanner/ext/geocoder/LuceneIndex.java @@ -11,6 +11,7 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Stream; +import javax.annotation.Nullable; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.en.EnglishAnalyzer; import org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper; @@ -40,12 +41,14 @@ import org.apache.lucene.search.suggest.document.FuzzyCompletionQuery; import org.apache.lucene.search.suggest.document.SuggestIndexSearcher; import org.apache.lucene.store.ByteBuffersDirectory; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; import org.opentripplanner.framework.collection.ListUtils; import org.opentripplanner.framework.i18n.I18NString; -import org.opentripplanner.standalone.api.OtpServerRequestContext; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.site.StopLocationsGroup; +import org.opentripplanner.transit.service.DefaultTransitService; +import org.opentripplanner.transit.service.TransitModel; import org.opentripplanner.transit.service.TransitService; public class LuceneIndex implements Serializable { @@ -65,9 +68,24 @@ public class LuceneIndex implements Serializable { private final SuggestIndexSearcher searcher; private final StopClusterMapper stopClusterMapper; - public LuceneIndex(TransitService transitService) { + /** + * Since the {@link TransitService} is request scoped, we don't inject it into this class. + * However, we do need some methods in the service and that's why we instantiate it manually in this + * constructor. + */ + public LuceneIndex(TransitModel transitModel, StopConsolidationService stopConsolidationService) { + this(new DefaultTransitService(transitModel), stopConsolidationService); + } + + /** + * This method is only visible for testing. + */ + LuceneIndex( + TransitService transitService, + @Nullable StopConsolidationService stopConsolidationService + ) { this.transitService = transitService; - this.stopClusterMapper = new StopClusterMapper(transitService); + this.stopClusterMapper = new StopClusterMapper(transitService, stopConsolidationService); this.analyzer = new PerFieldAnalyzerWrapper( @@ -144,18 +162,6 @@ public LuceneIndex(TransitService transitService) { } } - public static synchronized LuceneIndex forServer(OtpServerRequestContext serverContext) { - var graph = serverContext.graph(); - var existingIndex = graph.getLuceneIndex(); - if (existingIndex != null) { - return existingIndex; - } - - var newIndex = new LuceneIndex(serverContext.transitService()); - graph.setLuceneIndex(newIndex); - return newIndex; - } - public Stream queryStopLocations(String query, boolean autocomplete) { return matchingDocuments(StopLocation.class, query, autocomplete) .map(document -> transitService.getStopLocation(FeedScopedId.parse(document.get(ID)))); @@ -252,6 +258,7 @@ private Stream matchingDocuments( String searchTerms, boolean autocomplete ) { + searchTerms = searchTerms.strip(); try { if (autocomplete) { var completionQuery = new FuzzyCompletionQuery( diff --git a/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java b/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java index d9f388ea0e8..98a617b809f 100644 --- a/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java +++ b/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java @@ -3,6 +3,7 @@ import static org.opentripplanner.ext.geocoder.StopCluster.LocationType.STATION; import static org.opentripplanner.ext.geocoder.StopCluster.LocationType.STOP; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.Iterables; import java.util.Collection; import java.util.List; @@ -10,6 +11,8 @@ import java.util.Optional; import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; +import org.opentripplanner.ext.stopconsolidation.model.StopReplacement; import org.opentripplanner.framework.collection.ListUtils; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; @@ -28,9 +31,14 @@ class StopClusterMapper { private final TransitService transitService; + private final StopConsolidationService stopConsolidationService; - StopClusterMapper(TransitService transitService) { + StopClusterMapper( + TransitService transitService, + @Nullable StopConsolidationService stopConsolidationService + ) { this.transitService = transitService; + this.stopConsolidationService = stopConsolidationService; } /** @@ -45,16 +53,71 @@ Iterable generateStopClusters( Collection stopLocations, Collection stopLocationsGroups ) { + var stopClusters = buildStopClusters(stopLocations); + var stationClusters = buildStationClusters(stopLocationsGroups); + var consolidatedStopClusters = buildConsolidatedStopClusters(); + + return Iterables.concat(stopClusters, stationClusters, consolidatedStopClusters); + } + + private Iterable buildConsolidatedStopClusters() { + var multiMap = stopConsolidationService + .replacements() + .stream() + .collect( + ImmutableListMultimap.toImmutableListMultimap( + StopReplacement::primary, + StopReplacement::secondary + ) + ); + return multiMap + .keySet() + .stream() + .map(primary -> { + var secondaryIds = multiMap.get(primary); + var secondaries = secondaryIds + .stream() + .map(transitService::getStopLocation) + .filter(Objects::nonNull) + .toList(); + var codes = ListUtils.combine( + ListUtils.ofNullable(primary.getCode()), + getCodes(secondaries) + ); + var names = ListUtils.combine( + ListUtils.ofNullable(primary.getName()), + getNames(secondaries) + ); + + return new LuceneStopCluster( + primary.getId().toString(), + secondaryIds.stream().map(id -> id.toString()).toList(), + names, + codes, + toCoordinate(primary.getCoordinate()) + ); + }) + .toList(); + } + + private static List buildStationClusters( + Collection stopLocationsGroups + ) { + return stopLocationsGroups.stream().map(StopClusterMapper::map).toList(); + } + + private List buildStopClusters(Collection stopLocations) { List stops = stopLocations .stream() // remove stop locations without a parent station .filter(sl -> sl.getParentStation() == null) + .filter(sl -> !stopConsolidationService.isPartOfConsolidatedStop(sl)) // stops without a name (for example flex areas) are useless for searching, so we remove them, too .filter(sl -> sl.getName() != null) .toList(); // if they are very close to each other and have the same name, only one is chosen (at random) - var deduplicatedStops = stops + return stops .stream() .collect( Collectors.groupingBy(sl -> @@ -66,9 +129,6 @@ Iterable generateStopClusters( .map(group -> map(group).orElse(null)) .filter(Objects::nonNull) .toList(); - var stations = stopLocationsGroups.stream().map(StopClusterMapper::map).toList(); - - return Iterables.concat(deduplicatedStops, stations); } private static LuceneStopCluster map(StopLocationsGroup g) { diff --git a/src/ext/java/org/opentripplanner/ext/geocoder/configure/GeocoderModule.java b/src/ext/java/org/opentripplanner/ext/geocoder/configure/GeocoderModule.java new file mode 100644 index 00000000000..9eaf6ade8e5 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/geocoder/configure/GeocoderModule.java @@ -0,0 +1,31 @@ +package org.opentripplanner.ext.geocoder.configure; + +import dagger.Module; +import dagger.Provides; +import jakarta.inject.Singleton; +import javax.annotation.Nullable; +import org.opentripplanner.ext.geocoder.LuceneIndex; +import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; +import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.transit.service.TransitModel; + +/** + * This module builds the Lucene geocoder based on whether the feature flag is on or off. + */ +@Module +public class GeocoderModule { + + @Provides + @Singleton + @Nullable + LuceneIndex luceneIndex( + TransitModel transitModel, + @Nullable StopConsolidationService stopConsolidationService + ) { + if (OTPFeature.SandboxAPIGeocoder.isOn()) { + return new LuceneIndex(transitModel, stopConsolidationService); + } else { + return null; + } + } +} diff --git a/src/ext/java/org/opentripplanner/ext/reportapi/model/TransitGroupPriorityReport.java b/src/ext/java/org/opentripplanner/ext/reportapi/model/TransitGroupPriorityReport.java index 635469cb3a2..66ad29fad56 100644 --- a/src/ext/java/org/opentripplanner/ext/reportapi/model/TransitGroupPriorityReport.java +++ b/src/ext/java/org/opentripplanner/ext/reportapi/model/TransitGroupPriorityReport.java @@ -4,9 +4,9 @@ import java.util.TreeMap; import java.util.TreeSet; import java.util.stream.Collectors; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.PriorityGroupConfigurator; import org.opentripplanner.routing.api.request.request.TransitRequest; import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; /** * This class is used to report all transit-groups used for transit-group-priority. The report is @@ -17,14 +17,14 @@ public class TransitGroupPriorityReport { public static String build(Collection patterns, TransitRequest request) { - var c = PriorityGroupConfigurator.of( + var service = new TransitGroupPriorityService( request.priorityGroupsByAgency(), request.priorityGroupsGlobal() ); var map = new TreeMap(); for (var it : patterns) { - int groupId = c.lookupTransitGroupPriorityId(it); + int groupId = service.lookupTransitGroupPriorityId(it); var de = map.computeIfAbsent(groupId, DebugEntity::new); de.add( it.getRoute().getAgency().getId().toString(), diff --git a/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java b/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java index aac485da4eb..be5b30b38b9 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java +++ b/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java @@ -36,7 +36,7 @@ import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimesFactory; -import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.updater.spi.DataValidationExceptionMapper; import org.opentripplanner.updater.spi.UpdateError; import org.rutebanken.netex.model.BusSubmodeEnumeration; @@ -52,7 +52,7 @@ class AddedTripBuilder { private static final Logger LOG = LoggerFactory.getLogger(AddedTripBuilder.class); - private final TransitModel transitModel; + private final TransitEditorService transitService; private final EntityResolver entityResolver; private final ZoneId timeZone; private final Function getTripPatternId; @@ -73,7 +73,7 @@ class AddedTripBuilder { AddedTripBuilder( EstimatedVehicleJourney estimatedVehicleJourney, - TransitModel transitModel, + TransitEditorService transitService, EntityResolver entityResolver, Function getTripPatternId ) { @@ -112,16 +112,16 @@ class AddedTripBuilder { calls = CallWrapper.of(estimatedVehicleJourney); - this.transitModel = transitModel; + this.transitService = transitService; this.entityResolver = entityResolver; this.getTripPatternId = getTripPatternId; - timeZone = transitModel.getTimeZone(); + timeZone = transitService.getTimeZone(); replacedTrips = getReplacedVehicleJourneys(estimatedVehicleJourney); } AddedTripBuilder( - TransitModel transitModel, + TransitEditorService transitService, EntityResolver entityResolver, Function getTripPatternId, FeedScopedId tripId, @@ -139,9 +139,9 @@ class AddedTripBuilder { String headsign, List replacedTrips ) { - this.transitModel = transitModel; + this.transitService = transitService; this.entityResolver = entityResolver; - this.timeZone = transitModel.getTimeZone(); + this.timeZone = transitService.getTimeZone(); this.getTripPatternId = getTripPatternId; this.tripId = tripId; this.operator = operator; @@ -168,7 +168,7 @@ Result build() { return UpdateError.result(tripId, NO_START_DATE); } - FeedScopedId calServiceId = transitModel.getOrCreateServiceIdForDate(serviceDate); + FeedScopedId calServiceId = transitService.getOrCreateServiceIdForDate(serviceDate); if (calServiceId == null) { return UpdateError.result(tripId, NO_START_DATE); } @@ -181,7 +181,7 @@ Result build() { } route = createRoute(agency); LOG.info("Adding route {} to transitModel.", route); - transitModel.getTransitModelIndex().addRoutes(route); + transitService.addRoutes(route); } Trip trip = createTrip(route, calServiceId); @@ -221,14 +221,14 @@ Result build() { RealTimeTripTimes tripTimes = TripTimesFactory.tripTimes( trip, aimedStopTimes, - transitModel.getDeduplicator() + transitService.getDeduplicator() ); // validate the scheduled trip times // they are in general superseded by real-time trip times // but in case of trip cancellation, OTP will fall back to scheduled trip times // therefore they must be valid tripTimes.validateNonIncreasingTimes(); - tripTimes.setServiceCode(transitModel.getServiceCodes().get(trip.getServiceId())); + tripTimes.setServiceCode(transitService.getServiceCodeForId(trip.getServiceId())); pattern.add(tripTimes); RealTimeTripTimes updatedTripTimes = tripTimes.copyScheduledTimes(); @@ -267,17 +267,14 @@ Result build() { // Adding trip to index necessary to include values in graphql-queries // TODO - SIRI: should more data be added to index? - transitModel.getTransitModelIndex().getTripForId().put(tripId, trip); - transitModel.getTransitModelIndex().getPatternForTrip().put(trip, pattern); - transitModel.getTransitModelIndex().getPatternsForRoute().put(route, pattern); - transitModel - .getTransitModelIndex() - .getTripOnServiceDateById() - .put(tripOnServiceDate.getId(), tripOnServiceDate); - transitModel - .getTransitModelIndex() - .getTripOnServiceDateForTripAndDay() - .put(new TripIdAndServiceDate(tripId, serviceDate), tripOnServiceDate); + transitService.addTripForId(tripId, trip); + transitService.addPatternForTrip(trip, pattern); + transitService.addPatternsForRoute(route, pattern); + transitService.addTripOnServiceDateById(tripOnServiceDate.getId(), tripOnServiceDate); + transitService.addTripOnServiceDateForTripAndDay( + new TripIdAndServiceDate(tripId, serviceDate), + tripOnServiceDate + ); return Result.success(new TripUpdate(stopPattern, updatedTripTimes, serviceDate)); } @@ -312,8 +309,7 @@ private Route createRoute(Agency agency) { */ @Nullable private Agency resolveAgency() { - return transitModel - .getTransitModelIndex() + return transitService .getAllRoutes() .stream() .filter(r -> r != null && r.getOperator() != null && r.getOperator().equals(operator)) diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java index db9c1fcc441..9e2dfaff76b 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java @@ -23,8 +23,8 @@ import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.transit.service.DefaultTransitService; +import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.transit.service.TransitModel; -import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.updater.TimetableSnapshotSourceParameters; import org.opentripplanner.updater.spi.DataValidationExceptionMapper; import org.opentripplanner.updater.spi.UpdateError; @@ -56,9 +56,13 @@ public class SiriTimetableSnapshotSource implements TimetableSnapshotProvider { * messages. */ private final SiriTripPatternCache tripPatternCache; - private final TransitModel transitModel; - private final TransitService transitService; + /** + * Long-lived transit editor service that has access to the timetable snapshot buffer. + * This differs from the usual use case where the transit service refers to the latest published + * timetable snapshot. + */ + private final TransitEditorService transitEditorService; private final TimetableSnapshotManager snapshotManager; @@ -72,10 +76,10 @@ public SiriTimetableSnapshotSource( parameters, () -> LocalDate.now(transitModel.getTimeZone()) ); - this.transitModel = transitModel; - this.transitService = new DefaultTransitService(transitModel); + this.transitEditorService = + new DefaultTransitService(transitModel, getTimetableSnapshotBuffer()); this.tripPatternCache = - new SiriTripPatternCache(tripPatternIdGenerator, transitService::getPatternForTrip); + new SiriTripPatternCache(tripPatternIdGenerator, transitEditorService::getPatternForTrip); transitModel.initTimetableSnapshotProvider(this); } @@ -104,26 +108,22 @@ public UpdateResult applyEstimatedTimetable( List> results = new ArrayList<>(); - snapshotManager.withLock(() -> { - if (incrementality == FULL_DATASET) { - // Remove all updates from the buffer - snapshotManager.clearBuffer(feedId); - } + if (incrementality == FULL_DATASET) { + // Remove all updates from the buffer + snapshotManager.clearBuffer(feedId); + } - for (var etDelivery : updates) { - for (var estimatedJourneyVersion : etDelivery.getEstimatedJourneyVersionFrames()) { - var journeys = estimatedJourneyVersion.getEstimatedVehicleJourneies(); - LOG.debug("Handling {} EstimatedVehicleJourneys.", journeys.size()); - for (EstimatedVehicleJourney journey : journeys) { - results.add(apply(journey, transitModel, fuzzyTripMatcher, entityResolver)); - } + for (var etDelivery : updates) { + for (var estimatedJourneyVersion : etDelivery.getEstimatedJourneyVersionFrames()) { + var journeys = estimatedJourneyVersion.getEstimatedVehicleJourneies(); + LOG.debug("Handling {} EstimatedVehicleJourneys.", journeys.size()); + for (EstimatedVehicleJourney journey : journeys) { + results.add(apply(journey, transitEditorService, fuzzyTripMatcher, entityResolver)); } } + } - LOG.debug("message contains {} trip updates", updates.size()); - - snapshotManager.purgeAndCommit(); - }); + LOG.debug("message contains {} trip updates", updates.size()); return UpdateResult.ofResults(results); } @@ -133,9 +133,13 @@ public TimetableSnapshot getTimetableSnapshot() { return snapshotManager.getTimetableSnapshot(); } + private TimetableSnapshot getTimetableSnapshotBuffer() { + return snapshotManager.getTimetableSnapshotBuffer(); + } + private Result apply( EstimatedVehicleJourney journey, - TransitModel transitModel, + TransitEditorService transitService, @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher, EntityResolver entityResolver ) { @@ -147,7 +151,7 @@ private Result apply( result = new AddedTripBuilder( journey, - transitModel, + transitService, entityResolver, tripPatternIdGenerator::generateUniqueTripPatternId ) @@ -197,11 +201,7 @@ private boolean shouldAddNewTrip( * Snapshot timetable is used as source if initialised, trip patterns scheduled timetable if not. */ private Timetable getCurrentTimetable(TripPattern tripPattern, LocalDate serviceDate) { - TimetableSnapshot timetableSnapshot = getTimetableSnapshot(); - if (timetableSnapshot != null) { - return timetableSnapshot.resolve(tripPattern, serviceDate); - } - return tripPattern.getScheduledTimetable(); + return getTimetableSnapshotBuffer().resolve(tripPattern, serviceDate); } private Result handleModifiedTrip( @@ -230,7 +230,7 @@ private Result handleModifiedTrip( if (trip != null) { // Found exact match - pattern = transitService.getPatternForTrip(trip); + pattern = transitEditorService.getPatternForTrip(trip); } else if (fuzzyTripMatcher != null) { // No exact match found - search for trips based on arrival-times/stop-patterns TripAndPattern tripAndPattern = fuzzyTripMatcher.match( @@ -265,7 +265,7 @@ private Result handleModifiedTrip( pattern, estimatedVehicleJourney, serviceDate, - transitModel.getTimeZone(), + transitEditorService.getTimeZone(), entityResolver ) .build(); @@ -312,7 +312,7 @@ private Result addTripToGraphAndBuffer(TripUpdate tr private boolean markScheduledTripAsDeleted(Trip trip, final LocalDate serviceDate) { boolean success = false; - final TripPattern pattern = transitService.getPatternForTrip(trip); + final TripPattern pattern = transitEditorService.getPatternForTrip(trip); if (pattern != null) { // Mark scheduled trip times for this trip in this pattern as deleted @@ -331,4 +331,11 @@ private boolean markScheduledTripAsDeleted(Trip trip, final LocalDate serviceDat return success; } + + /** + * Flush pending changes in the timetable snapshot buffer and publish a new snapshot. + */ + public void flushBuffer() { + snapshotManager.purgeAndCommit(); + } } diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationService.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationService.java index 11ad4be69ff..68efe8744cc 100644 --- a/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationService.java +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/StopConsolidationService.java @@ -43,4 +43,6 @@ public interface StopConsolidationService { * For a given stop id return the primary stop if it is part of a consolidated stop group. */ Optional primaryStop(FeedScopedId id); + + boolean isPartOfConsolidatedStop(StopLocation sl); } diff --git a/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java b/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java index 216489512f5..9f31e366be5 100644 --- a/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java +++ b/src/ext/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationService.java @@ -67,6 +67,11 @@ public boolean isSecondaryStop(StopLocation stop) { return repo.groups().stream().anyMatch(r -> r.secondaries().contains(stop.getId())); } + @Override + public boolean isPartOfConsolidatedStop(StopLocation sl) { + return isSecondaryStop(sl) || isPrimaryStop(sl); + } + @Override public boolean isActive() { return !repo.groups().isEmpty(); diff --git a/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/DigitransitStopPropertyMapper.java b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/DigitransitStopPropertyMapper.java index d10e221b1d5..edf9c7d8188 100644 --- a/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/DigitransitStopPropertyMapper.java +++ b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/DigitransitStopPropertyMapper.java @@ -1,5 +1,7 @@ package org.opentripplanner.ext.vectortiles.layers.stops; +import static org.opentripplanner.inspector.vector.KeyValue.kv; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Collection; @@ -52,10 +54,7 @@ protected static Collection getBaseKeyValues( new KeyValue("desc", i18NStringMapper.mapToApi(stop.getDescription())), new KeyValue("type", getType(transitService, stop)), new KeyValue("routes", getRoutes(transitService, stop)), - new KeyValue( - "parentStation", - stop.getParentStation() != null ? stop.getParentStation().getId() : null - ) + kv("parentStation", stop.getParentStation() != null ? stop.getParentStation().getId() : null) ); } diff --git a/src/main/java/org/opentripplanner/apis/APIEndpoints.java b/src/main/java/org/opentripplanner/apis/APIEndpoints.java index fe8db5b3911..b6ad08ea4d9 100644 --- a/src/main/java/org/opentripplanner/apis/APIEndpoints.java +++ b/src/main/java/org/opentripplanner/apis/APIEndpoints.java @@ -61,8 +61,6 @@ private APIEndpoints() { addIfEnabled(SandboxAPIMapboxVectorTilesApi, VectorTilesResource.class); addIfEnabled(SandboxAPIParkAndRideApi, ParkAndRideResource.class); addIfEnabled(SandboxAPIGeocoder, GeocoderResource.class); - // scheduled to be removed and only here for backwards compatibility - addIfEnabled(SandboxAPIGeocoder, GeocoderResource.GeocoderResourceOldPath.class); // scheduled to be removed addIfEnabled(APIBikeRental, BikeRental.class); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java b/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java index 212dbd1b150..8ec3172db52 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java @@ -18,6 +18,7 @@ import java.util.Optional; import javax.annotation.Nonnull; import org.locationtech.jts.geom.Geometry; +import org.opentripplanner.framework.graphql.scalar.DateScalarFactory; import org.opentripplanner.framework.graphql.scalar.DurationScalarFactory; import org.opentripplanner.framework.json.ObjectMappers; import org.opentripplanner.framework.model.Cost; @@ -235,6 +236,8 @@ private static Optional validateCost(int cost) { ) .build(); + public static final GraphQLScalarType LOCAL_DATE_SCALAR = DateScalarFactory.createGtfsDateScalar(); + public static final GraphQLScalarType GEOJSON_SCALAR = GraphQLScalarType .newScalar() .name("GeoJson") diff --git a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java index ca059723acd..43a8399e70c 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java @@ -22,8 +22,6 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.opentripplanner.apis.gtfs.datafetchers.AgencyImpl; @@ -87,7 +85,6 @@ import org.opentripplanner.apis.support.graphql.LoggingDataFetcherExceptionHandler; import org.opentripplanner.ext.actuator.MicrometerGraphQLInstrumentation; import org.opentripplanner.framework.application.OTPFeature; -import org.opentripplanner.framework.concurrent.OtpRequestThreadFactory; import org.opentripplanner.framework.graphql.GraphQLResponseSerializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -98,10 +95,6 @@ class GtfsGraphQLIndex { private static final GraphQLSchema indexSchema = buildSchema(); - static final ExecutorService threadPool = Executors.newCachedThreadPool( - OtpRequestThreadFactory.of("gtfs-api-%d") - ); - protected static GraphQLSchema buildSchema() { try { URL url = Objects.requireNonNull(GtfsGraphQLIndex.class.getResource("schema.graphqls")); @@ -119,6 +112,7 @@ protected static GraphQLSchema buildSchema() { .scalar(GraphQLScalars.COORDINATE_VALUE_SCALAR) .scalar(GraphQLScalars.COST_SCALAR) .scalar(GraphQLScalars.RELUCTANCE_SCALAR) + .scalar(GraphQLScalars.LOCAL_DATE_SCALAR) .scalar(ExtendedScalars.GraphQLLong) .scalar(ExtendedScalars.Locale) .scalar( diff --git a/src/main/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilter.java b/src/main/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilter.java new file mode 100644 index 00000000000..8eecfe6273b --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilter.java @@ -0,0 +1,88 @@ +package org.opentripplanner.apis.gtfs; + +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 + * has a method to filter routes by checking if their patterns operate on the required days. + *

+ * Once a more complete filtering engine is in place in the core data model, this code should be + * there rather than a separate class in the API package. + */ +public class PatternByServiceDatesFilter { + + private final Function> getPatternsForRoute; + private final Function> getServiceDatesForTrip; + private final LocalDateRange range; + + /** + * This method is not private to enable unit testing. + *

+ */ + PatternByServiceDatesFilter( + LocalDateRange range, + Function> getPatternsForRoute, + Function> getServiceDatesForTrip + ) { + this.getPatternsForRoute = Objects.requireNonNull(getPatternsForRoute); + this.getServiceDatesForTrip = Objects.requireNonNull(getServiceDatesForTrip); + this.range = range; + + if (range.unlimited()) { + throw new IllegalArgumentException("start and end cannot be both null"); + } else if (range.startBeforeEnd()) { + throw new IllegalArgumentException("start must be before end"); + } + } + + 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. + */ + public Collection filterPatterns(Collection tripPatterns) { + return tripPatterns.stream().filter(this::hasServicesOnDate).toList(); + } + + /** + * Filter the routes by listing all their patterns' service dates and checking if they + * operate on the specified dates. + */ + public Collection filterRoutes(Stream routeStream) { + return routeStream + .filter(r -> { + var patterns = getPatternsForRoute.apply(r); + return !this.filterPatterns(patterns).isEmpty(); + }) + .toList(); + } + + private boolean hasServicesOnDate(TripPattern pattern) { + return pattern + .scheduledTripsAsStream() + .anyMatch(trip -> { + var dates = getServiceDatesForTrip.apply(trip); + + return dates.stream().anyMatch(range::contains); + }); + } +} 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 f187a49d9c7..0e70c13074b 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java @@ -27,11 +27,13 @@ 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.time.LocalDateRangeUtil; import org.opentripplanner.ext.fares.impl.DefaultFareService; import org.opentripplanner.ext.fares.impl.GtfsFaresService; import org.opentripplanner.ext.fares.model.FareRuleSet; @@ -611,6 +613,11 @@ public DataFetcher> routes() { GraphQLUtils.startsWith(route.getLongName(), name, environment.getLocale()) ); } + + if (LocalDateRangeUtil.hasServiceDateFilter(args.getGraphQLServiceDates())) { + var filter = new PatternByServiceDatesFilter(args.getGraphQLServiceDates(), transitService); + routeStream = filter.filterRoutes(routeStream).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 9f9b3c60b31..a3f557951f0 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RouteImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RouteImpl.java @@ -9,11 +9,13 @@ 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.time.LocalDateRangeUtil; import org.opentripplanner.routing.alertpatch.EntitySelector; import org.opentripplanner.routing.alertpatch.TransitAlert; import org.opentripplanner.routing.services.TransitAlertService; @@ -174,8 +176,19 @@ public DataFetcher mode() { @Override public DataFetcher> patterns() { - return environment -> - getTransitService(environment).getPatternsForRoute(getSource(environment)); + return environment -> { + final TransitService transitService = getTransitService(environment); + var patterns = transitService.getPatternsForRoute(getSource(environment)); + + var args = new GraphQLTypes.GraphQLRoutePatternsArgs(environment.getArguments()); + + if (LocalDateRangeUtil.hasServiceDateFilter(args.getGraphQLServiceDates())) { + var filter = new PatternByServiceDatesFilter(args.getGraphQLServiceDates(), transitService); + return filter.filterPatterns(patterns); + } else { + return patterns; + } + }; } @Override 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 541219481ef..67051444cdf 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java @@ -1268,6 +1268,35 @@ public void setGraphQLOriginModesWithParentStation( } } + public static class GraphQLLocalDateRangeInput { + + private java.time.LocalDate end; + private java.time.LocalDate start; + + public GraphQLLocalDateRangeInput(Map args) { + if (args != null) { + this.end = (java.time.LocalDate) args.get("end"); + this.start = (java.time.LocalDate) args.get("start"); + } + } + + public java.time.LocalDate getGraphQLEnd() { + return this.end; + } + + public java.time.LocalDate getGraphQLStart() { + return this.start; + } + + public void setGraphQLEnd(java.time.LocalDate end) { + this.end = end; + } + + public void setGraphQLStart(java.time.LocalDate start) { + this.start = start; + } + } + /** Identifies whether this stop represents a stop or station. */ public enum GraphQLLocationType { ENTRANCE, @@ -3459,6 +3488,7 @@ public static class GraphQLQueryTypeRoutesArgs { private List feeds; private List ids; private String name; + private GraphQLLocalDateRangeInput serviceDates; private List transportModes; public GraphQLQueryTypeRoutesArgs(Map args) { @@ -3466,6 +3496,8 @@ public GraphQLQueryTypeRoutesArgs(Map args) { this.feeds = (List) args.get("feeds"); this.ids = (List) args.get("ids"); this.name = (String) args.get("name"); + this.serviceDates = + new GraphQLLocalDateRangeInput((Map) args.get("serviceDates")); if (args.get("transportModes") != null) { this.transportModes = ((List) args.get("transportModes")).stream() @@ -3488,6 +3520,10 @@ public String getGraphQLName() { return this.name; } + public GraphQLLocalDateRangeInput getGraphQLServiceDates() { + return this.serviceDates; + } + public List getGraphQLTransportModes() { return this.transportModes; } @@ -3504,6 +3540,10 @@ public void setGraphQLName(String name) { this.name = name; } + public void setGraphQLServiceDates(GraphQLLocalDateRangeInput serviceDates) { + this.serviceDates = serviceDates; + } + public void setGraphQLTransportModes(List transportModes) { this.transportModes = transportModes; } @@ -3943,6 +3983,26 @@ public void setGraphQLLanguage(String language) { } } + public static class GraphQLRoutePatternsArgs { + + private GraphQLLocalDateRangeInput serviceDates; + + public GraphQLRoutePatternsArgs(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; + } + } + /** Entities that are relevant for routes that can contain alerts */ public enum GraphQLRouteAlertType { AGENCY, diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml b/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml index b9ee0ac3e16..29490a28b78 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml @@ -34,7 +34,7 @@ config: Speed: Double Reluctance: Double Ratio: Double - + LocalDate: java.time.LocalDate mappers: AbsoluteDirection: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLAbsoluteDirection#GraphQLAbsoluteDirection Agency: org.opentripplanner.transit.model.organization.Agency#Agency diff --git a/src/main/java/org/opentripplanner/apis/gtfs/model/LocalDateRange.java b/src/main/java/org/opentripplanner/apis/gtfs/model/LocalDateRange.java new file mode 100644 index 00000000000..dfecfdcd960 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/model/LocalDateRange.java @@ -0,0 +1,33 @@ +package org.opentripplanner.apis.gtfs.model; + +import java.time.LocalDate; +import javax.annotation.Nullable; + +/** + * See the API documentation for a discussion of {@code startInclusive} and {@code endExclusive}. + */ +public record LocalDateRange(@Nullable LocalDate startInclusive, @Nullable LocalDate endExclusive) { + /** + * Does it actually define a limit or is the range unlimited? + */ + public boolean unlimited() { + return startInclusive == null && endExclusive == null; + } + + /** + * Is the start date before the end? + */ + public boolean startBeforeEnd() { + return startInclusive != null && endExclusive != null && startInclusive.isAfter(endExclusive); + } + + /** + * Is the given LocalDate instance inside of this date range? + */ + public boolean contains(LocalDate date) { + return ( + (startInclusive == null || date.isEqual(startInclusive) || date.isAfter(startInclusive)) && + (endExclusive == null || date.isBefore(endExclusive)) + ); + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/support/time/LocalDateRangeUtil.java b/src/main/java/org/opentripplanner/apis/gtfs/support/time/LocalDateRangeUtil.java new file mode 100644 index 00000000000..b4545f10658 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/support/time/LocalDateRangeUtil.java @@ -0,0 +1,18 @@ +package org.opentripplanner.apis.gtfs.support.time; + +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.apis.gtfs.model.LocalDateRange; + +public class LocalDateRangeUtil { + + /** + * Checks if a service date filter input has at least one filter set. If both start and end are + * null then no filtering is necessary and this method returns null. + */ + public static boolean hasServiceDateFilter(GraphQLTypes.GraphQLLocalDateRangeInput dateRange) { + return ( + dateRange != null && + !new LocalDateRange(dateRange.getGraphQLStart(), dateRange.getGraphQLEnd()).unlimited() + ); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java b/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java index 6e63ddf3921..b325eac3653 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java @@ -434,6 +434,13 @@ public class EnumTypes { "the road, drive to a drop-off point along the road, and walk the rest of the way. " + "This can include various taxi-services or kiss & ride." ) + .value( + "car_rental", + StreetMode.CAR_RENTAL, + "Walk to a car rental point along " + + "the road, drive to a drop-off point along the road, and walk the rest of the way. " + + "This can include car rentals at fixed locations or free-floating services." + ) .value( "flexible", StreetMode.FLEXIBLE, diff --git a/src/main/java/org/opentripplanner/apis/transmodel/support/GqlUtil.java b/src/main/java/org/opentripplanner/apis/transmodel/support/GqlUtil.java index 1f0722eb991..16083085500 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/support/GqlUtil.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/support/GqlUtil.java @@ -15,12 +15,12 @@ import java.util.Locale; import org.opentripplanner.apis.transmodel.TransmodelRequestContext; import org.opentripplanner.apis.transmodel.mapping.TransitIdMapper; -import org.opentripplanner.apis.transmodel.model.scalars.DateScalarFactory; import org.opentripplanner.apis.transmodel.model.scalars.DateTimeScalarFactory; import org.opentripplanner.apis.transmodel.model.scalars.DoubleFunctionFactory; import org.opentripplanner.apis.transmodel.model.scalars.LocalTimeScalarFactory; import org.opentripplanner.apis.transmodel.model.scalars.TimeScalarFactory; import org.opentripplanner.framework.graphql.GraphQLUtils; +import org.opentripplanner.framework.graphql.scalar.DateScalarFactory; import org.opentripplanner.framework.graphql.scalar.DurationScalarFactory; import org.opentripplanner.routing.graphfinder.GraphFinder; import org.opentripplanner.routing.vehicle_parking.VehicleParkingService; @@ -45,7 +45,7 @@ public class GqlUtil { public GqlUtil(ZoneId timeZone) { this.dateTimeScalar = DateTimeScalarFactory.createMillisecondsSinceEpochAsDateTimeStringScalar(timeZone); - this.dateScalar = DateScalarFactory.createDateScalar(); + this.dateScalar = DateScalarFactory.createTransmodelDateScalar(); this.doubleFunctionScalar = DoubleFunctionFactory.createDoubleFunctionScalar(); this.localTimeScalar = LocalTimeScalarFactory.createLocalTimeScalar(); this.timeScalar = TimeScalarFactory.createSecondsSinceMidnightAsTimeObject(); diff --git a/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java b/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java index 7741a7a58cb..21cdfee9ef7 100644 --- a/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java +++ b/src/main/java/org/opentripplanner/apis/vectortiles/DebugStyleSpec.java @@ -23,6 +23,7 @@ import org.opentripplanner.street.model.edge.StreetVehicleParkingLink; import org.opentripplanner.street.model.edge.TemporaryFreeEdge; import org.opentripplanner.street.model.edge.TemporaryPartialStreetEdge; +import org.opentripplanner.street.model.vertex.VehicleParkingEntranceVertex; /** * A Mapbox/Mapblibre style specification for rendering debug information about transit and @@ -38,7 +39,8 @@ public class DebugStyleSpec { "© OpenStreetMap Contributors" ); private static final String MAGENTA = "#f21d52"; - private static final String GREEN = "#22DD9E"; + private static final String BRIGHT_GREEN = "#22DD9E"; + private static final String DARK_GREEN = "#136b04"; private static final String PURPLE = "#BC55F2"; private static final String BLACK = "#140d0e"; private static final int MAX_ZOOM = 23; @@ -101,7 +103,7 @@ static StyleSpec build( .ofId("link") .typeLine() .vectorSourceLayer(edges) - .lineColor(GREEN) + .lineColor(BRIGHT_GREEN) .edgeFilter( StreetTransitStopLink.class, StreetTransitEntranceLink.class, @@ -125,11 +127,24 @@ static StyleSpec build( .minZoom(15) .maxZoom(MAX_ZOOM) .intiallyHidden(), + StyleBuilder + .ofId("parking-vertex") + .typeCircle() + .vectorSourceLayer(vertices) + .vertexFilter(VehicleParkingEntranceVertex.class) + .circleStroke(BLACK, CIRCLE_STROKE) + .circleRadius( + new ZoomDependentNumber(1, List.of(new ZoomStop(13, 1.4f), new ZoomStop(MAX_ZOOM, 10))) + ) + .circleColor(DARK_GREEN) + .minZoom(13) + .maxZoom(MAX_ZOOM) + .intiallyHidden(), StyleBuilder .ofId("area-stop") .typeFill() .vectorSourceLayer(areaStops) - .fillColor(GREEN) + .fillColor(BRIGHT_GREEN) .fillOpacity(0.5f) .fillOutlineColor(BLACK) .minZoom(6) @@ -138,7 +153,7 @@ static StyleSpec build( .ofId("group-stop") .typeFill() .vectorSourceLayer(groupStops) - .fillColor(GREEN) + .fillColor(BRIGHT_GREEN) .fillOpacity(0.5f) .fillOutlineColor(BLACK) .minZoom(6) diff --git a/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java b/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java index 28c1e792fd1..d842b5e6687 100644 --- a/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java +++ b/src/main/java/org/opentripplanner/apis/vectortiles/model/StyleBuilder.java @@ -12,6 +12,7 @@ import org.opentripplanner.framework.collection.ListUtils; import org.opentripplanner.framework.json.ObjectMappers; import org.opentripplanner.street.model.edge.Edge; +import org.opentripplanner.street.model.vertex.Vertex; /** * Builds a Maplibre/Mapbox vector tile @@ -192,9 +193,15 @@ public StyleBuilder intiallyHidden() { */ @SafeVarargs public final StyleBuilder edgeFilter(Class... classToFilter) { - var clazzes = Arrays.stream(classToFilter).map(Class::getSimpleName).toList(); - filter = ListUtils.combine(List.of("in", "class"), clazzes); - return this; + return filterClasses(classToFilter); + } + + /** + * Only apply the style to the given vertices. + */ + @SafeVarargs + public final StyleBuilder vertexFilter(Class... classToFilter) { + return filterClasses(classToFilter); } public JsonNode toJson() { @@ -216,6 +223,12 @@ public JsonNode toJson() { return OBJECT_MAPPER.valueToTree(copy); } + private StyleBuilder filterClasses(Class... classToFilter) { + var clazzes = Arrays.stream(classToFilter).map(Class::getSimpleName).toList(); + filter = ListUtils.combine(List.of("in", "class"), clazzes); + return this; + } + private String validateColor(String color) { if (!color.startsWith("#")) { throw new IllegalArgumentException("Colors must start with '#'"); diff --git a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java index 8ee5d100b94..749772fae12 100644 --- a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java +++ b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java @@ -90,6 +90,14 @@ public enum OTPFeature { FlexRouting(false, true, "Enable FLEX routing."), GoogleCloudStorage(false, true, "Enable Google Cloud Storage integration."), LegacyRestApi(false, true, "Enable legacy REST API. This API will be removed in the future."), + MultiCriteriaGroupMaxFilter( + false, + false, + "Keep the best itinerary with respect to each criteria used in the transit-routing search. " + + "For example the itinerary with the lowest cost, fewest transfers, and each unique transit-group " + + "(transit-group-priority) is kept, even if the max-limit is exceeded. This is turned off by default " + + "for now, until this feature is well tested." + ), RealtimeResolver( false, true, diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/scalars/DateScalarFactory.java b/src/main/java/org/opentripplanner/framework/graphql/scalar/DateScalarFactory.java similarity index 62% rename from src/main/java/org/opentripplanner/apis/transmodel/model/scalars/DateScalarFactory.java rename to src/main/java/org/opentripplanner/framework/graphql/scalar/DateScalarFactory.java index 6d45018ed2a..931a98fc5d9 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/scalars/DateScalarFactory.java +++ b/src/main/java/org/opentripplanner/framework/graphql/scalar/DateScalarFactory.java @@ -1,4 +1,4 @@ -package org.opentripplanner.apis.transmodel.model.scalars; +package org.opentripplanner.framework.graphql.scalar; import graphql.language.StringValue; import graphql.schema.Coercing; @@ -9,21 +9,40 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import javax.annotation.Nullable; public class DateScalarFactory { - private static final String DOCUMENTATION = + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE; + public static final String TRANSMODEL_DESCRIPTION = "Local date using the ISO 8601 format: `YYYY-MM-DD`. Example: `2020-05-17`."; - private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE; + private static final String TRANSMODEL_NAME = "Date"; + private static final String GTFS_NAME = "LocalDate"; private DateScalarFactory() {} - public static GraphQLScalarType createDateScalar() { + public static GraphQLScalarType createTransmodelDateScalar() { + return createDateScalar(TRANSMODEL_NAME, TRANSMODEL_DESCRIPTION); + } + + public static GraphQLScalarType createGtfsDateScalar() { + // description comes from schema.graphqls + return createDateScalar(GTFS_NAME, null); + } + + /** + * @param description Nullable description that allows caller to pass in null which leads to the + * description from schema.graphqls to be used. + */ + private static GraphQLScalarType createDateScalar( + String scalarName, + @Nullable String description + ) { return GraphQLScalarType .newScalar() - .name("Date") - .description(DOCUMENTATION) + .name(scalarName) + .description(description) .coercing( new Coercing() { @Override @@ -33,7 +52,7 @@ public String serialize(Object input) throws CoercingSerializeException { } throw new CoercingSerializeException( - "Only LocalDate is supported to serialize but found " + input + "Only %s is supported to serialize but found %s".formatted(scalarName, input) ); } @@ -43,7 +62,7 @@ public LocalDate parseValue(Object input) throws CoercingParseValueException { return LocalDate.from(FORMATTER.parse((String) input)); } catch (DateTimeParseException e) { throw new CoercingParseValueException( - "Expected type 'Date' but was '" + input + "'." + "Expected type '%s' but was '%s'.".formatted(scalarName, input) ); } } diff --git a/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java b/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java index c036c113e26..3c1408089a4 100644 --- a/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java +++ b/src/main/java/org/opentripplanner/graph_builder/GraphBuilder.java @@ -127,7 +127,7 @@ public static GraphBuilder create( graphBuilder.addModule(factory.osmBoardingLocationsModule()); } - // This module is outside the hasGTFS conditional block because it also links things like bike rental + // This module is outside the hasGTFS conditional block because it also links things like parking // which need to be handled even when there's no transit. graphBuilder.addModule(factory.streetLinkerModule()); diff --git a/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java b/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java index 287a1a71c21..3137b66070f 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/DirectTransferGenerator.java @@ -67,9 +67,7 @@ public DirectTransferGenerator( @Override public void buildGraph() { /* Initialize transit model index which is needed by the nearby stop finder. */ - if (transitModel.getTransitModelIndex() == null) { - transitModel.index(); - } + transitModel.index(); /* The linker will use streets if they are available, or straight-line distance otherwise. */ NearbyStopFinder nearbyStopFinder = createNearbyStopFinder(); diff --git a/src/main/java/org/opentripplanner/graph_builder/module/StreetLinkerModule.java b/src/main/java/org/opentripplanner/graph_builder/module/StreetLinkerModule.java index 37334161b0a..48e6e484a0c 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/StreetLinkerModule.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/StreetLinkerModule.java @@ -258,16 +258,12 @@ private void linkTransitEntrances(Graph graph) { } private void linkVehicleParks(Graph graph, DataImportIssueStore issueStore) { - if (graph.hasLinkedBikeParks) { - LOG.info("Already linked vehicle parks to graph..."); - return; - } LOG.info("Linking vehicle parks to graph..."); List vehicleParkingToRemove = new ArrayList<>(); for (VehicleParkingEntranceVertex vehicleParkingEntranceVertex : graph.getVerticesOfType( VehicleParkingEntranceVertex.class )) { - if (vehicleParkingEntranceHasLinks(vehicleParkingEntranceVertex)) { + if (vehicleParkingEntranceVertex.isLinkedToGraph()) { continue; } @@ -296,22 +292,6 @@ private void linkVehicleParks(Graph graph, DataImportIssueStore issueStore) { var vehicleParkingService = graph.getVehicleParkingService(); vehicleParkingService.updateVehicleParking(List.of(), vehicleParkingToRemove); } - graph.hasLinkedBikeParks = true; - } - - private boolean vehicleParkingEntranceHasLinks( - VehicleParkingEntranceVertex vehicleParkingEntranceVertex - ) { - return !( - vehicleParkingEntranceVertex - .getIncoming() - .stream() - .allMatch(VehicleParkingEdge.class::isInstance) && - vehicleParkingEntranceVertex - .getOutgoing() - .stream() - .allMatch(VehicleParkingEdge.class::isInstance) - ); } /** diff --git a/src/main/java/org/opentripplanner/inspector/vector/KeyValue.java b/src/main/java/org/opentripplanner/inspector/vector/KeyValue.java index 6c8b0f3aa4e..edad5e1b295 100644 --- a/src/main/java/org/opentripplanner/inspector/vector/KeyValue.java +++ b/src/main/java/org/opentripplanner/inspector/vector/KeyValue.java @@ -1,7 +1,43 @@ package org.opentripplanner.inspector.vector; +import jakarta.annotation.Nullable; +import java.util.Collection; +import java.util.stream.Collectors; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +/** + * A key value pair that represents data being sent to the vector tile library for visualisation + * in a map (including popups). + *

+ * The underlying format (and library) supports only a limited number of Java types and silently + * drops those that aren't supported: https://github.com/CI-CMG/mapbox-vector-tile/blob/master/src/main/java/edu/colorado/cires/cmg/mvt/encoding/MvtValue.java#L18-L40 + *

+ * For this reason this class also has static initializer that automatically converts common + * OTP classes into vector tile-compatible strings. + */ public record KeyValue(String key, Object value) { public static KeyValue kv(String key, Object value) { return new KeyValue(key, value); } + + /** + * A {@link FeedScopedId} is not a type that can be converted to a vector tile feature property + * value. Therefore, we convert it to a string after performing a null check. + */ + public static KeyValue kv(String key, @Nullable FeedScopedId value) { + if (value != null) { + return new KeyValue(key, value.toString()); + } else { + return new KeyValue(key, null); + } + } + + /** + * Takes a key and a collection of values, calls toString on the values and joins them using + * comma as the separator. + */ + public static KeyValue kColl(String key, Collection value) { + var values = value.stream().map(Object::toString).collect(Collectors.joining(",")); + return new KeyValue(key, values); + } } diff --git a/src/main/java/org/opentripplanner/inspector/vector/vertex/VertexPropertyMapper.java b/src/main/java/org/opentripplanner/inspector/vector/vertex/VertexPropertyMapper.java index a493269cc3b..01f5263b11a 100644 --- a/src/main/java/org/opentripplanner/inspector/vector/vertex/VertexPropertyMapper.java +++ b/src/main/java/org/opentripplanner/inspector/vector/vertex/VertexPropertyMapper.java @@ -1,15 +1,22 @@ package org.opentripplanner.inspector.vector.vertex; +import static org.opentripplanner.inspector.vector.KeyValue.kColl; import static org.opentripplanner.inspector.vector.KeyValue.kv; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.opentripplanner.apis.support.mapping.PropertyMapper; import org.opentripplanner.framework.collection.ListUtils; import org.opentripplanner.inspector.vector.KeyValue; +import org.opentripplanner.routing.vehicle_parking.VehicleParking; +import org.opentripplanner.routing.vehicle_parking.VehicleParkingEntrance; import org.opentripplanner.service.vehiclerental.street.VehicleRentalPlaceVertex; import org.opentripplanner.street.model.vertex.BarrierVertex; +import org.opentripplanner.street.model.vertex.VehicleParkingEntranceVertex; import org.opentripplanner.street.model.vertex.Vertex; +import org.opentripplanner.street.search.TraverseMode; public class VertexPropertyMapper extends PropertyMapper { @@ -22,9 +29,36 @@ protected Collection map(Vertex input) { List properties = switch (input) { case BarrierVertex v -> List.of(kv("permission", v.getBarrierPermissions().toString())); - case VehicleRentalPlaceVertex v -> List.of(kv("rentalId", v.getStation().getId())); + case VehicleRentalPlaceVertex v -> List.of(kv("rentalId", v.getStation())); + case VehicleParkingEntranceVertex v -> List.of( + kv("parkingId", v.getVehicleParking().getId()), + kColl("spacesFor", spacesFor(v.getVehicleParking())), + kColl("traversalPermission", traversalPermissions(v.getParkingEntrance())) + ); default -> List.of(); }; return ListUtils.combine(baseProps, properties); } + + private Set spacesFor(VehicleParking vehicleParking) { + var ret = new HashSet(); + if (vehicleParking.hasAnyCarPlaces()) { + ret.add(TraverseMode.CAR); + } + if (vehicleParking.hasBicyclePlaces()) { + ret.add(TraverseMode.BICYCLE); + } + return ret; + } + + private Set traversalPermissions(VehicleParkingEntrance entrance) { + var ret = new HashSet(); + if (entrance.isCarAccessible()) { + ret.add(TraverseMode.CAR); + } + if (entrance.isWalkAccessible()) { + ret.add(TraverseMode.WALK); + } + return ret; + } } diff --git a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java index d7d9385d9b8..c0a7737abce 100644 --- a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java +++ b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java @@ -65,6 +65,11 @@ * up timetables on this class could conceivably be replaced with snapshotting entire views of the * transit network. It would also be possible to make the realtime version of Timetables or * TripTimes the primary view, and include references back to their scheduled versions. + *

+ * Implementation note: when a snapshot is committed, the mutable state of this class is stored + * in final fields and completely initialized in the constructor. This provides an additional + * guarantee of safe-publication without synchronization. + * (see final Field Semantics) */ public class TimetableSnapshot { @@ -93,7 +98,7 @@ public class TimetableSnapshot { * The compound key approach better reflects the fact that there should be only one Timetable per * TripPattern and date. */ - private Map> timetables = new HashMap<>(); + private final Map> timetables; /** * For cases where the trip pattern (sequence of stops visited) has been changed by a realtime @@ -101,7 +106,7 @@ public class TimetableSnapshot { * trip ID and the service date. * TODO RT_AB: clarify if this is an index or the original source of truth. */ - private Map realtimeAddedTripPattern = new HashMap<>(); + private final Map realtimeAddedTripPattern; /** * This is an index of TripPatterns, not the primary collection. It tracks which TripPatterns @@ -111,13 +116,13 @@ public class TimetableSnapshot { * more than once. * TODO RT_AB: More general handling of all realtime indexes outside primary data structures. */ - private SetMultimap patternsForStop = HashMultimap.create(); + private final SetMultimap patternsForStop; /** * Boolean value indicating that timetable snapshot is read only if true. Once it is true, it * shouldn't be possible to change it to false anymore. */ - private boolean readOnly = false; + private final boolean readOnly; /** * Boolean value indicating that this timetable snapshot contains changes compared to the state of @@ -125,6 +130,22 @@ public class TimetableSnapshot { */ private boolean dirty = false; + public TimetableSnapshot() { + this(new HashMap<>(), new HashMap<>(), HashMultimap.create(), false); + } + + private TimetableSnapshot( + Map> timetables, + Map realtimeAddedTripPattern, + SetMultimap patternsForStop, + boolean readOnly + ) { + this.timetables = timetables; + this.realtimeAddedTripPattern = realtimeAddedTripPattern; + this.patternsForStop = patternsForStop; + this.readOnly = readOnly; + } + /** * Returns an updated timetable for the specified pattern if one is available in this snapshot, or * the originally scheduled timetable if there are no updates in this snapshot. @@ -235,12 +256,15 @@ public TimetableSnapshot commit(TransitLayerUpdater transitLayerUpdater, boolean throw new ConcurrentModificationException("This TimetableSnapshot is read-only."); } - TimetableSnapshot ret = new TimetableSnapshot(); if (!force && !this.isDirty()) { return null; } - ret.timetables = Map.copyOf(timetables); - ret.realtimeAddedTripPattern = Map.copyOf(realtimeAddedTripPattern); + TimetableSnapshot ret = new TimetableSnapshot( + Map.copyOf(timetables), + Map.copyOf(realtimeAddedTripPattern), + ImmutableSetMultimap.copyOf(patternsForStop), + true + ); if (transitLayerUpdater != null) { transitLayerUpdater.update(dirtyTimetables, timetables); @@ -249,9 +273,6 @@ public TimetableSnapshot commit(TransitLayerUpdater transitLayerUpdater, boolean this.dirtyTimetables.clear(); this.dirty = false; - ret.patternsForStop = ImmutableSetMultimap.copyOf(patternsForStop); - - ret.readOnly = true; // mark the snapshot as henceforth immutable return ret; } diff --git a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java index 43c18cec59d..373b99f0bc6 100644 --- a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java +++ b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java @@ -24,6 +24,7 @@ import org.opentripplanner.model.transfer.ConstrainedTransfer; import org.opentripplanner.model.transfer.TransferPoint; import org.opentripplanner.routing.api.request.framework.TimePenalty; +import org.opentripplanner.routing.vehicle_parking.VehicleParking; import org.opentripplanner.transit.model.basic.Notice; import org.opentripplanner.transit.model.framework.AbstractTransitEntity; import org.opentripplanner.transit.model.framework.DefaultEntityById; @@ -116,6 +117,8 @@ public class OtpTransitServiceBuilder { private final EntityById groupOfRouteById = new DefaultEntityById<>(); + private final List vehicleParkings = new ArrayList<>(); + private final DataImportIssueStore issueStore; public OtpTransitServiceBuilder(StopModel stopModel, DataImportIssueStore issueStore) { @@ -264,6 +267,14 @@ public CalendarServiceData buildCalendarServiceData() { ); } + /** + * The list of parking lots contained in the transit data (so far only NeTEx). + * Note that parking lots can also be sourced from OSM data as well as realtime updaters. + */ + public List vehicleParkings() { + return vehicleParkings; + } + public OtpTransitService build() { return new OtpTransitServiceImpl(this); } diff --git a/src/main/java/org/opentripplanner/model/plan/Itinerary.java b/src/main/java/org/opentripplanner/model/plan/Itinerary.java index cb14227e83d..dee80addd91 100644 --- a/src/main/java/org/opentripplanner/model/plan/Itinerary.java +++ b/src/main/java/org/opentripplanner/model/plan/Itinerary.java @@ -177,11 +177,6 @@ public Leg lastLeg() { return getLegs().get(getLegs().size() - 1); } - /** Get the first transit leg if one exist */ - public Optional firstTransitLeg() { - return getLegs().stream().filter(TransitLeg.class::isInstance).findFirst(); - } - /** * An itinerary can be flagged for removal with a system notice. *

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

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

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

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

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

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

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

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

+ * Reads: Start at A, walk 2 minutes to stop B, take bus 55, board at 12:04 and alight at 12:14 + * ... + */ + public String toStr() { + // No translater needed, stop indexes are never passed to the builder + PathStringBuilder buf = new PathStringBuilder(null); + buf.stop(firstLeg().getFrom().name.toString()); + + for (Leg leg : legs) { + if (leg.isWalkingLeg()) { + buf.walk((int) leg.getDuration().toSeconds()); + } else if (leg instanceof TransitLeg transitLeg) { + buf.transit( + transitLeg.getMode().name(), + transitLeg.getTrip().logName(), + transitLeg.getStartTime(), + transitLeg.getEndTime() + ); + } else if (leg instanceof StreetLeg streetLeg) { + buf.street(streetLeg.getMode().name(), leg.getStartTime(), leg.getEndTime()); + } + buf.stop(leg.getTo().name.toString()); + } + + // The generalizedCost2 is printed as is, it is a special cost and the scale depends on the + // use-case. + buf.summary( + RaptorCostConverter.toRaptorCost(generalizedCost), + getGeneralizedCost2().orElse(RaptorConstants.NOT_SET) + ); + + return buf.toString(); + } + private static int penaltyCost(TimeAndCost penalty) { return penalty.cost().toSeconds(); } diff --git a/src/main/java/org/opentripplanner/model/plan/Leg.java b/src/main/java/org/opentripplanner/model/plan/Leg.java index 1ee72761d66..2a0b6726560 100644 --- a/src/main/java/org/opentripplanner/model/plan/Leg.java +++ b/src/main/java/org/opentripplanner/model/plan/Leg.java @@ -184,6 +184,7 @@ default Route getRoute() { /** * For transit legs, the trip. For non-transit legs, null. */ + @Nullable default Trip getTrip() { return null; } diff --git a/src/main/java/org/opentripplanner/model/plan/grouppriority/TransitGroupPriorityItineraryDecorator.java b/src/main/java/org/opentripplanner/model/plan/grouppriority/TransitGroupPriorityItineraryDecorator.java new file mode 100644 index 00000000000..9c89e9d4ca8 --- /dev/null +++ b/src/main/java/org/opentripplanner/model/plan/grouppriority/TransitGroupPriorityItineraryDecorator.java @@ -0,0 +1,49 @@ +package org.opentripplanner.model.plan.grouppriority; + +import java.util.Collection; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.model.plan.Leg; +import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; +import org.opentripplanner.transit.model.network.grouppriority.DefaultTransitGroupPriorityCalculator; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; + +/** + * This class will set the {@link Itinerary#getGeneralizedCost2()} value if the feature is + * enabled and no such value is set. The AStar router does not produce itineraries with this, + * so we decorate itineraries with this here to make sure the `c2` is set correct and can be + * used in the itinerary-filter-chain. + */ +public class TransitGroupPriorityItineraryDecorator { + + private final TransitGroupPriorityService priorityGroupConfigurator; + private final RaptorTransitGroupPriorityCalculator transitGroupCalculator; + + public TransitGroupPriorityItineraryDecorator( + TransitGroupPriorityService priorityGroupConfigurator + ) { + this.priorityGroupConfigurator = priorityGroupConfigurator; + this.transitGroupCalculator = new DefaultTransitGroupPriorityCalculator(); + } + + public void decorate(Collection itineraries) { + if (!priorityGroupConfigurator.isEnabled()) { + return; + } + for (Itinerary it : itineraries) { + decorate(it); + } + } + + public void decorate(Itinerary itinerary) { + if (itinerary.getGeneralizedCost2().isEmpty() && priorityGroupConfigurator.isEnabled()) { + int c2 = priorityGroupConfigurator.baseGroupId(); + for (Leg leg : itinerary.getLegs()) { + if (leg.getTrip() != null) { + int newGroupId = priorityGroupConfigurator.lookupTransitGroupPriorityId(leg.getTrip()); + c2 = transitGroupCalculator.mergeInGroupId(c2, newGroupId); + } + } + itinerary.setGeneralizedCost2(c2); + } + } +} diff --git a/src/main/java/org/opentripplanner/netex/NetexBundle.java b/src/main/java/org/opentripplanner/netex/NetexBundle.java index 8d6f098de89..3cd52cd246e 100644 --- a/src/main/java/org/opentripplanner/netex/NetexBundle.java +++ b/src/main/java/org/opentripplanner/netex/NetexBundle.java @@ -9,6 +9,7 @@ import org.opentripplanner.datastore.api.DataSource; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.model.impl.OtpTransitServiceBuilder; +import org.opentripplanner.netex.config.IgnorableFeature; import org.opentripplanner.netex.config.NetexFeedParameters; import org.opentripplanner.netex.index.NetexEntityIndex; import org.opentripplanner.netex.loader.GroupEntries; @@ -45,7 +46,7 @@ public class NetexBundle implements Closeable { private final Set ferryIdsNotAllowedForBicycle; private final double maxStopToShapeSnapDistance; private final boolean noTransfersOnIsolatedStops; - private final boolean ignoreFareFrame; + private final Set ignoredFeatures; /** The NeTEx entities loaded from the input files and passed on to the mapper. */ private NetexEntityIndex index = new NetexEntityIndex(); /** Report errors to issue store */ @@ -62,7 +63,7 @@ public NetexBundle( Set ferryIdsNotAllowedForBicycle, double maxStopToShapeSnapDistance, boolean noTransfersOnIsolatedStops, - boolean ignoreFareFrame + Set ignorableFeatures ) { this.feedId = feedId; this.source = source; @@ -71,7 +72,7 @@ public NetexBundle( this.ferryIdsNotAllowedForBicycle = ferryIdsNotAllowedForBicycle; this.maxStopToShapeSnapDistance = maxStopToShapeSnapDistance; this.noTransfersOnIsolatedStops = noTransfersOnIsolatedStops; - this.ignoreFareFrame = ignoreFareFrame; + this.ignoredFeatures = Set.copyOf(ignorableFeatures); } /** load the bundle, map it to the OTP transit model and return */ @@ -136,7 +137,7 @@ private void loadFileEntries() { }); } mapper.finishUp(); - NetexDocumentParser.finnishUp(); + NetexDocumentParser.finishUp(); } /** @@ -179,7 +180,7 @@ private void loadSingeFileEntry(String fileDescription, DataSource entry) { LOG.info("reading entity {}: {}", fileDescription, entry.name()); issueStore.startProcessingSource(entry.name()); PublicationDeliveryStructure doc = xmlParser.parseXmlDoc(entry.asInputStream()); - NetexDocumentParser.parseAndPopulateIndex(index, doc, ignoreFareFrame); + NetexDocumentParser.parseAndPopulateIndex(index, doc, ignoredFeatures); } catch (JAXBException e) { throw new RuntimeException(e.getMessage(), e); } finally { diff --git a/src/main/java/org/opentripplanner/netex/NetexModule.java b/src/main/java/org/opentripplanner/netex/NetexModule.java index b9a05d25b10..2bf3403395c 100644 --- a/src/main/java/org/opentripplanner/netex/NetexModule.java +++ b/src/main/java/org/opentripplanner/netex/NetexModule.java @@ -13,6 +13,7 @@ import org.opentripplanner.model.calendar.ServiceDateInterval; import org.opentripplanner.model.impl.OtpTransitServiceBuilder; import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.vehicle_parking.VehicleParkingHelper; import org.opentripplanner.standalone.config.BuildConfig; import org.opentripplanner.transit.service.TransitModel; @@ -100,6 +101,11 @@ public void buildGraph() { ); transitModel.validateTimeZones(); + + var lots = transitBuilder.vehicleParkings(); + graph.getVehicleParkingService().updateVehicleParking(lots, List.of()); + var linker = new VehicleParkingHelper(graph); + lots.forEach(linker::linkVehicleParkingToGraph); } transitModel.updateCalendarServiceData(hasActiveTransit, calendarServiceData, issueStore); diff --git a/src/main/java/org/opentripplanner/netex/config/IgnorableFeature.java b/src/main/java/org/opentripplanner/netex/config/IgnorableFeature.java new file mode 100644 index 00000000000..53fe7f87f48 --- /dev/null +++ b/src/main/java/org/opentripplanner/netex/config/IgnorableFeature.java @@ -0,0 +1,9 @@ +package org.opentripplanner.netex.config; + +/** + * Optional data that can be ignored during the NeTEx parsing process. + */ +public enum IgnorableFeature { + FARE_FRAME, + PARKING, +} diff --git a/src/main/java/org/opentripplanner/netex/config/NetexFeedParameters.java b/src/main/java/org/opentripplanner/netex/config/NetexFeedParameters.java index cffecea0d48..0c6c75c4db3 100644 --- a/src/main/java/org/opentripplanner/netex/config/NetexFeedParameters.java +++ b/src/main/java/org/opentripplanner/netex/config/NetexFeedParameters.java @@ -1,6 +1,8 @@ package org.opentripplanner.netex.config; import static java.util.Objects.requireNonNull; +import static org.opentripplanner.netex.config.IgnorableFeature.FARE_FRAME; +import static org.opentripplanner.netex.config.IgnorableFeature.PARKING; import java.net.URI; import java.util.Collection; @@ -29,7 +31,7 @@ public class NetexFeedParameters implements DataSourceConfig { private static final String SHARED_GROUP_FILE_PATTERN = "(\\w{3})-.*-shared\\.xml"; private static final String GROUP_FILE_PATTERN = "(\\w{3})-.*\\.xml"; private static final boolean NO_TRANSFERS_ON_ISOLATED_STOPS = false; - private static final boolean IGNORE_FARE_FRAME = false; + private static final Set IGNORED_FEATURES = Set.of(PARKING); private static final Set FERRY_IDS_NOT_ALLOWED_FOR_BICYCLE = Collections.emptySet(); @@ -48,7 +50,7 @@ public class NetexFeedParameters implements DataSourceConfig { private final String ignoreFilePattern; private final Set ferryIdsNotAllowedForBicycle; private final boolean noTransfersOnIsolatedStops; - private final boolean ignoreFareFrame; + private final Set ignoredFeatures; private NetexFeedParameters() { this.source = null; @@ -63,7 +65,7 @@ private NetexFeedParameters() { } this.ferryIdsNotAllowedForBicycle = FERRY_IDS_NOT_ALLOWED_FOR_BICYCLE; this.noTransfersOnIsolatedStops = NO_TRANSFERS_ON_ISOLATED_STOPS; - this.ignoreFareFrame = IGNORE_FARE_FRAME; + this.ignoredFeatures = IGNORED_FEATURES; } private NetexFeedParameters(Builder builder) { @@ -75,7 +77,7 @@ private NetexFeedParameters(Builder builder) { this.ignoreFilePattern = requireNonNull(builder.ignoreFilePattern); this.ferryIdsNotAllowedForBicycle = Set.copyOf(builder.ferryIdsNotAllowedForBicycle); this.noTransfersOnIsolatedStops = builder.noTransfersOnIsolatedStops; - this.ignoreFareFrame = builder.ignoreFareFrame; + this.ignoredFeatures = Set.copyOf(builder.ignoredFeatures); } public static Builder of() { @@ -127,7 +129,11 @@ public boolean noTransfersOnIsolatedStops() { /** See {@link org.opentripplanner.standalone.config.buildconfig.NetexConfig}. */ public boolean ignoreFareFrame() { - return ignoreFareFrame; + return ignoredFeatures.contains(FARE_FRAME); + } + + public boolean ignoreParking() { + return ignoredFeatures.contains(PARKING); } @Override @@ -142,7 +148,7 @@ public boolean equals(Object o) { sharedFilePattern.equals(that.sharedFilePattern) && sharedGroupFilePattern.equals(that.sharedGroupFilePattern) && groupFilePattern.equals(that.groupFilePattern) && - ignoreFareFrame == that.ignoreFareFrame && + ignoredFeatures.equals(that.ignoredFeatures) && ferryIdsNotAllowedForBicycle.equals(that.ferryIdsNotAllowedForBicycle) ); } @@ -156,7 +162,7 @@ public int hashCode() { sharedFilePattern, sharedGroupFilePattern, groupFilePattern, - ignoreFareFrame, + ignoredFeatures, ferryIdsNotAllowedForBicycle ); } @@ -171,11 +177,15 @@ public String toString() { .addStr("sharedGroupFilePattern", sharedGroupFilePattern, DEFAULT.sharedGroupFilePattern) .addStr("groupFilePattern", groupFilePattern, DEFAULT.groupFilePattern) .addStr("ignoreFilePattern", ignoreFilePattern, DEFAULT.ignoreFilePattern) - .addBoolIfTrue("ignoreFareFrame", ignoreFareFrame) + .addCol("ignoredFeatures", ignoredFeatures) .addCol("ferryIdsNotAllowedForBicycle", ferryIdsNotAllowedForBicycle, Set.of()) .toString(); } + public Set ignoredFeatures() { + return ignoredFeatures; + } + public static class Builder { private final NetexFeedParameters original; @@ -187,7 +197,7 @@ public static class Builder { private String ignoreFilePattern; private final Set ferryIdsNotAllowedForBicycle = new HashSet<>(); private boolean noTransfersOnIsolatedStops; - private boolean ignoreFareFrame; + private final Set ignoredFeatures; private Builder(NetexFeedParameters original) { this.original = original; @@ -199,7 +209,7 @@ private Builder(NetexFeedParameters original) { this.ignoreFilePattern = original.ignoreFilePattern; this.ferryIdsNotAllowedForBicycle.addAll(original.ferryIdsNotAllowedForBicycle); this.noTransfersOnIsolatedStops = original.noTransfersOnIsolatedStops; - this.ignoreFareFrame = original.ignoreFareFrame; + this.ignoredFeatures = new HashSet<>(original.ignoredFeatures); } public URI source() { @@ -247,7 +257,19 @@ public Builder withNoTransfersOnIsolatedStops(boolean noTransfersOnIsolatedStops } public Builder withIgnoreFareFrame(boolean ignoreFareFrame) { - this.ignoreFareFrame = ignoreFareFrame; + return applyIgnore(ignoreFareFrame, FARE_FRAME); + } + + public Builder withIgnoreParking(boolean ignoreParking) { + return applyIgnore(ignoreParking, PARKING); + } + + private Builder applyIgnore(boolean ignore, IgnorableFeature feature) { + if (ignore) { + ignoredFeatures.add(feature); + } else { + ignoredFeatures.remove(feature); + } return this; } diff --git a/src/main/java/org/opentripplanner/netex/configure/NetexConfigure.java b/src/main/java/org/opentripplanner/netex/configure/NetexConfigure.java index 464c03f28e1..50c49836246 100644 --- a/src/main/java/org/opentripplanner/netex/configure/NetexConfigure.java +++ b/src/main/java/org/opentripplanner/netex/configure/NetexConfigure.java @@ -75,7 +75,7 @@ public NetexBundle netexBundle( config.ferryIdsNotAllowedForBicycle(), buildParams.maxStopToShapeSnapDistance, config.noTransfersOnIsolatedStops(), - config.ignoreFareFrame() + config.ignoredFeatures() ); } diff --git a/src/main/java/org/opentripplanner/netex/index/NetexEntityIndex.java b/src/main/java/org/opentripplanner/netex/index/NetexEntityIndex.java index 0ca734d5b6a..0cb94f66a77 100644 --- a/src/main/java/org/opentripplanner/netex/index/NetexEntityIndex.java +++ b/src/main/java/org/opentripplanner/netex/index/NetexEntityIndex.java @@ -28,6 +28,7 @@ import org.rutebanken.netex.model.OperatingDay; import org.rutebanken.netex.model.OperatingPeriod_VersionStructure; import org.rutebanken.netex.model.Operator; +import org.rutebanken.netex.model.Parking; import org.rutebanken.netex.model.Quay; import org.rutebanken.netex.model.Route; import org.rutebanken.netex.model.ServiceJourney; @@ -97,8 +98,9 @@ public class NetexEntityIndex { public final HierarchicalVersionMapById stopPlaceById; public final HierarchicalVersionMapById tariffZonesById; public final HierarchicalMapById brandingById; + public final HierarchicalMapById parkings; - // Relations between entities - The Netex XML sometimes rely on the the + // Relations between entities - The Netex XML sometimes relies on the // nested structure of the XML document, rater than explicit references. // Since we throw away the document we need to keep track of these. @@ -142,6 +144,7 @@ public NetexEntityIndex() { this.tariffZonesById = new HierarchicalVersionMapById<>(); this.brandingById = new HierarchicalMapById<>(); this.timeZone = new HierarchicalElement<>(); + this.parkings = new HierarchicalMapById<>(); } /** @@ -184,6 +187,7 @@ public NetexEntityIndex(NetexEntityIndex parent) { this.tariffZonesById = new HierarchicalVersionMapById<>(parent.tariffZonesById); this.brandingById = new HierarchicalMapById<>(parent.brandingById); this.timeZone = new HierarchicalElement<>(parent.timeZone); + this.parkings = new HierarchicalMapById<>(parent.parkings); } /** @@ -353,6 +357,11 @@ public ReadOnlyHierarchicalVersionMapById getStopPlaceById() { return stopPlaceById; } + @Override + public ReadOnlyHierarchicalMapById getParkingsById() { + return parkings; + } + @Override public ReadOnlyHierarchicalVersionMapById getTariffZonesById() { return tariffZonesById; diff --git a/src/main/java/org/opentripplanner/netex/index/api/NetexEntityIndexReadOnlyView.java b/src/main/java/org/opentripplanner/netex/index/api/NetexEntityIndexReadOnlyView.java index 3c7bc98b36a..37b8e9790b9 100644 --- a/src/main/java/org/opentripplanner/netex/index/api/NetexEntityIndexReadOnlyView.java +++ b/src/main/java/org/opentripplanner/netex/index/api/NetexEntityIndexReadOnlyView.java @@ -19,6 +19,7 @@ import org.rutebanken.netex.model.OperatingDay; import org.rutebanken.netex.model.OperatingPeriod_VersionStructure; import org.rutebanken.netex.model.Operator; +import org.rutebanken.netex.model.Parking; import org.rutebanken.netex.model.Quay; import org.rutebanken.netex.model.Route; import org.rutebanken.netex.model.ServiceJourney; @@ -80,6 +81,8 @@ public interface NetexEntityIndexReadOnlyView { ReadOnlyHierarchicalVersionMapById getStopPlaceById(); + ReadOnlyHierarchicalMapById getParkingsById(); + ReadOnlyHierarchicalVersionMapById getTariffZonesById(); ReadOnlyHierarchicalMapById getBrandingById(); diff --git a/src/main/java/org/opentripplanner/netex/loader/parser/NetexDocumentParser.java b/src/main/java/org/opentripplanner/netex/loader/parser/NetexDocumentParser.java index 925b6dfd019..f8058f2df8e 100644 --- a/src/main/java/org/opentripplanner/netex/loader/parser/NetexDocumentParser.java +++ b/src/main/java/org/opentripplanner/netex/loader/parser/NetexDocumentParser.java @@ -1,8 +1,12 @@ package org.opentripplanner.netex.loader.parser; +import static org.opentripplanner.netex.config.IgnorableFeature.FARE_FRAME; + import jakarta.xml.bind.JAXBElement; import java.util.Collection; import java.util.List; +import java.util.Set; +import org.opentripplanner.netex.config.IgnorableFeature; import org.opentripplanner.netex.index.NetexEntityIndex; import org.rutebanken.netex.model.Common_VersionFrameStructure; import org.rutebanken.netex.model.CompositeFrame; @@ -30,11 +34,11 @@ public class NetexDocumentParser { private static final Logger LOG = LoggerFactory.getLogger(NetexDocumentParser.class); private final NetexEntityIndex netexIndex; - private final boolean ignoreFareFrame; + private final Set ignoredFeatures; - private NetexDocumentParser(NetexEntityIndex netexIndex, boolean ignoreFareFrame) { + private NetexDocumentParser(NetexEntityIndex netexIndex, Set ignoredFeatures) { this.netexIndex = netexIndex; - this.ignoreFareFrame = ignoreFareFrame; + this.ignoredFeatures = ignoredFeatures; } /** @@ -44,12 +48,12 @@ private NetexDocumentParser(NetexEntityIndex netexIndex, boolean ignoreFareFrame public static void parseAndPopulateIndex( NetexEntityIndex index, PublicationDeliveryStructure doc, - boolean ignoreFareFrame + Set ignoredFeatures ) { - new NetexDocumentParser(index, ignoreFareFrame).parse(doc); + new NetexDocumentParser(index, ignoredFeatures).parse(doc); } - public static void finnishUp() { + public static void finishUp() { ServiceFrameParser.logSummary(); } @@ -74,8 +78,8 @@ private void parseCommonFrame(Common_VersionFrameStructure value) { } else if (value instanceof ServiceFrame) { parse((ServiceFrame) value, new ServiceFrameParser(netexIndex.flexibleStopPlaceById)); } else if (value instanceof SiteFrame) { - parse((SiteFrame) value, new SiteFrameParser()); - } else if (!ignoreFareFrame && value instanceof FareFrame) { + parse((SiteFrame) value, new SiteFrameParser(ignoredFeatures)); + } else if (!ignoredFeatures.contains(FARE_FRAME) && value instanceof FareFrame) { parse((FareFrame) value, new FareFrameParser()); } else if (value instanceof CompositeFrame) { // We recursively parse composite frames and content until there diff --git a/src/main/java/org/opentripplanner/netex/loader/parser/NetexParser.java b/src/main/java/org/opentripplanner/netex/loader/parser/NetexParser.java index 54b0043e072..3c24562ef6f 100644 --- a/src/main/java/org/opentripplanner/netex/loader/parser/NetexParser.java +++ b/src/main/java/org/opentripplanner/netex/loader/parser/NetexParser.java @@ -16,7 +16,7 @@ abstract class NetexParser { /** - * Currently a lot of elements on a frame is skipped. If any of these elements are pressent we + * Currently a lot of elements on a frame is skipped. If any of these elements are present we * print a warning for elements that might be relevant for OTP and an info message for none * relevant elements. */ @@ -39,10 +39,10 @@ static void verifyCommonUnusedPropertiesIsNotSet(Logger log, VersionFrame_Versio /** * Log a warning for Netex elements which is not mapped. There might be something wrong with the * data or there might be something wrong with the Netex data import(ignoring these elements). The - * element should be relevant to OTP. OTP do not support Netex 100%, but elements in Nordic + * element should be relevant to OTP. OTP does not support NeTEx 100%, but elements in the Nordic * profile, see https://enturas.atlassian.net/wiki/spaces/PUBLIC/overview should be supported. *

- * If you get this warning and think the element should be mapped, please feel free to report an + * If you see this warning and think the element should be mapped, please feel free to report an * issue on GitHub. */ static void warnOnMissingMapping(Logger log, Object rel) { diff --git a/src/main/java/org/opentripplanner/netex/loader/parser/SiteFrameParser.java b/src/main/java/org/opentripplanner/netex/loader/parser/SiteFrameParser.java index 8cbe0c8aee6..38cd91ded98 100644 --- a/src/main/java/org/opentripplanner/netex/loader/parser/SiteFrameParser.java +++ b/src/main/java/org/opentripplanner/netex/loader/parser/SiteFrameParser.java @@ -1,14 +1,19 @@ package org.opentripplanner.netex.loader.parser; +import static org.opentripplanner.netex.config.IgnorableFeature.PARKING; + import jakarta.xml.bind.JAXBElement; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Set; import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.netex.config.IgnorableFeature; import org.opentripplanner.netex.index.NetexEntityIndex; import org.opentripplanner.netex.support.JAXBUtils; import org.rutebanken.netex.model.FlexibleStopPlace; import org.rutebanken.netex.model.GroupOfStopPlaces; +import org.rutebanken.netex.model.Parking; import org.rutebanken.netex.model.Quay; import org.rutebanken.netex.model.Quays_RelStructure; import org.rutebanken.netex.model.Site_VersionFrameStructure; @@ -36,6 +41,14 @@ class SiteFrameParser extends NetexParser { private final Collection quays = new ArrayList<>(); + private final Collection parkings = new ArrayList<>(0); + + private final Set ignoredFeatures; + + SiteFrameParser(Set ignoredFeatures) { + this.ignoredFeatures = ignoredFeatures; + } + @Override public void parse(Site_VersionFrameStructure frame) { if (frame.getStopPlaces() != null) { @@ -51,6 +64,10 @@ public void parse(Site_VersionFrameStructure frame) { if (frame.getTariffZones() != null) { parseTariffZones(frame.getTariffZones().getTariffZone()); } + + if (!ignoredFeatures.contains(PARKING) && frame.getParkings() != null) { + parseParkings(frame.getParkings().getParking()); + } // Keep list sorted alphabetically warnOnMissingMapping(LOG, frame.getAccesses()); warnOnMissingMapping(LOG, frame.getAddresses()); @@ -59,7 +76,6 @@ public void parse(Site_VersionFrameStructure frame) { warnOnMissingMapping(LOG, frame.getCheckConstraintDelays()); warnOnMissingMapping(LOG, frame.getCheckConstraintThroughputs()); warnOnMissingMapping(LOG, frame.getNavigationPaths()); - warnOnMissingMapping(LOG, frame.getParkings()); warnOnMissingMapping(LOG, frame.getPathJunctions()); warnOnMissingMapping(LOG, frame.getPathLinks()); warnOnMissingMapping(LOG, frame.getPointsOfInterest()); @@ -79,6 +95,7 @@ void setResultOnIndex(NetexEntityIndex netexIndex) { netexIndex.stopPlaceById.addAll(stopPlaces); netexIndex.tariffZonesById.addAll(tariffZones); netexIndex.quayById.addAll(quays); + netexIndex.parkings.addAll(parkings); } private void parseFlexibleStopPlaces(Collection flexibleStopPlacesList) { @@ -89,6 +106,10 @@ private void parseGroupsOfStopPlaces(Collection groupsOfStopP groupsOfStopPlaces.addAll(groupsOfStopPlacesList); } + private void parseParkings(List parking) { + parkings.addAll(parking); + } + private void parseStopPlaces(List> stopPlaceList) { for (JAXBElement jaxBStopPlace : stopPlaceList) { StopPlace stopPlace = (StopPlace) jaxBStopPlace.getValue(); diff --git a/src/main/java/org/opentripplanner/netex/mapping/MultiModalStationMapper.java b/src/main/java/org/opentripplanner/netex/mapping/MultiModalStationMapper.java index ef4b0a25cfd..5a4fb23bbb9 100644 --- a/src/main/java/org/opentripplanner/netex/mapping/MultiModalStationMapper.java +++ b/src/main/java/org/opentripplanner/netex/mapping/MultiModalStationMapper.java @@ -1,6 +1,7 @@ package org.opentripplanner.netex.mapping; import java.util.Collection; +import javax.annotation.Nullable; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; @@ -21,6 +22,7 @@ public MultiModalStationMapper(DataImportIssueStore issueStore, FeedScopedIdFact this.idFactory = idFactory; } + @Nullable MultiModalStation map(StopPlace stopPlace, Collection childStations) { MultiModalStationBuilder multiModalStation = MultiModalStation .of(idFactory.createId(stopPlace.getId())) @@ -34,13 +36,13 @@ MultiModalStation map(StopPlace stopPlace, Collection childStations) { if (coordinate == null) { issueStore.add( "MultiModalStationWithoutCoordinates", - "MultiModal station {} does not contain any coordinates.", + "MultiModal station %s does not contain any coordinates.", multiModalStation.getId() ); + return null; } else { multiModalStation.withCoordinate(coordinate); + return multiModalStation.build(); } - - return multiModalStation.build(); } } diff --git a/src/main/java/org/opentripplanner/netex/mapping/NetexMapper.java b/src/main/java/org/opentripplanner/netex/mapping/NetexMapper.java index c3c9ad2d0ae..025a2349874 100644 --- a/src/main/java/org/opentripplanner/netex/mapping/NetexMapper.java +++ b/src/main/java/org/opentripplanner/netex/mapping/NetexMapper.java @@ -77,9 +77,9 @@ public class NetexMapper { /** * Shared/cached entity index, used by more than one mapper. This index provides alternative - * indexes to netex entites, as well as global indexes to OTP domain objects needed in the mapping + * indexes to netex entities, as well as global indexes to OTP domain objects needed in the mapping * process. Some of these indexes are feed scoped, and some are file group level scoped. As a rule - * of tomb the indexes for OTP Model entities are global(small memory overhead), while the indexes + * of thumb the indexes for OTP Model entities are global(small memory overhead), while the indexes * for the Netex entities follow the main index {@link #currentNetexIndex}, hence sopped by file * group. */ @@ -158,7 +158,7 @@ public void finishUp() { /** *

- * This method mapes the last Netex file imported using the *local* entities in the hierarchical + * This method maps the last Netex file imported using the *local* entities in the hierarchical * {@link NetexEntityIndexReadOnlyView}. *

*

@@ -199,6 +199,8 @@ public void mapNetexToOtp(NetexEntityIndexReadOnlyView netexIndex) { mapTripPatterns(serviceIds); mapNoticeAssignments(); + mapVehicleParkings(); + addEntriesToGroupMapperForPostProcessingLater(); } @@ -332,8 +334,9 @@ private void mapMultiModalStopPlaces() { .getStationsByMultiModalStationRfs() .get(multiModalStopPlace.getId()); var multiModalStation = mapper.map(multiModalStopPlace, stations); - - transitBuilder.stopModel().withMultiModalStation(multiModalStation); + if (multiModalStation != null) { + transitBuilder.stopModel().withMultiModalStation(multiModalStation); + } } } @@ -519,6 +522,19 @@ private void addEntriesToGroupMapperForPostProcessingLater() { } } + private void mapVehicleParkings() { + var mapper = new VehicleParkingMapper(idFactory, issueStore); + currentNetexIndex + .getParkingsById() + .localKeys() + .forEach(id -> { + var parking = mapper.map(currentNetexIndex.getParkingsById().lookup(id)); + if (parking != null) { + transitBuilder.vehicleParkings().add(parking); + } + }); + } + /** * The start of period is used to find the valid entities based on the current time. This should * probably be configurable in the future, or even better incorporate the version number into the diff --git a/src/main/java/org/opentripplanner/netex/mapping/VehicleParkingMapper.java b/src/main/java/org/opentripplanner/netex/mapping/VehicleParkingMapper.java new file mode 100644 index 00000000000..862c5f0c648 --- /dev/null +++ b/src/main/java/org/opentripplanner/netex/mapping/VehicleParkingMapper.java @@ -0,0 +1,103 @@ +package org.opentripplanner.netex.mapping; + +import static org.rutebanken.netex.model.ParkingVehicleEnumeration.CYCLE; +import static org.rutebanken.netex.model.ParkingVehicleEnumeration.E_CYCLE; +import static org.rutebanken.netex.model.ParkingVehicleEnumeration.PEDAL_CYCLE; + +import java.util.Set; +import javax.annotation.Nullable; +import org.opentripplanner.framework.i18n.NonLocalizedString; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.netex.mapping.support.FeedScopedIdFactory; +import org.opentripplanner.routing.vehicle_parking.VehicleParking; +import org.opentripplanner.routing.vehicle_parking.VehicleParkingSpaces; +import org.rutebanken.netex.model.Parking; +import org.rutebanken.netex.model.ParkingVehicleEnumeration; + +/** + * Maps from NeTEx Parking to an internal {@link VehicleParking}. + */ +class VehicleParkingMapper { + + private final FeedScopedIdFactory idFactory; + + private static final Set BICYCLE_TYPES = Set.of( + PEDAL_CYCLE, + E_CYCLE, + CYCLE + ); + private final DataImportIssueStore issueStore; + + VehicleParkingMapper(FeedScopedIdFactory idFactory, DataImportIssueStore issueStore) { + this.idFactory = idFactory; + this.issueStore = issueStore; + } + + @Nullable + VehicleParking map(Parking parking) { + if (parking.getTotalCapacity() == null) { + issueStore.add( + "MissingParkingCapacity", + "NeTEx Parking '%s' does not contain totalCapacity", + parkingDebugId(parking) + ); + return null; + } + return VehicleParking + .builder() + .id(idFactory.createId(parking.getId())) + .name(NonLocalizedString.ofNullable(parking.getName().getValue())) + .coordinate(WgsCoordinateMapper.mapToDomain(parking.getCentroid())) + .capacity(mapCapacity(parking)) + .bicyclePlaces(hasBikes(parking)) + .carPlaces(!hasBikes(parking)) + .entrance(mapEntrance(parking)) + .build(); + } + + /** + * In the Nordic profile many fields of {@link Parking} are optional so even adding the ID to the + * issue store can lead to NPEs. For this reason we have a lot of fallbacks. + */ + private static String parkingDebugId(Parking parking) { + if (parking.getId() != null) { + return parking.getId(); + } else if (parking.getName() != null) { + return parking.getName().getValue(); + } else if (parking.getCentroid() != null) { + return parking.getCentroid().toString(); + } else { + return parking.toString(); + } + } + + private VehicleParking.VehicleParkingEntranceCreator mapEntrance(Parking parking) { + return builder -> + builder + .entranceId(idFactory.createId(parking.getId() + "/entrance")) + .coordinate(WgsCoordinateMapper.mapToDomain(parking.getCentroid())) + .walkAccessible(true) + .carAccessible(true); + } + + private static VehicleParkingSpaces mapCapacity(Parking parking) { + var builder = VehicleParkingSpaces.builder(); + int capacity = parking.getTotalCapacity().intValue(); + + // we assume that if we have something bicycle-like in the vehicle types it's a bicycle parking + // lot + // it's not possible in NeTEx to split the spaces between the types, so if you want that + // you have to define two parking lots with the same coordinates + if (hasBikes(parking)) { + builder.bicycleSpaces(capacity); + } else { + builder.carSpaces(capacity); + } + + return builder.build(); + } + + private static boolean hasBikes(Parking parking) { + return parking.getParkingVehicleTypes().stream().anyMatch(BICYCLE_TYPES::contains); + } +} diff --git a/src/main/java/org/opentripplanner/netex/support/NetexVersionHelper.java b/src/main/java/org/opentripplanner/netex/support/NetexVersionHelper.java index 4e6cf4967c0..8f7a7f14913 100644 --- a/src/main/java/org/opentripplanner/netex/support/NetexVersionHelper.java +++ b/src/main/java/org/opentripplanner/netex/support/NetexVersionHelper.java @@ -16,6 +16,15 @@ */ public class NetexVersionHelper { + /** + * @see NetexVersionHelper#versionOf(EntityInVersionStructure) + */ + private static final String ANY = "any"; + /** + * A special value that represents an unknown version. + */ + private static final int UNKNOWN_VERSION = -1; + /** * private constructor to prevent instantiation of utility class */ @@ -23,10 +32,19 @@ private NetexVersionHelper() {} /** * According to the Norwegian Netex profile the version number must be a positive - * increasing integer. A bigger value indicate a later version. + * increasing integer. A bigger value indicates a later version. + * However, the special value "any" is also supported and returns a constant meaning "unknown". + * The EPIP profile at + * http://netex.uk/netex/doc/2019.05.07-v1.1_FinalDraft/prCEN_TS_16614-PI_Profile_FV_%28E%29-2019-Final-Draft-v3.pdf (page 33) + * defines this as follows: "Use "any" if the VERSION is unknown (note that this will trigger NeTEx's + * XML automatic consistency check)." */ public static int versionOf(EntityInVersionStructure e) { - return Integer.parseInt(e.getVersion()); + if (e.getVersion().equals(ANY)) { + return UNKNOWN_VERSION; + } else { + return Integer.parseInt(e.getVersion()); + } } /** @@ -34,7 +52,7 @@ public static int versionOf(EntityInVersionStructure e) { * elements exist in the collection {@code -1} is returned. */ public static int latestVersionIn(Collection list) { - return list.stream().mapToInt(NetexVersionHelper::versionOf).max().orElse(-1); + return list.stream().mapToInt(NetexVersionHelper::versionOf).max().orElse(UNKNOWN_VERSION); } /** diff --git a/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java b/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java index a1897d87124..d181cde4564 100644 --- a/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java +++ b/src/main/java/org/opentripplanner/openstreetmap/model/OSMNode.java @@ -38,7 +38,8 @@ public boolean hasCrossingTrafficLight() { return hasTag("crossing") && "traffic_signals".equals(getTag("crossing")); } - /* Checks if this node is a barrier which prevents motor vehicle traffic + /** + * Checks if this node is a barrier which prevents motor vehicle traffic. * * @return true if it is */ diff --git a/src/main/java/org/opentripplanner/raptor/api/request/MultiCriteriaRequest.java b/src/main/java/org/opentripplanner/raptor/api/request/MultiCriteriaRequest.java index 368b4660922..683c9807af5 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/MultiCriteriaRequest.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/MultiCriteriaRequest.java @@ -18,7 +18,7 @@ public class MultiCriteriaRequest { private final RelaxFunction relaxC1; @Nullable - private final RaptorTransitGroupCalculator transitPriorityCalculator; + private final RaptorTransitGroupPriorityCalculator transitPriorityCalculator; private final List passThroughPoints; @@ -63,7 +63,7 @@ public RelaxFunction relaxC1() { return relaxC1; } - public Optional transitPriorityCalculator() { + public Optional transitPriorityCalculator() { return Optional.ofNullable(transitPriorityCalculator); } @@ -140,7 +140,7 @@ public static class Builder { private final MultiCriteriaRequest original; private RelaxFunction relaxC1; - private RaptorTransitGroupCalculator transitPriorityCalculator; + private RaptorTransitGroupPriorityCalculator transitPriorityCalculator; private List passThroughPoints; private Double relaxCostAtDestination; @@ -163,11 +163,11 @@ public Builder withRelaxC1(RelaxFunction relaxC1) { } @Nullable - public RaptorTransitGroupCalculator transitPriorityCalculator() { + public RaptorTransitGroupPriorityCalculator transitPriorityCalculator() { return transitPriorityCalculator; } - public Builder withTransitPriorityCalculator(RaptorTransitGroupCalculator value) { + public Builder withTransitPriorityCalculator(RaptorTransitGroupPriorityCalculator value) { transitPriorityCalculator = value; return this; } diff --git a/src/main/java/org/opentripplanner/raptor/api/request/RaptorTransitGroupCalculator.java b/src/main/java/org/opentripplanner/raptor/api/request/RaptorTransitGroupPriorityCalculator.java similarity index 85% rename from src/main/java/org/opentripplanner/raptor/api/request/RaptorTransitGroupCalculator.java rename to src/main/java/org/opentripplanner/raptor/api/request/RaptorTransitGroupPriorityCalculator.java index b5f0598415e..2b96a7b1470 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/RaptorTransitGroupCalculator.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/RaptorTransitGroupPriorityCalculator.java @@ -2,7 +2,7 @@ import org.opentripplanner.raptor.api.model.DominanceFunction; -public interface RaptorTransitGroupCalculator { +public interface RaptorTransitGroupPriorityCalculator { /** * Merge in the transit group id with an existing set. Note! Both the set * and the group id type is {@code int}. @@ -11,7 +11,7 @@ public interface RaptorTransitGroupCalculator { * @param boardingGroupId the transit group id to add to the given set. * @return the new computed set of groupIds */ - int mergeGroupIds(int currentGroupIds, int boardingGroupId); + int mergeInGroupId(int currentGroupIds, int boardingGroupId); /** * This is the dominance function to use for comparing transit-groups. diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/configure/McRangeRaptorConfig.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/configure/McRangeRaptorConfig.java index 8eef90950dd..3673e78ee47 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/configure/McRangeRaptorConfig.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/configure/McRangeRaptorConfig.java @@ -6,7 +6,7 @@ import org.opentripplanner.raptor.api.model.DominanceFunction; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.request.MultiCriteriaRequest; -import org.opentripplanner.raptor.api.request.RaptorTransitGroupCalculator; +import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; import org.opentripplanner.raptor.rangeraptor.context.SearchContext; import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; import org.opentripplanner.raptor.rangeraptor.internalapi.ParetoSetCost; @@ -201,7 +201,7 @@ private DominanceFunction dominanceFunctionC2() { return null; } - private RaptorTransitGroupCalculator getTransitGroupPriorityCalculator() { + private RaptorTransitGroupPriorityCalculator getTransitGroupPriorityCalculator() { return mcRequest().transitPriorityCalculator().orElseThrow(); } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/ride/c2/TransitGroupPriorityRideFactory.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/ride/c2/TransitGroupPriorityRideFactory.java index 5d65c40d021..323067cc240 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/ride/c2/TransitGroupPriorityRideFactory.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/ride/c2/TransitGroupPriorityRideFactory.java @@ -2,7 +2,7 @@ import org.opentripplanner.raptor.api.model.RaptorTripPattern; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; -import org.opentripplanner.raptor.api.request.RaptorTransitGroupCalculator; +import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; import org.opentripplanner.raptor.rangeraptor.multicriteria.arrivals.McStopArrival; import org.opentripplanner.raptor.rangeraptor.multicriteria.ride.PatternRide; import org.opentripplanner.raptor.rangeraptor.multicriteria.ride.PatternRideFactory; @@ -15,10 +15,10 @@ public class TransitGroupPriorityRideFactory implements PatternRideFactory> { private int currentPatternGroupPriority; - private final RaptorTransitGroupCalculator transitGroupPriorityCalculator; + private final RaptorTransitGroupPriorityCalculator transitGroupPriorityCalculator; public TransitGroupPriorityRideFactory( - RaptorTransitGroupCalculator transitGroupPriorityCalculator + RaptorTransitGroupPriorityCalculator transitGroupPriorityCalculator ) { this.transitGroupPriorityCalculator = transitGroupPriorityCalculator; } @@ -55,6 +55,6 @@ public void prepareForTransitWith(RaptorTripPattern pattern) { * Currently transit-group-priority is the only usage of c2 */ private int calculateC2(int c2) { - return transitGroupPriorityCalculator.mergeGroupIds(c2, currentPatternGroupPriority); + return transitGroupPriorityCalculator.mergeInGroupId(c2, currentPatternGroupPriority); } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java b/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java index 06ceaeebeb2..7ac3dd1caf6 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java @@ -16,6 +16,7 @@ import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.framework.time.ServiceDateUtils; import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.model.plan.grouppriority.TransitGroupPriorityItineraryDecorator; import org.opentripplanner.model.plan.paging.cursor.PageCursorInput; import org.opentripplanner.raptor.api.request.RaptorTuningParameters; import org.opentripplanner.raptor.api.request.SearchParams; @@ -36,6 +37,7 @@ import org.opentripplanner.routing.framework.DebugTimingAggregator; import org.opentripplanner.service.paging.PagingService; import org.opentripplanner.standalone.api.OtpServerRequestContext; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,6 +66,7 @@ public class RoutingWorker { */ private final ZonedDateTime transitSearchTimeZero; private final AdditionalSearchDays additionalSearchDays; + private final TransitGroupPriorityService transitGroupPriorityService; private SearchParams raptorSearchParamsUsed = null; private PageCursorInput pageCursorInput = null; @@ -79,6 +82,12 @@ public RoutingWorker(OtpServerRequestContext serverContext, RouteRequest request this.transitSearchTimeZero = ServiceDateUtils.asStartOfService(request.dateTime(), zoneId); this.additionalSearchDays = createAdditionalSearchDays(serverContext.raptorTuningParameters(), zoneId, request); + this.transitGroupPriorityService = + TransitGroupPriorityService.of( + request.preferences().transit().relaxTransitGroupPriority(), + request.journey().transit().priorityGroupsByAgency(), + request.journey().transit().priorityGroupsGlobal() + ); } public RoutingResponse route() { @@ -122,6 +131,9 @@ public RoutingResponse route() { routeTransit(itineraries, routingErrors); } + // Set C2 value for Street and FLEX if transit-group-priority is used + new TransitGroupPriorityItineraryDecorator(transitGroupPriorityService).decorate(itineraries); + debugTimingAggregator.finishedRouting(); // Filter itineraries @@ -258,6 +270,7 @@ private Void routeTransit(List itineraries, Collection var transitResults = TransitRouter.route( request, serverContext, + transitGroupPriorityService, transitSearchTimeZero, additionalSearchDays, debugTimingAggregator diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java index e8b8ed43c1c..071814a7abf 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/ItineraryListFilterChainBuilder.java @@ -12,6 +12,7 @@ import java.util.function.Function; import javax.annotation.Nullable; import org.opentripplanner.ext.accessibilityscore.DecorateWithAccessibilityScore; +import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.collection.ListSection; import org.opentripplanner.framework.lang.Sandbox; import org.opentripplanner.model.plan.Itinerary; @@ -27,6 +28,8 @@ import org.opentripplanner.routing.algorithm.filterchain.filters.system.NumItinerariesFilter; import org.opentripplanner.routing.algorithm.filterchain.filters.system.OutsideSearchWindowFilter; import org.opentripplanner.routing.algorithm.filterchain.filters.system.PagingFilter; +import org.opentripplanner.routing.algorithm.filterchain.filters.system.SingleCriteriaComparator; +import org.opentripplanner.routing.algorithm.filterchain.filters.system.mcmax.McMaxLimitFilter; import org.opentripplanner.routing.algorithm.filterchain.filters.transit.DecorateTransitAlert; import org.opentripplanner.routing.algorithm.filterchain.filters.transit.KeepItinerariesWithFewestTransfers; import org.opentripplanner.routing.algorithm.filterchain.filters.transit.RemoveItinerariesWithShortStreetLeg; @@ -64,7 +67,6 @@ public class ItineraryListFilterChainBuilder { private static final int NOT_SET = -1; private final SortOrder sortOrder; private final List groupBySimilarity = new ArrayList<>(); - private ItineraryFilterDebugProfile debug = ItineraryFilterDebugProfile.OFF; private int maxNumberOfItineraries = NOT_SET; private ListSection maxNumberOfItinerariesCropSection = ListSection.TAIL; @@ -86,6 +88,7 @@ public class ItineraryListFilterChainBuilder { private double minBikeParkingDistance; private boolean removeTransitIfWalkingIsBetter = true; private ItinerarySortKey itineraryPageCut; + private boolean transitGroupPriorityUsed = false; /** * Sandbox filters which decorate the itineraries with extra information. @@ -292,6 +295,15 @@ public ItineraryListFilterChainBuilder withPagingDeduplicationFilter( return this; } + /** + * Adjust filters to include multi-criteria parameter c2 and treat it as the + * transit-group. + */ + public ItineraryListFilterChainBuilder withTransitGroupPriority() { + this.transitGroupPriorityUsed = true; + return this; + } + /** * If set, walk-all-the-way itineraries are removed. This happens AFTER e.g. the group-by and * remove-transit-with-higher-cost-than-best-on-street-only filter. This make sure that poor @@ -531,7 +543,7 @@ private ItineraryListFilter buildGroupBySameRoutesAndStopsFilter() { GroupBySameRoutesAndStops::new, List.of( new SortingFilter(SortOrderComparator.comparator(sortOrder)), - new RemoveFilter(new MaxLimit(GroupBySameRoutesAndStops.TAG, 1)) + new RemoveFilter(createMaxLimitFilter(GroupBySameRoutesAndStops.TAG, 1)) ) ); } @@ -574,7 +586,7 @@ private List buildGroupByTripIdAndDistanceFilters() { GroupByAllSameStations::new, List.of( new SortingFilter(generalizedCostComparator()), - new RemoveFilter(new MaxLimit(innerGroupName, 1)) + new RemoveFilter(createMaxLimitFilter(innerGroupName, 1)) ) ) ); @@ -587,7 +599,7 @@ private List buildGroupByTripIdAndDistanceFilters() { } addSort(nested, generalizedCostComparator()); - addRemoveFilter(nested, new MaxLimit(tag, group.maxNumOfItinerariesPerGroup)); + addRemoveFilter(nested, createMaxLimitFilter(tag, group.maxNumOfItinerariesPerGroup)); nested.add(new KeepItinerariesWithFewestTransfers(sysTags)); @@ -620,4 +632,20 @@ private static void addDecorateFilter( ) { filters.add(new DecorateFilter(decorator)); } + + private RemoveItineraryFlagger createMaxLimitFilter(String filterName, int maxLimit) { + if (OTPFeature.MultiCriteriaGroupMaxFilter.isOn()) { + List comparators = new ArrayList<>(); + comparators.add(SingleCriteriaComparator.compareGeneralizedCost()); + comparators.add(SingleCriteriaComparator.compareNumTransfers()); + if (transitGroupPriorityUsed) { + comparators.add(SingleCriteriaComparator.compareTransitGroupsPriority()); + } + return new McMaxLimitFilter(filterName, maxLimit, comparators); + } + // Default is to just use a "hard" max limit + else { + return new MaxLimit(filterName, maxLimit); + } + } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/SingleCriteriaComparator.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/SingleCriteriaComparator.java new file mode 100644 index 00000000000..fdaaf0d7657 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/SingleCriteriaComparator.java @@ -0,0 +1,69 @@ +package org.opentripplanner.routing.algorithm.filterchain.filters.system; + +import java.util.Comparator; +import java.util.function.ToIntFunction; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.transit.model.network.grouppriority.DefaultTransitGroupPriorityCalculator; + +/** + * Comparator used to compare a SINGLE criteria for dominance. The difference between this and the + * {@link org.opentripplanner.raptor.util.paretoset.ParetoComparator} is that: + *

    + *
  1. This applies to one criteria, not multiple.
  2. + *
  3. This interface applies to itineraries; It is not generic.
  4. + *
+ * A set of instances of this interface can be used to create a pareto-set. See + * {@link org.opentripplanner.raptor.util.paretoset.ParetoSet} and + * {@link org.opentripplanner.raptor.util.paretoset.ParetoComparator}. + *

+ * This interface extends {@link Comparator} so elements can be sorted as well. Not all criteria + * can be sorted, if so the {@link #strictOrder()} should return false (this is the default). + */ +@FunctionalInterface +public interface SingleCriteriaComparator { + DefaultTransitGroupPriorityCalculator GROUP_PRIORITY_CALCULATOR = new DefaultTransitGroupPriorityCalculator(); + + /** + * The left criteria dominates the right criteria. Note! The right criteria may dominate + * the left criteria if there is no {@link #strictOrder()}. If left and right are equals, then + * there is no dominance. + */ + boolean leftDominanceExist(Itinerary left, Itinerary right); + + /** + * Return true if the criteria can be deterministically sorted. + */ + default boolean strictOrder() { + return false; + } + + static SingleCriteriaComparator compareNumTransfers() { + return compareLessThan(Itinerary::getNumberOfTransfers); + } + + static SingleCriteriaComparator compareGeneralizedCost() { + return compareLessThan(Itinerary::getGeneralizedCost); + } + + @SuppressWarnings("OptionalGetWithoutIsPresent") + static SingleCriteriaComparator compareTransitGroupsPriority() { + return (left, right) -> + GROUP_PRIORITY_CALCULATOR + .dominanceFunction() + .leftDominateRight(left.getGeneralizedCost2().get(), right.getGeneralizedCost2().get()); + } + + static SingleCriteriaComparator compareLessThan(final ToIntFunction op) { + return new SingleCriteriaComparator() { + @Override + public boolean leftDominanceExist(Itinerary left, Itinerary right) { + return op.applyAsInt(left) < op.applyAsInt(right); + } + + @Override + public boolean strictOrder() { + return true; + } + }; + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/Group.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/Group.java new file mode 100644 index 00000000000..7bfdad83e8f --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/Group.java @@ -0,0 +1,55 @@ +package org.opentripplanner.routing.algorithm.filterchain.filters.system.mcmax; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * The purpose of a group is to maintain a list of items, all optimal for a single + * criteria/comparator. After the group is created, then the criteria is no longer needed, so we do + * not keep a reference to the original criteria. + */ +class Group implements Iterable { + + private final List items = new ArrayList<>(); + + public Group(Item firstItem) { + add(firstItem); + } + + Item first() { + return items.getFirst(); + } + + boolean isEmpty() { + return items.isEmpty(); + } + + boolean isSingleItemGroup() { + return items.size() == 1; + } + + void add(Item item) { + item.incGroupCount(); + items.add(item); + } + + void removeAllItems() { + items.forEach(Item::decGroupCount); + items.clear(); + } + + void addNewDominantItem(Item item) { + removeAllItems(); + add(item); + } + + boolean contains(Item item) { + return this.items.contains(item); + } + + @Override + public Iterator iterator() { + return items.iterator(); + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/Item.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/Item.java new file mode 100644 index 00000000000..36c3d662493 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/Item.java @@ -0,0 +1,47 @@ +package org.opentripplanner.routing.algorithm.filterchain.filters.system.mcmax; + +import org.opentripplanner.model.plan.Itinerary; + +/** + * An item is a decorated itinerary. The extra information added is the index in the input list + * (sort order) and a groupCount. The sort order is used to break ties, while the group-count is + * used to select the itinerary witch exist in the highest number of groups. The group dynamically + * updates the group-count; The count is incremented when an item is added to a group, and + * decremented when the group is removed from the State. + */ +class Item { + + private final Itinerary item; + private final int index; + private int groupCount = 0; + + Item(Itinerary item, int index) { + this.item = item; + this.index = index; + } + + /** + * An item is better than another if the groupCount is higher, and in case of a tie, if the sort + * index is lower. + */ + public boolean betterThan(Item o) { + return groupCount != o.groupCount ? groupCount > o.groupCount : index < o.index; + } + + Itinerary item() { + return item; + } + + void incGroupCount() { + ++this.groupCount; + } + + void decGroupCount() { + --this.groupCount; + } + + @Override + public String toString() { + return "Item #%d {count:%d, %s}".formatted(index, groupCount, item.toStr()); + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/McMaxLimitFilter.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/McMaxLimitFilter.java new file mode 100644 index 00000000000..c0b07e7400a --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/McMaxLimitFilter.java @@ -0,0 +1,105 @@ +package org.opentripplanner.routing.algorithm.filterchain.filters.system.mcmax; + +import java.util.List; +import java.util.function.Predicate; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.routing.algorithm.filterchain.filters.system.SingleCriteriaComparator; +import org.opentripplanner.routing.algorithm.filterchain.framework.spi.RemoveItineraryFlagger; + +/** + * This filter is used to reduce a set of itineraries down to the specified limit, if possible. + * The filter is guaranteed to keep at least the given {@code minNumItineraries} and/or the best + * itinerary for each criterion. The criterion is defined using the list of {@code comparators}. + *

+ * The main usage of this filter is to combine it with a transit grouping filter and for each group + * make sure there is at least {@code minNumItineraries} and that the best itinerary with respect + * to each criterion is kept. So, if the grouping is based on time and riding common trips, then + * this filter will use the remaining criterion (transfers, generalized-cost, + * [transit-group-priority]) to filter the grouped set of itineraries. DO NOT INCLUDE CRITERIA + * USED TO GROUP THE ITINERARIES, ONLY THE REMAINING CRITERION USED IN THE RAPTOR SEARCH. + *

+ * IMPLEMENTATION DETAILS + *

+ * This is not a trivial problem. In most cases, the best itinerary for a given criteria is unique, + * but there might be ties - same number of transfers, same cost, and/or different priority groups. + * In case of a tie, we will look if an itinerary is "best-in-group" for more than one criterion, + * if so we pick the one which is best in the highest number of groups. Again, if there is a tie + * (best in the same number of groups), then we fall back to the given itinerary sorting order. + *

+ * This filter will use the order of the input itineraries to break ties. So, make sure to call the + * appropriate sort function before this filter is invoked. + *

+ * Note! For criteria like num-of-transfers or generalized-cost, there is only one set of "best" + * itineraries, and usually there are only one or a few itineraries. In case there is more than one, + * picking just one is fine. But, for transit-group-priority there might be more than one optimal + * set of itineraries. For each set, we need to pick one itinerary for the final result. Each of + * these sets may or may not have more than one itinerary. If you group by agency, then there will + * be at least one itinerary for each agency present in the result (simplified, an itinerary may + * consist of legs with different agencies). The transit-group-priority pareto-function used by + * Raptor is reused, so we do not need to worry about the logic here. + *

+ * Let's discuss an example (this example also exists as a unit-test case): + *

+ *   minNumItineraries = 4
+ *   comparators = [ generalized-cost, min-num-transfers, transit-group-priority ]
+ *   itineraries: [
+ *    #0 : [ 1000, 2, (a) ]
+ *    #1 : [ 1000, 3, (a,b) ]
+ *    #2 : [ 1000, 3, (b) ]
+ *    #3 : [ 1200, 1, (a,b) ]
+ *    #4 : [ 1200, 1, (a) ]
+ *    #5 : [ 1300, 2, (c) ]
+ *    #6 : [ 1300, 3, (c) ]
+ *   ]
+ * 
+ * The best itineraries by generalized-cost are (#0, #1, #2). The best itineraries by + * min-num-transfers are (#3, #4). The best itineraries by transit-group-priority are + * (a:(#0, #4), b:(#2), c:(#5, #6)). + *

+ * So we need to pick one from each group (#0, #1, #2), (#3, #4), (#0, #4), (#2), and (#5, #6). + * Since #2 is a single, we pick it first. Itinerary #2 is also one of the best + * generalized-cost itineraries - so we are done with generalized-cost itineraries as well. The two + * groups left are (#3, #4), (#0, #4), and (#5, #6). #4 exists in 2 groups, so we pick it next. Now + * we are left with (#5, #6). To break the tie, we look at the sort-order. We pick + * itinerary #5. Result: #2, #4, and #5. + *

+ * The `minNumItineraries` limit is not met, so we need to pick another itinerary, we use the + * sort-order again and add itinerary #0. The result returned is: [#0, #2, #4, #5] + */ +public class McMaxLimitFilter implements RemoveItineraryFlagger { + + private final String name; + private final int minNumItineraries; + private final List comparators; + + public McMaxLimitFilter( + String name, + int minNumItineraries, + List comparators + ) { + this.name = name; + this.minNumItineraries = minNumItineraries; + this.comparators = comparators; + } + + @Override + public String name() { + return name; + } + + @Override + public List flagForRemoval(List itineraries) { + if (itineraries.size() <= minNumItineraries) { + return List.of(); + } + var state = new State(itineraries, comparators); + state.findAllSingleItemGroupsAndAddTheItemToTheResult(); + state.findTheBestItemsUntilAllGroupsAreRepresentedInTheResult(); + state.fillUpTheResultWithMinimumNumberOfItineraries(minNumItineraries); + + // We now have the itineraries we want, but we must invert this and return the + // list of itineraries to drop - keeping the original order + var ok = state.getResult(); + return itineraries.stream().filter(Predicate.not(ok::contains)).toList(); + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/State.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/State.java new file mode 100644 index 00000000000..93b8b1097c9 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/State.java @@ -0,0 +1,201 @@ +package org.opentripplanner.routing.algorithm.filterchain.filters.system.mcmax; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.annotation.Nullable; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.routing.algorithm.filterchain.filters.system.SingleCriteriaComparator; + +/** + * Keep a list of items, groups and the result in progress. This is just a class for + * simple bookkeeping for the state of the filter. + */ +class State { + + private final List items; + private final List groups; + private final List result = new ArrayList<>(); + + /** + * Initialize the state by wrapping each itinerary in an item (with index) and create groups for + * each criterion with the best itineraries (can be more than one with, for example, the same + * cost). There should be at least one itinerary from each group surviving the filtering process. + * The same itinerary can exist in multiple groups. + */ + State(List itineraries, List comparators) { + this.items = createListOfItems(itineraries); + this.groups = createGroups(items, comparators); + } + + List getResult() { + return result.stream().map(Item::item).toList(); + } + + /** + * Find and add all groups with a single item in them and add them to the result + */ + void findAllSingleItemGroupsAndAddTheItemToTheResult() { + var item = findItemInFirstSingleItemGroup(groups); + while (item != null) { + addToResult(item); + item = findItemInFirstSingleItemGroup(groups); + } + } + + /** + * Find the items with the highest group count and the lowest index. Theoretically, there might be + * a smaller set of itineraries that TOGETHER represent all groups than what we achieve here, but + * it is far more complicated to compute - so this is probably good enough. + */ + void findTheBestItemsUntilAllGroupsAreRepresentedInTheResult() { + while (!groups.isEmpty()) { + addToResult(findBestItem(groups)); + } + } + + /** + * Fill up with itineraries until the minimum number of itineraries is reached + */ + void fillUpTheResultWithMinimumNumberOfItineraries(int minNumItineraries) { + int end = Math.min(items.size(), minNumItineraries); + for (int i = 0; result.size() < end; ++i) { + var it = items.get(i); + if (!result.contains(it)) { + result.add(it); + } + } + } + + private void addToResult(Item item) { + result.add(item); + removeGroupsWitchContainsItem(item); + } + + /** + * If an itinerary is accepted into the final result, then all groups that contain that itinerary + * can be removed. In addition, the item groupCount should be decremented if a group is dropped. + * This makes sure that the groups represented in the final result do not count when selecting the + * next item. + */ + private void removeGroupsWitchContainsItem(Item item) { + for (Group group : groups) { + if (group.contains(item)) { + group.removeAllItems(); + } + } + groups.removeIf(Group::isEmpty); + } + + /** + * The best item is the one which exists in most groups, and in case of a tie, the sort order/ + * itinerary index is used. + */ + private static Item findBestItem(List groups) { + var candidate = groups.getFirst().first(); + for (Group group : groups) { + for (Item item : group) { + if (item.betterThan(candidate)) { + candidate = item; + } + } + } + return candidate; + } + + /** + * Search through all groups and return all items witch comes from groups with only one item. + */ + @Nullable + private static Item findItemInFirstSingleItemGroup(List groups) { + return groups + .stream() + .filter(Group::isSingleItemGroup) + .findFirst() + .map(Group::first) + .orElse(null); + } + + private static ArrayList createListOfItems(List itineraries) { + var items = new ArrayList(); + for (int i = 0; i < itineraries.size(); i++) { + items.add(new Item(itineraries.get(i), i)); + } + return items; + } + + private static List createGroups( + Collection items, + List comparators + ) { + List groups = new ArrayList<>(); + for (SingleCriteriaComparator comparator : comparators) { + if (comparator.strictOrder()) { + groups.add(createOrderedGroup(items, comparator)); + } else { + groups.addAll(createUnorderedGroups(items, comparator)); + } + } + return groups; + } + + /** + * In a strict ordered group only one optimal value exist for the criteria defined by the given + * {@code comparator}. All items that have this value should be included in the group created. + */ + private static Group createOrderedGroup( + Collection items, + SingleCriteriaComparator comparator + ) { + Group group = null; + for (Item item : items) { + if (group == null) { + group = new Group(item); + continue; + } + var current = group.first(); + if (comparator.leftDominanceExist(item.item(), current.item())) { + group.addNewDominantItem(item); + } else if (!comparator.leftDominanceExist(current.item(), item.item())) { + group.add(item); + } + } + return group; + } + + /** + * For a none strict ordered criteria, multiple optimal values exist. The criterion is defined by + * the given {@code comparator}. This method will create a group for each optimal value found in + * the given set of items. + * + * @see #createOrderedGroup(Collection, SingleCriteriaComparator) + */ + private static Collection createUnorderedGroups( + Collection items, + SingleCriteriaComparator comparator + ) { + List result = new ArrayList<>(); + + for (Item item : items) { + int groupCount = result.size(); + for (Group group : result) { + var groupItem = group.first().item(); + if (comparator.leftDominanceExist(groupItem, item.item())) { + if (comparator.leftDominanceExist(item.item(), groupItem)) { + // Mutual dominance => the item belong in another group + --groupCount; + } + } else { + if (comparator.leftDominanceExist(item.item(), groupItem)) { + group.removeAllItems(); + } + group.add(item); + } + } + if (groupCount == 0) { + result.add(new Group(item)); + } + } + return result; + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java index 651d94b4eac..c1fab68f999 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RouteRequestToFilterChainMapper.java @@ -94,6 +94,10 @@ public static ItineraryListFilterChain createFilterChain( .withRemoveTransitIfWalkingIsBetter(true) .withDebugEnabled(params.debug()); + if (!request.preferences().transit().relaxTransitGroupPriority().isNormal()) { + builder.withTransitGroupPriority(); + } + var fareService = context.graph().getFareService(); if (fareService != null) { builder.withFareDecorator(new DecorateWithFare(fareService)); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java index 06f4ff1cf45..37bb7270902 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java @@ -41,6 +41,7 @@ import org.opentripplanner.routing.framework.DebugTimingAggregator; import org.opentripplanner.standalone.api.OtpServerRequestContext; import org.opentripplanner.street.search.TemporaryVerticesContainer; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; public class TransitRouter { @@ -48,6 +49,7 @@ public class TransitRouter { private final RouteRequest request; private final OtpServerRequestContext serverContext; + private final TransitGroupPriorityService transitGroupPriorityService; private final DebugTimingAggregator debugTimingAggregator; private final ZonedDateTime transitSearchTimeZero; private final AdditionalSearchDays additionalSearchDays; @@ -56,12 +58,14 @@ public class TransitRouter { private TransitRouter( RouteRequest request, OtpServerRequestContext serverContext, + TransitGroupPriorityService transitGroupPriorityService, ZonedDateTime transitSearchTimeZero, AdditionalSearchDays additionalSearchDays, DebugTimingAggregator debugTimingAggregator ) { this.request = request; this.serverContext = serverContext; + this.transitGroupPriorityService = transitGroupPriorityService; this.transitSearchTimeZero = transitSearchTimeZero; this.additionalSearchDays = additionalSearchDays; this.debugTimingAggregator = debugTimingAggregator; @@ -71,6 +75,7 @@ private TransitRouter( public static TransitRouterResult route( RouteRequest request, OtpServerRequestContext serverContext, + TransitGroupPriorityService priorityGroupConfigurator, ZonedDateTime transitSearchTimeZero, AdditionalSearchDays additionalSearchDays, DebugTimingAggregator debugTimingAggregator @@ -78,6 +83,7 @@ public static TransitRouterResult route( TransitRouter transitRouter = new TransitRouter( request, serverContext, + priorityGroupConfigurator, transitSearchTimeZero, additionalSearchDays, debugTimingAggregator @@ -309,6 +315,7 @@ private RaptorRoutingRequestTransitData createRequestTransitDataProvider( ) { return new RaptorRoutingRequestTransitData( transitLayer, + transitGroupPriorityService, transitSearchTimeZero, additionalSearchDays.additionalSearchDaysInPast(), additionalSearchDays.additionalSearchDaysInFuture(), diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java index 879536fdcd0..948b132e408 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java @@ -21,10 +21,10 @@ import org.opentripplanner.raptor.rangeraptor.SystemErrDebugLogger; import org.opentripplanner.routing.algorithm.raptoradapter.router.performance.PerformanceTimersForRaptor; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.grouppriority.TransitGroupPriority32n; import org.opentripplanner.routing.api.request.DebugEventType; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.framework.CostLinearFunction; +import org.opentripplanner.transit.model.network.grouppriority.DefaultTransitGroupPriorityCalculator; import org.opentripplanner.transit.model.site.StopLocation; public class RaptorRequestMapper { @@ -119,7 +119,7 @@ private RaptorRequest doMap() { mcBuilder.withPassThroughPoints(mapPassThroughPoints()); r.relaxGeneralizedCostAtDestination().ifPresent(mcBuilder::withRelaxCostAtDestination); } else if (!pt.relaxTransitGroupPriority().isNormal()) { - mcBuilder.withTransitPriorityCalculator(TransitGroupPriority32n.priorityCalculator()); + mcBuilder.withTransitPriorityCalculator(new DefaultTransitGroupPriorityCalculator()); mcBuilder.withRelaxC1(mapRelaxCost(pt.relaxTransitGroupPriority())); } }); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransfersMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransfersMapper.java index 74a5d7a6352..c7e9ea05320 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransfersMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransfersMapper.java @@ -6,7 +6,7 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.Transfer; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.service.StopModel; -import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.transit.service.TransitService; class TransfersMapper { @@ -14,7 +14,7 @@ class TransfersMapper { * Copy pre-calculated transfers from the original graph * @return a list where each element is a list of transfers for the corresponding stop index */ - static List> mapTransfers(StopModel stopModel, TransitModel transitModel) { + static List> mapTransfers(StopModel stopModel, TransitService transitService) { List> transferByStopIndex = new ArrayList<>(); for (int i = 0; i < stopModel.stopIndexSize(); ++i) { @@ -26,7 +26,7 @@ static List> mapTransfers(StopModel stopModel, TransitModel trans ArrayList list = new ArrayList<>(); - for (PathTransfer pathTransfer : transitModel.getTransfersByStop(stop)) { + for (PathTransfer pathTransfer : transitService.getTransfersByStop(stop)) { if (pathTransfer.to instanceof RegularStop) { int toStopIndex = pathTransfer.to.getIndex(); Transfer newTransfer; diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java index 33a076bb8d6..8ce328fe1b6 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerMapper.java @@ -27,8 +27,10 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.StopTransferPriority; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.StopModel; import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.transit.service.TransitService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,10 +49,12 @@ public class TransitLayerMapper { private static final Logger LOG = LoggerFactory.getLogger(TransitLayerMapper.class); - private final TransitModel transitModel; + private final TransitService transitService; + private final StopModel stopModel; private TransitLayerMapper(TransitModel transitModel) { - this.transitModel = transitModel; + this.transitService = new DefaultTransitService(transitModel); + this.stopModel = transitModel.getStopModel(); } public static TransitLayer map( @@ -74,20 +78,19 @@ private TransitLayer map(TransitTuningParameters tuningParameters) { HashMap> tripPatternsByStopByDate; List> transferByStopIndex; ConstrainedTransfersForPatterns constrainedTransfers = null; - StopModel stopModel = transitModel.getStopModel(); LOG.info("Mapping transitLayer from TransitModel..."); - Collection allTripPatterns = transitModel.getAllTripPatterns(); + Collection allTripPatterns = transitService.getAllTripPatterns(); tripPatternsByStopByDate = mapTripPatterns(allTripPatterns); - transferByStopIndex = mapTransfers(stopModel, transitModel); + transferByStopIndex = mapTransfers(stopModel, transitService); TransferIndexGenerator transferIndexGenerator = null; if (OTPFeature.TransferConstraints.isOn()) { transferIndexGenerator = - new TransferIndexGenerator(transitModel.getTransferService().listAll(), allTripPatterns); + new TransferIndexGenerator(transitService.getTransferService().listAll(), allTripPatterns); constrainedTransfers = transferIndexGenerator.generateTransfers(); } @@ -98,9 +101,9 @@ private TransitLayer map(TransitTuningParameters tuningParameters) { return new TransitLayer( tripPatternsByStopByDate, transferByStopIndex, - transitModel.getTransferService(), + transitService.getTransferService(), stopModel, - transitModel.getTimeZone(), + transitService.getTimeZone(), transferCache, constrainedTransfers, transferIndexGenerator, @@ -118,13 +121,10 @@ private HashMap> mapTripPatterns( Collection allTripPatterns ) { TripPatternForDateMapper tripPatternForDateMapper = new TripPatternForDateMapper( - transitModel.getTransitModelIndex().getServiceCodesRunningForDate() + transitService.getServiceCodesRunningForDate() ); - Set allServiceDates = transitModel - .getTransitModelIndex() - .getServiceCodesRunningForDate() - .keySet(); + Set allServiceDates = transitService.getAllServiceCodes(); List tripPatternForDates = Collections.synchronizedList(new ArrayList<>()); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java index 5188bdef8b1..934bec39c11 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java @@ -2,7 +2,6 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.SetMultimap; -import gnu.trove.set.TIntSet; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; @@ -20,7 +19,7 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; -import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.transit.service.TransitEditorService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,9 +39,7 @@ public class TransitLayerUpdater { private static final Logger LOG = LoggerFactory.getLogger(TransitLayerUpdater.class); - private final TransitModel transitModel; - - private final Map serviceCodesRunningForDate; + private final TransitEditorService transitService; /** * Cache the TripPatternForDates indexed on the original TripPatterns in order to avoid this @@ -58,19 +55,15 @@ public class TransitLayerUpdater { private final Map> tripPatternsRunningOnDateMapCache = new HashMap<>(); - public TransitLayerUpdater( - TransitModel transitModel, - Map serviceCodesRunningForDate - ) { - this.transitModel = transitModel; - this.serviceCodesRunningForDate = serviceCodesRunningForDate; + public TransitLayerUpdater(TransitEditorService transitService) { + this.transitService = transitService; } public void update( Set updatedTimetables, Map> timetables ) { - if (!transitModel.hasRealtimeTransitLayer()) { + if (!transitService.hasRealtimeTransitLayer()) { return; } @@ -78,11 +71,11 @@ public void update( // Make a shallow copy of the realtime transit layer. Only the objects that are copied will be // changed during this update process. - TransitLayer realtimeTransitLayer = new TransitLayer(transitModel.getRealtimeTransitLayer()); + TransitLayer realtimeTransitLayer = new TransitLayer(transitService.getRealtimeTransitLayer()); // Instantiate a TripPatternForDateMapper with the new TripPattern mappings TripPatternForDateMapper tripPatternForDateMapper = new TripPatternForDateMapper( - serviceCodesRunningForDate + transitService.getServiceCodesRunningForDate() ); Set datesToBeUpdated = new HashSet<>(); @@ -229,7 +222,7 @@ public void update( // Switch out the reference with the updated realtimeTransitLayer. This is synchronized to // guarantee that the reference is set after all the fields have been updated. - transitModel.setRealtimeTransitLayer(realtimeTransitLayer); + transitService.setRealtimeTransitLayer(realtimeTransitLayer); LOG.debug( "UPDATING {} tripPatterns took {} ms", diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapper.java index 30551421138..cd00b9356dc 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapper.java @@ -41,7 +41,7 @@ public class TripPatternForDateMapper { * @param serviceCodesRunningForDate - READ ONLY */ TripPatternForDateMapper(Map serviceCodesRunningForDate) { - this.serviceCodesRunningForDate = Collections.unmodifiableMap(serviceCodesRunningForDate); + this.serviceCodesRunningForDate = serviceCodesRunningForDate; } /** diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupConfigurator.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupConfigurator.java deleted file mode 100644 index 6ef82786b99..00000000000 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupConfigurator.java +++ /dev/null @@ -1,144 +0,0 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit.request; - -import gnu.trove.impl.Constants; -import gnu.trove.map.TObjectIntMap; -import gnu.trove.map.hash.TObjectIntHashMap; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.stream.Stream; -import org.opentripplanner.framework.lang.ArrayUtils; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.grouppriority.TransitGroupPriority32n; -import org.opentripplanner.routing.api.request.request.filter.TransitGroupSelect; -import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.network.TripPattern; - -/** - * This class dynamically builds an index of transit-group-ids from the - * provided {@link TransitGroupSelect}s while serving the caller with - * group-ids for each requested pattern. It is made for optimal - * performance, since it is used in request scope. - *

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

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

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

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

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

- * @throws IllegalArgumentException if more than 32 group-ids are requested. - */ - public int lookupTransitGroupPriorityId(TripPattern tripPattern) { - if (!enabled || tripPattern == null) { - return baseGroupId; - } - - for (var it : agencyMatchersIds) { - if (it.matcher().match(tripPattern)) { - var agencyId = tripPattern.getRoute().getAgency().getId(); - int groupId = it.ids().get(agencyId); - - if (groupId < 0) { - groupId = nextGroupId(); - it.ids.put(agencyId, groupId); - } - return groupId; - } - } - - for (var it : globalMatchersIds) { - if (it.matcher.match(tripPattern)) { - return it.groupId(); - } - } - // Fallback to base-group-id - return baseGroupId; - } - - public int baseGroupId() { - return baseGroupId; - } - - private int nextGroupId() { - return TransitGroupPriority32n.groupId(++groupIndexCounter); - } - - /** Pair of matcher and groupId. Used only inside this class. */ - record MatcherAndId(PriorityGroupMatcher matcher, int groupId) {} - - /** Matcher with map of ids by agency. */ - record MatcherAgencyAndIds(PriorityGroupMatcher matcher, TObjectIntMap ids) { - MatcherAgencyAndIds(PriorityGroupMatcher matcher) { - this( - matcher, - new TObjectIntHashMap<>(Constants.DEFAULT_CAPACITY, Constants.DEFAULT_LOAD_FACTOR, -1) - ); - } - } -} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java index 19b5ccf8502..5b9d81fa6c3 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java @@ -30,10 +30,11 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.GeneralizedCostParametersMapper; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.transit.model.network.RoutingTripPattern; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; /** * This is the data provider for the Range Raptor search engine. It uses data from the TransitLayer, - * but filters it by dates and modes per request. Transfers durations are pre-calculated per request + * but filters it by dates and modes per request. Transfer durations are pre-calculated per request * based on walk speed. */ public class RaptorRoutingRequestTransitData implements RaptorTransitDataProvider { @@ -71,6 +72,7 @@ public class RaptorRoutingRequestTransitData implements RaptorTransitDataProvide public RaptorRoutingRequestTransitData( TransitLayer transitLayer, + TransitGroupPriorityService transitGroupPriorityService, ZonedDateTime transitSearchTimeZero, int additionalPastSearchDays, int additionalFutureSearchDays, @@ -82,7 +84,7 @@ public RaptorRoutingRequestTransitData( this.transitSearchTimeZero = transitSearchTimeZero; // Delegate to the creator to construct the needed data structures. The code is messy so - // it is nice to NOT have it in the class. It isolate this code to only be available at + // it is nice to NOT have it in the class. It isolates this code to only be available at // the time of construction var transitDataCreator = new RaptorRoutingRequestTransitDataCreator( transitLayer, @@ -92,7 +94,7 @@ public RaptorRoutingRequestTransitData( additionalPastSearchDays, additionalFutureSearchDays, filter, - createTransitGroupPriorityConfigurator(request) + transitGroupPriorityService ); this.patternIndex = transitDataCreator.createPatternIndex(tripPatterns); this.activeTripPatternsPerStop = transitDataCreator.createTripPatternsPerStop(tripPatterns); @@ -242,15 +244,4 @@ public RaptorConstrainedBoardingSearch transferConstraintsReverseS } return new ConstrainedBoardingSearch(false, toStopTransfers, fromStopTransfers); } - - private PriorityGroupConfigurator createTransitGroupPriorityConfigurator(RouteRequest request) { - if (request.preferences().transit().relaxTransitGroupPriority().isNormal()) { - return PriorityGroupConfigurator.empty(); - } - var transitRequest = request.journey().transit(); - return PriorityGroupConfigurator.of( - transitRequest.priorityGroupsByAgency(), - transitRequest.priorityGroupsGlobal() - ); - } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreator.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreator.java index f987e4f7a21..815bf839e31 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreator.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreator.java @@ -19,6 +19,7 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripPatternForDate; import org.opentripplanner.transit.model.network.RoutingTripPattern; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; import org.opentripplanner.transit.model.timetable.TripTimes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -93,7 +94,7 @@ static List merge( ZonedDateTime transitSearchTimeZero, List patternForDateList, TransitDataProviderFilter filter, - PriorityGroupConfigurator priorityGroupConfigurator + TransitGroupPriorityService transitGroupPriorityService ) { // Group TripPatternForDate objects by TripPattern. // This is done in a loop to increase performance. @@ -147,7 +148,7 @@ static List merge( tripPattern.getAlightingPossible(), BoardAlight.ALIGHT ), - priorityGroupConfigurator.lookupTransitGroupPriorityId(tripPattern.getPattern()) + transitGroupPriorityService.lookupTransitGroupPriorityId(tripPattern.getPattern()) ) ); } @@ -159,7 +160,7 @@ List createTripPatterns( int additionalPastSearchDays, int additionalFutureSearchDays, TransitDataProviderFilter filter, - PriorityGroupConfigurator priorityGroupConfigurator + TransitGroupPriorityService transitGroupPriorityService ) { List tripPatternForDates = getTripPatternsForDateRange( additionalPastSearchDays, @@ -167,7 +168,7 @@ List createTripPatterns( filter ); - return merge(transitSearchTimeZero, tripPatternForDates, filter, priorityGroupConfigurator); + return merge(transitSearchTimeZero, tripPatternForDates, filter, transitGroupPriorityService); } private static List filterActiveTripPatterns( diff --git a/src/main/java/org/opentripplanner/routing/graph/Graph.java b/src/main/java/org/opentripplanner/routing/graph/Graph.java index 4b6c9938317..fa3d12b92f7 100644 --- a/src/main/java/org/opentripplanner/routing/graph/Graph.java +++ b/src/main/java/org/opentripplanner/routing/graph/Graph.java @@ -16,7 +16,6 @@ import javax.annotation.Nullable; import org.locationtech.jts.geom.Geometry; import org.opentripplanner.ext.dataoverlay.configuration.DataOverlayParameterBindings; -import org.opentripplanner.ext.geocoder.LuceneIndex; import org.opentripplanner.framework.geometry.CompactElevationProfile; import org.opentripplanner.framework.geometry.GeometryUtils; import org.opentripplanner.model.calendar.openinghours.OpeningHoursCalendarService; @@ -90,12 +89,6 @@ public class Graph implements Serializable { /** True if OSM data was loaded into this Graph. */ public boolean hasStreets = false; - /** - * Have bike parks already been linked to the graph. As the linking happens twice if a base graph - * is used, we store information on whether bike park linking should be skipped. - */ - - public boolean hasLinkedBikeParks = false; /** * The difference in meters between the WGS84 ellipsoid height and geoid height at the graph's * center @@ -136,7 +129,6 @@ public class Graph implements Serializable { * creating the data overlay context when routing. */ public DataOverlayParameterBindings dataOverlayParameterBindings; - private LuceneIndex luceneIndex; @Inject public Graph( @@ -384,14 +376,6 @@ public void setFareService(FareService fareService) { this.fareService = fareService; } - public LuceneIndex getLuceneIndex() { - return luceneIndex; - } - - public void setLuceneIndex(LuceneIndex luceneIndex) { - this.luceneIndex = luceneIndex; - } - private void indexIfNotIndexed(StopModel stopModel) { if (streetIndex == null) { index(stopModel); diff --git a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParking.java b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParking.java index 6ebd34e1287..d5dfc4ce8c1 100644 --- a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParking.java +++ b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParking.java @@ -308,6 +308,7 @@ public boolean equals(Object o) { public String toString() { return ToStringBuilder .of(VehicleParking.class) + .addStr("id", id.toString()) .addStr("name", name.toString()) .addObj("coordinate", coordinate) .toString(); diff --git a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingHelper.java b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingHelper.java index 507ce9329ed..257f2805ca2 100644 --- a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingHelper.java +++ b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingHelper.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.street.model.edge.StreetVehicleParkingLink; import org.opentripplanner.street.model.edge.VehicleParkingEdge; @@ -30,7 +29,7 @@ public List createVehicleParkingVertices( .getEntrances() .stream() .map(vertexFactory::vehicleParkingEntrance) - .collect(Collectors.toList()); + .toList(); } public static void linkVehicleParkingEntrances( diff --git a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingSpaces.java b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingSpaces.java index de25fc75521..8b21cc21f2c 100644 --- a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingSpaces.java +++ b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingSpaces.java @@ -2,6 +2,7 @@ import java.io.Serializable; import java.util.Objects; +import org.opentripplanner.framework.tostring.ToStringBuilder; /** * The number of spaces by type. {@code null} if unknown. @@ -70,6 +71,16 @@ public boolean equals(Object o) { ); } + @Override + public String toString() { + return ToStringBuilder + .of(VehicleParkingSpaces.class) + .addNum("carSpaces", carSpaces) + .addNum("wheelchairAccessibleCarSpaces", wheelchairAccessibleCarSpaces) + .addNum("bicycleSpaces", bicycleSpaces) + .toString(); + } + public static class VehicleParkingSpacesBuilder { private Integer bicycleSpaces; diff --git a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index 6552d82770f..3a577611617 100644 --- a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -8,6 +8,7 @@ import org.opentripplanner.ext.dataoverlay.routing.DataOverlayContext; import org.opentripplanner.ext.emissions.EmissionsService; import org.opentripplanner.ext.flex.FlexParameters; +import org.opentripplanner.ext.geocoder.LuceneIndex; import org.opentripplanner.ext.ridehailing.RideHailingService; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; import org.opentripplanner.framework.application.OTPFeature; @@ -136,4 +137,7 @@ default DataOverlayContext dataOverlayContext(RouteRequest request) { ) ); } + + @Nullable + LuceneIndex lucenceIndex(); } diff --git a/src/main/java/org/opentripplanner/standalone/config/buildconfig/NetexConfig.java b/src/main/java/org/opentripplanner/standalone/config/buildconfig/NetexConfig.java index d4cd4521e54..52bccf605d8 100644 --- a/src/main/java/org/opentripplanner/standalone/config/buildconfig/NetexConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/buildconfig/NetexConfig.java @@ -3,6 +3,7 @@ import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_0; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_2; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_3; +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_6; import org.opentripplanner.netex.config.NetexFeedParameters; import org.opentripplanner.standalone.config.framework.json.NodeAdapter; @@ -164,6 +165,14 @@ private static NetexFeedParameters.Builder mapFilePatternParameters( .summary("Ignore contents of the FareFrame") .docDefaultValue(base.ignoreFareFrame()) .asBoolean(base.ignoreFareFrame()) + ) + .withIgnoreParking( + config + .of("ignoreParking") + .since(V2_6) + .summary("Ignore Parking elements.") + .docDefaultValue(base.ignoreParking()) + .asBoolean(base.ignoreParking()) ); } diff --git a/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java index 6fa33aac267..dc91388458a 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java @@ -514,7 +514,7 @@ the access legs used. In other cases where the access(CAR) is faster than transi guaranteed to be optimal. Use itinerary-filters to limit what is presented to the client. The duration can be set per mode(`maxDurationForMode`), because some street modes searches are much more resource intensive than others. A default value is applied if the mode specific value -do not exist. +does not exist. """ ) .asDuration(dftAccessEgress.maxDuration().defaultValue()), @@ -554,7 +554,7 @@ duration can be set per mode(`maxDurationForMode`), because some street modes se guaranteed to be optimal. Use itinerary-filters to limit what is presented to the client. The duration can be set per mode(`maxDirectStreetDurationForMode`), because some street modes searches are much more resource intensive than others. A default value is applied if the mode specific value -do not exist." +does not exist." """ ) .asDuration(dft.maxDirectDuration().defaultValue()), diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java index b1bc6888753..52f7970ccd7 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java @@ -5,7 +5,6 @@ import org.opentripplanner.apis.transmodel.TransmodelAPI; import org.opentripplanner.datastore.api.DataSource; import org.opentripplanner.ext.emissions.EmissionsDataModel; -import org.opentripplanner.ext.geocoder.LuceneIndex; import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; import org.opentripplanner.framework.application.LogMDCSupport; import org.opentripplanner.framework.application.OTPFeature; @@ -33,6 +32,7 @@ import org.opentripplanner.standalone.server.OTPWebApplication; import org.opentripplanner.street.model.StreetLimitationParameters; import org.opentripplanner.street.model.elevation.ElevationUtils; +import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; import org.opentripplanner.updater.configure.UpdaterConfigurator; import org.opentripplanner.visualizer.GraphVisualizer; @@ -180,8 +180,9 @@ private void setupTransitRoutingServer() { } if (OTPFeature.SandboxAPIGeocoder.isOn()) { - LOG.info("Creating debug client geocoder lucene index"); - LuceneIndex.forServer(createServerContext()); + LOG.info("Initializing geocoder"); + // eagerly initialize the geocoder + this.factory.luceneIndex(); } } @@ -202,7 +203,7 @@ public static void creatTransitLayerForRaptor( TransitModel transitModel, TransitTuningParameters tuningParameters ) { - if (!transitModel.hasTransit() || transitModel.getTransitModelIndex() == null) { + if (!transitModel.hasTransit() || !transitModel.isIndexed()) { LOG.warn( "Cannot create Raptor data, that requires the graph to have transit data and be indexed." ); @@ -211,10 +212,7 @@ public static void creatTransitLayerForRaptor( transitModel.setTransitLayer(TransitLayerMapper.map(tuningParameters, transitModel)); transitModel.setRealtimeTransitLayer(new TransitLayer(transitModel.getTransitLayer())); transitModel.setTransitLayerUpdater( - new TransitLayerUpdater( - transitModel, - transitModel.getTransitModelIndex().getServiceCodesRunningForDate() - ) + new TransitLayerUpdater(new DefaultTransitService(transitModel)) ); } diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java index fe95fe0447d..b307776ef52 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java @@ -6,6 +6,8 @@ import javax.annotation.Nullable; import org.opentripplanner.ext.emissions.EmissionsDataModel; import org.opentripplanner.ext.emissions.EmissionsServiceModule; +import org.opentripplanner.ext.geocoder.LuceneIndex; +import org.opentripplanner.ext.geocoder.configure.GeocoderModule; import org.opentripplanner.ext.interactivelauncher.configuration.InteractiveLauncherModule; import org.opentripplanner.ext.ridehailing.configure.RideHailingServicesModule; import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; @@ -56,6 +58,7 @@ StopConsolidationServiceModule.class, InteractiveLauncherModule.class, StreetLimitationParametersServiceModule.class, + GeocoderModule.class, } ) public interface ConstructApplicationFactory { @@ -87,6 +90,9 @@ public interface ConstructApplicationFactory { StreetLimitationParameters streetLimitationParameters(); + @Nullable + LuceneIndex luceneIndex(); + @Component.Builder interface Builder { @BindsInstance diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java index eb244ce726c..6c830054c49 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java @@ -7,6 +7,7 @@ import javax.annotation.Nullable; import org.opentripplanner.astar.spi.TraverseVisitor; import org.opentripplanner.ext.emissions.EmissionsService; +import org.opentripplanner.ext.geocoder.LuceneIndex; import org.opentripplanner.ext.interactivelauncher.api.LauncherRequestDecorator; import org.opentripplanner.ext.ridehailing.RideHailingService; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; @@ -40,7 +41,8 @@ OtpServerRequestContext providesServerContext( StreetLimitationParametersService streetLimitationParametersService, @Nullable TraverseVisitor traverseVisitor, EmissionsService emissionsService, - LauncherRequestDecorator launcherRequestDecorator + LauncherRequestDecorator launcherRequestDecorator, + @Nullable LuceneIndex luceneIndex ) { var defaultRequest = launcherRequestDecorator.intercept(routerConfig.routingRequestDefaults()); @@ -60,7 +62,8 @@ OtpServerRequestContext providesServerContext( rideHailingServices, stopConsolidationService, streetLimitationParametersService, - traverseVisitor + traverseVisitor, + luceneIndex ); } diff --git a/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java index 7a4ccea9247..0e81193d787 100644 --- a/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java @@ -7,6 +7,7 @@ import org.opentripplanner.astar.spi.TraverseVisitor; import org.opentripplanner.ext.emissions.EmissionsService; import org.opentripplanner.ext.flex.FlexParameters; +import org.opentripplanner.ext.geocoder.LuceneIndex; import org.opentripplanner.ext.ridehailing.RideHailingService; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; import org.opentripplanner.inspector.raster.TileRendererManager; @@ -49,6 +50,7 @@ public class DefaultServerRequestContext implements OtpServerRequestContext { private final EmissionsService emissionsService; private final StopConsolidationService stopConsolidationService; private final StreetLimitationParametersService streetLimitationParametersService; + private final LuceneIndex luceneIndex; /** * Make sure all mutable components are copied/cloned before calling this constructor. @@ -70,7 +72,8 @@ private DefaultServerRequestContext( StopConsolidationService stopConsolidationService, StreetLimitationParametersService streetLimitationParametersService, FlexParameters flexParameters, - TraverseVisitor traverseVisitor + TraverseVisitor traverseVisitor, + @Nullable LuceneIndex luceneIndex ) { this.graph = graph; this.transitService = transitService; @@ -89,6 +92,7 @@ private DefaultServerRequestContext( this.emissionsService = emissionsService; this.stopConsolidationService = stopConsolidationService; this.streetLimitationParametersService = streetLimitationParametersService; + this.luceneIndex = luceneIndex; } /** @@ -110,7 +114,8 @@ public static DefaultServerRequestContext create( List rideHailingServices, @Nullable StopConsolidationService stopConsolidationService, StreetLimitationParametersService streetLimitationParametersService, - @Nullable TraverseVisitor traverseVisitor + @Nullable TraverseVisitor traverseVisitor, + @Nullable LuceneIndex luceneIndex ) { return new DefaultServerRequestContext( graph, @@ -129,7 +134,8 @@ public static DefaultServerRequestContext create( stopConsolidationService, streetLimitationParametersService, flexParameters, - traverseVisitor + traverseVisitor, + luceneIndex ); } @@ -235,6 +241,12 @@ public VectorTileConfig vectorTileConfig() { return vectorTileConfig; } + @Nullable + @Override + public LuceneIndex lucenceIndex() { + return luceneIndex; + } + @Override public EmissionsService emissionsService() { return emissionsService; diff --git a/src/main/java/org/opentripplanner/street/model/edge/StreetVehicleParkingLink.java b/src/main/java/org/opentripplanner/street/model/edge/StreetVehicleParkingLink.java index e682a7bfac1..2ad9d0f39c4 100644 --- a/src/main/java/org/opentripplanner/street/model/edge/StreetVehicleParkingLink.java +++ b/src/main/java/org/opentripplanner/street/model/edge/StreetVehicleParkingLink.java @@ -2,6 +2,7 @@ import javax.annotation.Nonnull; import org.locationtech.jts.geom.LineString; +import org.opentripplanner.framework.geometry.GeometryUtils; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.routing.api.request.preference.VehicleParkingPreferences; @@ -95,11 +96,8 @@ public I18NString getName() { return vehicleParkingEntranceVertex.getName(); } + @Override public LineString getGeometry() { - return null; - } - - public double getDistanceMeters() { - return 0; + return GeometryUtils.makeLineString(fromv.getCoordinate(), tov.getCoordinate()); } } diff --git a/src/main/java/org/opentripplanner/street/model/vertex/VehicleParkingEntranceVertex.java b/src/main/java/org/opentripplanner/street/model/vertex/VehicleParkingEntranceVertex.java index a3ab0aa17b0..ba7a1540715 100644 --- a/src/main/java/org/opentripplanner/street/model/vertex/VehicleParkingEntranceVertex.java +++ b/src/main/java/org/opentripplanner/street/model/vertex/VehicleParkingEntranceVertex.java @@ -1,10 +1,12 @@ package org.opentripplanner.street.model.vertex; +import java.util.Collection; import java.util.Objects; import javax.annotation.Nonnull; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.routing.vehicle_parking.VehicleParking; import org.opentripplanner.routing.vehicle_parking.VehicleParkingEntrance; +import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.edge.StreetVehicleParkingLink; import org.opentripplanner.street.model.edge.VehicleParkingEdge; @@ -49,4 +51,15 @@ public boolean isCarAccessible() { public boolean isWalkAccessible() { return parkingEntrance.isWalkAccessible(); } + + /** + * Is this vertex already linked to the graph with a {@link StreetVehicleParkingLink}? + */ + public boolean isLinkedToGraph() { + return hasLink(getIncoming()) || hasLink(getOutgoing()); + } + + private boolean hasLink(Collection edges) { + return edges.stream().anyMatch(StreetVehicleParkingLink.class::isInstance); + } } diff --git a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java index f9734c1cb1a..57c71d06113 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java @@ -331,17 +331,6 @@ public boolean isBoardAndAlightAt(int stopIndex, PickDrop value) { /* METHODS THAT DELEGATE TO THE SCHEDULED TIMETABLE */ - // TODO: These should probably be deprecated. That would require grabbing the scheduled timetable, - // and would avoid mistakes where real-time updates are accidentally not taken into account. - - public boolean stopPatternIsEqual(TripPattern other) { - return stopPattern.equals(other.stopPattern); - } - - public Trip getTrip(int tripIndex) { - return scheduledTimetable.getTripTimes(tripIndex).getTrip(); - } - // TODO OTP2 this method modifies the state, it will be refactored in a subsequent step /** * Add the given tripTimes to this pattern's scheduled timetable, recording the corresponding trip diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/BinarySetOperator.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/BinarySetOperator.java similarity index 79% rename from src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/BinarySetOperator.java rename to src/main/java/org/opentripplanner/transit/model/network/grouppriority/BinarySetOperator.java index 35e5b8c0918..4d8ce2c8fd7 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/BinarySetOperator.java +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/BinarySetOperator.java @@ -1,4 +1,4 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit.request; +package org.opentripplanner.transit.model.network.grouppriority; /** * Used to concatenate matches with either the logical "AND" or "OR" operator. diff --git a/src/main/java/org/opentripplanner/transit/model/network/grouppriority/DefaultTransitGroupPriorityCalculator.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/DefaultTransitGroupPriorityCalculator.java new file mode 100644 index 00000000000..3e96cf18f95 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/DefaultTransitGroupPriorityCalculator.java @@ -0,0 +1,26 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +import org.opentripplanner.raptor.api.model.DominanceFunction; +import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; + +/** + * Implement {@link RaptorTransitGroupPriorityCalculator}. + */ +public final class DefaultTransitGroupPriorityCalculator + implements RaptorTransitGroupPriorityCalculator { + + @Override + public int mergeInGroupId(int currentGroupIds, int boardingGroupId) { + return TransitGroupPriority32n.mergeInGroupId(currentGroupIds, boardingGroupId); + } + + @Override + public DominanceFunction dominanceFunction() { + return TransitGroupPriority32n::dominate; + } + + @Override + public String toString() { + return "DefaultTransitGroupCalculator{Using TGP32n}"; + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/network/grouppriority/EntityAdapter.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/EntityAdapter.java new file mode 100644 index 00000000000..760da3d87ca --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/EntityAdapter.java @@ -0,0 +1,16 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +/** + * These are the keys used to group transit trips and trip-patterns. This is used to calculate a + * unique groupId based on the request config. We use the adapter pattern to be able to generate + * the groupId based on different input types (TripPattern and Trip). + */ +interface EntityAdapter { + TransitMode mode(); + String subMode(); + FeedScopedId agencyId(); + FeedScopedId routeId(); +} diff --git a/src/main/java/org/opentripplanner/transit/model/network/grouppriority/Matcher.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/Matcher.java new file mode 100644 index 00000000000..bb5b4075364 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/Matcher.java @@ -0,0 +1,9 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +interface Matcher { + boolean match(EntityAdapter entity); + + default boolean isEmpty() { + return false; + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupMatcher.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/Matchers.java similarity index 54% rename from src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupMatcher.java rename to src/main/java/org/opentripplanner/transit/model/network/grouppriority/Matchers.java index c017f2862ab..7e1e7e6853a 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupMatcher.java +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/Matchers.java @@ -1,7 +1,7 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit.request; +package org.opentripplanner.transit.model.network.grouppriority; -import static org.opentripplanner.routing.algorithm.raptoradapter.transit.request.BinarySetOperator.AND; -import static org.opentripplanner.routing.algorithm.raptoradapter.transit.request.BinarySetOperator.OR; +import static org.opentripplanner.transit.model.network.grouppriority.BinarySetOperator.AND; +import static org.opentripplanner.transit.model.network.grouppriority.BinarySetOperator.OR; import java.util.ArrayList; import java.util.Arrays; @@ -18,7 +18,6 @@ import org.opentripplanner.routing.api.request.request.filter.TransitGroupSelect; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.network.TripPattern; /** * This class turns a {@link TransitGroupSelect} into a matcher. @@ -28,49 +27,38 @@ * a `CompositeMatcher`. So, a new matcher is only created if the field in the * select is present. */ -public abstract class PriorityGroupMatcher { +final class Matchers { - private static final PriorityGroupMatcher NOOP = new PriorityGroupMatcher() { - @Override - boolean match(TripPattern pattern) { - return false; - } + private static final Matcher NOOP = new EmptyMatcher(); - @Override - boolean isEmpty() { - return true; - } - }; - - public static PriorityGroupMatcher of(TransitGroupSelect select) { + static Matcher of(TransitGroupSelect select) { if (select.isEmpty()) { return NOOP; } - List list = new ArrayList<>(); + List list = new ArrayList<>(); if (!select.modes().isEmpty()) { list.add(new ModeMatcher(select.modes())); } if (!select.subModeRegexp().isEmpty()) { - list.add( - new RegExpMatcher("SubMode", select.subModeRegexp(), p -> p.getNetexSubmode().name()) - ); + list.add(new RegExpMatcher("SubMode", select.subModeRegexp(), EntityAdapter::subMode)); } if (!select.agencyIds().isEmpty()) { - list.add(new IdMatcher("Agency", select.agencyIds(), p -> p.getRoute().getAgency().getId())); + list.add(new IdMatcher("Agency", select.agencyIds(), EntityAdapter::agencyId)); } if (!select.routeIds().isEmpty()) { - list.add(new IdMatcher("Route", select.routeIds(), p -> p.getRoute().getId())); + list.add(new IdMatcher("Route", select.routeIds(), EntityAdapter::routeId)); } return andOf(list); } - static PriorityGroupMatcher[] of(Collection selectors) { + @SuppressWarnings("unchecked") + static Matcher[] of(Collection selectors) { return selectors .stream() - .map(PriorityGroupMatcher::of) - .filter(Predicate.not(PriorityGroupMatcher::isEmpty)) - .toArray(PriorityGroupMatcher[]::new); + .map(Matchers::of) + .filter(Predicate.not(Matcher::isEmpty)) + .toArray(Matcher[]::new); } private static String arrayToString(BinarySetOperator op, T[] values) { @@ -81,9 +69,9 @@ private static String colToString(BinarySetOperator op, Collection values return values.stream().map(Objects::toString).collect(Collectors.joining(" " + op + " ")); } - private static PriorityGroupMatcher andOf(List list) { + private static Matcher andOf(List list) { // Remove empty/noop matchers - list = list.stream().filter(Predicate.not(PriorityGroupMatcher::isEmpty)).toList(); + list = list.stream().filter(Predicate.not(Matcher::isEmpty)).toList(); if (list.isEmpty()) { return NOOP; @@ -94,13 +82,25 @@ private static PriorityGroupMatcher andOf(List list) { return new AndMatcher(list); } - abstract boolean match(TripPattern pattern); + private static final class EmptyMatcher implements Matcher { + + @Override + public boolean match(EntityAdapter entity) { + return false; + } + + @Override + public boolean isEmpty() { + return true; + } - boolean isEmpty() { - return false; + @Override + public String toString() { + return "Empty"; + } } - private static final class ModeMatcher extends PriorityGroupMatcher { + private static final class ModeMatcher implements Matcher { private final Set modes; @@ -109,8 +109,8 @@ public ModeMatcher(List modes) { } @Override - boolean match(TripPattern pattern) { - return modes.contains(pattern.getMode()); + public boolean match(EntityAdapter entity) { + return modes.contains(entity.mode()); } @Override @@ -119,26 +119,26 @@ public String toString() { } } - private static final class RegExpMatcher extends PriorityGroupMatcher { + private static final class RegExpMatcher implements Matcher { private final String typeName; - private final Pattern[] subModeRegexp; - private final Function toValue; + private final Pattern[] patterns; + private final Function toValue; public RegExpMatcher( String typeName, - List subModeRegexp, - Function toValue + List regexps, + Function toValue ) { this.typeName = typeName; - this.subModeRegexp = subModeRegexp.stream().map(Pattern::compile).toArray(Pattern[]::new); + this.patterns = regexps.stream().map(Pattern::compile).toArray(Pattern[]::new); this.toValue = toValue; } @Override - boolean match(TripPattern pattern) { - var value = toValue.apply(pattern); - for (Pattern p : subModeRegexp) { + public boolean match(EntityAdapter entity) { + var value = toValue.apply(entity); + for (Pattern p : patterns) { if (p.matcher(value).matches()) { return true; } @@ -148,20 +148,20 @@ boolean match(TripPattern pattern) { @Override public String toString() { - return typeName + "Regexp(" + arrayToString(OR, subModeRegexp) + ')'; + return typeName + "Regexp(" + arrayToString(OR, patterns) + ')'; } } - private static final class IdMatcher extends PriorityGroupMatcher { + private static final class IdMatcher implements Matcher { private final String typeName; private final Set ids; - private final Function idProvider; + private final Function idProvider; public IdMatcher( String typeName, List ids, - Function idProvider + Function idProvider ) { this.typeName = typeName; this.ids = new HashSet<>(ids); @@ -169,8 +169,8 @@ public IdMatcher( } @Override - boolean match(TripPattern pattern) { - return ids.contains(idProvider.apply(pattern)); + public boolean match(EntityAdapter entity) { + return ids.contains(idProvider.apply(entity)); } @Override @@ -183,18 +183,18 @@ public String toString() { * Takes a list of matchers and provide a single interface. All matchers in the list must match * for the composite matcher to return a match. */ - private static final class AndMatcher extends PriorityGroupMatcher { + private static final class AndMatcher implements Matcher { - private final PriorityGroupMatcher[] matchers; + private final Matcher[] matchers; - public AndMatcher(List matchers) { - this.matchers = matchers.toArray(PriorityGroupMatcher[]::new); + public AndMatcher(List matchers) { + this.matchers = matchers.toArray(Matcher[]::new); } @Override - boolean match(TripPattern pattern) { + public boolean match(EntityAdapter entity) { for (var m : matchers) { - if (!m.match(pattern)) { + if (!m.match(entity)) { return false; } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/grouppriority/TransitGroupPriority32n.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriority32n.java similarity index 51% rename from src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/grouppriority/TransitGroupPriority32n.java rename to src/main/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriority32n.java index feb3f6f7b3a..32423070e09 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/grouppriority/TransitGroupPriority32n.java +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriority32n.java @@ -1,55 +1,31 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.grouppriority; - -import org.opentripplanner.raptor.api.model.DominanceFunction; -import org.opentripplanner.raptor.api.request.RaptorTransitGroupCalculator; +package org.opentripplanner.transit.model.network.grouppriority; /** - * This is a "BitSet" implementation for groupId. It can store upto 32 groups, + * This is a "BitSet" implementation for groupId. It can store up to 31 groups, * a set with few elements does NOT dominate a set with more elements. */ -public class TransitGroupPriority32n { +class TransitGroupPriority32n { private static final int GROUP_ZERO = 0; private static final int MIN_SEQ_NO = 0; private static final int MAX_SEQ_NO = 32; - public static RaptorTransitGroupCalculator priorityCalculator() { - return new RaptorTransitGroupCalculator() { - @Override - public int mergeGroupIds(int currentGroupIds, int boardingGroupId) { - return mergeInGroupId(currentGroupIds, boardingGroupId); - } - - @Override - public DominanceFunction dominanceFunction() { - return TransitGroupPriority32n::dominate; - } - - @Override - public String toString() { - return "TransitGroupPriority32nCalculator{}"; - } - }; - } - /** - * Left dominate right, if right contains a group which does not exist in left. Left - * do NOT dominate right if they are equals or left is a super set of right. + * Left dominates right: + * - if right contains a group which does not exist in the left. + * Left do NOT dominate right: + * - if they are equals or + * - left is a superset of right. */ - public static boolean dominate(int left, int right) { + static boolean dominate(int left, int right) { return ((left ^ right) & right) != 0; } - @Override - public String toString() { - return "TransitGroupPriority32n{}"; - } - /** * Use this method to map from a continuous group index [0..32) to the groupId used - * during routing. The ID is implementation specific and optimized for performance. + * during routing. The ID is implementation-specific and optimized for performance. */ - public static int groupId(final int priorityGroupIndex) { + static int groupId(final int priorityGroupIndex) { assertValidGroupSeqNo(priorityGroupIndex); return priorityGroupIndex == MIN_SEQ_NO ? GROUP_ZERO : 0x01 << (priorityGroupIndex - 1); } @@ -57,10 +33,15 @@ public static int groupId(final int priorityGroupIndex) { /** * Merge a groupId into a set of groupIds. */ - public static int mergeInGroupId(final int currentSetOfGroupIds, final int newGroupId) { + static int mergeInGroupId(final int currentSetOfGroupIds, final int newGroupId) { return currentSetOfGroupIds | newGroupId; } + @Override + public String toString() { + return "TransitGroupPriority32n{}"; + } + private static void assertValidGroupSeqNo(int priorityGroupIndex) { if (priorityGroupIndex < MIN_SEQ_NO) { throw new IllegalArgumentException( diff --git a/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriorityService.java b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriorityService.java new file mode 100644 index 00000000000..048b2279a88 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriorityService.java @@ -0,0 +1,176 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +import gnu.trove.impl.Constants; +import gnu.trove.map.TObjectIntMap; +import gnu.trove.map.hash.TObjectIntHashMap; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; +import org.opentripplanner.framework.lang.ArrayUtils; +import org.opentripplanner.routing.api.request.framework.CostLinearFunction; +import org.opentripplanner.routing.api.request.request.filter.TransitGroupSelect; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.Trip; + +/** + * This class dynamically builds an index of transit-group-ids from the provided + * {@link TransitGroupSelect}s while serving the caller with group-ids for each requested + * trip/pattern. It is made for optimal performance, since it is used in request scope. + *

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

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

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

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

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

- * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix - * - this when doing the issue #3030. * * @param serviceDate service date for the added service id * @return service-id for date if it exist or is created. If the given service date is outside the @@ -540,10 +539,15 @@ public void setHasScheduledService(boolean hasScheduledService) { * The caller is responsible for calling the {@link #index()} method if it is a * possibility that the index is not initialized (during graph build). */ - public @Nullable TransitModelIndex getTransitModelIndex() { + @Nullable + TransitModelIndex getTransitModelIndex() { return index; } + public boolean isIndexed() { + return index != null; + } + public boolean hasFlexTrips() { return !flexTripsById.isEmpty(); } diff --git a/src/main/java/org/opentripplanner/transit/service/TransitModelIndex.java b/src/main/java/org/opentripplanner/transit/service/TransitModelIndex.java index 84759ffae72..36ab937416c 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitModelIndex.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitModelIndex.java @@ -15,7 +15,6 @@ import org.opentripplanner.ext.flex.FlexIndex; import org.opentripplanner.ext.flex.trip.FlexTrip; import org.opentripplanner.framework.application.OTPFeature; -import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.model.calendar.CalendarService; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.GroupOfRoutes; @@ -35,7 +34,7 @@ * For performance reasons these indexes are not part of the serialized state of the graph. * They are rebuilt at runtime after graph deserialization. */ -public class TransitModelIndex { +class TransitModelIndex { private static final Logger LOG = LoggerFactory.getLogger(TransitModelIndex.class); @@ -118,24 +117,20 @@ public class TransitModelIndex { LOG.info("Transit Model index init complete."); } - public Agency getAgencyForId(FeedScopedId id) { + Agency getAgencyForId(FeedScopedId id) { return agencyForId.get(id); } - public Route getRouteForId(FeedScopedId id) { + Route getRouteForId(FeedScopedId id) { return routeForId.get(id); } - /** - * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix - * - this when doing the issue #3030. - */ - public void addRoutes(Route route) { + void addRoutes(Route route) { routeForId.put(route.getId(), route); } /** Dynamically generate the set of Routes passing though a Stop on demand. */ - public Set getRoutesForStop(StopLocation stop) { + Set getRoutesForStop(StopLocation stop) { Set routes = new HashSet<>(); for (TripPattern p : getPatternsForStop(stop)) { routes.add(p.getRoute()); @@ -143,75 +138,57 @@ public Set getRoutesForStop(StopLocation stop) { return routes; } - public Collection getPatternsForStop(StopLocation stop) { + Collection getPatternsForStop(StopLocation stop) { return patternsForStopId.get(stop); } - public Collection getTripsForStop(StopLocation stop) { + Collection getTripsForStop(StopLocation stop) { return getPatternsForStop(stop) .stream() .flatMap(TripPattern::scheduledTripsAsStream) .collect(Collectors.toList()); } - /** - * Returns all the patterns for a specific stop. If timetableSnapshot is included, new patterns - * added by realtime updates are added to the collection. A set is used here because trip patterns - * that were updated by realtime data is both part of the TransitModelIndex and the TimetableSnapshot. - */ - public Collection getPatternsForStop( - StopLocation stop, - TimetableSnapshot timetableSnapshot - ) { - Set tripPatterns = new HashSet<>(getPatternsForStop(stop)); - - if (timetableSnapshot != null) { - tripPatterns.addAll(timetableSnapshot.getPatternsForStop(stop)); - } - - return tripPatterns; - } - /** * Get a list of all operators spanning across all feeds. */ - public Collection getAllOperators() { + Collection getAllOperators() { return getOperatorForId().values(); } - public Map getOperatorForId() { + Map getOperatorForId() { return operatorForId; } - public Map getTripForId() { + Map getTripForId() { return tripForId; } - public Map getTripOnServiceDateById() { + Map getTripOnServiceDateById() { return tripOnServiceDateById; } - public Map getTripOnServiceDateForTripAndDay() { + Map getTripOnServiceDateForTripAndDay() { return tripOnServiceDateForTripAndDay; } - public Collection getAllRoutes() { + Collection getAllRoutes() { return routeForId.values(); } - public Map getPatternForTrip() { + Map getPatternForTrip() { return patternForTrip; } - public Multimap getPatternsForRoute() { + Multimap getPatternsForRoute() { return patternsForRoute; } - public Map getServiceCodesRunningForDate() { + Map getServiceCodesRunningForDate() { return serviceCodesRunningForDate; } - public FlexIndex getFlexIndex() { + FlexIndex getFlexIndex() { return flexIndex; } @@ -252,11 +229,11 @@ private void initalizeServiceCodesForDate(TransitModel transitModel) { } } - public Multimap getRoutesForGroupOfRoutes() { + Multimap getRoutesForGroupOfRoutes() { return routesForGroupOfRoutes; } - public Map getGroupOfRoutesForId() { + Map getGroupOfRoutesForId() { return groupOfRoutesForId; } } diff --git a/src/main/java/org/opentripplanner/transit/service/TransitService.java b/src/main/java/org/opentripplanner/transit/service/TransitService.java index 83b65c44d12..94870643f71 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitService.java @@ -8,8 +8,10 @@ import java.time.ZonedDateTime; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; +import javax.annotation.Nullable; import org.locationtech.jts.geom.Envelope; import org.opentripplanner.ext.flex.FlexIndex; import org.opentripplanner.model.FeedInfo; @@ -25,6 +27,7 @@ import org.opentripplanner.transit.model.basic.Notice; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.AbstractTransitEntity; +import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.GroupOfRoutes; import org.opentripplanner.transit.model.network.Route; @@ -93,8 +96,16 @@ public interface TransitService { Set getRoutesForStop(StopLocation stop); + /** + * Return all the scheduled trip patterns for a specific stop + * (not taking into account real-time updates). + */ Collection getPatternsForStop(StopLocation stop); + /** + * Returns all the patterns for a specific stop. If includeRealtimeUpdates is set, new patterns + * added by realtime updates are added to the collection. + */ Collection getPatternsForStop(StopLocation stop, boolean includeRealtimeUpdates); Collection getTripsForStop(StopLocation stop); @@ -127,8 +138,16 @@ public interface TransitService { Collection getAllRoutes(); + /** + * Return the scheduled trip pattern for a given trip (not taking into account real-time updates) + */ TripPattern getPatternForTrip(Trip trip); + /** + * Return the trip pattern for a given trip on a service date. The real-time updated version + * is returned if it exists, otherwise the scheduled trip pattern is returned. + * + */ TripPattern getPatternForTrip(Trip trip, LocalDate serviceDate); Collection getPatternsForRoute(Route route); @@ -167,6 +186,11 @@ List stopTimesForPatternAtStop( GroupOfRoutes getGroupOfRoutesForId(FeedScopedId id); + /** + * Return the timetable for a given trip pattern and date, taking into account real-time updates. + * If no real-times update are applied, fall back to scheduled data. + */ + @Nullable Timetable getTimetableForTripPattern(TripPattern tripPattern, LocalDate serviceDate); TripPattern getRealtimeAddedTripPattern(FeedScopedId tripId, LocalDate serviceDate); @@ -231,4 +255,10 @@ List stopTimesForPatternAtStop( * So, if more patterns of mode BUS than RAIL visit the stop, the result will be [BUS,RAIL]. */ List getModesOfStopLocation(StopLocation stop); + + Deduplicator getDeduplicator(); + + Set getAllServiceCodes(); + + Map getServiceCodesRunningForDate(); } diff --git a/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java b/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java index 1e9b730ee3a..103120b7ecb 100644 --- a/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java +++ b/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import org.opentripplanner.ext.siri.SiriTimetableSnapshotSource; import org.opentripplanner.ext.siri.updater.SiriETUpdater; import org.opentripplanner.ext.siri.updater.SiriSXUpdater; @@ -20,6 +21,7 @@ import org.opentripplanner.updater.UpdatersParameters; import org.opentripplanner.updater.alert.GtfsRealtimeAlertsUpdater; import org.opentripplanner.updater.spi.GraphUpdater; +import org.opentripplanner.updater.spi.TimetableSnapshotFlush; import org.opentripplanner.updater.trip.MqttGtfsRealtimeUpdater; import org.opentripplanner.updater.trip.PollingTripUpdater; import org.opentripplanner.updater.trip.TimetableSnapshotSource; @@ -94,6 +96,9 @@ private void configure() { ); GraphUpdaterManager updaterManager = new GraphUpdaterManager(graph, transitModel, updaters); + + configureTimetableSnapshotFlush(updaterManager); + updaterManager.startUpdaters(); // Stop the updater manager if it contains nothing @@ -223,4 +228,21 @@ private TimetableSnapshotSource provideGtfsTimetableSnapshot() { } return gtfsTimetableSnapshotSource; } + + /** + * If SIRI or GTFS real-time updaters are in use, configure a periodic flush of the timetable + * snapshot. + */ + private void configureTimetableSnapshotFlush(GraphUpdaterManager updaterManager) { + if (siriTimetableSnapshotSource != null || gtfsTimetableSnapshotSource != null) { + updaterManager + .getScheduler() + .scheduleWithFixedDelay( + new TimetableSnapshotFlush(siriTimetableSnapshotSource, gtfsTimetableSnapshotSource), + 0, + updatersParameters.timetableSnapshotParameters().maxSnapshotFrequency().toSeconds(), + TimeUnit.SECONDS + ); + } + } } diff --git a/src/main/java/org/opentripplanner/updater/spi/TimetableSnapshotFlush.java b/src/main/java/org/opentripplanner/updater/spi/TimetableSnapshotFlush.java new file mode 100644 index 00000000000..3f5c8f4d23d --- /dev/null +++ b/src/main/java/org/opentripplanner/updater/spi/TimetableSnapshotFlush.java @@ -0,0 +1,43 @@ +package org.opentripplanner.updater.spi; + +import org.opentripplanner.ext.siri.SiriTimetableSnapshotSource; +import org.opentripplanner.updater.trip.TimetableSnapshotSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Flush the timetable snapshot buffer by committing pending changes. + * Exceptions occurring during the flush are caught and ignored: the scheduler can then retry + * the task later. + */ +public class TimetableSnapshotFlush implements Runnable { + + private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshotFlush.class); + + private final SiriTimetableSnapshotSource siriTimetableSnapshotSource; + private final TimetableSnapshotSource gtfsTimetableSnapshotSource; + + public TimetableSnapshotFlush( + SiriTimetableSnapshotSource siriTimetableSnapshotSource, + TimetableSnapshotSource gtfsTimetableSnapshotSource + ) { + this.siriTimetableSnapshotSource = siriTimetableSnapshotSource; + this.gtfsTimetableSnapshotSource = gtfsTimetableSnapshotSource; + } + + @Override + public void run() { + try { + LOG.debug("Flushing timetable snapshot buffer"); + if (siriTimetableSnapshotSource != null) { + siriTimetableSnapshotSource.flushBuffer(); + } + if (gtfsTimetableSnapshotSource != null) { + gtfsTimetableSnapshotSource.flushBuffer(); + } + LOG.debug("Flushed timetable snapshot buffer"); + } catch (Throwable t) { + LOG.error("Error flushing timetable snapshot buffer", t); + } + } +} diff --git a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java index bf7c8c98919..b5683a9a62e 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java +++ b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java @@ -2,13 +2,12 @@ import java.time.LocalDate; import java.util.Objects; -import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import javax.annotation.Nullable; -import org.opentripplanner.framework.time.CountdownTimer; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; +import org.opentripplanner.routing.util.ConcurrentPublished; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.framework.Result; import org.opentripplanner.transit.model.network.TripPattern; @@ -30,36 +29,18 @@ public final class TimetableSnapshotManager { private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshotManager.class); private final TransitLayerUpdater transitLayerUpdater; - /** - * Lock to indicate that buffer is in use - */ - private final ReentrantLock bufferLock = new ReentrantLock(true); /** - * The working copy of the timetable snapshot. Should not be visible to routing threads. Should - * only be modified by a thread that holds a lock on {@link #bufferLock}. All public methods that - * might modify this buffer will correctly acquire the lock. By design, only one thread should - * ever be writing to this buffer. - * TODO RT_AB: research and document why this lock is needed since only one thread should ever be - * writing to this buffer. One possible reason may be a need to suspend writes while indexing - * and swapping out the buffer. But the original idea was to make a new copy of the buffer - * before re-indexing it. While refactoring or rewriting parts of this system, we could throw - * an exception if a writing section is entered by more than one thread. + * The working copy of the timetable snapshot. Should not be visible to routing threads. + * By design, only one thread should ever be writing to this buffer. */ private final TimetableSnapshot buffer = new TimetableSnapshot(); /** * The last committed snapshot that was handed off to a routing thread. This snapshot may be given - * to more than one routing thread if the maximum snapshot frequency is exceeded. - */ - private volatile TimetableSnapshot snapshot = null; - - /** - * If a timetable snapshot is requested less than this number of milliseconds after the previous - * snapshot, just return the same one. Throttles the potentially resource-consuming task of - * duplicating a TripPattern -> Timetable map and indexing the new Timetables. + * to more than one routing thread. */ - private final CountdownTimer snapshotFrequencyThrottle; + private final ConcurrentPublished snapshot = new ConcurrentPublished<>(); /** * Should expired real-time data be purged from the graph. @@ -85,7 +66,6 @@ public TimetableSnapshotManager( Supplier localDateNow ) { this.transitLayerUpdater = transitLayerUpdater; - this.snapshotFrequencyThrottle = new CountdownTimer(parameters.maxSnapshotFrequency()); this.purgeExpiredData = parameters.purgeExpiredData(); this.localDateNow = Objects.requireNonNull(localDateNow); // Force commit so that snapshot initializes @@ -99,19 +79,17 @@ public TimetableSnapshotManager( * to the snapshot to release resources. */ public TimetableSnapshot getTimetableSnapshot() { - // Try to get a lock on the buffer - if (bufferLock.tryLock()) { - // Make a new snapshot if necessary - try { - commitTimetableSnapshot(false); - return snapshot; - } finally { - bufferLock.unlock(); - } - } - // No lock could be obtained because there is either a snapshot commit busy or updates - // are applied at this moment, just return the current snapshot - return snapshot; + return snapshot.get(); + } + + /** + * @return the current timetable snapshot buffer that contains pending changes (not yet published + * in a snapshot). + * This should be used in the context of an updater to build a TransitEditorService that sees all + * the changes applied so far by real-time updates. + */ + public TimetableSnapshot getTimetableSnapshotBuffer() { + return buffer; } /** @@ -122,21 +100,12 @@ public TimetableSnapshot getTimetableSnapshot() { * * @param force Force the committing of a new snapshot even if the above conditions are not met. */ - public void commitTimetableSnapshot(final boolean force) { - if (force || snapshotFrequencyThrottle.timeIsUp()) { - if (force || buffer.isDirty()) { - LOG.debug("Committing {}", buffer); - snapshot = buffer.commit(transitLayerUpdater, force); - - // We only reset the timer when the snapshot is updated. This will cause the first - // update to be committed after a silent period. This should not have any effect in - // a busy updater. It is however useful when manually testing the updater. - snapshotFrequencyThrottle.restart(); - } else { - LOG.debug("Buffer was unchanged, keeping old snapshot."); - } + void commitTimetableSnapshot(final boolean force) { + if (force || buffer.isDirty()) { + LOG.debug("Committing {}", buffer); + snapshot.publish(buffer.commit(transitLayerUpdater, force)); } else { - LOG.debug("Snapshot frequency exceeded. Reusing snapshot {}", snapshot); + LOG.debug("Buffer was unchanged, keeping old snapshot."); } } @@ -205,22 +174,6 @@ private boolean purgeExpiredData() { return buffer.purgeExpiredData(previously); } - /** - * Execute a {@code Runnable} with a locked snapshot buffer and release the lock afterwards. While - * the action of locking and unlocking is not complicated to do for calling code, this method - * exists so that the lock instance is a private field. - */ - public void withLock(Runnable action) { - bufferLock.lock(); - - try { - action.run(); - } finally { - // Always release lock - bufferLock.unlock(); - } - } - /** * Clear all data of snapshot for the provided feed id */ diff --git a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java index cc39d82369b..56e3c380b67 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java +++ b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java @@ -1,7 +1,9 @@ package org.opentripplanner.updater.trip; +import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_ARRIVAL_TIME; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_DEPARTURE_TIME; +import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_INPUT_STRUCTURE; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NOT_IMPLEMENTED_DUPLICATED; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NOT_IMPLEMENTED_UNSCHEDULED; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_SERVICE_ON_DATE; @@ -19,6 +21,7 @@ import com.google.common.collect.Multimaps; import com.google.transit.realtime.GtfsRealtime; import com.google.transit.realtime.GtfsRealtime.TripDescriptor; +import com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship; import com.google.transit.realtime.GtfsRealtime.TripUpdate; import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate; import de.mfdz.MfdzRealtimeExtensions; @@ -88,7 +91,13 @@ public class TimetableSnapshotSource implements TimetableSnapshotProvider { private final TripPatternCache tripPatternCache = new TripPatternCache(); private final ZoneId timeZone; - private final TransitEditorService transitService; + + /** + * Long-lived transit editor service that has access to the timetable snapshot buffer. + * This differs from the usual use case where the transit service refers to the latest published + * timetable snapshot. + */ + private final TransitEditorService transitEditorService; private final Deduplicator deduplicator; @@ -116,7 +125,8 @@ public TimetableSnapshotSource( this.snapshotManager = new TimetableSnapshotManager(transitModel.getTransitLayerUpdater(), parameters, localDateNow); this.timeZone = transitModel.getTimeZone(); - this.transitService = new DefaultTransitService(transitModel); + this.transitEditorService = + new DefaultTransitService(transitModel, snapshotManager.getTimetableSnapshotBuffer()); this.deduplicator = transitModel.getDeduplicator(); this.serviceCodes = transitModel.getServiceCodes(); this.localDateNow = localDateNow; @@ -150,122 +160,120 @@ public UpdateResult applyTripUpdates( return UpdateResult.empty(); } - Map failuresByRelationship = new HashMap<>(); + Map failuresByRelationship = new HashMap<>(); List> results = new ArrayList<>(); - snapshotManager.withLock(() -> { - if (updateIncrementality == FULL_DATASET) { - // Remove all updates from the buffer - snapshotManager.clearBuffer(feedId); + if (updateIncrementality == FULL_DATASET) { + // Remove all updates from the buffer + snapshotManager.clearBuffer(feedId); + } + + LOG.debug("message contains {} trip updates", updates.size()); + int uIndex = 0; + for (TripUpdate tripUpdate : updates) { + if (!tripUpdate.hasTrip()) { + debug(feedId, "", "Missing TripDescriptor in gtfs-rt trip update: \n{}", tripUpdate); + continue; } - LOG.debug("message contains {} trip updates", updates.size()); - int uIndex = 0; - for (TripUpdate tripUpdate : updates) { - if (!tripUpdate.hasTrip()) { - debug(feedId, "", "Missing TripDescriptor in gtfs-rt trip update: \n{}", tripUpdate); - continue; - } + if (fuzzyTripMatcher != null) { + final TripDescriptor trip = fuzzyTripMatcher.match(feedId, tripUpdate.getTrip()); + tripUpdate = tripUpdate.toBuilder().setTrip(trip).build(); + } - if (fuzzyTripMatcher != null) { - final TripDescriptor trip = fuzzyTripMatcher.match(feedId, tripUpdate.getTrip()); - tripUpdate = tripUpdate.toBuilder().setTrip(trip).build(); - } + final TripDescriptor tripDescriptor = tripUpdate.getTrip(); - final TripDescriptor tripDescriptor = tripUpdate.getTrip(); + if (!tripDescriptor.hasTripId() || tripDescriptor.getTripId().isBlank()) { + debug(feedId, "", "No trip id found for gtfs-rt trip update: \n{}", tripUpdate); + results.add(Result.failure(UpdateError.noTripId(INVALID_INPUT_STRUCTURE))); + continue; + } + + FeedScopedId tripId = new FeedScopedId(feedId, tripUpdate.getTrip().getTripId()); - if (!tripDescriptor.hasTripId() || tripDescriptor.getTripId().isBlank()) { - debug(feedId, "", "No trip id found for gtfs-rt trip update: \n{}", tripUpdate); + LocalDate serviceDate; + if (tripDescriptor.hasStartDate()) { + try { + serviceDate = ServiceDateUtils.parseString(tripDescriptor.getStartDate()); + } catch (final ParseException e) { + debug( + tripId, + "Failed to parse start date in gtfs-rt trip update: {}", + tripDescriptor.getStartDate() + ); continue; } + } else { + // TODO: figure out the correct service date. For the special case that a trip + // starts for example at 40:00, yesterday would probably be a better guess. + serviceDate = localDateNow.get(); + } + // Determine what kind of trip update this is + var scheduleRelationship = Objects.requireNonNullElse( + tripDescriptor.getScheduleRelationship(), + SCHEDULED + ); + if (updateIncrementality == DIFFERENTIAL) { + purgePatternModifications(scheduleRelationship, tripId, serviceDate); + } - FeedScopedId tripId = new FeedScopedId(feedId, tripUpdate.getTrip().getTripId()); + uIndex += 1; + LOG.debug("trip update #{} ({} updates) :", uIndex, tripUpdate.getStopTimeUpdateCount()); + LOG.trace("{}", tripUpdate); - LocalDate serviceDate; - if (tripDescriptor.hasStartDate()) { - try { - serviceDate = ServiceDateUtils.parseString(tripDescriptor.getStartDate()); - } catch (final ParseException e) { - debug( + Result result; + try { + result = + switch (scheduleRelationship) { + case SCHEDULED -> handleScheduledTrip( + tripUpdate, tripId, - "Failed to parse start date in gtfs-rt trip update: {}", - tripDescriptor.getStartDate() + serviceDate, + backwardsDelayPropagationType ); - continue; - } - } else { - // TODO: figure out the correct service date. For the special case that a trip - // starts for example at 40:00, yesterday would probably be a better guess. - serviceDate = localDateNow.get(); - } - // Determine what kind of trip update this is - final TripDescriptor.ScheduleRelationship tripScheduleRelationship = determineTripScheduleRelationship( - tripDescriptor - ); - if (updateIncrementality == DIFFERENTIAL) { - purgePatternModifications(tripScheduleRelationship, tripId, serviceDate); - } - - uIndex += 1; - LOG.debug("trip update #{} ({} updates) :", uIndex, tripUpdate.getStopTimeUpdateCount()); - LOG.trace("{}", tripUpdate); - - Result result; - try { - result = - switch (tripScheduleRelationship) { - case SCHEDULED -> handleScheduledTrip( - tripUpdate, - tripId, - serviceDate, - backwardsDelayPropagationType - ); - case ADDED -> validateAndHandleAddedTrip( - tripUpdate, - tripDescriptor, - tripId, - serviceDate - ); - case CANCELED -> handleCanceledTrip( - tripId, - serviceDate, - CancelationType.CANCEL, - updateIncrementality - ); - case DELETED -> handleCanceledTrip( - tripId, - serviceDate, - CancelationType.DELETE, - updateIncrementality - ); - case REPLACEMENT -> validateAndHandleModifiedTrip( - tripUpdate, - tripDescriptor, - tripId, - serviceDate - ); - case UNSCHEDULED -> UpdateError.result(tripId, NOT_IMPLEMENTED_UNSCHEDULED); - case DUPLICATED -> UpdateError.result(tripId, NOT_IMPLEMENTED_DUPLICATED); - }; - } catch (DataValidationException e) { - result = DataValidationExceptionMapper.toResult(e); - } + case ADDED -> validateAndHandleAddedTrip( + tripUpdate, + tripDescriptor, + tripId, + serviceDate + ); + case CANCELED -> handleCanceledTrip( + tripId, + serviceDate, + CancelationType.CANCEL, + updateIncrementality + ); + case DELETED -> handleCanceledTrip( + tripId, + serviceDate, + CancelationType.DELETE, + updateIncrementality + ); + case REPLACEMENT -> validateAndHandleModifiedTrip( + tripUpdate, + tripDescriptor, + tripId, + serviceDate + ); + case UNSCHEDULED -> UpdateError.result(tripId, NOT_IMPLEMENTED_UNSCHEDULED); + case DUPLICATED -> UpdateError.result(tripId, NOT_IMPLEMENTED_DUPLICATED); + }; + } catch (DataValidationException e) { + result = DataValidationExceptionMapper.toResult(e); + } - results.add(result); - if (result.isFailure()) { - debug(tripId, "Failed to apply TripUpdate."); - LOG.trace(" Contents: {}", tripUpdate); - if (failuresByRelationship.containsKey(tripScheduleRelationship)) { - var c = failuresByRelationship.get(tripScheduleRelationship); - failuresByRelationship.put(tripScheduleRelationship, ++c); - } else { - failuresByRelationship.put(tripScheduleRelationship, 1); - } + results.add(result); + if (result.isFailure()) { + debug(tripId, "Failed to apply TripUpdate."); + LOG.trace(" Contents: {}", tripUpdate); + if (failuresByRelationship.containsKey(scheduleRelationship)) { + var c = failuresByRelationship.get(scheduleRelationship); + failuresByRelationship.put(scheduleRelationship, ++c); + } else { + failuresByRelationship.put(scheduleRelationship, 1); } } - - snapshotManager.purgeAndCommit(); - }); + } var updateResult = UpdateResult.ofResults(results); @@ -282,7 +290,7 @@ public UpdateResult applyTripUpdates( * added trip pattern. */ private void purgePatternModifications( - TripDescriptor.ScheduleRelationship tripScheduleRelationship, + ScheduleRelationship tripScheduleRelationship, FeedScopedId tripId, LocalDate serviceDate ) { @@ -290,8 +298,8 @@ private void purgePatternModifications( if ( !isPreviouslyAddedTrip(tripId, pattern, serviceDate) || ( - tripScheduleRelationship != TripDescriptor.ScheduleRelationship.CANCELED && - tripScheduleRelationship != TripDescriptor.ScheduleRelationship.DELETED + tripScheduleRelationship != ScheduleRelationship.CANCELED && + tripScheduleRelationship != ScheduleRelationship.DELETED ) ) { // Remove previous realtime updates for this trip. This is necessary to avoid previous @@ -327,7 +335,7 @@ public TimetableSnapshot getTimetableSnapshot() { private static void logUpdateResult( String feedId, - Map failuresByRelationship, + Map failuresByRelationship, UpdateResult updateResult ) { ResultLogger.logUpdateResult(feedId, "gtfs-rt-trip-updates", updateResult); @@ -345,27 +353,6 @@ private static void logUpdateResult( }); } - /** - * Determine how the trip update should be handled. - * - * @param tripDescriptor trip descriptor - * @return TripDescriptor.ScheduleRelationship indicating how the trip update should be handled - */ - private TripDescriptor.ScheduleRelationship determineTripScheduleRelationship( - final TripDescriptor tripDescriptor - ) { - // Assume default value - TripDescriptor.ScheduleRelationship tripScheduleRelationship = - TripDescriptor.ScheduleRelationship.SCHEDULED; - - // If trip update contains schedule relationship, use it - if (tripDescriptor.hasScheduleRelationship()) { - tripScheduleRelationship = tripDescriptor.getScheduleRelationship(); - } - - return tripScheduleRelationship; - } - private Result handleScheduledTrip( TripUpdate tripUpdate, FeedScopedId tripId, @@ -384,8 +371,8 @@ private Result handleScheduledTrip( return UpdateError.result(tripId, NO_UPDATES); } - final FeedScopedId serviceId = transitService.getTripForId(tripId).getServiceId(); - final Set serviceDates = transitService + final FeedScopedId serviceId = transitEditorService.getTripForId(tripId).getServiceId(); + final Set serviceDates = transitEditorService .getCalendarService() .getServiceDatesForServiceId(serviceId); if (!serviceDates.contains(serviceDate)) { @@ -428,7 +415,7 @@ private Result handleScheduledTrip( .cancelStops(skippedStopIndices) .build(); - final Trip trip = transitService.getTripForId(tripId); + final Trip trip = transitEditorService.getTripForId(tripId); // Get cached trip pattern or create one if it doesn't exist yet final TripPattern newPattern = tripPatternCache.getOrCreateTripPattern( newStopPattern, @@ -466,7 +453,7 @@ private Result validateAndHandleAddedTrip( // // Check whether trip id already exists in graph - final Trip trip = transitService.getTripForId(tripId); + final Trip trip = transitEditorService.getTripForId(tripId); if (trip != null) { // TODO: should we support this and add a new instantiation of this trip (making it @@ -520,7 +507,7 @@ private List removeUnknownStops(TripUpdate tripUpdate, FeedScope .filter(StopTimeUpdate::hasStopId) .filter(st -> { var stopId = new FeedScopedId(tripId.getFeedId(), st.getStopId()); - var stopFound = transitService.getRegularStop(stopId) != null; + var stopFound = transitEditorService.getRegularStop(stopId) != null; if (!stopFound) { debug(tripId, "Stop '{}' not found in graph. Removing from ADDED trip.", st.getStopId()); } @@ -569,7 +556,7 @@ private List checkNewStopTimeUpdatesAndFindStops( // Find stops if (stopTimeUpdate.hasStopId()) { // Find stop - final var stop = transitService.getRegularStop( + final var stop = transitEditorService.getRegularStop( new FeedScopedId(tripId.getFeedId(), stopTimeUpdate.getStopId()) ); if (stop != null) { @@ -652,7 +639,7 @@ private Result handleAddedTrip( tripBuilder.withRoute(route); // Find service ID running on this service date - final Set serviceIds = transitService + final Set serviceIds = transitEditorService .getCalendarService() .getServiceIdsOnDate(serviceDate); if (serviceIds.isEmpty()) { @@ -680,7 +667,7 @@ private Result handleAddedTrip( private Route getOrCreateRoute(TripDescriptor tripDescriptor, FeedScopedId tripId) { if (routeExists(tripId.getFeedId(), tripDescriptor)) { // Try to find route - return transitService.getRouteForId( + return transitEditorService.getRouteForId( new FeedScopedId(tripId.getFeedId(), tripDescriptor.getRouteId()) ); } @@ -695,7 +682,7 @@ else if ( var addedRouteExtension = AddedRoute.ofTripDescriptor(tripDescriptor); - var agency = transitService + var agency = transitEditorService .findAgencyById(new FeedScopedId(tripId.getFeedId(), addedRouteExtension.agencyId())) .orElseGet(() -> fallbackAgency(tripId.getFeedId())); @@ -711,7 +698,7 @@ else if ( builder.withUrl(addedRouteExtension.routeUrl()); var route = builder.build(); - transitService.addRoutes(route); + transitEditorService.addRoutes(route); return route; } // no information about the rout is given, so we create a dummy one @@ -727,7 +714,7 @@ else if ( I18NString longName = NonLocalizedString.ofNullable(tripDescriptor.getTripId()); builder.withLongName(longName); var route = builder.build(); - transitService.addRoutes(route); + transitEditorService.addRoutes(route); return route; } } @@ -739,14 +726,14 @@ private Agency fallbackAgency(String feedId) { return Agency .of(new FeedScopedId(feedId, "autogenerated-gtfs-rt-added-route")) .withName("Agency automatically added by GTFS-RT update") - .withTimezone(transitService.getTimeZone().toString()) + .withTimezone(transitEditorService.getTimeZone().toString()) .build(); } private boolean routeExists(String feedId, TripDescriptor tripDescriptor) { if (tripDescriptor.hasRouteId() && StringUtils.hasValue(tripDescriptor.getRouteId())) { var routeId = new FeedScopedId(feedId, tripDescriptor.getRouteId()); - return Objects.nonNull(transitService.getRouteForId(routeId)); + return Objects.nonNull(transitEditorService.getRouteForId(routeId)); } else { return false; } @@ -835,7 +822,7 @@ private Result addTripToGraphAndBuffer( // Create StopPattern final StopPattern stopPattern = new StopPattern(stopTimes); - final TripPattern originalTripPattern = transitService.getPatternForTrip(trip); + final TripPattern originalTripPattern = transitEditorService.getPatternForTrip(trip); // Get cached trip pattern or create one if it doesn't exist yet final TripPattern pattern = tripPatternCache.getOrCreateTripPattern( stopPattern, @@ -981,7 +968,7 @@ private Result validateAndHandleModifiedTrip( // // Check whether trip id already exists in graph - Trip trip = transitService.getTripForId(tripId); + Trip trip = transitEditorService.getTripForId(tripId); if (trip == null) { // TODO: should we support this and consider it an ADDED trip? @@ -996,7 +983,7 @@ private Result validateAndHandleModifiedTrip( return UpdateError.result(tripId, NO_START_DATE); } else { // Check whether service date is served by trip - final Set serviceIds = transitService + final Set serviceIds = transitEditorService .getCalendarService() .getServiceIdsOnDate(serviceDate); if (!serviceIds.contains(trip.getServiceId())) { @@ -1097,8 +1084,8 @@ private Result handleCanceledTrip( * @return trip pattern or null if no trip pattern was found */ private TripPattern getPatternForTripId(FeedScopedId tripId) { - Trip trip = transitService.getTripForId(tripId); - return transitService.getPatternForTrip(trip); + Trip trip = transitEditorService.getTripForId(tripId); + return transitEditorService.getPatternForTrip(trip); } private static void debug(FeedScopedId id, String message, Object... params) { @@ -1114,4 +1101,8 @@ private enum CancelationType { CANCEL, DELETE, } + + public void flushBuffer() { + snapshotManager.purgeAndCommit(); + } } diff --git a/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java b/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java index 745418f5132..f1355ca0fa4 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java @@ -3,13 +3,13 @@ import com.google.transit.realtime.GtfsRealtime.VehiclePosition; import java.time.LocalDate; import java.util.List; -import java.util.Optional; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.updater.GtfsRealtimeFuzzyTripMatcher; import org.opentripplanner.updater.spi.PollingGraphUpdater; import org.opentripplanner.updater.spi.WriteToGraphCallback; @@ -45,18 +45,20 @@ public PollingVehiclePositionUpdater( super(params); this.vehiclePositionSource = new GtfsRealtimeHttpVehiclePositionSource(params.url(), params.headers()); - var index = transitModel.getTransitModelIndex(); + // TODO Inject TransitService, do not create it here. We currently do not + // support dagger injection in updaters, so this is ok for now. + TransitService transitService = new DefaultTransitService(transitModel); var fuzzyTripMatcher = params.fuzzyTripMatching() - ? new GtfsRealtimeFuzzyTripMatcher(new DefaultTransitService(transitModel)) + ? new GtfsRealtimeFuzzyTripMatcher(transitService) : null; this.realtimeVehiclePatternMatcher = new RealtimeVehiclePatternMatcher( params.feedId(), - tripId -> index.getTripForId().get(tripId), - trip -> index.getPatternForTrip().get(trip), + transitService::getTripForId, + transitService::getPatternForTrip, (trip, date) -> getPatternIncludingRealtime(transitModel, trip, date), realtimeVehicleRepository, - transitModel.getTimeZone(), + transitService.getTimeZone(), fuzzyTripMatcher, params.vehiclePositionFeatures() ); @@ -99,9 +101,8 @@ private static TripPattern getPatternIncludingRealtime( Trip trip, LocalDate sd ) { - return Optional - .ofNullable(transitModel.getTimetableSnapshot()) - .map(snapshot -> snapshot.getRealtimeAddedTripPattern(trip.getId(), sd)) - .orElseGet(() -> transitModel.getTransitModelIndex().getPatternForTrip().get(trip)); + // a new instance of DefaultTransitService must be created to retrieve + // the current TimetableSnapshot + return (new DefaultTransitService(transitModel)).getPatternForTrip(trip, sd); } } diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 6e1195f5901..927af19f8b1 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -1620,9 +1620,17 @@ type QueryType { "Only return routes with these feedIds" feeds: [String], "Only return routes with these ids" - ids: [String], + ids: [String] @deprecated(reason : "Since it is hard to reason about the ID filter being combined with others in this resolver, it will be moved to a separate one."), "Query routes by this name" name: String, + """ + Only include routes whose pattern operates 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, "Only include routes, which use one of these modes" transportModes: [Mode] ): [Route] @@ -1847,7 +1855,16 @@ type Route implements Node { "Transport mode of this route, e.g. `BUS`" mode: TransitMode "List of patterns which operate on this route" - patterns: [Pattern] + patterns( + """ + Filter patterns by the service dates they operate on. + + **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 + ): [Pattern] "Short name of the route, usually a line number, e.g. 550" shortName: String """ @@ -3517,6 +3534,13 @@ scalar GeoJson @specifiedBy(url : "https://www.rfcreader.com/#rfc7946") scalar Grams +""" +An ISO-8601-formatted local date, i.e. `2024-05-24` for the 24th of May, 2024. + +ISO-8601 allows many different date formats, however only the most common one - `yyyy-MM-dd` - is accepted. +""" +scalar LocalDate @specifiedBy(url : "https://www.iso.org/standard/70907.html") + "A IETF BCP 47 language tag" scalar Locale @specifiedBy(url : "https://www.rfcreader.com/#rfc5646") @@ -3857,6 +3881,23 @@ input InputUnpreferred { useUnpreferredRoutesPenalty: Int @deprecated(reason : "Use unpreferredCost instead") } +"Filters an entity by a date range." +input LocalDateRangeInput { + """ + **Exclusive** end date of the filter. This means that if you want a time window from Sunday to + Sunday, `end` must be on Monday. + + If `null` this means that no end filter is applied and all entities that are after or on `start` + are selected. + """ + end: LocalDate + """ + **Inclusive** start date of the filter. If `null` this means that no `start` filter is applied and all + dates that are before `end` are selected. + """ + start: LocalDate +} + """ The filter definition to include or exclude parking facilities used during routing. diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index 64440780f78..0d2bf71dcc6 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -775,7 +775,7 @@ type QueryType { "Input type for executing a travel search for a trip between two locations. Returns trip patterns describing suggested alternatives for the trip." trip( "Time and cost penalty on access/egress modes." - accessEgressPenalty: [PenaltyForStreetMode!] = [{streetMode : car_park, timePenalty : "20m + 2.0 t", costFactor : 1.5}, {streetMode : flexible, timePenalty : "10m + 1.30 t", costFactor : 1.3}], + accessEgressPenalty: [PenaltyForStreetMode!] = [{streetMode : car_park, timePenalty : "20m + 2.0 t", costFactor : 1.5}, {streetMode : car_rental, timePenalty : "20m + 2.0 t", costFactor : 1.5}, {streetMode : flexible, timePenalty : "10m + 1.30 t", costFactor : 1.3}], "The alightSlack is the minimum extra time after exiting a public transport vehicle. This is the default value used, if not overridden by the 'alightSlackList'." alightSlackDefault: Int = 0, "List of alightSlack for a given set of modes. Defaults: []" @@ -1773,6 +1773,8 @@ enum StreetMode { car_park "Walk to a pickup point along the road, drive to a drop-off point along the road, and walk the rest of the way. This can include various taxi-services or kiss & ride." car_pickup + "Walk to a car rental point along the road, drive to a drop-off point along the road, and walk the rest of the way. This can include car rentals at fixed locations or free-floating services." + car_rental "Walk to an eligible pickup area for flexible transportation, ride to an eligible drop-off area and then walk the rest of the way." flexible "Walk only" diff --git a/src/test/java/org/opentripplanner/GtfsTest.java b/src/test/java/org/opentripplanner/GtfsTest.java index baf12225c9a..074e31147a3 100644 --- a/src/test/java/org/opentripplanner/GtfsTest.java +++ b/src/test/java/org/opentripplanner/GtfsTest.java @@ -233,6 +233,7 @@ protected void setUp() throws Exception { updates, feedId.getId() ); + timetableSnapshotSource.flushBuffer(); alertsUpdateHandler.update(feedMessage); } catch (Exception exception) {} } diff --git a/src/test/java/org/opentripplanner/TestServerContext.java b/src/test/java/org/opentripplanner/TestServerContext.java index 2f4ded1121d..90dca6ff840 100644 --- a/src/test/java/org/opentripplanner/TestServerContext.java +++ b/src/test/java/org/opentripplanner/TestServerContext.java @@ -55,6 +55,7 @@ public static OtpServerRequestContext createServerContext( List.of(), null, createStreetLimitationParametersService(), + null, null ); creatTransitLayerForRaptor(transitModel, routerConfig.transitTuningConfig()); diff --git a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index 2fb7a8d9db2..79590ca2775 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -93,6 +93,7 @@ import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.opentripplanner.transit.service.DefaultTransitService; +import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.transit.service.TransitModel; import org.opentripplanner.transit.service.TransitService; @@ -196,8 +197,20 @@ static void setup() { .toList(); var busRoute = routes.stream().filter(r -> r.getMode().equals(BUS)).findFirst().get(); + TransitEditorService transitService = new DefaultTransitService(transitModel) { + private final TransitAlertService alertService = new TransitAlertServiceImpl(transitModel); + + @Override + public List getModesOfStopLocation(StopLocation stop) { + return List.of(BUS, FERRY); + } - routes.forEach(route -> transitModel.getTransitModelIndex().addRoutes(route)); + @Override + public TransitAlertService getTransitAlertService() { + return alertService; + } + }; + routes.forEach(transitService::addRoutes); var step1 = walkStep("street") .withRelativeDirection(RelativeDirection.DEPART) @@ -254,20 +267,6 @@ static void setup() { var emissions = new Emissions(new Grams(123.0)); i1.setEmissionsPerPerson(emissions); - var transitService = new DefaultTransitService(transitModel) { - private final TransitAlertService alertService = new TransitAlertServiceImpl(transitModel); - - @Override - public List getModesOfStopLocation(StopLocation stop) { - return List.of(BUS, FERRY); - } - - @Override - public TransitAlertService getTransitAlertService() { - return alertService; - } - }; - var alerts = ListUtils.combine(List.of(alert), getTransitAlert(entitySelector)); transitService.getTransitAlertService().setAlerts(alerts); diff --git a/src/test/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilterTest.java b/src/test/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilterTest.java new file mode 100644 index 00000000000..f01bac12006 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilterTest.java @@ -0,0 +1,159 @@ +package org.opentripplanner.apis.gtfs; + +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 java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.apis.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.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(); + + 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), + Arguments.of(parse("2024-05-02"), parse("2024-05-01")), + Arguments.of(parse("2024-05-03"), parse("2024-05-01")) + ); + } + + @ParameterizedTest + @MethodSource("invalidRangeCases") + void invalidRange(LocalDate start, LocalDate end) { + assertThrows( + IllegalArgumentException.class, + () -> + new PatternByServiceDatesFilter( + new LocalDateRange(start, end), + r -> List.of(), + d -> List.of() + ) + ); + } + + static List validRangeCases() { + return List.of( + Arguments.of(parse("2024-05-02"), parse("2024-05-02")), + Arguments.of(parse("2024-05-02"), parse("2024-05-03")), + Arguments.of(null, parse("2024-05-03")), + Arguments.of(parse("2024-05-03"), null) + ); + } + + @ParameterizedTest + @MethodSource("validRangeCases") + void validRange(LocalDate start, LocalDate end) { + assertDoesNotThrow(() -> + new PatternByServiceDatesFilter( + new LocalDateRange(start, end), + r -> List.of(), + d -> List.of() + ) + ); + } + + static List ranges() { + return List.of( + Arguments.of(null, parse("2024-05-03"), NOT_REMOVED), + Arguments.of(parse("2024-05-03"), null, NOT_REMOVED), + Arguments.of(parse("2024-05-01"), null, NOT_REMOVED), + Arguments.of(null, parse("2024-04-30"), REMOVED), + Arguments.of(null, parse("2024-05-01"), REMOVED), + Arguments.of(parse("2024-05-02"), parse("2024-05-02"), REMOVED), + Arguments.of(parse("2024-05-02"), parse("2024-05-03"), REMOVED), + Arguments.of(parse("2024-05-02"), parse("2024-06-01"), REMOVED), + Arguments.of(parse("2025-01-01"), null, REMOVED), + Arguments.of(parse("2025-01-01"), parse("2025-01-02"), REMOVED), + Arguments.of(null, parse("2023-12-31"), REMOVED), + Arguments.of(parse("2023-12-31"), parse("2024-04-30"), REMOVED) + ); + } + + @ParameterizedTest + @MethodSource("ranges") + void filterPatterns(LocalDate start, LocalDate end, FilterExpectation expectation) { + var filter = defaultFilter(start, end); + + var filterInput = List.of(PATTERN_1); + var filterOutput = filter.filterPatterns(filterInput); + + if (expectation == NOT_REMOVED) { + assertEquals(filterOutput, filterInput); + } else { + assertEquals(List.of(), filterOutput); + } + } + + @ParameterizedTest + @MethodSource("ranges") + 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()); + + if (expectation == NOT_REMOVED) { + assertEquals(filterOutput, filterInput); + } else { + assertEquals(List.of(), filterOutput); + } + } + + private static PatternByServiceDatesFilter defaultFilter(LocalDate start, LocalDate end) { + return new PatternByServiceDatesFilter( + new LocalDateRange(start, end), + route -> List.of(PATTERN_1), + trip -> List.of(parse("2024-05-01"), parse("2024-06-01")) + ); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/mapping/LocalDateRangeMapperTest.java b/src/test/java/org/opentripplanner/apis/gtfs/mapping/LocalDateRangeMapperTest.java new file mode 100644 index 00000000000..cbd771df933 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/mapping/LocalDateRangeMapperTest.java @@ -0,0 +1,42 @@ +package org.opentripplanner.apis.gtfs.mapping; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.apis.gtfs.support.time.LocalDateRangeUtil; + +class LocalDateRangeMapperTest { + + private static final LocalDate DATE = LocalDate.parse("2024-05-27"); + + private static List noFilterCases() { + var list = new ArrayList(); + list.add(null); + list.add(new GraphQLTypes.GraphQLLocalDateRangeInput(Map.of())); + return list; + } + + @ParameterizedTest + @MethodSource("noFilterCases") + void hasNoServiceDateFilter(GraphQLTypes.GraphQLLocalDateRangeInput input) { + assertFalse(LocalDateRangeUtil.hasServiceDateFilter(input)); + } + + private static List> hasFilterCases() { + return List.of(Map.of("start", DATE), Map.of("end", DATE), Map.of("start", DATE, "end", DATE)); + } + + @ParameterizedTest + @MethodSource("hasFilterCases") + void hasServiceDateFilter(Map params) { + var input = new GraphQLTypes.GraphQLLocalDateRangeInput(params); + assertTrue(LocalDateRangeUtil.hasServiceDateFilter(input)); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/model/LocalDateRangeTest.java b/src/test/java/org/opentripplanner/apis/gtfs/model/LocalDateRangeTest.java new file mode 100644 index 00000000000..5d079f47459 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/model/LocalDateRangeTest.java @@ -0,0 +1,24 @@ +package org.opentripplanner.apis.gtfs.model; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDate; +import org.junit.jupiter.api.Test; + +class LocalDateRangeTest { + + private static final LocalDate DATE = LocalDate.parse("2024-06-01"); + + @Test + void limited() { + assertFalse(new LocalDateRange(DATE, DATE).unlimited()); + assertFalse(new LocalDateRange(DATE, null).unlimited()); + assertFalse(new LocalDateRange(null, DATE).unlimited()); + } + + @Test + void unlimited() { + assertTrue(new LocalDateRange(null, null).unlimited()); + } +} diff --git a/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java b/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java index 42e4607a8b6..ab7e2b62b7c 100644 --- a/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java +++ b/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java @@ -145,6 +145,7 @@ void setup() { List.of(), null, new DefaultStreetLimitationParametersService(new StreetLimitationParameters()), + null, null ), null, diff --git a/src/test/java/org/opentripplanner/datastore/file/ZipFileDataSourceTest.java b/src/test/java/org/opentripplanner/datastore/file/ZipFileDataSourceTest.java index da46fc430b6..b5b8f797eb3 100644 --- a/src/test/java/org/opentripplanner/datastore/file/ZipFileDataSourceTest.java +++ b/src/test/java/org/opentripplanner/datastore/file/ZipFileDataSourceTest.java @@ -74,7 +74,6 @@ public void testIO() throws IOException { Collection content = subject.content(); Collection names = content.stream().map(DataSource::name).toList(); - //System.out.println(names); assertTrue( names.containsAll(List.of("agency.txt", "stops.txt", "trips.txt")), names.toString() diff --git a/src/test/java/org/opentripplanner/datastore/file/ZipStreamDataSourceDecoratorTest.java b/src/test/java/org/opentripplanner/datastore/file/ZipStreamDataSourceDecoratorTest.java index 09479d85478..21d4bc6bed0 100644 --- a/src/test/java/org/opentripplanner/datastore/file/ZipStreamDataSourceDecoratorTest.java +++ b/src/test/java/org/opentripplanner/datastore/file/ZipStreamDataSourceDecoratorTest.java @@ -100,7 +100,6 @@ void testIO() throws IOException { Collection content = subject.content(); Collection names = content.stream().map(DataSource::name).toList(); - System.out.println(names); assertTrue(names.containsAll(EXPECTED_ZIP_ENTRIES), names.toString()); DataSource entry = subject.entry("agency.txt"); diff --git a/src/test/java/org/opentripplanner/framework/graphql/scalar/DateScalarFactoryTest.java b/src/test/java/org/opentripplanner/framework/graphql/scalar/DateScalarFactoryTest.java new file mode 100644 index 00000000000..db95f0ba8ce --- /dev/null +++ b/src/test/java/org/opentripplanner/framework/graphql/scalar/DateScalarFactoryTest.java @@ -0,0 +1,54 @@ +package org.opentripplanner.framework.graphql.scalar; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import graphql.schema.CoercingParseValueException; +import graphql.schema.GraphQLScalarType; +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class DateScalarFactoryTest { + + private static final GraphQLScalarType GTFS_SCALAR = DateScalarFactory.createGtfsDateScalar(); + private static final GraphQLScalarType TRANSMODEL_SCALAR = DateScalarFactory.createTransmodelDateScalar(); + private static final List INVALID_DATES = List.of( + "2024-05", + "2024", + "2024-99-04", + "202405-23", + "20240523" + ); + + static Stream succesfulCases() { + return Stream.of(GTFS_SCALAR, TRANSMODEL_SCALAR).map(s -> Arguments.of(s, "2024-05-23")); + } + + @ParameterizedTest + @MethodSource("succesfulCases") + void parse(GraphQLScalarType scalar, String input) { + var result = scalar.getCoercing().parseValue(input); + assertInstanceOf(LocalDate.class, result); + var date = (LocalDate) result; + assertEquals(LocalDate.of(2024, 5, 23), date); + } + + static Stream invalidCases() { + return INVALID_DATES + .stream() + .flatMap(date -> + Stream.of(Arguments.of(GTFS_SCALAR, date), Arguments.of(TRANSMODEL_SCALAR, date)) + ); + } + + @ParameterizedTest + @MethodSource("invalidCases") + void failParsing(GraphQLScalarType scalar, String input) { + assertThrows(CoercingParseValueException.class, () -> scalar.getCoercing().parseValue(input)); + } +} diff --git a/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java b/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java index 67ca61f0403..a1aa4f9d753 100644 --- a/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java +++ b/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java @@ -125,9 +125,8 @@ void testInterline( void staySeatedNotAllowed() { var transferService = new DefaultTransferService(); - var fromTrip = patterns.get(0).getTrip(0); - var toTrip = patterns.get(1).getTrip(0); - + var fromTrip = patterns.get(0).getScheduledTimetable().getTripTimes().get(0).getTrip(); + var toTrip = patterns.get(1).getScheduledTimetable().getTripTimes().get(0).getTrip(); var notAllowed = new StaySeatedNotAllowed(fromTrip, toTrip); var calendarService = new CalendarServiceData(); diff --git a/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java b/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java index 28be5b3a7e2..edaafabd753 100644 --- a/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java +++ b/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java @@ -55,6 +55,8 @@ */ public class TestItineraryBuilder implements PlanTestConstants { + private static final int NOT_SET = -1_999_999; + public static final LocalDate SERVICE_DAY = LocalDate.of(2020, Month.FEBRUARY, 2); public static final Route BUS_ROUTE = route("1").withMode(TransitMode.BUS).build(); public static final Route RAIL_ROUTE = route("2").withMode(TransitMode.RAIL).build(); @@ -69,7 +71,8 @@ public class TestItineraryBuilder implements PlanTestConstants { private final List legs = new ArrayList<>(); private Place lastPlace; private int lastEndTime; - private int cost = 0; + private int c1 = 0; + private int c2 = NOT_SET; private TestItineraryBuilder(Place origin, int startTime) { this.lastPlace = origin; @@ -241,7 +244,7 @@ public TestItineraryBuilder flex(int start, int end, Place to) { FlexibleTransitLeg leg = new FlexibleTransitLeg(edge, newTime(start), newTime(end), legCost); legs.add(leg); - cost += legCost; + c1 += legCost; // Setup for adding another leg lastEndTime = end; @@ -330,17 +333,6 @@ public TestItineraryBuilder faresV2Rail( ); } - public Itinerary egress(int walkDuration) { - walk(walkDuration, null); - return build(); - } - - public Itinerary build() { - Itinerary itinerary = new Itinerary(legs); - itinerary.setGeneralizedCost(cost); - return itinerary; - } - public TestItineraryBuilder frequencyBus(int tripId, int startTime, int endTime, Place to) { return transit( RAIL_ROUTE, @@ -401,6 +393,34 @@ public TestItineraryBuilder carHail(int duration, Place to) { return this; } + public TestItineraryBuilder withGeneralizedCost2(int c2) { + this.c2 = c2; + return this; + } + + public Itinerary egress(int walkDuration) { + walk(walkDuration, null); + return build(); + } + + /** + * Override any value set for c1. The given value will be assigned to the itinerary + * independent of any values set on the legs. + */ + public Itinerary build(int c1) { + this.c1 = c1; + return build(); + } + + public Itinerary build() { + Itinerary itinerary = new Itinerary(legs); + itinerary.setGeneralizedCost(c1); + if (c2 != NOT_SET) { + itinerary.setGeneralizedCost2(c2); + } + return itinerary; + } + /* private methods */ /** Create a dummy trip */ @@ -506,7 +526,7 @@ public TestItineraryBuilder transit( leg.setDistanceMeters(speed(leg.getMode()) * (end - start)); legs.add(leg); - cost += legCost; + c1 += legCost; // Setup for adding another leg lastEndTime = end; @@ -536,7 +556,7 @@ private StreetLeg streetLeg( .build(); legs.add(leg); - cost += legCost; + c1 += legCost; // Setup for adding another leg lastEndTime = endTime; diff --git a/src/test/java/org/opentripplanner/netex/config/NetexFeedParametersTest.java b/src/test/java/org/opentripplanner/netex/config/NetexFeedParametersTest.java index d010e3a0d9e..93624afe339 100644 --- a/src/test/java/org/opentripplanner/netex/config/NetexFeedParametersTest.java +++ b/src/test/java/org/opentripplanner/netex/config/NetexFeedParametersTest.java @@ -89,7 +89,7 @@ void testCopyOfEqualsAndHashCode() { @Test void testToString() { - assertEquals("NetexFeedParameters{}", DEFAULT.toString()); + assertEquals("NetexFeedParameters{ignoredFeatures: [PARKING]}", DEFAULT.toString()); assertEquals( "NetexFeedParameters{" + "source: https://my.test.com, " + @@ -98,6 +98,7 @@ void testToString() { "sharedGroupFilePattern: '[sharedGoupFil]+', " + "groupFilePattern: '[groupFile]+', " + "ignoreFilePattern: '[ignoreFl]+', " + + "ignoredFeatures: [PARKING], " + "ferryIdsNotAllowedForBicycle: [Ferry:Id]" + "}", subject.toString() diff --git a/src/test/java/org/opentripplanner/netex/loader/parser/SiteFrameParserTest.java b/src/test/java/org/opentripplanner/netex/loader/parser/SiteFrameParserTest.java index efd9365b84d..bc60cff2d0e 100644 --- a/src/test/java/org/opentripplanner/netex/loader/parser/SiteFrameParserTest.java +++ b/src/test/java/org/opentripplanner/netex/loader/parser/SiteFrameParserTest.java @@ -6,6 +6,7 @@ import jakarta.xml.bind.JAXBElement; import java.util.Collection; +import java.util.Set; import org.junit.jupiter.api.Test; import org.opentripplanner.netex.NetexTestDataSupport; import org.opentripplanner.netex.index.NetexEntityIndex; @@ -21,7 +22,7 @@ class SiteFrameParserTest { @Test void testParseQuays() { - SiteFrameParser siteFrameParser = new SiteFrameParser(); + SiteFrameParser siteFrameParser = new SiteFrameParser(Set.of()); SiteFrame siteFrame = OBJECT_FACTORY.createSiteFrame(); NetexEntityIndex netexEntityIndex = new NetexEntityIndex(); diff --git a/src/test/java/org/opentripplanner/netex/mapping/MultiModalStationMapperTest.java b/src/test/java/org/opentripplanner/netex/mapping/MultiModalStationMapperTest.java new file mode 100644 index 00000000000..218942d29ef --- /dev/null +++ b/src/test/java/org/opentripplanner/netex/mapping/MultiModalStationMapperTest.java @@ -0,0 +1,33 @@ +package org.opentripplanner.netex.mapping; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.graph_builder.issue.service.DefaultDataImportIssueStore; +import org.opentripplanner.netex.NetexTestDataSupport; +import org.opentripplanner.netex.mapping.support.FeedScopedIdFactory; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.rutebanken.netex.model.StopPlace; + +class MultiModalStationMapperTest { + + @Test + void testMissingCoordinates() { + DataImportIssueStore dataIssueStore = new DefaultDataImportIssueStore(); + FeedScopedIdFactory feedScopeIdFactory = new FeedScopedIdFactory(TransitModelForTest.FEED_ID); + MultiModalStationMapper multiModalStationMapper = new MultiModalStationMapper( + dataIssueStore, + feedScopeIdFactory + ); + StopPlace stopPlace = new StopPlace(); + stopPlace.setId(NetexTestDataSupport.STOP_PLACE_ID); + assertNull(multiModalStationMapper.map(stopPlace, List.of())); + assertEquals(1, dataIssueStore.listIssues().size()); + assertEquals( + "MultiModalStationWithoutCoordinates", + dataIssueStore.listIssues().getFirst().getType() + ); + } +} diff --git a/src/test/java/org/opentripplanner/netex/mapping/VehicleParkingMapperTest.java b/src/test/java/org/opentripplanner/netex/mapping/VehicleParkingMapperTest.java new file mode 100644 index 00000000000..bf56be1be1b --- /dev/null +++ b/src/test/java/org/opentripplanner/netex/mapping/VehicleParkingMapperTest.java @@ -0,0 +1,108 @@ +package org.opentripplanner.netex.mapping; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.rutebanken.netex.model.ParkingVehicleEnumeration.AGRICULTURAL_VEHICLE; +import static org.rutebanken.netex.model.ParkingVehicleEnumeration.ALL_PASSENGER_VEHICLES; +import static org.rutebanken.netex.model.ParkingVehicleEnumeration.CAR; +import static org.rutebanken.netex.model.ParkingVehicleEnumeration.CAR_WITH_CARAVAN; +import static org.rutebanken.netex.model.ParkingVehicleEnumeration.CYCLE; +import static org.rutebanken.netex.model.ParkingVehicleEnumeration.PEDAL_CYCLE; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.framework.geometry.WgsCoordinate; +import org.opentripplanner.graph_builder.issue.api.DataImportIssue; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.graph_builder.issue.service.DefaultDataImportIssueStore; +import org.opentripplanner.netex.mapping.support.FeedScopedIdFactory; +import org.opentripplanner.routing.vehicle_parking.VehicleParking; +import org.opentripplanner.routing.vehicle_parking.VehicleParkingSpaces; +import org.rutebanken.netex.model.LocationStructure; +import org.rutebanken.netex.model.MultilingualString; +import org.rutebanken.netex.model.Parking; +import org.rutebanken.netex.model.ParkingVehicleEnumeration; +import org.rutebanken.netex.model.SimplePoint_VersionStructure; + +class VehicleParkingMapperTest { + + public static List> carCases() { + return List.of(Set.of(), Set.of(CAR, AGRICULTURAL_VEHICLE, ALL_PASSENGER_VEHICLES)); + } + + @ParameterizedTest + @MethodSource("carCases") + void mapCarLot(Set vehicleTypes) { + var vp = mapper().map(parking(vehicleTypes)); + assertCommonProperties(vp); + assertTrue(vp.hasAnyCarPlaces()); + assertEquals(VehicleParkingSpaces.builder().carSpaces(10).build(), vp.getCapacity()); + } + + public static List> bicycleCases() { + return List.of(Set.of(CYCLE), Set.of(PEDAL_CYCLE, CAR, CAR_WITH_CARAVAN)); + } + + @ParameterizedTest + @MethodSource("bicycleCases") + void mapBicycleLot(Set vehicleTypes) { + var vp = mapper().map(parking(vehicleTypes)); + assertCommonProperties(vp); + assertTrue(vp.hasBicyclePlaces()); + assertEquals(VehicleParkingSpaces.builder().bicycleSpaces(10).build(), vp.getCapacity()); + } + + @Test + void dropEmptyCapacity() { + var parking = parking(Set.of(CAR)); + parking.setTotalCapacity(null); + var issueStore = new DefaultDataImportIssueStore(); + var vp = mapper(issueStore).map(parking); + assertNull(vp); + assertEquals( + List.of("MissingParkingCapacity"), + issueStore.listIssues().stream().map(DataImportIssue::getType).toList() + ); + } + + private VehicleParkingMapper mapper() { + return mapper(DataImportIssueStore.NOOP); + } + + private static VehicleParkingMapper mapper(DataImportIssueStore issueStore) { + return new VehicleParkingMapper(new FeedScopedIdFactory("parking"), issueStore); + } + + private static void assertCommonProperties(VehicleParking vp) { + assertEquals("A name", vp.getName().toString()); + assertEquals(new WgsCoordinate(10, 20), vp.getCoordinate()); + assertEquals( + "[VehicleParkingEntrance{entranceId: parking:LOT1/entrance, coordinate: (10.0, 20.0), carAccessible: true, walkAccessible: true}]", + vp.getEntrances().toString() + ); + } + + private static Parking parking(Set vehicleTypes) { + var name = new MultilingualString(); + name.setValue("A name"); + var point = new SimplePoint_VersionStructure(); + var loc = new LocationStructure(); + loc.setLatitude(new BigDecimal(10)); + loc.setLongitude(new BigDecimal(20)); + point.setLocation(loc); + + var parking = new Parking(); + parking.setId("LOT1"); + parking.setName(name); + parking.setCentroid(point); + parking.setTotalCapacity(BigInteger.TEN); + parking.getParkingVehicleTypes().addAll(vehicleTypes); + return parking; + } +} diff --git a/src/test/java/org/opentripplanner/netex/support/NetexVersionHelperTest.java b/src/test/java/org/opentripplanner/netex/support/NetexVersionHelperTest.java index d55b573a5d5..1374d871154 100644 --- a/src/test/java/org/opentripplanner/netex/support/NetexVersionHelperTest.java +++ b/src/test/java/org/opentripplanner/netex/support/NetexVersionHelperTest.java @@ -19,32 +19,39 @@ import org.rutebanken.netex.model.EntityInVersionStructure; import org.rutebanken.netex.model.ValidBetween; -public class NetexVersionHelperTest { +class NetexVersionHelperTest { private static final EntityInVersionStructure E_VER_1 = new EntityInVersionStructure() .withVersion("1"); private static final EntityInVersionStructure E_VER_2 = new EntityInVersionStructure() .withVersion("2"); + private static final EntityInVersionStructure E_VER_ANY = new EntityInVersionStructure() + .withVersion("any"); @Test - public void versionOfTest() { + void versionOfTest() { assertEquals(1, versionOf(E_VER_1)); } @Test - public void latestVersionInTest() { + void any() { + assertEquals(-1, versionOf(E_VER_ANY)); + } + + @Test + void latestVersionInTest() { assertEquals(2, latestVersionIn(Arrays.asList(E_VER_1, E_VER_2))); assertEquals(-1, latestVersionIn(Collections.emptyList())); } @Test - public void lastestVersionedElementInTest() { + void lastestVersionedElementInTest() { assertEquals(E_VER_2, latestVersionedElementIn(Arrays.asList(E_VER_1, E_VER_2))); assertNull(latestVersionedElementIn(Collections.emptyList())); } @Test - public void comparingVersionTest() { + void comparingVersionTest() { // Given a comparator (subject under test) Comparator subject = comparingVersion(); // And a entity with version as the E_VER_1 entity @@ -62,7 +69,7 @@ public void comparingVersionTest() { } @Test - public void testFirstRelevantDateTime() { + void testFirstRelevantDateTime() { var may1st = LocalDateTime.of(2021, MAY, 1, 14, 0); var may2nd = LocalDateTime.of(2021, MAY, 2, 14, 0); var may3rd = LocalDateTime.of(2021, MAY, 3, 14, 0); diff --git a/src/test/java/org/opentripplanner/raptor/moduletests/K01_TransitPriorityTest.java b/src/test/java/org/opentripplanner/raptor/moduletests/K01_TransitPriorityTest.java index 00b1d528e7e..f03eb5335a0 100644 --- a/src/test/java/org/opentripplanner/raptor/moduletests/K01_TransitPriorityTest.java +++ b/src/test/java/org/opentripplanner/raptor/moduletests/K01_TransitPriorityTest.java @@ -22,7 +22,7 @@ import org.opentripplanner.raptor._data.transit.TestTripSchedule; import org.opentripplanner.raptor.api.request.RaptorProfile; import org.opentripplanner.raptor.api.request.RaptorRequestBuilder; -import org.opentripplanner.raptor.api.request.RaptorTransitGroupCalculator; +import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.raptor.moduletests.support.TestGroupPriorityCalculator; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter; @@ -34,7 +34,7 @@ */ public class K01_TransitPriorityTest { - private static final RaptorTransitGroupCalculator PRIORITY_GROUP_CALCULATOR = + private static final RaptorTransitGroupPriorityCalculator PRIORITY_GROUP_CALCULATOR = TestGroupPriorityCalculator.PRIORITY_CALCULATOR; private static final int C1_SLACK_90s = RaptorCostConverter.toRaptorCost(90); diff --git a/src/test/java/org/opentripplanner/raptor/moduletests/K02_TransitPriorityDestinationTest.java b/src/test/java/org/opentripplanner/raptor/moduletests/K02_TransitPriorityDestinationTest.java index c6ab8e337ea..fb21128728c 100644 --- a/src/test/java/org/opentripplanner/raptor/moduletests/K02_TransitPriorityDestinationTest.java +++ b/src/test/java/org/opentripplanner/raptor/moduletests/K02_TransitPriorityDestinationTest.java @@ -25,7 +25,7 @@ import org.opentripplanner.raptor._data.transit.TestTripSchedule; import org.opentripplanner.raptor.api.request.RaptorProfile; import org.opentripplanner.raptor.api.request.RaptorRequestBuilder; -import org.opentripplanner.raptor.api.request.RaptorTransitGroupCalculator; +import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.raptor.moduletests.support.TestGroupPriorityCalculator; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter; @@ -37,7 +37,7 @@ */ public class K02_TransitPriorityDestinationTest { - private static final RaptorTransitGroupCalculator PRIORITY_GROUP_CALCULATOR = + private static final RaptorTransitGroupPriorityCalculator PRIORITY_GROUP_CALCULATOR = TestGroupPriorityCalculator.PRIORITY_CALCULATOR; private static final int C1_SLACK_90s = RaptorCostConverter.toRaptorCost(90); diff --git a/src/test/java/org/opentripplanner/raptor/moduletests/support/TestGroupPriorityCalculator.java b/src/test/java/org/opentripplanner/raptor/moduletests/support/TestGroupPriorityCalculator.java index cdbe82f18a6..3debd487345 100644 --- a/src/test/java/org/opentripplanner/raptor/moduletests/support/TestGroupPriorityCalculator.java +++ b/src/test/java/org/opentripplanner/raptor/moduletests/support/TestGroupPriorityCalculator.java @@ -5,21 +5,21 @@ import org.junit.jupiter.api.Test; import org.opentripplanner.raptor.api.model.DominanceFunction; -import org.opentripplanner.raptor.api.request.RaptorTransitGroupCalculator; +import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; -public class TestGroupPriorityCalculator implements RaptorTransitGroupCalculator { +public class TestGroupPriorityCalculator implements RaptorTransitGroupPriorityCalculator { - public static final RaptorTransitGroupCalculator PRIORITY_CALCULATOR = new TestGroupPriorityCalculator(); + public static final RaptorTransitGroupPriorityCalculator PRIORITY_CALCULATOR = new TestGroupPriorityCalculator(); public static final int GROUP_A = 0x01; public static final int GROUP_B = 0x02; public static final int GROUP_C = 0x04; - private static final int GROUP_AB = PRIORITY_CALCULATOR.mergeGroupIds(GROUP_A, GROUP_B); - private static final int GROUP_AC = PRIORITY_CALCULATOR.mergeGroupIds(GROUP_A, GROUP_C); + private static final int GROUP_AB = PRIORITY_CALCULATOR.mergeInGroupId(GROUP_A, GROUP_B); + private static final int GROUP_AC = PRIORITY_CALCULATOR.mergeInGroupId(GROUP_A, GROUP_C); @Override - public int mergeGroupIds(int currentGroupIds, int boardingGroupId) { + public int mergeInGroupId(int currentGroupIds, int boardingGroupId) { return currentGroupIds | boardingGroupId; } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/SingleCriteriaComparatorTest.java b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/SingleCriteriaComparatorTest.java new file mode 100644 index 00000000000..3626ada63b3 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/SingleCriteriaComparatorTest.java @@ -0,0 +1,110 @@ +package org.opentripplanner.routing.algorithm.filterchain.filters.system; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.model.plan.Place; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.network.grouppriority.DefaultTransitGroupPriorityCalculator; + +class SingleCriteriaComparatorTest { + + private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); + private static final DefaultTransitGroupPriorityCalculator GROUP_PRIORITY_CALCULATOR = new DefaultTransitGroupPriorityCalculator(); + + private static final Place A = TEST_MODEL.place("A", 10, 11); + private static final Place B = TEST_MODEL.place("B", 10, 13); + private static final Place C = TEST_MODEL.place("C", 10, 14); + private static final Place D = TEST_MODEL.place("D", 10, 15); + + private static final int START = 1000; + private static final int TX_AT = 1500; + private static final int END_LOW = 2000; + + // [Tx, Cost] => [0, 1240] + private static final Itinerary zeroTransferLowCost = newItinerary(A) + .bus(1, START, END_LOW, B) + .walk(60, C) + .build(); + // [Tx, Cost] => [0, 1360] + private static final Itinerary zeroTransferHighCost = newItinerary(A) + .bus(1, START, END_LOW, B) + .walk(120, C) + .build(); + // [Tx, Cost] => [1, 1240] + private static final Itinerary oneTransferLowCost = newItinerary(A) + .bus(1, START, TX_AT, B) + .bus(2, TX_AT, END_LOW, C) + .build(); + + @BeforeAll + static void setUp() { + assertEquals(0, zeroTransferLowCost.getNumberOfTransfers()); + assertEquals(0, zeroTransferHighCost.getNumberOfTransfers()); + assertEquals(1, oneTransferLowCost.getNumberOfTransfers()); + + int expectedCost = zeroTransferLowCost.getGeneralizedCost(); + assertTrue(expectedCost < zeroTransferHighCost.getGeneralizedCost()); + assertEquals(expectedCost, oneTransferLowCost.getGeneralizedCost()); + } + + @Test + void strictOrder() { + assertTrue(SingleCriteriaComparator.compareNumTransfers().strictOrder()); + assertTrue(SingleCriteriaComparator.compareGeneralizedCost().strictOrder()); + assertFalse(SingleCriteriaComparator.compareTransitGroupsPriority().strictOrder()); + } + + @Test + void compareNumTransfers() { + var subject = SingleCriteriaComparator.compareNumTransfers(); + + // leftDominanceExist + assertFalse(subject.leftDominanceExist(zeroTransferHighCost, zeroTransferLowCost)); + assertTrue(subject.leftDominanceExist(zeroTransferLowCost, oneTransferLowCost)); + assertFalse(subject.leftDominanceExist(oneTransferLowCost, zeroTransferLowCost)); + + // strict order expected + assertTrue(subject.strictOrder()); + } + + @Test + void compareGeneralizedCost() { + var subject = SingleCriteriaComparator.compareGeneralizedCost(); + + // leftDominanceExist + assertFalse(subject.leftDominanceExist(zeroTransferHighCost, zeroTransferLowCost)); + assertTrue(subject.leftDominanceExist(zeroTransferLowCost, zeroTransferHighCost)); + assertFalse(subject.leftDominanceExist(zeroTransferLowCost, oneTransferLowCost)); + + // strict order expected + assertTrue(subject.strictOrder()); + } + + @Test + void compareTransitPriorityGroups() { + var group1 = newItinerary(A).bus(1, START, END_LOW, C).withGeneralizedCost2(1).build(); + var group2 = newItinerary(A).bus(1, START, END_LOW, C).withGeneralizedCost2(2).build(); + var group1And2 = newItinerary(A) + .bus(1, START, END_LOW, C) + .withGeneralizedCost2(GROUP_PRIORITY_CALCULATOR.mergeInGroupId(1, 2)) + .build(); + + var subject = SingleCriteriaComparator.compareTransitGroupsPriority(); + + assertTrue(subject.leftDominanceExist(group1, group2)); + assertTrue(subject.leftDominanceExist(group2, group1)); + assertTrue(subject.leftDominanceExist(group1, group1And2)); + assertTrue(subject.leftDominanceExist(group2, group1And2)); + assertFalse(subject.leftDominanceExist(group1And2, group1)); + assertFalse(subject.leftDominanceExist(group1And2, group1)); + + // Cannot be ordered => compare will fail + assertFalse(subject.strictOrder()); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/ItemTest.java b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/ItemTest.java new file mode 100644 index 00000000000..c8cd04212e5 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/ItemTest.java @@ -0,0 +1,56 @@ +package org.opentripplanner.routing.algorithm.filterchain.filters.system.mcmax; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary; + +import org.junit.jupiter.api.Test; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.model.plan.Place; +import org.opentripplanner.transit.model._data.TransitModelForTest; + +class ItemTest { + + private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); + private static final Place A = TEST_MODEL.place("A", 10, 11); + private static final Place B = TEST_MODEL.place("B", 10, 11); + private static final Itinerary ITINERARY = newItinerary(A).bus(1, 1, 2, B).build(); + + @Test + void betterThan() { + var i1 = new Item(ITINERARY, 3); + var i2 = new Item(ITINERARY, 7); + + // i1 is better than i2 because the index is lower + assertTrue(i1.betterThan(i2)); + assertFalse(i2.betterThan(i1)); + + // Incrementing both does not change anything + i1.incGroupCount(); + i2.incGroupCount(); + assertTrue(i1.betterThan(i2)); + assertFalse(i2.betterThan(i1)); + + // Incrementing i2 make it better + i2.incGroupCount(); + assertFalse(i1.betterThan(i2)); + assertTrue(i2.betterThan(i1)); + } + + @Test + void item() { + assertSame(ITINERARY, new Item(ITINERARY, 7).item()); + } + + @Test + void testToString() { + Item item = new Item(ITINERARY, 7); + assertEquals("Item #7 {count:0, A ~ BUS 1 0:00:01 0:00:02 ~ B [C₁121]}", item.toString()); + item.incGroupCount(); + assertEquals("Item #7 {count:1, A ~ BUS 1 0:00:01 0:00:02 ~ B [C₁121]}", item.toString()); + item.decGroupCount(); + assertEquals("Item #7 {count:0, A ~ BUS 1 0:00:01 0:00:02 ~ B [C₁121]}", item.toString()); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/McMaxLimitFilterTest.java b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/McMaxLimitFilterTest.java new file mode 100644 index 00000000000..d5f2323ad90 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/filterchain/filters/system/mcmax/McMaxLimitFilterTest.java @@ -0,0 +1,196 @@ +package org.opentripplanner.routing.algorithm.filterchain.filters.system.mcmax; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary; + +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.model.plan.Place; +import org.opentripplanner.routing.algorithm.filterchain.filters.system.SingleCriteriaComparator; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.network.grouppriority.DefaultTransitGroupPriorityCalculator; + +class McMaxLimitFilterTest { + + private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); + private static final DefaultTransitGroupPriorityCalculator GROUP_PRIORITY_CALCULATOR = new DefaultTransitGroupPriorityCalculator(); + + private static final Place A = TEST_MODEL.place("A", 10, 11); + private static final Place B = TEST_MODEL.place("B", 10, 13); + private static final Place C = TEST_MODEL.place("C", 10, 14); + private static final Place D = TEST_MODEL.place("D", 10, 15); + private static final Place E = TEST_MODEL.place("E", 10, 15); + private static final Place[] PLACES = { A, B, C, D, E }; + + private static final int START = 3600 * 10; + + // Note! Each group id needs to be a power of 2. This is implementation-specific, but using the + // TransitGroupPriorityService here to generate these ids is a bit over-kill. + private static final int GROUP_A = 0x01; + private static final int GROUP_B = 0x02; + private static final int GROUP_C = 0x04; + private static final int GROUP_AB = GROUP_PRIORITY_CALCULATOR.mergeInGroupId(GROUP_A, GROUP_B); + private static final int GROUP_BC = GROUP_PRIORITY_CALCULATOR.mergeInGroupId(GROUP_B, GROUP_C); + private static final int GROUP_ABC = GROUP_PRIORITY_CALCULATOR.mergeInGroupId(GROUP_AB, GROUP_C); + + private static final boolean EXP_KEEP = true; + private static final boolean EXP_DROP = false; + + private static final int COST_LOW = 1000; + private static final int COST_MED = 1200; + private static final int COST_HIGH = 1500; + + private static final int TX_0 = 0; + private static final int TX_1 = 1; + private static final int TX_2 = 2; + + private final McMaxLimitFilter subject = new McMaxLimitFilter( + "test", + 2, + List.of( + SingleCriteriaComparator.compareGeneralizedCost(), + SingleCriteriaComparator.compareNumTransfers(), + SingleCriteriaComparator.compareTransitGroupsPriority() + ) + ); + + static TestRow row( + boolean expected, + int c1, + int nTransfers, + int transitGroups, + String description + ) { + return new TestRow(expected, c1, nTransfers, transitGroups); + } + + static List> filterTestCases() { + return List.of( + List.of(/* Should not fail for an empty list of itineraries*/), + List.of( + // Test minNumItinerariesLimit = 2 + row(EXP_KEEP, COST_LOW, TX_1, GROUP_A, "Best in everything"), + row(EXP_KEEP, COST_HIGH, TX_2, GROUP_AB, "Worse, kept because minNumItinerariesLimit is 2") + ), + List.of( + // Test minNumItinerariesLimit, first is added + row(EXP_KEEP, COST_HIGH, TX_2, GROUP_ABC, "Worst, kept because of minNumItinerariesLimit"), + row(EXP_KEEP, COST_LOW, TX_0, GROUP_A, "Best in everything"), + row(EXP_DROP, COST_HIGH, TX_1, GROUP_AB, "Dropped because not better than #2.") + ), + List.of( + // The minNumItinerariesLimit is met, so no extra itinerary(#0) is added + row(EXP_DROP, COST_HIGH, TX_2, GROUP_AB, "First element is dropped"), + row(EXP_KEEP, COST_LOW, TX_1, GROUP_B, "Best cost and group B"), + row(EXP_KEEP, COST_MED, TX_0, GROUP_A, "Best nTransfers and group A") + ), + List.of( + row(EXP_KEEP, COST_LOW, TX_2, GROUP_A, "Best: c1 and group A"), + row(EXP_DROP, COST_LOW, TX_1, GROUP_AB, "Best compromise: c1, Tx, and group AB"), + row(EXP_KEEP, COST_LOW, TX_2, GROUP_C, "Best: c1 and group C"), + row(EXP_KEEP, COST_MED, TX_0, GROUP_BC, "Best: num-of-transfers") + ), + /** + * This is the example explained in JavaDoc {@link McMaxLimitFilter} + */ + List.of( + row(EXP_DROP, COST_LOW, TX_1, GROUP_A, ""), + row(EXP_DROP, COST_LOW, TX_2, GROUP_AB, ""), + row(EXP_KEEP, COST_LOW, TX_2, GROUP_B, "Kept -> Only one in group B"), + row(EXP_DROP, COST_MED, TX_0, GROUP_AB, ""), + row(EXP_KEEP, COST_MED, TX_0, GROUP_A, "Kept -> Best transfer and group A"), + row(EXP_KEEP, COST_HIGH, TX_1, GROUP_C, "Kept -> Best group C, tie with #6"), + row(EXP_DROP, COST_HIGH, TX_2, GROUP_C, "") + ) + ); + } + + @ParameterizedTest + @MethodSource("filterTestCases") + void filterTest(List rows) { + var input = rows.stream().map(TestRow::create).toList(); + var expected = rows.stream().filter(TestRow::expected).map(TestRow::create).toList(); + + var result = subject.removeMatchesForTest(input); + + assertEquals(toStr(expected), toStr(result)); + } + + @Test + void testName() { + assertEquals("test", subject.name()); + } + + /** + * Make sure the test setup is correct - this does not test anything in src/main + */ + @Test + void testGroupsToString() { + assertEquals("A", groupsToString(GROUP_A)); + assertEquals("B", groupsToString(GROUP_B)); + assertEquals("C", groupsToString(GROUP_C)); + assertEquals("AB", groupsToString(GROUP_AB)); + assertEquals("BC", groupsToString(GROUP_BC)); + assertEquals("ABC", groupsToString(GROUP_ABC)); + } + + private static String groupsToString(int groups) { + var buf = new StringBuilder(); + char ch = 'A'; + // Check for 5 groups - the test does not use so many, but it does not matter + for (int i = 0; i < 5; ++i) { + int mask = 1 << i; + if ((groups & mask) != 0) { + buf.append(ch); + } + ch = (char) (ch + 1); + } + return buf.toString(); + } + + private static String toStr(List list) { + return list + .stream() + .map(i -> + "[ %d %d %s ]".formatted( + i.getGeneralizedCost(), + i.getNumberOfTransfers(), + groupsToString(i.getGeneralizedCost2().orElse(-1)) + ) + ) + .collect(Collectors.joining(", ")); + } + + record TestRow(boolean expected, int c1, int nTransfers, int transitGroupIds) { + Itinerary create() { + int start = START; + var builder = newItinerary(A); + + if (nTransfers < 0) { + builder.drive(start, ++start, E); + } else { + builder.bus(1, ++start, ++start, PLACES[1]); + for (int i = 0; i < nTransfers; i++) { + builder.bus(1, ++start, ++start, PLACES[i + 2]); + } + builder.withGeneralizedCost2(transitGroupIds); + } + return builder.build(c1); + } + + @Override + public String toString() { + // The red-x is a unicode character(U+274C) and should be visible in most IDEs. + return "%s %d %d %s".formatted( + expected ? "" : "❌", + c1, + nTransfers, + groupsToString(transitGroupIds) + ); + } + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/mapping/__snapshots__/CarSnapshotTest.snap b/src/test/java/org/opentripplanner/routing/algorithm/mapping/__snapshots__/CarSnapshotTest.snap index 8f527b15bf7..5d83d85cd7a 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/mapping/__snapshots__/CarSnapshotTest.snap +++ b/src/test/java/org/opentripplanner/routing/algorithm/mapping/__snapshots__/CarSnapshotTest.snap @@ -28,8 +28,8 @@ org.opentripplanner.routing.algorithm.mapping.CarSnapshotTest.directCarPark=[ "generalizedCost" : 61, "interlineWithPreviousLeg" : false, "legGeometry" : { - "length" : 5, - "points" : "ya|tGv~{kV??nCEB|Dn@@" + "length" : 6, + "points" : "ya|tGv~{kV??nCEB|Dn@@??" }, "mode" : "CAR", "pathway" : false, @@ -147,8 +147,8 @@ org.opentripplanner.routing.algorithm.mapping.CarSnapshotTest.directCarPark=[ "generalizedCost" : 285, "interlineWithPreviousLeg" : false, "legGeometry" : { - "length" : 5, - "points" : "gz{tGrd|kVn@@CgElCEB?" + "length" : 6, + "points" : "gz{tGrd|kV??n@@CgElCEB?" }, "mode" : "WALK", "pathway" : false, diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/grouppriority/TransitGroupPriority32nTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/grouppriority/TransitGroupPriority32nTest.java deleted file mode 100644 index 2713a190dbf..00000000000 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/cost/grouppriority/TransitGroupPriority32nTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.grouppriority; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; -import org.opentripplanner.raptor.api.request.RaptorTransitGroupCalculator; - -class TransitGroupPriority32nTest { - - private static final int GROUP_INDEX_0 = 0; - private static final int GROUP_INDEX_1 = 1; - private static final int GROUP_INDEX_2 = 2; - private static final int GROUP_INDEX_30 = 30; - private static final int GROUP_INDEX_31 = 31; - - private static final int GROUP_0 = TransitGroupPriority32n.groupId(GROUP_INDEX_0); - private static final int GROUP_1 = TransitGroupPriority32n.groupId(GROUP_INDEX_1); - private static final int GROUP_2 = TransitGroupPriority32n.groupId(GROUP_INDEX_2); - private static final int GROUP_30 = TransitGroupPriority32n.groupId(GROUP_INDEX_30); - private static final int GROUP_31 = TransitGroupPriority32n.groupId(GROUP_INDEX_31); - private static final RaptorTransitGroupCalculator subjct = TransitGroupPriority32n.priorityCalculator(); - - @Test - void groupId() { - assertEqualsHex(0x00_00_00_00, TransitGroupPriority32n.groupId(0)); - assertEqualsHex(0x00_00_00_01, TransitGroupPriority32n.groupId(1)); - assertEqualsHex(0x00_00_00_02, TransitGroupPriority32n.groupId(2)); - assertEqualsHex(0x00_00_00_04, TransitGroupPriority32n.groupId(3)); - assertEqualsHex(0x40_00_00_00, TransitGroupPriority32n.groupId(31)); - assertEqualsHex(0x80_00_00_00, TransitGroupPriority32n.groupId(32)); - - assertThrows(IllegalArgumentException.class, () -> TransitGroupPriority32n.groupId(-1)); - assertThrows(IllegalArgumentException.class, () -> TransitGroupPriority32n.groupId(33)); - } - - @Test - void mergeTransitGroupPriorityIds() { - assertEqualsHex(GROUP_0, subjct.mergeGroupIds(GROUP_0, GROUP_0)); - assertEqualsHex(GROUP_1, subjct.mergeGroupIds(GROUP_1, GROUP_1)); - assertEqualsHex(GROUP_0 | GROUP_1, subjct.mergeGroupIds(GROUP_0, GROUP_1)); - assertEqualsHex(GROUP_30 | GROUP_31, subjct.mergeGroupIds(GROUP_30, GROUP_31)); - assertEqualsHex( - GROUP_0 | GROUP_1 | GROUP_2 | GROUP_30 | GROUP_31, - subjct.mergeGroupIds(GROUP_0 | GROUP_1 | GROUP_2 | GROUP_30, GROUP_31) - ); - } - - @Test - void dominanceFunction() { - assertFalse(subjct.dominanceFunction().leftDominateRight(GROUP_0, GROUP_0)); - assertFalse(subjct.dominanceFunction().leftDominateRight(GROUP_31, GROUP_31)); - assertFalse(subjct.dominanceFunction().leftDominateRight(GROUP_1 | GROUP_2, GROUP_1 | GROUP_2)); - - assertTrue(subjct.dominanceFunction().leftDominateRight(GROUP_0, GROUP_1)); - assertFalse(subjct.dominanceFunction().leftDominateRight(GROUP_1, GROUP_0)); - - assertTrue(subjct.dominanceFunction().leftDominateRight(GROUP_1, GROUP_1 | GROUP_2)); - assertFalse(subjct.dominanceFunction().leftDominateRight(GROUP_1 | GROUP_2, GROUP_1)); - } - - static void assertEqualsHex(int expected, int actual) { - assertEquals(expected, actual, "%08x == %08x".formatted(expected, actual)); - } -} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreatorTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreatorTest.java index ed71b3de400..ea815a2f47f 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreatorTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreatorTest.java @@ -19,6 +19,7 @@ import org.opentripplanner.transit.model.network.RoutingTripPattern; import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; import org.opentripplanner.transit.model.timetable.TripTimes; @@ -60,12 +61,13 @@ public void testMergeTripPatterns() { tripPatternsForDates.add(new TripPatternForDate(tripPattern1, tripTimes, List.of(), third)); tripPatternsForDates.add(new TripPatternForDate(tripPattern3, tripTimes, List.of(), third)); - // Patterns containing trip schedules for all 3 days. Trip schedules for later days are offset in time when requested. + // Patterns containing trip schedules for all 3 days. Trip schedules for later days are offset + // in time when requested. List combinedTripPatterns = RaptorRoutingRequestTransitDataCreator.merge( startOfTime, tripPatternsForDates, new TestTransitDataProviderFilter(), - PriorityGroupConfigurator.empty() + TransitGroupPriorityService.empty() ); // Get the results diff --git a/src/test/java/org/opentripplanner/routing/graph/DefaultRoutingServiceTest.java b/src/test/java/org/opentripplanner/routing/graph/DefaultRoutingServiceTest.java index e9d286d5a92..8c56bb89a1f 100644 --- a/src/test/java/org/opentripplanner/routing/graph/DefaultRoutingServiceTest.java +++ b/src/test/java/org/opentripplanner/routing/graph/DefaultRoutingServiceTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Collection; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; @@ -18,6 +19,8 @@ import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.service.DefaultTransitService; +import org.opentripplanner.transit.service.TransitService; /** * Check that the graph index is created, that GTFS elements can be found in the index, and that the @@ -27,6 +30,15 @@ */ public class DefaultRoutingServiceTest extends GtfsTest { + private TransitService transitService; + + @BeforeEach + @Override + public void setUp() throws Exception { + super.setUp(); + transitService = new DefaultTransitService(transitModel); + } + @Override public String getFeedName() { return "gtfs/simple"; @@ -44,16 +56,16 @@ public void testIdLookup() { } /* Agencies */ - String feedId = transitModel.getFeedIds().iterator().next(); + String feedId = transitService.getFeedIds().iterator().next(); Agency agency; - agency = transitModel.getTransitModelIndex().getAgencyForId(new FeedScopedId(feedId, "azerty")); + agency = transitService.getAgencyForId(new FeedScopedId(feedId, "azerty")); assertNull(agency); - agency = transitModel.getTransitModelIndex().getAgencyForId(new FeedScopedId(feedId, "agency")); + agency = transitService.getAgencyForId(new FeedScopedId(feedId, "agency")); assertEquals(feedId + ":" + "agency", agency.getId().toString()); assertEquals("Fake Agency", agency.getName()); /* Stops */ - transitModel.getStopModel().getRegularStop(new FeedScopedId("X", "Y")); + transitService.getRegularStop(new FeedScopedId("X", "Y")); /* Trips */ // graph.index.tripForId; // graph.index.routeForId; @@ -67,21 +79,18 @@ public void testIdLookup() { */ @Test public void testPatternsCoherent() { - for (Trip trip : transitModel.getTransitModelIndex().getTripForId().values()) { - TripPattern pattern = transitModel.getTransitModelIndex().getPatternForTrip().get(trip); + for (Trip trip : transitService.getAllTrips()) { + TripPattern pattern = transitService.getPatternForTrip(trip); assertTrue(pattern.scheduledTripsAsStream().anyMatch(t -> t.equals(trip))); } /* This one depends on a feed where each TripPattern appears on only one route. */ - for (Route route : transitModel.getTransitModelIndex().getAllRoutes()) { - for (TripPattern pattern : transitModel - .getTransitModelIndex() - .getPatternsForRoute() - .get(route)) { + for (Route route : transitService.getAllRoutes()) { + for (TripPattern pattern : transitService.getPatternsForRoute(route)) { assertEquals(pattern.getRoute(), route); } } - for (var stop : transitModel.getStopModel().listStopLocations()) { - for (TripPattern pattern : transitModel.getTransitModelIndex().getPatternsForStop(stop)) { + for (var stop : transitService.listStopLocations()) { + for (TripPattern pattern : transitService.getPatternsForStop(stop)) { int stopPos = pattern.findStopPosition(stop); assertTrue(stopPos >= 0, "Stop position exist"); } @@ -90,13 +99,13 @@ public void testPatternsCoherent() { @Test public void testSpatialIndex() { - String feedId = transitModel.getFeedIds().iterator().next(); + String feedId = transitService.getFeedIds().iterator().next(); FeedScopedId idJ = new FeedScopedId(feedId, "J"); - var stopJ = transitModel.getStopModel().getRegularStop(idJ); + var stopJ = transitService.getRegularStop(idJ); FeedScopedId idL = new FeedScopedId(feedId, "L"); - var stopL = transitModel.getStopModel().getRegularStop(idL); + var stopL = transitService.getRegularStop(idL); FeedScopedId idM = new FeedScopedId(feedId, "M"); - var stopM = transitModel.getStopModel().getRegularStop(idM); + var stopM = transitService.getRegularStop(idM); TransitStopVertex stopvJ = graph.getStopVertexForStopId(idJ); TransitStopVertex stopvL = graph.getStopVertexForStopId(idL); TransitStopVertex stopvM = graph.getStopVertexForStopId(idM); @@ -106,7 +115,7 @@ public void testSpatialIndex() { SphericalDistanceLibrary.metersToLonDegrees(100, stopJ.getLat()), SphericalDistanceLibrary.metersToDegrees(100) ); - Collection stops = transitModel.getStopModel().findRegularStops(env); + Collection stops = transitService.findRegularStops(env); assertTrue(stops.contains(stopJ)); assertTrue(stops.contains(stopL)); assertTrue(stops.contains(stopM)); diff --git a/src/test/java/org/opentripplanner/smoketest/SeattleSmokeTest.java b/src/test/java/org/opentripplanner/smoketest/SeattleSmokeTest.java index 94e4b3dea1a..506ee2732b9 100644 --- a/src/test/java/org/opentripplanner/smoketest/SeattleSmokeTest.java +++ b/src/test/java/org/opentripplanner/smoketest/SeattleSmokeTest.java @@ -1,5 +1,6 @@ package org.opentripplanner.smoketest; +import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -160,8 +161,8 @@ public void sharedStop() throws IOException { var first = itineraries.getFirst(); var leg = first.transitLegs().getFirst(); - assertEquals("510", leg.route().shortName().get()); - assertEquals("Sound Transit", leg.route().agency().name()); + assertThat(Set.of("510", "415")).contains(leg.route().shortName().get()); + assertThat(Set.of("Sound Transit", "Community Transit")).contains(leg.route().agency().name()); var stop = leg.from().stop().get(); assertEquals("Olive Way & 6th Ave", stop.name()); diff --git a/src/test/java/org/opentripplanner/street/model/edge/StreetVehicleParkingLinkTest.java b/src/test/java/org/opentripplanner/street/model/edge/StreetVehicleParkingLinkTest.java index 99de720a5a2..8a3316e44e0 100644 --- a/src/test/java/org/opentripplanner/street/model/edge/StreetVehicleParkingLinkTest.java +++ b/src/test/java/org/opentripplanner/street/model/edge/StreetVehicleParkingLinkTest.java @@ -9,6 +9,7 @@ import java.util.Set; import java.util.stream.Stream; +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; @@ -17,6 +18,7 @@ import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.vehicle_parking.VehicleParking; import org.opentripplanner.routing.vehicle_parking.VehicleParkingEntrance; +import org.opentripplanner.street.model._data.StreetModelForTest; import org.opentripplanner.street.model.vertex.VehicleParkingEntranceVertex; import org.opentripplanner.street.model.vertex.Vertex; import org.opentripplanner.street.search.request.StreetSearchRequest; @@ -38,37 +40,25 @@ static Stream testCases() { @ParameterizedTest(name = "Parking[tags={0}], Request[not={1}, select={2}] should traverse={3}") @MethodSource("testCases") - void foo(Set parkingTags, Set not, Set select, boolean shouldTraverse) { + void parkingFilters( + Set parkingTags, + Set not, + Set select, + boolean shouldTraverse + ) { var streetVertex = intersectionVertex(1, 1); - var parking = VehicleParking - .builder() - .id(id("parking")) - .coordinate(new WgsCoordinate(1, 1)) - .tags(parkingTags) - .build(); - - var entrance = VehicleParkingEntrance - .builder() - .vehicleParking(parking) - .entranceId(id("entrance")) - .coordinate(new WgsCoordinate(1, 1)) - .name(new NonLocalizedString("entrance")) - .walkAccessible(true) - .carAccessible(true) - .build(); - - var entranceVertex = new VehicleParkingEntranceVertex(entrance); + final var entranceVertex = buildVertex(parkingTags); var req = StreetSearchRequest.of(); req.withMode(StreetMode.BIKE_TO_PARK); req.withPreferences(p -> - p.withBike(bike -> { + p.withBike(bike -> bike.withParking(parkingPreferences -> { parkingPreferences.withRequiredVehicleParkingTags(select); parkingPreferences.withBannedVehicleParkingTags(not); parkingPreferences.withCost(0); - }); - }) + }) + ) ); var edge = StreetVehicleParkingLink.createStreetVehicleParkingLink( @@ -84,6 +74,53 @@ void foo(Set parkingTags, Set not, Set select, boolean s } } + @Test + void notLinkedToGraph() { + var vertex = buildVertex(Set.of()); + assertFalse(vertex.isLinkedToGraph()); + } + + @Test + void linkedToGraphWithIncoming() { + var vertex = buildVertex(Set.of()); + var streetVertex = StreetModelForTest.intersectionVertex(1, 1); + vertex.addIncoming( + StreetVehicleParkingLink.createStreetVehicleParkingLink(streetVertex, vertex) + ); + assertTrue(vertex.isLinkedToGraph()); + } + + @Test + void linkedToGraphWithOutgoing() { + var vertex = buildVertex(Set.of()); + var streetVertex = StreetModelForTest.intersectionVertex(1, 1); + vertex.addOutgoing( + StreetVehicleParkingLink.createStreetVehicleParkingLink(streetVertex, vertex) + ); + assertTrue(vertex.isLinkedToGraph()); + } + + private static VehicleParkingEntranceVertex buildVertex(Set parkingTags) { + var parking = VehicleParking + .builder() + .id(id("parking")) + .coordinate(new WgsCoordinate(1, 1)) + .tags(parkingTags) + .build(); + + var entrance = VehicleParkingEntrance + .builder() + .vehicleParking(parking) + .entranceId(id("entrance")) + .coordinate(new WgsCoordinate(1, 1)) + .name(new NonLocalizedString("entrance")) + .walkAccessible(true) + .carAccessible(true) + .build(); + + return new VehicleParkingEntranceVertex(entrance); + } + private State[] traverse(Vertex fromV, Edge edge, StreetSearchRequest request) { var state = new State(fromV, request); diff --git a/src/test/java/org/opentripplanner/transit/model/network/grouppriority/DefaultTransitGroupPriorityCalculatorTest.java b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/DefaultTransitGroupPriorityCalculatorTest.java new file mode 100644 index 00000000000..34a7d3ef698 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/DefaultTransitGroupPriorityCalculatorTest.java @@ -0,0 +1,32 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class DefaultTransitGroupPriorityCalculatorTest { + + private final DefaultTransitGroupPriorityCalculator subject = new DefaultTransitGroupPriorityCalculator(); + + @Test + void mergeGroupIds() { + // Smoke test, should not fail + subject.mergeInGroupId(1, 2); + } + + @Test + void dominanceFunction() { + // This is assuming 1 & 2 represent different transit-groups - this just a smoke test to + // see that the delegation works as expected. The 'leftDominateRight' is unit-tested elsewhere. + assertTrue(subject.dominanceFunction().leftDominateRight(1, 2)); + assertTrue(subject.dominanceFunction().leftDominateRight(2, 1)); + assertFalse(subject.dominanceFunction().leftDominateRight(1, 1)); + } + + @Test + void testToString() { + assertEquals("DefaultTransitGroupCalculator{Using TGP32n}", subject.toString()); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupMatcherTest.java b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/MatchersTest.java similarity index 70% rename from src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupMatcherTest.java rename to src/test/java/org/opentripplanner/transit/model/network/grouppriority/MatchersTest.java index 91d0142f9ef..cdda3755d37 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupMatcherTest.java +++ b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/MatchersTest.java @@ -1,4 +1,4 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit.request; +package org.opentripplanner.transit.model.network.grouppriority; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -6,12 +6,12 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TestRouteData; import org.opentripplanner.routing.api.request.request.filter.TransitGroupSelect; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.network.TripPattern; -class PriorityGroupMatcherTest { +class MatchersTest { private final TestRouteData r1 = TestRouteData .rail("R1") @@ -25,16 +25,24 @@ class PriorityGroupMatcherTest { .withSubmode("localFerry") .build(); - private final TripPattern rail1 = r1.getTripPattern(); - private final TripPattern bus = b1.getTripPattern(); - private final TripPattern ferry = f1.getTripPattern(); - private final FeedScopedId r1agencyId = rail1.getRoute().getAgency().getId(); - private final FeedScopedId r1routeId = rail1.getRoute().getId(); + private final EntityAdapter rail1 = new TripPatternAdapter(r1.getTripPattern()); + private final EntityAdapter bus = new TripPatternAdapter(b1.getTripPattern()); + private final EntityAdapter ferry = new TripPatternAdapter(f1.getTripPattern()); + private final FeedScopedId r1agencyId = rail1.agencyId(); + private final FeedScopedId r1routeId = rail1.routeId(); private final FeedScopedId anyId = new FeedScopedId("F", "ANY"); + @Test + void testEmptySelect() { + var m = Matchers.of(TransitGroupSelect.of().build()); + assertEquals("Empty", m.toString()); + assertTrue(m.isEmpty()); + assertFalse(m.match(bus)); + } + @Test void testMode() { - var m = PriorityGroupMatcher.of( + var m = Matchers.of( TransitGroupSelect.of().addModes(List.of(TransitMode.BUS, TransitMode.TRAM)).build() ); assertEquals("Mode(BUS | TRAM)", m.toString()); @@ -46,18 +54,14 @@ void testMode() { @Test void testAgencyIds() { - var m1 = PriorityGroupMatcher.of( - TransitGroupSelect.of().addAgencyIds(List.of(r1agencyId)).build() - ); - var m2 = PriorityGroupMatcher.of( - TransitGroupSelect.of().addAgencyIds(List.of(r1agencyId, anyId)).build() - ); + var m1 = Matchers.of(TransitGroupSelect.of().addAgencyIds(List.of(r1agencyId)).build()); + var m2 = Matchers.of(TransitGroupSelect.of().addAgencyIds(List.of(r1agencyId, anyId)).build()); var matchers = List.of(m1, m2); assertEquals("AgencyId(F:A1)", m1.toString()); assertEquals("AgencyId(F:A1 | F:ANY)", m2.toString()); - for (PriorityGroupMatcher m : matchers) { + for (Matcher m : matchers) { assertFalse(m.isEmpty()); assertTrue(m.match(rail1)); assertTrue(m.match(ferry)); @@ -67,18 +71,14 @@ void testAgencyIds() { @Test void routeIds() { - var m1 = PriorityGroupMatcher.of( - TransitGroupSelect.of().addRouteIds(List.of(r1routeId)).build() - ); - var m2 = PriorityGroupMatcher.of( - TransitGroupSelect.of().addRouteIds(List.of(r1routeId, anyId)).build() - ); + var m1 = Matchers.of(TransitGroupSelect.of().addRouteIds(List.of(r1routeId)).build()); + var m2 = Matchers.of(TransitGroupSelect.of().addRouteIds(List.of(r1routeId, anyId)).build()); var matchers = List.of(m1, m2); assertEquals("RouteId(F:R1)", m1.toString()); assertEquals("RouteId(F:R1 | F:ANY)", m2.toString()); - for (PriorityGroupMatcher m : matchers) { + for (Matcher m : matchers) { assertFalse(m.isEmpty()); assertTrue(m.match(rail1)); assertFalse(m.match(ferry)); @@ -88,7 +88,7 @@ void routeIds() { @Test void testSubMode() { - var subject = PriorityGroupMatcher.of( + var subject = Matchers.of( TransitGroupSelect.of().addSubModeRegexp(List.of(".*local.*")).build() ); @@ -102,7 +102,7 @@ void testSubMode() { @Test void testAnd() { - var subject = PriorityGroupMatcher.of( + var subject = Matchers.of( TransitGroupSelect .of() .addSubModeRegexp(List.of("express")) @@ -124,7 +124,7 @@ void testAnd() { @Test void testToString() { - var subject = PriorityGroupMatcher.of( + var subject = Matchers.of( TransitGroupSelect .of() .addModes(List.of(TransitMode.BUS, TransitMode.TRAM)) diff --git a/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriority32nTest.java b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriority32nTest.java new file mode 100644 index 00000000000..9f681dc16c6 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriority32nTest.java @@ -0,0 +1,68 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriority32n.dominate; +import static org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriority32n.groupId; +import static org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriority32n.mergeInGroupId; + +import org.junit.jupiter.api.Test; + +class TransitGroupPriority32nTest { + + private static final int GROUP_INDEX_0 = 0; + private static final int GROUP_INDEX_1 = 1; + private static final int GROUP_INDEX_2 = 2; + private static final int GROUP_INDEX_30 = 30; + private static final int GROUP_INDEX_31 = 31; + + private static final int GROUP_0 = groupId(GROUP_INDEX_0); + private static final int GROUP_1 = groupId(GROUP_INDEX_1); + private static final int GROUP_2 = groupId(GROUP_INDEX_2); + private static final int GROUP_30 = groupId(GROUP_INDEX_30); + private static final int GROUP_31 = groupId(GROUP_INDEX_31); + + @Test + void testGroupId() { + assertEqualsHex(0x00_00_00_00, groupId(0)); + assertEqualsHex(0x00_00_00_01, groupId(1)); + assertEqualsHex(0x00_00_00_02, groupId(2)); + assertEqualsHex(0x00_00_00_04, groupId(3)); + assertEqualsHex(0x40_00_00_00, groupId(31)); + assertEqualsHex(0x80_00_00_00, groupId(32)); + + assertThrows(IllegalArgumentException.class, () -> groupId(-1)); + assertThrows(IllegalArgumentException.class, () -> groupId(33)); + } + + @Test + void mergeTransitGroupPriorityIds() { + assertEqualsHex(GROUP_0, mergeInGroupId(GROUP_0, GROUP_0)); + assertEqualsHex(GROUP_1, mergeInGroupId(GROUP_1, GROUP_1)); + assertEqualsHex(GROUP_0 | GROUP_1, mergeInGroupId(GROUP_0, GROUP_1)); + assertEqualsHex(GROUP_30 | GROUP_31, mergeInGroupId(GROUP_30, GROUP_31)); + assertEqualsHex( + GROUP_0 | GROUP_1 | GROUP_2 | GROUP_30 | GROUP_31, + mergeInGroupId(GROUP_0 | GROUP_1 | GROUP_2 | GROUP_30, GROUP_31) + ); + } + + @Test + void dominanceFunction() { + assertFalse(dominate(GROUP_0, GROUP_0)); + assertFalse(dominate(GROUP_31, GROUP_31)); + assertFalse(dominate(GROUP_1 | GROUP_2, GROUP_1 | GROUP_2)); + + assertTrue(dominate(GROUP_0, GROUP_1)); + assertFalse(dominate(GROUP_1, GROUP_0)); + + assertTrue(dominate(GROUP_1, GROUP_1 | GROUP_2)); + assertFalse(dominate(GROUP_1 | GROUP_2, GROUP_1)); + } + + static void assertEqualsHex(int expected, int actual) { + assertEquals(expected, actual, "%08x == %08x".formatted(expected, actual)); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupConfiguratorTest.java b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriorityServiceTest.java similarity index 75% rename from src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupConfiguratorTest.java rename to src/test/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriorityServiceTest.java index 7f974927c1b..fe2cb361f07 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/PriorityGroupConfiguratorTest.java +++ b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TransitGroupPriorityServiceTest.java @@ -1,4 +1,4 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit.request; +package org.opentripplanner.transit.model.network.grouppriority; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TestTransitCaseData.STOP_A; @@ -7,12 +7,14 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TestRouteData; import org.opentripplanner.routing.api.request.request.filter.TransitGroupSelect; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.timetable.Trip; -class PriorityGroupConfiguratorTest { +class TransitGroupPriorityServiceTest { private static final String AGENCY_A1 = "A1"; private static final String AGENCY_A2 = "A2"; @@ -65,13 +67,15 @@ class PriorityGroupConfiguratorTest { private final TripPattern railR3 = routeR3.getTripPattern(); private final TripPattern ferryF3 = routeF3.getTripPattern(); private final TripPattern busB3 = routeB3.getTripPattern(); + private final TripPattern nullTripPattern = null; + private final Trip nullTrip = null; @Test void emptyConfigurationShouldReturnGroupZero() { - var subject = PriorityGroupConfigurator.of(List.of(), List.of()); + var subject = TransitGroupPriorityService.empty(); assertEquals(subject.baseGroupId(), subject.lookupTransitGroupPriorityId(railR1)); assertEquals(subject.baseGroupId(), subject.lookupTransitGroupPriorityId(busB2)); - assertEquals(subject.baseGroupId(), subject.lookupTransitGroupPriorityId(null)); + assertEquals(subject.baseGroupId(), subject.lookupTransitGroupPriorityId(nullTripPattern)); } @Test @@ -82,21 +86,32 @@ void lookupTransitGroupIdByAgency() { .build(); // Add matcher `byAgency` for bus and real - var subject = PriorityGroupConfigurator.of(List.of(select), List.of()); + var subject = new TransitGroupPriorityService(List.of(select), List.of()); // Agency groups are indexed (group-id set) at request time - assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(null)); + assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(nullTripPattern)); assertEquals(EXP_GROUP_1, subject.lookupTransitGroupPriorityId(busB2)); assertEquals(EXP_GROUP_2, subject.lookupTransitGroupPriorityId(railR3)); assertEquals(EXP_GROUP_3, subject.lookupTransitGroupPriorityId(railR1)); assertEquals(EXP_GROUP_2, subject.lookupTransitGroupPriorityId(busB3)); assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(ferryF3)); + + // Verify we get the same result with using the trip, not trip-pattern + assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(nullTrip)); + assertEquals( + EXP_GROUP_1, + subject.lookupTransitGroupPriorityId(busB2.getScheduledTimetable().getTripTimes(0).getTrip()) + ); + assertEquals( + EXP_GROUP_2, + subject.lookupTransitGroupPriorityId(railR3.getScheduledTimetable().getTripTimes(0).getTrip()) + ); } @Test void lookupTransitPriorityGroupIdByGlobalMode() { // Global groups are indexed (group-id set) at construction time - var subject = PriorityGroupConfigurator.of( + var subject = new TransitGroupPriorityService( List.of(), List.of( TransitGroupSelect.of().addModes(List.of(TransitMode.BUS)).build(), @@ -104,12 +119,19 @@ void lookupTransitPriorityGroupIdByGlobalMode() { ) ); - assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(null)); + assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(nullTripPattern)); assertEquals(EXP_GROUP_2, subject.lookupTransitGroupPriorityId(railR1)); assertEquals(EXP_GROUP_1, subject.lookupTransitGroupPriorityId(busB2)); assertEquals(EXP_GROUP_2, subject.lookupTransitGroupPriorityId(railR3)); assertEquals(EXP_GROUP_1, subject.lookupTransitGroupPriorityId(busB3)); assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(ferryF3)); + + // Verify we get the same result with using the trip, not trip-pattern + assertEquals(EXP_GROUP_ID_BASE, subject.lookupTransitGroupPriorityId(nullTrip)); + assertEquals( + EXP_GROUP_2, + subject.lookupTransitGroupPriorityId(railR1.getScheduledTimetable().getTripTimes(0).getTrip()) + ); } private static TestRouteData route( diff --git a/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TripAdapterTest.java b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TripAdapterTest.java new file mode 100644 index 00000000000..c1aeeea1a59 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/network/grouppriority/TripAdapterTest.java @@ -0,0 +1,34 @@ +package org.opentripplanner.transit.model.network.grouppriority; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.timetable.Trip; + +class TripAdapterTest { + + private final Trip trip = TransitModelForTest.trip("Trip").build(); + + private final TripAdapter subject = new TripAdapter(trip); + + @Test + void mode() { + assertEquals(trip.getMode(), subject.mode()); + } + + @Test + void subMode() { + assertEquals(trip.getNetexSubMode().name(), subject.subMode()); + } + + @Test + void agencyId() { + assertEquals(trip.getRoute().getAgency().getId(), subject.agencyId()); + } + + @Test + void routeId() { + assertEquals(trip.getRoute().getId(), subject.routeId()); + } +} diff --git a/src/test/java/org/opentripplanner/transit/model/site/MultiModalStationTest.java b/src/test/java/org/opentripplanner/transit/model/site/MultiModalStationTest.java index 4ca8e8d33ea..615b0c49674 100644 --- a/src/test/java/org/opentripplanner/transit/model/site/MultiModalStationTest.java +++ b/src/test/java/org/opentripplanner/transit/model/site/MultiModalStationTest.java @@ -8,6 +8,7 @@ import java.util.Set; import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.transit.model._data.TransitModelForTest; @@ -26,6 +27,7 @@ class MultiModalStationTest { .of(TransitModelForTest.id(ID)) .withName(NAME) .withChildStations(CHILD_STATIONS) + .withCoordinate(new WgsCoordinate(1, 1)) .build(); @Test diff --git a/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java b/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java index f04fe782a0a..54733f084d6 100644 --- a/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java +++ b/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java @@ -6,16 +6,22 @@ import static org.opentripplanner.transit.model.basic.TransitMode.RAIL; import static org.opentripplanner.transit.model.basic.TransitMode.TRAM; +import java.time.LocalDate; import java.util.Collection; import java.util.List; +import java.util.Set; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.framework.Deduplicator; +import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; +import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; class DefaultTransitServiceTest { @@ -33,6 +39,13 @@ class DefaultTransitServiceTest { static TripPattern FERRY_PATTERN = TEST_MODEL.pattern(FERRY).build(); static TripPattern BUS_PATTERN = TEST_MODEL.pattern(BUS).build(); + static StopPattern REAL_TIME_STOP_PATTERN = TransitModelForTest.stopPattern(STOP_A, STOP_B); + static TripPattern REAL_TIME_PATTERN = TEST_MODEL + .pattern(BUS) + .withStopPattern(REAL_TIME_STOP_PATTERN) + .withCreatedByRealtimeUpdater(true) + .build(); + @BeforeAll static void setup() { var stopModel = TEST_MODEL @@ -46,6 +59,20 @@ static void setup() { transitModel.addTripPattern(RAIL_PATTERN.getId(), RAIL_PATTERN); transitModel.index(); + transitModel.initTimetableSnapshotProvider(() -> { + TimetableSnapshot timetableSnapshot = new TimetableSnapshot(); + RealTimeTripTimes tripTimes = RealTimeTripTimes.of( + ScheduledTripTimes + .of() + .withTrip(TransitModelForTest.trip("REAL_TIME_TRIP").build()) + .withDepartureTimes(new int[] { 0, 1 }) + .build() + ); + timetableSnapshot.update(REAL_TIME_PATTERN, tripTimes, LocalDate.now()); + + return timetableSnapshot.commit(); + }); + service = new DefaultTransitService(transitModel) { @Override @@ -76,4 +103,16 @@ void stationModes() { var modes = service.getModesOfStopLocationsGroup(STATION); assertEquals(List.of(RAIL, FERRY, TRAM), modes); } + + @Test + void getPatternForStopsWithoutRealTime() { + Collection patternsForStop = service.getPatternsForStop(STOP_B, false); + assertEquals(Set.of(FERRY_PATTERN, RAIL_PATTERN), patternsForStop); + } + + @Test + void getPatternForStopsWithRealTime() { + Collection patternsForStop = service.getPatternsForStop(STOP_B, true); + assertEquals(Set.of(FERRY_PATTERN, RAIL_PATTERN, REAL_TIME_PATTERN), patternsForStop); + } } diff --git a/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java b/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java index e045f49c01d..85a33281f81 100644 --- a/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java +++ b/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java @@ -121,6 +121,7 @@ public SpeedTest( List.of(), null, TestServerContext.createStreetLimitationParametersService(), + null, null ); // Creating transitLayerForRaptor should be integrated into the TransitModel, but for now diff --git a/src/test/java/org/opentripplanner/updater/spi/UpdateResultAssertions.java b/src/test/java/org/opentripplanner/updater/spi/UpdateResultAssertions.java new file mode 100644 index 00000000000..38468744886 --- /dev/null +++ b/src/test/java/org/opentripplanner/updater/spi/UpdateResultAssertions.java @@ -0,0 +1,23 @@ +package org.opentripplanner.updater.spi; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Set; + +public class UpdateResultAssertions { + + public static void assertFailure(UpdateError.UpdateErrorType expectedError, UpdateResult result) { + assertEquals(Set.of(expectedError), result.failures().keySet()); + } + + public static void assertSuccess(UpdateResult updateResult) { + var errorCodes = updateResult.failures().keySet(); + assertEquals( + Set.of(), + errorCodes, + "Update result should have no error codes but had %s".formatted(errorCodes) + ); + assertTrue(updateResult.successful() > 0); + } +} diff --git a/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java b/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java index d557aa1319b..bf6f743eac7 100644 --- a/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java +++ b/src/test/java/org/opentripplanner/updater/trip/RealtimeTestEnvironment.java @@ -15,6 +15,7 @@ import org.opentripplanner.ext.siri.SiriFuzzyTripMatcher; import org.opentripplanner.ext.siri.SiriTimetableSnapshotSource; import org.opentripplanner.ext.siri.updater.EstimatedTimetableHandler; +import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.model.StopTime; import org.opentripplanner.model.TimetableSnapshot; @@ -55,16 +56,19 @@ public final class RealtimeTestEnvironment { ); public static final LocalDate SERVICE_DATE = LocalDate.of(2024, 5, 8); public static final FeedScopedId SERVICE_ID = TransitModelForTest.id("CAL_1"); + public static final String STOP_A1_ID = "A1"; + public static final String STOP_B1_ID = "B1"; + public static final String STOP_C1_ID = "C1"; private final TransitModelForTest testModel = TransitModelForTest.of(); public final ZoneId timeZone = ZoneId.of(TransitModelForTest.TIME_ZONE_ID); public final Station stationA = testModel.station("A").build(); public final Station stationB = testModel.station("B").build(); public final Station stationC = testModel.station("C").build(); public final Station stationD = testModel.station("D").build(); - public final RegularStop stopA1 = testModel.stop("A1").withParentStation(stationA).build(); - public final RegularStop stopB1 = testModel.stop("B1").withParentStation(stationB).build(); + public final RegularStop stopA1 = testModel.stop(STOP_A1_ID).withParentStation(stationA).build(); + public final RegularStop stopB1 = testModel.stop(STOP_B1_ID).withParentStation(stationB).build(); public final RegularStop stopB2 = testModel.stop("B2").withParentStation(stationB).build(); - public final RegularStop stopC1 = testModel.stop("C1").withParentStation(stationC).build(); + public final RegularStop stopC1 = testModel.stop(STOP_C1_ID).withParentStation(stationC).build(); public final RegularStop stopD1 = testModel.stop("D1").withParentStation(stationD).build(); public final StopModel stopModel = testModel .stopModelBuilder() @@ -110,12 +114,20 @@ private RealtimeTestEnvironment(SourceType sourceType) { Route route1 = TransitModelForTest.route(route1Id).build(); trip1 = - createTrip("TestTrip1", route1, List.of(new Stop(stopA1, 10, 11), new Stop(stopB1, 20, 21))); + createTrip( + "TestTrip1", + route1, + List.of(new StopCall(stopA1, 10, 11), new StopCall(stopB1, 20, 21)) + ); trip2 = createTrip( "TestTrip2", route1, - List.of(new Stop(stopA1, 60, 61), new Stop(stopB1, 70, 71), new Stop(stopC1, 80, 81)) + List.of( + new StopCall(stopA1, 60, 61), + new StopCall(stopB1, 70, 71), + new StopCall(stopC1, 80, 81) + ) ); CalendarServiceData calendarServiceData = new CalendarServiceData(); @@ -205,7 +217,7 @@ public DateTimeHelper getDateTimeHelper() { } public TripPattern getPatternForTrip(Trip trip) { - return transitModel.getTransitModelIndex().getPatternForTrip().get(trip); + return getTransitService().getPatternForTrip(trip); } public TimetableSnapshot getTimetableSnapshot() { @@ -272,13 +284,15 @@ public UpdateResult applyTripUpdates( UpdateIncrementality incrementality ) { Objects.requireNonNull(gtfsSource, "Test environment is configured for SIRI only"); - return gtfsSource.applyTripUpdates( + UpdateResult updateResult = gtfsSource.applyTripUpdates( null, BackwardsDelayPropagationType.REQUIRED_NO_DATA, incrementality, updates, getFeedId() ); + commitTimetableSnapshot(); + return updateResult; } // private methods @@ -288,11 +302,28 @@ private UpdateResult applyEstimatedTimetable( boolean fuzzyMatching ) { Objects.requireNonNull(siriSource, "Test environment is configured for GTFS-RT only"); - return getEstimatedTimetableHandler(fuzzyMatching).applyUpdate(updates, DIFFERENTIAL); + UpdateResult updateResult = getEstimatedTimetableHandler(fuzzyMatching) + .applyUpdate(updates, DIFFERENTIAL); + commitTimetableSnapshot(); + return updateResult; } - private Trip createTrip(String id, Route route, List stops) { - var trip = Trip.of(id(id)).withRoute(route).withServiceId(SERVICE_ID).build(); + private void commitTimetableSnapshot() { + if (siriSource != null) { + siriSource.flushBuffer(); + } + if (gtfsSource != null) { + 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()) @@ -314,7 +345,7 @@ private Trip createTrip(String id, Route route, List stops) { final TripPattern pattern = TransitModelForTest .tripPattern(id + "Pattern", route) - .withStopPattern(TransitModelForTest.stopPattern(stops.stream().map(Stop::stop).toList())) + .withStopPattern(TransitModelForTest.stopPattern(stops.stream().map(StopCall::stop).toList())) .build(); pattern.add(tripTimes); @@ -339,5 +370,5 @@ private StopTime createStopTime( return st; } - protected record Stop(RegularStop stop, int arrivalTime, int departureTime) {} + private record StopCall(RegularStop stop, int arrivalTime, int departureTime) {} } diff --git a/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotManagerTest.java b/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotManagerTest.java index 8f5c278b586..384e8a41fd1 100644 --- a/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotManagerTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotManagerTest.java @@ -63,19 +63,16 @@ SameAssert not() { static Stream purgeExpiredDataTestCases() { return Stream.of( - // purgeExpiredData maxSnapshotFrequency || snapshots PatternSnapshotA PatternSnapshotB - Arguments.of(Boolean.TRUE, -1, NotSame, NotSame), - Arguments.of(Boolean.FALSE, -1, NotSame, Same), - Arguments.of(Boolean.TRUE, 1000, NotSame, NotSame), - Arguments.of(Boolean.FALSE, 1000, Same, Same) + // purgeExpiredData || snapshots PatternSnapshotA PatternSnapshotB + Arguments.of(Boolean.TRUE, NotSame, NotSame), + Arguments.of(Boolean.FALSE, NotSame, Same) ); } - @ParameterizedTest(name = "purgeExpired: {0}, maxFrequency: {1} || {2} {3}") + @ParameterizedTest(name = "purgeExpired: {0} || {1} {2}") @MethodSource("purgeExpiredDataTestCases") public void testPurgeExpiredData( boolean purgeExpiredData, - int maxSnapshotFrequency, SameAssert expSnapshots, SameAssert expPatternAeqB ) { @@ -85,9 +82,7 @@ public void testPurgeExpiredData( var snapshotManager = new TimetableSnapshotManager( null, - TimetableSnapshotSourceParameters.DEFAULT - .withPurgeExpiredData(purgeExpiredData) - .withMaxSnapshotFrequency(Duration.ofMillis(maxSnapshotFrequency)), + TimetableSnapshotSourceParameters.DEFAULT.withPurgeExpiredData(purgeExpiredData), clock::get ); diff --git a/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java b/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java index 1dad90b416b..e53ba07cbcf 100644 --- a/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java @@ -1,8 +1,6 @@ package org.opentripplanner.updater.trip; -import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.ADDED; import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.CANCELED; -import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED; import static com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship.SKIPPED; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -18,33 +16,28 @@ import com.google.transit.realtime.GtfsRealtime.TripUpdate; import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent; import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate; -import de.mfdz.MfdzRealtimeExtensions.StopTimePropertiesExtension.DropOffPickupType; import java.time.Duration; import java.time.LocalDate; import java.util.List; -import java.util.stream.Stream; import javax.annotation.Nonnull; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.opentripplanner.ConstantsForTests; import org.opentripplanner.TestOtpModel; import org.opentripplanner._support.time.ZoneIds; -import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.framework.time.ServiceDateUtils; -import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TimetableSnapshot; -import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.RealTimeState; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.updater.GtfsRealtimeFuzzyTripMatcher; import org.opentripplanner.updater.TimetableSnapshotSourceParameters; -import org.opentripplanner.updater.spi.UpdateSuccess.WarningType; public class TimetableSnapshotSourceTest { @@ -57,6 +50,7 @@ public class TimetableSnapshotSourceTest { ) .build(); private TransitModel transitModel; + private TransitService transitService; private final GtfsRealtimeFuzzyTripMatcher TRIP_MATCHER_NOOP = null; @@ -66,8 +60,9 @@ public class TimetableSnapshotSourceTest { public void setUp() { TestOtpModel model = ConstantsForTests.buildGtfsGraph(ConstantsForTests.SIMPLE_GTFS); transitModel = model.transitModel(); + transitService = new DefaultTransitService(transitModel); - feedId = transitModel.getFeedIds().stream().findFirst().get(); + feedId = transitService.getFeedIds().stream().findFirst().get(); } @Test @@ -87,63 +82,6 @@ public void testGetSnapshot() { assertSame(snapshot, updater.getTimetableSnapshot()); } - @Test - public void testGetSnapshotWithMaxSnapshotFrequencyCleared() { - var updater = new TimetableSnapshotSource( - TimetableSnapshotSourceParameters.DEFAULT.withMaxSnapshotFrequency(Duration.ofMillis(-1)), - transitModel - ); - - final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); - - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(CANCELLATION), - feedId - ); - - final TimetableSnapshot newSnapshot = updater.getTimetableSnapshot(); - assertNotNull(newSnapshot); - assertNotSame(snapshot, newSnapshot); - } - - /** - * This test just asserts that invalid trip ids don't throw an exception and are ignored instead - */ - @Test - public void invalidTripId() { - var updater = new TimetableSnapshotSource( - TimetableSnapshotSourceParameters.DEFAULT, - transitModel - ); - - Stream - .of("", null) - .forEach(id -> { - var tripDescriptorBuilder = TripDescriptor.newBuilder(); - tripDescriptorBuilder.setTripId(""); - tripDescriptorBuilder.setScheduleRelationship( - TripDescriptor.ScheduleRelationship.SCHEDULED - ); - var tripUpdateBuilder = TripUpdate.newBuilder(); - - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - var tripUpdate = tripUpdateBuilder.build(); - - var result = updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - - assertEquals(0, result.successful()); - }); - } - @Test public void testHandleModifiedTrip() { // GIVEN @@ -159,7 +97,7 @@ public void testHandleModifiedTrip() { tripDescriptorBuilder.setStartDate(ServiceDateUtils.asCompactString(SERVICE_DATE)); final long midnightSecondsSinceEpoch = ServiceDateUtils - .asStartOfService(SERVICE_DATE, transitModel.getTimeZone()) + .asStartOfService(SERVICE_DATE, transitService.getTimeZone()) .toEpochSecond(); final TripUpdate.Builder tripUpdateBuilder = TripUpdate.newBuilder(); @@ -261,6 +199,7 @@ public void testHandleModifiedTrip() { List.of(tripUpdate), feedId ); + updater.flushBuffer(); // THEN final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); @@ -268,11 +207,8 @@ public void testHandleModifiedTrip() { // Original trip pattern { final FeedScopedId tripId = new FeedScopedId(feedId, modifiedTripId); - final Trip trip = transitModel.getTransitModelIndex().getTripForId().get(tripId); - final TripPattern originalTripPattern = transitModel - .getTransitModelIndex() - .getPatternForTrip() - .get(trip); + final Trip trip = transitService.getTripForId(tripId); + final TripPattern originalTripPattern = transitService.getPatternForTrip(trip); final Timetable originalTimetableForToday = snapshot.resolve( originalTripPattern, @@ -346,430 +282,6 @@ public void testHandleModifiedTrip() { } } - @Nested - class Scheduled { - - @Test - public void scheduled() { - // GIVEN - - String scheduledTripId = "1.1"; - - var builder = new TripUpdateBuilder( - scheduledTripId, - SERVICE_DATE, - SCHEDULED, - transitModel.getTimeZone() - ) - .addDelayedStopTime(1, 0) - .addDelayedStopTime(2, 60, 80) - .addDelayedStopTime(3, 90, 90); - - var tripUpdate = builder.build(); - - var updater = defaultUpdater(); - - // WHEN - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - - // THEN - final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); - - final FeedScopedId tripId = new FeedScopedId(feedId, scheduledTripId); - final Trip trip = transitModel.getTransitModelIndex().getTripForId().get(tripId); - final TripPattern originalTripPattern = transitModel - .getTransitModelIndex() - .getPatternForTrip() - .get(trip); - - final Timetable originalTimetableForToday = snapshot.resolve( - originalTripPattern, - SERVICE_DATE - ); - final Timetable originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); - - assertNotSame(originalTimetableForToday, originalTimetableScheduled); - - final int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(tripId); - assertTrue( - originalTripIndexScheduled > -1, - "Original trip should be found in scheduled time table" - ); - final TripTimes originalTripTimesScheduled = originalTimetableScheduled.getTripTimes( - originalTripIndexScheduled - ); - assertFalse( - originalTripTimesScheduled.isCanceledOrDeleted(), - "Original trip times should not be canceled in scheduled time table" - ); - assertEquals(RealTimeState.SCHEDULED, originalTripTimesScheduled.getRealTimeState()); - - final int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); - assertTrue( - originalTripIndexForToday > -1, - "Original trip should be found in time table for service date" - ); - final TripTimes originalTripTimesForToday = originalTimetableForToday.getTripTimes( - originalTripIndexForToday - ); - assertEquals(RealTimeState.UPDATED, originalTripTimesForToday.getRealTimeState()); - assertEquals(0, originalTripTimesForToday.getArrivalDelay(0)); - assertEquals(0, originalTripTimesForToday.getDepartureDelay(0)); - assertEquals(60, originalTripTimesForToday.getArrivalDelay(1)); - assertEquals(80, originalTripTimesForToday.getDepartureDelay(1)); - assertEquals(90, originalTripTimesForToday.getArrivalDelay(2)); - assertEquals(90, originalTripTimesForToday.getDepartureDelay(2)); - } - - @Test - public void scheduledTripWithSkippedAndNoData() { - // GIVEN - - String scheduledTripId = "1.1"; - - var builder = new TripUpdateBuilder( - scheduledTripId, - SERVICE_DATE, - SCHEDULED, - transitModel.getTimeZone() - ) - .addNoDataStop(1) - .addSkippedStop(2) - .addNoDataStop(3); - - var tripUpdate = builder.build(); - - var updater = defaultUpdater(); - - // WHEN - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - - // THEN - final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); - - // Original trip pattern - { - final FeedScopedId tripId = new FeedScopedId(feedId, scheduledTripId); - final Trip trip = transitModel.getTransitModelIndex().getTripForId().get(tripId); - final TripPattern originalTripPattern = transitModel - .getTransitModelIndex() - .getPatternForTrip() - .get(trip); - - final Timetable originalTimetableForToday = snapshot.resolve( - originalTripPattern, - SERVICE_DATE - ); - final Timetable originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); - - assertNotSame(originalTimetableForToday, originalTimetableScheduled); - - final int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(tripId); - assertTrue( - originalTripIndexScheduled > -1, - "Original trip should be found in scheduled time table" - ); - final TripTimes originalTripTimesScheduled = originalTimetableScheduled.getTripTimes( - originalTripIndexScheduled - ); - assertFalse( - originalTripTimesScheduled.isCanceledOrDeleted(), - "Original trip times should not be canceled in scheduled time table" - ); - assertEquals(RealTimeState.SCHEDULED, originalTripTimesScheduled.getRealTimeState()); - - final int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); - assertTrue( - originalTripIndexForToday > -1, - "Original trip should be found in time table for service date" - ); - final TripTimes originalTripTimesForToday = originalTimetableForToday.getTripTimes( - originalTripIndexForToday - ); - assertTrue( - originalTripTimesForToday.isDeleted(), - "Original trip times should be deleted in time table for service date" - ); - // original trip should be deleted - assertEquals(RealTimeState.DELETED, originalTripTimesForToday.getRealTimeState()); - } - - // New trip pattern - { - final TripPattern newTripPattern = snapshot.getRealtimeAddedTripPattern( - new FeedScopedId(feedId, scheduledTripId), - SERVICE_DATE - ); - assertNotNull(newTripPattern, "New trip pattern should be found"); - - final Timetable newTimetableForToday = snapshot.resolve(newTripPattern, SERVICE_DATE); - final Timetable newTimetableScheduled = snapshot.resolve(newTripPattern, null); - - assertNotSame(newTimetableForToday, newTimetableScheduled); - - assertTrue(newTripPattern.canBoard(0)); - assertFalse(newTripPattern.canBoard(1)); - assertTrue(newTripPattern.canBoard(2)); - - assertEquals(new NonLocalizedString("foo"), newTripPattern.getTripHeadsign()); - assertEquals( - newTripPattern.getOriginalTripPattern().getTripHeadsign(), - newTripPattern.getTripHeadsign() - ); - - final int newTimetableForTodayModifiedTripIndex = newTimetableForToday.getTripIndex( - scheduledTripId - ); - assertTrue( - newTimetableForTodayModifiedTripIndex > -1, - "New trip should be found in time table for service date" - ); - - var newTripTimes = newTimetableForToday.getTripTimes(newTimetableForTodayModifiedTripIndex); - assertEquals(RealTimeState.UPDATED, newTripTimes.getRealTimeState()); - - assertEquals( - -1, - newTimetableScheduled.getTripIndex(scheduledTripId), - "New trip should not be found in scheduled time table" - ); - - assertEquals(0, newTripTimes.getArrivalDelay(0)); - assertEquals(0, newTripTimes.getDepartureDelay(0)); - assertEquals(0, newTripTimes.getArrivalDelay(1)); - assertEquals(0, newTripTimes.getDepartureDelay(1)); - assertEquals(0, newTripTimes.getArrivalDelay(2)); - assertEquals(0, newTripTimes.getDepartureDelay(2)); - assertTrue(newTripTimes.isNoDataStop(0)); - assertTrue(newTripTimes.isCancelledStop(1)); - assertTrue(newTripTimes.isNoDataStop(2)); - } - } - } - - @Nested - class Added { - - final String addedTripId = "added_trip"; - - @Test - public void addedTrip() { - var builder = new TripUpdateBuilder( - addedTripId, - SERVICE_DATE, - ADDED, - transitModel.getTimeZone() - ); - - builder.addStopTime("A", 30).addStopTime("C", 40).addStopTime("E", 55); - - var tripUpdate = builder.build(); - - var updater = defaultUpdater(); - - // WHEN - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - - // THEN - assertAddedTrip(SERVICE_DATE, this.addedTripId, updater); - } - - private TripPattern assertAddedTrip( - LocalDate serviceDate, - String tripId, - TimetableSnapshotSource updater - ) { - var stopA = transitModel.getStopModel().getRegularStop(new FeedScopedId(feedId, "A")); - // Get the trip pattern of the added trip which goes through stopA - var snapshot = updater.getTimetableSnapshot(); - var patternsAtA = snapshot.getPatternsForStop(stopA); - - assertNotNull(patternsAtA, "Added trip pattern should be found"); - assertEquals(1, patternsAtA.size()); - var tripPattern = patternsAtA.stream().findFirst().get(); - - final Timetable forToday = snapshot.resolve(tripPattern, serviceDate); - final Timetable schedule = snapshot.resolve(tripPattern, null); - - assertNotSame(forToday, schedule); - - final int forTodayAddedTripIndex = forToday.getTripIndex(tripId); - assertTrue( - forTodayAddedTripIndex > -1, - "Added trip should be found in time table for service date" - ); - assertEquals( - RealTimeState.ADDED, - forToday.getTripTimes(forTodayAddedTripIndex).getRealTimeState() - ); - - final int scheduleTripIndex = schedule.getTripIndex(tripId); - assertEquals(-1, scheduleTripIndex, "Added trip should not be found in scheduled time table"); - return tripPattern; - } - - @Test - public void addedTripWithNewRoute() { - // GIVEN - - final var builder = new TripUpdateBuilder( - addedTripId, - SERVICE_DATE, - ADDED, - transitModel.getTimeZone() - ); - // add extension to set route name, url, mode - builder.addTripExtension(); - - builder - .addStopTime("A", 30, DropOffPickupType.PHONE_AGENCY) - .addStopTime("C", 40, DropOffPickupType.COORDINATE_WITH_DRIVER) - .addStopTime("E", 55, DropOffPickupType.NONE); - - var tripUpdate = builder.build(); - - var updater = defaultUpdater(); - - // WHEN - var result = updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - - // THEN - - assertTrue(result.warnings().isEmpty()); - - var pattern = assertAddedTrip(SERVICE_DATE, addedTripId, updater); - - var route = pattern.getRoute(); - assertEquals(TripUpdateBuilder.ROUTE_URL, route.getUrl()); - assertEquals(TripUpdateBuilder.ROUTE_NAME, route.getName()); - assertEquals(TransitMode.RAIL, route.getMode()); - - var fromTransitModel = transitModel.getTransitModelIndex().getRouteForId(route.getId()); - assertEquals(fromTransitModel, route); - - assertEquals(PickDrop.CALL_AGENCY, pattern.getBoardType(0)); - assertEquals(PickDrop.CALL_AGENCY, pattern.getAlightType(0)); - - assertEquals(PickDrop.COORDINATE_WITH_DRIVER, pattern.getBoardType(1)); - assertEquals(PickDrop.COORDINATE_WITH_DRIVER, pattern.getAlightType(1)); - } - - @Test - public void addedWithUnknownStop() { - // GIVEN - final var builder = new TripUpdateBuilder( - addedTripId, - SERVICE_DATE, - ADDED, - transitModel.getTimeZone() - ); - // add extension to set route name, url, mode - builder.addTripExtension(); - - builder - .addStopTime("A", 30, DropOffPickupType.PHONE_AGENCY) - .addStopTime("UNKNOWN_STOP_ID", 40, DropOffPickupType.COORDINATE_WITH_DRIVER) - .addStopTime("E", 55, DropOffPickupType.NONE); - - var tripUpdate = builder.build(); - - var updater = defaultUpdater(); - - // WHEN - var result = updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - - // THEN - - assertFalse(result.warnings().isEmpty()); - - assertEquals(List.of(WarningType.UNKNOWN_STOPS_REMOVED_FROM_ADDED_TRIP), result.warnings()); - - var pattern = assertAddedTrip(SERVICE_DATE, addedTripId, updater); - - assertEquals(2, pattern.getStops().size()); - } - - @Test - public void repeatedlyAddedTripWithNewRoute() { - // GIVEN - - final var builder = new TripUpdateBuilder( - addedTripId, - SERVICE_DATE, - ADDED, - transitModel.getTimeZone() - ); - // add extension to set route name, url, mode - builder.addTripExtension(); - - builder - .addStopTime("A", 30, DropOffPickupType.PHONE_AGENCY) - .addStopTime("C", 40, DropOffPickupType.COORDINATE_WITH_DRIVER) - .addStopTime("E", 55, DropOffPickupType.NONE); - - var tripUpdate = builder.build(); - - var updater = defaultUpdater(); - - // WHEN - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - var pattern = assertAddedTrip(SERVICE_DATE, addedTripId, updater); - var firstRoute = pattern.getRoute(); - - // apply the update a second time to check that no new route instance is created but the old one is reused - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdate), - feedId - ); - var secondPattern = assertAddedTrip(SERVICE_DATE, addedTripId, updater); - var secondRoute = secondPattern.getRoute(); - - // THEN - - assertSame(firstRoute, secondRoute); - assertNotNull(transitModel.getTransitModelIndex().getRouteForId(firstRoute.getId())); - } - } - @Nonnull private TimetableSnapshotSource defaultUpdater() { return new TimetableSnapshotSource( diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/addition/AddedTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/addition/AddedTest.java new file mode 100644 index 00000000000..8371c5dda3a --- /dev/null +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/addition/AddedTest.java @@ -0,0 +1,152 @@ +package org.opentripplanner.updater.trip.moduletests.addition; + +import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.ADDED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess; +import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.SERVICE_DATE; +import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.STOP_A1_ID; +import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.STOP_B1_ID; +import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.STOP_C1_ID; + +import de.mfdz.MfdzRealtimeExtensions.StopTimePropertiesExtension.DropOffPickupType; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.model.PickDrop; +import org.opentripplanner.transit.model.basic.TransitMode; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.RealTimeState; +import org.opentripplanner.updater.spi.UpdateSuccess; +import org.opentripplanner.updater.trip.RealtimeTestEnvironment; +import org.opentripplanner.updater.trip.TripUpdateBuilder; + +class AddedTest { + + final String ADDED_TRIP_ID = "added_trip"; + + @Test + void addedTrip() { + var env = RealtimeTestEnvironment.gtfs(); + + var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, env.timeZone) + .addStopTime(STOP_A1_ID, 30) + .addStopTime(STOP_B1_ID, 40) + .addStopTime(STOP_C1_ID, 55) + .build(); + + assertSuccess(env.applyTripUpdate(tripUpdate)); + assertAddedTrip(this.ADDED_TRIP_ID, env); + } + + @Test + void addedTripWithNewRoute() { + var env = RealtimeTestEnvironment.gtfs(); + var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, env.timeZone) + .addTripExtension() + .addStopTime(STOP_A1_ID, 30, DropOffPickupType.PHONE_AGENCY) + .addStopTime(STOP_B1_ID, 40, DropOffPickupType.COORDINATE_WITH_DRIVER) + .addStopTime(STOP_B1_ID, 55, DropOffPickupType.NONE) + .build(); + + var result = env.applyTripUpdate(tripUpdate); + assertSuccess(result); + assertTrue(result.warnings().isEmpty()); + + var pattern = assertAddedTrip(ADDED_TRIP_ID, env); + + var route = pattern.getRoute(); + assertEquals(TripUpdateBuilder.ROUTE_URL, route.getUrl()); + assertEquals(TripUpdateBuilder.ROUTE_NAME, route.getName()); + assertEquals(TransitMode.RAIL, route.getMode()); + + var fromTransitModel = env.getTransitService().getRouteForId(route.getId()); + assertEquals(fromTransitModel, route); + + assertEquals(PickDrop.CALL_AGENCY, pattern.getBoardType(0)); + assertEquals(PickDrop.CALL_AGENCY, pattern.getAlightType(0)); + + assertEquals(PickDrop.COORDINATE_WITH_DRIVER, pattern.getBoardType(1)); + assertEquals(PickDrop.COORDINATE_WITH_DRIVER, pattern.getAlightType(1)); + } + + @Test + void addedWithUnknownStop() { + var env = RealtimeTestEnvironment.gtfs(); + var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, env.timeZone) + // add extension to set route name, url, mode + .addTripExtension() + .addStopTime(STOP_A1_ID, 30, DropOffPickupType.PHONE_AGENCY) + .addStopTime("UNKNOWN_STOP_ID", 40, DropOffPickupType.COORDINATE_WITH_DRIVER) + .addStopTime(STOP_C1_ID, 55, DropOffPickupType.NONE) + .build(); + + var result = env.applyTripUpdate(tripUpdate); + assertSuccess(result); + + assertEquals( + List.of(UpdateSuccess.WarningType.UNKNOWN_STOPS_REMOVED_FROM_ADDED_TRIP), + result.warnings() + ); + + var pattern = assertAddedTrip(ADDED_TRIP_ID, env); + + assertEquals(2, pattern.getStops().size()); + } + + @Test + void repeatedlyAddedTripWithNewRoute() { + var env = RealtimeTestEnvironment.gtfs(); + var tripUpdate = new TripUpdateBuilder(ADDED_TRIP_ID, SERVICE_DATE, ADDED, env.timeZone) + // add extension to set route name, url, mode + .addTripExtension() + .addStopTime(STOP_A1_ID, 30, DropOffPickupType.PHONE_AGENCY) + .addStopTime(STOP_B1_ID, 40, DropOffPickupType.COORDINATE_WITH_DRIVER) + .addStopTime(STOP_C1_ID, 55, DropOffPickupType.NONE) + .build(); + + assertSuccess(env.applyTripUpdate(tripUpdate)); + var pattern = assertAddedTrip(ADDED_TRIP_ID, env); + var firstRoute = pattern.getRoute(); + + // apply the update a second time to check that no new route instance is created but the old one is reused + env.applyTripUpdate(tripUpdate); + var secondPattern = assertAddedTrip(ADDED_TRIP_ID, env); + var secondRoute = secondPattern.getRoute(); + + assertSame(firstRoute, secondRoute); + assertNotNull(env.getTransitService().getRouteForId(firstRoute.getId())); + } + + private TripPattern assertAddedTrip(String tripId, RealtimeTestEnvironment env) { + var snapshot = env.getTimetableSnapshot(); + var stopA = env.transitModel.getStopModel().getRegularStop(env.stopA1.getId()); + // Get the trip pattern of the added trip which goes through stopA + var patternsAtA = env.getTimetableSnapshot().getPatternsForStop(stopA); + + assertNotNull(patternsAtA, "Added trip pattern should be found"); + assertEquals(1, patternsAtA.size()); + var tripPattern = patternsAtA.stream().findFirst().get(); + + var forToday = snapshot.resolve(tripPattern, SERVICE_DATE); + var schedule = snapshot.resolve(tripPattern, null); + + assertNotSame(forToday, schedule); + + final int forTodayAddedTripIndex = forToday.getTripIndex(tripId); + assertTrue( + forTodayAddedTripIndex > -1, + "Added trip should be found in time table for service date" + ); + assertEquals( + RealTimeState.ADDED, + forToday.getTripTimes(forTodayAddedTripIndex).getRealTimeState() + ); + + final int scheduleTripIndex = schedule.getTripIndex(tripId); + assertEquals(-1, scheduleTripIndex, "Added trip should not be found in scheduled time table"); + return tripPattern; + } +} diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java index b2c31e2254e..c85225b7828 100644 --- a/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CancellationDeletionTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess; import static org.opentripplanner.updater.trip.UpdateIncrementality.DIFFERENTIAL; import com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship; @@ -11,8 +12,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.opentripplanner.model.Timetable; -import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.transit.model.timetable.RealTimeState; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; import org.opentripplanner.updater.trip.TripUpdateBuilder; @@ -32,7 +31,7 @@ static List cases() { @ParameterizedTest @MethodSource("cases") - public void cancelledTrip(ScheduleRelationship relationship, RealTimeState state) { + void cancelledTrip(ScheduleRelationship relationship, RealTimeState state) { var env = RealtimeTestEnvironment.gtfs(); var pattern1 = env.getPatternForTrip(env.trip1); @@ -45,13 +44,11 @@ public void cancelledTrip(ScheduleRelationship relationship, RealTimeState state env.timeZone ) .build(); - var result = env.applyTripUpdate(update); + assertSuccess(env.applyTripUpdate(update)); - assertEquals(1, result.successful()); - - final TimetableSnapshot snapshot = env.getTimetableSnapshot(); - final Timetable forToday = snapshot.resolve(pattern1, RealtimeTestEnvironment.SERVICE_DATE); - final Timetable schedule = snapshot.resolve(pattern1, null); + var snapshot = env.getTimetableSnapshot(); + var forToday = snapshot.resolve(pattern1, RealtimeTestEnvironment.SERVICE_DATE); + var schedule = snapshot.resolve(pattern1, null); assertNotSame(forToday, schedule); assertNotSame(forToday.getTripTimes(tripIndex1), schedule.getTripTimes(tripIndex1)); @@ -73,7 +70,7 @@ public void cancelledTrip(ScheduleRelationship relationship, RealTimeState state */ @ParameterizedTest @MethodSource("cases") - public void cancelingAddedTrip(ScheduleRelationship relationship, RealTimeState state) { + void cancelingAddedTrip(ScheduleRelationship relationship, RealTimeState state) { var env = RealtimeTestEnvironment.gtfs(); var addedTripId = "added-trip"; // First add ADDED trip @@ -88,9 +85,7 @@ public void cancelingAddedTrip(ScheduleRelationship relationship, RealTimeState .addStopTime(env.stopC1.getId().getId(), 55) .build(); - var result = env.applyTripUpdate(update, DIFFERENTIAL); - - assertEquals(1, result.successful()); + assertSuccess(env.applyTripUpdate(update, DIFFERENTIAL)); // Cancel or delete the added trip update = @@ -101,19 +96,17 @@ public void cancelingAddedTrip(ScheduleRelationship relationship, RealTimeState env.timeZone ) .build(); - result = env.applyTripUpdate(update, DIFFERENTIAL); - - assertEquals(1, result.successful()); + assertSuccess(env.applyTripUpdate(update, DIFFERENTIAL)); - final TimetableSnapshot snapshot = env.getTimetableSnapshot(); + var snapshot = env.getTimetableSnapshot(); // Get the trip pattern of the added trip which goes through stopA var patternsAtA = snapshot.getPatternsForStop(env.stopA1); assertNotNull(patternsAtA, "Added trip pattern should be found"); var tripPattern = patternsAtA.stream().findFirst().get(); - final Timetable forToday = snapshot.resolve(tripPattern, RealtimeTestEnvironment.SERVICE_DATE); - final Timetable schedule = snapshot.resolve(tripPattern, null); + var forToday = snapshot.resolve(tripPattern, RealtimeTestEnvironment.SERVICE_DATE); + var schedule = snapshot.resolve(tripPattern, null); assertNotSame(forToday, schedule); diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java index 4cfe1e5500d..5298853f36d 100644 --- a/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/DelayedTest.java @@ -2,26 +2,29 @@ import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess; +import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.SERVICE_DATE; import org.junit.jupiter.api.Test; -import org.opentripplanner.model.Timetable; -import org.opentripplanner.model.TimetableSnapshot; +import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.RealTimeState; +import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; import org.opentripplanner.updater.trip.TripUpdateBuilder; /** * Delays should be applied to the first trip but should leave the second trip untouched. */ -public class DelayedTest { +class DelayedTest { private static final int DELAY = 1; private static final int STOP_SEQUENCE = 1; @Test - public void delayed() { + void singleStopDelay() { var env = RealtimeTestEnvironment.gtfs(); var tripUpdate = new TripUpdateBuilder( @@ -37,52 +40,86 @@ public void delayed() { assertEquals(1, result.successful()); - // trip1 should be modified - { - var pattern1 = env.getPatternForTrip(env.trip1); - final int trip1Index = pattern1.getScheduledTimetable().getTripIndex(env.trip1.getId()); - - final TimetableSnapshot snapshot = env.getTimetableSnapshot(); - final Timetable trip1Realtime = snapshot.resolve( - pattern1, - RealtimeTestEnvironment.SERVICE_DATE - ); - final Timetable trip1Scheduled = snapshot.resolve(pattern1, null); - - assertNotSame(trip1Realtime, trip1Scheduled); - assertNotSame( - trip1Realtime.getTripTimes(trip1Index), - trip1Scheduled.getTripTimes(trip1Index) - ); - assertEquals(1, trip1Realtime.getTripTimes(trip1Index).getArrivalDelay(STOP_SEQUENCE)); - assertEquals(1, trip1Realtime.getTripTimes(trip1Index).getDepartureDelay(STOP_SEQUENCE)); - - assertEquals( - RealTimeState.SCHEDULED, - trip1Scheduled.getTripTimes(trip1Index).getRealTimeState() - ); - assertEquals( - RealTimeState.UPDATED, - trip1Realtime.getTripTimes(trip1Index).getRealTimeState() - ); - } - - // trip2 should keep the scheduled information - { - var pattern = env.getPatternForTrip(env.trip2); - final int tripIndex = pattern.getScheduledTimetable().getTripIndex(env.trip2.getId()); - - final TimetableSnapshot snapshot = env.getTimetableSnapshot(); - final Timetable realtime = snapshot.resolve(pattern, RealtimeTestEnvironment.SERVICE_DATE); - final Timetable scheduled = snapshot.resolve(pattern, null); - - assertSame(realtime, scheduled); - assertSame(realtime.getTripTimes(tripIndex), scheduled.getTripTimes(tripIndex)); - assertEquals(0, realtime.getTripTimes(tripIndex).getArrivalDelay(STOP_SEQUENCE)); - assertEquals(0, realtime.getTripTimes(tripIndex).getDepartureDelay(STOP_SEQUENCE)); - - assertEquals(RealTimeState.SCHEDULED, scheduled.getTripTimes(tripIndex).getRealTimeState()); - assertEquals(RealTimeState.SCHEDULED, realtime.getTripTimes(tripIndex).getRealTimeState()); - } + var pattern1 = env.getPatternForTrip(env.trip1); + int trip1Index = pattern1.getScheduledTimetable().getTripIndex(env.trip1.getId()); + + var snapshot = env.getTimetableSnapshot(); + var trip1Realtime = snapshot.resolve(pattern1, RealtimeTestEnvironment.SERVICE_DATE); + var trip1Scheduled = snapshot.resolve(pattern1, null); + + assertNotSame(trip1Realtime, trip1Scheduled); + assertNotSame(trip1Realtime.getTripTimes(trip1Index), trip1Scheduled.getTripTimes(trip1Index)); + assertEquals(DELAY, trip1Realtime.getTripTimes(trip1Index).getArrivalDelay(STOP_SEQUENCE)); + assertEquals(DELAY, trip1Realtime.getTripTimes(trip1Index).getDepartureDelay(STOP_SEQUENCE)); + + assertEquals( + RealTimeState.SCHEDULED, + trip1Scheduled.getTripTimes(trip1Index).getRealTimeState() + ); + + assertEquals( + "SCHEDULED | A1 0:00:10 0:00:11 | B1 0:00:20 0:00:21", + env.getScheduledTimetable(env.trip1.getId()) + ); + assertEquals( + "UPDATED | A1 [ND] 0:00:10 0:00:11 | B1 0:00:21 0:00:22", + env.getRealtimeTimetable(env.trip1.getId().getId()) + ); + } + + /** + * Tests delays to multiple stop times, where arrival and departure do not have the same delay. + */ + @Test + void complexDelay() { + var env = RealtimeTestEnvironment.gtfs(); + + var tripId = env.trip2.getId().getId(); + + var tripUpdate = new TripUpdateBuilder(tripId, SERVICE_DATE, SCHEDULED, env.timeZone) + .addDelayedStopTime(0, 0) + .addDelayedStopTime(1, 60, 80) + .addDelayedStopTime(2, 90, 90) + .build(); + + assertSuccess(env.applyTripUpdate(tripUpdate)); + + var snapshot = env.getTimetableSnapshot(); + + final TripPattern originalTripPattern = env.getTransitService().getPatternForTrip(env.trip2); + + var originalTimetableForToday = snapshot.resolve(originalTripPattern, SERVICE_DATE); + var originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); + + assertNotSame(originalTimetableForToday, originalTimetableScheduled); + + final int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(tripId); + assertTrue( + originalTripIndexScheduled > -1, + "Original trip should be found in scheduled time table" + ); + final TripTimes originalTripTimesScheduled = originalTimetableScheduled.getTripTimes( + originalTripIndexScheduled + ); + assertFalse( + originalTripTimesScheduled.isCanceledOrDeleted(), + "Original trip times should not be canceled in scheduled time table" + ); + assertEquals(RealTimeState.SCHEDULED, originalTripTimesScheduled.getRealTimeState()); + + final int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); + assertTrue( + originalTripIndexForToday > -1, + "Original trip should be found in time table for service date" + ); + + assertEquals( + "SCHEDULED | A1 0:01 0:01:01 | B1 0:01:10 0:01:11 | C1 0:01:20 0:01:21", + env.getScheduledTimetable(env.trip2.getId()) + ); + assertEquals( + "UPDATED | A1 0:01 0:01:01 | B1 0:02:10 0:02:31 | C1 0:02:50 0:02:51", + env.getRealtimeTimetable(env.trip2) + ); } } diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java index 9afb7e76261..de699324bb6 100644 --- a/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/delay/SkippedTest.java @@ -6,16 +6,14 @@ import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess; +import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.SERVICE_DATE; import static org.opentripplanner.updater.trip.UpdateIncrementality.DIFFERENTIAL; import org.junit.jupiter.api.Test; -import org.opentripplanner.model.Timetable; -import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.RealTimeState; -import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesStringBuilder; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; import org.opentripplanner.updater.trip.TripUpdateBuilder; @@ -25,98 +23,26 @@ public class SkippedTest { @Test - public void scheduledTripWithSkippedAndScheduled() { + void scheduledTripWithSkippedAndScheduled() { var env = RealtimeTestEnvironment.gtfs(); String scheduledTripId = env.trip2.getId().getId(); - var tripUpdate = new TripUpdateBuilder( - scheduledTripId, - RealtimeTestEnvironment.SERVICE_DATE, - SCHEDULED, - env.timeZone - ) + var tripUpdate = new TripUpdateBuilder(scheduledTripId, SERVICE_DATE, SCHEDULED, env.timeZone) .addDelayedStopTime(0, 0) .addSkippedStop(1) .addDelayedStopTime(2, 90) .build(); - var result = env.applyTripUpdate(tripUpdate); - - assertEquals(1, result.successful()); - - final TimetableSnapshot snapshot = env.getTimetableSnapshot(); - - // Original trip pattern - { - final FeedScopedId tripId = env.trip2.getId(); - final Trip trip = env.transitModel.getTransitModelIndex().getTripForId().get(tripId); - final TripPattern originalTripPattern = env.transitModel - .getTransitModelIndex() - .getPatternForTrip() - .get(trip); - - final Timetable originalTimetableForToday = snapshot.resolve( - originalTripPattern, - RealtimeTestEnvironment.SERVICE_DATE - ); - final Timetable originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); - - assertNotSame(originalTimetableForToday, originalTimetableScheduled); - - final int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); - final TripTimes originalTripTimesForToday = originalTimetableForToday.getTripTimes( - originalTripIndexForToday - ); - assertTrue( - originalTripTimesForToday.isDeleted(), - "Original trip times should be deleted in time table for service date" - ); - // original trip should be canceled - assertEquals(RealTimeState.DELETED, originalTripTimesForToday.getRealTimeState()); - } - - // New trip pattern - { - final TripPattern newTripPattern = snapshot.getRealtimeAddedTripPattern( - env.trip2.getId(), - RealtimeTestEnvironment.SERVICE_DATE - ); - - final Timetable newTimetableForToday = snapshot.resolve( - newTripPattern, - RealtimeTestEnvironment.SERVICE_DATE - ); - final Timetable newTimetableScheduled = snapshot.resolve(newTripPattern, null); - - assertNotSame(newTimetableForToday, newTimetableScheduled); - - assertTrue(newTripPattern.canBoard(0)); - assertFalse(newTripPattern.canBoard(1)); - assertTrue(newTripPattern.canBoard(2)); - - final int newTimetableForTodayModifiedTripIndex = newTimetableForToday.getTripIndex( - scheduledTripId - ); - - var newTripTimes = newTimetableForToday.getTripTimes(newTimetableForTodayModifiedTripIndex); - assertEquals(RealTimeState.UPDATED, newTripTimes.getRealTimeState()); - - assertEquals( - -1, - newTimetableScheduled.getTripIndex(scheduledTripId), - "New trip should not be found in scheduled time table" - ); - - assertEquals(0, newTripTimes.getArrivalDelay(0)); - assertEquals(0, newTripTimes.getDepartureDelay(0)); - assertEquals(42, newTripTimes.getArrivalDelay(1)); - assertEquals(47, newTripTimes.getDepartureDelay(1)); - assertEquals(90, newTripTimes.getArrivalDelay(2)); - assertEquals(90, newTripTimes.getDepartureDelay(2)); - assertFalse(newTripTimes.isCancelledStop(0)); - assertTrue(newTripTimes.isCancelledStop(1)); - assertFalse(newTripTimes.isNoDataStop(2)); - } + assertSuccess(env.applyTripUpdate(tripUpdate)); + + assertOriginalTripPatternIsDeleted(env, env.trip2.getId()); + + assertNewTripTimesIsUpdated(env, env.trip2.getId()); + + assertEquals( + "UPDATED | A1 0:01 0:01:01 | B1 [C] 0:01:52 0:01:58 | C1 0:02:50 0:02:51", + env.getRealtimeTimetable(scheduledTripId) + ); } /** @@ -129,29 +55,22 @@ public void scheduledTripWithSkippedAndScheduled() { * the new stop-skipping trip pattern should also be removed. */ @Test - public void scheduledTripWithPreviouslySkipped() { + void scheduledTripWithPreviouslySkipped() { var env = RealtimeTestEnvironment.gtfs(); var tripId = env.trip2.getId(); - var tripUpdate = new TripUpdateBuilder( - tripId.getId(), - RealtimeTestEnvironment.SERVICE_DATE, - SCHEDULED, - env.timeZone - ) + var tripUpdate = new TripUpdateBuilder(tripId.getId(), SERVICE_DATE, SCHEDULED, env.timeZone) .addDelayedStopTime(0, 0) .addSkippedStop(1) .addDelayedStopTime(2, 90) .build(); - var result = env.applyTripUpdate(tripUpdate, DIFFERENTIAL); - - assertEquals(1, result.successful()); + assertSuccess(env.applyTripUpdate(tripUpdate, DIFFERENTIAL)); // Create update to the same trip but now the skipped stop is no longer skipped var scheduledBuilder = new TripUpdateBuilder( tripId.getId(), - RealtimeTestEnvironment.SERVICE_DATE, + SERVICE_DATE, SCHEDULED, env.timeZone ) @@ -162,64 +81,130 @@ public void scheduledTripWithPreviouslySkipped() { tripUpdate = scheduledBuilder.build(); // apply the update with the previously skipped stop now scheduled - result = env.applyTripUpdate(tripUpdate, DIFFERENTIAL); + assertSuccess(env.applyTripUpdate(tripUpdate, DIFFERENTIAL)); - assertEquals(1, result.successful()); // Check that the there is no longer a realtime added trip pattern for the trip and that the // stoptime updates have gone through var snapshot = env.getTimetableSnapshot(); - { - final TripPattern newTripPattern = snapshot.getRealtimeAddedTripPattern( - env.trip2.getId(), - RealtimeTestEnvironment.SERVICE_DATE - ); - assertNull(newTripPattern); - final Trip trip = env.transitModel.getTransitModelIndex().getTripForId().get(tripId); - - final TripPattern originalTripPattern = env.transitModel - .getTransitModelIndex() - .getPatternForTrip() - .get(trip); - final Timetable originalTimetableForToday = snapshot.resolve( - originalTripPattern, - RealtimeTestEnvironment.SERVICE_DATE - ); - - final Timetable originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); - - assertNotSame(originalTimetableForToday, originalTimetableScheduled); - - final int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(tripId); - - assertTrue( - originalTripIndexScheduled > -1, - "Original trip should be found in scheduled time table" - ); - final TripTimes originalTripTimesScheduled = originalTimetableScheduled.getTripTimes( - originalTripIndexScheduled - ); - assertFalse( - originalTripTimesScheduled.isCanceledOrDeleted(), - "Original trip times should not be canceled in scheduled time table" - ); - assertEquals(RealTimeState.SCHEDULED, originalTripTimesScheduled.getRealTimeState()); - final int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); - - assertTrue( - originalTripIndexForToday > -1, - "Original trip should be found in time table for service date" - ); - final TripTimes originalTripTimesForToday = originalTimetableForToday.getTripTimes( - originalTripIndexForToday - ); - assertEquals(RealTimeState.UPDATED, originalTripTimesForToday.getRealTimeState()); - assertEquals(0, originalTripTimesForToday.getArrivalDelay(0)); - assertEquals(0, originalTripTimesForToday.getDepartureDelay(0)); - assertEquals(50, originalTripTimesForToday.getArrivalDelay(1)); - assertEquals(50, originalTripTimesForToday.getDepartureDelay(1)); - assertEquals(90, originalTripTimesForToday.getArrivalDelay(2)); - assertEquals(90, originalTripTimesForToday.getDepartureDelay(2)); - } + assertNull(snapshot.getRealtimeAddedTripPattern(tripId, SERVICE_DATE)); + + assertNewTripTimesIsUpdated(env, tripId); + + assertEquals( + "SCHEDULED | A1 0:01 0:01:01 | B1 0:01:10 0:01:11 | C1 0:01:20 0:01:21", + env.getScheduledTimetable(tripId) + ); + assertEquals( + "UPDATED | A1 0:01 0:01:01 | B1 0:02 0:02:01 | C1 0:02:50 0:02:51", + env.getRealtimeTimetable(tripId, SERVICE_DATE) + ); + } + + /** + * Tests a mixture of SKIPPED and NO_DATA. + */ + @Test + void skippedNoData() { + var env = RealtimeTestEnvironment.gtfs(); + + final FeedScopedId tripId = env.trip2.getId(); + + var tripUpdate = new TripUpdateBuilder(tripId.getId(), SERVICE_DATE, SCHEDULED, env.timeZone) + .addNoDataStop(0) + .addSkippedStop(1) + .addNoDataStop(2) + .build(); + + assertSuccess(env.applyTripUpdate(tripUpdate)); + + assertOriginalTripPatternIsDeleted(env, tripId); + + assertNewTripTimesIsUpdated(env, tripId); + + assertEquals( + "UPDATED | A1 [ND] 0:01 0:01:01 | B1 [C] 0:01:10 0:01:11 | C1 [ND] 0:01:20 0:01:21", + env.getRealtimeTimetable(env.trip2) + ); + } + + private static void assertOriginalTripPatternIsDeleted( + RealtimeTestEnvironment env, + FeedScopedId tripId + ) { + var trip = env.getTransitService().getTripForId(tripId); + var originalTripPattern = env.getTransitService().getPatternForTrip(trip); + var snapshot = env.getTimetableSnapshot(); + var originalTimetableForToday = snapshot.resolve(originalTripPattern, SERVICE_DATE); + var originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); + + assertNotSame(originalTimetableForToday, originalTimetableScheduled); + + int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(tripId); + assertTrue( + originalTripIndexScheduled > -1, + "Original trip should be found in scheduled time table" + ); + var originalTripTimesScheduled = originalTimetableScheduled.getTripTimes( + originalTripIndexScheduled + ); + assertFalse( + originalTripTimesScheduled.isCanceledOrDeleted(), + "Original trip times should not be canceled in scheduled time table" + ); + + assertEquals( + "SCHEDULED | A1 0:01 0:01:01 | B1 0:01:10 0:01:11 | C1 0:01:20 0:01:21", + TripTimesStringBuilder.encodeTripTimes(originalTripTimesScheduled, originalTripPattern) + ); + + int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); + assertTrue( + originalTripIndexForToday > -1, + "Original trip should be found in time table for service date" + ); + var originalTripTimesForToday = originalTimetableForToday.getTripTimes( + originalTripIndexForToday + ); + assertTrue( + originalTripTimesForToday.isDeleted(), + "Original trip times should be deleted in time table for service date" + ); + // original trip should be deleted + assertEquals(RealTimeState.DELETED, originalTripTimesForToday.getRealTimeState()); + } + + private static void assertNewTripTimesIsUpdated( + RealtimeTestEnvironment env, + FeedScopedId tripId + ) { + var originalTripPattern = env.getTransitService().getPatternForTrip(env.trip2); + var snapshot = env.getTimetableSnapshot(); + var originalTimetableForToday = snapshot.resolve(originalTripPattern, SERVICE_DATE); + + var originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); + + assertNotSame(originalTimetableForToday, originalTimetableScheduled); + + int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(tripId); + + assertTrue( + originalTripIndexScheduled > -1, + "Original trip should be found in scheduled time table" + ); + var originalTripTimesScheduled = originalTimetableScheduled.getTripTimes( + originalTripIndexScheduled + ); + assertFalse( + originalTripTimesScheduled.isCanceledOrDeleted(), + "Original trip times should not be canceled in scheduled time table" + ); + assertEquals(RealTimeState.SCHEDULED, originalTripTimesScheduled.getRealTimeState()); + int originalTripIndexForToday = originalTimetableForToday.getTripIndex(tripId); + + assertTrue( + originalTripIndexForToday > -1, + "Original trip should be found in time table for service date" + ); } } diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java index 00831333943..da362451753 100644 --- a/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidInputTest.java @@ -1,14 +1,13 @@ package org.opentripplanner.updater.trip.moduletests.rejection; import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_SERVICE_ON_DATE; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertFailure; import static org.opentripplanner.updater.trip.RealtimeTestEnvironment.SERVICE_DATE; import java.time.LocalDate; import java.util.List; -import java.util.Set; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.opentripplanner.updater.trip.RealtimeTestEnvironment; @@ -18,7 +17,7 @@ * A trip with start date that is outside the service period shouldn't throw an exception and is * ignored instead. */ -public class InvalidInputTest { +class InvalidInputTest { public static List cases() { return List.of(SERVICE_DATE.minusYears(10), SERVICE_DATE.plusYears(10)); @@ -26,21 +25,17 @@ public static List cases() { @ParameterizedTest @MethodSource("cases") - public void invalidTripDate(LocalDate date) { + void invalidTripDate(LocalDate date) { var env = RealtimeTestEnvironment.gtfs(); var update = new TripUpdateBuilder(env.trip1.getId().getId(), date, SCHEDULED, env.timeZone) - .addDelayedStopTime(1, 0) .addDelayedStopTime(2, 60, 80) - .addDelayedStopTime(3, 90, 90) .build(); var result = env.applyTripUpdate(update); var snapshot = env.getTimetableSnapshot(); assertTrue(snapshot.isEmpty()); - assertEquals(1, result.failed()); - var errors = result.failures().keySet(); - assertEquals(Set.of(NO_SERVICE_ON_DATE), errors); + assertFailure(NO_SERVICE_ON_DATE, result); } } diff --git a/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidTripIdTest.java b/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidTripIdTest.java new file mode 100644 index 00000000000..83c2547dbc7 --- /dev/null +++ b/src/test/java/org/opentripplanner/updater/trip/moduletests/rejection/InvalidTripIdTest.java @@ -0,0 +1,38 @@ +package org.opentripplanner.updater.trip.moduletests.rejection; + +import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED; +import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_INPUT_STRUCTURE; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertFailure; + +import com.google.transit.realtime.GtfsRealtime; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.updater.trip.RealtimeTestEnvironment; + +class InvalidTripIdTest { + + static Stream invalidCases() { + return Stream.of(null, "", " "); + } + + /** + * This test just asserts that invalid trip ids don't throw an exception and are ignored instead + */ + @ParameterizedTest(name = "tripId=\"{0}\"") + @MethodSource("invalidCases") + void invalidTripId(String tripId) { + var env = RealtimeTestEnvironment.gtfs(); + var tripDescriptorBuilder = GtfsRealtime.TripDescriptor.newBuilder(); + if (tripId != null) { + tripDescriptorBuilder.setTripId(tripId); + } + tripDescriptorBuilder.setScheduleRelationship(SCHEDULED); + var tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); + + tripUpdateBuilder.setTrip(tripDescriptorBuilder); + var tripUpdate = tripUpdateBuilder.build(); + + assertFailure(INVALID_INPUT_STRUCTURE, env.applyTripUpdate(tripUpdate)); + } +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/routes-extended.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/routes-extended.json index 8739eab0045..8856972ce4e 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/routes-extended.json +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/routes-extended.json @@ -11,7 +11,8 @@ }, "mode" : "CARPOOL", "sortOrder" : 12, - "bikesAllowed" : "ALLOWED" + "bikesAllowed" : "ALLOWED", + "patterns" : [ ] }, { "longName" : "Long name for SUBWAY", @@ -23,7 +24,8 @@ }, "mode" : "SUBWAY", "sortOrder" : 2, - "bikesAllowed" : "NO_INFORMATION" + "bikesAllowed" : "NO_INFORMATION", + "patterns" : [ ] }, { "longName" : "Long name for BUS", @@ -35,7 +37,8 @@ }, "mode" : "BUS", "sortOrder" : 3, - "bikesAllowed" : "ALLOWED" + "bikesAllowed" : "ALLOWED", + "patterns" : [ ] }, { "longName" : "Long name for FERRY", @@ -47,7 +50,8 @@ }, "mode" : "FERRY", "sortOrder" : 5, - "bikesAllowed" : "NO_INFORMATION" + "bikesAllowed" : "NO_INFORMATION", + "patterns" : [ ] }, { "longName" : "Long name for COACH", @@ -59,7 +63,8 @@ }, "mode" : "COACH", "sortOrder" : 1, - "bikesAllowed" : "NOT_ALLOWED" + "bikesAllowed" : "NOT_ALLOWED", + "patterns" : [ ] }, { "longName" : "Long name for TRAM", @@ -71,7 +76,8 @@ }, "mode" : "TRAM", "sortOrder" : 4, - "bikesAllowed" : "NOT_ALLOWED" + "bikesAllowed" : "NOT_ALLOWED", + "patterns" : [ ] }, { "longName" : "Long name for CABLE_CAR", @@ -83,7 +89,8 @@ }, "mode" : "CABLE_CAR", "sortOrder" : 7, - "bikesAllowed" : "NOT_ALLOWED" + "bikesAllowed" : "NOT_ALLOWED", + "patterns" : [ ] }, { "longName" : "Long name for FUNICULAR", @@ -95,7 +102,8 @@ }, "mode" : "FUNICULAR", "sortOrder" : 9, - "bikesAllowed" : "ALLOWED" + "bikesAllowed" : "ALLOWED", + "patterns" : [ ] }, { "longName" : "Long name for RAIL", @@ -107,7 +115,8 @@ }, "mode" : "RAIL", "sortOrder" : null, - "bikesAllowed" : "ALLOWED" + "bikesAllowed" : "ALLOWED", + "patterns" : [ ] }, { "longName" : "Long name for MONORAIL", @@ -119,7 +128,8 @@ }, "mode" : "MONORAIL", "sortOrder" : 11, - "bikesAllowed" : "NO_INFORMATION" + "bikesAllowed" : "NO_INFORMATION", + "patterns" : [ ] }, { "longName" : "Long name for GONDOLA", @@ -131,7 +141,8 @@ }, "mode" : "GONDOLA", "sortOrder" : 8, - "bikesAllowed" : "NO_INFORMATION" + "bikesAllowed" : "NO_INFORMATION", + "patterns" : [ ] }, { "longName" : "Long name for TROLLEYBUS", @@ -143,7 +154,8 @@ }, "mode" : "TROLLEYBUS", "sortOrder" : 10, - "bikesAllowed" : "NOT_ALLOWED" + "bikesAllowed" : "NOT_ALLOWED", + "patterns" : [ ] }, { "longName" : "Long name for AIRPLANE", @@ -155,7 +167,8 @@ }, "mode" : "AIRPLANE", "sortOrder" : 6, - "bikesAllowed" : "ALLOWED" + "bikesAllowed" : "ALLOWED", + "patterns" : [ ] }, { "longName" : "Long name for TAXI", @@ -167,7 +180,8 @@ }, "mode" : "TAXI", "sortOrder" : 13, - "bikesAllowed" : "NOT_ALLOWED" + "bikesAllowed" : "NOT_ALLOWED", + "patterns" : [ ] } ] } diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/routes-extended.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/routes-extended.graphql index 90972cfa8cd..7f5c68961aa 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/queries/routes-extended.graphql +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/routes-extended.graphql @@ -10,5 +10,8 @@ mode sortOrder bikesAllowed + patterns(serviceDates: { start: "2024-05-23", end: "2024-05-30" }) { + name + } } } diff --git a/src/test/resources/org/opentripplanner/apis/vectortiles/style.json b/src/test/resources/org/opentripplanner/apis/vectortiles/style.json index 6f981b7f67d..5a2ed9572e2 100644 --- a/src/test/resources/org/opentripplanner/apis/vectortiles/style.json +++ b/src/test/resources/org/opentripplanner/apis/vectortiles/style.json @@ -191,6 +191,52 @@ "visibility" : "none" } }, + { + "id" : "parking-vertex", + "type" : "circle", + "source" : "vectorSource", + "source-layer" : "vertices", + "minzoom" : 13, + "maxzoom" : 23, + "paint" : { + "circle-stroke-color" : "#140d0e", + "circle-stroke-width" : { + "base" : 1.0, + "stops" : [ + [ + 15, + 0.2 + ], + [ + 23, + 3.0 + ] + ] + }, + "circle-radius" : { + "base" : 1.0, + "stops" : [ + [ + 13, + 1.4 + ], + [ + 23, + 10.0 + ] + ] + }, + "circle-color" : "#136b04" + }, + "filter" : [ + "in", + "class", + "VehicleParkingEntranceVertex" + ], + "layout" : { + "visibility" : "none" + } + }, { "id" : "area-stop", "type" : "fill",