feat: add route selection functionality and improve station handling
- Added `vitest` for testing and created initial tests for route utilities. - Implemented route selection logic in the App component, allowing users to select start and end stations. - Updated the NetworkMap component to reflect focused and selected stations, including visual indicators for start and end stations. - Enhanced the route panel UI to display selected route information and estimated lengths. - Introduced utility functions for building track adjacency and computing routes based on selected stations. - Improved styling for route selection and station list items to enhance user experience.
This commit is contained in:
6
TODO.md
6
TODO.md
@@ -16,12 +16,12 @@
|
|||||||
- [x] Define geographic bounding boxes and filtering rules for importing real-world stations from OpenStreetMap.
|
- [x] Define geographic bounding boxes and filtering rules for importing real-world stations from OpenStreetMap.
|
||||||
- [x] Implement an import script/CLI that pulls OSM station data and normalizes it to the PostGIS schema.
|
- [x] Implement an import script/CLI that pulls OSM station data and normalizes it to the PostGIS schema.
|
||||||
- [x] Expose backend CRUD endpoints for stations (create, update, archive) with validation and geometry handling.
|
- [x] Expose backend CRUD endpoints for stations (create, update, archive) with validation and geometry handling.
|
||||||
- [ ] Build React map tooling for selecting a station.
|
- [x] Build React map tooling for selecting a station.
|
||||||
- [ ] Build tools for station editing, including form validation.
|
- [ ] Enhance map UI to support selecting two stations and previewing the rail corridor between them.
|
||||||
- [ ] Define track selection criteria and tagging rules for harvesting OSM rail segments within target regions.
|
- [ ] Define track selection criteria and tagging rules for harvesting OSM rail segments within target regions.
|
||||||
- [ ] Extend the importer to load track geometries and associate them with existing stations.
|
- [ ] Extend the importer to load track geometries and associate them with existing stations.
|
||||||
- [ ] Implement backend track-management APIs with length/speed validation and topology checks.
|
- [ ] Implement backend track-management APIs with length/speed validation and topology checks.
|
||||||
- [ ] Create a frontend track-drawing workflow (polyline editor, snapping to stations, undo/redo).
|
- [ ] Implement track path mapping along existing OSM rail segments between chosen stations.
|
||||||
- [ ] Design train connection manager requirements (link trains to operating tracks, manage consist data).
|
- [ ] Design train connection manager requirements (link trains to operating tracks, manage consist data).
|
||||||
- [ ] Implement backend services and APIs to attach trains to routes and update assignments.
|
- [ ] Implement backend services and APIs to attach trains to routes and update assignments.
|
||||||
- [ ] Add UI flows for managing train connections, including visual feedback on the map.
|
- [ ] Add UI flows for managing train connections, including visual feedback on the map.
|
||||||
|
|||||||
711
frontend/package-lock.json
generated
711
frontend/package-lock.json
generated
@@ -30,7 +30,8 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"vite": "^5.4.0"
|
"vite": "^5.4.0",
|
||||||
|
"vitest": "^1.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@@ -885,6 +886,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/@jest/schemas": {
|
||||||
|
"version": "29.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
|
||||||
|
"integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sinclair/typebox": "^0.27.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
@@ -1322,6 +1336,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@sinclair/typebox": {
|
||||||
|
"version": "0.27.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||||
|
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -1699,6 +1720,109 @@
|
|||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/expect": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/spy": "1.6.1",
|
||||||
|
"@vitest/utils": "1.6.1",
|
||||||
|
"chai": "^4.3.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/runner": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/utils": "1.6.1",
|
||||||
|
"p-limit": "^5.0.0",
|
||||||
|
"pathe": "^1.1.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/runner/node_modules/p-limit": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"yocto-queue": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/runner/node_modules/yocto-queue": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/snapshot": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"magic-string": "^0.30.5",
|
||||||
|
"pathe": "^1.1.1",
|
||||||
|
"pretty-format": "^29.7.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/spy": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyspy": "^2.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/utils": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"diff-sequences": "^29.6.3",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"loupe": "^2.3.7",
|
||||||
|
"pretty-format": "^29.7.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
@@ -1722,6 +1846,19 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/acorn-walk": {
|
||||||
|
"version": "8.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||||
|
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
@@ -1942,6 +2079,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/assertion-error": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ast-types-flow": {
|
"node_modules/ast-types-flow": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
||||||
@@ -2069,6 +2216,16 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cac": {
|
||||||
|
"version": "6.7.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||||
|
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||||
@@ -2150,6 +2307,25 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/chai": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"assertion-error": "^1.1.0",
|
||||||
|
"check-error": "^1.0.3",
|
||||||
|
"deep-eql": "^4.1.3",
|
||||||
|
"get-func-name": "^2.0.2",
|
||||||
|
"loupe": "^2.3.6",
|
||||||
|
"pathval": "^1.1.1",
|
||||||
|
"type-detect": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -2167,6 +2343,19 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/check-error": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-func-name": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -2194,6 +2383,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/confbox": {
|
||||||
|
"version": "0.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
|
||||||
|
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/convert-source-map": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
@@ -2302,6 +2498,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/deep-eql": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"type-detect": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
@@ -2345,6 +2554,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/diff-sequences": {
|
||||||
|
"version": "29.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
|
||||||
|
"integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/doctrine": {
|
"node_modules/doctrine": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
@@ -3120,6 +3339,16 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esutils": {
|
"node_modules/esutils": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
@@ -3130,6 +3359,30 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/execa": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.3",
|
||||||
|
"get-stream": "^8.0.1",
|
||||||
|
"human-signals": "^5.0.0",
|
||||||
|
"is-stream": "^3.0.0",
|
||||||
|
"merge-stream": "^2.0.0",
|
||||||
|
"npm-run-path": "^5.1.0",
|
||||||
|
"onetime": "^6.0.0",
|
||||||
|
"signal-exit": "^4.1.0",
|
||||||
|
"strip-final-newline": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -3355,6 +3608,16 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-func-name": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
@@ -3394,6 +3657,19 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-stream": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-symbol-description": {
|
"node_modules/get-symbol-description": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
|
||||||
@@ -3618,6 +3894,16 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/human-signals": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "7.0.5",
|
"version": "7.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||||
@@ -3994,6 +4280,19 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-stream": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-string": {
|
"node_modules/is-string": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
|
||||||
@@ -4255,6 +4554,23 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/local-pkg": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mlly": "^1.7.3",
|
||||||
|
"pkg-types": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
@@ -4290,6 +4606,16 @@
|
|||||||
"loose-envify": "cli.js"
|
"loose-envify": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/loupe": {
|
||||||
|
"version": "2.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
|
||||||
|
"integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-func-name": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -4300,6 +4626,16 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/magic-string": {
|
||||||
|
"version": "0.30.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
||||||
|
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -4310,6 +4646,13 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/merge-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@@ -4334,6 +4677,19 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-fn": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
@@ -4360,6 +4716,26 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mlly": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.15.0",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"pkg-types": "^1.3.1",
|
||||||
|
"ufo": "^1.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mlly/node_modules/pathe": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -4400,6 +4776,35 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/npm-run-path": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"path-key": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/npm-run-path/node_modules/path-key": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -4533,6 +4938,22 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/onetime": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-fn": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -4651,6 +5072,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pathe": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/pathval": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -4671,6 +5109,25 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pkg-types": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"confbox": "^0.1.8",
|
||||||
|
"mlly": "^1.7.4",
|
||||||
|
"pathe": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pkg-types/node_modules/pathe": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.56.0",
|
"version": "1.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz",
|
||||||
@@ -4783,6 +5240,41 @@
|
|||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pretty-format": {
|
||||||
|
"version": "29.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
||||||
|
"integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jest/schemas": "^29.6.3",
|
||||||
|
"ansi-styles": "^5.0.0",
|
||||||
|
"react-is": "^18.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pretty-format/node_modules/ansi-styles": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pretty-format/node_modules/react-is": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -5276,6 +5768,26 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/siginfo": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/signal-exit": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -5286,6 +5798,20 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stackback": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/std-env": {
|
||||||
|
"version": "3.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
|
||||||
|
"integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/stop-iteration-iterator": {
|
"node_modules/stop-iteration-iterator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||||
@@ -5436,6 +5962,19 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-final-newline": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||||
@@ -5449,6 +5988,26 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-literal": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^9.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-literal/node_modules/js-tokens": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
@@ -5482,6 +6041,33 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tinybench": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tinypool": {
|
||||||
|
"version": "0.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz",
|
||||||
|
"integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyspy": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
@@ -5547,6 +6133,16 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/type-detect": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/type-fest": {
|
"node_modules/type-fest": {
|
||||||
"version": "0.20.2",
|
"version": "0.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||||
@@ -5652,6 +6248,13 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ufo": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||||
@@ -5779,6 +6382,95 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-node": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cac": "^6.7.14",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"pathe": "^1.1.1",
|
||||||
|
"picocolors": "^1.0.0",
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite-node": "vite-node.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.0.0 || >=20.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vitest": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/expect": "1.6.1",
|
||||||
|
"@vitest/runner": "1.6.1",
|
||||||
|
"@vitest/snapshot": "1.6.1",
|
||||||
|
"@vitest/spy": "1.6.1",
|
||||||
|
"@vitest/utils": "1.6.1",
|
||||||
|
"acorn-walk": "^8.3.2",
|
||||||
|
"chai": "^4.3.10",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"execa": "^8.0.1",
|
||||||
|
"local-pkg": "^0.5.0",
|
||||||
|
"magic-string": "^0.30.5",
|
||||||
|
"pathe": "^1.1.1",
|
||||||
|
"picocolors": "^1.0.0",
|
||||||
|
"std-env": "^3.5.0",
|
||||||
|
"strip-literal": "^2.0.0",
|
||||||
|
"tinybench": "^2.5.1",
|
||||||
|
"tinypool": "^0.8.3",
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"vite-node": "1.6.1",
|
||||||
|
"why-is-node-running": "^2.2.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vitest": "vitest.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.0.0 || >=20.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@edge-runtime/vm": "*",
|
||||||
|
"@types/node": "^18.0.0 || >=20.0.0",
|
||||||
|
"@vitest/browser": "1.6.1",
|
||||||
|
"@vitest/ui": "1.6.1",
|
||||||
|
"happy-dom": "*",
|
||||||
|
"jsdom": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@edge-runtime/vm": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/ui": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"happy-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jsdom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@@ -5884,6 +6576,23 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/why-is-node-running": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"siginfo": "^2.0.0",
|
||||||
|
"stackback": "0.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"why-is-node-running": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
||||||
"format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
|
"format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
|
||||||
|
"test": "vitest run",
|
||||||
"test:e2e": "playwright test"
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"vite": "^5.4.0"
|
"vite": "^5.4.0",
|
||||||
|
"vitest": "^1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,114 @@
|
|||||||
import './styles/global.css';
|
import './styles/global.css';
|
||||||
|
|
||||||
|
import type { LatLngExpression } from 'leaflet';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { LoginForm } from './components/auth/LoginForm';
|
import { LoginForm } from './components/auth/LoginForm';
|
||||||
import { NetworkMap } from './components/map/NetworkMap';
|
import { NetworkMap } from './components/map/NetworkMap';
|
||||||
import { useNetworkSnapshot } from './hooks/useNetworkSnapshot';
|
import { useNetworkSnapshot } from './hooks/useNetworkSnapshot';
|
||||||
import { useAuth } from './state/AuthContext';
|
import { useAuth } from './state/AuthContext';
|
||||||
|
import type { Station } from './types/domain';
|
||||||
|
import { buildTrackAdjacency, computeRoute } from './utils/route';
|
||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
const { token, user, status: authStatus, logout } = useAuth();
|
const { token, user, status: authStatus, logout } = useAuth();
|
||||||
const isAuthenticated = authStatus === 'authenticated' && token !== null;
|
const isAuthenticated = authStatus === 'authenticated' && token !== null;
|
||||||
const { data, status, error } = useNetworkSnapshot(isAuthenticated ? token : null);
|
const { data, status, error } = useNetworkSnapshot(isAuthenticated ? token : null);
|
||||||
const [selectedStationId, setSelectedStationId] = useState<string | null>(null);
|
const [focusedStationId, setFocusedStationId] = useState<string | null>(null);
|
||||||
|
const [routeSelection, setRouteSelection] = useState<{
|
||||||
|
startId: string | null;
|
||||||
|
endId: string | null;
|
||||||
|
}>({ startId: null, endId: null });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status !== 'success' || !data?.stations.length) {
|
if (status !== 'success' || !data?.stations.length) {
|
||||||
setSelectedStationId(null);
|
setFocusedStationId(null);
|
||||||
|
setRouteSelection({ startId: null, endId: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!focusedStationId || !hasStation(data.stations, focusedStationId)) {
|
||||||
!selectedStationId ||
|
setFocusedStationId(data.stations[0].id);
|
||||||
!data.stations.some((station) => station.id === selectedStationId)
|
|
||||||
) {
|
|
||||||
setSelectedStationId(data.stations[0].id);
|
|
||||||
}
|
}
|
||||||
}, [status, data, selectedStationId]);
|
}, [status, data, focusedStationId]);
|
||||||
|
|
||||||
const selectedStation = useMemo(() => {
|
useEffect(() => {
|
||||||
if (!data || !selectedStationId) {
|
if (status !== 'success' || !data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRouteSelection((current) => {
|
||||||
|
const startExists = current.startId
|
||||||
|
? hasStation(data.stations, current.startId)
|
||||||
|
: false;
|
||||||
|
const endExists = current.endId
|
||||||
|
? hasStation(data.stations, current.endId)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
startId: startExists ? current.startId : null,
|
||||||
|
endId: endExists ? current.endId : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [status, data]);
|
||||||
|
|
||||||
|
const stationById = useMemo(() => {
|
||||||
|
if (!data) {
|
||||||
|
return new Map<string, Station>();
|
||||||
|
}
|
||||||
|
const lookup = new Map<string, Station>();
|
||||||
|
for (const station of data.stations) {
|
||||||
|
lookup.set(station.id, station);
|
||||||
|
}
|
||||||
|
return lookup;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const trackAdjacency = useMemo(
|
||||||
|
() => buildTrackAdjacency(data ? data.tracks : []),
|
||||||
|
[data]
|
||||||
|
);
|
||||||
|
|
||||||
|
const routeComputation = useMemo(() => {
|
||||||
|
const core = computeRoute({
|
||||||
|
startId: routeSelection.startId,
|
||||||
|
endId: routeSelection.endId,
|
||||||
|
stationById,
|
||||||
|
adjacency: trackAdjacency,
|
||||||
|
});
|
||||||
|
|
||||||
|
const segments = core.stations ? buildSegmentsFromStations(core.stations) : [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...core,
|
||||||
|
segments,
|
||||||
|
};
|
||||||
|
}, [routeSelection, stationById, trackAdjacency]);
|
||||||
|
|
||||||
|
const focusedStation = useMemo(() => {
|
||||||
|
if (!data || !focusedStationId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return data.stations.find((station) => station.id === selectedStationId) ?? null;
|
return stationById.get(focusedStationId) ?? null;
|
||||||
}, [data, selectedStationId]);
|
}, [data, focusedStationId, stationById]);
|
||||||
|
|
||||||
|
const handleStationSelection = (stationId: string) => {
|
||||||
|
setFocusedStationId(stationId);
|
||||||
|
setRouteSelection((current) => {
|
||||||
|
if (!current.startId || (current.startId && current.endId)) {
|
||||||
|
return { startId: stationId, endId: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.startId === stationId) {
|
||||||
|
return { startId: stationId, endId: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { startId: current.startId, endId: stationId };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearRouteSelection = () => {
|
||||||
|
setRouteSelection({ startId: null, endId: null });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
@@ -65,10 +141,71 @@ function App(): JSX.Element {
|
|||||||
<div className="map-wrapper">
|
<div className="map-wrapper">
|
||||||
<NetworkMap
|
<NetworkMap
|
||||||
snapshot={data}
|
snapshot={data}
|
||||||
selectedStationId={selectedStationId}
|
focusedStationId={focusedStationId}
|
||||||
onSelectStation={(id) => setSelectedStationId(id)}
|
startStationId={routeSelection.startId}
|
||||||
|
endStationId={routeSelection.endId}
|
||||||
|
routeSegments={routeComputation.segments}
|
||||||
|
onStationClick={handleStationSelection}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="route-panel">
|
||||||
|
<div className="route-panel__header">
|
||||||
|
<h3>Route Selection</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ghost-button"
|
||||||
|
onClick={clearRouteSelection}
|
||||||
|
disabled={!routeSelection.startId && !routeSelection.endId}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="route-panel__hint">
|
||||||
|
Click a station to set the origin, then click another station to
|
||||||
|
preview the rail corridor between them.
|
||||||
|
</p>
|
||||||
|
<dl className="route-panel__meta">
|
||||||
|
<div>
|
||||||
|
<dt>Origin</dt>
|
||||||
|
<dd>
|
||||||
|
{routeSelection.startId
|
||||||
|
? (stationById.get(routeSelection.startId)?.name ??
|
||||||
|
'Unknown station')
|
||||||
|
: 'Choose a station'}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Destination</dt>
|
||||||
|
<dd>
|
||||||
|
{routeSelection.endId
|
||||||
|
? (stationById.get(routeSelection.endId)?.name ??
|
||||||
|
'Unknown station')
|
||||||
|
: 'Choose a station'}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Estimated Length</dt>
|
||||||
|
<dd>
|
||||||
|
{routeComputation.totalLength !== null
|
||||||
|
? `${(routeComputation.totalLength / 1000).toFixed(2)} km`
|
||||||
|
: 'N/A'}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
{routeComputation.error && (
|
||||||
|
<p className="route-panel__error">{routeComputation.error}</p>
|
||||||
|
)}
|
||||||
|
{!routeComputation.error && routeComputation.stations && (
|
||||||
|
<div className="route-panel__path">
|
||||||
|
<span>Path:</span>
|
||||||
|
<ol>
|
||||||
|
{routeComputation.stations.map((station) => (
|
||||||
|
<li key={`route-station-${station.id}`}>{station.name}</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="grid">
|
<div className="grid">
|
||||||
<div>
|
<div>
|
||||||
<h3>Stations</h3>
|
<h3>Stations</h3>
|
||||||
@@ -78,12 +215,20 @@ function App(): JSX.Element {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`station-list-item${
|
className={`station-list-item${
|
||||||
station.id === selectedStationId
|
station.id === focusedStationId
|
||||||
? ' station-list-item--selected'
|
? ' station-list-item--selected'
|
||||||
: ''
|
: ''
|
||||||
|
}${
|
||||||
|
station.id === routeSelection.startId
|
||||||
|
? ' station-list-item--start'
|
||||||
|
: ''
|
||||||
|
}${
|
||||||
|
station.id === routeSelection.endId
|
||||||
|
? ' station-list-item--end'
|
||||||
|
: ''
|
||||||
}`}
|
}`}
|
||||||
aria-pressed={station.id === selectedStationId}
|
aria-pressed={station.id === focusedStationId}
|
||||||
onClick={() => setSelectedStationId(station.id)}
|
onClick={() => handleStationSelection(station.id)}
|
||||||
>
|
>
|
||||||
<span className="station-list-item__name">
|
<span className="station-list-item__name">
|
||||||
{station.name}
|
{station.name}
|
||||||
@@ -92,6 +237,14 @@ function App(): JSX.Element {
|
|||||||
{station.latitude.toFixed(3)},{' '}
|
{station.latitude.toFixed(3)},{' '}
|
||||||
{station.longitude.toFixed(3)}
|
{station.longitude.toFixed(3)}
|
||||||
</span>
|
</span>
|
||||||
|
{station.id === routeSelection.startId && (
|
||||||
|
<span className="station-list-item__badge">Origin</span>
|
||||||
|
)}
|
||||||
|
{station.id === routeSelection.endId && (
|
||||||
|
<span className="station-list-item__badge">
|
||||||
|
Destination
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -120,43 +273,43 @@ function App(): JSX.Element {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{selectedStation && (
|
{focusedStation && (
|
||||||
<div className="selected-station">
|
<div className="selected-station">
|
||||||
<h3>Selected Station</h3>
|
<h3>Focused Station</h3>
|
||||||
<dl>
|
<dl>
|
||||||
<div>
|
<div>
|
||||||
<dt>Name</dt>
|
<dt>Name</dt>
|
||||||
<dd>{selectedStation.name}</dd>
|
<dd>{focusedStation.name}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Coordinates</dt>
|
<dt>Coordinates</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{selectedStation.latitude.toFixed(5)},{' '}
|
{focusedStation.latitude.toFixed(5)},{' '}
|
||||||
{selectedStation.longitude.toFixed(5)}
|
{focusedStation.longitude.toFixed(5)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
{selectedStation.code && (
|
{focusedStation.code && (
|
||||||
<div>
|
<div>
|
||||||
<dt>Code</dt>
|
<dt>Code</dt>
|
||||||
<dd>{selectedStation.code}</dd>
|
<dd>{focusedStation.code}</dd>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{typeof selectedStation.elevationM === 'number' && (
|
{typeof focusedStation.elevationM === 'number' && (
|
||||||
<div>
|
<div>
|
||||||
<dt>Elevation</dt>
|
<dt>Elevation</dt>
|
||||||
<dd>{selectedStation.elevationM.toFixed(1)} m</dd>
|
<dd>{focusedStation.elevationM.toFixed(1)} m</dd>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedStation.osmId && (
|
{focusedStation.osmId && (
|
||||||
<div>
|
<div>
|
||||||
<dt>OSM ID</dt>
|
<dt>OSM ID</dt>
|
||||||
<dd>{selectedStation.osmId}</dd>
|
<dd>{focusedStation.osmId}</dd>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<dt>Status</dt>
|
<dt>Status</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{(selectedStation.isActive ?? true) ? 'Active' : 'Inactive'}
|
{(focusedStation.isActive ?? true) ? 'Active' : 'Inactive'}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
@@ -172,3 +325,20 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
|
function hasStation(stations: Station[], id: string): boolean {
|
||||||
|
return stations.some((station) => station.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSegmentsFromStations(stations: Station[]): LatLngExpression[][] {
|
||||||
|
const segments: LatLngExpression[][] = [];
|
||||||
|
for (let index = 0; index < stations.length - 1; index += 1) {
|
||||||
|
const current = stations[index];
|
||||||
|
const next = stations[index + 1];
|
||||||
|
segments.push([
|
||||||
|
[current.latitude, current.longitude],
|
||||||
|
[next.latitude, next.longitude],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ import 'leaflet/dist/leaflet.css';
|
|||||||
|
|
||||||
interface NetworkMapProps {
|
interface NetworkMapProps {
|
||||||
readonly snapshot: NetworkSnapshot;
|
readonly snapshot: NetworkSnapshot;
|
||||||
readonly selectedStationId?: string | null;
|
readonly focusedStationId?: string | null;
|
||||||
readonly onSelectStation?: (stationId: string) => void;
|
readonly startStationId?: string | null;
|
||||||
|
readonly endStationId?: string | null;
|
||||||
|
readonly routeSegments?: LatLngExpression[][];
|
||||||
|
readonly onStationClick?: (stationId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StationPosition {
|
interface StationPosition {
|
||||||
@@ -29,8 +32,11 @@ const DEFAULT_CENTER: LatLngExpression = [51.505, -0.09];
|
|||||||
|
|
||||||
export function NetworkMap({
|
export function NetworkMap({
|
||||||
snapshot,
|
snapshot,
|
||||||
selectedStationId,
|
focusedStationId,
|
||||||
onSelectStation,
|
startStationId,
|
||||||
|
endStationId,
|
||||||
|
routeSegments = [],
|
||||||
|
onStationClick,
|
||||||
}: NetworkMapProps): JSX.Element {
|
}: NetworkMapProps): JSX.Element {
|
||||||
const stationPositions = useMemo<StationPosition[]>(() => {
|
const stationPositions = useMemo<StationPosition[]>(() => {
|
||||||
return snapshot.stations.map((station) => ({
|
return snapshot.stations.map((station) => ({
|
||||||
@@ -86,12 +92,12 @@ export function NetworkMap({
|
|||||||
] as LatLngBoundsExpression;
|
] as LatLngBoundsExpression;
|
||||||
}, [stationPositions]);
|
}, [stationPositions]);
|
||||||
|
|
||||||
const selectedPosition = useMemo(() => {
|
const focusedPosition = useMemo(() => {
|
||||||
if (!selectedStationId) {
|
if (!focusedStationId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return stationLookup.get(selectedStationId) ?? null;
|
return stationLookup.get(focusedStationId) ?? null;
|
||||||
}, [selectedStationId, stationLookup]);
|
}, [focusedStationId, stationLookup]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MapContainer
|
<MapContainer
|
||||||
@@ -104,35 +110,52 @@ export function NetworkMap({
|
|||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
/>
|
/>
|
||||||
{selectedPosition ? <StationFocus position={selectedPosition} /> : null}
|
{focusedPosition ? <StationFocus position={focusedPosition} /> : null}
|
||||||
{trackSegments.map((segment, index) => (
|
{trackSegments.map((segment, index) => (
|
||||||
<Polyline
|
<Polyline
|
||||||
key={`track-${index}`}
|
key={`track-${index}`}
|
||||||
positions={segment}
|
positions={segment}
|
||||||
pathOptions={{ color: '#38bdf8', weight: 4 }}
|
pathOptions={{ color: '#334155', weight: 3, opacity: 0.8 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{routeSegments.map((segment, index) => (
|
||||||
|
<Polyline
|
||||||
|
key={`route-${index}`}
|
||||||
|
positions={segment}
|
||||||
|
pathOptions={{ color: '#facc15', weight: 6, opacity: 0.9 }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{stationPositions.map((station) => (
|
{stationPositions.map((station) => (
|
||||||
<CircleMarker
|
<CircleMarker
|
||||||
key={station.id}
|
key={station.id}
|
||||||
center={station.position}
|
center={station.position}
|
||||||
radius={station.id === selectedStationId ? 9 : 6}
|
radius={station.id === focusedStationId ? 9 : 6}
|
||||||
pathOptions={{
|
pathOptions={{
|
||||||
color: station.id === selectedStationId ? '#34d399' : '#f97316',
|
color: resolveMarkerStroke(
|
||||||
fillColor: station.id === selectedStationId ? '#6ee7b7' : '#fed7aa',
|
station.id,
|
||||||
fillOpacity: 0.95,
|
startStationId,
|
||||||
weight: station.id === selectedStationId ? 3 : 1,
|
endStationId,
|
||||||
|
focusedStationId
|
||||||
|
),
|
||||||
|
fillColor: resolveMarkerFill(
|
||||||
|
station.id,
|
||||||
|
startStationId,
|
||||||
|
endStationId,
|
||||||
|
focusedStationId
|
||||||
|
),
|
||||||
|
fillOpacity: 0.96,
|
||||||
|
weight: station.id === focusedStationId ? 3 : 1,
|
||||||
}}
|
}}
|
||||||
eventHandlers={{
|
eventHandlers={{
|
||||||
click: () => {
|
click: () => {
|
||||||
onSelectStation?.(station.id);
|
onStationClick?.(station.id);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
direction="top"
|
direction="top"
|
||||||
offset={[0, -8]}
|
offset={[0, -8]}
|
||||||
permanent={station.id === selectedStationId}
|
permanent={station.id === focusedStationId}
|
||||||
sticky
|
sticky
|
||||||
>
|
>
|
||||||
{station.name}
|
{station.name}
|
||||||
@@ -152,3 +175,39 @@ function StationFocus({ position }: { position: LatLngExpression }): null {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveMarkerStroke(
|
||||||
|
stationId: string,
|
||||||
|
startStationId?: string | null,
|
||||||
|
endStationId?: string | null,
|
||||||
|
focusedStationId?: string | null
|
||||||
|
): string {
|
||||||
|
if (stationId === startStationId) {
|
||||||
|
return '#38bdf8';
|
||||||
|
}
|
||||||
|
if (stationId === endStationId) {
|
||||||
|
return '#fb923c';
|
||||||
|
}
|
||||||
|
if (stationId === focusedStationId) {
|
||||||
|
return '#22c55e';
|
||||||
|
}
|
||||||
|
return '#f97316';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMarkerFill(
|
||||||
|
stationId: string,
|
||||||
|
startStationId?: string | null,
|
||||||
|
endStationId?: string | null,
|
||||||
|
focusedStationId?: string | null
|
||||||
|
): string {
|
||||||
|
if (stationId === startStationId) {
|
||||||
|
return '#bae6fd';
|
||||||
|
}
|
||||||
|
if (stationId === endStationId) {
|
||||||
|
return '#fed7aa';
|
||||||
|
}
|
||||||
|
if (stationId === focusedStationId) {
|
||||||
|
return '#bbf7d0';
|
||||||
|
}
|
||||||
|
return '#ffe4c7';
|
||||||
|
}
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ body {
|
|||||||
background-color 0.18s ease,
|
background-color 0.18s ease,
|
||||||
border-color 0.18s ease,
|
border-color 0.18s ease,
|
||||||
transform 0.18s ease;
|
transform 0.18s ease;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.station-list-item:hover,
|
.station-list-item:hover,
|
||||||
@@ -118,6 +119,16 @@ body {
|
|||||||
box-shadow: 0 8px 18px -10px rgba(45, 212, 191, 0.65);
|
box-shadow: 0 8px 18px -10px rgba(45, 212, 191, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.station-list-item--start {
|
||||||
|
border-color: rgba(56, 189, 248, 0.8);
|
||||||
|
background: rgba(14, 165, 233, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-list-item--end {
|
||||||
|
border-color: rgba(249, 115, 22, 0.8);
|
||||||
|
background: rgba(234, 88, 12, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
.station-list-item__name {
|
.station-list-item__name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -128,6 +139,27 @@ body {
|
|||||||
font-family: 'Fira Code', 'Source Code Pro', monospace;
|
font-family: 'Fira Code', 'Source Code Pro', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.station-list-item__badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
padding: 0.1rem 0.45rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(148, 163, 184, 0.18);
|
||||||
|
color: rgba(226, 232, 240, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-list-item--start .station-list-item__badge {
|
||||||
|
background: rgba(56, 189, 248, 0.35);
|
||||||
|
color: #0ea5e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-list-item--end .station-list-item__badge {
|
||||||
|
background: rgba(249, 115, 22, 0.35);
|
||||||
|
color: #f97316;
|
||||||
|
}
|
||||||
|
|
||||||
.grid h3 {
|
.grid h3 {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
@@ -151,6 +183,89 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.route-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.85rem;
|
||||||
|
padding: 1.1rem 1.35rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(250, 204, 21, 0.3);
|
||||||
|
background: rgba(161, 98, 7, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-panel__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-panel__hint {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: rgba(226, 232, 240, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-panel__meta {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-panel__meta > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-panel__meta dt {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: rgba(226, 232, 240, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-panel__meta dd {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: rgba(226, 232, 240, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-panel__error {
|
||||||
|
color: #f87171;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-panel__path {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6rem;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-panel__path span {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: rgba(226, 232, 240, 0.7);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-panel__path ol {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-panel__path li::after {
|
||||||
|
content: '→';
|
||||||
|
margin-left: 0.35rem;
|
||||||
|
color: rgba(250, 204, 21, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-panel__path li:last-child::after {
|
||||||
|
content: '';
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.selected-station {
|
.selected-station {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
padding: 1rem 1.25rem;
|
padding: 1rem 1.25rem;
|
||||||
|
|||||||
141
frontend/src/utils/route.test.ts
Normal file
141
frontend/src/utils/route.test.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { buildTrackAdjacency, computeRoute } from './route';
|
||||||
|
import type { Station, Track } from '../types/domain';
|
||||||
|
|
||||||
|
const baseTimestamps = {
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('route utilities', () => {
|
||||||
|
it('finds a multi-hop path across connected tracks', () => {
|
||||||
|
const stations: Station[] = [
|
||||||
|
{
|
||||||
|
id: 'station-a',
|
||||||
|
name: 'Alpha',
|
||||||
|
latitude: 51.5,
|
||||||
|
longitude: -0.1,
|
||||||
|
...baseTimestamps,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'station-b',
|
||||||
|
name: 'Bravo',
|
||||||
|
latitude: 51.52,
|
||||||
|
longitude: -0.11,
|
||||||
|
...baseTimestamps,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'station-c',
|
||||||
|
name: 'Charlie',
|
||||||
|
latitude: 51.54,
|
||||||
|
longitude: -0.12,
|
||||||
|
...baseTimestamps,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'station-d',
|
||||||
|
name: 'Delta',
|
||||||
|
latitude: 51.55,
|
||||||
|
longitude: -0.15,
|
||||||
|
...baseTimestamps,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tracks: Track[] = [
|
||||||
|
{
|
||||||
|
id: 'track-ab',
|
||||||
|
startStationId: 'station-a',
|
||||||
|
endStationId: 'station-b',
|
||||||
|
lengthMeters: 1200,
|
||||||
|
maxSpeedKph: 120,
|
||||||
|
...baseTimestamps,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'track-bc',
|
||||||
|
startStationId: 'station-b',
|
||||||
|
endStationId: 'station-c',
|
||||||
|
lengthMeters: 1500,
|
||||||
|
maxSpeedKph: 110,
|
||||||
|
...baseTimestamps,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'track-cd',
|
||||||
|
startStationId: 'station-c',
|
||||||
|
endStationId: 'station-d',
|
||||||
|
lengthMeters: 900,
|
||||||
|
maxSpeedKph: 115,
|
||||||
|
...baseTimestamps,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const stationById = new Map(stations.map((station) => [station.id, station]));
|
||||||
|
const adjacency = buildTrackAdjacency(tracks);
|
||||||
|
|
||||||
|
const result = computeRoute({
|
||||||
|
startId: 'station-a',
|
||||||
|
endId: 'station-d',
|
||||||
|
stationById,
|
||||||
|
adjacency,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.error).toBeNull();
|
||||||
|
expect(result.stations?.map((station) => station.id)).toEqual([
|
||||||
|
'station-a',
|
||||||
|
'station-b',
|
||||||
|
'station-c',
|
||||||
|
'station-d',
|
||||||
|
]);
|
||||||
|
expect(result.tracks.map((track) => track.id)).toEqual([
|
||||||
|
'track-ab',
|
||||||
|
'track-bc',
|
||||||
|
'track-cd',
|
||||||
|
]);
|
||||||
|
expect(result.totalLength).toBe(1200 + 1500 + 900);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when no path exists', () => {
|
||||||
|
const stations: Station[] = [
|
||||||
|
{
|
||||||
|
id: 'station-a',
|
||||||
|
name: 'Alpha',
|
||||||
|
latitude: 51.5,
|
||||||
|
longitude: -0.1,
|
||||||
|
...baseTimestamps,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'station-b',
|
||||||
|
name: 'Bravo',
|
||||||
|
latitude: 51.6,
|
||||||
|
longitude: -0.2,
|
||||||
|
...baseTimestamps,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tracks: Track[] = [
|
||||||
|
{
|
||||||
|
id: 'track-self',
|
||||||
|
startStationId: 'station-a',
|
||||||
|
endStationId: 'station-a',
|
||||||
|
lengthMeters: 0,
|
||||||
|
maxSpeedKph: 80,
|
||||||
|
...baseTimestamps,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const stationById = new Map(stations.map((station) => [station.id, station]));
|
||||||
|
const adjacency = buildTrackAdjacency(tracks);
|
||||||
|
|
||||||
|
const result = computeRoute({
|
||||||
|
startId: 'station-a',
|
||||||
|
endId: 'station-b',
|
||||||
|
stationById,
|
||||||
|
adjacency,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.stations).toBeNull();
|
||||||
|
expect(result.tracks).toHaveLength(0);
|
||||||
|
expect(result.error).toBe(
|
||||||
|
'No rail connection found between the selected stations.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
185
frontend/src/utils/route.ts
Normal file
185
frontend/src/utils/route.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import type { Station, Track } from '../types/domain';
|
||||||
|
|
||||||
|
export interface NeighborEdge {
|
||||||
|
readonly neighborId: string;
|
||||||
|
readonly track: Track;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TrackAdjacency = Map<string, NeighborEdge[]>;
|
||||||
|
|
||||||
|
export interface ComputeRouteParams {
|
||||||
|
readonly startId?: string | null;
|
||||||
|
readonly endId?: string | null;
|
||||||
|
readonly stationById: Map<string, Station>;
|
||||||
|
readonly adjacency: TrackAdjacency;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RouteComputation {
|
||||||
|
readonly stations: Station[] | null;
|
||||||
|
readonly tracks: Track[];
|
||||||
|
readonly totalLength: number | null;
|
||||||
|
readonly error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTrackAdjacency(tracks: readonly Track[]): TrackAdjacency {
|
||||||
|
const adjacency: TrackAdjacency = new Map();
|
||||||
|
|
||||||
|
const register = (fromId: string, toId: string, track: Track) => {
|
||||||
|
if (!adjacency.has(fromId)) {
|
||||||
|
adjacency.set(fromId, []);
|
||||||
|
}
|
||||||
|
adjacency.get(fromId)!.push({ neighborId: toId, track });
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const track of tracks) {
|
||||||
|
register(track.startStationId, track.endStationId, track);
|
||||||
|
register(track.endStationId, track.startStationId, track);
|
||||||
|
}
|
||||||
|
|
||||||
|
return adjacency;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeRoute({
|
||||||
|
startId,
|
||||||
|
endId,
|
||||||
|
stationById,
|
||||||
|
adjacency,
|
||||||
|
}: ComputeRouteParams): RouteComputation {
|
||||||
|
if (!startId || !endId) {
|
||||||
|
return emptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stationById.has(startId) || !stationById.has(endId)) {
|
||||||
|
return {
|
||||||
|
stations: null,
|
||||||
|
tracks: [],
|
||||||
|
totalLength: null,
|
||||||
|
error: 'Selected stations are no longer available.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startId === endId) {
|
||||||
|
const station = stationById.get(startId);
|
||||||
|
return {
|
||||||
|
stations: station ? [station] : null,
|
||||||
|
tracks: [],
|
||||||
|
totalLength: 0,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const queue: string[] = [];
|
||||||
|
const parent = new Map<string, { prev: string | null; via: Track | null }>();
|
||||||
|
|
||||||
|
queue.push(startId);
|
||||||
|
visited.add(startId);
|
||||||
|
parent.set(startId, { prev: null, via: null });
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift()!;
|
||||||
|
if (current === endId) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const neighbors = adjacency.get(current) ?? [];
|
||||||
|
for (const { neighborId, track } of neighbors) {
|
||||||
|
if (visited.has(neighborId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited.add(neighborId);
|
||||||
|
parent.set(neighborId, { prev: current, via: track });
|
||||||
|
queue.push(neighborId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parent.has(endId)) {
|
||||||
|
return {
|
||||||
|
stations: null,
|
||||||
|
tracks: [],
|
||||||
|
totalLength: null,
|
||||||
|
error: 'No rail connection found between the selected stations.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const stationPath: string[] = [];
|
||||||
|
const trackSequence: Track[] = [];
|
||||||
|
let cursor: string | null = endId;
|
||||||
|
|
||||||
|
while (cursor) {
|
||||||
|
const details = parent.get(cursor);
|
||||||
|
if (!details) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
stationPath.push(cursor);
|
||||||
|
if (details.via) {
|
||||||
|
trackSequence.push(details.via);
|
||||||
|
}
|
||||||
|
cursor = details.prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
stationPath.reverse();
|
||||||
|
trackSequence.reverse();
|
||||||
|
|
||||||
|
const stations = stationPath
|
||||||
|
.map((id) => stationById.get(id))
|
||||||
|
.filter((station): station is Station => Boolean(station));
|
||||||
|
|
||||||
|
const totalLength = computeTotalLength(trackSequence, stations);
|
||||||
|
|
||||||
|
return {
|
||||||
|
stations,
|
||||||
|
tracks: trackSequence,
|
||||||
|
totalLength,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeTotalLength(tracks: Track[], stations: Station[]): number | null {
|
||||||
|
if (tracks.length === 0 && stations.length <= 1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasTrackLengths = tracks.every(
|
||||||
|
(track) =>
|
||||||
|
typeof track.lengthMeters === 'number' && Number.isFinite(track.lengthMeters)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasTrackLengths) {
|
||||||
|
return tracks.reduce((total, track) => total + (track.lengthMeters ?? 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stations.length < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
for (let index = 0; index < stations.length - 1; index += 1) {
|
||||||
|
total += haversineDistance(stations[index], stations[index + 1]);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
function haversineDistance(a: Station, b: Station): number {
|
||||||
|
const R = 6371_000;
|
||||||
|
const toRad = (value: number) => (value * Math.PI) / 180;
|
||||||
|
const dLat = toRad(b.latitude - a.latitude);
|
||||||
|
const dLon = toRad(b.longitude - a.longitude);
|
||||||
|
const lat1 = toRad(a.latitude);
|
||||||
|
const lat2 = toRad(b.latitude);
|
||||||
|
|
||||||
|
const sinDLat = Math.sin(dLat / 2);
|
||||||
|
const sinDLon = Math.sin(dLon / 2);
|
||||||
|
const root = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon;
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(root), Math.sqrt(1 - root));
|
||||||
|
return R * c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyResult(): RouteComputation {
|
||||||
|
return {
|
||||||
|
stations: null,
|
||||||
|
tracks: [],
|
||||||
|
totalLength: null,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"types": ["vite/client"]
|
"types": ["vite/client", "vitest"]
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
8
frontend/vitest.config.ts
Normal file
8
frontend/vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
include: ['src/**/*.test.ts'],
|
||||||
|
environment: 'node',
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user