diff --git a/client/package-lock.json b/client/package-lock.json index dc6c6dfa1a3..09cdf3b18a2 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,7 +12,7 @@ "bootstrap": "5.3.3", "graphql": "16.9.0", "graphql-request": "7.1.0", - "maplibre-gl": "4.5.0", + "maplibre-gl": "4.5.2", "react": "18.3.1", "react-bootstrap": "2.10.4", "react-dom": "18.3.1", @@ -20,16 +20,16 @@ }, "devDependencies": { "@graphql-codegen/cli": "5.0.2", - "@graphql-codegen/client-preset": "4.3.2", + "@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.17.0", - "@typescript-eslint/parser": "7.17.0", + "@typescript-eslint/eslint-plugin": "7.18.0", + "@typescript-eslint/parser": "7.18.0", "@vitejs/plugin-react": "4.3.1", - "@vitest/coverage-v8": "2.0.4", + "@vitest/coverage-v8": "2.0.5", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.29.1", @@ -40,8 +40,8 @@ "jsdom": "24.1.1", "prettier": "3.3.3", "typescript": "5.5.4", - "vite": "5.3.4", - "vitest": "2.0.4" + "vite": "5.4.1", + "vitest": "2.0.5" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1861,9 +1861,9 @@ } }, "node_modules/@graphql-codegen/client-preset": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-4.3.2.tgz", - "integrity": "sha512-42jHyG6u2uFDIVNvzue8zR529aPT16EYIJQmvMk8XuYHo3PneQVlWmQ3j2fBy+RuWCBzpJKPKm7IGSKiw19nmg==", + "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": { @@ -3846,11 +3846,6 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, - "node_modules/@types/junit-report-builder": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz", - "integrity": "sha512-R5M+SYhMbwBeQcNXYWNCZkl09vkVfAtcPIaCGdzIkkbeaTrVbGQ7HVgi4s+EmM/M1K4ZuWQH0jGcvMvNePfxYA==" - }, "node_modules/@types/mapbox__point-geometry": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", @@ -3943,17 +3938,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz", - "integrity": "sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==", + "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.17.0", - "@typescript-eslint/type-utils": "7.17.0", - "@typescript-eslint/utils": "7.17.0", - "@typescript-eslint/visitor-keys": "7.17.0", + "@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", @@ -3977,16 +3972,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==", + "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.17.0", - "@typescript-eslint/types": "7.17.0", - "@typescript-eslint/typescript-estree": "7.17.0", - "@typescript-eslint/visitor-keys": "7.17.0", + "@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": { @@ -4006,14 +4001,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz", - "integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==", + "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.17.0", - "@typescript-eslint/visitor-keys": "7.17.0" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -4024,14 +4019,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.17.0.tgz", - "integrity": "sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==", + "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.17.0", - "@typescript-eslint/utils": "7.17.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -4052,9 +4047,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz", - "integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==", + "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": { @@ -4066,14 +4061,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz", - "integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==", + "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.17.0", - "@typescript-eslint/visitor-keys": "7.17.0", + "@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", @@ -4108,16 +4103,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz", - "integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==", + "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.17.0", - "@typescript-eslint/types": "7.17.0", - "@typescript-eslint/typescript-estree": "7.17.0" + "@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" @@ -4131,13 +4126,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz", - "integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==", + "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.17.0", + "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -4175,9 +4170,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.4.tgz", - "integrity": "sha512-i4lx/Wpg5zF1h2op7j0wdwuEQxaL/YTwwQaKuKMHYj7MMh8c7I4W7PNfOptZBCSBZI0z1qwn64o0pM/pA8Tz1g==", + "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": { @@ -4198,18 +4193,18 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "2.0.4" + "vitest": "2.0.5" } }, "node_modules/@vitest/expect": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.4.tgz", - "integrity": "sha512-39jr5EguIoanChvBqe34I8m1hJFI4+jxvdOpD7gslZrVQBKhh8H9eD7J/LJX4zakrw23W+dITQTDqdt43xVcJw==", + "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": "2.0.4", - "@vitest/utils": "2.0.4", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", "chai": "^5.1.1", "tinyrainbow": "^1.2.0" }, @@ -4218,9 +4213,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.4.tgz", - "integrity": "sha512-RYZl31STbNGqf4l2eQM1nvKPXE0NhC6Eq0suTTePc4mtMQ1Fn8qZmjV4emZdEdG2NOWGKSCrHZjmTqDCDoeFBw==", + "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": { @@ -4231,13 +4226,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.4.tgz", - "integrity": "sha512-Gk+9Su/2H2zNfNdeJR124gZckd5st4YoSuhF1Rebi37qTXKnqYyFCd9KP4vl2cQHbtuVKjfEKrNJxHHCW8thbQ==", + "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": { - "@vitest/utils": "2.0.4", + "@vitest/utils": "2.0.5", "pathe": "^1.1.2" }, "funding": { @@ -4245,13 +4240,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.4.tgz", - "integrity": "sha512-or6Mzoz/pD7xTvuJMFYEtso1vJo1S5u6zBTinfl+7smGUhqybn6VjzCDMhmTyVOFWwkCMuNjmNNxnyXPgKDoPw==", + "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": { - "@vitest/pretty-format": "2.0.4", + "@vitest/pretty-format": "2.0.5", "magic-string": "^0.30.10", "pathe": "^1.1.2" }, @@ -4260,9 +4255,9 @@ } }, "node_modules/@vitest/spy": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.4.tgz", - "integrity": "sha512-uTXU56TNoYrTohb+6CseP8IqNwlNdtPwEO0AWl+5j7NelS6x0xZZtP0bDWaLvOfUbaYwhhWp1guzXUxkC7mW7Q==", + "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": { @@ -4273,13 +4268,13 @@ } }, "node_modules/@vitest/utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.4.tgz", - "integrity": "sha512-Zc75QuuoJhOBnlo99ZVUkJIuq4Oj0zAkrQ2VzCqNCx6wAwViHEh5Fnp4fiJTE9rA+sAoXRf00Z9xGgfEzV6fzQ==", + "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": { - "@vitest/pretty-format": "2.0.4", + "@vitest/pretty-format": "2.0.5", "estree-walker": "^3.0.3", "loupe": "^3.1.1", "tinyrainbow": "^1.2.0" @@ -5701,9 +5696,10 @@ } }, "node_modules/earcut": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", - "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.0.tgz", + "integrity": "sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==", + "license": "ISC" }, "node_modules/eastasianwidth": { "version": "0.2.0", @@ -8731,9 +8727,9 @@ } }, "node_modules/maplibre-gl": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.5.0.tgz", - "integrity": "sha512-qOS1hn4d/pn2i0uva4S5Oz+fACzTkgBKq+NpwT/Tqzi4MSyzcWNtDELzLUSgWqHfNIkGCl5CZ/w7dtis+t4RCw==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.5.2.tgz", + "integrity": "sha512-vlWL9EY2bSGg5FAt0mKPfYqlfX15uLW5D3kKv4Xjn54nIVn01MKdfUJMAVIr+8fXVqfSX6c095Iy5XnV+T76kQ==", "license": "BSD-3-Clause", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", @@ -8746,22 +8742,21 @@ "@maplibre/maplibre-gl-style-spec": "^20.3.0", "@types/geojson": "^7946.0.14", "@types/geojson-vt": "3.2.5", - "@types/junit-report-builder": "^3.0.2", "@types/mapbox__point-geometry": "^0.1.4", "@types/mapbox__vector-tile": "^1.3.4", "@types/pbf": "^3.0.5", "@types/supercluster": "^7.1.3", - "earcut": "^2.2.4", + "earcut": "^3.0.0", "geojson-vt": "^4.0.2", "gl-matrix": "^3.4.3", "global-prefix": "^3.0.0", "kdbush": "^4.0.2", "murmurhash-js": "^1.0.0", - "pbf": "^3.2.1", + "pbf": "^3.3.0", "potpack": "^2.0.0", - "quickselect": "^2.0.0", + "quickselect": "^3.0.0", "supercluster": "^8.0.1", - "tinyqueue": "^2.0.3", + "tinyqueue": "^3.0.0", "vt-pbf": "^3.1.3" }, "engines": { @@ -8772,6 +8767,18 @@ "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" } }, + "node_modules/maplibre-gl/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -9495,9 +9502,10 @@ } }, "node_modules/pbf": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", - "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", "dependencies": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" @@ -9535,9 +9543,9 @@ } }, "node_modules/postcss": { - "version": "8.4.40", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", - "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "dev": true, "funding": [ { @@ -11392,14 +11400,14 @@ } }, "node_modules/vite": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz", - "integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.1.tgz", + "integrity": "sha512-1oE6yuNXssjrZdblI9AfBbHCC41nnyoVoEZxQnID6yvQZAFBzxxkqoFLtHUMkYunL8hwOLEjgTuxpkRxvba3kA==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.39", + "postcss": "^8.4.41", "rollup": "^4.13.0" }, "bin": { @@ -11419,6 +11427,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -11436,6 +11445,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -11448,9 +11460,9 @@ } }, "node_modules/vite-node": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.4.tgz", - "integrity": "sha512-ZpJVkxcakYtig5iakNeL7N3trufe3M6vGuzYAr4GsbCTwobDeyPJpE4cjDhhPluv8OvQCFzu2LWp6GkoKRITXA==", + "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": { @@ -11471,19 +11483,19 @@ } }, "node_modules/vitest": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.4.tgz", - "integrity": "sha512-luNLDpfsnxw5QSW4bISPe6tkxVvv5wn2BBs/PuDRkhXZ319doZyLOBr1sjfB5yCEpTiU7xCAdViM8TNVGPwoog==", + "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": { "@ampproject/remapping": "^2.3.0", - "@vitest/expect": "2.0.4", - "@vitest/pretty-format": "^2.0.4", - "@vitest/runner": "2.0.4", - "@vitest/snapshot": "2.0.4", - "@vitest/spy": "2.0.4", - "@vitest/utils": "2.0.4", + "@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", @@ -11494,7 +11506,7 @@ "tinypool": "^1.0.0", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.0.4", + "vite-node": "2.0.5", "why-is-node-running": "^2.3.0" }, "bin": { @@ -11509,8 +11521,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.0.4", - "@vitest/ui": "2.0.4", + "@vitest/browser": "2.0.5", + "@vitest/ui": "2.0.5", "happy-dom": "*", "jsdom": "*" }, diff --git a/client/package.json b/client/package.json index 8ce0ed0cbdb..2dcbd623562 100644 --- a/client/package.json +++ b/client/package.json @@ -21,7 +21,7 @@ "bootstrap": "5.3.3", "graphql": "16.9.0", "graphql-request": "7.1.0", - "maplibre-gl": "4.5.0", + "maplibre-gl": "4.5.2", "react": "18.3.1", "react-bootstrap": "2.10.4", "react-dom": "18.3.1", @@ -29,16 +29,16 @@ }, "devDependencies": { "@graphql-codegen/cli": "5.0.2", - "@graphql-codegen/client-preset": "4.3.2", + "@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.17.0", - "@typescript-eslint/parser": "7.17.0", + "@typescript-eslint/eslint-plugin": "7.18.0", + "@typescript-eslint/parser": "7.18.0", "@vitejs/plugin-react": "4.3.1", - "@vitest/coverage-v8": "2.0.4", + "@vitest/coverage-v8": "2.0.5", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.29.1", @@ -49,7 +49,7 @@ "jsdom": "24.1.1", "prettier": "3.3.3", "typescript": "5.5.4", - "vite": "5.3.4", - "vitest": "2.0.4" + "vite": "5.4.1", + "vitest": "2.0.5" } } diff --git a/client/src/components/ItineraryList/ItineraryLegDetails.tsx b/client/src/components/ItineraryList/ItineraryLegDetails.tsx index 56fdf430388..e75813a4a45 100644 --- a/client/src/components/ItineraryList/ItineraryLegDetails.tsx +++ b/client/src/components/ItineraryList/ItineraryLegDetails.tsx @@ -10,14 +10,9 @@ export function ItineraryLegDetails({ leg, isLast }: { leg: Leg; isLast: boolean
{formatDistance(leg.distance)}, {formatDuration(leg.duration)}
-
- - - -
+ + -{' '} +
{leg.mode}{' '} {leg.line && ( @@ -28,9 +23,9 @@ export function ItineraryLegDetails({ leg, isLast }: { leg: Leg; isLast: boolean , {leg.authority?.name} )}{' '} -
{leg.mode !== Mode.Foot && ( <> +
{leg.fromPlace.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/style.css b/client/src/style.css index 7dd2565c449..1a24ac2c072 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -86,7 +86,7 @@ } .itinerary-leg-details .mode { - margin-top: 10px; + margin-top: 2px; } .itinerary-header-itinerary-number { 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/doc/templates/VehicleParking.md b/doc/templates/VehicleParking.md index 5d149e40f9a..721cbc2657a 100644 --- a/doc/templates/VehicleParking.md +++ b/doc/templates/VehicleParking.md @@ -48,6 +48,16 @@ All updaters have the following parameters in common: +## SIRI-FM + +The SIRI-FM updater works slightly differently from the others in that it only updates the availability +of parking but does not create new lots in realtime. + +The data source must conform to the [Italian SIRI-FM](https://github.com/5Tsrl/siri-italian-profile) profile +which requires SIRI 2.1. + + + ## Changelog - Create initial sandbox implementation (January 2022, [#3796](https://github.com/opentripplanner/OpenTripPlanner/pull/3796)) diff --git a/doc/user/BuildConfiguration.md b/doc/user/BuildConfiguration.md index 561963f1a1f..b311991120e 100644 --- a/doc/user/BuildConfiguration.md +++ b/doc/user/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/doc/user/Changelog.md b/doc/user/Changelog.md index 1527e18c873..453e2cc6089 100644 --- a/doc/user/Changelog.md +++ b/doc/user/Changelog.md @@ -51,6 +51,11 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - 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) +- SIRI-FM vehicle parking updates [#5979](https://github.com/opentripplanner/OpenTripPlanner/pull/5979) +- Take realtime patterns into account when storing realtime vehicles [#5994](https://github.com/opentripplanner/OpenTripPlanner/pull/5994) +- Debug client itinerary list style improvements [#6012](https://github.com/opentripplanner/OpenTripPlanner/pull/6012) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.5.0 (2024-03-13) diff --git a/doc/user/RouteRequest.md b/doc/user/RouteRequest.md index 28c5e288971..674ab238888 100644 --- a/doc/user/RouteRequest.md +++ b/doc/user/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/doc/user/RouterConfiguration.md b/doc/user/RouterConfiguration.md index 53aa82f66f2..4e565cfe17d 100644 --- a/doc/user/RouterConfiguration.md +++ b/doc/user/RouterConfiguration.md @@ -875,6 +875,12 @@ Used to group requests when monitoring OTP. "feedId" : "bikeep", "sourceType" : "bikeep", "url" : "https://services.bikeep.com/location/v1/public-areas/no-baia-mobility/locations" + }, + { + "type" : "vehicle-parking", + "feedId" : "parking", + "sourceType" : "siri-fm", + "url" : "https://transmodel.api.opendatahub.com/siri-lite/fm/parking" } ], "rideHailingServices" : [ diff --git a/doc/user/examples/entur/build-config.json b/doc/user/examples/entur/build-config.json index e9351882774..2acea588234 100644 --- a/doc/user/examples/entur/build-config.json +++ b/doc/user/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/doc/user/sandbox/VehicleParking.md b/doc/user/sandbox/VehicleParking.md index a5adde1d4c2..db057bd9dbd 100644 --- a/doc/user/sandbox/VehicleParking.md +++ b/doc/user/sandbox/VehicleParking.md @@ -55,13 +55,13 @@ All updaters have the following parameters in common: The id of the data source, which will be the prefix of the parking lot's id. -This will end up in the API responses as the feed id of of the parking lot. +This will end up in the API responses as the feed id of the parking lot.

sourceType

**Since version:** `2.2` ∙ **Type:** `enum` ∙ **Cardinality:** `Required` **Path:** /updaters/[2] -**Enum values:** `park-api` | `bicycle-park-api` | `hsl-park` | `bikely` | `noi-open-data-hub` | `bikeep` +**Enum values:** `park-api` | `bicycle-park-api` | `hsl-park` | `bikely` | `noi-open-data-hub` | `bikeep` | `siri-fm` The source of the vehicle updates. @@ -125,13 +125,13 @@ Used for converting abstract opening hours into concrete points in time. The id of the data source, which will be the prefix of the parking lot's id. -This will end up in the API responses as the feed id of of the parking lot. +This will end up in the API responses as the feed id of the parking lot.

sourceType

**Since version:** `2.2` ∙ **Type:** `enum` ∙ **Cardinality:** `Required` **Path:** /updaters/[3] -**Enum values:** `park-api` | `bicycle-park-api` | `hsl-park` | `bikely` | `noi-open-data-hub` | `bikeep` +**Enum values:** `park-api` | `bicycle-park-api` | `hsl-park` | `bikely` | `noi-open-data-hub` | `bikeep` | `siri-fm` The source of the vehicle updates. @@ -210,13 +210,13 @@ Tags to add to the parking lots. The id of the data source, which will be the prefix of the parking lot's id. -This will end up in the API responses as the feed id of of the parking lot. +This will end up in the API responses as the feed id of the parking lot.

sourceType

**Since version:** `2.2` ∙ **Type:** `enum` ∙ **Cardinality:** `Required` **Path:** /updaters/[4] -**Enum values:** `park-api` | `bicycle-park-api` | `hsl-park` | `bikely` | `noi-open-data-hub` | `bikeep` +**Enum values:** `park-api` | `bicycle-park-api` | `hsl-park` | `bikely` | `noi-open-data-hub` | `bikeep` | `siri-fm` The source of the vehicle updates. @@ -275,13 +275,13 @@ HTTP headers to add to the request. Any header key, value can be inserted. The id of the data source, which will be the prefix of the parking lot's id. -This will end up in the API responses as the feed id of of the parking lot. +This will end up in the API responses as the feed id of the parking lot.

sourceType

**Since version:** `2.2` ∙ **Type:** `enum` ∙ **Cardinality:** `Required` **Path:** /updaters/[5] -**Enum values:** `park-api` | `bicycle-park-api` | `hsl-park` | `bikely` | `noi-open-data-hub` | `bikeep` +**Enum values:** `park-api` | `bicycle-park-api` | `hsl-park` | `bikely` | `noi-open-data-hub` | `bikeep` | `siri-fm` The source of the vehicle updates. @@ -336,13 +336,13 @@ HTTP headers to add to the request. Any header key, value can be inserted. The id of the data source, which will be the prefix of the parking lot's id. -This will end up in the API responses as the feed id of of the parking lot. +This will end up in the API responses as the feed id of the parking lot.

sourceType

**Since version:** `2.2` ∙ **Type:** `enum` ∙ **Cardinality:** `Required` **Path:** /updaters/[14] -**Enum values:** `park-api` | `bicycle-park-api` | `hsl-park` | `bikely` | `noi-open-data-hub` | `bikeep` +**Enum values:** `park-api` | `bicycle-park-api` | `hsl-park` | `bikely` | `noi-open-data-hub` | `bikeep` | `siri-fm` The source of the vehicle updates. @@ -373,6 +373,87 @@ HTTP headers to add to the request. Any header key, value can be inserted. +## SIRI-FM + +The SIRI-FM updater works slightly differently from the others in that it only updates the availability +of parking but does not create new lots in realtime. + +The data source must conform to the [Italian SIRI-FM](https://github.com/5Tsrl/siri-italian-profile) profile +which requires SIRI 2.1. + + + + +| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | +|----------------------------------|:---------------:|------------------------------------------------------------------------------|:----------:|---------------|:-----:| +| type = "vehicle-parking" | `enum` | The type of the updater. | *Required* | | 1.5 | +| [feedId](#u__15__feedId) | `string` | The id of the data source, which will be the prefix of the parking lot's id. | *Required* | | 2.2 | +| frequency | `duration` | How often to update the source. | *Optional* | `"PT1M"` | 2.6 | +| [sourceType](#u__15__sourceType) | `enum` | The source of the vehicle updates. | *Required* | | 2.2 | +| [url](#u__15__url) | `uri` | URL of the SIRI-FM Light endpoint. | *Required* | | 2.6 | +| [headers](#u__15__headers) | `map of string` | HTTP headers to add to the request. Any header key, value can be inserted. | *Optional* | | 2.6 | + + +#### Details + +

feedId

+ +**Since version:** `2.2` ∙ **Type:** `string` ∙ **Cardinality:** `Required` +**Path:** /updaters/[15] + +The id of the data source, which will be the prefix of the parking lot's id. + +This will end up in the API responses as the feed id of the parking lot. + +

sourceType

+ +**Since version:** `2.2` ∙ **Type:** `enum` ∙ **Cardinality:** `Required` +**Path:** /updaters/[15] +**Enum values:** `park-api` | `bicycle-park-api` | `hsl-park` | `bikely` | `noi-open-data-hub` | `bikeep` | `siri-fm` + +The source of the vehicle updates. + +

url

+ +**Since version:** `2.6` ∙ **Type:** `uri` ∙ **Cardinality:** `Required` +**Path:** /updaters/[15] + +URL of the SIRI-FM Light endpoint. + +SIRI Light means that it must be available as a HTTP GET request rather than the usual +SIRI request mechanism of HTTP POST. + +The contents must also conform to the [Italian SIRI profile](https://github.com/5Tsrl/siri-italian-profile) +which requires SIRI 2.1. + + +

headers

+ +**Since version:** `2.6` ∙ **Type:** `map of string` ∙ **Cardinality:** `Optional` +**Path:** /updaters/[15] + +HTTP headers to add to the request. Any header key, value can be inserted. + + + +##### Example configuration + +```JSON +// router-config.json +{ + "updaters" : [ + { + "type" : "vehicle-parking", + "feedId" : "parking", + "sourceType" : "siri-fm", + "url" : "https://transmodel.api.opendatahub.com/siri-lite/fm/parking" + } + ] +} +``` + + + ## Changelog - Create initial sandbox implementation (January 2022, [#3796](https://github.com/opentripplanner/OpenTripPlanner/pull/3796)) 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 6763eca09c1..940d8de5f94 100644 --- a/pom.xml +++ b/pom.xml @@ -56,18 +56,18 @@ - 155 + 156 31.3 - 2.51.1 + 2.52 2.17.2 - 3.1.7 - 5.10.3 + 3.1.8 + 5.11.0 1.13.2 5.6.0 1.5.6 9.11.1 - 2.0.13 + 2.0.14 2.0.15 1.27 4.0.5 @@ -546,7 +546,7 @@ com.google.cloud libraries-bom - 26.40.0 + 26.44.0 pom import @@ -577,7 +577,7 @@ net.logstash.logback logstash-logback-encoder - 7.4 + 8.0 @@ -691,6 +691,12 @@ ${junit.version} test + + com.google.truth + truth + 1.4.4 + test + com.tngtech.archunit archunit @@ -853,7 +859,7 @@ com.graphql-java graphql-java - 22.1 + 22.2 com.graphql-java @@ -917,7 +923,7 @@ org.apache.commons commons-compress - 1.26.2 + 1.27.0 test @@ -994,7 +1000,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.4 + 3.2.5 sign-artifacts diff --git a/renovate.json5 b/renovate.json5 index d8ba10984e5..5a56c64cccb 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -34,7 +34,7 @@ "matchFiles": ["client/package.json"], "matchUpdateTypes": ["patch", "minor"], "groupName": "Debug UI dependencies (non-major)", - "schedule": ["on the first day of the week"], + "schedule": ["on the 3rd and 17th day of the month"], "reviewers": ["testower"] }, { @@ -44,6 +44,24 @@ }, // 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 + { + "description": "Automerge test dependencies in a single PR", + "groupName": "Test dependencies", + "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:", + ], + "automerge": true, + "schedule": "on the 17th day of the month" + }, { "matchPackageNames": [ "org.mobilitydata:gbfs-java-model" @@ -53,12 +71,21 @@ "automerge": true }, { + "description": "Automerge Maven plugins in a single PR", + "groupName": "Maven plugins", "matchPackageNames": [ - "ch.qos.logback:logback-classic", "io.github.git-commit-id:git-commit-id-maven-plugin", - "org.apache.maven.plugins:maven-gpg-plugin" + "org.apache.maven.plugins:maven-gpg-plugin", + "org.codehaus.mojo:build-helper-maven-plugin", + "org.apache.maven.plugins:maven-source-plugin", + "com.hubspot.maven.plugins:prettier-maven-plugin", + "com.google.cloud.tools:jib-maven-plugin", + "org.apache.maven.plugins:maven-shade-plugin", + "org.apache.maven.plugins:maven-compiler-plugin", + "org.apache.maven.plugins:maven-jar-plugin", + "org.sonatype.plugins:nexus-staging-maven-plugin" ], - "schedule": "on the 19th day of the month", + "schedule": "on the 23rd day of the month", "automerge": true }, { @@ -75,18 +102,17 @@ "org.onebusaway:onebusaway-gtfs", "com.google.cloud:libraries-bom", "com.google.guava:guava", - "@graphql-codegen/add", - "@graphql-codegen/cli", - "@graphql-codegen/java", - "@graphql-codegen/java-resolvers", - "graphql", "io.micrometer:micrometer-registry-prometheus", "io.micrometer:micrometer-registry-influx" ], - // we don't use the 'monthly' preset because that only fires on the first day of the month - // when there might already other PRs open "schedule": "on the 7th through 8th day of the month" }, + { + "groupName": "Update GTFS API code generation in a single PR", + "matchFiles": ["src/main/java/org/opentripplanner/apis/gtfs/generated/package.json"], + "reviewers": ["optionsome", "leonardehrenfried"], + "schedule": "on the 11th through 12th day of the month" + }, { "description": "in order to keep review burden low, don't update these quite so frequently", "matchPackagePrefixes": [ @@ -97,6 +123,7 @@ ] }, { + "groupName": "mkdocs", "description": "automerge mkdocs-material every quarter", "matchPackageNames": [ "mkdocs", @@ -108,30 +135,14 @@ "automerge": true }, { - "description": "automatically merge test, logging and build dependencies", - "matchPackageNames": [ - "org.mockito:mockito-core", - "com.tngtech.archunit:archunit", - "org.apache.maven.plugins:maven-surefire-plugin", - "me.fabriciorby:maven-surefire-junit5-tree-reporter", - "org.jacoco:jacoco-maven-plugin", // coverage plugin - "org.apache.commons:commons-compress", // only used by tests - // maven plugins - "org.codehaus.mojo:build-helper-maven-plugin", - "org.apache.maven.plugins:maven-source-plugin", - "com.hubspot.maven.plugins:prettier-maven-plugin", - "com.google.cloud.tools:jib-maven-plugin", - "org.apache.maven.plugins:maven-shade-plugin", - "org.apache.maven.plugins:maven-compiler-plugin", - "org.apache.maven.plugins:maven-jar-plugin", - "org.sonatype.plugins:nexus-staging-maven-plugin" - ], + "description": "Automerge logging dependencies in a single PR", + "groupName": "logging", "matchPackagePrefixes": [ - "org.junit.jupiter:", - "org.slf4j:" + "org.slf4j:", + "ch.qos.logback:" ], "automerge": true, - "schedule": "after 11pm and before 5am every weekday" + "schedule": "on the 4th day of the month" }, { "description": "give some projects time to publish a changelog before opening the PR", diff --git a/src/client/index.html b/src/client/index.html index 767f8b1d45a..42b2a26d059 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/geocoder/EnglishNgramAnalyzerTest.java b/src/ext-test/java/org/opentripplanner/ext/geocoder/EnglishNgramAnalyzerTest.java index 615ef90cbbd..2c352e0f760 100644 --- a/src/ext-test/java/org/opentripplanner/ext/geocoder/EnglishNgramAnalyzerTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/geocoder/EnglishNgramAnalyzerTest.java @@ -5,17 +5,17 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; class EnglishNgramAnalyzerTest { @Test - void ngram() throws IOException { - var analyzer = new EnglishNGramAnalyzer(); - List result = analyze("Alexanderplatz", analyzer); + void ngram() { + List result = tokenize("Alexanderplatz"); //System.out.println(result.stream().collect(Collectors.joining("\",\"", "\"", "\""))); assertEquals( @@ -82,14 +82,79 @@ void ngram() throws IOException { ); } - public List analyze(String text, Analyzer analyzer) throws IOException { - List result = new ArrayList<>(); - TokenStream tokenStream = analyzer.tokenStream("name", text); - CharTermAttribute attr = tokenStream.addAttribute(CharTermAttribute.class); - tokenStream.reset(); - while (tokenStream.incrementToken()) { - result.add(attr.toString()); + @Test + void ampersand() { + List result = tokenize("Meridian Ave N & N 148th St"); + + assertEquals( + List.of( + "Meri", + "Merid", + "Meridi", + "Meridia", + "Meridian", + "erid", + "eridi", + "eridia", + "eridian", + "ridi", + "ridia", + "ridian", + "idia", + "idian", + "dian", + "Av", + "N", + "N", + "148", + "St" + ), + result + ); + } + + @ParameterizedTest + @CsvSource( + value = { + "1st:1", + "2nd:2", + "3rd:3", + "4th:4", + "6th:6", + "148th:148", + "102nd:102", + "1003rd:1003", + "St:St", + "S3:S3", + "Aard:Aard", + }, + delimiter = ':' + ) + void numberSuffixes(String input, String expected) { + var result = tokenize(input); + assertEquals(List.of(expected), result); + } + + @Test + void wordBoundary() { + var result = tokenize("1stst"); + assertEquals(List.of("1sts", "1stst", "stst"), result); + } + + private List tokenize(String text) { + try (var analyzer = new EnglishNGramAnalyzer()) { + List result; + TokenStream tokenStream; + result = new ArrayList<>(); + tokenStream = analyzer.tokenStream("name", text); + CharTermAttribute attr = tokenStream.addAttribute(CharTermAttribute.class); + tokenStream.reset(); + while (tokenStream.incrementToken()) { + result.add(attr.toString()); + } + return result; + } catch (IOException e) { + throw new RuntimeException(e); } - return result; } } 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..de6e600037c 100644 --- a/src/ext-test/java/org/opentripplanner/ext/geocoder/LuceneIndexTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/geocoder/LuceneIndexTest.java @@ -12,12 +12,14 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; -import javax.annotation.Nonnull; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; 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; @@ -96,6 +98,10 @@ class LuceneIndexTest { .withCoordinate(52.52277, 13.41046) .build(); + static final RegularStop MERIDIAN_AVE = TEST_MODEL.stop("Meridian Ave N & N 148th St").build(); + static final RegularStop MERIDIAN_N1 = TEST_MODEL.stop("Meridian N & Spencer").build(); + static final RegularStop MERIDIAN_N2 = TEST_MODEL.stop("N 205th St & Meridian Ave N").build(); + static LuceneIndex index; static StopClusterMapper mapper; @@ -111,7 +117,10 @@ static void setup() { LICHTERFELDE_OST_2, WESTHAFEN, ARTS_CENTER, - ARTHUR + ARTHUR, + MERIDIAN_N1, + MERIDIAN_N2, + MERIDIAN_AVE ) .forEach(stopModel::withRegularStop); List @@ -160,8 +169,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 @@ -289,9 +302,32 @@ void agenciesAndFeedPublisher() { assertEquals(List.of(StopClusterMapper.toAgency(BVG)), cluster.primary().agencies()); assertEquals("A Publisher", cluster.primary().feedPublisher().name()); } + + @ParameterizedTest + @ValueSource( + strings = { + "Meridian Ave N & N 148th", + "Meridian Ave N & N 148", + "Meridian Ave N N 148", + "Meridian Ave N 148", + "Meridian & 148 N", + "148 N & Meridian", + "Meridian & N 148", + "Meridian Ave 148", + "Meridian Av 148", + "meridian av 148", + } + ) + void numericAdjectives(String query) { + var names = index.queryStopClusters(query).map(c -> c.primary().name()).toList(); + assertEquals( + Stream.of(MERIDIAN_AVE, MERIDIAN_N2, MERIDIAN_N1).map(s -> s.getName().toString()).toList(), + names + ); + } } - private static @Nonnull Function primaryId() { + private static Function primaryId() { return c -> c.primary().id(); } } 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/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-test/java/org/opentripplanner/ext/vehicleparking/sirifm/SiriFmUpdaterTest.java b/src/ext-test/java/org/opentripplanner/ext/vehicleparking/sirifm/SiriFmUpdaterTest.java new file mode 100644 index 00000000000..07502b597d9 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/vehicleparking/sirifm/SiriFmUpdaterTest.java @@ -0,0 +1,28 @@ +package org.opentripplanner.ext.vehicleparking.sirifm; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; +import org.junit.jupiter.api.Test; +import org.opentripplanner.test.support.ResourceLoader; +import org.opentripplanner.updater.spi.HttpHeaders; + +class SiriFmUpdaterTest { + + @Test + void parse() { + var uri = ResourceLoader.of(this).uri("siri-fm.xml"); + var parameters = new SiriFmUpdaterParameters( + "noi", + uri, + "noi", + Duration.ofSeconds(30), + HttpHeaders.empty() + ); + var updater = new SiriFmDatasource(parameters); + updater.update(); + var updates = updater.getUpdates(); + + assertEquals(4, updates.size()); + } +} diff --git a/src/ext-test/resources/org/opentripplanner/ext/vehicleparking/sirifm/siri-fm.xml b/src/ext-test/resources/org/opentripplanner/ext/vehicleparking/sirifm/siri-fm.xml new file mode 100644 index 00000000000..f4595488284 --- /dev/null +++ b/src/ext-test/resources/org/opentripplanner/ext/vehicleparking/sirifm/siri-fm.xml @@ -0,0 +1,56 @@ + + + 2024-07-17T11:07:40Z + RAP Alto Adige - Open Data Hub + + 2024-07-17T11:07:40Z + RAP Alto Adige - Open Data Hub + + IT:ITH10:Parking:105 + + available + + + presentCount + bays + 33 + + + + IT:ITH10:Parking:TRENTO_areaexsitviacanestrinip1 + + notAvailable + + + presentCount + bays + 300 + + + + IT:ITH10:Parking:TRENTO_autosilobuonconsigliop3 + + notAvailable + + + presentCount + bays + 633 + + + + IT:ITH10:Parking:TRENTO_cteviabomportop6 + + notAvailable + + + presentCount + bays + 250 + + + + + \ No newline at end of file diff --git a/src/ext/java/org/opentripplanner/ext/geocoder/EnglishNGramAnalyzer.java b/src/ext/java/org/opentripplanner/ext/geocoder/EnglishNGramAnalyzer.java index ffe46604744..17bf529a559 100644 --- a/src/ext/java/org/opentripplanner/ext/geocoder/EnglishNGramAnalyzer.java +++ b/src/ext/java/org/opentripplanner/ext/geocoder/EnglishNGramAnalyzer.java @@ -1,14 +1,16 @@ package org.opentripplanner.ext.geocoder; +import java.util.regex.Pattern; import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.LowerCaseFilter; import org.apache.lucene.analysis.TokenStream; -import org.apache.lucene.analysis.core.LowerCaseFilter; import org.apache.lucene.analysis.core.StopFilter; import org.apache.lucene.analysis.en.EnglishAnalyzer; import org.apache.lucene.analysis.en.EnglishPossessiveFilter; import org.apache.lucene.analysis.en.PorterStemFilter; import org.apache.lucene.analysis.miscellaneous.CapitalizationFilter; import org.apache.lucene.analysis.ngram.NGramTokenFilter; +import org.apache.lucene.analysis.pattern.PatternReplaceFilter; import org.apache.lucene.analysis.standard.StandardTokenizer; /** @@ -17,14 +19,21 @@ * of a stop name can be matched efficiently. *

* For example the query of "exanderpl" will match the stop name "Alexanderplatz". + *

+ * It also removes number suffixes in the American street names, like "147th Street", which will + * be tokenized to "147 Street". */ class EnglishNGramAnalyzer extends Analyzer { + // Matches one or more numbers followed by the English suffixes "st", "nd", "rd", "th" + private static final Pattern NUMBER_SUFFIX_PATTERN = Pattern.compile("(\\d+)(st|nd|rd|th)\\b"); + @Override protected TokenStreamComponents createComponents(String fieldName) { StandardTokenizer src = new StandardTokenizer(); TokenStream result = new EnglishPossessiveFilter(src); result = new LowerCaseFilter(result); + result = new PatternReplaceFilter(result, NUMBER_SUFFIX_PATTERN, "$1", true); result = new StopFilter(result, EnglishAnalyzer.ENGLISH_STOP_WORDS_SET); result = new PorterStemFilter(result); result = new CapitalizationFilter(result); 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..71b80ac58a6 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( @@ -281,7 +288,7 @@ private Stream matchingDocuments( } }); } else { - var nameParser = new QueryParser(NAME, analyzer); + var nameParser = new QueryParser(NAME_NGRAM, analyzer); var nameQuery = nameParser.parse(searchTerms); var ngramNameQuery = new TermQuery( 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/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/ext/java/org/opentripplanner/ext/vehicleparking/bikeep/BikeepUpdaterParameters.java b/src/ext/java/org/opentripplanner/ext/vehicleparking/bikeep/BikeepUpdaterParameters.java index be937ecdd5e..86e03731dd8 100644 --- a/src/ext/java/org/opentripplanner/ext/vehicleparking/bikeep/BikeepUpdaterParameters.java +++ b/src/ext/java/org/opentripplanner/ext/vehicleparking/bikeep/BikeepUpdaterParameters.java @@ -1,5 +1,7 @@ package org.opentripplanner.ext.vehicleparking.bikeep; +import static org.opentripplanner.updater.vehicle_parking.VehicleParkingUpdaterParameters.UpdateType.FULL; + import java.net.URI; import java.time.Duration; import org.opentripplanner.updater.spi.HttpHeaders; @@ -22,4 +24,9 @@ public record BikeepUpdaterParameters( public VehicleParkingSourceType sourceType() { return VehicleParkingSourceType.BIKEEP; } + + @Override + public UpdateType updateType() { + return FULL; + } } diff --git a/src/ext/java/org/opentripplanner/ext/vehicleparking/bikely/BikelyUpdaterParameters.java b/src/ext/java/org/opentripplanner/ext/vehicleparking/bikely/BikelyUpdaterParameters.java index 26e40f4ec4a..73f26a43aa4 100644 --- a/src/ext/java/org/opentripplanner/ext/vehicleparking/bikely/BikelyUpdaterParameters.java +++ b/src/ext/java/org/opentripplanner/ext/vehicleparking/bikely/BikelyUpdaterParameters.java @@ -1,5 +1,7 @@ package org.opentripplanner.ext.vehicleparking.bikely; +import static org.opentripplanner.updater.vehicle_parking.VehicleParkingUpdaterParameters.UpdateType.FULL; + import java.net.URI; import java.time.Duration; import org.opentripplanner.updater.spi.HttpHeaders; @@ -22,4 +24,9 @@ public record BikelyUpdaterParameters( public VehicleParkingSourceType sourceType() { return VehicleParkingSourceType.BIKELY; } + + @Override + public UpdateType updateType() { + return FULL; + } } diff --git a/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslParkUpdaterParameters.java b/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslParkUpdaterParameters.java index 4b75d38ea6f..b4ad24ae080 100644 --- a/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslParkUpdaterParameters.java +++ b/src/ext/java/org/opentripplanner/ext/vehicleparking/hslpark/HslParkUpdaterParameters.java @@ -1,5 +1,7 @@ package org.opentripplanner.ext.vehicleparking.hslpark; +import static org.opentripplanner.updater.vehicle_parking.VehicleParkingUpdaterParameters.UpdateType.FULL; + import java.time.Duration; import java.time.ZoneId; import org.opentripplanner.updater.vehicle_parking.VehicleParkingSourceType; @@ -25,4 +27,9 @@ public record HslParkUpdaterParameters( public Duration frequency() { return Duration.ofSeconds(utilizationsFrequencySec); } + + @Override + public UpdateType updateType() { + return FULL; + } } diff --git a/src/ext/java/org/opentripplanner/ext/vehicleparking/noi/NoiUpdaterParameters.java b/src/ext/java/org/opentripplanner/ext/vehicleparking/noi/NoiUpdaterParameters.java index 371ffdc9f33..b34769c9dd2 100644 --- a/src/ext/java/org/opentripplanner/ext/vehicleparking/noi/NoiUpdaterParameters.java +++ b/src/ext/java/org/opentripplanner/ext/vehicleparking/noi/NoiUpdaterParameters.java @@ -1,5 +1,7 @@ package org.opentripplanner.ext.vehicleparking.noi; +import static org.opentripplanner.updater.vehicle_parking.VehicleParkingUpdaterParameters.UpdateType.FULL; + import java.net.URI; import java.time.Duration; import org.opentripplanner.updater.spi.HttpHeaders; @@ -22,4 +24,9 @@ public record NoiUpdaterParameters( public VehicleParkingSourceType sourceType() { return VehicleParkingSourceType.NOI_OPEN_DATA_HUB; } + + @Override + public UpdateType updateType() { + return FULL; + } } diff --git a/src/ext/java/org/opentripplanner/ext/vehicleparking/parkapi/ParkAPIUpdaterParameters.java b/src/ext/java/org/opentripplanner/ext/vehicleparking/parkapi/ParkAPIUpdaterParameters.java index 263987c60d0..3014d932e08 100644 --- a/src/ext/java/org/opentripplanner/ext/vehicleparking/parkapi/ParkAPIUpdaterParameters.java +++ b/src/ext/java/org/opentripplanner/ext/vehicleparking/parkapi/ParkAPIUpdaterParameters.java @@ -1,5 +1,7 @@ package org.opentripplanner.ext.vehicleparking.parkapi; +import static org.opentripplanner.updater.vehicle_parking.VehicleParkingUpdaterParameters.UpdateType.FULL; + import java.time.Duration; import java.time.ZoneId; import java.util.List; @@ -21,4 +23,9 @@ public record ParkAPIUpdaterParameters( VehicleParkingSourceType sourceType, ZoneId timeZone ) - implements VehicleParkingUpdaterParameters {} + implements VehicleParkingUpdaterParameters { + @Override + public UpdateType updateType() { + return FULL; + } +} diff --git a/src/ext/java/org/opentripplanner/ext/vehicleparking/sirifm/SiriFmDatasource.java b/src/ext/java/org/opentripplanner/ext/vehicleparking/sirifm/SiriFmDatasource.java new file mode 100644 index 00000000000..abfdf4d29be --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/vehicleparking/sirifm/SiriFmDatasource.java @@ -0,0 +1,90 @@ +package org.opentripplanner.ext.vehicleparking.sirifm; + +import static uk.org.siri.siri21.CountingTypeEnumeration.PRESENT_COUNT; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.entur.siri21.util.SiriXml; +import org.opentripplanner.framework.io.OtpHttpClient; +import org.opentripplanner.framework.io.OtpHttpClientFactory; +import org.opentripplanner.framework.tostring.ToStringBuilder; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.updater.spi.DataSource; +import org.opentripplanner.updater.spi.HttpHeaders; +import org.opentripplanner.updater.vehicle_parking.AvailabiltyUpdate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import uk.org.siri.siri21.FacilityConditionStructure; + +/** + * Parses SIRI 2.1 XML data into parking availability updates. The data needs to conform to the + * Italian profile of SIRI-FM. + */ +public class SiriFmDatasource implements DataSource { + + private static final Logger LOG = LoggerFactory.getLogger(SiriFmDatasource.class); + private final SiriFmUpdaterParameters params; + private final OtpHttpClient httpClient; + private final Map headers; + private List updates = List.of(); + + public SiriFmDatasource(SiriFmUpdaterParameters parameters) { + params = parameters; + headers = HttpHeaders.of().acceptApplicationXML().add(parameters.httpHeaders()).build().asMap(); + httpClient = new OtpHttpClientFactory().create(LOG); + } + + @Override + public String toString() { + return ToStringBuilder + .of(this.getClass()) + .addStr("url", this.params.url().toString()) + .toString(); + } + + @Override + public boolean update() { + updates = + httpClient.getAndMap( + params.url(), + headers, + resp -> { + var siri = SiriXml.parseXml(resp); + + return Stream + .ofNullable(siri.getServiceDelivery()) + .flatMap(sd -> sd.getFacilityMonitoringDeliveries().stream()) + .flatMap(d -> d.getFacilityConditions().stream()) + .filter(this::conformsToItalianProfile) + .map(this::mapToUpdate) + .toList(); + } + ); + return true; + } + + private AvailabiltyUpdate mapToUpdate(FacilityConditionStructure c) { + var id = new FeedScopedId(params.feedId(), c.getFacilityRef().getValue()); + var available = c.getMonitoredCountings().getFirst().getCount().intValue(); + return new AvailabiltyUpdate(id, available); + } + + /** + * Checks if the {@link FacilityConditionStructure} contains all the necessary information that + * are required by the Italian Siri-FM profile. + */ + private boolean conformsToItalianProfile(FacilityConditionStructure c) { + return ( + c.getFacilityRef() != null && + c.getFacilityRef().getValue() != null && + c.getMonitoredCountings().size() == 1 && + c.getMonitoredCountings().getFirst().getCountingType() == PRESENT_COUNT + ); + } + + @Override + public List getUpdates() { + return updates; + } +} diff --git a/src/ext/java/org/opentripplanner/ext/vehicleparking/sirifm/SiriFmUpdaterParameters.java b/src/ext/java/org/opentripplanner/ext/vehicleparking/sirifm/SiriFmUpdaterParameters.java new file mode 100644 index 00000000000..5afdffb7067 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/vehicleparking/sirifm/SiriFmUpdaterParameters.java @@ -0,0 +1,34 @@ +package org.opentripplanner.ext.vehicleparking.sirifm; + +import static org.opentripplanner.updater.vehicle_parking.VehicleParkingSourceType.SIRI_FM; +import static org.opentripplanner.updater.vehicle_parking.VehicleParkingUpdaterParameters.UpdateType.AVAILABILITY_ONLY; + +import java.net.URI; +import java.time.Duration; +import org.opentripplanner.ext.vehicleparking.noi.NoiUpdater; +import org.opentripplanner.updater.spi.HttpHeaders; +import org.opentripplanner.updater.vehicle_parking.VehicleParkingSourceType; +import org.opentripplanner.updater.vehicle_parking.VehicleParkingUpdaterParameters; + +/** + * Class that extends {@link VehicleParkingUpdaterParameters} with parameters required by {@link + * NoiUpdater}. + */ +public record SiriFmUpdaterParameters( + String configRef, + URI url, + String feedId, + Duration frequency, + HttpHeaders httpHeaders +) + implements VehicleParkingUpdaterParameters { + @Override + public VehicleParkingSourceType sourceType() { + return SIRI_FM; + } + + @Override + public UpdateType updateType() { + return AVAILABILITY_ONLY; + } +} 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/generated/package.json b/src/main/java/org/opentripplanner/apis/gtfs/generated/package.json index 6a840640ca9..d6cbf02d5e7 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/package.json +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/package.json @@ -10,10 +10,10 @@ }, "license": "LGPL-3.0", "dependencies": { - "@graphql-codegen/add": "5.0.2", + "@graphql-codegen/add": "5.0.3", "@graphql-codegen/cli": "5.0.2", "@graphql-codegen/java": "4.0.1", "@graphql-codegen/java-resolvers": "3.0.0", - "graphql": "16.8.1" + "graphql": "16.9.0" } } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/yarn.lock b/src/main/java/org/opentripplanner/apis/gtfs/generated/yarn.lock index 77829ecc911..25686abd94a 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/yarn.lock +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/yarn.lock @@ -699,7 +699,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" -"@graphql-codegen/add@5.0.2", "@graphql-codegen/add@^5.0.2": +"@graphql-codegen/add@5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@graphql-codegen/add/-/add-5.0.3.tgz#1ede6bac9a93661ed7fa5808b203d079e1b1d215" + integrity sha512-SxXPmramkth8XtBlAHu4H4jYcYXM/o3p01+psU+0NADQowA8jtYkK6MW5rV6T+CxkEaNZItfSmZRPgIuypcqnA== + dependencies: + "@graphql-codegen/plugin-helpers" "^5.0.3" + tslib "~2.6.0" + +"@graphql-codegen/add@^5.0.2": version "5.0.2" resolved "https://registry.yarnpkg.com/@graphql-codegen/add/-/add-5.0.2.tgz#71b3ae0465a4537172dddb84531b6967ca5545f2" integrity sha512-ouBkSvMFUhda5VoKumo/ZvsZM9P5ZTyDsI8LW18VxSNWOjrTeLXBWHG8Gfaai0HwhflPtCYVABbriEcOmrRShQ== @@ -2173,10 +2181,10 @@ graphql-ws@^5.14.0: resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.14.0.tgz#766f249f3974fc2c48fae0d1fb20c2c4c79cd591" integrity sha512-itrUTQZP/TgswR4GSSYuwWUzrE/w5GhbwM2GX3ic2U7aw33jgEsayfIlvaj7/GcIvZgNMzsPTrE5hqPuFUiE5g== -graphql@16.8.1: - version "16.8.1" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" - integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== +graphql@16.9.0: + version "16.9.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.9.0.tgz#1c310e63f16a49ce1fbb230bd0a000e99f6f115f" + integrity sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw== has-flag@^3.0.0: version "3.0.0" 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/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/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/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/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/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/NetexMapper.java b/src/main/java/org/opentripplanner/netex/mapping/NetexMapper.java index 991c0477266..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(); } @@ -520,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/routing/algorithm/raptoradapter/transit/TransitLayer.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java index c92bbd750ad..e5f0544f584 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java @@ -1,7 +1,6 @@ package org.opentripplanner.routing.algorithm.raptoradapter.transit; import java.time.LocalDate; -import java.time.ZoneId; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -51,8 +50,6 @@ public class TransitLayer { private final StopModel stopModel; - private final ZoneId transitDataZoneId; - private final RaptorRequestTransferCache transferCache; private ConstrainedTransfersForPatterns constrainedTransfers; @@ -73,7 +70,6 @@ public TransitLayer(TransitLayer transitLayer) { transitLayer.transfersByStopIndex, transitLayer.transferService, transitLayer.stopModel, - transitLayer.transitDataZoneId, transitLayer.transferCache, transitLayer.constrainedTransfers, transitLayer.transferIndexGenerator, @@ -86,7 +82,6 @@ public TransitLayer( List> transfersByStopIndex, TransferService transferService, StopModel stopModel, - ZoneId transitDataZoneId, RaptorRequestTransferCache transferCache, ConstrainedTransfersForPatterns constrainedTransfers, TransferIndexGenerator transferIndexGenerator, @@ -96,7 +91,6 @@ public TransitLayer( this.transfersByStopIndex = transfersByStopIndex; this.transferService = transferService; this.stopModel = stopModel; - this.transitDataZoneId = transitDataZoneId; this.transferCache = transferCache; this.constrainedTransfers = constrainedTransfers; this.transferIndexGenerator = transferIndexGenerator; @@ -117,16 +111,6 @@ public Collection getTripPatternsForRunningDate(LocalDate da return tripPatternsRunningOnDate.getOrDefault(date, List.of()); } - /** - * This is the time zone which is used for interpreting all local "service" times (in transfers, - * trip schedules and so on). This is the time zone of the internal OTP time - which is used in - * logging and debugging. This is independent of the time zone of imported data and of the time - * zone used on any API - it can be the same, but it does not need to. - */ - public ZoneId getTransitDataZoneId() { - return transitDataZoneId; - } - public int getStopCount() { return stopModel.stopIndexSize(); } 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 8ce328fe1b6..d375b4c546c 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 @@ -103,7 +103,6 @@ private TransitLayer map(TransitTuningParameters tuningParameters) { transferByStopIndex, transitService.getTransferService(), stopModel, - transitService.getTimeZone(), transferCache, constrainedTransfers, transferIndexGenerator, 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..098af909296 100644 --- a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParking.java +++ b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParking.java @@ -96,8 +96,10 @@ public class VehicleParking implements Serializable { private final List entrances = new ArrayList<>(); /** * The currently available spaces at this vehicle parking. + *

+ * The volatile keyword is used to ensure safe publication by clearing CPU caches. */ - private VehicleParkingSpaces availability; + private volatile VehicleParkingSpaces availability; /** * The vehicle parking group this parking belongs to. */ @@ -239,19 +241,24 @@ public boolean hasRealTimeDataForMode( return false; } - switch (traverseMode) { - case BICYCLE: - return availability.getBicycleSpaces() != null; - case CAR: + return switch (traverseMode) { + case BICYCLE -> availability.getBicycleSpaces() != null; + case CAR -> { var places = wheelchairAccessibleCarPlaces ? availability.getWheelchairAccessibleCarSpaces() : availability.getCarSpaces(); - return places != null; - default: - return false; - } + yield places != null; + } + default -> false; + }; } + /** + * The only mutable method in this class: it allows to update the available parking spaces during + * real-time updates. + * Since the entity is used both by writer threads (real-time updates) and reader threads + * (A* routing), the variable holding the information is marked as volatile. + */ public void updateAvailability(VehicleParkingSpaces vehicleParkingSpaces) { this.availability = vehicleParkingSpaces; } @@ -308,6 +315,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/VehicleParkingService.java b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingService.java index 639066871be..b0c08a2309b 100644 --- a/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingService.java +++ b/src/main/java/org/opentripplanner/routing/vehicle_parking/VehicleParkingService.java @@ -21,13 +21,17 @@ public class VehicleParkingService implements Serializable { /** * To ensure that his is thread-safe, the set stored here should always be immutable. + *

+ * The volatile keyword is used to ensure safe publication by clearing CPU caches. */ - private Set vehicleParkings = Set.of(); + private volatile Set vehicleParkings = Set.of(); /** * To ensure that his is thread-safe, {@link ImmutableListMultimap} is used. + *

+ * The volatile keyword is used to ensure safe publication by clearing CPU caches. */ - private ImmutableListMultimap vehicleParkingGroups = ImmutableListMultimap.of(); + private volatile ImmutableListMultimap vehicleParkingGroups = ImmutableListMultimap.of(); /** * Does atomic update of {@link VehicleParking} and index of {@link VehicleParkingGroup} in this 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/service/realtimevehicles/internal/DefaultRealtimeVehicleService.java b/src/main/java/org/opentripplanner/service/realtimevehicles/internal/DefaultRealtimeVehicleService.java index 07fa48fa84f..0058cdd9e15 100644 --- a/src/main/java/org/opentripplanner/service/realtimevehicles/internal/DefaultRealtimeVehicleService.java +++ b/src/main/java/org/opentripplanner/service/realtimevehicles/internal/DefaultRealtimeVehicleService.java @@ -8,7 +8,6 @@ import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nonnull; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; @@ -33,8 +32,16 @@ public DefaultRealtimeVehicleService(TransitService transitService) { this.transitService = transitService; } + /** + * Stores the relationship between a list of realtime vehicles with a pattern. If the pattern is + * a realtime-added one, then the original (scheduled) one is used as the key for the map storing + * the information. + */ @Override public void setRealtimeVehicles(TripPattern pattern, List updates) { + if (pattern.getOriginalTripPattern() != null) { + pattern = pattern.getOriginalTripPattern(); + } vehicles.put(pattern, List.copyOf(updates)); } @@ -43,8 +50,18 @@ public void clearRealtimeVehicles(TripPattern pattern) { vehicles.remove(pattern); } + /** + * Gets the realtime vehicles for a given pattern. If the pattern is a realtime-added one + * then the original (scheduled) one is used for the lookup instead, so you receive the correct + * result no matter if you use the realtime or static information. + * + * @see DefaultRealtimeVehicleService#setRealtimeVehicles(TripPattern, List) + */ @Override public List getRealtimeVehicles(@Nonnull TripPattern pattern) { + if (pattern.getOriginalTripPattern() != null) { + pattern = pattern.getOriginalTripPattern(); + } // the list is made immutable during insertion, so we can safely return them return vehicles.getOrDefault(pattern, List.of()); } 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/routerconfig/updaters/VehicleParkingUpdaterConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/VehicleParkingUpdaterConfig.java index c687899009f..88b47aac4eb 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/VehicleParkingUpdaterConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/VehicleParkingUpdaterConfig.java @@ -13,6 +13,7 @@ import org.opentripplanner.ext.vehicleparking.hslpark.HslParkUpdaterParameters; import org.opentripplanner.ext.vehicleparking.noi.NoiUpdaterParameters; import org.opentripplanner.ext.vehicleparking.parkapi.ParkAPIUpdaterParameters; +import org.opentripplanner.ext.vehicleparking.sirifm.SiriFmUpdaterParameters; import org.opentripplanner.standalone.config.framework.json.NodeAdapter; import org.opentripplanner.updater.vehicle_parking.VehicleParkingSourceType; import org.opentripplanner.updater.vehicle_parking.VehicleParkingUpdaterParameters; @@ -29,7 +30,7 @@ public static VehicleParkingUpdaterParameters create(String updaterRef, NodeAdap .of("feedId") .since(V2_2) .summary("The id of the data source, which will be the prefix of the parking lot's id.") - .description("This will end up in the API responses as the feed id of of the parking lot.") + .description("This will end up in the API responses as the feed id of the parking lot.") .asString(); return switch (sourceType) { case HSL_PARK -> new HslParkUpdaterParameters( @@ -100,6 +101,30 @@ public static VehicleParkingUpdaterParameters create(String updaterRef, NodeAdap .asDuration(Duration.ofMinutes(1)), HttpHeadersConfig.headers(c, V2_6) ); + case SIRI_FM -> new SiriFmUpdaterParameters( + updaterRef, + c + .of("url") + .since(V2_6) + .summary("URL of the SIRI-FM Light endpoint.") + .description( + """ + SIRI Light means that it must be available as a HTTP GET request rather than the usual + SIRI request mechanism of HTTP POST. + + The contents must also conform to the [Italian SIRI profile](https://github.com/5Tsrl/siri-italian-profile) + which requires SIRI 2.1. + """ + ) + .asUri(), + feedId, + c + .of("frequency") + .since(V2_6) + .summary("How often to update the source.") + .asDuration(Duration.ofMinutes(1)), + HttpHeadersConfig.headers(c, V2_6) + ); }; } 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 7d07ca29f0e..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; @@ -181,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(); } } 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/updater/configure/UpdaterConfigurator.java b/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java index 103120b7ecb..83e0bd0fe85 100644 --- a/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java +++ b/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java @@ -25,13 +25,13 @@ import org.opentripplanner.updater.trip.MqttGtfsRealtimeUpdater; import org.opentripplanner.updater.trip.PollingTripUpdater; import org.opentripplanner.updater.trip.TimetableSnapshotSource; +import org.opentripplanner.updater.vehicle_parking.AvailabilityDatasourceFactory; +import org.opentripplanner.updater.vehicle_parking.VehicleParkingAvailabilityUpdater; import org.opentripplanner.updater.vehicle_parking.VehicleParkingDataSourceFactory; import org.opentripplanner.updater.vehicle_parking.VehicleParkingUpdater; import org.opentripplanner.updater.vehicle_position.PollingVehiclePositionUpdater; import org.opentripplanner.updater.vehicle_rental.VehicleRentalUpdater; import org.opentripplanner.updater.vehicle_rental.datasources.VehicleRentalDataSourceFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Sets up and starts all the graph updaters. @@ -42,8 +42,6 @@ */ public class UpdaterConfigurator { - private static final Logger LOG = LoggerFactory.getLogger(UpdaterConfigurator.class); - private final Graph graph; private final TransitModel transitModel; private final UpdatersParameters updatersParameters; @@ -187,15 +185,32 @@ private List createUpdatersFromConfig() { ); } for (var configItem : updatersParameters.getVehicleParkingUpdaterParameters()) { - var source = VehicleParkingDataSourceFactory.create(configItem, openingHoursCalendarService); - updaters.add( - new VehicleParkingUpdater( - configItem, - source, - graph.getLinker(), - graph.getVehicleParkingService() - ) - ); + switch (configItem.updateType()) { + case FULL -> { + var source = VehicleParkingDataSourceFactory.create( + configItem, + openingHoursCalendarService + ); + updaters.add( + new VehicleParkingUpdater( + configItem, + source, + graph.getLinker(), + graph.getVehicleParkingService() + ) + ); + } + case AVAILABILITY_ONLY -> { + var source = AvailabilityDatasourceFactory.create(configItem); + updaters.add( + new VehicleParkingAvailabilityUpdater( + configItem, + source, + graph.getVehicleParkingService() + ) + ); + } + } } for (var configItem : updatersParameters.getSiriAzureETUpdaterParameters()) { updaters.add( diff --git a/src/main/java/org/opentripplanner/updater/vehicle_parking/AvailabilityDatasourceFactory.java b/src/main/java/org/opentripplanner/updater/vehicle_parking/AvailabilityDatasourceFactory.java new file mode 100644 index 00000000000..876cd303c78 --- /dev/null +++ b/src/main/java/org/opentripplanner/updater/vehicle_parking/AvailabilityDatasourceFactory.java @@ -0,0 +1,23 @@ +package org.opentripplanner.updater.vehicle_parking; + +import org.opentripplanner.ext.vehicleparking.sirifm.SiriFmDatasource; +import org.opentripplanner.ext.vehicleparking.sirifm.SiriFmUpdaterParameters; +import org.opentripplanner.updater.spi.DataSource; + +/** + * Class that can be used to return a custom vehicle parking {@link DataSource}. + */ +public class AvailabilityDatasourceFactory { + + public static DataSource create(VehicleParkingUpdaterParameters parameters) { + return switch (parameters.sourceType()) { + case SIRI_FM -> new SiriFmDatasource((SiriFmUpdaterParameters) parameters); + case PARK_API, + BICYCLE_PARK_API, + HSL_PARK, + BIKEEP, + NOI_OPEN_DATA_HUB, + BIKELY -> throw new IllegalArgumentException("Cannot instantiate SIRI-FM data source"); + }; + } +} diff --git a/src/main/java/org/opentripplanner/updater/vehicle_parking/AvailabiltyUpdate.java b/src/main/java/org/opentripplanner/updater/vehicle_parking/AvailabiltyUpdate.java new file mode 100644 index 00000000000..a9d352cbaf2 --- /dev/null +++ b/src/main/java/org/opentripplanner/updater/vehicle_parking/AvailabiltyUpdate.java @@ -0,0 +1,12 @@ +package org.opentripplanner.updater.vehicle_parking; + +import java.util.Objects; +import org.opentripplanner.framework.lang.IntUtils; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +public record AvailabiltyUpdate(FeedScopedId vehicleParkingId, int spacesAvailable) { + public AvailabiltyUpdate { + Objects.requireNonNull(vehicleParkingId); + IntUtils.requireNotNegative(spacesAvailable); + } +} diff --git a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java new file mode 100644 index 00000000000..31074aafe38 --- /dev/null +++ b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdater.java @@ -0,0 +1,104 @@ +package org.opentripplanner.updater.vehicle_parking; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.opentripplanner.framework.tostring.ToStringBuilder; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.vehicle_parking.VehicleParking; +import org.opentripplanner.routing.vehicle_parking.VehicleParkingService; +import org.opentripplanner.routing.vehicle_parking.VehicleParkingSpaces; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.updater.GraphWriterRunnable; +import org.opentripplanner.updater.spi.DataSource; +import org.opentripplanner.updater.spi.PollingGraphUpdater; +import org.opentripplanner.updater.spi.WriteToGraphCallback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Graph updater that dynamically sets availability information on vehicle parking lots. This + * updater fetches data from a single {@link DataSource}. + */ +public class VehicleParkingAvailabilityUpdater extends PollingGraphUpdater { + + private static final Logger LOG = LoggerFactory.getLogger( + VehicleParkingAvailabilityUpdater.class + ); + private final DataSource source; + private WriteToGraphCallback saveResultOnGraph; + + private final VehicleParkingService vehicleParkingService; + + public VehicleParkingAvailabilityUpdater( + VehicleParkingUpdaterParameters parameters, + DataSource source, + VehicleParkingService vehicleParkingService + ) { + super(parameters); + this.source = source; + this.vehicleParkingService = vehicleParkingService; + + LOG.info("Creating vehicle-parking updater running every {}: {}", pollingPeriod(), source); + } + + @Override + public void setup(WriteToGraphCallback writeToGraphCallback) { + this.saveResultOnGraph = writeToGraphCallback; + } + + @Override + protected void runPolling() { + if (source.update()) { + var updates = source.getUpdates(); + + var graphWriterRunnable = new AvailabilityUpdater(updates); + saveResultOnGraph.execute(graphWriterRunnable); + } + } + + private class AvailabilityUpdater implements GraphWriterRunnable { + + private final List updates; + private final Map parkingById; + + private AvailabilityUpdater(List updates) { + this.updates = List.copyOf(updates); + this.parkingById = + vehicleParkingService + .getVehicleParkings() + .collect(Collectors.toUnmodifiableMap(VehicleParking::getId, Function.identity())); + } + + @Override + public void run(Graph graph, TransitModel ignored) { + updates.forEach(this::handleUpdate); + } + + private void handleUpdate(AvailabiltyUpdate update) { + if (!parkingById.containsKey(update.vehicleParkingId())) { + LOG.warn( + "Parking with id {} does not exist. Skipping availability update.", + update.vehicleParkingId() + ); + } else { + var parking = parkingById.get(update.vehicleParkingId()); + var builder = VehicleParkingSpaces.builder(); + if (parking.hasCarPlaces()) { + builder.carSpaces(update.spacesAvailable()); + } + if (parking.hasBicyclePlaces()) { + builder.bicycleSpaces(update.spacesAvailable()); + } + parking.updateAvailability(builder.build()); + } + } + } + + @Override + public String toString() { + return ToStringBuilder.of(this.getClass()).addObj("source", source).toString(); + } +} diff --git a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingDataSourceFactory.java b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingDataSourceFactory.java index a956fda0d87..09ecb67a54d 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingDataSourceFactory.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingDataSourceFactory.java @@ -42,6 +42,7 @@ public static DataSource create( case BIKELY -> new BikelyUpdater((BikelyUpdaterParameters) parameters); case NOI_OPEN_DATA_HUB -> new NoiUpdater((NoiUpdaterParameters) parameters); case BIKEEP -> new BikeepUpdater((BikeepUpdaterParameters) parameters); + case SIRI_FM -> throw new IllegalArgumentException("Cannot instantiate SIRI-FM data source"); }; } } diff --git a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingSourceType.java b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingSourceType.java index f6a28177d8e..1601245b16c 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingSourceType.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingSourceType.java @@ -7,4 +7,5 @@ public enum VehicleParkingSourceType { BIKELY, NOI_OPEN_DATA_HUB, BIKEEP, + SIRI_FM, } diff --git a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterParameters.java b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterParameters.java index 3422ec3e300..bff0022383f 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterParameters.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterParameters.java @@ -8,4 +8,10 @@ */ public interface VehicleParkingUpdaterParameters extends PollingGraphUpdaterParameters { VehicleParkingSourceType sourceType(); + UpdateType updateType(); + + enum UpdateType { + FULL, + AVAILABILITY_ONLY, + } } 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/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/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/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/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/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/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java b/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java index 5da127de2ba..8f5d1a0208e 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java @@ -237,7 +237,6 @@ private static TransitLayer getTransitLayer() { null, null, null, - null, null ); } 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/TransitLayerTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayerTest.java index 7c674252e6a..58a56ccb96f 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayerTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayerTest.java @@ -65,7 +65,6 @@ void testGetTripPatternsRunningOnDateCopy() { null, null, null, - null, null ); var runningOnDate = transitLayer.getTripPatternsRunningOnDateCopy(date); @@ -95,7 +94,6 @@ void testGetTripPatternsForRunningDate() { null, null, null, - null, null ); var runningOnDate = transitLayer.getTripPatternsForRunningDate(date); @@ -124,7 +122,6 @@ void testGetTripPatternsOnServiceDateCopyWithSameRunningAndServiceDate() { null, null, null, - null, null ); var startingOnDate = transitLayer.getTripPatternsOnServiceDateCopy(date); @@ -153,7 +150,6 @@ void testGetTripPatternsOnServiceDateCopyWithServiceRunningAfterMidnight() { null, null, null, - null, null ); var startingOnDate = transitLayer.getTripPatternsOnServiceDateCopy(serviceDate); @@ -187,7 +183,6 @@ void testGetTripPatternsOnServiceDateCopyWithServiceRunningBeforeAndAfterMidnigh null, null, null, - null, null ); var startingOnDate = transitLayer.getTripPatternsOnServiceDateCopy(firstRunningDate); diff --git a/src/test/java/org/opentripplanner/service/realtimevehicles/internal/DefaultRealtimeVehicleServiceTest.java b/src/test/java/org/opentripplanner/service/realtimevehicles/internal/DefaultRealtimeVehicleServiceTest.java new file mode 100644 index 00000000000..a1c9dddaab0 --- /dev/null +++ b/src/test/java/org/opentripplanner/service/realtimevehicles/internal/DefaultRealtimeVehicleServiceTest.java @@ -0,0 +1,55 @@ +package org.opentripplanner.service.realtimevehicles.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.framework.geometry.WgsCoordinate.GREENWICH; +import static org.opentripplanner.transit.model._data.TransitModelForTest.route; +import static org.opentripplanner.transit.model._data.TransitModelForTest.tripPattern; + +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; +import org.opentripplanner.transit.model._data.TransitModelForTest; +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.service.DefaultTransitService; +import org.opentripplanner.transit.service.TransitModel; + +class DefaultRealtimeVehicleServiceTest { + + private static final Route ROUTE = route("r1").build(); + private static final TransitModelForTest MODEL = TransitModelForTest.of(); + private static final StopPattern STOP_PATTERN = TransitModelForTest.stopPattern( + MODEL.stop("1").build(), + MODEL.stop("2").build() + ); + private static final TripPattern ORIGINAL = tripPattern("original", ROUTE) + .withStopPattern(STOP_PATTERN) + .build(); + private static final Instant TIME = Instant.ofEpochSecond(1000); + private static final List VEHICLES = List.of( + RealtimeVehicle.builder().withTime(TIME).withCoordinates(GREENWICH).build() + ); + + @Test + void originalPattern() { + var service = new DefaultRealtimeVehicleService(new DefaultTransitService(new TransitModel())); + service.setRealtimeVehicles(ORIGINAL, VEHICLES); + var updates = service.getRealtimeVehicles(ORIGINAL); + assertEquals(VEHICLES, updates); + } + + @Test + void realtimeAddedPattern() { + var service = new DefaultRealtimeVehicleService(new DefaultTransitService(new TransitModel())); + var realtimePattern = tripPattern("realtime-added", ROUTE) + .withStopPattern(STOP_PATTERN) + .withOriginalTripPattern(ORIGINAL) + .withCreatedByRealtimeUpdater(true) + .build(); + service.setRealtimeVehicles(realtimePattern, VEHICLES); + var updates = service.getRealtimeVehicles(ORIGINAL); + assertEquals(VEHICLES, updates); + } +} 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/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/vehicle_parking/VehicleParkingAvailabilityUpdaterTest.java b/src/test/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdaterTest.java new file mode 100644 index 00000000000..87222eeba8f --- /dev/null +++ b/src/test/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingAvailabilityUpdaterTest.java @@ -0,0 +1,156 @@ +package org.opentripplanner.updater.vehicle_parking; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.opentripplanner.standalone.config.framework.json.JsonSupport.newNodeAdapterForTest; +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; + +import com.google.common.util.concurrent.Futures; +import java.util.List; +import java.util.concurrent.Future; +import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.geometry.WgsCoordinate; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.vehicle_parking.VehicleParking; +import org.opentripplanner.routing.vehicle_parking.VehicleParkingService; +import org.opentripplanner.routing.vehicle_parking.VehicleParkingSpaces; +import org.opentripplanner.standalone.config.routerconfig.updaters.VehicleParkingUpdaterConfig; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.updater.GraphUpdaterManager; +import org.opentripplanner.updater.GraphWriterRunnable; +import org.opentripplanner.updater.spi.DataSource; +import org.opentripplanner.updater.spi.GraphUpdater; + +class VehicleParkingAvailabilityUpdaterTest { + + private static final VehicleParkingUpdaterParameters PARAMETERS = VehicleParkingUpdaterConfig.create( + "ref", + newNodeAdapterForTest( + """ + { + "type" : "vehicle-parking", + "feedId" : "parking", + "sourceType" : "siri-fm", + "frequency": "0s", + "url" : "https://transmodel.api.opendatahub.com/siri-lite/fm/parking" + } + """ + ) + ); + + private static final FeedScopedId ID = id("parking1"); + private static final AvailabiltyUpdate DEFAULT_UPDATE = new AvailabiltyUpdate(ID, 8); + + @Test + void updateCarAvailability() { + var service = buildParkingService(VehicleParkingSpaces.builder().carSpaces(10).build()); + var updater = new VehicleParkingAvailabilityUpdater( + PARAMETERS, + new StubDatasource(DEFAULT_UPDATE), + service + ); + + runUpdaterOnce(updater); + + var updated = service.getVehicleParkings().toList().getFirst(); + assertEquals(ID, updated.getId()); + assertEquals(8, updated.getAvailability().getCarSpaces()); + assertNull(updated.getAvailability().getBicycleSpaces()); + } + + @Test + void updateBicycleAvailability() { + var service = buildParkingService(VehicleParkingSpaces.builder().bicycleSpaces(15).build()); + var updater = new VehicleParkingAvailabilityUpdater( + PARAMETERS, + new StubDatasource(DEFAULT_UPDATE), + service + ); + + runUpdaterOnce(updater); + + var updated = service.getVehicleParkings().toList().getFirst(); + assertEquals(ID, updated.getId()); + assertEquals(8, updated.getAvailability().getBicycleSpaces()); + assertNull(updated.getAvailability().getCarSpaces()); + } + + @Test + void notFound() { + var service = buildParkingService(VehicleParkingSpaces.builder().bicycleSpaces(15).build()); + var updater = new VehicleParkingAvailabilityUpdater( + PARAMETERS, + new StubDatasource(new AvailabiltyUpdate(id("not-found"), 100)), + service + ); + + runUpdaterOnce(updater); + + var updated = service.getVehicleParkings().toList().getFirst(); + assertEquals(ID, updated.getId()); + assertNull(updated.getAvailability()); + } + + private static VehicleParkingService buildParkingService(VehicleParkingSpaces capacity) { + var service = new VehicleParkingService(); + + var parking = parkingBuilder() + .carPlaces(capacity.getCarSpaces() != null) + .bicyclePlaces(capacity.getBicycleSpaces() != null) + .capacity(capacity) + .build(); + service.updateVehicleParking(List.of(parking), List.of()); + return service; + } + + private static VehicleParking.VehicleParkingBuilder parkingBuilder() { + return VehicleParking + .builder() + .id(ID) + .name(I18NString.of("parking")) + .coordinate(WgsCoordinate.GREENWICH); + } + + private void runUpdaterOnce(VehicleParkingAvailabilityUpdater updater) { + class GraphUpdaterMock extends GraphUpdaterManager { + + private static final Graph GRAPH = new Graph(); + private static final TransitModel TRANSIT_MODEL = new TransitModel(); + + public GraphUpdaterMock(List updaters) { + super(GRAPH, TRANSIT_MODEL, updaters); + } + + @Override + public Future execute(GraphWriterRunnable runnable) { + runnable.run(GRAPH, TRANSIT_MODEL); + return Futures.immediateVoidFuture(); + } + } + + var graphUpdaterManager = new GraphUpdaterMock(List.of(updater)); + graphUpdaterManager.startUpdaters(); + graphUpdaterManager.stop(false); + } + + private static class StubDatasource implements DataSource { + + private final AvailabiltyUpdate update; + + private StubDatasource(AvailabiltyUpdate update) { + this.update = update; + } + + @Override + public boolean update() { + return true; + } + + @Override + public List getUpdates() { + return List.of(update); + } + } +} diff --git a/src/test/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterTest.java b/src/test/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterTest.java index a31b5cfb387..f49299eb4ea 100644 --- a/src/test/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterTest.java +++ b/src/test/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdaterTest.java @@ -54,6 +54,11 @@ public VehicleParkingSourceType sourceType() { return null; } + @Override + public UpdateType updateType() { + return UpdateType.FULL; + } + @Override public Duration frequency() { return Duration.ZERO; 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", diff --git a/src/test/resources/standalone/config/router-config.json b/src/test/resources/standalone/config/router-config.json index 3a3ef9b4cf0..c526de423c1 100644 --- a/src/test/resources/standalone/config/router-config.json +++ b/src/test/resources/standalone/config/router-config.json @@ -438,6 +438,13 @@ "feedId": "bikeep", "sourceType": "bikeep", "url": "https://services.bikeep.com/location/v1/public-areas/no-baia-mobility/locations" + }, + // SIRI-FM vehicle parking updater + { + "type": "vehicle-parking", + "feedId": "parking", + "sourceType": "siri-fm", + "url": "https://transmodel.api.opendatahub.com/siri-lite/fm/parking" } ], "rideHailingServices": [