diff --git a/client-next/package-lock.json b/client-next/package-lock.json index 83f19bf3c48..75fec4ca86c 100644 --- a/client-next/package-lock.json +++ b/client-next/package-lock.json @@ -11,8 +11,8 @@ "@googlemaps/polyline-codec": "1.0.28", "bootstrap": "5.3.3", "graphql": "16.8.1", - "graphql-request": "6.1.0", - "maplibre-gl": "4.2.0", + "graphql-request": "7.0.1", + "maplibre-gl": "4.3.2", "react": "18.3.1", "react-bootstrap": "2.10.2", "react-dom": "18.3.1", @@ -20,27 +20,27 @@ }, "devDependencies": { "@graphql-codegen/cli": "5.0.2", - "@graphql-codegen/client-preset": "4.2.5", + "@graphql-codegen/client-preset": "4.2.6", "@graphql-codegen/introspection": "4.0.3", "@parcel/watcher": "2.4.1", "@testing-library/react": "15.0.7", - "@types/react": "18.3.1", + "@types/react": "18.3.3", "@types/react-dom": "18.3.0", - "@typescript-eslint/eslint-plugin": "7.8.0", - "@typescript-eslint/parser": "7.8.0", - "@vitejs/plugin-react": "4.2.1", + "@typescript-eslint/eslint-plugin": "7.11.0", + "@typescript-eslint/parser": "7.11.0", + "@vitejs/plugin-react": "4.3.0", "@vitest/coverage-v8": "1.6.0", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.29.1", "eslint-plugin-jsx-a11y": "6.8.0", - "eslint-plugin-react": "7.34.1", + "eslint-plugin-react": "7.34.2", "eslint-plugin-react-hooks": "4.6.2", - "eslint-plugin-react-refresh": "0.4.6", - "jsdom": "24.0.0", - "prettier": "3.2.5", + "eslint-plugin-react-refresh": "0.4.7", + "jsdom": "24.1.0", + "prettier": "3.3.0", "typescript": "5.4.5", - "vite": "5.2.11", + "vite": "5.2.12", "vitest": "1.6.0" } }, @@ -214,12 +214,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", + "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.2", + "@babel/highlight": "^7.24.6", "picocolors": "^1.0.0" }, "engines": { @@ -227,30 +228,32 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.1.tgz", - "integrity": "sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.6.tgz", + "integrity": "sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz", - "integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.6.tgz", + "integrity": "sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.1", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.1", - "@babel/parser": "^7.24.1", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/code-frame": "^7.24.6", + "@babel/generator": "^7.24.6", + "@babel/helper-compilation-targets": "^7.24.6", + "@babel/helper-module-transforms": "^7.24.6", + "@babel/helpers": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/template": "^7.24.6", + "@babel/traverse": "^7.24.6", + "@babel/types": "^7.24.6", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -266,12 +269,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz", - "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz", + "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.0", + "@babel/types": "^7.24.6", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -293,13 +297,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz", + "integrity": "sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", + "@babel/compat-data": "^7.24.6", + "@babel/helper-validator-option": "^7.24.6", "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -332,34 +337,37 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", + "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz", + "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz", + "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -378,28 +386,30 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz", + "integrity": "sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.0" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz", + "integrity": "sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-module-imports": "^7.24.6", + "@babel/helper-simple-access": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -421,10 +431,11 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", - "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz", + "integrity": "sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -447,12 +458,13 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz", + "integrity": "sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -471,65 +483,70 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", + "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz", + "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", + "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz", + "integrity": "sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.1.tgz", - "integrity": "sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.6.tgz", + "integrity": "sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0" + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", - "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", + "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.6", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -543,6 +560,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -555,6 +573,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -569,6 +588,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "1.1.3" } @@ -577,13 +597,15 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@babel/highlight/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } @@ -593,6 +615,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -602,6 +625,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -610,10 +634,11 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", - "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", + "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==", "dev": true, + "license": "MIT", "bin": { "parser": "bin/babel-parser.js" }, @@ -1002,12 +1027,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.1.tgz", - "integrity": "sha512-kDJgnPujTmAZ/9q2CN4m2/lRsUUPDvsG3+tSHWUJIzMGTt5U/b/fwWd3RO3n+5mjLrsBrVa5eKFRVSQbi3dF1w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.6.tgz", + "integrity": "sha512-FfZfHXtQ5jYPQsCRyLpOv2GeLIIJhs8aydpNh39vRDjhD411XcfWDni5i7OjP/Rs8GAtTn7sWFFELJSHqkIxYg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.6" }, "engines": { "node": ">=6.9.0" @@ -1089,33 +1115,35 @@ } }, "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", + "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/code-frame": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/types": "^7.24.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", - "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.1", - "@babel/generator": "^7.24.1", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.24.1", - "@babel/types": "^7.24.0", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz", + "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.6", + "@babel/generator": "^7.24.6", + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-function-name": "^7.24.6", + "@babel/helper-hoist-variables": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/types": "^7.24.6", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1124,13 +1152,14 @@ } }, "node_modules/@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", + "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1143,6 +1172,109 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@dprint/darwin-arm64": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@dprint/darwin-arm64/-/darwin-arm64-0.45.1.tgz", + "integrity": "sha512-pH0/uKLJ5SJPoHhOwLWFMhCmL0BY3FzWQbull8OGMK/FRkIPgOl2adZSovtUZpUMGWyDOzIWH1fW9X2DuMhnEg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@dprint/darwin-x64": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@dprint/darwin-x64/-/darwin-x64-0.45.1.tgz", + "integrity": "sha512-YUj421LmBLDlxpIER3pORKfQmpmXD50n5mClHjpZrnl17WTiHtQ+jHvDJdJoxH2eS66W0mQyxLoGo5SfFfiM7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@dprint/formatter": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@dprint/formatter/-/formatter-0.3.0.tgz", + "integrity": "sha512-N9fxCxbaBOrDkteSOzaCqwWjso5iAe+WJPsHC021JfHNj2ThInPNEF13ORDKta3llq5D1TlclODCvOvipH7bWQ==", + "license": "MIT" + }, + "node_modules/@dprint/linux-arm64-glibc": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@dprint/linux-arm64-glibc/-/linux-arm64-glibc-0.45.1.tgz", + "integrity": "sha512-lJ7s/pOQWRJ0mstjZQnVyX2/3QRXZ9cpFHJDZ7e81Y8QSn/iqxTrnK0DPgxUrDG8hYKQmWQdQLU4sP5DKBz0Jg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@dprint/linux-arm64-musl": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@dprint/linux-arm64-musl/-/linux-arm64-musl-0.45.1.tgz", + "integrity": "sha512-un2awe1L1sAJLsCPSEUrE0/cgupdzbYFoyBOutyU1zHR9KQn47AtIDw+chvuinU4xleHDuEGyXGuJ6NE+Ky6vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@dprint/linux-x64-glibc": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@dprint/linux-x64-glibc/-/linux-x64-glibc-0.45.1.tgz", + "integrity": "sha512-5Civht90S/g8zlyYB7n4oH78p+sLbNqeFCFuImJRK7uRxZwCRya7lji6RwlB6DQ7qngVqovTHj9RLOYfZzfVlg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@dprint/linux-x64-musl": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@dprint/linux-x64-musl/-/linux-x64-musl-0.45.1.tgz", + "integrity": "sha512-p2/gjnHDd8GRCvtey5HZO4o/He6pSmY/zpcCuIXprFW9P0vNlEj3DFhz4FPpOKXM+csrsVWWs2E0T/xr5QZtVg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@dprint/typescript": { + "version": "0.90.5", + "resolved": "https://registry.npmjs.org/@dprint/typescript/-/typescript-0.90.5.tgz", + "integrity": "sha512-/1aP6saonFvJyQN3l2is6eTOec3GnLGyW+opid/eDm8pnlhwzYl8A9p36pI6WO5jLl/a9Ghod+LWpvSOuXFGUw==", + "license": "MIT" + }, + "node_modules/@dprint/win32-x64": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@dprint/win32-x64/-/win32-x64-0.45.1.tgz", + "integrity": "sha512-2l78XM7KsW46P2Yv6uPB3fE+y92EsBlrCxi+RVQ0pbznPFdMdkLyGgaCuh683zdld14jHlaADpIQ7YchGAEMAg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", @@ -1693,20 +1825,21 @@ } }, "node_modules/@graphql-codegen/client-preset": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-4.2.5.tgz", - "integrity": "sha512-hAdB6HN8EDmkoBtr0bPUN/7NH6svzqbcTDMWBCRXPESXkl7y80po+IXrXUjsSrvhKG8xkNXgJNz/2mjwHzywcA==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-4.2.6.tgz", + "integrity": "sha512-e7SzPb+nxNJfsD0uG+NSyzIeTtCXTouX5VThmcCoqGMDLgF5Lo7932B3HtZCvzmzqcXxRjJ81CmkA2LhlqIbCw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/template": "^7.20.7", "@graphql-codegen/add": "^5.0.2", - "@graphql-codegen/gql-tag-operations": "4.0.6", - "@graphql-codegen/plugin-helpers": "^5.0.3", - "@graphql-codegen/typed-document-node": "^5.0.6", - "@graphql-codegen/typescript": "^4.0.6", - "@graphql-codegen/typescript-operations": "^4.2.0", - "@graphql-codegen/visitor-plugin-common": "^5.1.0", + "@graphql-codegen/gql-tag-operations": "4.0.7", + "@graphql-codegen/plugin-helpers": "^5.0.4", + "@graphql-codegen/typed-document-node": "^5.0.7", + "@graphql-codegen/typescript": "^4.0.7", + "@graphql-codegen/typescript-operations": "^4.2.1", + "@graphql-codegen/visitor-plugin-common": "^5.2.0", "@graphql-tools/documents": "^1.0.0", "@graphql-tools/utils": "^10.0.0", "@graphql-typed-document-node/core": "3.2.0", @@ -1732,13 +1865,14 @@ } }, "node_modules/@graphql-codegen/gql-tag-operations": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-4.0.6.tgz", - "integrity": "sha512-y6iXEDpDNjwNxJw3WZqX1/Znj0QHW7+y8O+t2V8qvbTT+3kb2lr9ntc8By7vCr6ctw9tXI4XKaJgpTstJDOwFA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-4.0.7.tgz", + "integrity": "sha512-2I69+IDC8pqAohH6cgKse/vPfJ/4TRTJX96PkAKz8S4RD54PUHtBmzCdBInIFEP/vQuH5mFUAaIKXXjznmGOsg==", "dev": true, + "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", - "@graphql-codegen/visitor-plugin-common": "5.1.0", + "@graphql-codegen/plugin-helpers": "^5.0.4", + "@graphql-codegen/visitor-plugin-common": "5.2.0", "@graphql-tools/utils": "^10.0.0", "auto-bind": "~4.0.0", "tslib": "~2.6.0" @@ -1762,10 +1896,11 @@ } }, "node_modules/@graphql-codegen/plugin-helpers": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-5.0.3.tgz", - "integrity": "sha512-yZ1rpULIWKBZqCDlvGIJRSyj1B2utkEdGmXZTBT/GVayP4hyRYlkd36AJV/LfEsVD8dnsKL5rLz2VTYmRNlJ5Q==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-5.0.4.tgz", + "integrity": "sha512-MOIuHFNWUnFnqVmiXtrI+4UziMTYrcquljaI5f/T/Bc7oO7sXcfkAvgkNWEEi9xWreYwvuer3VHCuPI/lAFWbw==", "dev": true, + "license": "MIT", "dependencies": { "@graphql-tools/utils": "^10.0.0", "change-case-all": "1.0.15", @@ -1793,13 +1928,14 @@ } }, "node_modules/@graphql-codegen/typed-document-node": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-5.0.6.tgz", - "integrity": "sha512-US0J95hOE2/W/h42w4oiY+DFKG7IetEN1mQMgXXeat1w6FAR5PlIz4JrRrEkiVfVetZ1g7K78SOwBD8/IJnDiA==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-5.0.7.tgz", + "integrity": "sha512-rgFh96hAbNwPUxLVlRcNhGaw2+y7ZGx7giuETtdO8XzPasTQGWGRkZ3wXQ5UUiTX4X3eLmjnuoXYKT7HoxSznQ==", "dev": true, + "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", - "@graphql-codegen/visitor-plugin-common": "5.1.0", + "@graphql-codegen/plugin-helpers": "^5.0.4", + "@graphql-codegen/visitor-plugin-common": "5.2.0", "auto-bind": "~4.0.0", "change-case-all": "1.0.15", "tslib": "~2.6.0" @@ -1809,14 +1945,15 @@ } }, "node_modules/@graphql-codegen/typescript": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-4.0.6.tgz", - "integrity": "sha512-IBG4N+Blv7KAL27bseruIoLTjORFCT3r+QYyMC3g11uY3/9TPpaUyjSdF70yBe5GIQ6dAgDU+ENUC1v7EPi0rw==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-4.0.7.tgz", + "integrity": "sha512-Gn+JNvQBJhBqH7s83piAJ6UeU/MTj9GXWFO9bdbl8PMLCAM1uFAtg04iHfkGCtDKXcUg5a3Dt/SZG85uk5KuhA==", "dev": true, + "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-codegen/plugin-helpers": "^5.0.4", "@graphql-codegen/schema-ast": "^4.0.2", - "@graphql-codegen/visitor-plugin-common": "5.1.0", + "@graphql-codegen/visitor-plugin-common": "5.2.0", "auto-bind": "~4.0.0", "tslib": "~2.6.0" }, @@ -1825,14 +1962,15 @@ } }, "node_modules/@graphql-codegen/typescript-operations": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-4.2.0.tgz", - "integrity": "sha512-lmuwYb03XC7LNRS8oo9M4/vlOrq/wOKmTLBHlltK2YJ1BO/4K/Q9Jdv/jDmJpNydHVR1fmeF4wAfsIp1f9JibA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-4.2.1.tgz", + "integrity": "sha512-LhEPsaP+AI65zfK2j6CBAL4RT0bJL/rR9oRWlvwtHLX0t7YQr4CP4BXgvvej9brYdedAxHGPWeV1tPHy5/z9KQ==", "dev": true, + "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", - "@graphql-codegen/typescript": "^4.0.6", - "@graphql-codegen/visitor-plugin-common": "5.1.0", + "@graphql-codegen/plugin-helpers": "^5.0.4", + "@graphql-codegen/typescript": "^4.0.7", + "@graphql-codegen/visitor-plugin-common": "5.2.0", "auto-bind": "~4.0.0", "tslib": "~2.6.0" }, @@ -1841,12 +1979,13 @@ } }, "node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-5.1.0.tgz", - "integrity": "sha512-eamQxtA9bjJqI2lU5eYoA1GbdMIRT2X8m8vhWYsVQVWD3qM7sx/IqJU0kx0J3Vd4/CSd36BzL6RKwksibytDIg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-5.2.0.tgz", + "integrity": "sha512-0p8AwmARaZCAlDFfQu6Sz+JV6SjbPDx3y2nNM7WAAf0au7Im/GpJ7Ke3xaIYBc1b2rTZ+DqSTJI/zomENGD9NA==", "dev": true, + "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-codegen/plugin-helpers": "^5.0.4", "@graphql-tools/optimize": "^2.0.0", "@graphql-tools/relay-operation-optimizer": "^7.0.0", "@graphql-tools/utils": "^10.0.0", @@ -2398,6 +2537,20 @@ "node": ">=16.0.0" } }, + "node_modules/@graphql-tools/prisma-loader/node_modules/graphql-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-6.1.0.tgz", + "integrity": "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0", + "cross-fetch": "^3.1.5" + }, + "peerDependencies": { + "graphql": "14 - 16" + } + }, "node_modules/@graphql-tools/prisma-loader/node_modules/urlpattern-polyfill": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", @@ -2756,6 +2909,85 @@ "gl-style-validate": "dist/gl-style-validate.mjs" } }, + "node_modules/@molt/command": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@molt/command/-/command-0.9.0.tgz", + "integrity": "sha512-1JI8dAlpqlZoXyKWVQggX7geFNPxBpocHIXQCsnxDjKy+3WX4SGyZVJXuLlqRRrX7FmQCuuMAfx642ovXmPA9g==", + "license": "MIT", + "dependencies": { + "@molt/types": "0.2.0", + "alge": "0.8.1", + "chalk": "^5.3.0", + "lodash.camelcase": "^4.3.0", + "lodash.snakecase": "^4.1.1", + "readline-sync": "^1.4.10", + "string-length": "^6.0.0", + "strip-ansi": "^7.1.0", + "ts-toolbelt": "^9.6.0", + "type-fest": "^4.3.1", + "zod": "^3.22.2" + } + }, + "node_modules/@molt/command/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@molt/command/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@molt/command/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@molt/command/node_modules/type-fest": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.2.tgz", + "integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@molt/types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@molt/types/-/types-0.2.0.tgz", + "integrity": "sha512-p6ChnEZDGjg9PYPec9BK6Yp5/DdSrYQvXTBAtgrnqX6N36cZy37ql1c8Tc5LclfIYBNG7EZp8NBcRTYJwyi84g==", + "license": "MIT", + "dependencies": { + "ts-toolbelt": "^9.6.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3469,12 +3701,6 @@ "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", "dev": true }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, "node_modules/@types/json-stable-stringify": { "version": "1.0.36", "resolved": "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.36.tgz", @@ -3535,9 +3761,10 @@ "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", - "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -3560,12 +3787,6 @@ "@types/react": "*" } }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true - }, "node_modules/@types/supercluster": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", @@ -3589,21 +3810,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz", - "integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz", + "integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/type-utils": "7.8.0", - "@typescript-eslint/utils": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", - "debug": "^4.3.4", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/type-utils": "7.11.0", + "@typescript-eslint/utils": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { @@ -3623,49 +3843,17 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/parser": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz", - "integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz", + "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/typescript-estree": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", "debug": "^4.3.4" }, "engines": { @@ -3685,13 +3873,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz", - "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", + "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0" + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -3702,13 +3891,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", - "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz", + "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.8.0", - "@typescript-eslint/utils": "7.8.0", + "@typescript-eslint/typescript-estree": "7.11.0", + "@typescript-eslint/utils": "7.11.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -3729,10 +3919,11 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", - "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", + "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || >=20.0.0" }, @@ -3742,13 +3933,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", - "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", + "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3769,26 +3961,12 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -3796,25 +3974,17 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/utils": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", - "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", + "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.15", - "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/typescript-estree": "7.8.0", - "semver": "^7.6.0" + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -3827,46 +3997,14 @@ "eslint": "^8.56.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", - "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", + "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/types": "7.11.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -3884,16 +4022,17 @@ "dev": true }, "node_modules/@vitejs/plugin-react": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", - "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.0.tgz", + "integrity": "sha512-KcEbMsn4Dpk+LIbHMj7gDPRKaTMStxxWRkRmxsg/jVdFdJCZWt1SchZcf0M4t8lIKdwwMsEyzhrcOXRrDPtOBw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/core": "^7.23.5", - "@babel/plugin-transform-react-jsx-self": "^7.23.3", - "@babel/plugin-transform-react-jsx-source": "^7.23.3", + "@babel/core": "^7.24.5", + "@babel/plugin-transform-react-jsx-self": "^7.24.5", + "@babel/plugin-transform-react-jsx-source": "^7.24.1", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.0" + "react-refresh": "^0.14.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -4192,6 +4331,18 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/alge": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/alge/-/alge-0.8.1.tgz", + "integrity": "sha512-kiV9nTt+XIauAXsowVygDxMZLplZxDWt0W8plE/nB32/V2ziM/P/TxDbSVK7FYIUt2Xo16h3/htDh199LNPCKQ==", + "license": "MIT", + "dependencies": { + "lodash.ismatch": "^4.4.0", + "remeda": "^1.0.0", + "ts-toolbelt": "^9.6.0", + "zod": "^3.17.3" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -4629,6 +4780,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -5117,6 +5269,7 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "dev": true, "dependencies": { "node-fetch": "^2.6.12" } @@ -5459,6 +5612,25 @@ "url": "https://dotenvx.com" } }, + "node_modules/dprint": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/dprint/-/dprint-0.45.1.tgz", + "integrity": "sha512-OYefcDgxd6jSdig/Cfkw1vdvyiOIRruCPnqGBbXpc95buDt9kvwL+Lic1OHc+SaQSsQub0BUZMd5+TNgy8Sh3A==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "dprint": "bin.js" + }, + "optionalDependencies": { + "@dprint/darwin-arm64": "0.45.1", + "@dprint/darwin-x64": "0.45.1", + "@dprint/linux-arm64-glibc": "0.45.1", + "@dprint/linux-arm64-musl": "0.45.1", + "@dprint/linux-x64-glibc": "0.45.1", + "@dprint/linux-x64-musl": "0.45.1", + "@dprint/win32-x64": "0.45.1" + } + }, "node_modules/dset": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.3.tgz", @@ -5507,10 +5679,11 @@ } }, "node_modules/es-abstract": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.2.tgz", - "integrity": "sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w==", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", "arraybuffer.prototype.slice": "^1.0.3", @@ -5551,11 +5724,11 @@ "safe-regex-test": "^1.0.3", "string.prototype.trim": "^1.2.9", "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.7", + "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.2", "typed-array-byte-length": "^1.0.1", "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.5", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", "which-typed-array": "^1.1.15" }, @@ -5588,14 +5761,15 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.0.18", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz", - "integrity": "sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==", + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", + "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", @@ -5963,29 +6137,30 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.34.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz", - "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==", + "version": "7.34.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz", + "integrity": "sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw==", "dev": true, + "license": "MIT", "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlast": "^1.2.4", + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.2", "array.prototype.toreversed": "^1.1.2", "array.prototype.tosorted": "^1.1.3", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.17", + "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7", - "object.hasown": "^1.1.3", - "object.values": "^1.1.7", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.hasown": "^1.1.4", + "object.values": "^1.2.0", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.10" + "string.prototype.matchall": "^4.0.11" }, "engines": { "node": ">=4" @@ -6007,10 +6182,11 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.6.tgz", - "integrity": "sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz", + "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==", "dev": true, + "license": "MIT", "peerDependencies": { "eslint": ">=7" } @@ -6020,6 +6196,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6030,6 +6207,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -6042,6 +6220,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6054,6 +6233,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -6888,12 +7068,20 @@ } }, "node_modules/graphql-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-6.1.0.tgz", - "integrity": "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-7.0.1.tgz", + "integrity": "sha512-hfGBZF6o6lC3C0th+aTMOFP6p8Ev+ydXn4PUlT8rvqPDUFCbaynXszjBCyu0saZIP3VGbJ67GpxW8UGK+tphSw==", + "license": "MIT", "dependencies": { + "@dprint/formatter": "^0.3.0", + "@dprint/typescript": "^0.90.4", "@graphql-typed-document-node/core": "^3.2.0", - "cross-fetch": "^3.1.5" + "@molt/command": "^0.9.0", + "dprint": "^0.45.1", + "zod": "^3.23.5" + }, + "bin": { + "graffle": "build/cli/generate.js" }, "peerDependencies": { "graphql": "14 - 16" @@ -7853,31 +8041,32 @@ } }, "node_modules/jsdom": { - "version": "24.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz", - "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz", + "integrity": "sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==", "dev": true, + "license": "MIT", "dependencies": { "cssstyle": "^4.0.1", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.0", "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.7", + "nwsapi": "^2.2.10", "parse5": "^7.1.2", - "rrweb-cssom": "^0.6.0", + "rrweb-cssom": "^0.7.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.3", + "tough-cookie": "^4.1.4", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0", - "ws": "^8.16.0", + "ws": "^8.17.0", "xml-name-validator": "^5.0.0" }, "engines": { @@ -7892,6 +8081,13 @@ } } }, + "node_modules/jsdom/node_modules/rrweb-cssom": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.0.tgz", + "integrity": "sha512-KlSv0pm9kgQSRxXEMgtivPJ4h826YHsuob8pSHcfSZsSXGtvpEAie8S0AnXuObEJ7nhikOb4ahwxDm0H2yW17g==", + "dev": true, + "license": "MIT" + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -8146,12 +8342,30 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.ismatch": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", + "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -8346,9 +8560,10 @@ } }, "node_modules/maplibre-gl": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.2.0.tgz", - "integrity": "sha512-x5GgYyKKn5UDvbUZFK7ng3Pq829/uYWDSVN/itZoP2slWSzKbjIXKi/Qhz5FnYiMXwpRgM08UIcVjtn1PLK9Tg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.3.2.tgz", + "integrity": "sha512-/oXDsb9I+LkjweL/28aFMLDZoIcXKNEhYNAZDLA4xgTNkfvKQmV/r0KZdxEMcVthincJzdyc6Y4N8YwZtHKNnQ==", + "license": "BSD-3-Clause", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -8466,6 +8681,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -8560,6 +8776,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -8578,17 +8795,20 @@ "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -8652,10 +8872,11 @@ "dev": true }, "node_modules/nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", - "dev": true + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", + "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", + "dev": true, + "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", @@ -9165,10 +9386,11 @@ } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.0.tgz", + "integrity": "sha512-J9odKxERhCQ10OC2yb93583f6UnYutOeiV5i0zEDS7UGTdUt0u+y8erxl3lBKvwo/JHyyoEdXjwp4dke9oyZ/g==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -9422,10 +9644,11 @@ "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==" }, "node_modules/react-refresh": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", - "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -9459,6 +9682,15 @@ "node": ">= 6" } }, + "node_modules/readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -9514,6 +9746,12 @@ "invariant": "^2.2.4" } }, + "node_modules/remeda": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-1.61.0.tgz", + "integrity": "sha512-caKfSz9rDeSKBQQnlJnVW3mbVdFgxgGWQKq1XlFokqjf+hQD5gxutLGTTY2A/x24UxVyJe9gH5fAkFI63ULw4A==", + "license": "MIT" + }, "node_modules/remedial": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/remedial/-/remedial-1.0.8.tgz", @@ -10102,6 +10340,48 @@ "integrity": "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==", "dev": true }, + "node_modules/string-length": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-6.0.0.tgz", + "integrity": "sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==", + "license": "MIT", + "dependencies": { + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -10427,10 +10707,11 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -10489,6 +10770,12 @@ "integrity": "sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==", "dev": true }, + "node_modules/ts-toolbelt": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "license": "Apache-2.0" + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -10856,10 +11143,11 @@ } }, "node_modules/vite": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "version": "5.2.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz", + "integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.20.1", "postcss": "^8.4.38", @@ -11250,10 +11538,11 @@ "dev": true }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -11356,6 +11645,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/client-next/package.json b/client-next/package.json index 5ff988bdc87..17171945476 100644 --- a/client-next/package.json +++ b/client-next/package.json @@ -20,8 +20,8 @@ "@googlemaps/polyline-codec": "1.0.28", "bootstrap": "5.3.3", "graphql": "16.8.1", - "graphql-request": "6.1.0", - "maplibre-gl": "4.2.0", + "graphql-request": "7.0.1", + "maplibre-gl": "4.3.2", "react": "18.3.1", "react-bootstrap": "2.10.2", "react-dom": "18.3.1", @@ -29,27 +29,27 @@ }, "devDependencies": { "@graphql-codegen/cli": "5.0.2", - "@graphql-codegen/client-preset": "4.2.5", + "@graphql-codegen/client-preset": "4.2.6", "@graphql-codegen/introspection": "4.0.3", "@parcel/watcher": "2.4.1", "@testing-library/react": "15.0.7", - "@types/react": "18.3.1", + "@types/react": "18.3.3", "@types/react-dom": "18.3.0", - "@typescript-eslint/eslint-plugin": "7.8.0", - "@typescript-eslint/parser": "7.8.0", - "@vitejs/plugin-react": "4.2.1", + "@typescript-eslint/eslint-plugin": "7.11.0", + "@typescript-eslint/parser": "7.11.0", + "@vitejs/plugin-react": "4.3.0", "@vitest/coverage-v8": "1.6.0", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.29.1", "eslint-plugin-jsx-a11y": "6.8.0", - "eslint-plugin-react": "7.34.1", + "eslint-plugin-react": "7.34.2", "eslint-plugin-react-hooks": "4.6.2", - "eslint-plugin-react-refresh": "0.4.6", - "jsdom": "24.0.0", - "prettier": "3.2.5", + "eslint-plugin-react-refresh": "0.4.7", + "jsdom": "24.1.0", + "prettier": "3.3.0", "typescript": "5.4.5", - "vite": "5.2.11", + "vite": "5.2.12", "vitest": "1.6.0" } } diff --git a/client-next/src/hooks/useServerInfo.ts b/client-next/src/hooks/useServerInfo.ts index aff463d571a..54bc67c0db7 100644 --- a/client-next/src/hooks/useServerInfo.ts +++ b/client-next/src/hooks/useServerInfo.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { graphql } from '../gql'; -import request from 'graphql-request'; +import { request } from 'graphql-request'; // eslint-disable-line import/no-unresolved import { QueryType } from '../gql/graphql.ts'; const endpoint = import.meta.env.VITE_API_URL; diff --git a/client-next/src/hooks/useTripQuery.ts b/client-next/src/hooks/useTripQuery.ts index 3720e2c450a..e96a04d7cc3 100644 --- a/client-next/src/hooks/useTripQuery.ts +++ b/client-next/src/hooks/useTripQuery.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from 'react'; import { graphql } from '../gql'; -import request from 'graphql-request'; +import { request } from 'graphql-request'; // eslint-disable-line import/no-unresolved import { QueryType, TripQueryVariables } from '../gql/graphql.ts'; const endpoint = import.meta.env.VITE_API_URL; diff --git a/docs/Changelog.md b/docs/Changelog.md index 55b76823b79..9df32c081af 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -17,10 +17,14 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Optionally abort startup when unknown configuration parameters were detected [#5676](https://github.com/opentripplanner/OpenTripPlanner/pull/5676) - Fix bug in heuristics cost calculation for egress legs [#5783](https://github.com/opentripplanner/OpenTripPlanner/pull/5783) - Fix handling of implicit access and egress mode parameters. [#5821](https://github.com/opentripplanner/OpenTripPlanner/pull/5821) -- Add prettier:write to docs and rephrase slightly [ci skip] [#5831](https://github.com/opentripplanner/OpenTripPlanner/pull/5831) - Make naming of stopTransferCosts/stopBoardAlightCosts consistent [#5822](https://github.com/opentripplanner/OpenTripPlanner/pull/5822) - Namer for applying street names to nearby sidewalks [#5774](https://github.com/opentripplanner/OpenTripPlanner/pull/5774) - Implement GTFS Flex safe duration spec draft [#5796](https://github.com/opentripplanner/OpenTripPlanner/pull/5796) +- Document and validate timeRange GraphQL parameter [#5834](https://github.com/opentripplanner/OpenTripPlanner/pull/5834) +- Log the origin of a request that causes a transfer cache addition. [#5874](https://github.com/opentripplanner/OpenTripPlanner/pull/5874) +- Fix handling of missing aimed departure time [#5865](https://github.com/opentripplanner/OpenTripPlanner/pull/5865) +- Add OTP request timeout GraphQL instrumentation [#5881](https://github.com/opentripplanner/OpenTripPlanner/pull/5881) +- Add feed publisher name and url to GTFS GraphQL API [#5835](https://github.com/opentripplanner/OpenTripPlanner/pull/5835) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.5.0 (2024-03-13) diff --git a/pom.xml b/pom.xml index de299018c21..8b5fb9c9181 100644 --- a/pom.xml +++ b/pom.xml @@ -58,18 +58,18 @@ 150 - 31.0 + 31.1 2.51.1 2.17.1 - 3.1.6 + 3.1.7 5.10.2 - 1.12.3 + 1.13.0 5.5.3 1.5.6 9.10.0 2.0.13 2.0.15 - 1.26 + 1.27 4.0.5 UTF-8 @@ -209,7 +209,7 @@ org.codehaus.mojo build-helper-maven-plugin - 3.5.0 + 3.6.0 build-helper-generate-sources @@ -363,7 +363,7 @@ properly if some input files are missing a terminating newline) --> org.apache.maven.plugins maven-shade-plugin - 3.5.3 + 3.6.0 package @@ -444,7 +444,7 @@ com.google.cloud.tools jib-maven-plugin - 3.4.2 + 3.4.3 org.opentripplanner.standalone.OTPMain @@ -672,9 +672,9 @@ - org.entur.gbfs + org.mobilitydata gbfs-java-model - 3.1.1 + 1.0.6 @@ -916,7 +916,7 @@ org.apache.commons commons-compress - 1.26.1 + 1.26.2 test @@ -1013,7 +1013,7 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.13 + 1.7.0 true ossrh diff --git a/renovate.json5 b/renovate.json5 index b3472c1a0d1..466bc36a94a 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -46,7 +46,7 @@ // noise, so we slow it down a bit { "matchPackageNames": [ - "org.entur.gbfs:gbfs-java-model" + "org.mobilitydata:gbfs-java-model" ], "matchUpdateTypes": ["patch"], "schedule": "on the 18th day of the month", @@ -122,7 +122,8 @@ "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.apache.maven.plugins:maven-jar-plugin", + "org.sonatype.plugins:nexus-staging-maven-plugin" ], "matchPackagePrefixes": [ "org.junit.jupiter:", diff --git a/src/client/debug-client-preview/index.html b/src/client/debug-client-preview/index.html index 974356161cb..717fce1baf2 100644 --- a/src/client/debug-client-preview/index.html +++ b/src/client/debug-client-preview/index.html @@ -5,8 +5,8 @@ OTP Debug Client - - + +
diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPathTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPathTest.java index 8bd3abee785..3d37de02af4 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPathTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPathTest.java @@ -21,7 +21,7 @@ class FlexPathTest { static List cases() { return List.of( - Arguments.of(TimePenalty.ZERO, THIRTY_MINS_IN_SECONDS), + Arguments.of(TimePenalty.NONE, THIRTY_MINS_IN_SECONDS), Arguments.of(TimePenalty.of(Duration.ofMinutes(10), 1), 2400), Arguments.of(TimePenalty.of(Duration.ofMinutes(10), 1.5f), 3300), Arguments.of(TimePenalty.of(Duration.ZERO, 3), 5400) @@ -30,8 +30,8 @@ static List cases() { @ParameterizedTest @MethodSource("cases") - void calculate(TimePenalty mod, int expectedSeconds) { - var modified = PATH.withDurationModifier(mod); + void calculate(TimePenalty penalty, int expectedSeconds) { + var modified = PATH.withTimePenalty(penalty); assertEquals(expectedSeconds, modified.durationSeconds); assertEquals(LineStrings.SIMPLE, modified.getGeometry()); } diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/trip/FlexTripsMapperTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/trip/FlexTripsMapperTest.java new file mode 100644 index 00000000000..0d0376bbd32 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/flex/trip/FlexTripsMapperTest.java @@ -0,0 +1,37 @@ +package org.opentripplanner.ext.flex.trip; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.opentripplanner.graph_builder.issue.api.DataImportIssueStore.NOOP; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.ext.flex.FlexStopTimesForTest; +import org.opentripplanner.ext.flex.FlexTripsMapper; +import org.opentripplanner.ext.flex.flexpathcalculator.DirectFlexPathCalculator; +import org.opentripplanner.model.StopTime; +import org.opentripplanner.model.impl.OtpTransitServiceBuilder; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.service.StopModel; + +class FlexTripsMapperTest { + + @Test + void defaultTimePenalty() { + var builder = new OtpTransitServiceBuilder(StopModel.of().build(), NOOP); + var stopTimes = List.of(stopTime(0), stopTime(1)); + builder.getStopTimesSortedByTrip().addAll(stopTimes); + var trips = FlexTripsMapper.createFlexTrips(builder, NOOP); + assertEquals("[UnscheduledTrip{F:flex-1}]", trips.toString()); + var unscheduled = (UnscheduledTrip) trips.getFirst(); + var unchanged = unscheduled.flexPathCalculator(new DirectFlexPathCalculator()); + assertInstanceOf(DirectFlexPathCalculator.class, unchanged); + } + + private static StopTime stopTime(int seq) { + var st = FlexStopTimesForTest.area("08:00", "18:00"); + st.setTrip(TransitModelForTest.trip("flex-1").build()); + st.setStopSequence(seq); + return st; + } +} 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 b1fb33dfdd3..cee6cf3a2d8 100644 --- a/src/ext-test/java/org/opentripplanner/ext/geocoder/LuceneIndexTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/geocoder/LuceneIndexTest.java @@ -10,7 +10,9 @@ import java.time.LocalDate; import java.util.List; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; +import javax.annotation.Nonnull; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -213,13 +215,13 @@ class StopClusters { ) void stopClustersWithTypos(String searchTerm) { var results = index.queryStopClusters(searchTerm).toList(); - var ids = results.stream().map(StopCluster::id).toList(); + var ids = results.stream().map(primaryId()).toList(); assertEquals(List.of(ALEXANDERPLATZ_STATION.getId()), ids); } @Test void fuzzyStopClusters() { - var result1 = index.queryStopClusters("arts").map(StopCluster::id).toList(); + var result1 = index.queryStopClusters("arts").map(primaryId()).toList(); assertEquals(List.of(ARTS_CENTER.getId()), result1); } @@ -227,7 +229,7 @@ void fuzzyStopClusters() { void deduplicatedStopClusters() { var result = index.queryStopClusters("lich").toList(); assertEquals(1, result.size()); - assertEquals(LICHTERFELDE_OST_1.getName().toString(), result.getFirst().name()); + assertEquals(LICHTERFELDE_OST_1.getName().toString(), result.getFirst().primary().name()); } @ParameterizedTest @@ -259,7 +261,7 @@ void deduplicatedStopClusters() { } ) void stopClustersWithSpace(String query) { - var result = index.queryStopClusters(query).map(StopCluster::id).toList(); + var result = index.queryStopClusters(query).map(primaryId()).toList(); assertEquals(List.of(FIVE_POINTS_STATION.getId()), result); } @@ -268,24 +270,28 @@ void stopClustersWithSpace(String query) { void fuzzyStopCode(String query) { var result = index.queryStopClusters(query).toList(); assertEquals(1, result.size()); - assertEquals(ARTS_CENTER.getName().toString(), result.getFirst().name()); + assertEquals(ARTS_CENTER.getName().toString(), result.getFirst().primary().name()); } @Test void modes() { var result = index.queryStopClusters("westh").toList(); assertEquals(1, result.size()); - var stop = result.getFirst(); - assertEquals(WESTHAFEN.getName().toString(), stop.name()); - assertEquals(List.of(FERRY.name(), BUS.name()), stop.modes()); + var cluster = result.getFirst(); + assertEquals(WESTHAFEN.getName().toString(), cluster.primary().name()); + assertEquals(List.of(FERRY.name(), BUS.name()), cluster.primary().modes()); } @Test void agenciesAndFeedPublisher() { - var result = index.queryStopClusters("alexanderplatz").toList().getFirst(); - assertEquals(ALEXANDERPLATZ_STATION.getName().toString(), result.name()); - assertEquals(List.of(StopClusterMapper.toAgency(BVG)), result.agencies()); - assertEquals("A Publisher", result.feedPublisher().name()); + var cluster = index.queryStopClusters("alexanderplatz").toList().getFirst(); + assertEquals(ALEXANDERPLATZ_STATION.getName().toString(), cluster.primary().name()); + assertEquals(List.of(StopClusterMapper.toAgency(BVG)), cluster.primary().agencies()); + assertEquals("A Publisher", cluster.primary().feedPublisher().name()); } } + + private static @Nonnull Function primaryId() { + return c -> c.primary().id(); + } } diff --git a/src/ext-test/java/org/opentripplanner/ext/siri/SiriEtBuilder.java b/src/ext-test/java/org/opentripplanner/ext/siri/SiriEtBuilder.java new file mode 100644 index 00000000000..3b54809b0a3 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/siri/SiriEtBuilder.java @@ -0,0 +1,316 @@ +package org.opentripplanner.ext.siri; + +import java.math.BigInteger; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import javax.annotation.Nullable; +import org.opentripplanner.DateTimeHelper; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.StopLocation; +import uk.org.siri.siri20.DataFrameRefStructure; +import uk.org.siri.siri20.DatedVehicleJourneyRef; +import uk.org.siri.siri20.EstimatedCall; +import uk.org.siri.siri20.EstimatedTimetableDeliveryStructure; +import uk.org.siri.siri20.EstimatedVehicleJourney; +import uk.org.siri.siri20.EstimatedVersionFrameStructure; +import uk.org.siri.siri20.FramedVehicleJourneyRefStructure; +import uk.org.siri.siri20.LineRef; +import uk.org.siri.siri20.OperatorRefStructure; +import uk.org.siri.siri20.QuayRefStructure; +import uk.org.siri.siri20.RecordedCall; +import uk.org.siri.siri20.StopAssignmentStructure; +import uk.org.siri.siri20.StopPointRef; +import uk.org.siri.siri20.VehicleJourneyRef; + +/** + * This is a helper class for constucting Siri ET messages to use in tests. + */ +public class SiriEtBuilder { + + private final EstimatedVehicleJourney evj; + private final DateTimeHelper dateTimeHelper; + + public SiriEtBuilder(DateTimeHelper dateTimeHelper) { + this.dateTimeHelper = dateTimeHelper; + this.evj = new EstimatedVehicleJourney(); + + // Set default values + evj.setMonitored(true); + } + + public List buildEstimatedTimetableDeliveries() { + var versionFrame = new EstimatedVersionFrameStructure(); + versionFrame.getEstimatedVehicleJourneies().add(evj); + + var etd = new EstimatedTimetableDeliveryStructure(); + etd.getEstimatedJourneyVersionFrames().add(versionFrame); + return List.of(etd); + } + + public SiriEtBuilder withCancellation(boolean canceled) { + evj.setCancellation(canceled); + return this; + } + + public SiriEtBuilder withMonitored(boolean monitored) { + evj.setMonitored(monitored); + return this; + } + + public SiriEtBuilder withIsExtraJourney(boolean isExtraJourney) { + evj.setExtraJourney(isExtraJourney); + return this; + } + + public SiriEtBuilder withDatedVehicleJourneyRef(String datedServiceJourneyId) { + var ref = new DatedVehicleJourneyRef(); + ref.setValue(datedServiceJourneyId); + evj.setDatedVehicleJourneyRef(ref); + return this; + } + + public SiriEtBuilder withEstimatedVehicleJourneyCode(String estimatedVehicleJourneyCode) { + evj.setEstimatedVehicleJourneyCode(estimatedVehicleJourneyCode); + return this; + } + + public SiriEtBuilder withOperatorRef(String operatorRef) { + var ref = new OperatorRefStructure(); + ref.setValue(operatorRef); + evj.setOperatorRef(ref); + return this; + } + + public SiriEtBuilder withLineRef(String lineRef) { + var ref = new LineRef(); + ref.setValue(lineRef); + evj.setLineRef(ref); + return this; + } + + public SiriEtBuilder withRecordedCalls( + Function producer + ) { + if (evj.getEstimatedCalls() != null) { + // If we call this after estimatedCalls, the ordering will be messed up + throw new RuntimeException( + "You need to call withRecordedCalls() before withEstimatedCalls()" + ); + } + var builder = new RecordedCallsBuilder(dateTimeHelper, 0); + + builder = producer.apply(builder); + + var calls = new EstimatedVehicleJourney.RecordedCalls(); + builder.build().forEach(call -> calls.getRecordedCalls().add(call)); + evj.setRecordedCalls(calls); + return this; + } + + public SiriEtBuilder withEstimatedCalls( + Function producer + ) { + int offset = evj.getRecordedCalls() == null + ? 0 + : evj.getRecordedCalls().getRecordedCalls().size(); + var builder = new EstimatedCallsBuilder(dateTimeHelper, offset); + + builder = producer.apply(builder); + + var calls = new EstimatedVehicleJourney.EstimatedCalls(); + builder.build().forEach(call -> calls.getEstimatedCalls().add(call)); + evj.setEstimatedCalls(calls); + return this; + } + + public SiriEtBuilder withVehicleJourneyRef(String id) { + var ref = new VehicleJourneyRef(); + ref.setValue(id); + evj.setVehicleJourneyRef(ref); + return this; + } + + public SiriEtBuilder withFramedVehicleJourneyRef( + Function producer + ) { + var builder = new FramedVehicleRefBuilder(); + builder = producer.apply(builder); + evj.setFramedVehicleJourneyRef(builder.build()); + return this; + } + + public static class FramedVehicleRefBuilder { + + private LocalDate serviceDate; + private String vehicleJourneyRef; + + public SiriEtBuilder.FramedVehicleRefBuilder withServiceDate(LocalDate serviceDate) { + this.serviceDate = serviceDate; + return this; + } + + public SiriEtBuilder.FramedVehicleRefBuilder withVehicleJourneyRef(String vehicleJourneyRef) { + this.vehicleJourneyRef = vehicleJourneyRef; + return this; + } + + public FramedVehicleJourneyRefStructure build() { + DataFrameRefStructure dataFrameRefStructure = new DataFrameRefStructure(); + if (serviceDate != null) { + dataFrameRefStructure.setValue(DateTimeFormatter.ISO_LOCAL_DATE.format(serviceDate)); + } + FramedVehicleJourneyRefStructure framedVehicleJourneyRefStructure = new FramedVehicleJourneyRefStructure(); + framedVehicleJourneyRefStructure.setDataFrameRef(dataFrameRefStructure); + framedVehicleJourneyRefStructure.setDatedVehicleJourneyRef(vehicleJourneyRef); + return framedVehicleJourneyRefStructure; + } + } + + public static class RecordedCallsBuilder { + + private final ArrayList calls; + private final int orderOffset; + private final DateTimeHelper dateTimeHelper; + + public RecordedCallsBuilder(DateTimeHelper dateTimeHelper, int orderOffset) { + this.dateTimeHelper = dateTimeHelper; + this.orderOffset = orderOffset; + this.calls = new ArrayList<>(); + } + + public RecordedCallsBuilder call(StopLocation stop) { + var call = new RecordedCall(); + call.setOrder(BigInteger.valueOf(orderOffset + calls.size())); + + var ref = new StopPointRef(); + ref.setValue(stop.getId().getId()); + call.setStopPointRef(ref); + + calls.add(call); + return this; + } + + public RecordedCallsBuilder arriveAimedActual(String aimedTime, String actualTime) { + var call = calls.getLast(); + call.setAimedArrivalTime(dateTimeHelper.zonedDateTime(aimedTime)); + call.setActualArrivalTime(dateTimeHelper.zonedDateTime(actualTime)); + return this; + } + + public RecordedCallsBuilder departAimedActual(String aimedTime, String actualTime) { + var call = calls.getLast(); + call.setAimedDepartureTime(dateTimeHelper.zonedDateTime(aimedTime)); + call.setActualDepartureTime(dateTimeHelper.zonedDateTime(actualTime)); + return this; + } + + public RecordedCallsBuilder withIsExtraCall(boolean extra) { + var call = calls.getLast(); + call.setExtraCall(extra); + return this; + } + + public RecordedCallsBuilder withIsCancellation(boolean cancel) { + var call = calls.getLast(); + call.setCancellation(cancel); + return this; + } + + public List build() { + return calls; + } + } + + public static class EstimatedCallsBuilder { + + private final ArrayList calls; + private final int orderOffset; + private final DateTimeHelper dateTimeHelper; + + public EstimatedCallsBuilder(DateTimeHelper dateTimeHelper, int orderOffset) { + this.dateTimeHelper = dateTimeHelper; + this.orderOffset = orderOffset; + this.calls = new ArrayList<>(); + } + + public EstimatedCallsBuilder call(StopLocation stop) { + var call = new EstimatedCall(); + call.setOrder(BigInteger.valueOf(orderOffset + calls.size())); + + var ref = new StopPointRef(); + ref.setValue(stop.getId().getId()); + call.setStopPointRef(ref); + + calls.add(call); + return this; + } + + public EstimatedCallsBuilder arriveAimedExpected( + @Nullable String aimedTime, + @Nullable String expectedTime + ) { + var call = calls.getLast(); + if (aimedTime != null) { + call.setAimedArrivalTime(dateTimeHelper.zonedDateTime(aimedTime)); + } + if (expectedTime != null) { + call.setExpectedArrivalTime(dateTimeHelper.zonedDateTime(expectedTime)); + } + return this; + } + + public EstimatedCallsBuilder departAimedExpected( + @Nullable String aimedTime, + @Nullable String expectedTime + ) { + var call = calls.getLast(); + if (aimedTime != null) { + call.setAimedDepartureTime(dateTimeHelper.zonedDateTime(aimedTime)); + } + if (expectedTime != null) { + call.setExpectedDepartureTime(dateTimeHelper.zonedDateTime(expectedTime)); + } + return this; + } + + public EstimatedCallsBuilder withIsExtraCall(boolean extra) { + var call = calls.getLast(); + call.setExtraCall(extra); + return this; + } + + public EstimatedCallsBuilder withIsCancellation(boolean cancel) { + var call = calls.getLast(); + call.setCancellation(cancel); + return this; + } + + public EstimatedCallsBuilder withArrivalStopAssignment( + RegularStop aimedQuay, + @Nullable RegularStop expectedQuay + ) { + var stopAssignmentStructure = new StopAssignmentStructure(); + + var aimed = new QuayRefStructure(); + aimed.setValue(aimedQuay.getId().getId()); + stopAssignmentStructure.setAimedQuayRef(aimed); + + if (expectedQuay != null) { + var expected = new QuayRefStructure(); + expected.setValue(expectedQuay.getId().getId()); + stopAssignmentStructure.setExpectedQuayRef(expected); + } + + var call = calls.getLast(); + call.setArrivalStopAssignment(stopAssignmentStructure); + return this; + } + + public List build() { + return calls; + } + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java b/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java new file mode 100644 index 00000000000..cfe080c47dc --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSourceTest.java @@ -0,0 +1,643 @@ +package org.opentripplanner.ext.siri; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.opentripplanner.DateTimeHelper; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; +import org.opentripplanner.model.StopTime; +import org.opentripplanner.model.calendar.CalendarServiceData; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.framework.Deduplicator; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.site.Station; +import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.RealTimeState; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; +import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; +import org.opentripplanner.transit.model.timetable.TripTimesStringBuilder; +import org.opentripplanner.transit.service.DefaultTransitService; +import org.opentripplanner.transit.service.StopModel; +import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.transit.service.TransitService; +import org.opentripplanner.updater.TimetableSnapshotSourceParameters; +import org.opentripplanner.updater.spi.UpdateError; +import org.opentripplanner.updater.spi.UpdateResult; +import uk.org.siri.siri20.EstimatedTimetableDeliveryStructure; + +class SiriTimetableSnapshotSourceTest { + + @Test + void testCancelTrip() { + var env = new RealtimeTestEnvironment(); + + assertEquals(RealTimeState.SCHEDULED, env.getTripTimesForTrip(env.trip1).getRealTimeState()); + + var updates = new SiriEtBuilder(env.getDateTimeHelper()) + .withDatedVehicleJourneyRef(env.trip1.getId().getId()) + .withCancellation(true) + .buildEstimatedTimetableDeliveries(); + + var result = env.applyEstimatedTimetable(updates); + + assertEquals(1, result.successful()); + assertEquals(RealTimeState.CANCELED, env.getTripTimesForTrip(env.trip1).getRealTimeState()); + } + + @Test + void testAddJourney() { + var env = new RealtimeTestEnvironment(); + + var updates = new SiriEtBuilder(env.getDateTimeHelper()) + .withEstimatedVehicleJourneyCode("newJourney") + .withIsExtraJourney(true) + .withOperatorRef(env.operator1Id.getId()) + .withLineRef(env.route1Id.getId()) + .withRecordedCalls(builder -> builder.call(env.stopC1).departAimedActual("00:01", "00:02")) + .withEstimatedCalls(builder -> builder.call(env.stopD1).arriveAimedExpected("00:03", "00:04")) + .buildEstimatedTimetableDeliveries(); + + var result = env.applyEstimatedTimetable(updates); + + assertEquals(1, result.successful()); + assertEquals("ADDED | C1 [R] 0:02 0:02 | D1 0:04 0:04", env.getRealtimeTimetable("newJourney")); + assertEquals( + "SCHEDULED | C1 0:01 0:01 | D1 0:03 0:03", + env.getScheduledTimetable("newJourney") + ); + } + + @Test + void testReplaceJourney() { + var env = new RealtimeTestEnvironment(); + + var updates = new SiriEtBuilder(env.getDateTimeHelper()) + .withEstimatedVehicleJourneyCode("newJourney") + .withIsExtraJourney(true) + // replace trip1 + .withVehicleJourneyRef(env.trip1.getId().getId()) + .withOperatorRef(env.operator1Id.getId()) + .withLineRef(env.route1Id.getId()) + .withRecordedCalls(builder -> builder.call(env.stopA1).departAimedActual("00:01", "00:02")) + .withEstimatedCalls(builder -> builder.call(env.stopC1).arriveAimedExpected("00:03", "00:04")) + .buildEstimatedTimetableDeliveries(); + + var result = env.applyEstimatedTimetable(updates); + + assertEquals(1, result.successful()); + + assertEquals("ADDED | A1 [R] 0:02 0:02 | C1 0:04 0:04", env.getRealtimeTimetable("newJourney")); + assertEquals( + "SCHEDULED | A1 0:01 0:01 | C1 0:03 0:03", + env.getScheduledTimetable("newJourney") + ); + + // Original trip should not get canceled + var originalTripTimes = env.getTripTimesForTrip(env.trip1); + assertEquals(RealTimeState.SCHEDULED, originalTripTimes.getRealTimeState()); + } + + /** + * Update calls without changing the pattern. Match trip by dated vehicle journey. + */ + @Test + void testUpdateJourneyWithDatedVehicleJourneyRef() { + var env = new RealtimeTestEnvironment(); + + var updates = updatedJourneyBuilder(env) + .withDatedVehicleJourneyRef(env.trip1.getId().getId()) + .buildEstimatedTimetableDeliveries(); + var result = env.applyEstimatedTimetable(updates); + assertEquals(1, result.successful()); + assertTripUpdated(env); + assertEquals( + "UPDATED | A1 0:00:15 0:00:15 | B1 0:00:25 0:00:25", + env.getRealtimeTimetable(env.trip1) + ); + } + + /** + * Update calls without changing the pattern. Match trip by framed vehicle journey. + */ + @Test + void testUpdateJourneyWithFramedVehicleJourneyRef() { + var env = new RealtimeTestEnvironment(); + + var updates = updatedJourneyBuilder(env) + .withFramedVehicleJourneyRef(builder -> + builder.withServiceDate(env.serviceDate).withVehicleJourneyRef(env.trip1.getId().getId()) + ) + .buildEstimatedTimetableDeliveries(); + var result = env.applyEstimatedTimetable(updates); + assertEquals(1, result.successful()); + assertTripUpdated(env); + } + + /** + * Update calls without changing the pattern. Missing reference to vehicle journey. + */ + @Test + void testUpdateJourneyWithoutJourneyRef() { + var env = new RealtimeTestEnvironment(); + + var updates = updatedJourneyBuilder(env).buildEstimatedTimetableDeliveries(); + var result = env.applyEstimatedTimetable(updates); + assertEquals(0, result.successful()); + assertFailure(UpdateError.UpdateErrorType.TRIP_NOT_FOUND, result); + } + + /** + * Update calls without changing the pattern. Fuzzy matching. + */ + @Test + void testUpdateJourneyWithFuzzyMatching() { + var env = new RealtimeTestEnvironment(); + + var updates = updatedJourneyBuilder(env).buildEstimatedTimetableDeliveries(); + var result = env.applyEstimatedTimetableWithFuzzyMatcher(updates); + assertEquals(1, result.successful()); + assertTripUpdated(env); + } + + /** + * Update calls without changing the pattern. Fuzzy matching. + * Edge case: invalid reference to vehicle journey and missing aimed departure time. + */ + @Test + void testUpdateJourneyWithFuzzyMatchingAndMissingAimedDepartureTime() { + var env = new RealtimeTestEnvironment(); + + var updates = new SiriEtBuilder(env.getDateTimeHelper()) + .withFramedVehicleJourneyRef(builder -> + builder.withServiceDate(env.serviceDate).withVehicleJourneyRef("XXX") + ) + .withEstimatedCalls(builder -> + builder + .call(env.stopA1) + .departAimedExpected(null, "00:00:12") + .call(env.stopB1) + .arriveAimedExpected("00:00:20", "00:00:22") + ) + .buildEstimatedTimetableDeliveries(); + + var result = env.applyEstimatedTimetableWithFuzzyMatcher(updates); + assertEquals(0, result.successful(), "Should fail gracefully"); + assertFailure(UpdateError.UpdateErrorType.NO_FUZZY_TRIP_MATCH, result); + } + + /** + * Change quay on a trip + */ + @Test + void testChangeQuay() { + var env = new RealtimeTestEnvironment(); + + var updates = new SiriEtBuilder(env.getDateTimeHelper()) + .withDatedVehicleJourneyRef(env.trip1.getId().getId()) + .withRecordedCalls(builder -> + builder.call(env.stopA1).departAimedActual("00:00:11", "00:00:15") + ) + .withEstimatedCalls(builder -> + builder.call(env.stopB2).arriveAimedExpected("00:00:20", "00:00:33") + ) + .buildEstimatedTimetableDeliveries(); + + var result = env.applyEstimatedTimetable(updates); + + assertEquals(1, result.successful()); + assertEquals( + "MODIFIED | A1 [R] 0:00:15 0:00:15 | B2 0:00:33 0:00:33", + env.getRealtimeTimetable(env.trip1) + ); + } + + @Test + void testCancelStop() { + var env = new RealtimeTestEnvironment(); + + var updates = new SiriEtBuilder(env.getDateTimeHelper()) + .withDatedVehicleJourneyRef(env.trip2.getId().getId()) + .withEstimatedCalls(builder -> + builder + .call(env.stopA1) + .departAimedExpected("00:01:01", "00:01:01") + .call(env.stopB1) + .withIsCancellation(true) + .call(env.stopC1) + .arriveAimedExpected("00:01:30", "00:01:30") + ) + .buildEstimatedTimetableDeliveries(); + + var result = env.applyEstimatedTimetable(updates); + + assertEquals(1, result.successful()); + assertEquals( + "MODIFIED | A1 0:01:01 0:01:01 | B1 [C] 0:01:10 0:01:11 | C1 0:01:30 0:01:30", + env.getRealtimeTimetable(env.trip2) + ); + } + + // TODO: support this + @Test + @Disabled("Not supported yet") + void testAddStop() { + var env = new RealtimeTestEnvironment(); + + var updates = new SiriEtBuilder(env.getDateTimeHelper()) + .withDatedVehicleJourneyRef(env.trip1.getId().getId()) + .withRecordedCalls(builder -> + builder.call(env.stopA1).departAimedActual("00:00:11", "00:00:15") + ) + .withEstimatedCalls(builder -> + builder + .call(env.stopD1) + .withIsExtraCall(true) + .arriveAimedExpected("00:00:19", "00:00:20") + .departAimedExpected("00:00:24", "00:00:25") + .call(env.stopB1) + .arriveAimedExpected("00:00:20", "00:00:33") + ) + .buildEstimatedTimetableDeliveries(); + + var result = env.applyEstimatedTimetable(updates); + + assertEquals(1, result.successful()); + assertEquals( + "MODIFIED | A1 0:00:15 0:00:15 | D1 [C] 0:00:20 0:00:25 | B1 0:00:33 0:00:33", + env.getRealtimeTimetable(env.trip1) + ); + } + + ///////////////// + // Error cases // + ///////////////// + + @Test + void testNotMonitored() { + var env = new RealtimeTestEnvironment(); + + var updates = new SiriEtBuilder(env.getDateTimeHelper()) + .withMonitored(false) + .buildEstimatedTimetableDeliveries(); + + var result = env.applyEstimatedTimetable(updates); + + assertFailure(UpdateError.UpdateErrorType.NOT_MONITORED, result); + } + + @Test + void testReplaceJourneyWithoutEstimatedVehicleJourneyCode() { + var env = new RealtimeTestEnvironment(); + + var updates = new SiriEtBuilder(env.getDateTimeHelper()) + .withDatedVehicleJourneyRef("newJourney") + .withIsExtraJourney(true) + .withVehicleJourneyRef(env.trip1.getId().getId()) + .withOperatorRef(env.operator1Id.getId()) + .withLineRef(env.route1Id.getId()) + .withEstimatedCalls(builder -> + builder + .call(env.stopA1) + .departAimedExpected("00:01", "00:02") + .call(env.stopC1) + .arriveAimedExpected("00:03", "00:04") + ) + .buildEstimatedTimetableDeliveries(); + + var result = env.applyEstimatedTimetable(updates); + + // TODO: this should have a more specific error type + assertFailure(UpdateError.UpdateErrorType.UNKNOWN, result); + } + + @Test + void testNegativeHopTime() { + var env = new RealtimeTestEnvironment(); + + var updates = new SiriEtBuilder(env.getDateTimeHelper()) + .withDatedVehicleJourneyRef(env.trip1.getId().getId()) + .withRecordedCalls(builder -> + builder + .call(env.stopA1) + .departAimedActual("00:00:11", "00:00:15") + .call(env.stopB1) + .arriveAimedActual("00:00:20", "00:00:14") + ) + .buildEstimatedTimetableDeliveries(); + + var result = env.applyEstimatedTimetable(updates); + + assertFailure(UpdateError.UpdateErrorType.NEGATIVE_HOP_TIME, result); + } + + @Test + void testNegativeDwellTime() { + var env = new RealtimeTestEnvironment(); + + var updates = new SiriEtBuilder(env.getDateTimeHelper()) + .withDatedVehicleJourneyRef(env.trip2.getId().getId()) + .withRecordedCalls(builder -> + builder + .call(env.stopA1) + .departAimedActual("00:01:01", "00:01:01") + .call(env.stopB1) + .arriveAimedActual("00:01:10", "00:01:13") + .departAimedActual("00:01:11", "00:01:12") + .call(env.stopB1) + .arriveAimedActual("00:01:20", "00:01:20") + ) + .buildEstimatedTimetableDeliveries(); + + var result = env.applyEstimatedTimetable(updates); + + assertFailure(UpdateError.UpdateErrorType.NEGATIVE_DWELL_TIME, result); + } + + // TODO: support this + @Test + @Disabled("Not supported yet") + void testExtraUnknownStop() { + var env = new RealtimeTestEnvironment(); + + var updates = new SiriEtBuilder(env.getDateTimeHelper()) + .withDatedVehicleJourneyRef(env.trip1.getId().getId()) + .withEstimatedCalls(builder -> + builder + .call(env.stopA1) + .departAimedExpected("00:00:11", "00:00:15") + // Unexpected extra stop without isExtraCall flag + .call(env.stopD1) + .arriveAimedExpected("00:00:19", "00:00:20") + .departAimedExpected("00:00:24", "00:00:25") + .call(env.stopB1) + .arriveAimedExpected("00:00:20", "00:00:33") + ) + .buildEstimatedTimetableDeliveries(); + + var result = env.applyEstimatedTimetable(updates); + + assertFailure(UpdateError.UpdateErrorType.INVALID_STOP_SEQUENCE, result); + } + + private void assertFailure(UpdateError.UpdateErrorType expectedError, UpdateResult result) { + assertEquals(Set.of(expectedError), result.failures().keySet()); + } + + private static SiriEtBuilder updatedJourneyBuilder(RealtimeTestEnvironment env) { + return new SiriEtBuilder(env.getDateTimeHelper()) + .withEstimatedCalls(builder -> + builder + .call(env.stopA1) + .departAimedExpected("00:00:11", "00:00:15") + .call(env.stopB1) + .arriveAimedExpected("00:00:20", "00:00:25") + ); + } + + private static void assertTripUpdated(RealtimeTestEnvironment env) { + assertEquals( + "UPDATED | A1 0:00:15 0:00:15 | B1 0:00:25 0:00:25", + env.getRealtimeTimetable(env.trip1) + ); + } + + private static class RealtimeTestEnvironment { + + public static final FeedScopedId SERVICE_ID = TransitModelForTest.id("SERVICE_ID"); + private final TransitModelForTest testModel = TransitModelForTest.of(); + public final ZoneId timeZone = ZoneId.of(TransitModelForTest.TIME_ZONE_ID); + public final Station stationA = testModel.station("A").build(); + public final Station stationB = testModel.station("B").build(); + public final Station stationC = testModel.station("C").build(); + public final Station stationD = testModel.station("D").build(); + public final RegularStop stopA1 = testModel.stop("A1").withParentStation(stationA).build(); + public final RegularStop stopB1 = testModel.stop("B1").withParentStation(stationB).build(); + public final RegularStop stopB2 = testModel.stop("B2").withParentStation(stationB).build(); + public final RegularStop stopC1 = testModel.stop("C1").withParentStation(stationC).build(); + public final RegularStop stopD1 = testModel.stop("D1").withParentStation(stationD).build(); + public final StopModel stopModel = testModel + .stopModelBuilder() + .withRegularStop(stopA1) + .withRegularStop(stopB1) + .withRegularStop(stopB2) + .withRegularStop(stopC1) + .withRegularStop(stopD1) + .build(); + + public final LocalDate serviceDate = LocalDate.of(2024, 5, 8); + public TransitModel transitModel; + public SiriTimetableSnapshotSource snapshotSource; + + public final FeedScopedId operator1Id = TransitModelForTest.id("TestOperator1"); + public final FeedScopedId route1Id = TransitModelForTest.id("TestRoute1"); + public Trip trip1; + public Trip trip2; + + public final DateTimeHelper dateTimeHelper = new DateTimeHelper(timeZone, serviceDate); + + public RealtimeTestEnvironment() { + transitModel = new TransitModel(stopModel, new Deduplicator()); + transitModel.initTimeZone(timeZone); + transitModel.addAgency(TransitModelForTest.AGENCY); + + Route route1 = TransitModelForTest.route(route1Id).build(); + + trip1 = + createTrip( + "TestTrip1", + route1, + List.of(new Stop(stopA1, 10, 11), new Stop(stopB1, 20, 21)) + ); + trip2 = + createTrip( + "TestTrip2", + route1, + List.of(new Stop(stopA1, 60, 61), new Stop(stopB1, 70, 71), new Stop(stopC1, 80, 81)) + ); + + CalendarServiceData calendarServiceData = new CalendarServiceData(); + calendarServiceData.putServiceDatesForServiceId( + SERVICE_ID, + List.of(serviceDate.minusDays(1), serviceDate, serviceDate.plusDays(1)) + ); + transitModel.getServiceCodes().put(SERVICE_ID, 0); + transitModel.updateCalendarServiceData(true, calendarServiceData, DataImportIssueStore.NOOP); + + transitModel.index(); + + var parameters = new TimetableSnapshotSourceParameters(Duration.ZERO, false); + snapshotSource = new SiriTimetableSnapshotSource(parameters, transitModel); + } + + private record Stop(RegularStop stop, int arrivalTime, int departureTime) {} + + private Trip createTrip(String id, Route route, List stops) { + var trip = Trip.of(id(id)).withRoute(route).withServiceId(SERVICE_ID).build(); + + var tripOnServiceDate = TripOnServiceDate + .of(trip.getId()) + .withTrip(trip) + .withServiceDate(serviceDate) + .build(); + + transitModel.addTripOnServiceDate(tripOnServiceDate.getId(), tripOnServiceDate); + + var stopTimes = IntStream + .range(0, stops.size()) + .mapToObj(i -> { + var stop = stops.get(i); + return createStopTime(trip, i, stop.stop(), stop.arrivalTime(), stop.departureTime()); + }) + .collect(Collectors.toList()); + + TripTimes tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, null); + + final TripPattern pattern = TransitModelForTest + .tripPattern(id + "Pattern", route) + .withStopPattern(TransitModelForTest.stopPattern(stops.stream().map(Stop::stop).toList())) + .build(); + pattern.add(tripTimes); + + transitModel.addTripPattern(pattern.getId(), pattern); + + return trip; + } + + public FeedScopedId id(String id) { + return TransitModelForTest.id(id); + } + + /** + * Returns a new fresh TransitService + */ + public TransitService getTransitService() { + return new DefaultTransitService(transitModel); + } + + public EntityResolver getEntityResolver() { + return new EntityResolver(getTransitService(), getFeedId()); + } + + public TripPattern getPatternForTrip(FeedScopedId tripId) { + return getPatternForTrip(tripId, serviceDate); + } + + public TripPattern getPatternForTrip(FeedScopedId tripId, LocalDate serviceDate) { + var transitService = getTransitService(); + var trip = transitService.getTripOnServiceDateById(tripId); + return transitService.getPatternForTrip(trip.getTrip(), serviceDate); + } + + /** + * Find the current TripTimes for a trip id on the default serviceDate + */ + public TripTimes getTripTimesForTrip(Trip trip) { + return getTripTimesForTrip(trip.getId(), serviceDate); + } + + public String getRealtimeTimetable(String tripId) { + return getRealtimeTimetable(id(tripId), serviceDate); + } + + public String getRealtimeTimetable(Trip trip) { + return getRealtimeTimetable(trip.getId(), serviceDate); + } + + public String getRealtimeTimetable(FeedScopedId tripId, LocalDate serviceDate) { + var tt = getTripTimesForTrip(tripId, serviceDate); + var pattern = getPatternForTrip(tripId); + + return TripTimesStringBuilder.encodeTripTimes(tt, pattern); + } + + public String getScheduledTimetable(String tripId) { + return getScheduledTimetable(id(tripId)); + } + + public String getScheduledTimetable(FeedScopedId tripId) { + var pattern = getPatternForTrip(tripId); + var tt = pattern.getScheduledTimetable().getTripTimes(tripId); + + return TripTimesStringBuilder.encodeTripTimes(tt, pattern); + } + + /** + * Find the current TripTimes for a trip id on the default serviceDate + */ + public TripTimes getTripTimesForTrip(String id) { + return getTripTimesForTrip(id(id), serviceDate); + } + + /** + * Find the current TripTimes for a trip id on a serviceDate + */ + public TripTimes getTripTimesForTrip(FeedScopedId tripId, LocalDate serviceDate) { + var transitService = getTransitService(); + var trip = transitService.getTripOnServiceDateById(tripId).getTrip(); + var pattern = transitService.getPatternForTrip(trip, serviceDate); + var timetable = transitService.getTimetableForTripPattern(pattern, serviceDate); + return timetable.getTripTimes(trip); + } + + public DateTimeHelper getDateTimeHelper() { + return dateTimeHelper; + } + + private StopTime createStopTime( + Trip trip, + int stopSequence, + StopLocation stop, + int arrivalTime, + int departureTime + ) { + var st = new StopTime(); + st.setTrip(trip); + st.setStopSequence(stopSequence); + st.setStop(stop); + st.setArrivalTime(arrivalTime); + st.setDepartureTime(departureTime); + return st; + } + + public String getFeedId() { + return TransitModelForTest.FEED_ID; + } + + public UpdateResult applyEstimatedTimetable(List updates) { + return applyEstimatedTimetable(updates, null); + } + + public UpdateResult applyEstimatedTimetableWithFuzzyMatcher( + List updates + ) { + SiriFuzzyTripMatcher siriFuzzyTripMatcher = new SiriFuzzyTripMatcher(getTransitService()); + return applyEstimatedTimetable(updates, siriFuzzyTripMatcher); + } + + private UpdateResult applyEstimatedTimetable( + List updates, + SiriFuzzyTripMatcher siriFuzzyTripMatcher + ) { + return this.snapshotSource.applyEstimatedTimetable( + siriFuzzyTripMatcher, + getEntityResolver(), + getFeedId(), + false, + updates + ); + } + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stops/RealtimeStopsLayerTest.java b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stops/RealtimeStopsLayerTest.java index 8f2e1f94fa6..bb258beb76f 100644 --- a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stops/RealtimeStopsLayerTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stops/RealtimeStopsLayerTest.java @@ -46,6 +46,7 @@ public void setUp() { .withName(name) .withDescription(desc) .withCoordinate(50, 10) + .withTimeZone(ZoneIds.HELSINKI) .build(); stop2 = StopModel @@ -54,6 +55,7 @@ public void setUp() { .withName(name) .withDescription(desc) .withCoordinate(51, 10) + .withTimeZone(ZoneIds.HELSINKI) .build(); } @@ -100,5 +102,6 @@ public TransitAlertService getTransitAlertService() { assertEquals("name", map.get("name")); assertEquals("desc", map.get("desc")); assertEquals(true, map.get("closedByServiceAlert")); + assertEquals(false, map.get("servicesRunningOnServiceDate")); } } diff --git a/src/ext/java/org/opentripplanner/ext/actuator/ActuatorAPI.java b/src/ext/java/org/opentripplanner/ext/actuator/ActuatorAPI.java index 0f6b6bd2601..f8b61a56ecc 100644 --- a/src/ext/java/org/opentripplanner/ext/actuator/ActuatorAPI.java +++ b/src/ext/java/org/opentripplanner/ext/actuator/ActuatorAPI.java @@ -2,8 +2,7 @@ import static org.apache.hc.core5.http.HttpHeaders.ACCEPT; -import io.micrometer.prometheus.PrometheusMeterRegistry; -import io.prometheus.client.exporter.common.TextFormat; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; @@ -23,6 +22,9 @@ public class ActuatorAPI { private static final Logger LOG = LoggerFactory.getLogger(ActuatorAPI.class); + public static final String CONTENT_TYPE_004 = "text/plain; version=0.0.4; charset=utf-8"; + public static final String CONTENT_TYPE_OPENMETRICS_100 = + "application/openmetrics-text; version=1.0.0; charset=utf-8"; /** * List the actuator endpoints available @@ -93,14 +95,14 @@ public Response health(@Context OtpServerRequestContext serverContext) { */ @GET @Path("/prometheus") - @Produces({ TextFormat.CONTENT_TYPE_004, TextFormat.CONTENT_TYPE_OPENMETRICS_100 }) + @Produces({ CONTENT_TYPE_004, CONTENT_TYPE_OPENMETRICS_100 }) public Response prometheus( @Context final PrometheusMeterRegistry prometheusRegistry, @HeaderParam(ACCEPT) @DefaultValue("*/*") final String acceptHeader ) { final var contentType = acceptHeader.contains("application/openmetrics-text") - ? TextFormat.CONTENT_TYPE_OPENMETRICS_100 - : TextFormat.CONTENT_TYPE_004; + ? CONTENT_TYPE_OPENMETRICS_100 + : CONTENT_TYPE_004; return Response .status(Response.Status.OK) diff --git a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPath.java b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPath.java index 3a692dff40b..4a494286313 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPath.java +++ b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPath.java @@ -41,12 +41,8 @@ public LineString getGeometry() { /** * Returns an (immutable) copy of this path with the duration modified. */ - public FlexPath withDurationModifier(TimePenalty mod) { - if (mod.isZero()) { - return this; - } else { - int updatedDuration = (int) mod.calculate(Duration.ofSeconds(durationSeconds)).toSeconds(); - return new FlexPath(distanceMeters, updatedDuration, geometrySupplier); - } + public FlexPath withTimePenalty(TimePenalty penalty) { + int updatedDuration = (int) penalty.calculate(Duration.ofSeconds(durationSeconds)).toSeconds(); + return new FlexPath(distanceMeters, updatedDuration, geometrySupplier); } } diff --git a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculator.java b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculator.java index 63b661f0f9a..a2252f3fec8 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculator.java +++ b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculator.java @@ -5,17 +5,17 @@ import org.opentripplanner.street.model.vertex.Vertex; /** - * A calculator to delegates the main computation to another instance and applies a duration - * modifier afterward. + * A calculator to delegates the main computation to another instance and applies a time penalty + * afterward. */ public class TimePenaltyCalculator implements FlexPathCalculator { private final FlexPathCalculator delegate; - private final TimePenalty factors; + private final TimePenalty penalty; public TimePenaltyCalculator(FlexPathCalculator delegate, TimePenalty penalty) { this.delegate = delegate; - this.factors = penalty; + this.penalty = penalty; } @Nullable @@ -26,7 +26,7 @@ public FlexPath calculateFlexPath(Vertex fromv, Vertex tov, int fromStopIndex, i if (path == null) { return null; } else { - return path.withDurationModifier(factors); + return path.withTimePenalty(penalty); } } } diff --git a/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTrip.java b/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTrip.java index 402d39e2aa7..a4c8d9568ca 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTrip.java +++ b/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTrip.java @@ -156,7 +156,7 @@ public Stream getFlexAccessTemplates( /** * Get the correct {@link FlexPathCalculator} depending on the {@code timePenalty}. - * If the modifier doesn't actually modify, we return the regular calculator. + * If the penalty would not change the result, we return the regular calculator. */ protected FlexPathCalculator flexPathCalculator(FlexPathCalculator calculator) { if (timePenalty.modifies()) { diff --git a/src/ext/java/org/opentripplanner/ext/geocoder/LuceneIndex.java b/src/ext/java/org/opentripplanner/ext/geocoder/LuceneIndex.java index 56769db0028..e0ea8ba36b9 100644 --- a/src/ext/java/org/opentripplanner/ext/geocoder/LuceneIndex.java +++ b/src/ext/java/org/opentripplanner/ext/geocoder/LuceneIndex.java @@ -6,11 +6,11 @@ import java.io.Serializable; import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Map; 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,7 +40,7 @@ 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.geocoder.StopCluster.Coordinate; +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; @@ -52,22 +52,22 @@ public class LuceneIndex implements Serializable { private static final String TYPE = "type"; private static final String ID = "id"; + private static final String SECONDARY_IDS = "secondary_ids"; private static final String SUGGEST = "suggest"; private static final String NAME = "name"; private static final String NAME_NGRAM = "name_ngram"; private static final String CODE = "code"; private static final String LAT = "latitude"; private static final String LON = "longitude"; - private static final String MODE = "mode"; - private static final String AGENCY_IDS = "agency_ids"; private final TransitService transitService; private final Analyzer analyzer; private final SuggestIndexSearcher searcher; + private final StopClusterMapper stopClusterMapper; public LuceneIndex(TransitService transitService) { this.transitService = transitService; - StopClusterMapper stopClusterMapper = new StopClusterMapper(transitService); + this.stopClusterMapper = new StopClusterMapper(transitService); this.analyzer = new PerFieldAnalyzerWrapper( @@ -95,12 +95,11 @@ public LuceneIndex(TransitService transitService) { directoryWriter, StopLocation.class, stopLocation.getId().toString(), - stopLocation.getName(), - stopLocation.getCode(), + List.of(), + ListUtils.ofNullable(stopLocation.getName()), + ListUtils.ofNullable(stopLocation.getCode()), stopLocation.getCoordinate().latitude(), - stopLocation.getCoordinate().longitude(), - Set.of(), - Set.of() + stopLocation.getCoordinate().longitude() ) ); @@ -111,12 +110,11 @@ public LuceneIndex(TransitService transitService) { directoryWriter, StopLocationsGroup.class, stopLocationsGroup.getId().toString(), - stopLocationsGroup.getName(), - null, + List.of(), + ListUtils.ofNullable(stopLocationsGroup.getName()), + List.of(), stopLocationsGroup.getCoordinate().latitude(), - stopLocationsGroup.getCoordinate().longitude(), - Set.of(), - Set.of() + stopLocationsGroup.getCoordinate().longitude() ) ); @@ -129,13 +127,12 @@ public LuceneIndex(TransitService transitService) { addToIndex( directoryWriter, StopCluster.class, - stopCluster.id().toString(), - I18NString.of(stopCluster.name()), - stopCluster.code(), + stopCluster.primaryId(), + stopCluster.secondaryIds(), + stopCluster.names(), + stopCluster.codes(), stopCluster.coordinate().lat(), - stopCluster.coordinate().lon(), - stopCluster.modes(), - stopCluster.agencyIds() + stopCluster.coordinate().lon() ) ); } @@ -183,30 +180,16 @@ public Stream queryStopClusters(String query) { } private StopCluster toStopCluster(Document document) { - var clusterId = FeedScopedId.parse(document.get(ID)); - var name = document.get(NAME); - var code = document.get(CODE); - var lat = document.getField(LAT).numericValue().doubleValue(); - var lon = document.getField(LON).numericValue().doubleValue(); - var modes = Arrays.asList(document.getValues(MODE)); - var agencies = Arrays - .stream(document.getValues(AGENCY_IDS)) - .map(id -> transitService.getAgencyForId(FeedScopedId.parse(id))) - .filter(Objects::nonNull) - .map(StopClusterMapper::toAgency) + var primaryId = FeedScopedId.parse(document.get(ID)); + var primary = stopClusterMapper.toLocation(primaryId); + + var secondaryIds = Arrays + .stream(document.getValues(SECONDARY_IDS)) + .map(FeedScopedId::parse) + .map(stopClusterMapper::toLocation) .toList(); - var feedPublisher = StopClusterMapper.toFeedPublisher( - transitService.getFeedInfo(clusterId.getFeedId()) - ); - return new StopCluster( - clusterId, - code, - name, - new Coordinate(lat, lon), - modes, - agencies, - feedPublisher - ); + + return new StopCluster(primary, secondaryIds); } static IndexWriterConfig iwcWithSuggestField(Analyzer analyzer, final Set suggestFields) { @@ -230,36 +213,33 @@ private static void addToIndex( IndexWriter writer, Class type, String id, - I18NString name, - @Nullable String code, + Collection secondaryIds, + Collection names, + Collection codes, double latitude, - double longitude, - Collection modes, - Collection agencyIds + double longitude ) { String typeName = type.getSimpleName(); Document document = new Document(); document.add(new StoredField(ID, id)); + for (var secondaryId : secondaryIds) { + document.add(new StoredField(SECONDARY_IDS, secondaryId)); + } document.add(new TextField(TYPE, typeName, Store.YES)); - document.add(new TextField(NAME, Objects.toString(name), Store.YES)); - document.add(new TextField(NAME_NGRAM, Objects.toString(name), Store.YES)); - document.add(new ContextSuggestField(SUGGEST, Objects.toString(name), 1, typeName)); + for (var name : names) { + document.add(new TextField(NAME, Objects.toString(name), Store.YES)); + document.add(new TextField(NAME_NGRAM, Objects.toString(name), Store.YES)); + document.add(new ContextSuggestField(SUGGEST, Objects.toString(name), 1, typeName)); + } document.add(new StoredField(LAT, latitude)); document.add(new StoredField(LON, longitude)); - if (code != null) { + for (var code : codes) { document.add(new TextField(CODE, code, Store.YES)); document.add(new ContextSuggestField(SUGGEST, code, 1, typeName)); } - for (var mode : modes) { - document.add(new TextField(MODE, mode, Store.YES)); - } - for (var ids : agencyIds) { - document.add(new TextField(AGENCY_IDS, ids, Store.YES)); - } - try { writer.addDocument(document); } catch (IOException ex) { diff --git a/src/ext/java/org/opentripplanner/ext/geocoder/LuceneStopCluster.java b/src/ext/java/org/opentripplanner/ext/geocoder/LuceneStopCluster.java index f58d7aa9af9..b4ee0f0919b 100644 --- a/src/ext/java/org/opentripplanner/ext/geocoder/LuceneStopCluster.java +++ b/src/ext/java/org/opentripplanner/ext/geocoder/LuceneStopCluster.java @@ -1,17 +1,15 @@ package org.opentripplanner.ext.geocoder; import java.util.Collection; -import javax.annotation.Nullable; -import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.framework.i18n.I18NString; /** * A package-private helper type for transporting data before serializing. */ record LuceneStopCluster( - FeedScopedId id, - @Nullable String code, - String name, - StopCluster.Coordinate coordinate, - Collection modes, - Collection agencyIds + String primaryId, + Collection secondaryIds, + Collection names, + Collection codes, + StopCluster.Coordinate coordinate ) {} diff --git a/src/ext/java/org/opentripplanner/ext/geocoder/StopCluster.java b/src/ext/java/org/opentripplanner/ext/geocoder/StopCluster.java index 8ffd44511fd..30647cc7b20 100644 --- a/src/ext/java/org/opentripplanner/ext/geocoder/StopCluster.java +++ b/src/ext/java/org/opentripplanner/ext/geocoder/StopCluster.java @@ -2,6 +2,7 @@ import java.util.Collection; import java.util.List; +import java.util.Objects; import javax.annotation.Nullable; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -14,15 +15,7 @@ * - if a stop has a parent station only the parent is returned * - if stops are closer than 10 meters to each and have an identical name, only one is returned */ -record StopCluster( - FeedScopedId id, - @Nullable String code, - String name, - Coordinate coordinate, - Collection modes, - List agencies, - @Nullable FeedPublisher feedPublisher -) { +record StopCluster(Location primary, Collection secondaries) { /** * Easily serializable version of a coordinate */ @@ -37,4 +30,29 @@ public record Agency(FeedScopedId id, String name) {} * Easily serializable version of a feed publisher */ public record FeedPublisher(String name) {} + + public enum LocationType { + STATION, + STOP, + } + + public record Location( + FeedScopedId id, + @Nullable String code, + LocationType type, + String name, + Coordinate coordinate, + Collection modes, + List agencies, + @Nullable FeedPublisher feedPublisher + ) { + public Location { + Objects.requireNonNull(id); + Objects.requireNonNull(name); + Objects.requireNonNull(type); + Objects.requireNonNull(coordinate); + Objects.requireNonNull(modes); + Objects.requireNonNull(agencies); + } + } } diff --git a/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java b/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java index 6f16d4a0cce..d9f388ea0e8 100644 --- a/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java +++ b/src/ext/java/org/opentripplanner/ext/geocoder/StopClusterMapper.java @@ -1,15 +1,23 @@ package org.opentripplanner.ext.geocoder; +import static org.opentripplanner.ext.geocoder.StopCluster.LocationType.STATION; +import static org.opentripplanner.ext.geocoder.StopCluster.LocationType.STOP; + import com.google.common.collect.Iterables; import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; +import javax.annotation.Nullable; import org.opentripplanner.framework.collection.ListUtils; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.model.FeedInfo; +import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.organization.Agency; +import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.site.StopLocationsGroup; import org.opentripplanner.transit.service.TransitService; @@ -37,7 +45,7 @@ Iterable generateStopClusters( Collection stopLocations, Collection stopLocationsGroups ) { - var stops = stopLocations + List stops = stopLocations .stream() // remove stop locations without a parent station .filter(sl -> sl.getParentStation() == null) @@ -46,50 +54,63 @@ Iterable generateStopClusters( .toList(); // if they are very close to each other and have the same name, only one is chosen (at random) - var deduplicatedStops = ListUtils - .distinctByKey( - stops, - sl -> new DeduplicationKey(sl.getName(), sl.getCoordinate().roundToApproximate100m()) + var deduplicatedStops = stops + .stream() + .collect( + Collectors.groupingBy(sl -> + new DeduplicationKey(sl.getName(), sl.getCoordinate().roundToApproximate100m()) + ) ) + .values() .stream() - .flatMap(s -> this.map(s).stream()) + .map(group -> map(group).orElse(null)) + .filter(Objects::nonNull) .toList(); - var stations = stopLocationsGroups.stream().map(this::map).toList(); + var stations = stopLocationsGroups.stream().map(StopClusterMapper::map).toList(); return Iterables.concat(deduplicatedStops, stations); } - LuceneStopCluster map(StopLocationsGroup g) { - var modes = transitService.getModesOfStopLocationsGroup(g).stream().map(Enum::name).toList(); - var agencies = agenciesForStopLocationsGroup(g) - .stream() - .map(s -> s.getId().toString()) - .toList(); + private static LuceneStopCluster map(StopLocationsGroup g) { + var childStops = g.getChildStops(); + var ids = childStops.stream().map(s -> s.getId().toString()).toList(); + var childNames = getNames(childStops); + var codes = getCodes(childStops); + return new LuceneStopCluster( - g.getId(), - null, - g.getName().toString(), - toCoordinate(g.getCoordinate()), - modes, - agencies + g.getId().toString(), + ids, + ListUtils.combine(List.of(g.getName()), childNames), + codes, + toCoordinate(g.getCoordinate()) ); } - Optional map(StopLocation sl) { - var agencies = agenciesForStopLocation(sl).stream().map(a -> a.getId().toString()).toList(); + private static List getCodes(Collection childStops) { + return childStops.stream().map(StopLocation::getCode).filter(Objects::nonNull).toList(); + } + + private static List getNames(Collection childStops) { + return childStops.stream().map(StopLocation::getName).filter(Objects::nonNull).toList(); + } + + private static Optional map(List stopLocations) { + var primary = stopLocations.getFirst(); + var secondaryIds = stopLocations.stream().skip(1).map(sl -> sl.getId().toString()).toList(); + var names = getNames(stopLocations); + var codes = getCodes(stopLocations); + return Optional - .ofNullable(sl.getName()) - .map(name -> { - var modes = transitService.getModesOfStopLocation(sl).stream().map(Enum::name).toList(); - return new LuceneStopCluster( - sl.getId(), - sl.getCode(), - name.toString(), - toCoordinate(sl.getCoordinate()), - modes, - agencies - ); - }); + .ofNullable(primary.getName()) + .map(name -> + new LuceneStopCluster( + primary.getId().toString(), + secondaryIds, + names, + codes, + toCoordinate(primary.getCoordinate()) + ) + ); } private List agenciesForStopLocation(StopLocation stop) { @@ -105,6 +126,59 @@ private List agenciesForStopLocationsGroup(StopLocationsGroup group) { .toList(); } + StopCluster.Location toLocation(FeedScopedId id) { + var loc = transitService.getStopLocation(id); + if (loc != null) { + var feedPublisher = toFeedPublisher(transitService.getFeedInfo(id.getFeedId())); + var modes = transitService.getModesOfStopLocation(loc).stream().map(Enum::name).toList(); + var agencies = agenciesForStopLocation(loc) + .stream() + .map(StopClusterMapper::toAgency) + .toList(); + return new StopCluster.Location( + loc.getId(), + loc.getCode(), + STOP, + loc.getName().toString(), + new StopCluster.Coordinate(loc.getLat(), loc.getLon()), + modes, + agencies, + feedPublisher + ); + } else { + var group = transitService.getStopLocationsGroup(id); + var feedPublisher = toFeedPublisher(transitService.getFeedInfo(id.getFeedId())); + var modes = transitService + .getModesOfStopLocationsGroup(group) + .stream() + .map(Enum::name) + .toList(); + var agencies = agenciesForStopLocationsGroup(group) + .stream() + .map(StopClusterMapper::toAgency) + .toList(); + return new StopCluster.Location( + group.getId(), + extractCode(group), + STATION, + group.getName().toString(), + new StopCluster.Coordinate(group.getLat(), group.getLon()), + modes, + agencies, + feedPublisher + ); + } + } + + @Nullable + private static String extractCode(StopLocationsGroup group) { + if (group instanceof Station station) { + return station.getCode(); + } else { + return null; + } + } + private static StopCluster.Coordinate toCoordinate(WgsCoordinate c) { return new StopCluster.Coordinate(c.latitude(), c.longitude()); } @@ -113,7 +187,7 @@ static StopCluster.Agency toAgency(Agency a) { return new StopCluster.Agency(a.getId(), a.getName()); } - static StopCluster.FeedPublisher toFeedPublisher(FeedInfo fi) { + private static StopCluster.FeedPublisher toFeedPublisher(FeedInfo fi) { if (fi == null) { return null; } else { diff --git a/src/ext/java/org/opentripplanner/ext/siri/EntityResolver.java b/src/ext/java/org/opentripplanner/ext/siri/EntityResolver.java index 5ca467ad1d5..3c142ac693d 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/EntityResolver.java +++ b/src/ext/java/org/opentripplanner/ext/siri/EntityResolver.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import javax.annotation.Nullable; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.organization.Operator; @@ -207,6 +208,7 @@ public Operator resolveOperator(String operatorRef) { return transitService.getOperatorForId(resolveId(operatorRef)); } + @Nullable public LocalDate resolveServiceDate(EstimatedVehicleJourney vehicleJourney) { if (vehicleJourney.getFramedVehicleJourneyRef() != null) { var dataFrame = vehicleJourney.getFramedVehicleJourneyRef().getDataFrameRef(); @@ -227,15 +229,18 @@ public LocalDate resolveServiceDate(EstimatedVehicleJourney vehicleJourney) { } } - ZonedDateTime date = CallWrapper.of(vehicleJourney).get(0).getAimedDepartureTime(); - - if (date == null) { + var datetime = CallWrapper + .of(vehicleJourney) + .stream() + .findFirst() + .map(CallWrapper::getAimedDepartureTime); + if (datetime.isEmpty()) { return null; } var daysOffset = calculateDayOffset(vehicleJourney); - return date.toLocalDate().minusDays(daysOffset); + return datetime.get().toLocalDate().minusDays(daysOffset); } /** diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java b/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java index 68b055bda46..8a747e765da 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriAlertsUpdateHandler.java @@ -35,9 +35,18 @@ import uk.org.siri.siri20.WorkflowStatusEnumeration; /** - * This updater applies the equivalent of GTFS Alerts, but from SIRI Situation Exchange feeds. NOTE - * this cannot handle situations where there are multiple feeds with different IDs (for now it may - * only work in single-feed regions). + * This updater applies the equivalent of GTFS Alerts, but from SIRI Situation Exchange (SX) feeds. + * As the incoming SIRI SX messages are mapped to internal TransitAlerts, their FeedScopedIds will + * be the single feed ID associated with this update handler, plus the situation number provided in + * the SIRI SX message. + * This class cannot handle situations where incoming messages are being applied to multiple static + * feeds with different IDs. For now it may only work in single-feed regions. A possible workaround + * is to assign the same feed ID to multiple static feeds where it is known that their entity IDs + * are all drawn from the same namespace (i.e. they are functionally fragments of the same feed). + * TODO RT_AB: Internal FeedScopedId creation strategy should probably be pluggable or configurable. + * TG has indicated this is a necessary condition for moving this updater out of sandbox. + * TODO RT_AB: The name should be clarified, as there is no such thing as "SIRI Alerts", and it + * is referencing the internal model concept of "Alerts" which are derived from GTFS terminology. */ public class SiriAlertsUpdateHandler { @@ -45,10 +54,17 @@ public class SiriAlertsUpdateHandler { private final String feedId; private final Set alerts = new HashSet<>(); private final TransitAlertService transitAlertService; - /** How long before the posted start of an event it should be displayed to users */ private final Duration earlyStart; + + /** + * This takes the parts of the SIRI SX message saying which transit entities are affected and + * maps them to the corresponding OTP internal model entity or entities. + */ private final AffectsMapper affectsMapper; + /** + * @param earlyStart display the alerts to users this long before their activePeriod begins + */ public SiriAlertsUpdateHandler( String feedId, TransitModel transitModel, @@ -90,7 +106,7 @@ public void update(ServiceDelivery delivery) { } else { TransitAlert alert = null; try { - alert = handleAlert(sxElement); + alert = mapSituationToAlert(sxElement); addedCounter++; } catch (Exception e) { LOG.info( @@ -120,7 +136,12 @@ public void update(ServiceDelivery delivery) { } } - private TransitAlert handleAlert(PtSituationElement situation) { + /** + * Build an internal model Alert from an incoming SIRI situation exchange element. + * May return null if the header, description, and detail text are all empty or missing in the + * SIRI message. In all other cases it will return a valid TransitAlert instance. + */ + private TransitAlert mapSituationToAlert(PtSituationElement situation) { TransitAlertBuilder alert = createAlertWithTexts(situation); if ( @@ -199,7 +220,10 @@ private long getEpochSecond(ZonedDateTime startTime) { } /* - * Creates alert from PtSituation with all textual content + * Creates a builder for an internal model TransitAlert. The builder is pre-filled with all + * textual content from the supplied SIRI PtSituation. The builder also has the feed scoped ID + * pre-set to the single feed ID associated with this update handler, plus the situation number + * provided in the SIRI PtSituation. */ private TransitAlertBuilder createAlertWithTexts(PtSituationElement situation) { return TransitAlert diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriFuzzyTripMatcher.java b/src/ext/java/org/opentripplanner/ext/siri/SiriFuzzyTripMatcher.java index 2f2f113fc79..a0b56dec58a 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriFuzzyTripMatcher.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriFuzzyTripMatcher.java @@ -11,6 +11,7 @@ import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.opentripplanner.framework.time.ServiceDateUtils; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.calendar.CalendarService; @@ -64,7 +65,10 @@ public static SiriFuzzyTripMatcher of(TransitService transitService) { return instance; } - private SiriFuzzyTripMatcher(TransitService transitService) { + /** + * Constructor with package access for tests only. + */ + SiriFuzzyTripMatcher(TransitService transitService) { this.transitService = transitService; initCache(this.transitService); } @@ -98,6 +102,7 @@ public Trip match( /** * Matches EstimatedVehicleJourney to a set of possible Trips based on tripId */ + @Nullable public TripAndPattern match( EstimatedVehicleJourney journey, EntityResolver entityResolver, @@ -110,6 +115,10 @@ public TripAndPattern match( return null; } + if (calls.getFirst().getAimedDepartureTime() == null) { + return null; + } + Set trips = null; if ( journey.getVehicleRef() != null && @@ -119,7 +128,7 @@ public TripAndPattern match( } if (trips == null || trips.isEmpty()) { - CallWrapper lastStop = calls.get(calls.size() - 1); + CallWrapper lastStop = calls.getLast(); String lastStopPoint = lastStop.getStopPointRef(); ZonedDateTime arrivalTime = lastStop.getAimedArrivalTime() != null ? lastStop.getAimedArrivalTime() @@ -269,20 +278,21 @@ private Set getCachedTripsByInternalPlanningCode(String internalPlanningCo /** * Finds the correct trip based on OTP-ServiceDate and SIRI-DepartureTime */ - private TripAndPattern getTripAndPatternForJourney( + @Nullable + TripAndPattern getTripAndPatternForJourney( Set trips, List calls, EntityResolver entityResolver, BiFunction getCurrentTimetable, BiFunction getRealtimeAddedTripPattern ) { - var journeyFirstStop = entityResolver.resolveQuay(calls.get(0).getStopPointRef()); - var journeyLastStop = entityResolver.resolveQuay(calls.get(calls.size() - 1).getStopPointRef()); + var journeyFirstStop = entityResolver.resolveQuay(calls.getFirst().getStopPointRef()); + var journeyLastStop = entityResolver.resolveQuay(calls.getLast().getStopPointRef()); if (journeyFirstStop == null || journeyLastStop == null) { return null; } - ZonedDateTime date = calls.get(0).getAimedDepartureTime(); + ZonedDateTime date = calls.getFirst().getAimedDepartureTime(); LocalDate serviceDate = date.toLocalDate(); int departureInSecondsSinceMidnight = ServiceDateUtils.secondsSinceStartOfService( @@ -359,7 +369,7 @@ private Trip getTripForJourney( } if (results.size() == 1) { - return results.get(0); + return results.getFirst(); } else if (results.size() > 1) { // Multiple possible matches - check if lineRef/routeId matches if ( @@ -376,7 +386,7 @@ private Trip getTripForJourney( } // Line does not match any routeId - return first result. - return results.get(0); + return results.getFirst(); } return null; diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java index 345f8deba20..99e9063418c 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java @@ -11,13 +11,9 @@ import java.time.LocalDate; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.locks.ReentrantLock; import javax.annotation.Nullable; -import org.opentripplanner.framework.time.CountdownTimer; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TimetableSnapshot; -import org.opentripplanner.model.TimetableSnapshotProvider; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; import org.opentripplanner.transit.model.framework.DataValidationException; import org.opentripplanner.transit.model.framework.Result; import org.opentripplanner.transit.model.network.TripPattern; @@ -32,6 +28,7 @@ import org.opentripplanner.updater.spi.UpdateError; import org.opentripplanner.updater.spi.UpdateResult; import org.opentripplanner.updater.spi.UpdateSuccess; +import org.opentripplanner.updater.trip.AbstractTimetableSnapshotSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.org.siri.siri20.EstimatedTimetableDeliveryStructure; @@ -42,20 +39,10 @@ * necessary to provide planning threads a consistent constant view of a graph with real-time data at * a specific point in time. */ -public class SiriTimetableSnapshotSource implements TimetableSnapshotProvider { +public class SiriTimetableSnapshotSource extends AbstractTimetableSnapshotSource { private static final Logger LOG = LoggerFactory.getLogger(SiriTimetableSnapshotSource.class); - /** - * The working copy of the timetable snapshot. Should not be visible to routing threads. Should - * only be modified by a thread that holds a lock on {@link #bufferLock}. All public methods that - * might modify this buffer will correctly acquire the lock. - */ - private final TimetableSnapshot buffer = new TimetableSnapshot(); - /** - * Lock to indicate that buffer is in use - */ - private final ReentrantLock bufferLock = new ReentrantLock(true); /** * Use a id generator to generate TripPattern ids for new TripPatterns created by RealTime * updates. @@ -69,74 +56,32 @@ public class SiriTimetableSnapshotSource implements TimetableSnapshotProvider { private final TransitModel transitModel; private final TransitService transitService; - private final TransitLayerUpdater transitLayerUpdater; - - /** - * If a timetable snapshot is requested less than this number of milliseconds after the previous - * snapshot, just return the same one. Throttles the potentially resource-consuming task of - * duplicating a TripPattern -> Timetable map and indexing the new Timetables. - */ - protected CountdownTimer snapshotFrequencyThrottle; - - /** - * The last committed snapshot that was handed off to a routing thread. This snapshot may be given - * to more than one routing thread if the maximum snapshot frequency is exceeded. - */ - private volatile TimetableSnapshot snapshot = null; - - /** Should expired real-time data be purged from the graph. */ - private final boolean purgeExpiredData; - - protected LocalDate lastPurgeDate = null; public SiriTimetableSnapshotSource( TimetableSnapshotSourceParameters parameters, TransitModel transitModel ) { + super( + transitModel.getTransitLayerUpdater(), + parameters, + () -> LocalDate.now(transitModel.getTimeZone()) + ); this.transitModel = transitModel; this.transitService = new DefaultTransitService(transitModel); - this.transitLayerUpdater = transitModel.getTransitLayerUpdater(); - this.snapshotFrequencyThrottle = new CountdownTimer(parameters.maxSnapshotFrequency()); - this.purgeExpiredData = parameters.purgeExpiredData(); this.tripPatternCache = new SiriTripPatternCache(tripPatternIdGenerator, transitService::getPatternForTrip); transitModel.initTimetableSnapshotProvider(this); - - // Force commit so that snapshot initializes - commitTimetableSnapshot(true); - } - - /** - * @return an up-to-date snapshot mapping TripPatterns to Timetables. This snapshot and the - * timetable objects it references are guaranteed to never change, so the requesting thread is - * provided a consistent view of all TripTimes. The routing thread need only release its reference - * to the snapshot to release resources. - */ - public TimetableSnapshot getTimetableSnapshot() { - TimetableSnapshot snapshotToReturn; - - // Try to get a lock on the buffer - if (bufferLock.tryLock()) { - // Make a new snapshot if necessary - try { - commitTimetableSnapshot(false); - return snapshot; - } finally { - bufferLock.unlock(); - } - } - // No lock could be obtained because there is either a snapshot commit busy or updates - // are applied at this moment, just return the current snapshot - return snapshot; } /** * Method to apply a trip update list to the most recent version of the timetable snapshot. + * FIXME RT_AB: TripUpdate is the GTFS term, and these SIRI ETs are never converted into that + * same internal model. * * @param fullDataset true iff the list with updates represent all updates that are active right * now, i.e. all previous updates should be disregarded - * @param updates SIRI VehicleMonitoringDeliveries that should be applied atomically + * @param updates SIRI EstimatedTimetable deliveries that should be applied atomically. */ public UpdateResult applyEstimatedTimetable( @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher, @@ -150,12 +95,9 @@ public UpdateResult applyEstimatedTimetable( return UpdateResult.empty(); } - // Acquire lock on buffer - bufferLock.lock(); - List> results = new ArrayList<>(); - try { + withLock(() -> { if (fullDataset) { // Remove all updates from the buffer buffer.clear(feedId); @@ -173,19 +115,9 @@ public UpdateResult applyEstimatedTimetable( LOG.debug("message contains {} trip updates", updates.size()); - // Make a snapshot after each message in anticipation of incoming requests - // Purge data if necessary (and force new snapshot if anything was purged) - // Make sure that the public (locking) getTimetableSnapshot function is not called. - if (purgeExpiredData) { - final boolean modified = purgeExpiredData(); - commitTimetableSnapshot(modified); - } else { - commitTimetableSnapshot(false); - } - } finally { - // Always release lock - bufferLock.unlock(); - } + purgeAndCommit(); + }); + return UpdateResult.ofResults(results); } @@ -247,31 +179,13 @@ private boolean shouldAddNewTrip( return entityResolver.resolveTrip(vehicleJourney) == null; } - private void commitTimetableSnapshot(final boolean force) { - if (force || snapshotFrequencyThrottle.timeIsUp()) { - if (force || buffer.isDirty()) { - LOG.debug("Committing {}", buffer); - snapshot = buffer.commit(transitLayerUpdater, force); - - // We only reset the timer when the snapshot is updated. This will cause the first - // update to be committed after a silent period. This should not have any effect in - // a busy updater. It is however useful when manually testing the updater. - snapshotFrequencyThrottle.restart(); - } else { - LOG.debug("Buffer was unchanged, keeping old snapshot."); - } - } else { - LOG.debug("Snapshot frequency exceeded. Reusing snapshot {}", snapshot); - } - } - /** * Get the latest timetable for TripPattern for a given service date. *

* Snapshot timetable is used as source if initialised, trip patterns scheduled timetable if not. */ private Timetable getCurrentTimetable(TripPattern tripPattern, LocalDate serviceDate) { - TimetableSnapshot timetableSnapshot = snapshot; + TimetableSnapshot timetableSnapshot = getTimetableSnapshot(); if (timetableSnapshot != null) { return timetableSnapshot.resolve(tripPattern, serviceDate); } @@ -372,12 +286,9 @@ private Result addTripToGraphAndBuffer(TripUpdate tr trip, serviceDate ); - - // Add new trip times to the buffer and return success + // Add new trip times to buffer, making protective copies as needed. Bubble success/error up. var result = buffer.update(pattern, tripUpdate.tripTimes(), serviceDate); - LOG.debug("Applied real-time data for trip {} on {}", trip, serviceDate); - return result; } @@ -430,19 +341,4 @@ private boolean removePreviousRealtimeUpdate(final Trip trip, final LocalDate se return success; } - - private boolean purgeExpiredData() { - final LocalDate today = LocalDate.now(transitModel.getTimeZone()); - final LocalDate previously = today.minusDays(2); // Just to be safe... - - if (lastPurgeDate != null && lastPurgeDate.compareTo(previously) > 0) { - return false; - } - - LOG.debug("purging expired real-time data"); - - lastPurgeDate = previously; - - return buffer.purgeExpiredData(previously); - } } diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java index 8b33d71ab96..8b5896a1bf6 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java @@ -12,7 +12,6 @@ import javax.annotation.Nonnull; import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.network.TripPatternBuilder; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Trip; @@ -20,23 +19,56 @@ import org.slf4j.LoggerFactory; /** - * A synchronized cache of trip patterns that are added to the graph due to GTFS-realtime messages. + * Threadsafe mechanism for tracking any TripPatterns added to the graph via SIRI realtime messages. + * This tracks only patterns added by realtime messages, not ones that already existed from the + * scheduled NeTEx. This is a "cache" in the sense that it will keep returning the same TripPattern + * when presented with the same StopPattern, so if realtime messages add many trips passing through + * the same sequence of stops, they will all end up on this same TripPattern. + *

+ * Note that there are two versions of this class, this one for GTFS-RT and another for SIRI. + * See additional comments in the Javadoc of the GTFS-RT version of this class, whose name is + * simply TripPatternCache. + * TODO RT_AB: To the extent that double SIRI/GTFS implementations are kept, prefix all names + * with GTFS or SIRI or NETEX rather than having no prefix on the GTFS versions. + * TODO RT_TG: There is no clear strategy for what should be in the cache and the transit model and the flow + * between them. The NeTEx and a GTFS version of this should be merged. Having NeTex and GTFS + * specific indexes inside is ok. With the increased usage of DatedServiceJourneys, this should probably + * be part of the main model - not a separate cashe. It is possible that this class works when it comes to + * the thread-safety, but just by looking at a few lines of code I see problems - a strategy needs to be + * analysed, designed and documented. */ public class SiriTripPatternCache { - private static final Logger log = LoggerFactory.getLogger(SiriTripPatternCache.class); + private static final Logger LOG = LoggerFactory.getLogger(SiriTripPatternCache.class); + // TODO RT_AB: Improve documentation. This seems to be the primary collection of added + // TripPatterns, with other collections serving as indexes. Similar to TripPatternCache.cache + // in the GTFS version of this class, but with service date as part of the key. private final Map cache = new HashMap<>(); + // TODO RT_AB: Improve documentation. This field appears to be an index that exists only in the + // SIRI version of this class (i.e. this version and not the older TripPatternCache that + // handles GTFS-RT). This index appears to be tailored for use by the Transmodel GraphQL APIs. private final ListMultimap patternsForStop = Multimaps.synchronizedListMultimap( ArrayListMultimap.create() ); + // TODO RT_AB: clarify name and add documentation to this field. private final Map updatedTripPatternsForTripCache = new HashMap<>(); + // TODO RT_AB: generalize this so we can generate IDs for SIRI or GTFS-RT sources. private final SiriTripPatternIdGenerator tripPatternIdGenerator; + private final Function getPatternForTrip; + /** + * TODO RT_AB: This class could potentially be reused for both SIRI and GTFS-RT, which may + * involve injecting a different ID generator and pattern fetching method. + * + * @param getPatternForTrip SiriTripPatternCache needs only this one feature of TransitService, so we retain + * only this function reference to effectively narrow the interface. This should also facilitate + * testing. + */ public SiriTripPatternCache( SiriTripPatternIdGenerator tripPatternIdGenerator, Function getPatternForTrip @@ -46,8 +78,13 @@ public SiriTripPatternCache( } /** - * Get cached trip pattern or create one if it doesn't exist yet. If a trip pattern is created, - * vertices and edges for this trip pattern are also created in the transitModel. + * Get cached trip pattern or create one if it doesn't exist yet. + * + * TODO RT_AB: Improve documentation and/or merge with GTFS version of this class. + * This was clearly derived from a method from TripPatternCache. This is the only non-dead-code + * public method on this class, and mirrors the only public method on the GTFS-RT version of + * TripPatternCache. It also explains why this class is called a "cache". It allows reusing the + * same TripPattern instance when many different trips are created or updated with the same pattern. * * @param stopPattern stop pattern to retrieve/create trip pattern * @param trip Trip containing route of new trip pattern in case a new trip pattern will be @@ -61,6 +98,9 @@ public synchronized TripPattern getOrCreateTripPattern( ) { TripPattern originalTripPattern = getPatternForTrip.apply(trip); + // TODO RT_AB: Verify implementation, which is different than the GTFS-RT version. + // It can return a TripPattern from the scheduled data, but protective copies are handled in + // TimetableSnapshot.update. Better document this aspect of the contract in this method's Javadoc. if (originalTripPattern.getStopPattern().equals(stopPattern)) { return originalTripPattern; } @@ -72,56 +112,57 @@ public synchronized TripPattern getOrCreateTripPattern( // Create TripPattern if it doesn't exist yet if (tripPattern == null) { var id = tripPatternIdGenerator.generateUniqueTripPatternId(trip); - TripPatternBuilder tripPatternBuilder = TripPattern - .of(id) - .withRoute(trip.getRoute()) - .withMode(trip.getMode()) - .withNetexSubmode(trip.getNetexSubMode()) - .withStopPattern(stopPattern); - - // TODO - SIRI: Add pattern to transitModel index? - - tripPatternBuilder.withCreatedByRealtimeUpdater(true); - tripPatternBuilder.withOriginalTripPattern(originalTripPattern); - - tripPattern = tripPatternBuilder.build(); + tripPattern = + TripPattern + .of(id) + .withRoute(trip.getRoute()) + .withMode(trip.getMode()) + .withNetexSubmode(trip.getNetexSubMode()) + .withStopPattern(stopPattern) + .withCreatedByRealtimeUpdater(true) + .withOriginalTripPattern(originalTripPattern) + .build(); + // TODO: Add pattern to transitModel index? // Add pattern to cache cache.put(key, tripPattern); } - /** - * - * When the StopPattern is first modified (e.g. change of platform), then updated (or vice versa), the stopPattern is altered, and - * the StopPattern-object for the different states will not be equal. - * - * This causes both tripPatterns to be added to all unchanged stops along the route, which again causes duplicate results - * in departureRow-searches (one departure for "updated", one for "modified"). - * - * Full example: - * Planned stops: Stop 1 - Platform 1, Stop 2 - Platform 1 - * - * StopPattern #rt1: "updated" stopPattern cached in 'patternsForStop': - * - Stop 1, Platform 1 - * - StopPattern #rt1 - * - Stop 2, Platform 1 - * - StopPattern #rt1 - * - * "modified" stopPattern: Stop 1 - Platform 1, Stop 2 - Platform 2 - * - * StopPattern #rt2: "modified" stopPattern cached in 'patternsForStop' will then be: - * - Stop 1, Platform 1 - * - StopPattern #rt1, StopPattern #rt2 - * - Stop 2, Platform 1 - * - StopPattern #rt1 - * - Stop 2, Platform 2 - * - StopPattern #rt2 - * - * - * Therefore, we must cleanup the duplicates by deleting the previously added (and thus outdated) - * tripPattern for all affected stops. In example above, "StopPattern #rt1" should be removed from all stops - * - */ + /* + When the StopPattern is first modified (e.g. change of platform), then updated (or vice + versa), the stopPattern is altered, and the StopPattern-object for the different states will + not be equal. + + This causes both tripPatterns to be added to all unchanged stops along the route, which again + causes duplicate results in departureRow-searches (one departure for "updated", one for + "modified"). + + Full example: + Planned stops: Stop 1 - Platform 1, Stop 2 - Platform 1 + + StopPattern #rt1: "updated" stopPattern cached in 'patternsForStop': + - Stop 1, Platform 1 + - StopPattern #rt1 + - Stop 2, Platform 1 + - StopPattern #rt1 + + "modified" stopPattern: Stop 1 - Platform 1, Stop 2 - Platform 2 + + StopPattern #rt2: "modified" stopPattern cached in 'patternsForStop' will then be: + - Stop 1, Platform 1 + - StopPattern #rt1, StopPattern #rt2 + - Stop 2, Platform 1 + - StopPattern #rt1 + - Stop 2, Platform 2 + - StopPattern #rt2 + + Therefore, we must clean up the duplicates by deleting the previously added (and thus + outdated) tripPattern for all affected stops. In example above, "StopPattern #rt1" should be + removed from all stops. + + TODO RT_AB: review why this particular case is handled in an ad-hoc manner. It seems like all + such indexes should be constantly rebuilt and versioned along with the TimetableSnapshot. + */ TripServiceDateKey tripServiceDateKey = new TripServiceDateKey(trip, serviceDate); if (updatedTripPatternsForTripCache.containsKey(tripServiceDateKey)) { // Remove previously added TripPatterns for the trip currently being updated - if the stopPattern does not match @@ -132,16 +173,14 @@ public synchronized TripPattern getOrCreateTripPattern( patternsForStop.values().removeAll(Arrays.asList(cachedTripPattern)); int sizeAfter = patternsForStop.values().size(); - log.debug( + LOG.debug( "Removed outdated TripPattern for {} stops in {} ms - tripId: {}", (sizeBefore - sizeAfter), (System.currentTimeMillis() - t1), trip.getId() ); - /* - TODO: Also remove previously updated - now outdated - TripPattern from cache ? - cache.remove(new StopPatternServiceDateKey(cachedTripPattern.stopPattern, serviceDate)); - */ + // TODO: Also remove previously updated - now outdated - TripPattern from cache ? + // cache.remove(new StopPatternServiceDateKey(cachedTripPattern.stopPattern, serviceDate)); } } @@ -160,6 +199,7 @@ public synchronized TripPattern getOrCreateTripPattern( /** * Returns any new TripPatterns added by real time information for a given stop. + * TODO RT_AB: this appears to be currently unused. Perhaps remove it if the API has changed. * * @param stop the stop * @return list of TripPatterns created by real time sources for the stop. @@ -169,6 +209,16 @@ public List getAddedTripPatternsForStop(RegularStop stop) { } } +// TODO RT_AB: move the below classes inside the above class as private static inner classes. +// Defining these additional classes in the same top-level class file is unconventional. + +/** + * Serves as the key for the collection of TripPatterns added by realtime messages. + * Must define hashcode and equals to confer semantic identity. + * TODO RT_AB: clarify why each date has a different TripPattern instead of a different Timetable. + * It seems like there's a separate TripPattern instance for each StopPattern and service date, + * rather a single TripPattern instance associated with a separate timetable for each date. + */ class StopPatternServiceDateKey { StopPattern stopPattern; @@ -194,6 +244,11 @@ public boolean equals(Object thatObject) { } } +/** + * An alternative key for looking up realtime-added TripPatterns by trip and service date instead + * of stop pattern and service date. Must define hashcode and equals to confer semantic identity. + * TODO RT_AB: verify whether one map is considered the definitive collection and the other an index. + */ class TripServiceDateKey { Trip trip; diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java index dc03550086f..1b56938d712 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternIdGenerator.java @@ -8,20 +8,20 @@ import org.opentripplanner.transit.model.timetable.Trip; /** - * This class generate a new id for new TripPatterns created real-time by the SIRI updaters. It is - * important to creat only on instance of this class, and inject it where it is needed. - *

- * The id generation is thread-safe, even if that is probably not needed. + * This class generates new unique IDs for TripPatterns created in response to real-time updates + * from the SIRI updaters. In non-test usage it is important to create only one instance of this + * class, and inject that single instance wherever it is needed. However, this single-instance + * usage pattern is not enforced due to differing needs in tests. + * The ID generation is threadsafe, even if that is probably not needed. */ class SiriTripPatternIdGenerator { private final AtomicInteger counter = new AtomicInteger(0); /** - * Generate unique trip pattern code for real-time added trip pattern. This function roughly - * follows the format of {@link GenerateTripPatternsOperation}. - *

- * The generator add a postfix 'RT' to indicate that this trip pattern is generated at REAL-TIME. + * Generate a unique ID for a trip pattern added in response to a realtime message. This function + * roughly follows the format of {@link GenerateTripPatternsOperation}. The generator suffixes the + * ID with 'RT' to indicate that this trip pattern is generated in response to a realtime message. */ FeedScopedId generateUniqueTripPatternId(Trip trip) { Route route = trip.getRoute(); diff --git a/src/ext/java/org/opentripplanner/ext/siri/mapper/AffectsMapper.java b/src/ext/java/org/opentripplanner/ext/siri/mapper/AffectsMapper.java index 734cc8edbd3..a1fb943f60c 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/mapper/AffectsMapper.java +++ b/src/ext/java/org/opentripplanner/ext/siri/mapper/AffectsMapper.java @@ -33,6 +33,10 @@ /** * Maps a {@link AffectsScopeStructure} to a list of {@link EntitySelector}s + * + * Concretely: this takes the parts of the SIRI SX (Alerts) message describing which transit + * entities are concerned by the alert, and maps them to EntitySelectors, which can match multiple + * OTP internal model entities that should be associated with the message. */ public class AffectsMapper { diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java b/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java index ac98839fb42..83edebe3911 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/EstimatedTimetableSource.java @@ -3,6 +3,11 @@ import java.util.Optional; import uk.org.siri.siri20.Siri; +/** + * Interface for a blocking, polling approach to retrieving SIRI realtime timetable updates. + * TODO RT_AB: Clearly document whether the methods should return as fast as possible, or if they + * should intentionally block and wait for refreshed data, and how this fits into the design. + */ public interface EstimatedTimetableSource { /** * Wait for one message to arrive, and decode it into a List of TripUpdates. Blocking call. diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETGooglePubsubUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETGooglePubsubUpdater.java index 9a1c5690c58..7f79413c478 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETGooglePubsubUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETGooglePubsubUpdater.java @@ -166,8 +166,8 @@ public SiriETGooglePubsubUpdater( } @Override - public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { - this.saveResultOnGraph = saveResultOnGraph; + public void setup(WriteToGraphCallback writeToGraphCallback) { + this.saveResultOnGraph = writeToGraphCallback; } @Override diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java index c8ccd2c533b..66007f4aed8 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriETUpdater.java @@ -77,8 +77,8 @@ public SiriETUpdater( } @Override - public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { - this.saveResultOnGraph = saveResultOnGraph; + public void setup(WriteToGraphCallback writeToGraphCallback) { + this.saveResultOnGraph = writeToGraphCallback; } /** diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java index 014d5b24061..5ededbb3bf0 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriSXUpdater.java @@ -31,8 +31,11 @@ public class SiriSXUpdater extends PollingGraphUpdater implements TransitAlertPr private final String url; private final String originalRequestorRef; private final TransitAlertService transitAlertService; + + // TODO RT_AB: Document why SiriAlertsUpdateHandler is a separate instance that persists across + // many graph update operations. private final SiriAlertsUpdateHandler updateHandler; - private WriteToGraphCallback saveResultOnGraph; + private WriteToGraphCallback writeToGraphCallback; private ZonedDateTime lastTimestamp = ZonedDateTime.now().minusWeeks(1); private String requestorRef; /** @@ -84,8 +87,8 @@ public SiriSXUpdater(SiriSXUpdaterParameters config, TransitModel transitModel) } @Override - public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { - this.saveResultOnGraph = saveResultOnGraph; + public void setup(WriteToGraphCallback writeToGraphCallback) { + this.writeToGraphCallback = writeToGraphCallback; } public TransitAlertService getTransitAlertService() { @@ -101,6 +104,10 @@ protected void runPolling() throws InterruptedException { retry.execute(this::updateSiri); } + /** + * This part of the update process has been factored out to allow repeated retries of the HTTP + * fetching operation in case the connection fails or some other disruption happens. + */ private void updateSiri() { boolean moreData = false; do { @@ -112,7 +119,37 @@ private void updateSiri() { // primitive, because the object moreData persists across iterations. final boolean markPrimed = !moreData; if (serviceDelivery.getSituationExchangeDeliveries() != null) { - saveResultOnGraph.execute((graph, transitModel) -> { + // FIXME RT_AB: This is submitting a reference to a method on a long-lived instance as a + // GraphWriterRunnable. These runnables were originally intended to be small, + // self-contained, throw-away update tasks. + // See org/opentripplanner/updater/trip/PollingTripUpdater.java:90 + // Clarify why the long-lived instance is capturing and holding so many references. + // The runnable should only contain the minimum needed to operate on the graph. + // Such runnables should be illustrated in documentation as e.g. a little box labeled + // "change trip ABC123 by making stop 53 late by 2 minutes." + // Also clarify how this runnable works without even using the supplied + // (graph, transitModel) parameters. There are multiple TransitAlertServices and they + // are not versioned along with the Graph, they are attached to updaters. + // + // This is submitting a runnable to an executor, but that runnable only writes back to + // objects referenced by updateHandler itself, rather than the graph or transitModel + // supplied for writing, and apparently with no versioning. This seems like a + // misinterpretation of the realtime design. + // If this is an intentional choice to live-patch a single server-wide instance of an + // alerts service/index while it's already in use by routing, we should be clear about + // this and document why it differs from the graph-writer design. Currently the code + // seems to follow some surface conventions of the threadsafe copy-on-write pattern + // without actually providing threadsafe behavior. + // It's a reasonable choice to defer processing the list of alerts to another thread than + // this fetching thread, but we probably don't want to defer any such processing to the + // graph writer thread, as that's explicitly restricted to be one single shared thread for + // the entire application. There seems to be a misunderstanding that the tasks are + // submitted to get them off the updater thread, but the real reason is to ensure + // consistent transactions in graph writing and reading. + // All that said, out of all the update types, Alerts (and SIRI SX) are probably the ones + // that would be most tolerant of non-versioned application-wide storage since they don't + // participate in routing and are tacked on to already-completed routing responses. + writeToGraphCallback.execute((graph, transitModel) -> { updateHandler.update(serviceDelivery); if (markPrimed) { primed = true; diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java b/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java index f211468cc66..a72c1797d38 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/azure/AbstractAzureSiriUpdater.java @@ -99,8 +99,8 @@ public AbstractAzureSiriUpdater(SiriAzureUpdaterParameters config, TransitModel protected abstract void errorConsumer(ServiceBusErrorContext errorContext); @Override - public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { - this.saveResultOnGraph = saveResultOnGraph; + public void setup(WriteToGraphCallback writeToGraphCallback) { + this.saveResultOnGraph = writeToGraphCallback; } @Override diff --git a/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/DigitransitRealtimeStopPropertyMapper.java b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/DigitransitRealtimeStopPropertyMapper.java index 55d67d28d71..cf555d6412f 100644 --- a/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/DigitransitRealtimeStopPropertyMapper.java +++ b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/DigitransitRealtimeStopPropertyMapper.java @@ -3,6 +3,7 @@ import static org.opentripplanner.ext.vectortiles.layers.stops.DigitransitStopPropertyMapper.getBaseKeyValues; import java.time.Instant; +import java.time.LocalDate; import java.util.Collection; import java.util.List; import java.util.Locale; @@ -10,6 +11,7 @@ import org.opentripplanner.framework.collection.ListUtils; import org.opentripplanner.framework.i18n.I18NStringMapper; import org.opentripplanner.inspector.vector.KeyValue; +import org.opentripplanner.routing.stoptimes.ArrivalDeparture; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.service.TransitService; @@ -32,10 +34,19 @@ protected Collection map(RegularStop stop) { .stream() .anyMatch(alert -> alert.noServiceAt(currentTime)); + var serviceDate = LocalDate.now(transitService.getTimeZone()); + boolean stopTimesExist = transitService + .getStopTimesForStop(stop, serviceDate, ArrivalDeparture.BOTH, true) + .stream() + .anyMatch(stopTime -> stopTime.times.size() > 0); + Collection sharedKeyValues = getBaseKeyValues(stop, i18NStringMapper, transitService); return ListUtils.combine( sharedKeyValues, - List.of(new KeyValue("closedByServiceAlert", noServiceAlert)) + List.of( + new KeyValue("closedByServiceAlert", noServiceAlert), + new KeyValue("servicesRunningOnServiceDate", stopTimesExist) + ) ); } } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java index 73f60fdda98..9175d3486e1 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java @@ -141,7 +141,6 @@ protected static GraphQLSchema buildSchema() { .type(typeWiring.build(DepartureRowImpl.class)) .type(typeWiring.build(elevationProfileComponentImpl.class)) .type(typeWiring.build(FeedImpl.class)) - .type(typeWiring.build(FeedImpl.class)) .type(typeWiring.build(GeometryImpl.class)) .type(typeWiring.build(ItineraryImpl.class)) .type(typeWiring.build(LegImpl.class)) diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/FeedImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/FeedImpl.java index 7d7c62136b6..d6488d3f375 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/FeedImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/FeedImpl.java @@ -9,6 +9,7 @@ import org.opentripplanner.apis.gtfs.GraphQLRequestContext; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.apis.gtfs.model.FeedPublisher; import org.opentripplanner.routing.alertpatch.EntitySelector; import org.opentripplanner.routing.alertpatch.TransitAlert; import org.opentripplanner.routing.services.TransitAlertService; @@ -64,6 +65,17 @@ public DataFetcher feedId() { return this::getSource; } + @Override + public DataFetcher publisher() { + return environment -> { + String id = getSource(environment); + return new FeedPublisher( + getTransitService(environment).getFeedInfo(id).getPublisherName(), + getTransitService(environment).getFeedInfo(id).getPublisherUrl() + ); + }; + } + private List getAgencies(DataFetchingEnvironment environment) { String id = getSource(environment); return getTransitService(environment) diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java index 4bce64634bc..4d740a41fb4 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java @@ -21,6 +21,7 @@ import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLRelativeDirection; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLRoutingErrorCode; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLTransitMode; +import org.opentripplanner.apis.gtfs.model.FeedPublisher; import org.opentripplanner.apis.gtfs.model.PlanPageInfo; import org.opentripplanner.apis.gtfs.model.RideHailingProvider; import org.opentripplanner.apis.gtfs.model.StopPosition; @@ -392,6 +393,15 @@ public interface GraphQLFeed { public DataFetcher> alerts(); public DataFetcher feedId(); + + public DataFetcher publisher(); + } + + /** Feed publisher information */ + public interface GraphQLFeedPublisher { + public DataFetcher name(); + + public DataFetcher url(); } public interface GraphQLGeometry { 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 6fff500d137..1b8487b26a8 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 @@ -63,6 +63,7 @@ config: elevationProfileComponent: org.opentripplanner.model.plan.ElevationProfile.Step Emissions: org.opentripplanner.model.plan.Emissions#Emissions Feed: String + FeedPublisher: org.opentripplanner.apis.gtfs.model.FeedPublisher#FeedPublisher Geometry: org.locationtech.jts.geom.Geometry#Geometry InputField: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLInputField#GraphQLInputField Itinerary: org.opentripplanner.model.plan.Itinerary#Itinerary diff --git a/src/main/java/org/opentripplanner/apis/gtfs/model/FeedPublisher.java b/src/main/java/org/opentripplanner/apis/gtfs/model/FeedPublisher.java new file mode 100644 index 00000000000..ba524b537f0 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/model/FeedPublisher.java @@ -0,0 +1,10 @@ +package org.opentripplanner.apis.gtfs.model; + +import java.util.Objects; + +public record FeedPublisher(String name, String url) { + public FeedPublisher { + Objects.requireNonNull(name); + Objects.requireNonNull(url); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/OTPRequestTimeoutInstrumentation.java b/src/main/java/org/opentripplanner/apis/transmodel/OTPRequestTimeoutInstrumentation.java new file mode 100644 index 00000000000..acb27eb4e87 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/OTPRequestTimeoutInstrumentation.java @@ -0,0 +1,36 @@ +package org.opentripplanner.apis.transmodel; + +import static graphql.execution.instrumentation.SimpleInstrumentationContext.noOp; + +import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.InstrumentationContext; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; +import java.util.concurrent.atomic.AtomicLong; +import org.opentripplanner.framework.application.OTPRequestTimeoutException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A GraphQL instrumentation that periodically checks the OTP request interruption status while the + * query is being processed. + */ +public class OTPRequestTimeoutInstrumentation implements Instrumentation { + + private static final Logger LOG = LoggerFactory.getLogger(OTPRequestTimeoutInstrumentation.class); + + private final AtomicLong fieldFetchCounter = new AtomicLong(); + + @Override + public InstrumentationContext beginFieldFetch( + InstrumentationFieldFetchParameters parameters, + InstrumentationState state + ) { + long fetched = fieldFetchCounter.incrementAndGet(); + if (fetched % 100000 == 0) { + LOG.debug("Fetched {} fields", fetched); + OTPRequestTimeoutException.checkForTimeout(); + } + return noOp(); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java index 051e369dde0..2cd405212c2 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java @@ -79,7 +79,10 @@ Response executeGraphQL( } private static Instrumentation createInstrumentation(int maxResolves, Iterable tracingTags) { - Instrumentation instrumentation = new MaxQueryComplexityInstrumentation(maxResolves); + Instrumentation instrumentation = new ChainedInstrumentation( + new MaxQueryComplexityInstrumentation(maxResolves), + new OTPRequestTimeoutInstrumentation() + ); if (OTPFeature.ActuatorAPI.isOn()) { instrumentation = diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/stop/QuayType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/stop/QuayType.java index 0b22086dfaf..e146b537c1c 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/stop/QuayType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/stop/QuayType.java @@ -1,5 +1,7 @@ package org.opentripplanner.apis.transmodel.model.stop; +import static org.opentripplanner.apis.transmodel.support.GqlUtil.getPositiveNonNullIntegerArgument; + import graphql.Scalars; import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLFieldDefinition; @@ -214,6 +216,9 @@ public static GraphQLObjectType create( GraphQLArgument .newArgument() .name("timeRange") + .description( + "Duration in seconds from start time to search forward for estimated calls. Must be a positive value. Default value is 24 hours" + ) .type(Scalars.GraphQLInt) .defaultValue(24 * 60 * 60) .build() @@ -299,8 +304,8 @@ public static GraphQLObjectType create( Integer departuresPerLineAndDestinationDisplay = environment.getArgument( "numberOfDeparturesPerLineAndDestinationDisplay" ); - Integer timeRangeInput = environment.getArgument("timeRange"); - Duration timeRange = Duration.ofSeconds(timeRangeInput.longValue()); + int timeRangeInput = getPositiveNonNullIntegerArgument(environment, "timeRange"); + Duration timeRange = Duration.ofSeconds(timeRangeInput); StopLocation stop = environment.getSource(); JourneyWhiteListed whiteListed = new JourneyWhiteListed(environment); diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/stop/StopPlaceType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/stop/StopPlaceType.java index d178e5125b5..13a1521bf1a 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/stop/StopPlaceType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/stop/StopPlaceType.java @@ -1,6 +1,7 @@ package org.opentripplanner.apis.transmodel.model.stop; import static java.lang.Boolean.TRUE; +import static org.opentripplanner.apis.transmodel.support.GqlUtil.getPositiveNonNullIntegerArgument; import graphql.Scalars; import graphql.schema.DataFetchingEnvironment; @@ -289,6 +290,9 @@ public static GraphQLObjectType create( GraphQLArgument .newArgument() .name("timeRange") + .description( + "Duration in seconds from start time to search forward for estimated calls. Must be a positive value. Default value is 24 hours" + ) .type(Scalars.GraphQLInt) .defaultValue(24 * 60 * 60) .build() @@ -356,8 +360,8 @@ public static GraphQLObjectType create( Integer departuresPerLineAndDestinationDisplay = environment.getArgument( "numberOfDeparturesPerLineAndDestinationDisplay" ); - Integer timeRangeInput = environment.getArgument("timeRange"); - Duration timeRage = Duration.ofSeconds(timeRangeInput.longValue()); + int timeRangeInput = getPositiveNonNullIntegerArgument(environment, "timeRange"); + Duration timeRange = Duration.ofSeconds(timeRangeInput); MonoOrMultiModalStation monoOrMultiModalStation = environment.getSource(); JourneyWhiteListed whiteListed = new JourneyWhiteListed(environment); @@ -374,7 +378,7 @@ public static GraphQLObjectType create( getTripTimesForStop( singleStop, startTime, - timeRage, + timeRange, arrivalDeparture, includeCancelledTrips, numberOfDepartures, @@ -419,7 +423,7 @@ public static GraphQLObjectType create( public static Stream getTripTimesForStop( StopLocation stop, Instant startTimeSeconds, - Duration timeRage, + Duration timeRange, ArrivalDeparture arrivalDeparture, boolean includeCancelledTrips, int numberOfDepartures, @@ -434,7 +438,7 @@ public static Stream getTripTimesForStop( List stopTimesInPatterns = transitService.stopTimesForStop( stop, startTimeSeconds, - timeRage, + timeRange, numberOfDepartures, arrivalDeparture, includeCancelledTrips 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 3700ea60332..1f0722eb991 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/support/GqlUtil.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/support/GqlUtil.java @@ -103,6 +103,25 @@ public static boolean hasArgument(DataFetchingEnvironment environment, String na return environment.containsArgument(name) && environment.getArgument(name) != null; } + /** + * Return the integer value of the argument or throw an exception if the value is null + * or strictly negative. + * This should generally be handled at the GraphQL schema level, + * but must sometimes be implemented programmatically to preserve backward compatibility. + */ + public static int getPositiveNonNullIntegerArgument( + DataFetchingEnvironment environment, + String argumentName + ) { + Integer argumentValue = environment.getArgument(argumentName); + if (argumentValue == null || argumentValue < 0) { + throw new IllegalArgumentException( + "The argument '" + argumentName + "' should be a non-null positive value: " + argumentValue + ); + } + return argumentValue; + } + public static List listOfNullSafe(T element) { return element == null ? List.of() : List.of(element); } diff --git a/src/main/java/org/opentripplanner/framework/collection/ListUtils.java b/src/main/java/org/opentripplanner/framework/collection/ListUtils.java index 513c0bcc0d3..5964a1674e3 100644 --- a/src/main/java/org/opentripplanner/framework/collection/ListUtils.java +++ b/src/main/java/org/opentripplanner/framework/collection/ListUtils.java @@ -57,4 +57,16 @@ public static List distinctByKey( return ret; } + + /** + * Take a single nullable variable and return an empty list if it is null. Otherwise + * return a list with one element. + */ + public static List ofNullable(T input) { + if (input == null) { + return List.of(); + } else { + return List.of(input); + } + } } diff --git a/src/main/java/org/opentripplanner/framework/retry/OtpRetry.java b/src/main/java/org/opentripplanner/framework/retry/OtpRetry.java index c53250c8fd6..1a9136653fd 100644 --- a/src/main/java/org/opentripplanner/framework/retry/OtpRetry.java +++ b/src/main/java/org/opentripplanner/framework/retry/OtpRetry.java @@ -17,6 +17,11 @@ public class OtpRetry { private final Duration initialRetryInterval; private final int backoffMultiplier; private final Runnable onRetry; + + /** + * A predicate to determine whether a particular exception should end the retry cycle or not. + * If the predicate returns true, retries will continue. False, and the retry cycle is broken. + */ private final Predicate retryableException; OtpRetry( diff --git a/src/main/java/org/opentripplanner/framework/time/DateUtils.java b/src/main/java/org/opentripplanner/framework/time/DateUtils.java index f2a0bc57f54..d212af56b79 100644 --- a/src/main/java/org/opentripplanner/framework/time/DateUtils.java +++ b/src/main/java/org/opentripplanner/framework/time/DateUtils.java @@ -11,6 +11,7 @@ import java.time.format.DateTimeParseException; import java.util.List; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,6 +52,7 @@ public class DateUtils { * Returns a Date object based on input date and time parameters Defaults to today / now (when * date / time are null) */ + @Nullable public static ZonedDateTime toZonedDateTime(String date, String time, ZoneId tz) { //LOG.debug("JVM default timezone is {}", TimeZone.getDefault()); LOG.debug("Parsing date {} and time {}", date, time); @@ -179,7 +181,18 @@ public static long absoluteTimeout(Duration timeout) { } } - private static LocalTime parseTime(String time) { + /** + * Parse a time string on different formats: + * + *
+   * Hour Minute Second      "10:02:03"
+   * Hour Minute             "10:02"
+   * Seconds past midningt   "3600"
+   * AM / PM                 "11:30 PM"
+   * 
+ */ + @Nullable + public static LocalTime parseTime(String time) { boolean amPm = false; int addHours = 0; int hour = 0, min = 0, sec = 0; diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java index ce65d6b0820..354c1f16177 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/GTFSToOtpTransitServiceMapper.java @@ -171,7 +171,7 @@ public void mapStopTripAndRouteDataIntoBuilder() { builder.getPathways().addAll(pathwayMapper.map(data.getAllPathways())); builder.getStopTimesSortedByTrip().addAll(stopTimeMapper.map(data.getAllStopTimes())); - builder.getFlexTimePenalty().putAll(tripMapper.flexSafeDurationModifiers()); + builder.getFlexTimePenalty().putAll(tripMapper.flexSafeTimePenalties()); builder.getTripsById().addAll(tripMapper.map(data.getAllTrips())); fareRulesBuilder.fareAttributes().addAll(fareAttributeMapper.map(data.getAllFareAttributes())); diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java index ca221c1c6ca..3a62ba2b269 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/TripMapper.java @@ -18,7 +18,7 @@ class TripMapper { private final TranslationHelper translationHelper; private final Map mappedTrips = new HashMap<>(); - private final Map flexSafeDurationModifiers = new HashMap<>(); + private final Map flexSafeTimePenalties = new HashMap<>(); TripMapper( RouteMapper routeMapper, @@ -45,8 +45,8 @@ Collection getMappedTrips() { /** * The map of flex duration factors per flex trip. */ - Map flexSafeDurationModifiers() { - return flexSafeDurationModifiers; + Map flexSafeTimePenalties() { + return flexSafeTimePenalties; } private Trip doMap(org.onebusaway.gtfs.model.Trip rhs) { @@ -73,11 +73,11 @@ private Trip doMap(org.onebusaway.gtfs.model.Trip rhs) { lhs.withBikesAllowed(BikeAccessMapper.mapForTrip(rhs)); var trip = lhs.build(); - mapSafeDurationModifier(rhs).ifPresent(f -> flexSafeDurationModifiers.put(trip, f)); + mapSafeTimePenalty(rhs).ifPresent(f -> flexSafeTimePenalties.put(trip, f)); return trip; } - private Optional mapSafeDurationModifier(org.onebusaway.gtfs.model.Trip rhs) { + private Optional mapSafeTimePenalty(org.onebusaway.gtfs.model.Trip rhs) { if (rhs.getSafeDurationFactor() == null && rhs.getSafeDurationOffset() == null) { return Optional.empty(); } else { diff --git a/src/main/java/org/opentripplanner/model/Timetable.java b/src/main/java/org/opentripplanner/model/Timetable.java index 031e95e8870..10c384e6211 100644 --- a/src/main/java/org/opentripplanner/model/Timetable.java +++ b/src/main/java/org/opentripplanner/model/Timetable.java @@ -42,14 +42,15 @@ import org.slf4j.LoggerFactory; /** + * A Timetable is a TripTimes (stop-level details like arrival and departure times) for each of the + * trips on a particular TripPattern. * Timetables provide most of the TripPattern functionality. Each TripPattern may possess more than * one Timetable when stop time updates are being applied: one for the scheduled stop times, one for * each snapshot of updated stop times, another for a working buffer of updated stop times, etc. *

- * TODO OTP2 - Move this to package: org.opentripplanner.model - * - after as Entur NeTEx PRs are merged. - * - Also consider moving its dependencies in: org.opentripplanner.routing - * - The NEW Timetable should not have any dependencies to + * TODO OTP2 - Move this to package: org.opentripplanner.model after as Entur NeTEx PRs are merged. + * Also consider moving its dependencies into package org.opentripplanner.routing. The NEW + * Timetable should not have any dependencies to [?] */ public class Timetable implements Serializable { @@ -126,7 +127,7 @@ public TripTimes getTripTimes(Trip trip) { public TripTimes getTripTimes(FeedScopedId tripId) { for (TripTimes tt : tripTimes) { - if (tt.getTrip().getId() == tripId) { + if (tt.getTrip().getId().equals(tripId)) { return tt; } } diff --git a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java index 066a27ba15a..e1dfd5681c0 100644 --- a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java +++ b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java @@ -20,7 +20,6 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; -import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.updater.spi.UpdateError; import org.opentripplanner.updater.spi.UpdateSuccess; @@ -28,56 +27,87 @@ import org.slf4j.LoggerFactory; /** - * Part of concurrency control for stoptime updates. + * A TimetableSnapshot holds a set of realtime-updated Timetables frozen at a moment in time. It + * can return a Timetable for any TripPattern in the public transit network considering all + * accumulated realtime updates, falling back on the scheduled Timetable if no updates have been + * applied for a given TripPattern. *

- * All updates should be performed on a snapshot before it is handed off to any searches. A single + * This is a central part of managing concurrency when many routing searches may be happening, but + * realtime updates are also streaming in which change the vehicle arrival and departure times. + * Any given request will only see one unchanging TimetableSnapshot over the course of its search. + *

+ * An instance of TimetableSnapshot first serves as a buffer to accumulate a batch of incoming + * updates on top of any already known updates to the base schedules. From time to time such a batch + * of updates is committed (like a database transaction). At this point the TimetableSnapshot is + * treated as immutable and becomes available for use by new incoming routing requests. + *

+ * All updates to a snapshot must be completed before it is handed off to any searches. A single * snapshot should be used for an entire search, and should remain unchanged for that duration to * provide a consistent view not only of trips that have been boarded, but of relative arrival and * departure times of other trips that have not necessarily been boarded. *

- * At this point, only one writing thread at a time is supported. + * A TimetableSnapshot instance may only be modified by a single thread. This makes it easier to + * reason about how the snapshot is built up and used. Write operations are applied one by one, in + * order, with no concurrent access. Read operations are then allowed concurrently by many threads + * after writing is forbidden. *

+ * The fact that TripPattern instances carry a reference only to their scheduled Timetable and not + * to their realtime timetable is largely due to historical path-dependence in OTP development. + * Streaming realtime support was added around 2013 as a sort of sandbox feature that was switched + * off by default. Looking up realtime timetables during routing was a fringe feature that needed + * to impose near-zero cost and avoid introducing complexity into the primary codebase. Now over + * ten years later, the principles of how this system operates are rather stable, but the + * implementation would benefit from some deduplication and cleanup. Once that is complete, looking + * up timetables on this class could conceivably be replaced with snapshotting entire views of the + * transit network. It would also be possible to make the realtime version of Timetables or + * TripTimes the primary view, and include references back to their scheduled versions. */ public class TimetableSnapshot { private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshot.class); /** - * A set of all timetables which have been modified and are waiting to be indexed. When - * dirty is null, it indicates that the snapshot is read-only. + * During the construction phase of the TimetableSnapshot, before it is considered immutable and + * used in routing, this Set holds all timetables that have been modified and are waiting to be + * indexed. This field will be set to null when the TimetableSnapshot becomes read-only. */ private final Set dirtyTimetables = new HashSet<>(); /** - * The timetables for different days, for each TripPattern (each sequence of stops on a particular - * Route) for which we have an updated Timetable. The keys include both TripPatterns from the - * scheduled GTFS, and TripPatterns added by realtime messages and tracked by the - * TripPatternCache. Note that the keys will not include all scheduled TripPatterns, only those - * for which we've got an update. We use a HashMap rather than a Map so we can clone it. If this - * turns out to be slow/spacious we can use an array with integer pattern indexes. The SortedSet - * members are copy-on-write. - * FIXME: this could be made into a flat hashtable with compound keys. + * For each TripPattern (sequence of stops on a particular Route) for which we have received a + * realtime update, an ordered set of timetables on different days. The key TripPatterns may + * include ones from the scheduled GTFS, as well as ones added by realtime messages and + * tracked by the TripPatternCache.

+ * Note that the keys do not include all scheduled TripPatterns, only those for which we have at + * least one update. The type of the field is specifically HashMap (rather than the more general + * Map interface) because we need to efficiently clone it.

+ * The members of the SortedSet (the Timetable for a particular day) are treated as copy-on-write + * when we're updating them. If an update will modify the timetable for a particular day, that + * timetable is replicated before any modifications are applied to avoid affecting any previous + * TimetableSnapshots still in circulation which reference that same Timetable instance.

+ * Alternative implementations: A. This could be an array indexed using the integer pattern + * indexes. B. It could be made into a flat hashtable with compound keys (TripPattern, LocalDate). + * The compound key approach better reflects the fact that there should be only one Timetable per + * TripPattern and date. */ private HashMap> timetables = new HashMap(); /** - *

- * Map containing the current trip pattern given a trip id and a service date, if it has been - * changed from the scheduled pattern with an update, for which the stopPattern is different. - *

- *

- * This is a HashMap and not a Map so the clone function is available. + * For cases where the trip pattern (sequence of stops visited) has been changed by a realtime + * update, a Map associating the updated trip pattern with a compound key of the feed-scoped + * trip ID and the service date. The type of this field is HashMap rather than the more general + * Map interface because we need to efficiently clone it whenever we start building up a new + * snapshot. TODO RT_AB: clarify if this is an index or the original source of truth. */ private HashMap realtimeAddedTripPattern = new HashMap<>(); /** - * This maps contains all of the new or updated TripPatterns added by realtime data indexed on - * stop. This has to be kept in order for them to be included in the stop times api call on a - * specific stop. - *

- * This is a SetMultimap, so that each pattern can only be added once. - *

- * TODO Find a generic way to keep all realtime indexes. + * This is an index of TripPatterns, not the primary collection. It tracks which TripPatterns + * that were updated or newly created by realtime messages contain which stops. This allows them + * to be readily found and included in API responses containing stop times at a specific stop. + * This is a SetMultimap, so that each pattern is only retained once per stop even if it's added + * more than once. + * TODO RT_AB: More general handling of all realtime indexes outside primary data structures. */ private SetMultimap patternsForStop = HashMultimap.create(); @@ -164,12 +194,12 @@ public boolean hasRealtimeAddedTripPatterns() { } /** - * Update the trip times of one trip in a timetable of a trip pattern. If the trip of the trip - * times does not exist yet in the timetable, add it. + * Update the TripTimes of one Trip in a Timetable of a TripPattern. If the Trip of the TripTimes + * does not exist yet in the Timetable, add it. This method will make a protective copy + * of the Timetable if such a copy has not already been made while building up this snapshot, + * handling both cases where patterns were pre-existing in static data or created by realtime data. * - * @param pattern trip pattern - * @param updatedTripTimes updated trip times - * @param serviceDate service day for which this update is valid + * @param serviceDate service day for which this update is valid * @return whether the update was actually applied */ public Result update( @@ -367,6 +397,13 @@ public void setPatternsForStop(SetMultimap patternsFo this.patternsForStop = patternsForStop; } + /** + * Does this snapshot contain any realtime data or is it completely empty? + */ + public boolean isEmpty() { + return dirtyTimetables.isEmpty() && timetables.isEmpty() && realtimeAddedTripPattern.isEmpty(); + } + /** * Clear timetable for all patterns matching the provided feed id. * diff --git a/src/main/java/org/opentripplanner/model/TimetableSnapshotProvider.java b/src/main/java/org/opentripplanner/model/TimetableSnapshotProvider.java index 3f3e54dbc30..047f0301141 100644 --- a/src/main/java/org/opentripplanner/model/TimetableSnapshotProvider.java +++ b/src/main/java/org/opentripplanner/model/TimetableSnapshotProvider.java @@ -1,9 +1,13 @@ package org.opentripplanner.model; /** - * This interface is used to retrieve the current instance of the TimetableSnapshot. Any provider - * implementing this interface is responsible for thread-safe access to the latest valid instance of - * the {@code TimetableSnapshot}. + * This interface is used to retrieve the latest available instance of TimetableSnapshot + * that is ready for use in routing. Slightly newer TimetableSnapshots may be available, but still + * in the process of accumulating updates or being indexed and finalized for read-only routing use. + *

+ * Any provider implementing this interface is responsible for ensuring access to the latest + * {@code TimetableSnapshot} is handled in a thread-safe manner, as this method can be called by + * any number of concurrent routing requests at once. *

* Note that in the long run we don't necessarily want multiple snapshot providers. Ideally we'll * just have one way of handling these concurrency concerns, so no need for an interface and diff --git a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java index 544ca29599d..43c18cec59d 100644 --- a/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java +++ b/src/main/java/org/opentripplanner/model/impl/OtpTransitServiceBuilder.java @@ -94,7 +94,7 @@ public class OtpTransitServiceBuilder { private final TripStopTimes stopTimesByTrip = new TripStopTimes(); - private final Map flexDurationFactors = new HashMap<>(); + private final Map flexTimePenalties = new HashMap<>(); private final EntityById fareZonesById = new DefaultEntityById<>(); @@ -214,7 +214,7 @@ public TripStopTimes getStopTimesSortedByTrip() { } public Map getFlexTimePenalty() { - return flexDurationFactors; + return flexTimePenalties; } public EntityById getFareZonesById() { diff --git a/src/main/java/org/opentripplanner/routing/alertpatch/EntityKey.java b/src/main/java/org/opentripplanner/routing/alertpatch/EntityKey.java index 69d5a638e93..00f2bdafcd0 100644 --- a/src/main/java/org/opentripplanner/routing/alertpatch/EntityKey.java +++ b/src/main/java/org/opentripplanner/routing/alertpatch/EntityKey.java @@ -3,6 +3,12 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.timetable.Direction; +/** + * This encompasses many different kinds of entity keys, all of which are simple record types, all + * grouped together as the only allowed implementations of a sealed marker interface. These key + * types represent various combinations used to look up Alerts that might be associated with a + * particular stop, or a stop on a route, or all routes of a certain type etc. + */ public sealed interface EntityKey { record Agency(FeedScopedId agencyId) implements EntityKey {} diff --git a/src/main/java/org/opentripplanner/routing/alertpatch/EntitySelector.java b/src/main/java/org/opentripplanner/routing/alertpatch/EntitySelector.java index 9aa44ec8f2f..9fa94a6b795 100644 --- a/src/main/java/org/opentripplanner/routing/alertpatch/EntitySelector.java +++ b/src/main/java/org/opentripplanner/routing/alertpatch/EntitySelector.java @@ -5,6 +5,14 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.timetable.Direction; +/** + * Describes which elements in the internal transit data model are affected by a realtime alert. + * Note that this is specific to alerts and doesn't seem to be used by anything else. + * This is probably because alerts are unique in their ability to attach themselves to many + * different routes, stops, etc. at once, while non-alert elements tend to be associated with very + * specific single other elements. + * @see EntityKey + */ public sealed interface EntitySelector { EntityKey key(); diff --git a/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java b/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java index 17a307ed205..d7a3c93abe5 100644 --- a/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java +++ b/src/main/java/org/opentripplanner/routing/alertpatch/TransitAlert.java @@ -15,6 +15,15 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.framework.TransitBuilder; +/** + * Internal representation of a GTFS-RT Service Alert or SIRI Situation Exchange (SX) message. + * These are text descriptions of problems affecting specific stops, routes, or other components + * of the transit system which will be displayed to users as text. + * Although they have flags describing the effect of the problem described in the text, these + * messages do not currently modify routing behavior on their own. They must be accompanied by + * messages of other types to actually impact routing. However, there is ongoing discussion about + * allowing Alerts to affect routing, especially for cases such as stop closure messages. + */ public class TransitAlert extends AbstractTransitEntity { private final I18NString headerText; 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 2dde108b97e..c92bbd750ad 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 @@ -17,6 +17,17 @@ import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.service.StopModel; +/** + * This is a replica of public transportation data already present in TransitModel, but rearranged + * and indexed differently for efficient use by the Raptor router. Patterns and trips are split out + * by days, retaining only the services actually running on any particular day. + * + * TODO RT_AB: this name may reflect usage in R5, where the TransportNetwork encompasses two + * sub-aggregates (one for the streets and one for the public transit data). Here, the TransitLayer + * seems to just be an indexed and rearranged copy of the main TransitModel instance. TG has + * indicated that "layer" should be restricted in its standard OO meaning, and this class should + * really be merged into TransitModel. + */ public class TransitLayer { /** @@ -154,6 +165,10 @@ public RaptorTransferIndex getRaptorTransfersForRequest(RouteRequest request) { return transferCache.get(transfersByStopIndex, request); } + public void initTransferCacheForRequest(RouteRequest request) { + transferCache.put(transfersByStopIndex, request); + } + public RaptorRequestTransferCache getTransferCache() { return transferCache; } 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 fe81d28c098..210c8b42066 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 @@ -33,15 +33,15 @@ import org.slf4j.LoggerFactory; /** - * Maps the TransitLayer object from the OTP Graph object. The ServiceDay hierarchy is reversed, + * Maps the TransitLayer object from the TransitModel object. The ServiceDay hierarchy is reversed, * with service days at the top level, which contains TripPatternForDate objects that contain only * TripSchedules running on that particular date. This makes it faster to filter out TripSchedules * when doing Range Raptor searches. *

- * CONCURRENCY: This mapper run part of the mapping in parallel using parallel streams. This improve - * startup time on the Norwegian graph by 20 seconds; reducing the this mapper from 36 seconds to 15 - * seconds, and the total startup time from 80 seconds to 60 seconds. (JAN 2020, MacBook Pro, 3.1 - * GHz i7) + * CONCURRENCY: This mapper runs part of the mapping in parallel using parallel streams. This + * improves startup time on the Norwegian network by 20 seconds, by reducing this mapper from 36 + * seconds to 15 seconds, and the total startup time from 80 seconds to 60 seconds. (JAN 2020, + * MacBook Pro, 3.1 GHz i7) */ public class TransitLayerMapper { @@ -60,8 +60,8 @@ public static TransitLayer map( return new TransitLayerMapper(transitModel).map(tuningParameters); } - // TODO We can save time by either pre-sorting these or use a sorting algorithm that is - // optimized for sorting nearly sorted list + // TODO We could save time by either pre-sorting these, or by using a sorting algorithm that is + // optimized for sorting nearly-sorted lists. static List getSortedTripTimes(Timetable timetable) { return timetable .getTripTimes() @@ -76,7 +76,7 @@ private TransitLayer map(TransitTuningParameters tuningParameters) { ConstrainedTransfersForPatterns constrainedTransfers = null; StopModel stopModel = transitModel.getStopModel(); - LOG.info("Mapping transitLayer from Graph..."); + LOG.info("Mapping transitLayer from TransitModel..."); Collection allTripPatterns = transitModel.getAllTripPatterns(); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java index fad5de83de0..5188bdef8b1 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java @@ -30,6 +30,11 @@ * id and replaced by their updated versions. The realtime TransitLayer is then switched out with * the updated copy in an atomic operation. This ensures that any TransitLayer that is referenced * from the Graph is never changed. + * + * This is a way of keeping the TransitLayer up to date (in sync with the TransitModel plus its most + * recent TimetableSnapshot) without repeatedly deriving it from scratch every few seconds. The same + * incremental changes are applied to both the TimetableSnapshot and the TransitLayer and they are + * published together. */ public class TransitLayerUpdater { diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRequestTransferCache.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRequestTransferCache.java index 32935d77fe0..99ad83d3f4d 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRequestTransferCache.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRequestTransferCache.java @@ -35,14 +35,20 @@ public LoadingCache getTransferCache() { return transferCache; } + public void put(List> transfersByStopIndex, RouteRequest request) { + final CacheKey cacheKey = new CacheKey(transfersByStopIndex, request); + final RaptorTransferIndex raptorTransferIndex = RaptorTransferIndex.create( + transfersByStopIndex, + cacheKey.request + ); + + LOG.info("Initializing cache with request: {}", cacheKey.options); + transferCache.put(cacheKey, raptorTransferIndex); + } + public RaptorTransferIndex get(List> transfersByStopIndex, RouteRequest request) { try { - return transferCache.get( - new CacheKey( - transfersByStopIndex, - StreetSearchRequestMapper.mapToTransferRequest(request).build() - ) - ); + return transferCache.get(new CacheKey(transfersByStopIndex, request)); } catch (ExecutionException e) { throw new RuntimeException("Failed to get item from transfer cache", e); } @@ -53,7 +59,7 @@ private CacheLoader cacheLoader() { @Override @Nonnull public RaptorTransferIndex load(@Nonnull CacheKey cacheKey) { - LOG.info("Adding request to cache: {}", cacheKey.options); + LOG.info("Adding runtime request to cache: {}", cacheKey.options); return RaptorTransferIndex.create(cacheKey.transfersByStopIndex, cacheKey.request); } }; @@ -65,10 +71,10 @@ private static class CacheKey { private final StreetSearchRequest request; private final StreetRelevantOptions options; - private CacheKey(List> transfersByStopIndex, StreetSearchRequest request) { + private CacheKey(List> transfersByStopIndex, RouteRequest request) { this.transfersByStopIndex = transfersByStopIndex; - this.request = request; - this.options = new StreetRelevantOptions(request); + this.request = StreetSearchRequestMapper.mapToTransferRequest(request).build(); + this.options = new StreetRelevantOptions(this.request); } @Override diff --git a/src/main/java/org/opentripplanner/routing/api/request/framework/TimePenalty.java b/src/main/java/org/opentripplanner/routing/api/request/framework/TimePenalty.java index 0c6fdd96436..3ee07034135 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/framework/TimePenalty.java +++ b/src/main/java/org/opentripplanner/routing/api/request/framework/TimePenalty.java @@ -7,6 +7,9 @@ public final class TimePenalty extends AbstractLinearFunction { public static final TimePenalty ZERO = new TimePenalty(Duration.ZERO, 0.0); + /** + * An instance that doesn't actually apply a penalty and returns the duration unchanged. + */ public static final TimePenalty NONE = new TimePenalty(Duration.ZERO, 1.0); private TimePenalty(Duration constant, double coefficient) { diff --git a/src/main/java/org/opentripplanner/routing/graph/Graph.java b/src/main/java/org/opentripplanner/routing/graph/Graph.java index 4b2ca75b1a7..4b6c9938317 100644 --- a/src/main/java/org/opentripplanner/routing/graph/Graph.java +++ b/src/main/java/org/opentripplanner/routing/graph/Graph.java @@ -37,18 +37,41 @@ import org.slf4j.LoggerFactory; /** - * A graph is really just one or more indexes into a set of vertexes. It used to keep edgelists for - * each vertex, but those are in the vertex now. + * This is one of the main data structures in OpenTripPlanner. It represents a mathematical object + * called a graph (https://en.wikipedia.org/wiki/Graph_theory) relative to which many routing + * algorithms are defined. A graph is made up of vertices and edges. These are also referred to as + * nodes and arcs or links, but in OTP we always use the vertices and edges terminology. + *

+ * In OTP1, the Graph contained vertices and edges representing the entire transportation network, + * including edges representing both street segments and public transit lines connecting stops. In + * OTP2, the Graph edges now represent only the street network. Transit routing is performed on + * other data structures suited to the Raptor algorithm (the TransitModel). Some transit-related + * vertices are still present in the Graph, specifically those representing transit stops, + * entrances, and elevators. Their presence in the street graph creates a connection between the two + * routable data structures (identifying where stops in the TransitModel are located relative to + * roads). + *

+ * Other data structures related to street routing, such as elevation data and vehicle parking + * information, are also collected here as fields of the Graph. For historical reasons the Graph + * sometimes serves as a catch-all, as it used to be the root of the object tree representing the + * whole transportation network. This use of the Graph object is being phased out and discouraged. + *

+ * In some sense the Graph is just some indexes into a set of vertices. The Graph used to hold lists + * of edges for each vertex, but those lists are now attached to the vertices themselves. + *

+ * TODO RT_AB: I favor renaming to StreetGraph to emphasize what it represents. TG agreed in review. */ public class Graph implements Serializable { private static final Logger LOG = LoggerFactory.getLogger(Graph.class); + /** Attaches text notes to street edges, which do not affect routing. */ public final StreetNotesService streetNotesService = new StreetNotesService(); - /* Ideally we could just get rid of vertex labels, but they're used in tests and graph building. */ + // Ideally we could just get rid of vertex labels, but they're used in tests and graph building. private final Map vertices = new ConcurrentHashMap<>(); + /** Conserve memory by reusing immutable instances of Strings, integer arrays, etc. */ public final transient Deduplicator deduplicator; public final Instant buildTime = Instant.now(); @@ -58,10 +81,10 @@ public class Graph implements Serializable { private transient StreetIndex streetIndex; - //ConvexHull of all the graph vertices. Generated at Graph build time. + /** The convex hull of all the graph vertices. Generated at the time the Graph is built. */ private Geometry convexHull = null; - /* The preferences that were used for graph building. */ + /** The preferences that were used for building this Graph instance. */ public Preferences preferences = null; /** True if OSM data was loaded into this Graph. */ @@ -71,26 +94,27 @@ public class Graph implements Serializable { * 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 */ public Double ellipsoidToGeoidDifference = 0.0; - /** - * Does this graph contain elevation data? - */ + + /** True if this graph contains elevation data. */ public boolean hasElevation = false; - /** - * If this graph contains elevation data, the minimum value. - */ + + /** If this graph contains elevation data, the minimum elevation value. Otherwise null. */ public Double minElevation = null; - /** - * If this graph contains elevation data, the maximum value. - */ + + /** If this graph contains elevation data, the maximum elevation value. Otherwise null. */ public Double maxElevation = null; - /** The distance between elevation samples used in CompactElevationProfile. */ + /** + * The horizontal distance across the ground between successive elevation samples in + * CompactElevationProfile. + */ // TODO refactoring transit model: remove and instead always serialize directly from and to the // static variable in CompactElevationProfile in SerializedGraphObject private double distanceBetweenElevationSamples; @@ -132,9 +156,7 @@ public Graph() { this(new Deduplicator(), null); } - /** - * Add the given vertex to the graph. - */ + /** Add the given vertex to the graph. */ public void addVertex(Vertex v) { Vertex old = vertices.put(v.getLabel(), v); if (old != null) { diff --git a/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java b/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java index d8c5dd19f3b..8da6fc21aa7 100644 --- a/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java +++ b/src/main/java/org/opentripplanner/routing/impl/DelegatingTransitAlertServiceImpl.java @@ -18,11 +18,23 @@ * This class is used to combine alerts from multiple {@link TransitAlertService}s. Each * {@link TransitAlertProvider} has its own service, and all need to be queried in order to fetch * all alerts. + * + * Concretely: every realtime updater receiving GTFS Alerts or SIRI Situation Exchange (SX) + * messages currently maintains its own private index of alerts separately from all other updaters. + * To make the set of all alerts from all updaters available in a single operation and associate it + * with the graph as a whole, the various indexes are merged in such a way as to have the same + * index as each individual index. */ public class DelegatingTransitAlertServiceImpl implements TransitAlertService { private final ArrayList transitAlertServices = new ArrayList<>(); + /** + * Constructor which scans over all existing GraphUpdaters associated with a TransitModel + * instance and retains references to all their TransitAlertService instances. + * This implies that these instances are expected to remain in use indefinitely (not be replaced + * with new instances or taken out of service over time). + */ public DelegatingTransitAlertServiceImpl(TransitModel transitModel) { if (transitModel.getUpdaterManager() != null) { transitModel @@ -38,7 +50,9 @@ public DelegatingTransitAlertServiceImpl(TransitModel transitModel) { @Override public void setAlerts(Collection alerts) { - throw new UnsupportedOperationException("Not supported"); + throw new UnsupportedOperationException( + "This delegating TransitAlertService is not intended to hold any TransitAlerts of its own." + ); } @Override diff --git a/src/main/java/org/opentripplanner/routing/impl/TransitAlertServiceImpl.java b/src/main/java/org/opentripplanner/routing/impl/TransitAlertServiceImpl.java index 285b4665c46..0cc24c691e9 100644 --- a/src/main/java/org/opentripplanner/routing/impl/TransitAlertServiceImpl.java +++ b/src/main/java/org/opentripplanner/routing/impl/TransitAlertServiceImpl.java @@ -16,6 +16,16 @@ import org.opentripplanner.transit.service.TransitModel; /** + * This is the primary implementation of TransitAlertService, which actually retains its own set + * of TransitAlerts and indexes them for fast lookup by which transit entity is affected. + * The only other implementation exists just to combine several instances of this primary + * implementation into one. + * TODO RT_AB: investigate why each updater has its own service instead of taking turns + * sequentially writing to a single service. Original design was for all data and indexes to be + * associated with the Graph or transit model (i.e. the object graph of instances of the transit + * model) and for updaters to submit write tasks that would patch the current version in a + * sequential way, e.g. "add these 10 alerts", "remove these 5 alerts", etc. + * * When an alert is added with more than one transit entity, e.g. a Stop and a Trip, both conditions * must be met for the alert to be displayed. This is the case in both the Norwegian interpretation * of SIRI, and the GTFS-RT alerts specification. @@ -32,13 +42,17 @@ public TransitAlertServiceImpl(TransitModel transitModel) { @Override public void setAlerts(Collection alerts) { + // FIXME RT_AB: this is patched live by updaters while in use (being read) by other threads + // performing trip planning. The single-action assignment helps a bit, but the map can be + // swapped out while the delegating service is in the middle of multiple calls that read from + // it. The consistent approach would be to duplicate the entire service, update it + // copy-on-write, and swap in the entire service after the update. Multimap newAlerts = HashMultimap.create(); for (TransitAlert alert : alerts) { for (EntitySelector entity : alert.entities()) { newAlerts.put(entity.key(), alert); } } - this.alerts = newAlerts; } diff --git a/src/main/java/org/opentripplanner/routing/services/TransitAlertService.java b/src/main/java/org/opentripplanner/routing/services/TransitAlertService.java index e80d56e039e..7b037192c77 100644 --- a/src/main/java/org/opentripplanner/routing/services/TransitAlertService.java +++ b/src/main/java/org/opentripplanner/routing/services/TransitAlertService.java @@ -8,6 +8,24 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.timetable.Direction; +/** + * A TransitAlertService stores a set of alerts (passenger-facing textual information associated + * with transit entities such as stops or routes) which are currently active and should be provided + * to end users when their itineraries include the relevant stop, route, etc. + * + * Its primary purpose is to index those alerts, which may be numerous, so they can be looked up + * rapidly and attached to the various pieces of an itinerary as it's being returned to the user. + * + * Most elements in an itinerary will have no alerts attached, so those cases need to return + * quickly. For example, no alerts on board stop A, no alerts on route 1 ridden, no alerts on alight + * stop B, no alerts on route 2 ridden, yes one alert found on alight stop C. + * + * The fact that alerts are relatively sparse (at the scale of the entire transportation system) + * is central to this implementation. Adding a list of alerts to every element in the system would + * mean storing large amounts of null or empty list references. Instead, alerts are looked up in + * maps allowing them to be attached to any object with minimal space overhead, but requiring some + * careful indexing to ensure their presence or absence on each object can be determined quickly. + */ public interface TransitAlertService { void setAlerts(Collection alerts); diff --git a/src/main/java/org/opentripplanner/routing/services/notes/StreetNotesService.java b/src/main/java/org/opentripplanner/routing/services/notes/StreetNotesService.java index 9dd24182461..966ac72c7d8 100644 --- a/src/main/java/org/opentripplanner/routing/services/notes/StreetNotesService.java +++ b/src/main/java/org/opentripplanner/routing/services/notes/StreetNotesService.java @@ -15,11 +15,11 @@ import org.slf4j.LoggerFactory; /** - * This service manage street edge notes. An edge note is an free-format alert (text) attached to an - * edge, which is returned in the itinerary when this edge is used, and which *does not have any - * impact on routing*. The last restriction is necessary as the edge do not know which notes it is - * attached to (this to prevent having to store note lists in the edge, which is memory consuming as - * only few edges will have notes). + * This service manages street edge notes. An edge note is a free-format alert (text) attached to an + * edge, which is returned along with any itinerary where this edge is used, and which does not + * have any impact on routing. Notes cannot affect routing because edges do not know which notes + * are attached to them. This avoids storing references to notes on the edge, which would probably + * not be worth the memory consumption as only a few edges have notes. *

* The service owns a list of StreetNotesSource, with a single static one used for graph building. * "Dynamic" notes can be returned by classes implementing StreetNoteSource, added to this service @@ -32,7 +32,7 @@ * traversed, ie "state back edge"). Usually matcher will match note based on the mode (cycling, * driving) or if a wheelchair access is requested. * - * @author laurent + * @author Laurent Grégoire */ public class StreetNotesService implements Serializable { diff --git a/src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleType.java b/src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleType.java index d9972dd3149..0d26682395d 100644 --- a/src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleType.java +++ b/src/main/java/org/opentripplanner/service/vehiclerental/model/RentalVehicleType.java @@ -3,7 +3,7 @@ import java.io.Serializable; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.entur.gbfs.v2_3.vehicle_types.GBFSVehicleType; +import org.mobilitydata.gbfs.v2_3.vehicle_types.GBFSVehicleType; import org.opentripplanner.street.model.RentalFormFactor; import org.opentripplanner.transit.model.framework.FeedScopedId; diff --git a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index 1adebc6a90e..b632ea6104b 100644 --- a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -30,11 +30,15 @@ import org.opentripplanner.transit.service.TransitService; /** - * The purpose of this class is to allow APIs (HTTP Resources) to access the OTP Server Context. + * The purpose of this class is to give APIs (HTTP Resources) read-only access to the OTP internal + * transit model. It allows individual API requests to use a limited number of methods and data + * structures without direct access to the internals of the server components. + * * By using an interface, and not injecting each service class we avoid giving the resources access - * to the server implementation. The context is injected by Jersey. An alternative to injecting this - * interface is to inject each individual component in the context - hence reducing the dependencies - * further. But there is not a "real" need for this. For example, we do not have unit tests on the + * to the server implementation. The context is injected by Jersey. Instead of injecting this + * context interface, it is conceivable to inject each of the individual items within this context. + * + * But there is not a "real" need for this. For example, we do not have unit tests on the * Resources. If we in the future would decide to write unit tests for the APIs, then we could * eliminate this interface and just inject the components. See the bind method in OTPServer. *

diff --git a/src/main/java/org/opentripplanner/standalone/config/framework/json/EnumMapper.java b/src/main/java/org/opentripplanner/standalone/config/framework/json/EnumMapper.java index 0e048a2e3e7..c57220c41d5 100644 --- a/src/main/java/org/opentripplanner/standalone/config/framework/json/EnumMapper.java +++ b/src/main/java/org/opentripplanner/standalone/config/framework/json/EnumMapper.java @@ -5,6 +5,14 @@ import org.opentripplanner.framework.doc.DocumentedEnum; import org.opentripplanner.framework.lang.StringUtils; +/** + * This converts strings appearing in configuration files into enum values. + * The values appearing in config files are case-insensitive and can use either dashes + * or underscores indiscriminately. + * Dashes are replaced with underscores, and the string is converted to upper case. + * In practice, this serves to convert from kebab-case to SCREAMING_SNAKE_CASE (which is + * conventional for Java enum values), leaving the latter unchanged if it's used in the config file. + */ public class EnumMapper { @SuppressWarnings("unchecked") @@ -12,6 +20,9 @@ public static > Optional mapToEnum(String text, Class ty return (Optional) mapToEnum2(text, type); } + /** + * Maps an individual value from a config file into its corresponding enum value. + */ public static Optional> mapToEnum2(String text, Class> type) { if (text == null) { return Optional.empty(); diff --git a/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java b/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java index 3ecc4d484cb..fd8bd00010d 100644 --- a/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java +++ b/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java @@ -42,6 +42,11 @@ import org.opentripplanner.routing.api.request.framework.TimePenalty; import org.opentripplanner.transit.model.framework.FeedScopedId; +/** + * TODO RT_AB: add Javadoc to clarify whether this is building a declarative representation of the + * parameter, or building a concrete key-value pair for a parameter in a config file being read + * at server startup, or both. + */ public class ParameterBuilder { private static final Object UNDEFINED = new Object(); diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java index 72a46254b58..b1bc6888753 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java @@ -233,7 +233,7 @@ public static void initializeTransferCache( LOG.info(progress.startMessage()); transferCacheRequests.forEach(request -> { - transitModel.getTransitLayer().getRaptorTransfersForRequest(request); + transitModel.getTransitLayer().initTransferCacheForRequest(request); //noinspection Convert2MethodRef progress.step(s -> LOG.info(s)); diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java index 4e9f50607ac..fe95fe0447d 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java @@ -37,7 +37,8 @@ import org.opentripplanner.visualizer.GraphVisualizer; /** - * Dagger dependency injection Factory to create components for the OTP construct application phase. + * A Factory used by the Dagger dependency injection system to create the components of OTP, which + * are then wired up to construct the application. */ @Singleton @Component( diff --git a/src/main/java/org/opentripplanner/standalone/server/OTPWebApplication.java b/src/main/java/org/opentripplanner/standalone/server/OTPWebApplication.java index b238a74c7d3..079dad36f53 100644 --- a/src/main/java/org/opentripplanner/standalone/server/OTPWebApplication.java +++ b/src/main/java/org/opentripplanner/standalone/server/OTPWebApplication.java @@ -3,8 +3,8 @@ import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.binder.jersey.server.DefaultJerseyTagsProvider; import io.micrometer.core.instrument.binder.jersey.server.MetricsApplicationEventListener; -import io.micrometer.prometheus.PrometheusConfig; -import io.micrometer.prometheus.PrometheusMeterRegistry; +import io.micrometer.prometheusmetrics.PrometheusConfig; +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry; import jakarta.ws.rs.container.ContainerResponseFilter; import jakarta.ws.rs.core.Application; import java.util.HashSet; diff --git a/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java b/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java index b5f11d387a9..3fc51cea0c6 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/StopPattern.java @@ -90,11 +90,11 @@ public static StopPatternBuilder create(int length) { * This has package local access since a StopPattern is a part of a TripPattern. To change it * use the {@link TripPattern#copyPlannedStopPattern()} method. */ - StopPatternBuilder mutate() { + StopPatternBuilder copyOf() { return new StopPatternBuilder(this, null); } - StopPatternBuilder mutate(StopPattern realTime) { + StopPatternBuilder copyOf(StopPattern realTime) { return new StopPatternBuilder(this, realTime); } diff --git a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java index 7e40aa2d13b..f9734c1cb1a 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/TripPattern.java @@ -42,6 +42,15 @@ * stop). Trips are assumed to be non-overtaking, so that an earlier trip never arrives after a * later trip. *

+ * The key of the TripPattern includes the Route, StopPattern, TransitMode, and SubMode. All trips + * grouped under a TripPattern should have the same values for these characteristics (with possible + * exceptions for TransitMode and SubMode). + * TODO RT_AB: We need to clarify exactly which characteristics are identical across the trips. + * Grouping into patterns serves more than one purpose: it conserves memory by not replicating + * details shared across all trips in the TripPattern; it reflects business practices outside + * routing; it is essential to optimizations in routing algorithms like Raptor. We may be + * conflating a domain model grouping with an internal routing grouping. + *

* This is called a JOURNEY_PATTERN in the Transmodel vocabulary. However, GTFS calls a Transmodel * JOURNEY a "trip", thus TripPattern. *

@@ -56,29 +65,58 @@ public final class TripPattern private static final Logger LOG = LoggerFactory.getLogger(TripPattern.class); private final Route route; + /** - * The stop-pattern help us reuse the same stops in several trip-patterns; Hence saving memory. - * The field should not be accessible outside the class, and all access is done through method - * delegation, like the {@link #numberOfStops()} and {@link #canBoard(int)} methods. + * This field should not be accessed outside this class. All access to the StopPattern is + * performed through method delegation, like the {@link #numberOfStops()} and + * {@link #canBoard(int)} methods. */ private final StopPattern stopPattern; + + /** + * TripPatterns hold a reference to a Timetable (i.e. TripTimes for all Trips in the pattern) for + * only scheduled trips from the GTFS or NeTEx data. If any trips were later updated in real time, + * there will be another Timetable holding those updates and reading through to the scheduled one. + * That other realtime Timetable is retrieved from a TimetableSnapshot (see end of Javadoc on + * TimetableSnapshot for more details). + * TODO RT_AB: The above system should be changed to integrate realtime and scheduled data more + * closely. The Timetable may become obsolete or change significantly when they are integrated. + */ private final Timetable scheduledTimetable; + + // This TransitMode is arguably a redundant replication/memoization of information on the Route. + // It appears that in the TripPatternBuilder it is only ever set from a Trip which is itself set + // from a Route. This does not just read through to Route because in Netex trips may override + // the mode of their route. But we need to establish with more clarity whether our internal model + // TripPatterns allow trips of mixed modes, or rather if a single mode is part of their unique key. private final TransitMode mode; + private final SubMode netexSubMode; private final boolean containsMultipleModes; private String name; + /** * Geometries of each inter-stop segment of the tripPattern. + * Not used in routing, only for API listing. + * TODO: Encapsulate the byte arrays in a class. */ private final byte[][] hopGeometries; /** * The original TripPattern this replaces at least for one modified trip. + * + * Currently this seems to only be set (via TripPatternBuilder) from TripPatternCache and + * SiriTripPatternCache. + * + * FIXME RT_AB: Revise comments to make it clear how this is used (it is only used rarely). */ private final TripPattern originalTripPattern; /** - * Has the TripPattern been created by a real-time update. + * When a trip is added or rerouted by a realtime update, this may give rise to a new TripPattern + * that did not exist in the scheduled data. For such TripPatterns this field will be true. If on + * the other hand this TripPattern instance was created from the schedule data, this field will be + * false. */ private final boolean createdByRealtimeUpdater; @@ -169,8 +207,8 @@ public StopPattern getStopPattern() { */ public StopPattern.StopPatternBuilder copyPlannedStopPattern() { return isModified() - ? originalTripPattern.stopPattern.mutate(stopPattern) - : stopPattern.mutate(); + ? originalTripPattern.stopPattern.copyOf(stopPattern) + : stopPattern.copyOf(); } public LineString getGeometry() { diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/TripTimesStringBuilder.java b/src/main/java/org/opentripplanner/transit/model/timetable/TripTimesStringBuilder.java new file mode 100644 index 00000000000..a5e77037dea --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/timetable/TripTimesStringBuilder.java @@ -0,0 +1,64 @@ +package org.opentripplanner.transit.model.timetable; + +import java.util.ArrayList; +import org.opentripplanner.framework.time.TimeUtils; +import org.opentripplanner.transit.model.network.TripPattern; + +public class TripTimesStringBuilder { + + /** + * This encodes the trip times and information about stops in a readable way in order to simplify + * testing/debugging. The format of the outputput string is: + * + *

+   * REALTIME_STATE | stop1 [FLAGS] arrivalTime departureTime | stop2 ...
+   *
+   * Where flags are:
+   * C: Canceled
+   * R: Recorded
+   * PI: Prediction Inaccurate
+   * ND: No Data
+   * 
+ * + * @throws IllegalStateException if TripTimes does not match the TripPattern + */ + public static String encodeTripTimes(TripTimes tripTimes, TripPattern pattern) { + var stops = pattern.getStops(); + + if (tripTimes.getNumStops() != stops.size()) { + throw new IllegalArgumentException( + "TripTimes and TripPattern have different number of stops" + ); + } + + StringBuilder s = new StringBuilder(tripTimes.getRealTimeState().toString()); + for (int i = 0; i < tripTimes.getNumStops(); i++) { + var depart = tripTimes.getDepartureTime(i); + var arrive = tripTimes.getArrivalTime(i); + var flags = new ArrayList(); + if (tripTimes.isCancelledStop(i)) { + flags.add("C"); + } + if (tripTimes.isRecordedStop(i)) { + flags.add("R"); + } + if (tripTimes.isPredictionInaccurate(i)) { + flags.add("PI"); + } + if (tripTimes.isNoDataStop(i)) { + flags.add("ND"); + } + + s.append(" | ").append(stops.get(i).getName()); + if (!flags.isEmpty()) { + s.append(" [").append(String.join(",", flags)).append("]"); + } + s + .append(" ") + .append(TimeUtils.timeToStrCompact(arrive)) + .append(" ") + .append(TimeUtils.timeToStrCompact(depart)); + } + return s.toString(); + } +} diff --git a/src/main/java/org/opentripplanner/transit/service/TransitModel.java b/src/main/java/org/opentripplanner/transit/service/TransitModel.java index 46e22a937be..d5211d5dd2e 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitModel.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitModel.java @@ -55,7 +55,28 @@ import org.slf4j.LoggerFactory; /** - * Repository for Transit entities. + * The TransitModel groups together all instances making up OTP's primary internal representation + * of the public transportation network. Although the names of many entities are derived from + * GTFS concepts, these are actually independent of the data source from which they are loaded. + * Both GTFS and NeTEx entities are mapped to these same internal OTP entities. If a concept exists + * in both GTFS and NeTEx, the GTFS name is used in the internal model. For concepts that exist + * only in NeTEx, the NeTEx name is used in the internal model. + * + * A TransitModel instance also includes references to some transient indexes of its contents, to + * the TransitLayer derived from it, and to some other services and utilities that operate upon + * its contents. + * + * The TransitModel stands in opposition to two other aggregates: the Graph (representing the + * street network) and the TransitLayer (representing many of the same things in the TransitModel + * but rearranged to be more efficient for Raptor routing). + * + * At this point the TransitModel is not often read directly. Many requests will look at the + * TransitLayer rather than the TransitModel it's derived from. Both are often accessed via the + * TransitService rather than directly reading the fields of TransitModel or TransitLayer. + * + * TODO RT_AB: consider renaming. By some definitions this is not really the model, but a top-level + * object grouping together instances of model classes with things that operate on and map those + * instances. */ public class TransitModel implements Serializable { @@ -79,6 +100,25 @@ public class TransitModel implements Serializable { private ZonedDateTime transitServiceStarts = LocalDate.MAX.atStartOfDay(ZoneId.systemDefault()); private ZonedDateTime transitServiceEnds = LocalDate.MIN.atStartOfDay(ZoneId.systemDefault()); + /** + * The TransitLayer representation (optimized and rearranged for Raptor) of this TransitModel's + * scheduled (non-realtime) contents. + */ + private transient TransitLayer transitLayer; + + /** + * This updater applies realtime changes queued up for the next TimetableSnapshot such that + * this TransitModel.realtimeSnapshot remains aligned with the service represented in + * (this TransitModel instance + that next TimetableSnapshot). This is a way of keeping the + * TransitLayer up to date without repeatedly deriving it from scratch every few seconds. The + * same incremental changes are applied to both sets of data and they are published together. + */ + private transient TransitLayerUpdater transitLayerUpdater; + + /** + * An optionally present second TransitLayer representing the contents of this TransitModel plus + * the results of realtime updates in the latest TimetableSnapshot. + */ private final transient ConcurrentPublished realtimeTransitLayer = new ConcurrentPublished<>(); private final transient Deduplicator deduplicator; @@ -102,9 +142,6 @@ public class TransitModel implements Serializable { private final Map> flexTripsById = new HashMap<>(); - private transient TransitLayer transitLayer; - private transient TransitLayerUpdater transitLayerUpdater; - private transient TransitAlertService transitAlertService; @Inject @@ -113,15 +150,15 @@ public TransitModel(StopModel stopModel, Deduplicator deduplicator) { this.deduplicator = deduplicator; } - /** Constructor for deserialization. */ + /** No-argument constructor, required for deserialization. */ public TransitModel() { this(new StopModel(), new Deduplicator()); } /** - * Perform indexing on timetables, and create transient data structures. This used to be done in - * readObject methods upon deserialization, but stand-alone mode now allows passing graphs from - * graphbuilder to server in memory, without a round trip through serialization. + * Perform indexing on timetables, and create transient data structures. This used to be done + * inline in readObject methods upon deserialization, but it is now possible to pass transit data + * from the graph builder to the server in memory, without a round trip through serialization. */ public void index() { if (index == null) { diff --git a/src/main/java/org/opentripplanner/transit/service/TransitService.java b/src/main/java/org/opentripplanner/transit/service/TransitService.java index 17354ec4a20..83b65c44d12 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitService.java @@ -44,7 +44,20 @@ import org.opentripplanner.updater.GraphUpdaterStatus; /** - * Entry point for read-only requests towards the transit API. + * TransitService is a read-only interface for retrieving public transport data. It provides a + * frozen view of all these elements at a point in time, which is not affected by incoming realtime + * data, allowing results to remain stable over the course of a request. This can be used for + * fetching tables of specific information like the routes passing through a particular stop, or for + * gaining access to the entirety of the data to perform routing. + *

+ * TODO RT_AB: this interface seems to provide direct access to TransitLayer but not TransitModel. + * Is this intentional, because TransitLayer is meant to be read-only and TransitModel is not? + * Should this be renamed TransitDataService since it seems to provide access to the data but + * not to transit routing functionality (which is provided by the RoutingService)? + * The DefaultTransitService implementation has a TransitModel instance and many of its methods + * read through to that TransitModel instance. But that field itself is not exposed, while the + * TransitLayer is here. It seems like exposing the raw TransitLayer is still a risk since it's + * copy-on-write and shares a lot of objects with any other TransitLayer instances. */ public interface TransitService { Collection getFeedIds(); diff --git a/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java b/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java index 09b7966908e..0dad35bfcd8 100644 --- a/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java +++ b/src/main/java/org/opentripplanner/updater/GraphUpdaterManager.java @@ -90,7 +90,7 @@ public GraphUpdaterManager(Graph graph, TransitModel transitModel, List + + + + + + + Polling Updater 2Threads, Queues, and Buffers Over TimeGraph UpdaterHTTP Handler 1HTTP Handler 2Streaming UpdaterPolling Updater 1rxfetchparsefetchparserxrxfetchparseExecutor Queue12applyapply13Buffer Transit DataLive Transit Data2applyapplyapply211applyABBCCDrouting on Arouting on CDEGarbage Collector \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/updater/package.md b/src/main/java/org/opentripplanner/updater/package.md new file mode 100644 index 00000000000..50c425bb6eb --- /dev/null +++ b/src/main/java/org/opentripplanner/updater/package.md @@ -0,0 +1,114 @@ +# Realtime Updaters + +## Realtime Data Sources + +Published transit data is broadly divided into two categories, which represent different time scales. On one hand we have scheduled data (also called planned or static data), and on the other hand realtime data. Scheduled data is supplied in GTFS or NeTEx format, with the corresponding realtime data supplied in the [GTFS-RT](https://gtfs.org/realtime/reference/) and [SIRI](https://www.siri-cen.eu/) formats, respectively. This package contains code that retrieves and decodes realtime data, then layers it on top of the static transit data in a live OTP instance while it continues to handle routing requests. + +Different data producers might update their scheduled data every month, week, or day; some even update it multiple times per day. Realtime data then covers any changes to service at a timescale shorter than that of a given producer's scheduled data. Broadly speaking, realtime data represents short-term unexpected or unplanned changes that modify the planned schedules, and could require changes to journeys that the riders would not expect from the schedule. OpenTripPlanner uses three main categories of realtime data which are summarized in the table below. The SIRI specification includes more types, but OTP handles only these three that correspond to the three GTFS-RT types. + +| GTFS-RT Name | SIRI Name | Description | +|----------------------------------------------------------------------------------|--------------------------|-----------------------------------------------------------------------| +| [Service Alert](https://gtfs.org/realtime/reference/#message-alert) | Situation Exchange (SX) | Text descriptions relevant to riders using a particular route or stop | +| [Trip Update](https://gtfs.org/realtime/reference/#message-tripupdate) | Estimated Timetable (ET) | Observed or expected arrival and departure times for near-term trips | +| [Vehicle Position](https://gtfs.org/realtime/reference/#message-vehicleposition) | Vehicle Monitoring (VM) | Physical location of vehicles currently providing service | + +GTFS-RT takes the form of binary protocol buffer messages that are typically fetched over HTTP. On the other hand, the SIRI specification originally described SOAP remote procedure calls retrieving an XML representation of the messages. Various projects instead adopted simple HTTP GET requests passing parameters in the URL and optionally returning a JSON representation instead of XML. This latter approach was officially recognized as "SIRI Lite", has become quite common, and is the approach supported by OTP. + +Because OTP handles both GTFS-RT and SIRI data sources, there will often be two equivalent classes for retrieving and interpreting a particular kind of realtime data. For example, there is a SiriAlertsUpdateHandler and an AlertsUpdateHandler. The SIRI variants are typically prefixed with `Siri` while the GTFS-RT ones have no prefix for historical reasons (the GTFS-RT versions were originally the only ones). These should perhaps be renamed with a `GtfsRt` prefix for symmetry. Once the incoming messages have been decoded, they will ideally be mapped into a single internal class that was originally derived from GTFS-RT but has been extended to cover all information afforded by both GTFS and SIRI. For example, both classes mentioned above produce TransitAlert instances. These uniform internal representations can then be applied to the internal transit model using a single mechanism, independent of the message source type. + +In practice, OTP does not yet use a single uniform internal representation for each of the three main message types. Particularly for TripUpdates/SIRI-ET, a lot of custom behavior was introduced for the SIRI case which led to a split between the two implementations. Our aim is to eventually merge the two systems back into one. NOTE: the comments on source code may be deceptive in cases where classes were copied and altered to handle SIRI data. This was sometimes done under time pressure to resolve production bugs. In these situations some comments were inadvertently duplicated without being updated. For example, despite many comments and field names mentioning "trip updates", SIRI estimated timetables are not converted into an internal TripUpdate object. A very notable case is the two implementations of TimetableSnapshotProvider: TimetableSnapshotSource and SiriTimetableSnapshotSource should really be a single implementation operating on internal data types independent of SIRI or GTFS-RT. + +## Realtime Concurrency + +The following approach to realtime concurrency was devised around 2013 when OTP first started consuming realtime data that affected routing results rather than just displaying messages. At first, the whole realtime system followed this approach. Some aspects of this system were maintained in subsequent work over the years, but because the details and rationale were not fully documented, misinterpretations and subtle inconsistencies were introduced. + +On 11 January 2024 a team of OTP developers reviewed this realtime concurrency approach together. The conclusion was that this approach remains sound, and that any consistency problems were not due to the approach itself, but rather due to its partial or erroneous implementation in realtime updater classes. Therefore, we decided to continue applying this approach in any new work on the realtime subsystem, at least until we encounter some situation that does not fit within this model. All existing realtime code that is not consistent with this approach should progressively be brought in line with it. + +In OTP's internal transit model, realtime data is currently stored separately from the scheduled data. This is only because realtime was originally introduced as an optional extra feature. Now that realtime is very commonly used, we intend to create a single unified transit model that will nonetheless continue to apply the same concurrency approach. + +The following is a sequence diagram showing how threads are intended to communicate. Unlike some common forms of sequence diagrams, time is on the horizontal axis here. Each horizontal line represents either a thread of execution (handling incoming realtime messages or routing requests) or a queue or buffer data structure. Dotted lines represent object references being handed off, and solid lines represent data being copied. + +![Realtime sequence diagram](images/updater-threads-queues.svg) + +At the top of the diagram are the GraphUpdater implementations. These fall broadly into two categories: polling updaters and streaming updaters. Polling updaters periodically send a request to server (often just a simple HTTP server) which returns a file containing the latest version of the updates. Streaming updaters are generally built around libraries implementing message-oriented protocols such as MQTT or AMQP, which fire a callback each time a new message is received. Polling updaters tend to return a full dataset describing the entire system state on each polling operation, while streaming updaters tend to receive incremental messages targeting individual transit trips. As such, polling updaters execute relatively infrequently (perhaps every minute or two) and process large responses, while streaming updaters execute very frequently (often many times per second) and operate on small messages in short bursts. Polling updaters are simpler in many ways and make use of common HTTP server components, but they introduce significant latency and redundant communication. Streaming updaters require more purpose-built or custom-configured components including message brokers, but bandwidth consumption and latency are lower, allowing routing results to reflect vehicle delays and positions immediately after they're reported. + +The GraphUpdaterManager coordinates all these updaters, and each runs freely in its own thread, receiving, deserializing, and validating data on its own schedule. Importantly, the GraphUpdaters are _not allowed to directly modify the transit data (Graph)_. Instead, they submit instances of GraphWriterRunnable which are queued up using the WriteToGraphCallback interface. These instances are essentially deferred code snippets that _are allowed_ to write to the Graph, but in a very controlled way. In short, there is exactly one thread that is allowed to make changes to the transit data, and those changes are queued up and executed in sequence, one at a time. + +As mentioned above, these GraphWriterRunnable instances must write to the transit data model in a very controlled way, following specific rules. They operate on a buffer containing a shallow copy of the whole transit data structure, and apply a copy-on-write strategy to avoid corrupting existing objects that may be visible to other parts of the system. When an instance is copied for writing, any references to it in parent objects must also be updated. Therefore, writes cause cascading copy operations, and all instances in the object tree back up to the root of the transit data structure must also be copied. As an optimization, if a GraphWriterRunnable is able to determine that the protective copy has already been made in this buffer (the part of the structure it needs to modify is somehow marked as being "dirty") it does not need to make another copy. If the update involves reading the existing data structure before making a change, those reads should be performed within the same contiguous chunk of deferred logic that performs the corresponding write, ensuring that there are no data races between write operations. + +This writable buffer of transit data is periodically made immutable and swapped into the role of a live snapshot, which is ready to be handed off to any incoming routing requests. Each time an immutable snapshot is created, a new writable buffer is created by making a shallow copy of the root instance in the transit data aggreagate. This functions like a double-buffering system, except that any number of snapshots can exist at once, and large subsets of the data can be shared across snapshots. As older snapshots (and their component parts) fall out of use, they are dereferenced and become eligible for garbage collection. Although the buffer swap could in principle occur after every write operation, it can incur significant copying and indexing overhead. When incremental message-oriented updaters are present this overhead would be incurred more often than necesary. Snapshots can be throttled to occur at most every few seconds, thereby reducing the total overhead at no perceptible cost to realtime visibility latency. + +This is essentially a multi-version snapshot concurrency control system, inspired by widely used database engines (and in fact informed by books on transactional database design). The end result is a system where: + +1. Writing operations are simple to reason about and cannot conflict because only one write happens at a time. +1. Multiple read operations (including routing requests) can occur concurrently. +1. Read operations do not need to pause while writes are happening. +1. Read operations see only fully completed write operations, never partial writes. +1. Each read operation sees a consistent, unchanging view of the transit data. +1. Each external API request sees a consistent data set, meaning all services that the + query directly or indirectly uses are operating on the same version of the data. + +An important characteristic of this approach is that _no locking is necessary_. However, some form of synchronization is used during the buffer swap operation to impose a consistent view of the whole data structure via a happens-before relationship as defined by the Java memory model. While pointers to objects can be handed between threads with no read-tearing of the pointer itself, there is no guarantee that the web of objects pointed to will be consistent without some explicit synchronization at the hand-off. + +Arguably the process of creating an immutable live snapshot (and a corresponding new writable buffer) should be handled by a GraphWriterRunnable on the single graph updater thread. This would serve to defer any queued modifications until the new buffer is in place, without introducing any further locking mechanisms. + +## Design Considerations + +This section summarizes the rationale behind some of the design decisions. + +### Realtime is Highly Concurrent + +An OTP instance can have multiple sources of realtime data at once. In some cases the transit data includes several feeds of scheduled data from different providers, with one or more types of realtime updates for those different feeds. + +In a large production OTP deployment, input data may be integrated into a single data source by an upstream system, or left as multiple data sources with guarantees about the uniqueness of identifiers. In either case, the single unified ID namespace allows realtime data to be easily associated with transit model entities. In practice, many OTP deployments do not have upstream data integration pipelines. In these cases OTP must manage several independent static and realtime data sources at once; feed IDs are used to keep namespaces separate, and to associate realtime data with the right subset of entities. Even when data feeds are well-integrated, the different kinds of realtime (arrival time updates, vehicle positions, or text alerts) may be split across multiple feeds as described in the GTFS-RT spec, which implies polling three different files. To handle these cases, it must be possible for more than one data source to specify the same feed ID. Eventually we want to make these feed IDs optional to simplify single-namespace OTP deployments. + +Each OTP instance in such a large configuration is also typically intended to handle several requests concurrently. Each incoming request needs to perform essentially random reads from the same large data structure representing the transit network, so there are efficiency benefits to many concurrent searches happening on the same instance, sharing this one large data structure. In a load-balanced cluster of OTP instances, realtime updates must be received and applied to each copy of the transportation network separately. So sharing each copy of the transportation network between a larger number of concurrent routing requests reduces the number of identical, arguably redundant network update processes going on simultaneously. + +In OTP the combined static and realtime transit data is a relatively large, deeply nested and interconnected data structure. It would take time to copy that structure, and especially to perform a deep copy of every nested object. Within a single instance, making multiple totally independent copies of this structure for different successive snapshots would tend to scatter reads from different routing threads across widely dispersed memory addresses, reducing cache efficiency. It could also lead to high (or highly variable) memory usage. In order to make updates to the transit data available frequently (as often as once every second, or as quickly as possible after each individual message comes in) we do not want to completely replicate the entire transit data structure for each snapshot. This would consume a significant fraction of the instance's available resources and likely degrade the aggregate performance of concurrently handled requests. + +### No Destructive Changes to Scheduled Data + +TripUpdates/SIRI-ET timetables cannot simply replace (overwrite) the original scheduled data. The updated timetables must be stored alongside the original scheduled ones. We need to retain the original data for several reasons. First, some routing requests should not be affected by realtime updates. The most obvious example is searches on future dates, which must use the scheduled trip objects. It is also common to show users delays relative to the originally planned schedule (like `8:30 +5min`). When a realtime disruption is severe enough, users may also expect or need to see the resulting itinerary in comparison with the one they expected to see in the absence of real-time disruptions. + +### Multiple Coexisting Read-only Snapshots + +Routing requests are relatively slow. They may take many seconds to process, and the response time is highly variable. For the duration that OTP is handling a single request, that request should see an effectively immutable, unchanging snapshot of the transit data. Even if new updates are constantly streaming in, each individual request must see a stable and unchanging view that remains internally consistent. Both the incoming realtime updates and the routing requests must behave like transactions that are “serializable” in the database concurrency control sense of the term: from any externally visible perspective, everything appears as if the reads and writes all happened in contiguous blocks in a single non-branching sequence, one after the other, even though much of the work is being done on threads in parallel. + +We take advantage of Java’s garbage collected environment here: once snapshots are no longer in use by any active routing code, they become unreferenced and are candidates for garbage collection. Of course the vast majority of the nested sub-objects used by a snapshot may still be in use, referenced by one or more successive snapshots that did not modify them, and therefore reused indefinitely. As long as each successive snapshot treats the previous ones (and all their nested sub-objects) as immutable, applying a copy-on-write policy to incorporate new information arriving in realtime updates, the garbage collector will weed out and deallocate subtrees as they are invalidated by those incoming realtime updates, once they are no longer needed by any active routing request. + +At any given moment there is a single most recent read-only snapshot of transit data, which is the one that will be handed off to any incoming routing requests. But when that single snapshot is updated, any requests that are currently executing will continue to hold their older snapshots. In this way there is always a single current snapshot, but an unlimited number of concurrently visible snapshots, each of which is being used by an unlimited number of concurrent routing requests. + +### Tolerance for Update Latency + +We don’t need every update for every individual trip to become visible to routing operations and end users independently of all other updates. We can batch updates together to a varying degree, trading off the number of separate snapshots present in memory at a given moment against the rapidity with which updates become visible to routing and to end users. Snapshots could be provided to the routing code on demand: if no requests are coming in, a series of individual updates will apply one after another to the same temporary buffer. As soon as a request comes in and needs a stable snapshot of the transit data to work with, the buffered set of transit data will be handed off to that request and subsequent updates applied to a new buffer in a copy-on-write fashion. However, a typical large system will require several load-balanced OTP instances that are essentially always busy, so such idle periods will rarely or never exist. Instead, we can create the same batching effect even with a continuous stream of incoming requests. For some period of time, typically a few seconds, all incoming requests will continue to be handed the last finalized snapshot and updates will accumulate to the new buffer. A new snapshot is created at regular intervals, independent of how many requests are arriving at the moment. This approach is also better for sparse reqeusts in that any pause or other overhead associated with snapshot creation is not incurred while the client is waiting, but rather proactively on its own thread. Clients always grab the latest available snapshot with no delay at all. + +### Derived Indexes + +In addition to the primary realtime and scheduled data, OTP maintains many derived indexes containing information that is implied by the primary data, but would be too slow to recompute every time it is needed. This includes for example which routes pass through each particular transit stop, or spatial indexes for fast lookups of which entities lie within bounding boxes or within a certain radius of a given point. As realtime data are applied, entities may be added or moved in ways that invalidate the contents of these derived indexes. + +Currently, there are many places in OTP where a single instance-wide index is maintained to be consistent with the latest realtime snapshot. When long-lived routing requests need to make use of these indexes during or at the end of the routing process, the index may have changed with respect to the unchanging snapshots the requests are working with. In fact, the indexes should be maintained using exactly the same strategy as the rest of the realtime transit data. They should first be managed with a copy-on-write strategy in a writable buffer that is only visible to the single-threaded writer actions, then transferred to an immutable snapshot that is handed off to the reading threads. + +## Full Dataset versus Incremental Messages + +The GTFS-RT specification includes an "incrementality" field. The specification says this field is unsupported and its behavior is undefined, but in practice people have been using this field since around 2013 in a fairly standardized way. An effort is underway to document its usage and update the standard (see https://github.com/google/transit/issues/84). + +GTFS-RT messages are most commonly distributed by HTTP GET polling. In this method, the consumer (OTP) has to make a request each time it wants to check for updates, and will receive all messages about all parts of the transit system at once, including messages it's already seen before. The incrementality field allows for some other options. As used in practice, there are three main aspects: + +- **Differential vs. full-dataset:** The incrementality field can take on two values: differential and full_dataset. In full-dataset mode, you'll get one big FeedMessage containing every update for every trip or vehicle. In differential mode, you'll receive updates for each trip or vehicle as they stream in, either individually or in small blocks. This may include a guarantee that an update will be provided on every entity at least once every n minutes, or alternatively the producer sending the full dataset when you first connect, then sending only changes. Once you're in differential mode, this opens up the possibilities below. + + - **Poll vs. push:** In practice differential messages are usually distributed individually or in small blocks via a message queue. This means the notifications are pushed by the message queueing system as soon as they arrive, rather than pulled by the consumer via occasional polling. It would in principle also be possible to provide differential updates by polling, with the producer actively tracking the last time each consumer polled (sessions), or the consumer including the producer-supplied timestamp of its last fetch, but we are not aware of any such implementations. Combining differential+push means that vehicle positions and trip updates can be received immediately after the vehicles report their positions. In some places vehicles provide updates every few seconds, so their position is genuinely known in real time. + +- **Message filtering:** Message queue systems often allow filtering by topic. A continuous stream of immediate small push messages is already an improvement over full dataset fetching, but if you only care about one route (for an arrival time display panel for example) you don't want to continuously receive and parse thousands of messages per second looking for the relevant ones. So rather than a "firehose" of every message, you subscribe to a topic that includes only messages for that one route. You then receive a single message every few seconds with the latest predictions for the routes you care about. + +The latency and bandwidth advantages of differential message passing systems are evident, particularly in large (national/metropolitan) realtime passenger information systems created through an intentional and thorough design process. + +SIRI allows for both polling and pub-sub approaches to message distribution. These correspond to the full dataset and differential modes described for GTFS-RT. + +## SIRI Resources + +- The official SIRI standardization page: https://www.siri-cen.eu/ +- Entur page on SIRI and GTFS-RT: https://developer.entur.org/pages-real-time-intro +- Original proposal and description of SIRI Lite (in French): http://www.normes-donnees-tc.org/wp-content/uploads/2017/01/Proposition-Profil-SIRI-Lite-initial-v1-2.pdf +- UK Government SIRI VM guidance: https://www.gov.uk/government/publications/technical-guidance-publishing-location-data-using-the-bus-open-data-service-siri-vm/technical-guidance-siri-vm +- Wikipedia page: https://en.wikipedia.org/wiki/Service_Interface_for_Real_Time_Information# + diff --git a/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java b/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java index 959827813cb..41912a573a1 100644 --- a/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java +++ b/src/main/java/org/opentripplanner/updater/spi/GraphUpdater.java @@ -1,39 +1,45 @@ package org.opentripplanner.updater.spi; /** - * Interface for graph updaters. Objects that implement this interface should always be configured - * via PreferencesConfigurable.configure after creating the object. GraphUpdaterConfigurator should - * take care of that. Beware that updaters run in separate threads at the same time. + * Interface for classes that fetch or receive information while the OTP instance is running and + * make changes to the Graph and associated transit data to reflect the current situation. This is + * typically information about disruptions to service, bicycle or parking availability, etc. *

- * The only allowed way to make changes to the graph in an updater is by executing (anonymous) - * GraphWriterRunnable objects via GraphUpdaterManager.execute. + * Each GraphUpdater implementation will be run in a separate thread, allowing it to make blocking + * calls to fetch data or even sleep between periodic polling operations without affecting the rest + * of the OTP instance. *

- * Example implementations can be found in ExampleGraphUpdater and ExamplePollingGraphUpdater. + * GraphUpdater implementations are instantiated by UpdaterConfigurator. Each updater configuration + * item in the router-config for a ThingUpdater is mapped to a corresponding configuration class + * ThingUpdaterParameters, which is passed to the ThingUpdater constructor. + *

+ * GraphUpdater implementations are only allowed to make changes to the Graph and related structures + * by submitting instances implementing GraphWriterRunnable (often anonymous functions) to the + * Graph writing callback function supplied to them by the GraphUpdaterManager after they're + * constructed. In this way, changes are queued up by many GraphUpdaters running in parallel on + * different threads, but are applied sequentially in a single-threaded manner to simplify reasoning + * about concurrent reads and writes to the Graph. */ public interface GraphUpdater { /** - * Graph updaters must be aware of their manager to be able to execute GraphWriterRunnables. - * GraphUpdaterConfigurator should take care of calling this function. - */ - void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph); - - /** - * Here the updater can be initialized. If it throws, the updater won't be started (i.e. the run - * method won't be called). All updaters' setup methods will be run sequentially in a - * single-threaded manner before updates begin, in order to avoid concurrent reads/writes. + * After a GraphUpdater is instantiated, the GraphUpdaterManager that instantiated it will + * immediately supply a callback via this method. The GraphUpdater will employ that callback + * every time it wants to queue up a write modification to the Graph or related data structures. */ - //void setup(Graph graph, TransitModel transitModel) throws Exception; + void setup(WriteToGraphCallback writeToGraphCallback); /** - * This method will run in its own thread. It pulls or receives updates and applies them to the - * graph. It must perform any writes to the graph by passing GraphWriterRunnables to - * GraphUpdaterManager.execute(). This queues up the write operations, ensuring that only one - * updater performs writes at a time. + * The GraphUpdaterManager will run this method in its own long-running thread. This method then + * pulls or receives updates and applies them to the graph. It must perform any writes to the + * graph by passing GraphWriterRunnables to the WriteToGraphCallback, which queues up the write + * operations, ensuring that only one submitted update performs writes at a time. */ void run() throws Exception; /** - * Here the updater can clean up after itself. + * When the GraphUpdaterManager wants to stop all GraphUpdaters (for example when OTP is shutting + * down) it will call this method, allowing the GraphUpdater implementation to shut down cleanly + * and release resources. */ default void teardown() {} @@ -49,8 +55,10 @@ default boolean isPrimed() { } /** - * This is the updater "type" used in the configuration file. It should ONLY be used to provide - * human friendly messages while logging and debugging. + * A GraphUpdater implementation uses this method to report its corresponding value of the "type" + * field in the configuration file. This value should ONLY be used when providing human-friendly + * messages while logging and debugging. Association of configuration to particular types is + * performed by the UpdatersConfig.Type constructor calling factory methods. */ String getConfigRef(); } diff --git a/src/main/java/org/opentripplanner/updater/trip/AbstractTimetableSnapshotSource.java b/src/main/java/org/opentripplanner/updater/trip/AbstractTimetableSnapshotSource.java new file mode 100644 index 00000000000..3df4f2aba59 --- /dev/null +++ b/src/main/java/org/opentripplanner/updater/trip/AbstractTimetableSnapshotSource.java @@ -0,0 +1,196 @@ +package org.opentripplanner.updater.trip; + +import java.time.LocalDate; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; +import org.opentripplanner.framework.time.CountdownTimer; +import org.opentripplanner.model.TimetableSnapshot; +import org.opentripplanner.model.TimetableSnapshotProvider; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; +import org.opentripplanner.updater.TimetableSnapshotSourceParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A base class for which abstracts away locking, updating, committing and purging of the timetable snapshot. + * In order to keep code reviews easier this is an intermediate stage and will be refactored further. + * In particular the following refactorings are planned: + * + * - use composition instead of inheritance + * - make the buffer private to this class and add an API for its access + * - create only one "snapshot manager" per transit model that is shared between Siri/GTFS-RT updaters + */ +public class AbstractTimetableSnapshotSource implements TimetableSnapshotProvider { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractTimetableSnapshotSource.class); + private final TransitLayerUpdater transitLayerUpdater; + /** + * Lock to indicate that buffer is in use + */ + private final ReentrantLock bufferLock = new ReentrantLock(true); + + /** + * The working copy of the timetable snapshot. Should not be visible to routing threads. Should + * only be modified by a thread that holds a lock on {@link #bufferLock}. All public methods that + * might modify this buffer will correctly acquire the lock. + */ + protected final TimetableSnapshot buffer = new TimetableSnapshot(); + + /** + * The working copy of the timetable snapshot. Should not be visible to routing threads. Should + * only be modified by a thread that holds a lock on {@link #bufferLock}. All public methods that + * might modify this buffer will correctly acquire the lock. By design, only one thread should + * ever be writing to this buffer. + * TODO RT_AB: research and document why this lock is needed since only one thread should ever be + * writing to this buffer. One possible reason may be a need to suspend writes while indexing + * and swapping out the buffer. But the original idea was to make a new copy of the buffer + * before re-indexing it. While refactoring or rewriting parts of this system, we could throw + * an exception if a writing section is entered by more than one thread. + */ + private volatile TimetableSnapshot snapshot = null; + + /** + * If a timetable snapshot is requested less than this number of milliseconds after the previous + * snapshot, just return the same one. Throttles the potentially resource-consuming task of + * duplicating a TripPattern -> Timetable map and indexing the new Timetables. + */ + private final CountdownTimer snapshotFrequencyThrottle; + + /** + * Should expired real-time data be purged from the graph. + * TODO RT_AB: Clarify exactly what "purge" means and in what circumstances would one turn it off. + */ + private final boolean purgeExpiredData; + /** + * We inject a provider to retrieve the current service-date(now). This enables us to unit-test + * the purgeExpiredData feature. + */ + private final Supplier localDateNow; + + private LocalDate lastPurgeDate = null; + + /** + * + * @param localDateNow This supplier allows you to inject a custom lambda to override what is + * considered 'today'. This is useful for unit testing. + */ + public AbstractTimetableSnapshotSource( + TransitLayerUpdater transitLayerUpdater, + TimetableSnapshotSourceParameters parameters, + Supplier localDateNow + ) { + this.transitLayerUpdater = transitLayerUpdater; + this.snapshotFrequencyThrottle = new CountdownTimer(parameters.maxSnapshotFrequency()); + this.purgeExpiredData = parameters.purgeExpiredData(); + this.localDateNow = Objects.requireNonNull(localDateNow); + // Force commit so that snapshot initializes + commitTimetableSnapshot(true); + } + + /** + * @return an up-to-date snapshot mapping TripPatterns to Timetables. This snapshot and the + * timetable objects it references are guaranteed to never change, so the requesting thread is + * provided a consistent view of all TripTimes. The routing thread need only release its reference + * to the snapshot to release resources. + */ + public final TimetableSnapshot getTimetableSnapshot() { + // Try to get a lock on the buffer + if (bufferLock.tryLock()) { + // Make a new snapshot if necessary + try { + commitTimetableSnapshot(false); + return snapshot; + } finally { + bufferLock.unlock(); + } + } + // No lock could be obtained because there is either a snapshot commit busy or updates + // are applied at this moment, just return the current snapshot + return snapshot; + } + + /** + * Request a commit of the timetable snapshot. + *

+ * If there are no updates buffered up or not enough time has elapsed, the existing snapshot + * is returned. + * + * @param force Force the committing of a new snapshot even if the above conditions are not met. + */ + public final void commitTimetableSnapshot(final boolean force) { + if (force || snapshotFrequencyThrottle.timeIsUp()) { + if (force || buffer.isDirty()) { + LOG.debug("Committing {}", buffer); + snapshot = buffer.commit(transitLayerUpdater, force); + + // We only reset the timer when the snapshot is updated. This will cause the first + // update to be committed after a silent period. This should not have any effect in + // a busy updater. It is however useful when manually testing the updater. + snapshotFrequencyThrottle.restart(); + } else { + LOG.debug("Buffer was unchanged, keeping old snapshot."); + } + } else { + LOG.debug("Snapshot frequency exceeded. Reusing snapshot {}", snapshot); + } + } + + /** + * Make a snapshot after each message in anticipation of incoming requests. + * Purge data if necessary (and force new snapshot if anything was purged). + * Make sure that the public (locking) getTimetableSnapshot function is not called. + */ + protected void purgeAndCommit() { + if (purgeExpiredData) { + final boolean modified = purgeExpiredData(); + commitTimetableSnapshot(modified); + } else { + commitTimetableSnapshot(false); + } + } + + /** + * Remove realtime data from previous service dates from the snapshot. This is useful so that + * instances that run for multiple days don't accumulate a lot of realtime data for past + * dates which would increase memory consumption. + * If your OTP instances are restarted throughout the day, this is less useful and can be + * turned off. + */ + protected final boolean purgeExpiredData() { + final LocalDate today = localDateNow.get(); + // TODO: Base this on numberOfDaysOfLongestTrip for tripPatterns + final LocalDate previously = today.minusDays(2); // Just to be safe... + + // Purge data only if we have changed date + if (lastPurgeDate != null && lastPurgeDate.compareTo(previously) >= 0) { + return false; + } + + LOG.debug("Purging expired realtime data"); + + lastPurgeDate = previously; + + return buffer.purgeExpiredData(previously); + } + + protected final LocalDate localDateNow() { + return localDateNow.get(); + } + + /** + * Execute a {@code Runnable} with a locked snapshot buffer and release the lock afterwards. While + * the action of locking and unlocking is not complicated to do for calling code, this method + * exists so that the lock instance is a private field. + */ + protected final void withLock(Runnable action) { + bufferLock.lock(); + + try { + action.run(); + } finally { + // Always release lock + bufferLock.unlock(); + } + } +} diff --git a/src/main/java/org/opentripplanner/updater/trip/MqttGtfsRealtimeUpdater.java b/src/main/java/org/opentripplanner/updater/trip/MqttGtfsRealtimeUpdater.java index 24474b6cdb3..11647c0a37f 100644 --- a/src/main/java/org/opentripplanner/updater/trip/MqttGtfsRealtimeUpdater.java +++ b/src/main/java/org/opentripplanner/updater/trip/MqttGtfsRealtimeUpdater.java @@ -83,8 +83,8 @@ public MqttGtfsRealtimeUpdater( } @Override - public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { - this.saveResultOnGraph = saveResultOnGraph; + public void setup(WriteToGraphCallback writeToGraphCallback) { + this.saveResultOnGraph = writeToGraphCallback; } @Override diff --git a/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java b/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java index 2dcc89095ac..351b0be0b83 100644 --- a/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java +++ b/src/main/java/org/opentripplanner/updater/trip/PollingTripUpdater.java @@ -71,8 +71,8 @@ public PollingTripUpdater( } @Override - public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { - this.saveResultOnGraph = saveResultOnGraph; + public void setup(WriteToGraphCallback writeToGraphCallback) { + this.saveResultOnGraph = writeToGraphCallback; } /** diff --git a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java index 049d4933c51..a530f348a00 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java +++ b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java @@ -21,7 +21,6 @@ import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate; import de.mfdz.MfdzRealtimeExtensions; import java.text.ParseException; -import java.time.Duration; import java.time.LocalDate; import java.time.ZoneId; import java.util.ArrayList; @@ -30,7 +29,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import javax.annotation.Nonnull; import org.opentripplanner.framework.i18n.I18NString; @@ -40,9 +38,6 @@ import org.opentripplanner.gtfs.mapping.TransitModeMapper; import org.opentripplanner.model.StopTime; import org.opentripplanner.model.Timetable; -import org.opentripplanner.model.TimetableSnapshot; -import org.opentripplanner.model.TimetableSnapshotProvider; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.DataValidationException; import org.opentripplanner.transit.model.framework.Deduplicator; @@ -76,7 +71,7 @@ * necessary to provide planning threads a consistent constant view of a graph with realtime data at * a specific point in time. */ -public class TimetableSnapshotSource implements TimetableSnapshotProvider { +public class TimetableSnapshotSource extends AbstractTimetableSnapshotSource { private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshotSource.class); @@ -85,60 +80,16 @@ public class TimetableSnapshotSource implements TimetableSnapshotProvider { */ private static final long MAX_ARRIVAL_DEPARTURE_TIME = 48 * 60 * 60; - /** - * The working copy of the timetable snapshot. Should not be visible to routing threads. Should - * only be modified by a thread that holds a lock on {@link #bufferLock}. All public methods that - * might modify this buffer will correctly acquire the lock. - */ - private final TimetableSnapshot buffer = new TimetableSnapshot(); - - /** - * Lock to indicate that buffer is in use - */ - private final ReentrantLock bufferLock = new ReentrantLock(true); - - /** - * A synchronized cache of trip patterns that are added to the graph due to GTFS-realtime - * messages. - */ - + /** A synchronized cache of trip patterns added to the graph due to GTFS-realtime messages. */ private final TripPatternCache tripPatternCache = new TripPatternCache(); private final ZoneId timeZone; private final TransitEditorService transitService; - private final TransitLayerUpdater transitLayerUpdater; - - /** - * If a timetable snapshot is requested less than this number of milliseconds after the previous - * snapshot, just return the same one. Throttles the potentially resource-consuming task of - * duplicating a TripPattern → Timetable map and indexing the new Timetables. - */ - private final Duration maxSnapshotFrequency; - - /** - * The last committed snapshot that was handed off to a routing thread. This snapshot may be given - * to more than one routing thread if the maximum snapshot frequency is exceeded. - */ - private volatile TimetableSnapshot snapshot = null; - - /** Should expired real-time data be purged from the graph. */ - private final boolean purgeExpiredData; - - protected LocalDate lastPurgeDate = null; - - /** Epoch time in milliseconds at which the last snapshot was generated. */ - protected long lastSnapshotTime = -1; private final Deduplicator deduplicator; private final Map serviceCodes; - /** - * We inject a provider to retrieve the current service-date(now). This enables us to unit-test - * the purgeExpiredData feature. - */ - private final Supplier localDateNow; - public TimetableSnapshotSource( TimetableSnapshotSourceParameters parameters, TransitModel transitModel @@ -155,45 +106,16 @@ public TimetableSnapshotSource( TransitModel transitModel, Supplier localDateNow ) { + super(transitModel.getTransitLayerUpdater(), parameters, localDateNow); this.timeZone = transitModel.getTimeZone(); this.transitService = new DefaultTransitService(transitModel); - this.transitLayerUpdater = transitModel.getTransitLayerUpdater(); this.deduplicator = transitModel.getDeduplicator(); this.serviceCodes = transitModel.getServiceCodes(); - this.maxSnapshotFrequency = parameters.maxSnapshotFrequency(); - this.purgeExpiredData = parameters.purgeExpiredData(); - this.localDateNow = localDateNow; // Inject this into the transit model transitModel.initTimetableSnapshotProvider(this); } - /** - * @return an up-to-date snapshot mapping TripPatterns to Timetables. This snapshot and the - * timetable objects it references are guaranteed to never change, so the requesting thread is - * provided a consistent view of all TripTimes. The routing thread need only release its reference - * to the snapshot to release resources. - */ - public TimetableSnapshot getTimetableSnapshot() { - TimetableSnapshot snapshotToReturn; - - // Try to get a lock on the buffer - if (bufferLock.tryLock()) { - // Make a new snapshot if necessary - try { - snapshotToReturn = getTimetableSnapshot(false); - } finally { - bufferLock.unlock(); - } - } else { - // No lock could be obtained because there is either a snapshot commit busy or updates - // are applied at this moment, just return the current snapshot - snapshotToReturn = snapshot; - } - - return snapshotToReturn; - } - /** * Method to apply a trip update list to the most recent version of the timetable snapshot. A * GTFS-RT feed is always applied against a single static feed (indicated by feedId). @@ -220,13 +142,10 @@ public UpdateResult applyTripUpdates( return UpdateResult.empty(); } - // Acquire lock on buffer - bufferLock.lock(); - Map failuresByRelationship = new HashMap<>(); List> results = new ArrayList<>(); - try { + withLock(() -> { if (fullDataset) { // Remove all updates from the buffer buffer.clear(feedId); @@ -269,7 +188,7 @@ public UpdateResult applyTripUpdates( } else { // TODO: figure out the correct service date. For the special case that a trip // starts for example at 40:00, yesterday would probably be a better guess. - serviceDate = localDateNow.get(); + serviceDate = localDateNow(); } uIndex += 1; @@ -325,19 +244,8 @@ public UpdateResult applyTripUpdates( } } - // Make a snapshot after each message in anticipation of incoming requests - // Purge data if necessary (and force new snapshot if anything was purged) - // Make sure that the public (locking) getTimetableSnapshot function is not called. - if (purgeExpiredData) { - final boolean modified = purgeExpiredData(); - getTimetableSnapshot(modified); - } else { - getTimetableSnapshot(false); - } - } finally { - // Always release lock - bufferLock.unlock(); - } + purgeAndCommit(); + }); var updateResult = UpdateResult.ofResults(results); @@ -367,22 +275,6 @@ private static void logUpdateResult( }); } - private TimetableSnapshot getTimetableSnapshot(final boolean force) { - final long now = System.currentTimeMillis(); - if (force || now - lastSnapshotTime > maxSnapshotFrequency.toMillis()) { - if (force || buffer.isDirty()) { - LOG.debug("Committing {}", buffer); - snapshot = buffer.commit(transitLayerUpdater, force); - } else { - LOG.debug("Buffer was unchanged, keeping old snapshot."); - } - lastSnapshotTime = System.currentTimeMillis(); - } else { - LOG.debug("Snapshot frequency exceeded. Reusing snapshot {}", snapshot); - } - return snapshot; - } - /** * Determine how the trip update should be handled. * @@ -1141,23 +1033,6 @@ private Result handleCanceledTrip( return Result.success(UpdateSuccess.noWarnings()); } - private boolean purgeExpiredData() { - final LocalDate today = localDateNow.get(); - // TODO: Base this on numberOfDaysOfLongestTrip for tripPatterns - final LocalDate previously = today.minusDays(2); // Just to be safe... - - // Purge data only if we have changed date - if (lastPurgeDate != null && lastPurgeDate.compareTo(previously) >= 0) { - return false; - } - - LOG.debug("purging expired realtime data"); - - lastPurgeDate = previously; - - return buffer.purgeExpiredData(previously); - } - /** * Retrieve a trip pattern given a trip id. * diff --git a/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java b/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java index b2bf7dc410f..bf8e75418ae 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java +++ b/src/main/java/org/opentripplanner/updater/trip/TripPatternCache.java @@ -13,9 +13,18 @@ import org.opentripplanner.transit.model.timetable.Trip; /** - * A synchronized cache of trip patterns that are added to the graph due to GTFS-realtime messages. + * Threadsafe mechanism for tracking any TripPatterns added to the graph via GTFS realtime messages. * This tracks only patterns added by realtime messages, not ones that already existed from the - * scheduled GTFS. + * scheduled GTFS. This is a "cache" in the sense that it will keep returning the same TripPattern + * when presented with the same StopPattern, so if realtime messages add many trips passing through + * the same sequence of stops, they will all end up on this same TripPattern. + *

+ * Note that there are two versions of this class, this one for GTFS-RT and another for SIRI. + * TODO RT_AB: consolidate TripPatternCache and SiriTripPatternCache. They seem to only be separate + * because SIRI- or GTFS-specific indexes of the added TripPatterns seem to have been added to + * this primary collection. + * FIXME RT_AB: the name does not make it clear that this has anything to do with elements that are + * only added due to realtime updates, and it is only loosely a cache. RealtimeAddedTripPatterns? */ public class TripPatternCache { @@ -24,11 +33,12 @@ public class TripPatternCache { * already existed from the scheduled GTFS. */ private final Map cache = new HashMap<>(); + + /** Used for producing sequential integers to ensure each added pattern has a unique name. */ private int counter = 0; /** - * Get cached trip pattern or create one if it doesn't exist yet. If a trip pattern is created, - * vertices and edges for this trip pattern are also created in the graph. + * Get cached trip pattern or create one if it doesn't exist yet. * * @param stopPattern stop pattern to retrieve/create trip pattern * @param trip the trip the new trip pattern will be created for @@ -50,17 +60,16 @@ public synchronized TripPattern getOrCreateTripPattern( // Generate unique code for trip pattern var id = generateUniqueTripPatternCode(trip); - TripPatternBuilder tripPatternBuilder = TripPattern - .of(id) - .withRoute(route) - .withMode(trip.getMode()) - .withNetexSubmode(trip.getNetexSubMode()) - .withStopPattern(stopPattern); - - tripPatternBuilder.withCreatedByRealtimeUpdater(true); - tripPatternBuilder.withOriginalTripPattern(originalTripPattern); - - tripPattern = tripPatternBuilder.build(); + tripPattern = + TripPattern + .of(id) + .withRoute(route) + .withMode(trip.getMode()) + .withNetexSubmode(trip.getNetexSubMode()) + .withStopPattern(stopPattern) + .withCreatedByRealtimeUpdater(true) + .withOriginalTripPattern(originalTripPattern) + .build(); // Add pattern to cache cache.put(stopPattern, tripPattern); @@ -72,6 +81,12 @@ public synchronized TripPattern getOrCreateTripPattern( /** * Generate unique trip pattern code for real-time added trip pattern. This function roughly * follows the format of the {@link GenerateTripPatternsOperation}. + * In the SIRI version of this class, this is provided by a SiriTripPatternIdGenerator. If the + * GTFS-RT and SIRI version of these classes are merged, this function could become a second + * implementation of TripPatternIdGenerator. + * This method is not static because it references a monotonically increasing integer counter. + * But like in SiriTripPatternIdGenerator, this could be encapsulated outside the cache object. + * TODO RT_AB: create GtfsRtTripPatternIdGenerator as part of merging the two TripPatternCaches. */ private FeedScopedId generateUniqueTripPatternCode(Trip trip) { FeedScopedId routeId = trip.getRoute().getId(); diff --git a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java index 185de6bddb7..171bd53f77a 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_parking/VehicleParkingUpdater.java @@ -64,8 +64,8 @@ public VehicleParkingUpdater( } @Override - public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { - this.saveResultOnGraph = saveResultOnGraph; + public void setup(WriteToGraphCallback writeToGraphCallback) { + this.saveResultOnGraph = writeToGraphCallback; } @Override diff --git a/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java b/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java index bb2e27c3ce8..745418f5132 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java @@ -69,8 +69,8 @@ public PollingVehiclePositionUpdater( } @Override - public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { - this.saveResultOnGraph = saveResultOnGraph; + public void setup(WriteToGraphCallback writeToGraphCallback) { + this.saveResultOnGraph = writeToGraphCallback; } /** diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java index 1e037a7c1c2..8dfa046b1f9 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java @@ -110,8 +110,8 @@ public VehicleRentalUpdater( } @Override - public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) { - this.saveResultOnGraph = saveResultOnGraph; + public void setup(WriteToGraphCallback writeToGraphCallback) { + this.saveResultOnGraph = writeToGraphCallback; } @Override diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoader.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoader.java index d4e5cbd4952..0e103231849 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoader.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoader.java @@ -7,10 +7,10 @@ import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; -import org.entur.gbfs.v2_3.gbfs.GBFS; -import org.entur.gbfs.v2_3.gbfs.GBFSFeed; -import org.entur.gbfs.v2_3.gbfs.GBFSFeedName; -import org.entur.gbfs.v2_3.gbfs.GBFSFeeds; +import org.mobilitydata.gbfs.v2_3.gbfs.GBFS; +import org.mobilitydata.gbfs.v2_3.gbfs.GBFSFeed; +import org.mobilitydata.gbfs.v2_3.gbfs.GBFSFeedName; +import org.mobilitydata.gbfs.v2_3.gbfs.GBFSFeeds; import org.opentripplanner.framework.io.OtpHttpClient; import org.opentripplanner.framework.io.OtpHttpClientException; import org.opentripplanner.framework.io.OtpHttpClientFactory; diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapper.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapper.java index c871ac49649..7fd884b7ab8 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapper.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapper.java @@ -7,8 +7,8 @@ import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import org.entur.gbfs.v2_3.free_bike_status.GBFSBike; -import org.entur.gbfs.v2_3.free_bike_status.GBFSRentalUris; +import org.mobilitydata.gbfs.v2_3.free_bike_status.GBFSBike; +import org.mobilitydata.gbfs.v2_3.free_bike_status.GBFSRentalUris; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris; diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsGeofencingZoneMapper.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsGeofencingZoneMapper.java index 910ab855585..caf5d97c0d4 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsGeofencingZoneMapper.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsGeofencingZoneMapper.java @@ -5,9 +5,9 @@ import java.util.List; import java.util.Objects; import javax.annotation.Nullable; -import org.entur.gbfs.v2_3.geofencing_zones.GBFSFeature; -import org.entur.gbfs.v2_3.geofencing_zones.GBFSGeofencingZones; import org.locationtech.jts.geom.Geometry; +import org.mobilitydata.gbfs.v2_3.geofencing_zones.GBFSFeature; +import org.mobilitydata.gbfs.v2_3.geofencing_zones.GBFSGeofencingZones; import org.opentripplanner.framework.geometry.GeometryUtils; import org.opentripplanner.framework.geometry.UnsupportedGeometryException; import org.opentripplanner.service.vehiclerental.model.GeofencingZone; diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsStationInformationMapper.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsStationInformationMapper.java index 496bd08c819..2f58476457c 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsStationInformationMapper.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsStationInformationMapper.java @@ -2,8 +2,8 @@ import java.util.Map; import java.util.stream.Collectors; -import org.entur.gbfs.v2_3.station_information.GBFSRentalUris; -import org.entur.gbfs.v2_3.station_information.GBFSStation; +import org.mobilitydata.gbfs.v2_3.station_information.GBFSRentalUris; +import org.mobilitydata.gbfs.v2_3.station_information.GBFSStation; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsStationStatusMapper.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsStationStatusMapper.java index 77053917e7a..21ee8d000c0 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsStationStatusMapper.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsStationStatusMapper.java @@ -4,8 +4,8 @@ import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -import org.entur.gbfs.v2_3.station_status.GBFSStation; -import org.entur.gbfs.v2_3.station_status.GBFSVehicleTypesAvailable; +import org.mobilitydata.gbfs.v2_3.station_status.GBFSStation; +import org.mobilitydata.gbfs.v2_3.station_status.GBFSVehicleTypesAvailable; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; import org.slf4j.Logger; diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsSystemInformationMapper.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsSystemInformationMapper.java index 6a32d27c3e7..8011fd1ce63 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsSystemInformationMapper.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsSystemInformationMapper.java @@ -1,6 +1,6 @@ package org.opentripplanner.updater.vehicle_rental.datasources; -import org.entur.gbfs.v2_3.system_information.GBFSData; +import org.mobilitydata.gbfs.v2_3.system_information.GBFSData; import org.opentripplanner.service.vehiclerental.model.VehicleRentalSystem; import org.opentripplanner.service.vehiclerental.model.VehicleRentalSystemAppInformation; diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSource.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSource.java index aed214102ff..c441efb974a 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSource.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSource.java @@ -6,14 +6,14 @@ import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; -import org.entur.gbfs.v2_3.free_bike_status.GBFSFreeBikeStatus; -import org.entur.gbfs.v2_3.geofencing_zones.GBFSGeofencingZones; -import org.entur.gbfs.v2_3.station_information.GBFSStationInformation; -import org.entur.gbfs.v2_3.station_status.GBFSStation; -import org.entur.gbfs.v2_3.station_status.GBFSStationStatus; -import org.entur.gbfs.v2_3.system_information.GBFSSystemInformation; -import org.entur.gbfs.v2_3.vehicle_types.GBFSVehicleType; -import org.entur.gbfs.v2_3.vehicle_types.GBFSVehicleTypes; +import org.mobilitydata.gbfs.v2_3.free_bike_status.GBFSFreeBikeStatus; +import org.mobilitydata.gbfs.v2_3.geofencing_zones.GBFSGeofencingZones; +import org.mobilitydata.gbfs.v2_3.station_information.GBFSStationInformation; +import org.mobilitydata.gbfs.v2_3.station_status.GBFSStation; +import org.mobilitydata.gbfs.v2_3.station_status.GBFSStationStatus; +import org.mobilitydata.gbfs.v2_3.system_information.GBFSSystemInformation; +import org.mobilitydata.gbfs.v2_3.vehicle_types.GBFSVehicleType; +import org.mobilitydata.gbfs.v2_3.vehicle_types.GBFSVehicleTypes; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.io.OtpHttpClient; import org.opentripplanner.framework.io.OtpHttpClientFactory; diff --git a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleTypeMapper.java b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleTypeMapper.java index b17c4b20200..5c7816781ce 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleTypeMapper.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleTypeMapper.java @@ -1,6 +1,6 @@ package org.opentripplanner.updater.vehicle_rental.datasources; -import org.entur.gbfs.v2_3.vehicle_types.GBFSVehicleType; +import org.mobilitydata.gbfs.v2_3.vehicle_types.GBFSVehicleType; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; import org.opentripplanner.street.model.RentalFormFactor; import org.opentripplanner.transit.model.framework.FeedScopedId; diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 25c0fd67696..79a794b3607 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -1164,6 +1164,18 @@ type fareComponent { routes: [Route] @deprecated } + +""" +Feed publisher information +""" +type FeedPublisher { + """Name of feed publisher""" + name: String! + + """Web address of feed publisher""" + url: String! +} + """ A feed provides routing data (stops, routes, timetables, etc.) from one or more public transport agencies. """ @@ -1174,6 +1186,9 @@ type Feed { """List of agencies which provide data to this feed""" agencies: [Agency] + "The publisher of the input transit data." + publisher: FeedPublisher + """ Alerts relevant for the feed. """ diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index d263667d672..e6f4208c643 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -571,6 +571,7 @@ type Quay implements PlaceInterface { omitNonBoarding: Boolean = false @deprecated(reason : "Non-functional. Use arrivalDeparture instead."), "DateTime for when to fetch estimated calls from. Default value is current time" startTime: DateTime, + "Duration in seconds from start time to search forward for estimated calls. Must be a positive value. Default value is 24 hours" timeRange: Int = 86400, "Parameters for indicating the only authorities and/or lines or quays to list estimatedCalls for" whiteListed: InputWhiteListed, @@ -1132,6 +1133,7 @@ type StopPlace implements PlaceInterface { numberOfDeparturesPerLineAndDestinationDisplay: Int, "DateTime for when to fetch estimated calls from. Default value is current time" startTime: DateTime, + "Duration in seconds from start time to search forward for estimated calls. Must be a positive value. Default value is 24 hours" timeRange: Int = 86400, "Parameters for indicating the only authorities and/or lines or quays to list estimatedCalls for" whiteListed: InputWhiteListed, diff --git a/src/test/java/org/opentripplanner/DateTimeHelper.java b/src/test/java/org/opentripplanner/DateTimeHelper.java new file mode 100644 index 00000000000..6a058118d1b --- /dev/null +++ b/src/test/java/org/opentripplanner/DateTimeHelper.java @@ -0,0 +1,30 @@ +package org.opentripplanner; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import org.opentripplanner.framework.time.DateUtils; + +public class DateTimeHelper { + + private final ZoneId zone; + private final LocalDate defaultDate; + + public DateTimeHelper(ZoneId zone, LocalDate defaultDate) { + this.zone = zone; + this.defaultDate = defaultDate; + } + + /** + * Create a zoned datetime from a time string using the default date and timezone + * + * @throws IllegalArgumentException if we can't parse the string as a time. + */ + public ZonedDateTime zonedDateTime(String timeString) { + var time = DateUtils.parseTime(timeString); + if (time == null) { + throw new IllegalArgumentException("Invalid time format"); + } + return defaultDate.atTime(time).atZone(zone); + } +} diff --git a/src/test/java/org/opentripplanner/GtfsTest.java b/src/test/java/org/opentripplanner/GtfsTest.java index 99f78c18e33..6f14a6db662 100644 --- a/src/test/java/org/opentripplanner/GtfsTest.java +++ b/src/test/java/org/opentripplanner/GtfsTest.java @@ -13,6 +13,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.InputStream; +import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; @@ -207,7 +208,9 @@ protected void setUp() throws Exception { serverContext = TestServerContext.createServerContext(graph, transitModel); timetableSnapshotSource = new TimetableSnapshotSource( - TimetableSnapshotSourceParameters.DEFAULT.withPurgeExpiredData(false), + TimetableSnapshotSourceParameters.DEFAULT + .withPurgeExpiredData(true) + .withMaxSnapshotFrequency(Duration.ZERO), transitModel ); alertPatchServiceImpl = new TransitAlertServiceImpl(transitModel); diff --git a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index fcc59af845f..614c8778c6b 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -43,6 +43,7 @@ import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.framework.model.Grams; +import org.opentripplanner.model.FeedInfo; import org.opentripplanner.model.fare.FareMedium; import org.opentripplanner.model.fare.FareProduct; import org.opentripplanner.model.fare.ItineraryFares; @@ -84,6 +85,7 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.BikeAccess; import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; @@ -151,6 +153,18 @@ static void setup() { transitModel.addTripPattern(id("pattern-1"), pattern); + var feedId = "testfeed"; + var feedInfo = FeedInfo.dummyForTest(feedId); + transitModel.addFeedInfo(feedInfo); + + var agency = Agency + .of(new FeedScopedId(feedId, "agency-xx")) + .withName("speedtransit") + .withUrl("www.otp-foo.bar") + .withTimezone("Europe/Berlin") + .build(); + transitModel.addAgency(agency); + transitModel.initTimeZone(ZoneIds.BERLIN); transitModel.index(); var routes = Arrays diff --git a/src/test/java/org/opentripplanner/apis/transmodel/support/GqlUtilTest.java b/src/test/java/org/opentripplanner/apis/transmodel/support/GqlUtilTest.java index d2d15168baf..40a95a6661a 100644 --- a/src/test/java/org/opentripplanner/apis/transmodel/support/GqlUtilTest.java +++ b/src/test/java/org/opentripplanner/apis/transmodel/support/GqlUtilTest.java @@ -2,11 +2,14 @@ import static graphql.execution.ExecutionContextBuilder.newExecutionContextBuilder; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import graphql.ExecutionInput; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionId; +import graphql.schema.DataFetchingEnvironment; import graphql.schema.DataFetchingEnvironmentImpl; +import java.util.HashMap; import java.util.Locale; import java.util.Map; import org.junit.jupiter.api.Test; @@ -14,6 +17,7 @@ public class GqlUtilTest { static final ExecutionContext executionContext; + private static final String TEST_ARGUMENT = "testArgument"; static { ExecutionInput executionInput = ExecutionInput @@ -29,6 +33,54 @@ public class GqlUtilTest { .build(); } + @Test + void testGetPositiveNonNullIntegerArgumentWithStrictlyPositiveValue() { + var env = buildEnvWithTestValue(1); + assertEquals(1, GqlUtil.getPositiveNonNullIntegerArgument(env, TEST_ARGUMENT)); + } + + @Test + void testGetPositiveNonNullIntegerArgumentWithZeroValue() { + var env = buildEnvWithTestValue(0); + assertEquals(0, GqlUtil.getPositiveNonNullIntegerArgument(env, TEST_ARGUMENT)); + } + + @Test + void testGetPositiveNonNullIntegerArgumentWithNegativeValue() { + var env = buildEnvWithTestValue(-1); + assertThrows( + IllegalArgumentException.class, + () -> GqlUtil.getPositiveNonNullIntegerArgument(env, TEST_ARGUMENT) + ); + } + + @Test + void testGetPositiveNonNullIntegerArgumentWithNullValue() { + var env = buildEnvWithTestValue(null); + assertThrows( + IllegalArgumentException.class, + () -> GqlUtil.getPositiveNonNullIntegerArgument(env, TEST_ARGUMENT) + ); + } + + @Test + void testGetPositiveNonNullIntegerArgumentWithoutValue() { + var env = DataFetchingEnvironmentImpl.newDataFetchingEnvironment(executionContext).build(); + assertThrows( + IllegalArgumentException.class, + () -> GqlUtil.getPositiveNonNullIntegerArgument(env, TEST_ARGUMENT) + ); + } + + private static DataFetchingEnvironment buildEnvWithTestValue(Integer value) { + Map argsMap = new HashMap<>(); + argsMap.put(TEST_ARGUMENT, value); + return DataFetchingEnvironmentImpl + .newDataFetchingEnvironment(executionContext) + .arguments(argsMap) + .build(); + } + @Test void testGetLocaleWithLangArgument() { var env = DataFetchingEnvironmentImpl diff --git a/src/test/java/org/opentripplanner/framework/collection/ListUtilsTest.java b/src/test/java/org/opentripplanner/framework/collection/ListUtilsTest.java index 6e72d5626c5..33dce1f5574 100644 --- a/src/test/java/org/opentripplanner/framework/collection/ListUtilsTest.java +++ b/src/test/java/org/opentripplanner/framework/collection/ListUtilsTest.java @@ -56,5 +56,11 @@ void distinctByKey() { assertEquals(List.of(first, third), deduplicated); } + @Test + void ofNullable() { + assertEquals(List.of(), ListUtils.ofNullable(null)); + assertEquals(List.of("A"), ListUtils.ofNullable("A")); + } + private record Wrapper(int i, String string) {} } diff --git a/src/test/java/org/opentripplanner/gtfs/mapping/TripMapperTest.java b/src/test/java/org/opentripplanner/gtfs/mapping/TripMapperTest.java index f24e405515d..964c3d8155e 100644 --- a/src/test/java/org/opentripplanner/gtfs/mapping/TripMapperTest.java +++ b/src/test/java/org/opentripplanner/gtfs/mapping/TripMapperTest.java @@ -111,14 +111,14 @@ void testMapCache() throws Exception { } @Test - void noFlexDurationModifier() { + void noFlexTimePenalty() { var mapper = defaultTripMapper(); mapper.map(TRIP); - assertTrue(mapper.flexSafeDurationModifiers().isEmpty()); + assertTrue(mapper.flexSafeTimePenalties().isEmpty()); } @Test - void flexDurationModifier() { + void flexTimePenalty() { var flexTrip = new Trip(); flexTrip.setId(new AgencyAndId("1", "1")); flexTrip.setSafeDurationFactor(1.5); @@ -126,8 +126,8 @@ void flexDurationModifier() { flexTrip.setRoute(new GtfsTestData().route); var mapper = defaultTripMapper(); var mapped = mapper.map(flexTrip); - var mod = mapper.flexSafeDurationModifiers().get(mapped); - assertEquals(1.5f, mod.coefficient()); - assertEquals(600, mod.constant().toSeconds()); + var penalty = mapper.flexSafeTimePenalties().get(mapped); + assertEquals(1.5f, penalty.coefficient()); + assertEquals(600, penalty.constant().toSeconds()); } } diff --git a/src/test/java/org/opentripplanner/mmri/AllModesAndAgenciesTest.java b/src/test/java/org/opentripplanner/mmri/AllModesAndAgenciesTest.java deleted file mode 100644 index f4e0a42fe83..00000000000 --- a/src/test/java/org/opentripplanner/mmri/AllModesAndAgenciesTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.opentripplanner.mmri; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.opentripplanner.GtfsTest; -import org.opentripplanner.model.plan.Itinerary; -import org.opentripplanner.model.plan.Leg; - -@Disabled("Requires stop-to-stop transfers without street network") -public class AllModesAndAgenciesTest extends GtfsTest { - - @Override - public final String getFeedName() { - return "mmri/1a"; - } - - @Test - public void test1a1() { - Itinerary itinerary = plan(+1388530800L, "1a1", "1a6", null, false, false, null, "", "", 5); - - Leg[] legs = itinerary.getLegs().toArray(new Leg[5]); - - validateLeg(legs[0], 1388530860000L, 1388530920000L, "1a2", "1a1", null); - validateLeg(legs[2], 1388530980000L, 1388531040000L, "1a4", "1a3", null); - validateLeg(legs[4], 1388531100000L, 1388531160000L, "1a6", "1a5", null); - - assertEquals("", itinerary.toStr()); - } -} diff --git a/src/test/java/org/opentripplanner/mmri/ExcludedStopsTest.java b/src/test/java/org/opentripplanner/mmri/ExcludedStopsTest.java deleted file mode 100644 index 4fe48f2c123..00000000000 --- a/src/test/java/org/opentripplanner/mmri/ExcludedStopsTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.opentripplanner.mmri; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.opentripplanner.GtfsTest; -import org.opentripplanner.model.plan.Itinerary; -import org.opentripplanner.model.plan.Leg; - -@Disabled("Requires stop banning") -public class ExcludedStopsTest extends GtfsTest { - - @Override - public final String getFeedName() { - return "mmri/3f"; - } - - @Test - public void test3f1() { - Itinerary itinerary = plan(+1388530860L, "3f1", "3f3", null, false, false, null, "", "3f2", 1); - - Leg leg = itinerary.getLegs().toArray(new Leg[1])[0]; - - validateLeg(leg, 1388530860000L, 1388531040000L, "3f3", "3f1", null); - - assertEquals("", itinerary.toStr()); - } -} diff --git a/src/test/java/org/opentripplanner/mmri/OnTripTest.java b/src/test/java/org/opentripplanner/mmri/OnTripTest.java deleted file mode 100644 index 2b0e574f352..00000000000 --- a/src/test/java/org/opentripplanner/mmri/OnTripTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.opentripplanner.mmri; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.opentripplanner.GtfsTest; -import org.opentripplanner.model.plan.Itinerary; -import org.opentripplanner.model.plan.Leg; - -@Disabled("Requires departing onboard trip") -public class OnTripTest extends GtfsTest { - - @Override - public final String getFeedName() { - return "mmri/2f"; - } - - @Test - public void test2f1() { - Itinerary itinerary = plan( - +1388530920L, - null, - "2f2", - "2f|intercity", - false, - false, - null, - "", - "", - 2 - ); - - Leg[] legs = itinerary.getLegs().toArray(new Leg[2]); - - validateLeg(legs[0], 1388530920000L, 1388531040000L, "2f3", null, null); - validateLeg(legs[1], 1388531160000L, 1388531340000L, "2f2", "2f3", null); - - assertEquals("", itinerary.toStr()); - } -} diff --git a/src/test/java/org/opentripplanner/mmri/PlannerstackScenarioTest.java b/src/test/java/org/opentripplanner/mmri/PlannerstackScenarioTest.java deleted file mode 100644 index 1e0ac9b96f2..00000000000 --- a/src/test/java/org/opentripplanner/mmri/PlannerstackScenarioTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.opentripplanner.mmri; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.opentripplanner.GtfsTest; -import org.opentripplanner.model.plan.Itinerary; -import org.opentripplanner.model.plan.Leg; - -@Disabled("Requires departing onboard a trip") -public class PlannerstackScenarioTest extends GtfsTest { - - @Override - public final String getFeedName() { - return "mmri/plannerstack_scenario"; - } - - @Test - public void testPlannerstackScenario() { - Itinerary itinerary = plan( - +1388531220L, - null, - "plannerstack_scenario2", - "plannerstack_scenario|intercity", - false, - false, - null, - "", - "", - 2 - ); - - Leg[] legs = itinerary.getLegs().toArray(new Leg[2]); - - validateLeg(legs[0], 1388531220000L, 1388531340000L, "plannerstack_scenario3", null, null); - validateLeg( - legs[1], - 1388531400000L, - 1388531640000L, - "plannerstack_scenario2", - "plannerstack_scenario3", - null - ); - - assertEquals("", itinerary.toStr()); - } -} diff --git a/src/test/java/org/opentripplanner/mmri/PreferencesTest.java b/src/test/java/org/opentripplanner/mmri/PreferencesTest.java index d3c55097511..c33f67345fc 100644 --- a/src/test/java/org/opentripplanner/mmri/PreferencesTest.java +++ b/src/test/java/org/opentripplanner/mmri/PreferencesTest.java @@ -1,9 +1,7 @@ package org.opentripplanner.mmri; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.opentripplanner.transit.model.basic.TransitMode.BUS; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.opentripplanner.GtfsTest; import org.opentripplanner.model.plan.Itinerary; @@ -30,28 +28,4 @@ public void test2c1() { itinerary.toStr() ); } - - @Test - @Disabled - public void test2c2() { - Itinerary itinerary = plan(+1388530860L, "2c1", "2c3", null, false, false, BUS, "", "", 3); - - Leg[] legs = itinerary.getLegs().toArray(new Leg[3]); - - validateLeg(legs[1], 1388530920000L, 1388531160000L, "2c5", "2c4", null); - - assertEquals("", itinerary.toStr()); - } - - @Test - @Disabled - public void test2c3() { - Itinerary itinerary = plan(+1388530860L, "2c1", "2c3", null, false, true, null, "", "", 3); - - Leg[] legs = itinerary.getLegs().toArray(new Leg[3]); - - validateLeg(legs[1], 1388530920000L, 1388531160000L, "2c5", "2c4", null); - - assertEquals("", itinerary.toStr()); - } } diff --git a/src/test/java/org/opentripplanner/mmri/ServiceAlertTest.java b/src/test/java/org/opentripplanner/mmri/ServiceAlertTest.java deleted file mode 100644 index 2d35f0a246d..00000000000 --- a/src/test/java/org/opentripplanner/mmri/ServiceAlertTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.opentripplanner.mmri; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.opentripplanner.GtfsTest; -import org.opentripplanner.model.plan.Itinerary; -import org.opentripplanner.model.plan.Leg; - -@Disabled("Service alerts not mapped correctly") -public class ServiceAlertTest extends GtfsTest { - - @Override - public final String getFeedName() { - return "mmri/3i"; - } - - @Test - public void test3i1() { - Itinerary itinerary = plan(+1388530860L, "3i1", "3i2", null, false, false, null, "", "", 1); - - Leg leg = itinerary.getLegs().toArray(new Leg[1])[0]; - - validateLeg(leg, 1388530860000L, 1388530920000L, "3i2", "3i1", "Unknown effect"); - - assertEquals("", itinerary.toStr()); - } -} diff --git a/src/test/java/org/opentripplanner/mmri/TimeTest.java b/src/test/java/org/opentripplanner/mmri/TimeTest.java index 77123d3e656..90be00a56bb 100644 --- a/src/test/java/org/opentripplanner/mmri/TimeTest.java +++ b/src/test/java/org/opentripplanner/mmri/TimeTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.opentripplanner.GtfsTest; import org.opentripplanner.model.plan.Itinerary; @@ -37,30 +36,6 @@ public void test1g2() { assertEquals("Stop 1g1 ~ BUS bus 0:01 0:02 ~ Stop 1g2 [C₁90]", itinerary.toStr()); } - @Test - @Disabled - public void test1g3() { - Itinerary itinerary = plan(+1388617380L, "1g1", "1g2", null, false, false, null, "", "", 1); - - Leg leg = itinerary.getLegs().toArray(new Leg[1])[0]; - - validateLeg(leg, 1388703660000L, 1388703720000L, "1g2", "1g1", null); - - assertEquals("", itinerary.toStr()); - } - - @Test - @Disabled - public void test1g4() { - Itinerary itinerary = plan(-1388617440L, "1g1", "1g2", null, false, false, null, "", "", 1); - - Leg leg = itinerary.getLegs().toArray(new Leg[1])[0]; - - validateLeg(leg, 1388531100000L, 1388531160000L, "1g2", "1g1", null); - - assertEquals("", itinerary.toStr()); - } - @Test public void test1g5() { Itinerary itinerary = plan(+1388703780L, "1g1", "1g2", null, false, false, null, "", "", 1); diff --git a/src/test/java/org/opentripplanner/mmri/TransferTimeTest.java b/src/test/java/org/opentripplanner/mmri/TransferTimeTest.java deleted file mode 100644 index 9b0fc12cf37..00000000000 --- a/src/test/java/org/opentripplanner/mmri/TransferTimeTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.opentripplanner.mmri; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.opentripplanner.GtfsTest; -import org.opentripplanner.model.plan.Itinerary; -import org.opentripplanner.model.plan.Leg; - -@Disabled -public class TransferTimeTest extends GtfsTest { - - @Override - public final String getFeedName() { - return "mmri/2a2"; - } - - @Test - public void test2a3() { - Itinerary itinerary = plan(+1388530860L, "2a3", "2a6", null, false, false, null, "", "", 3); - - Leg[] legs = itinerary.getLegs().toArray(new Leg[3]); - - validateLeg(legs[0], 1388530860000L, 1388530920000L, "2a4", "2a3", null); - validateLeg(legs[2], 1388531280000L, 1388531340000L, "2a6", "2a5", null); - - assertEquals("", itinerary.toStr()); - } - - @Test - public void test2a4() { - Itinerary itinerary = plan(+1388530920L, "2a3", "2a6", null, false, false, null, "", "", 3); - - Leg[] legs = itinerary.getLegs().toArray(new Leg[3]); - - validateLeg(legs[0], 1388531040000L, 1388531100000L, "2a4", "2a3", null); - validateLeg(legs[2], 1388531400000L, 1388531460000L, "2a6", "2a5", null); - - assertEquals("", itinerary.toStr()); - } - - @Test - public void test2a5() { - Itinerary itinerary = plan(-1388531460L, "2a3", "2a6", null, false, false, null, "", "", 3); - - Leg[] legs = itinerary.getLegs().toArray(new Leg[3]); - - validateLeg(legs[0], 1388531040000L, 1388531100000L, "2a4", "2a3", null); - validateLeg(legs[2], 1388531400000L, 1388531460000L, "2a6", "2a5", null); - - assertEquals("", itinerary.toStr()); - } -} diff --git a/src/test/java/org/opentripplanner/model/TimetableTest.java b/src/test/java/org/opentripplanner/model/TimetableTest.java index 07b44844291..9e6a7467dc5 100644 --- a/src/test/java/org/opentripplanner/model/TimetableTest.java +++ b/src/test/java/org/opentripplanner/model/TimetableTest.java @@ -66,6 +66,12 @@ public static void setUp() throws Exception { trip_1_1_index = timetable.getTripIndex(new FeedScopedId(feedId, TRIP_ID)); } + @Test + public void getGetTripTimes() { + var tt = timetable.getTripTimes(new FeedScopedId(feedId, TRIP_ID)); + assertNotNull(tt); + } + @Test public void tripNotFoundInPattern() { // non-existing trip diff --git a/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java b/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java index e01809d2b9f..2a4f2dfb701 100644 --- a/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java +++ b/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java @@ -229,9 +229,13 @@ public StopPattern stopPattern(int numberOfStops) { } public static StopPattern stopPattern(RegularStop... stops) { - var builder = StopPattern.create(stops.length); - for (int i = 0; i < stops.length; i++) { - builder.stops.with(i, stops[i]); + return stopPattern(Arrays.asList(stops)); + } + + public static StopPattern stopPattern(List stops) { + var builder = StopPattern.create(stops.size()); + for (int i = 0; i < stops.size(); i++) { + builder.stops.with(i, stops.get(i)); builder.pickups.with(i, PickDrop.SCHEDULED); builder.dropoffs.with(i, PickDrop.SCHEDULED); } diff --git a/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java b/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java index d92aad0e159..1658505aa51 100644 --- a/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java +++ b/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java @@ -73,7 +73,7 @@ void replaceStop() { assertEquals(List.of(s1, s2, s3), pattern.getStops()); - var updated = pattern.mutate().replaceStop(s2.getId(), s4).build(); + var updated = pattern.copyOf().replaceStop(s2.getId(), s4).build(); assertEquals(List.of(s1, s4, s3), updated.getStops()); } } diff --git a/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java b/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java index 8c28290de66..eeb7368091c 100644 --- a/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java @@ -7,7 +7,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_SERVICE_ON_DATE; @@ -97,15 +96,6 @@ public void testGetSnapshot() throws InvalidProtocolBufferException { final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); assertNotNull(snapshot); assertSame(snapshot, updater.getTimetableSnapshot()); - - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - fullDataset, - List.of(TripUpdate.parseFrom(cancellation)), - feedId - ); - assertSame(snapshot, updater.getTimetableSnapshot()); } @Test @@ -140,11 +130,7 @@ public void testHandleCanceledTrip() throws InvalidProtocolBufferException { final int tripIndex = pattern.getScheduledTimetable().getTripIndex(tripId); final int tripIndex2 = pattern.getScheduledTimetable().getTripIndex(tripId2); - var updater = new TimetableSnapshotSource( - TimetableSnapshotSourceParameters.DEFAULT, - transitModel, - () -> SERVICE_DATE - ); + var updater = defaultUpdater(); updater.applyTripUpdates( TRIP_MATCHER_NOOP, @@ -231,7 +217,7 @@ public void invalidTripId() { tripUpdateBuilder.setTrip(tripDescriptorBuilder); var tripUpdate = tripUpdateBuilder.build(); - updater.applyTripUpdates( + var result = updater.applyTripUpdates( TRIP_MATCHER_NOOP, REQUIRED_NO_DATA, fullDataset, @@ -239,8 +225,7 @@ public void invalidTripId() { feedId ); - var snapshot = updater.getTimetableSnapshot(); - assertNull(snapshot); + assertEquals(0, result.successful()); }); } @@ -611,7 +596,8 @@ public void invalidTripDate() { // THEN final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); - assertNull(snapshot); + assertTrue(snapshot.isEmpty()); + assertFalse(snapshot.isDirty()); assertEquals(1, result.failed()); var errors = result.failures(); assertEquals(1, errors.get(NO_SERVICE_ON_DATE).size()); @@ -1065,7 +1051,7 @@ public void repeatedlyAddedTripWithNewRoute() { @Nonnull private TimetableSnapshotSource defaultUpdater() { return new TimetableSnapshotSource( - TimetableSnapshotSourceParameters.DEFAULT, + new TimetableSnapshotSourceParameters(Duration.ZERO, true), transitModel, () -> SERVICE_DATE ); @@ -1145,6 +1131,7 @@ public void testPurgeExpiredData( List.of(tripUpdateYesterday), feedId ); + updater.commitTimetableSnapshot(true); final TimetableSnapshot snapshotA = updater.getTimetableSnapshot(); diff --git a/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoaderTest.java b/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoaderTest.java index 9a0dbcdfe87..836d1ef3124 100644 --- a/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoaderTest.java +++ b/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFeedLoaderTest.java @@ -13,21 +13,21 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import org.entur.gbfs.v2_3.free_bike_status.GBFSFreeBikeStatus; -import org.entur.gbfs.v2_3.geofencing_zones.GBFSGeofencingZones; -import org.entur.gbfs.v2_3.station_information.GBFSStation; -import org.entur.gbfs.v2_3.station_information.GBFSStationInformation; -import org.entur.gbfs.v2_3.station_status.GBFSStationStatus; -import org.entur.gbfs.v2_3.system_alerts.GBFSSystemAlerts; -import org.entur.gbfs.v2_3.system_calendar.GBFSSystemCalendar; -import org.entur.gbfs.v2_3.system_hours.GBFSSystemHours; -import org.entur.gbfs.v2_3.system_information.GBFSSystemInformation; -import org.entur.gbfs.v2_3.system_pricing_plans.GBFSSystemPricingPlans; -import org.entur.gbfs.v2_3.system_regions.GBFSSystemRegions; -import org.entur.gbfs.v2_3.vehicle_types.GBFSVehicleType; -import org.entur.gbfs.v2_3.vehicle_types.GBFSVehicleTypes; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.mobilitydata.gbfs.v2_3.free_bike_status.GBFSFreeBikeStatus; +import org.mobilitydata.gbfs.v2_3.geofencing_zones.GBFSGeofencingZones; +import org.mobilitydata.gbfs.v2_3.station_information.GBFSStation; +import org.mobilitydata.gbfs.v2_3.station_information.GBFSStationInformation; +import org.mobilitydata.gbfs.v2_3.station_status.GBFSStationStatus; +import org.mobilitydata.gbfs.v2_3.system_alerts.GBFSSystemAlerts; +import org.mobilitydata.gbfs.v2_3.system_calendar.GBFSSystemCalendar; +import org.mobilitydata.gbfs.v2_3.system_hours.GBFSSystemHours; +import org.mobilitydata.gbfs.v2_3.system_information.GBFSSystemInformation; +import org.mobilitydata.gbfs.v2_3.system_pricing_plans.GBFSSystemPricingPlans; +import org.mobilitydata.gbfs.v2_3.system_regions.GBFSSystemRegions; +import org.mobilitydata.gbfs.v2_3.vehicle_types.GBFSVehicleType; +import org.mobilitydata.gbfs.v2_3.vehicle_types.GBFSVehicleTypes; import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.updater.spi.HttpHeaders; import org.slf4j.LoggerFactory; @@ -181,7 +181,7 @@ private void validateV22Feed(GbfsFeedLoader loader) { GBFSStationStatus stationStatus = loader.getFeed(GBFSStationStatus.class); assertNotNull(stationStatus); - List stationStatuses = stationStatus + List stationStatuses = stationStatus .getData() .getStations(); assertEquals(6, stationStatuses.size()); @@ -228,7 +228,7 @@ private void validateV10Feed(GbfsFeedLoader loader) { GBFSStationStatus stationStatus = loader.getFeed(GBFSStationStatus.class); assertNotNull(stationStatus); - List stationStatuses = stationStatus + List stationStatuses = stationStatus .getData() .getStations(); assertEquals(10, stationStatuses.size()); diff --git a/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapperTest.java b/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapperTest.java index 8e5b900a401..02295ce09e9 100644 --- a/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapperTest.java +++ b/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsFreeVehicleStatusMapperTest.java @@ -3,8 +3,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.Map; -import org.entur.gbfs.v2_3.free_bike_status.GBFSBike; import org.junit.jupiter.api.Test; +import org.mobilitydata.gbfs.v2_3.free_bike_status.GBFSBike; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; import org.opentripplanner.service.vehiclerental.model.VehicleRentalSystem; import org.opentripplanner.street.model.RentalFormFactor; diff --git a/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSourceTest.java b/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSourceTest.java index 018a3d06e5b..7b062bcc17c 100644 --- a/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSourceTest.java +++ b/src/test/java/org/opentripplanner/updater/vehicle_rental/datasources/GbfsVehicleRentalDataSourceTest.java @@ -8,8 +8,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import org.entur.gbfs.v2_3.vehicle_types.GBFSVehicleType; import org.junit.jupiter.api.Test; +import org.mobilitydata.gbfs.v2_3.vehicle_types.GBFSVehicleType; import org.opentripplanner.framework.io.OtpHttpClientFactory; import org.opentripplanner.service.vehiclerental.model.GeofencingZone; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; diff --git a/src/test/resources/mmri/1a/agency.txt b/src/test/resources/mmri/1a/agency.txt deleted file mode 100644 index d6f92d6af7f..00000000000 --- a/src/test/resources/mmri/1a/agency.txt +++ /dev/null @@ -1,2 +0,0 @@ -agency_id,agency_name,agency_url,agency_timezone,agency_lang -MMRI,Multimodale Reisinformatie,http://mmri.nl/,Europe/Amsterdam,nl diff --git a/src/test/resources/mmri/1a/calendar_dates.txt b/src/test/resources/mmri/1a/calendar_dates.txt deleted file mode 100644 index bc0abe90518..00000000000 --- a/src/test/resources/mmri/1a/calendar_dates.txt +++ /dev/null @@ -1,4 +0,0 @@ -date,service_id,exception_type -20140101,1a|bus|1,1 -20140101,1a|ferry|1,1 -20140101,1a|train|1,1 diff --git a/src/test/resources/mmri/1a/routes.txt b/src/test/resources/mmri/1a/routes.txt deleted file mode 100644 index 130126398bf..00000000000 --- a/src/test/resources/mmri/1a/routes.txt +++ /dev/null @@ -1,4 +0,0 @@ -agency_id,route_id,route_short_name,route_long_name,route_type -MMRI,1a|bus,bus,,3 -MMRI,1a|ferry,ferry,,4 -MMRI,1a|train,train,,2 diff --git a/src/test/resources/mmri/1a/stop_times.txt b/src/test/resources/mmri/1a/stop_times.txt deleted file mode 100644 index 280a530103d..00000000000 --- a/src/test/resources/mmri/1a/stop_times.txt +++ /dev/null @@ -1,7 +0,0 @@ -trip_id,arrival_time,departure_time,stop_id,stop_sequence -1a|bus|1,00:01:00,00:01:00,1a1,1 -1a|bus|1,00:02:00,00:02:00,1a2,2 -1a|ferry|1,00:03:00,00:03:00,1a3,3 -1a|ferry|1,00:04:00,00:04:00,1a4,4 -1a|train|1,00:05:00,00:05:00,1a5,5 -1a|train|1,00:06:00,00:06:00,1a6,6 diff --git a/src/test/resources/mmri/1a/stops.txt b/src/test/resources/mmri/1a/stops.txt deleted file mode 100644 index 11b92589dd2..00000000000 --- a/src/test/resources/mmri/1a/stops.txt +++ /dev/null @@ -1,7 +0,0 @@ -stop_id,stop_name,stop_lat,stop_lon -1a1,Stop 1a1,1.101,1.1 -1a2,Stop 1a2,1.102,1.1 -1a3,Stop 1a3,1.103,1.1 -1a4,Stop 1a4,1.104,1.1 -1a5,Stop 1a5,1.105,1.1 -1a6,Stop 1a6,1.106,1.1 diff --git a/src/test/resources/mmri/1a/transfers.txt b/src/test/resources/mmri/1a/transfers.txt deleted file mode 100644 index 9bdfd232de9..00000000000 --- a/src/test/resources/mmri/1a/transfers.txt +++ /dev/null @@ -1,3 +0,0 @@ -from_stop_id,to_stop_id,transfer_type,min_transfer_time -1a2,1a3,2,0 -1a4,1a5,2,0 diff --git a/src/test/resources/mmri/1a/trips.txt b/src/test/resources/mmri/1a/trips.txt deleted file mode 100644 index 4573658c9a5..00000000000 --- a/src/test/resources/mmri/1a/trips.txt +++ /dev/null @@ -1,4 +0,0 @@ -route_id,service_id,trip_id -1a|bus,1a|bus|1,1a|bus|1 -1a|ferry,1a|ferry|1,1a|ferry|1 -1a|train,1a|train|1,1a|train|1 diff --git a/src/test/resources/mmri/2a2/agency.txt b/src/test/resources/mmri/2a2/agency.txt deleted file mode 100644 index d6f92d6af7f..00000000000 --- a/src/test/resources/mmri/2a2/agency.txt +++ /dev/null @@ -1,2 +0,0 @@ -agency_id,agency_name,agency_url,agency_timezone,agency_lang -MMRI,Multimodale Reisinformatie,http://mmri.nl/,Europe/Amsterdam,nl diff --git a/src/test/resources/mmri/2a2/calendar_dates.txt b/src/test/resources/mmri/2a2/calendar_dates.txt deleted file mode 100644 index 2461c7fe35f..00000000000 --- a/src/test/resources/mmri/2a2/calendar_dates.txt +++ /dev/null @@ -1,2 +0,0 @@ -date,service_id,exception_type -20140101,ignore,1 diff --git a/src/test/resources/mmri/2a2/routes.txt b/src/test/resources/mmri/2a2/routes.txt deleted file mode 100644 index f8da6695081..00000000000 --- a/src/test/resources/mmri/2a2/routes.txt +++ /dev/null @@ -1,3 +0,0 @@ -agency_id,route_id,route_short_name,route_long_name,route_type -MMRI,2a2|bus|1,bus 1,,3 -MMRI,2a2|bus|2,bus 2,,3 diff --git a/src/test/resources/mmri/2a2/stop_times.txt b/src/test/resources/mmri/2a2/stop_times.txt deleted file mode 100644 index a3bb8933370..00000000000 --- a/src/test/resources/mmri/2a2/stop_times.txt +++ /dev/null @@ -1,15 +0,0 @@ -trip_id,arrival_time,departure_time,stop_id,stop_sequence -2a2|bus|1|1,00:01:00,00:01:00,2a3,1 -2a2|bus|1|1,00:02:00,00:02:00,2a4,2 -2a2|bus|1|2,00:04:00,00:04:00,2a3,1 -2a2|bus|1|2,00:05:00,00:05:00,2a4,2 -2a2|bus|1|3,00:07:00,00:07:00,2a3,1 -2a2|bus|1|3,00:08:00,00:08:00,2a4,2 -2a2|bus|2|1,00:02:00,00:02:00,2a5,1 -2a2|bus|2|1,00:03:00,00:03:00,2a6,2 -2a2|bus|2|2,00:05:00,00:05:00,2a5,1 -2a2|bus|2|2,00:06:00,00:06:00,2a6,2 -2a2|bus|2|3,00:08:00,00:08:00,2a5,1 -2a2|bus|2|3,00:09:00,00:09:00,2a6,2 -2a2|bus|2|4,00:10:00,00:10:00,2a5,1 -2a2|bus|2|4,00:11:00,00:11:00,2a6,2 \ No newline at end of file diff --git a/src/test/resources/mmri/2a2/stops.txt b/src/test/resources/mmri/2a2/stops.txt deleted file mode 100644 index c3c457bc753..00000000000 --- a/src/test/resources/mmri/2a2/stops.txt +++ /dev/null @@ -1,5 +0,0 @@ -stop_id,stop_name,stop_lat,stop_lon -2a3,Stop 2a3,2.103,2.102 -2a4,Stop 2a4,2.104,2.102 -2a5,Stop 2a5,2.105,2.102 -2a6,Stop 2a6,2.106,2.102 diff --git a/src/test/resources/mmri/2a2/transfers.txt b/src/test/resources/mmri/2a2/transfers.txt deleted file mode 100644 index 4ec2f4adf41..00000000000 --- a/src/test/resources/mmri/2a2/transfers.txt +++ /dev/null @@ -1,2 +0,0 @@ -from_stop_id,to_stop_id,transfer_type,min_transfer_time -2a4,2a5,2,300 diff --git a/src/test/resources/mmri/2a2/trips.txt b/src/test/resources/mmri/2a2/trips.txt deleted file mode 100644 index 1a8f2a56fdc..00000000000 --- a/src/test/resources/mmri/2a2/trips.txt +++ /dev/null @@ -1,8 +0,0 @@ -route_id,service_id,trip_id -2a2|bus|1,ignore,2a2|bus|1|1 -2a2|bus|1,ignore,2a2|bus|1|2 -2a2|bus|1,ignore,2a2|bus|1|3 -2a2|bus|2,ignore,2a2|bus|2|1 -2a2|bus|2,ignore,2a2|bus|2|2 -2a2|bus|2,ignore,2a2|bus|2|3 -2a2|bus|2,ignore,2a2|bus|2|4 \ No newline at end of file diff --git a/src/test/resources/mmri/2f/agency.txt b/src/test/resources/mmri/2f/agency.txt deleted file mode 100644 index d6f92d6af7f..00000000000 --- a/src/test/resources/mmri/2f/agency.txt +++ /dev/null @@ -1,2 +0,0 @@ -agency_id,agency_name,agency_url,agency_timezone,agency_lang -MMRI,Multimodale Reisinformatie,http://mmri.nl/,Europe/Amsterdam,nl diff --git a/src/test/resources/mmri/2f/calendar_dates.txt b/src/test/resources/mmri/2f/calendar_dates.txt deleted file mode 100644 index 2461c7fe35f..00000000000 --- a/src/test/resources/mmri/2f/calendar_dates.txt +++ /dev/null @@ -1,2 +0,0 @@ -date,service_id,exception_type -20140101,ignore,1 diff --git a/src/test/resources/mmri/2f/routes.txt b/src/test/resources/mmri/2f/routes.txt deleted file mode 100644 index 5875e0081d4..00000000000 --- a/src/test/resources/mmri/2f/routes.txt +++ /dev/null @@ -1,4 +0,0 @@ -agency_id,route_id,route_short_name,route_long_name,route_type -MMRI,2f|intercity,,Intercity,2 -MMRI,2f|slt|1,,Local train 1,2 -MMRI,2f|slt|2,,Local train 2,2 diff --git a/src/test/resources/mmri/2f/stop_times.txt b/src/test/resources/mmri/2f/stop_times.txt deleted file mode 100644 index 559d84d992f..00000000000 --- a/src/test/resources/mmri/2f/stop_times.txt +++ /dev/null @@ -1,9 +0,0 @@ -trip_id,arrival_time,departure_time,stop_id,stop_sequence -2f|intercity,00:01:00,00:01:00,2f1,1 -2f|intercity,00:04:00,00:04:00,2f3,2 -2f|slt|1,00:01:00,00:01:00,2f1,1 -2f|slt|1,00:03:00,00:03:00,2f2,2 -2f|slt|1,00:05:00,00:05:00,2f3,3 -2f|slt|2,00:06:00,00:06:00,2f3,1 -2f|slt|2,00:09:00,00:09:00,2f2,2 -2f|slt|2,00:11:00,00:11:00,2f1,3 diff --git a/src/test/resources/mmri/2f/stops.txt b/src/test/resources/mmri/2f/stops.txt deleted file mode 100644 index 538a3240e55..00000000000 --- a/src/test/resources/mmri/2f/stops.txt +++ /dev/null @@ -1,4 +0,0 @@ -stop_id,stop_name,stop_lat,stop_lon -2f1,Stop 2f1,2.701,2.700 -2f2,Stop 2f2,2.702,2.700 -2f3,Stop 2f3,2.703,2.700 diff --git a/src/test/resources/mmri/2f/trips.txt b/src/test/resources/mmri/2f/trips.txt deleted file mode 100644 index f9dba48f134..00000000000 --- a/src/test/resources/mmri/2f/trips.txt +++ /dev/null @@ -1,4 +0,0 @@ -route_id,service_id,trip_id -2f|intercity,ignore,2f|intercity -2f|slt|1,ignore,2f|slt|1 -2f|slt|2,ignore,2f|slt|2 diff --git a/src/test/resources/mmri/3f/agency.txt b/src/test/resources/mmri/3f/agency.txt deleted file mode 100644 index d6f92d6af7f..00000000000 --- a/src/test/resources/mmri/3f/agency.txt +++ /dev/null @@ -1,2 +0,0 @@ -agency_id,agency_name,agency_url,agency_timezone,agency_lang -MMRI,Multimodale Reisinformatie,http://mmri.nl/,Europe/Amsterdam,nl diff --git a/src/test/resources/mmri/3f/calendar_dates.txt b/src/test/resources/mmri/3f/calendar_dates.txt deleted file mode 100644 index 2461c7fe35f..00000000000 --- a/src/test/resources/mmri/3f/calendar_dates.txt +++ /dev/null @@ -1,2 +0,0 @@ -date,service_id,exception_type -20140101,ignore,1 diff --git a/src/test/resources/mmri/3f/routes.txt b/src/test/resources/mmri/3f/routes.txt deleted file mode 100644 index 33917fc7217..00000000000 --- a/src/test/resources/mmri/3f/routes.txt +++ /dev/null @@ -1,3 +0,0 @@ -agency_id,route_id,route_short_name,route_long_name,route_type -MMRI,3f|1,,Intercity 1,2 -MMRI,3f|2,,Intercity 2,2 diff --git a/src/test/resources/mmri/3f/stop_times.txt b/src/test/resources/mmri/3f/stop_times.txt deleted file mode 100644 index 563936db238..00000000000 --- a/src/test/resources/mmri/3f/stop_times.txt +++ /dev/null @@ -1,7 +0,0 @@ -trip_id,arrival_time,departure_time,stop_id,stop_sequence -3f|1,00:01:00,00:01:00,3f1,1 -3f|1,00:02:00,00:02:00,3f2,2 -3f|1,00:03:00,00:03:00,3f3,3 -3f|2,00:01:00,00:01:00,3f1,1 -3f|2,00:02:00,00:02:00,3f4,2 -3f|2,00:04:00,00:04:00,3f3,3 diff --git a/src/test/resources/mmri/3f/stops.txt b/src/test/resources/mmri/3f/stops.txt deleted file mode 100644 index 7ccbea9059f..00000000000 --- a/src/test/resources/mmri/3f/stops.txt +++ /dev/null @@ -1,5 +0,0 @@ -stop_id,stop_name,stop_lat,stop_lon -3f1,Stop 3f1,3.601,3.602 -3f2,Stop 3f2,3.602,3.601 -3f3,Stop 3f3,3.603,3.602 -3f4,Stop 3f4,3.602,3.603 diff --git a/src/test/resources/mmri/3f/trips.txt b/src/test/resources/mmri/3f/trips.txt deleted file mode 100644 index c16ec0732bb..00000000000 --- a/src/test/resources/mmri/3f/trips.txt +++ /dev/null @@ -1,3 +0,0 @@ -route_id,service_id,trip_id -3f|1,ignore,3f|1 -3f|2,ignore,3f|2 diff --git a/src/test/resources/mmri/3i.pb b/src/test/resources/mmri/3i.pb deleted file mode 100644 index f03fd3dc6bf..00000000000 Binary files a/src/test/resources/mmri/3i.pb and /dev/null differ diff --git a/src/test/resources/mmri/3i/agency.txt b/src/test/resources/mmri/3i/agency.txt deleted file mode 100644 index d6f92d6af7f..00000000000 --- a/src/test/resources/mmri/3i/agency.txt +++ /dev/null @@ -1,2 +0,0 @@ -agency_id,agency_name,agency_url,agency_timezone,agency_lang -MMRI,Multimodale Reisinformatie,http://mmri.nl/,Europe/Amsterdam,nl diff --git a/src/test/resources/mmri/3i/calendar_dates.txt b/src/test/resources/mmri/3i/calendar_dates.txt deleted file mode 100644 index 2461c7fe35f..00000000000 --- a/src/test/resources/mmri/3i/calendar_dates.txt +++ /dev/null @@ -1,2 +0,0 @@ -date,service_id,exception_type -20140101,ignore,1 diff --git a/src/test/resources/mmri/3i/routes.txt b/src/test/resources/mmri/3i/routes.txt deleted file mode 100644 index a70c8bf0979..00000000000 --- a/src/test/resources/mmri/3i/routes.txt +++ /dev/null @@ -1,2 +0,0 @@ -agency_id,route_id,route_short_name,route_long_name,route_type -MMRI,3i|1,bus 1,,3 \ No newline at end of file diff --git a/src/test/resources/mmri/3i/stop_times.txt b/src/test/resources/mmri/3i/stop_times.txt deleted file mode 100644 index 6954a3066aa..00000000000 --- a/src/test/resources/mmri/3i/stop_times.txt +++ /dev/null @@ -1,3 +0,0 @@ -trip_id,arrival_time,departure_time,stop_id,stop_sequence -3i|1,00:01:00,00:01:00,3i1,1 -3i|1,00:02:00,00:02:00,3i2,2 \ No newline at end of file diff --git a/src/test/resources/mmri/3i/stops.txt b/src/test/resources/mmri/3i/stops.txt deleted file mode 100644 index 62645f565ee..00000000000 --- a/src/test/resources/mmri/3i/stops.txt +++ /dev/null @@ -1,3 +0,0 @@ -stop_id,stop_name,stop_lat,stop_lon -3i1,Stop 3i1,3.801,3.8 -3i2,Stop 3i2,3.802,3.8 diff --git a/src/test/resources/mmri/3i/trips.txt b/src/test/resources/mmri/3i/trips.txt deleted file mode 100644 index 1269810469d..00000000000 --- a/src/test/resources/mmri/3i/trips.txt +++ /dev/null @@ -1,2 +0,0 @@ -route_id,service_id,trip_id -3i|1,ignore,3i|1 \ No newline at end of file diff --git a/src/test/resources/mmri/plannerstack_scenario.pb b/src/test/resources/mmri/plannerstack_scenario.pb deleted file mode 100644 index 810f9b3e6f1..00000000000 Binary files a/src/test/resources/mmri/plannerstack_scenario.pb and /dev/null differ diff --git a/src/test/resources/mmri/plannerstack_scenario/agency.txt b/src/test/resources/mmri/plannerstack_scenario/agency.txt deleted file mode 100644 index d6f92d6af7f..00000000000 --- a/src/test/resources/mmri/plannerstack_scenario/agency.txt +++ /dev/null @@ -1,2 +0,0 @@ -agency_id,agency_name,agency_url,agency_timezone,agency_lang -MMRI,Multimodale Reisinformatie,http://mmri.nl/,Europe/Amsterdam,nl diff --git a/src/test/resources/mmri/plannerstack_scenario/calendar_dates.txt b/src/test/resources/mmri/plannerstack_scenario/calendar_dates.txt deleted file mode 100644 index 2461c7fe35f..00000000000 --- a/src/test/resources/mmri/plannerstack_scenario/calendar_dates.txt +++ /dev/null @@ -1,2 +0,0 @@ -date,service_id,exception_type -20140101,ignore,1 diff --git a/src/test/resources/mmri/plannerstack_scenario/routes.txt b/src/test/resources/mmri/plannerstack_scenario/routes.txt deleted file mode 100644 index ea764d3d770..00000000000 --- a/src/test/resources/mmri/plannerstack_scenario/routes.txt +++ /dev/null @@ -1,4 +0,0 @@ -agency_id,route_id,route_short_name,route_long_name,route_type -MMRI,plannerstack_scenario|intercity,,Intercity,2 -MMRI,plannerstack_scenario|slt|1,,Local train 1,2 -MMRI,plannerstack_scenario|slt|2,,Local train 2,2 diff --git a/src/test/resources/mmri/plannerstack_scenario/stop_times.txt b/src/test/resources/mmri/plannerstack_scenario/stop_times.txt deleted file mode 100644 index 7c67a1ef04c..00000000000 --- a/src/test/resources/mmri/plannerstack_scenario/stop_times.txt +++ /dev/null @@ -1,12 +0,0 @@ -trip_id,arrival_time,departure_time,stop_id,stop_sequence -plannerstack_scenario|intercity,00:01:00,00:01:00,plannerstack_scenario1,1 -plannerstack_scenario|intercity,00:04:00,00:04:00,plannerstack_scenario3,2 -plannerstack_scenario|slt|1,00:01:00,00:01:00,plannerstack_scenario1,1 -plannerstack_scenario|slt|1,00:03:00,00:03:00,plannerstack_scenario2,2 -plannerstack_scenario|slt|1,00:05:00,00:05:00,plannerstack_scenario3,3 -plannerstack_scenario|slt|2,00:06:00,00:06:00,plannerstack_scenario3,1 -plannerstack_scenario|slt|2,00:09:00,00:09:00,plannerstack_scenario2,2 -plannerstack_scenario|slt|2,00:11:00,00:11:00,plannerstack_scenario1,3 -plannerstack_scenario|slt|3,00:10:00,00:10:00,plannerstack_scenario3,1 -plannerstack_scenario|slt|3,00:14:00,00:14:00,plannerstack_scenario2,2 -plannerstack_scenario|slt|3,00:16:00,00:16:00,plannerstack_scenario1,3 \ No newline at end of file diff --git a/src/test/resources/mmri/plannerstack_scenario/stops.txt b/src/test/resources/mmri/plannerstack_scenario/stops.txt deleted file mode 100644 index 4dac741a416..00000000000 --- a/src/test/resources/mmri/plannerstack_scenario/stops.txt +++ /dev/null @@ -1,4 +0,0 @@ -stop_id,stop_name,stop_lat,stop_lon -plannerstack_scenario1,Stop plannerstack_scenario1,2.701,2.700 -plannerstack_scenario2,Stop plannerstack_scenario2,2.702,2.700 -plannerstack_scenario3,Stop plannerstack_scenario3,2.703,2.700 diff --git a/src/test/resources/mmri/plannerstack_scenario/trips.txt b/src/test/resources/mmri/plannerstack_scenario/trips.txt deleted file mode 100644 index ce1cd4690a2..00000000000 --- a/src/test/resources/mmri/plannerstack_scenario/trips.txt +++ /dev/null @@ -1,5 +0,0 @@ -route_id,service_id,trip_id -plannerstack_scenario|intercity,ignore,plannerstack_scenario|intercity -plannerstack_scenario|slt|1,ignore,plannerstack_scenario|slt|1 -plannerstack_scenario|slt|2,ignore,plannerstack_scenario|slt|2 -plannerstack_scenario|slt|2,ignore,plannerstack_scenario|slt|3 diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/feedinfo.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/feedinfo.json new file mode 100644 index 00000000000..3cf3bd06d66 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/feedinfo.json @@ -0,0 +1,18 @@ +{ + "data" : { + "feeds" : [ + { + "agencies" : [ + { + "name" : "speedtransit", + "url" : "www.otp-foo.bar" + } + ], + "publisher" : { + "name" : "publisher", + "url" : "www.z.org" + } + } + ] + } +} \ No newline at end of file diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/feedinfo.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/feedinfo.graphql new file mode 100644 index 00000000000..68516d26237 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/feedinfo.graphql @@ -0,0 +1,12 @@ +{ + feeds { + agencies { + name + url + } + publisher { + name + url + } + } +} \ No newline at end of file