Maps Update
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -45,3 +45,4 @@ package-lock.json
|
|||||||
|
|
||||||
.venv/
|
.venv/
|
||||||
roadcast/data.csv
|
roadcast/data.csv
|
||||||
|
web/public/Crashes_in_DC.csv
|
||||||
|
|||||||
118
web/bun.lock
118
web/bun.lock
@@ -5,7 +5,9 @@
|
|||||||
"name": "my-app",
|
"name": "my-app",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/mapbox-gl-directions": "^4.3.1",
|
"@mapbox/mapbox-gl-directions": "^4.3.1",
|
||||||
|
"@mapbox/mapbox-gl-geocoder": "^5.1.2",
|
||||||
"@types/mapbox-gl": "^3.4.1",
|
"@types/mapbox-gl": "^3.4.1",
|
||||||
|
"csv-parser": "^3.2.0",
|
||||||
"mapbox-gl": "^3.15.0",
|
"mapbox-gl": "^3.15.0",
|
||||||
"next": "15.5.4",
|
"next": "15.5.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
@@ -93,12 +95,20 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@mapbox/fusspot": ["@mapbox/fusspot@0.4.0", "", { "dependencies": { "is-plain-obj": "^1.1.0", "xtend": "^4.0.1" } }, "sha512-6sys1vUlhNCqMvJOqPEPSi0jc9tg7aJ//oG1A16H3PXoIt9whtNngD7UzBHUVTH15zunR/vRvMtGNVsogm1KzA=="],
|
||||||
|
|
||||||
"@mapbox/jsonlint-lines-primitives": ["@mapbox/jsonlint-lines-primitives@2.0.2", "", {}, "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ=="],
|
"@mapbox/jsonlint-lines-primitives": ["@mapbox/jsonlint-lines-primitives@2.0.2", "", {}, "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ=="],
|
||||||
|
|
||||||
"@mapbox/mapbox-gl-directions": ["@mapbox/mapbox-gl-directions@4.3.1", "", { "dependencies": { "@mapbox/polyline": "^1.1.1", "lodash.debounce": "^4.0.6", "lodash.isequal": "^4.2.0", "lodash.template": "^4.2.5", "merge-options": "^3.0.4", "redux": "^4.2.0", "redux-thunk": "^2.4.2", "suggestions": "^1.7.1", "turf-extent": "^1.0.4" }, "peerDependencies": { "mapbox-gl": "^1 || ^2 || ^3" } }, "sha512-dWyKPBC+ZT3BV/+VYArMZVfKg5LY+YLLCb9P54INifacfkHUMzYgrWlSod/viGnuMz1wlLFitwAqipZE+m9uww=="],
|
"@mapbox/mapbox-gl-directions": ["@mapbox/mapbox-gl-directions@4.3.1", "", { "dependencies": { "@mapbox/polyline": "^1.1.1", "lodash.debounce": "^4.0.6", "lodash.isequal": "^4.2.0", "lodash.template": "^4.2.5", "merge-options": "^3.0.4", "redux": "^4.2.0", "redux-thunk": "^2.4.2", "suggestions": "^1.7.1", "turf-extent": "^1.0.4" }, "peerDependencies": { "mapbox-gl": "^1 || ^2 || ^3" } }, "sha512-dWyKPBC+ZT3BV/+VYArMZVfKg5LY+YLLCb9P54INifacfkHUMzYgrWlSod/viGnuMz1wlLFitwAqipZE+m9uww=="],
|
||||||
|
|
||||||
|
"@mapbox/mapbox-gl-geocoder": ["@mapbox/mapbox-gl-geocoder@5.1.2", "", { "dependencies": { "@mapbox/mapbox-sdk": "^0.16.1", "events": "^3.3.0", "lodash.debounce": "^4.0.6", "nanoid": "^3.1.31", "subtag": "^0.5.0", "suggestions": "^1.6.0", "xtend": "^4.0.1" } }, "sha512-UjtGKL/bfaUTf4NfDaKCeYIvtMIJi9nr94QQB13p8uPXhSfjo931zhWNAib3YY7xkNqzVEBWzFGuB1IT5w7lGA=="],
|
||||||
|
|
||||||
"@mapbox/mapbox-gl-supported": ["@mapbox/mapbox-gl-supported@3.0.0", "", {}, "sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg=="],
|
"@mapbox/mapbox-gl-supported": ["@mapbox/mapbox-gl-supported@3.0.0", "", {}, "sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg=="],
|
||||||
|
|
||||||
|
"@mapbox/mapbox-sdk": ["@mapbox/mapbox-sdk@0.16.2", "", { "dependencies": { "@mapbox/fusspot": "^0.4.0", "@mapbox/parse-mapbox-token": "^0.2.0", "@mapbox/polyline": "^1.0.0", "eventemitter3": "^3.1.0", "form-data": "^3.0.4", "got": "^11.8.5", "is-plain-obj": "^1.1.0", "xtend": "^4.0.1" } }, "sha512-II8KrqOD+neL94bCakBQfYmNSD8A3MQHyxxtZ9Hy5nZQWFacocpj2PjPvQqgctmddWZQ9GS+WBgHPVamhuM9xA=="],
|
||||||
|
|
||||||
|
"@mapbox/parse-mapbox-token": ["@mapbox/parse-mapbox-token@0.2.0", "", { "dependencies": { "base-64": "^0.1.0" } }, "sha512-BjeuG4sodYaoTygwXIuAWlZV6zUv4ZriYAQhXikzx+7DChycMUQ9g85E79Htat+AsBg+nStFALehlOhClYm5cQ=="],
|
||||||
|
|
||||||
"@mapbox/point-geometry": ["@mapbox/point-geometry@1.1.0", "", {}, "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="],
|
"@mapbox/point-geometry": ["@mapbox/point-geometry@1.1.0", "", {}, "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="],
|
||||||
|
|
||||||
"@mapbox/polyline": ["@mapbox/polyline@1.2.1", "", { "dependencies": { "meow": "^9.0.0" }, "bin": { "polyline": "bin/polyline.bin.js" } }, "sha512-sn0V18O3OzW4RCcPoUIVDWvEGQaBNH9a0y5lgqrf5hUycyw1CzrhEoxV5irzrMNXKCkw1xRsZXcaVbsVZggHXA=="],
|
"@mapbox/polyline": ["@mapbox/polyline@1.2.1", "", { "dependencies": { "meow": "^9.0.0" }, "bin": { "polyline": "bin/polyline.bin.js" } }, "sha512-sn0V18O3OzW4RCcPoUIVDWvEGQaBNH9a0y5lgqrf5hUycyw1CzrhEoxV5irzrMNXKCkw1xRsZXcaVbsVZggHXA=="],
|
||||||
@@ -131,12 +141,16 @@
|
|||||||
|
|
||||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.4", "", { "os": "win32", "cpu": "x64" }, "sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg=="],
|
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.4", "", { "os": "win32", "cpu": "x64" }, "sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg=="],
|
||||||
|
|
||||||
|
"@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
|
||||||
|
|
||||||
"@skeletonlabs/skeleton": ["@skeletonlabs/skeleton@3.2.2", "", { "peerDependencies": { "tailwindcss": "^4.0.0" } }, "sha512-dAunBAWqRMcNTGAvCKUgpADJdbtqL65eNEb7pDIKQZ6bI6qsxakR6MuF2E4B3jmUEpcaxaggDp0UdnUjlkAZ1Q=="],
|
"@skeletonlabs/skeleton": ["@skeletonlabs/skeleton@3.2.2", "", { "peerDependencies": { "tailwindcss": "^4.0.0" } }, "sha512-dAunBAWqRMcNTGAvCKUgpADJdbtqL65eNEb7pDIKQZ6bI6qsxakR6MuF2E4B3jmUEpcaxaggDp0UdnUjlkAZ1Q=="],
|
||||||
|
|
||||||
"@skeletonlabs/skeleton-react": ["@skeletonlabs/skeleton-react@1.4.1", "", { "dependencies": { "@zag-js/accordion": "^1.18.3", "@zag-js/avatar": "^1.18.3", "@zag-js/file-upload": "^1.18.3", "@zag-js/pagination": "^1.18.3", "@zag-js/progress": "^1.18.3", "@zag-js/radio-group": "^1.18.3", "@zag-js/rating-group": "^1.18.3", "@zag-js/react": "^1.18.3", "@zag-js/slider": "^1.18.3", "@zag-js/switch": "^1.18.3", "@zag-js/tabs": "^1.18.3", "@zag-js/tags-input": "^1.18.3", "@zag-js/toast": "^1.18.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-tuA8Lmu6+LKDHn2XP+X3VB4jfznNXXw2G4qKsAJ6JyoEJwaAArgTIeQUxtKnvVbGSQBIGZGm6mL/XAqtPd09ZQ=="],
|
"@skeletonlabs/skeleton-react": ["@skeletonlabs/skeleton-react@1.4.1", "", { "dependencies": { "@zag-js/accordion": "^1.18.3", "@zag-js/avatar": "^1.18.3", "@zag-js/file-upload": "^1.18.3", "@zag-js/pagination": "^1.18.3", "@zag-js/progress": "^1.18.3", "@zag-js/radio-group": "^1.18.3", "@zag-js/rating-group": "^1.18.3", "@zag-js/react": "^1.18.3", "@zag-js/slider": "^1.18.3", "@zag-js/switch": "^1.18.3", "@zag-js/tabs": "^1.18.3", "@zag-js/tags-input": "^1.18.3", "@zag-js/toast": "^1.18.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-tuA8Lmu6+LKDHn2XP+X3VB4jfznNXXw2G4qKsAJ6JyoEJwaAArgTIeQUxtKnvVbGSQBIGZGm6mL/XAqtPd09ZQ=="],
|
||||||
|
|
||||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||||
|
|
||||||
|
"@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.13", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.18", "source-map-js": "^1.2.1", "tailwindcss": "4.1.13" } }, "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.13", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.18", "source-map-js": "^1.2.1", "tailwindcss": "4.1.13" } }, "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.13", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.13", "@tailwindcss/oxide-darwin-arm64": "4.1.13", "@tailwindcss/oxide-darwin-x64": "4.1.13", "@tailwindcss/oxide-freebsd-x64": "4.1.13", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", "@tailwindcss/oxide-linux-x64-musl": "4.1.13", "@tailwindcss/oxide-wasm32-wasi": "4.1.13", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" } }, "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA=="],
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.13", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.13", "@tailwindcss/oxide-darwin-arm64": "4.1.13", "@tailwindcss/oxide-darwin-x64": "4.1.13", "@tailwindcss/oxide-freebsd-x64": "4.1.13", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", "@tailwindcss/oxide-linux-x64-musl": "4.1.13", "@tailwindcss/oxide-wasm32-wasi": "4.1.13", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" } }, "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA=="],
|
||||||
@@ -167,10 +181,16 @@
|
|||||||
|
|
||||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.13", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.13", "@tailwindcss/oxide": "4.1.13", "postcss": "^8.4.41", "tailwindcss": "4.1.13" } }, "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ=="],
|
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.13", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.13", "@tailwindcss/oxide": "4.1.13", "postcss": "^8.4.41", "tailwindcss": "4.1.13" } }, "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ=="],
|
||||||
|
|
||||||
|
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
|
||||||
|
|
||||||
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
||||||
|
|
||||||
"@types/geojson-vt": ["@types/geojson-vt@3.2.5", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g=="],
|
"@types/geojson-vt": ["@types/geojson-vt@3.2.5", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g=="],
|
||||||
|
|
||||||
|
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="],
|
||||||
|
|
||||||
|
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
|
||||||
|
|
||||||
"@types/mapbox-gl": ["@types/mapbox-gl@3.4.1", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg=="],
|
"@types/mapbox-gl": ["@types/mapbox-gl@3.4.1", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg=="],
|
||||||
|
|
||||||
"@types/mapbox__point-geometry": ["@types/mapbox__point-geometry@0.1.4", "", {}, "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA=="],
|
"@types/mapbox__point-geometry": ["@types/mapbox__point-geometry@0.1.4", "", {}, "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA=="],
|
||||||
@@ -187,6 +207,8 @@
|
|||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="],
|
"@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="],
|
||||||
|
|
||||||
|
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
|
||||||
|
|
||||||
"@types/supercluster": ["@types/supercluster@7.1.3", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA=="],
|
"@types/supercluster": ["@types/supercluster@7.1.3", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA=="],
|
||||||
|
|
||||||
"@vis.gl/react-mapbox": ["@vis.gl/react-mapbox@8.0.4", "", { "peerDependencies": { "mapbox-gl": ">=3.5.0", "react": ">=16.3.0", "react-dom": ">=16.3.0" }, "optionalPeers": ["mapbox-gl"] }, "sha512-NFk0vsWcNzSs0YCsVdt2100Zli9QWR+pje8DacpLkkGEAXFaJsFtI1oKD0Hatiate4/iAIW39SQHhgfhbeEPfQ=="],
|
"@vis.gl/react-mapbox": ["@vis.gl/react-mapbox@8.0.4", "", { "peerDependencies": { "mapbox-gl": ">=3.5.0", "react": ">=16.3.0", "react-dom": ">=16.3.0" }, "optionalPeers": ["mapbox-gl"] }, "sha512-NFk0vsWcNzSs0YCsVdt2100Zli9QWR+pje8DacpLkkGEAXFaJsFtI1oKD0Hatiate4/iAIW39SQHhgfhbeEPfQ=="],
|
||||||
@@ -251,10 +273,20 @@
|
|||||||
|
|
||||||
"assign-symbols": ["assign-symbols@1.0.0", "", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="],
|
"assign-symbols": ["assign-symbols@1.0.0", "", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="],
|
||||||
|
|
||||||
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
|
"base-64": ["base-64@0.1.0", "", {}, "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA=="],
|
||||||
|
|
||||||
"bytewise": ["bytewise@1.1.0", "", { "dependencies": { "bytewise-core": "^1.2.2", "typewise": "^1.0.3" } }, "sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ=="],
|
"bytewise": ["bytewise@1.1.0", "", { "dependencies": { "bytewise-core": "^1.2.2", "typewise": "^1.0.3" } }, "sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ=="],
|
||||||
|
|
||||||
"bytewise-core": ["bytewise-core@1.2.3", "", { "dependencies": { "typewise-core": "^1.2" } }, "sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA=="],
|
"bytewise-core": ["bytewise-core@1.2.3", "", { "dependencies": { "typewise-core": "^1.2" } }, "sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA=="],
|
||||||
|
|
||||||
|
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
|
||||||
|
|
||||||
|
"cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||||
|
|
||||||
"camelcase-keys": ["camelcase-keys@6.2.2", "", { "dependencies": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", "quick-lru": "^4.0.1" } }, "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg=="],
|
"camelcase-keys": ["camelcase-keys@6.2.2", "", { "dependencies": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", "quick-lru": "^4.0.1" } }, "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg=="],
|
||||||
@@ -267,46 +299,94 @@
|
|||||||
|
|
||||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||||
|
|
||||||
|
"clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
|
||||||
|
|
||||||
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
"csscolorparser": ["csscolorparser@1.0.3", "", {}, "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w=="],
|
"csscolorparser": ["csscolorparser@1.0.3", "", {}, "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w=="],
|
||||||
|
|
||||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"csv-parser": ["csv-parser@3.2.0", "", { "bin": { "csv-parser": "bin/csv-parser" } }, "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA=="],
|
||||||
|
|
||||||
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
||||||
|
|
||||||
"decamelize-keys": ["decamelize-keys@1.1.1", "", { "dependencies": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" } }, "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg=="],
|
"decamelize-keys": ["decamelize-keys@1.1.1", "", { "dependencies": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" } }, "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg=="],
|
||||||
|
|
||||||
|
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||||
|
|
||||||
|
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
|
||||||
|
|
||||||
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
"detect-libc": ["detect-libc@2.1.1", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="],
|
"detect-libc": ["detect-libc@2.1.1", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
"earcut": ["earcut@3.0.2", "", {}, "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ=="],
|
"earcut": ["earcut@3.0.2", "", {}, "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ=="],
|
||||||
|
|
||||||
|
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
||||||
|
|
||||||
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
|
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
|
||||||
|
|
||||||
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||||
|
|
||||||
|
"eventemitter3": ["eventemitter3@3.1.2", "", {}, "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="],
|
||||||
|
|
||||||
|
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
|
||||||
|
|
||||||
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
|
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
|
||||||
|
|
||||||
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||||
|
|
||||||
|
"form-data": ["form-data@3.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35" } }, "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
"fuzzy": ["fuzzy@0.1.3", "", {}, "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w=="],
|
"fuzzy": ["fuzzy@0.1.3", "", {}, "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w=="],
|
||||||
|
|
||||||
"geojson-vt": ["geojson-vt@4.0.2", "", {}, "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A=="],
|
"geojson-vt": ["geojson-vt@4.0.2", "", {}, "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A=="],
|
||||||
|
|
||||||
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
|
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
|
||||||
|
|
||||||
"get-value": ["get-value@2.0.6", "", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="],
|
"get-value": ["get-value@2.0.6", "", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="],
|
||||||
|
|
||||||
"gl-matrix": ["gl-matrix@3.4.4", "", {}, "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ=="],
|
"gl-matrix": ["gl-matrix@3.4.4", "", {}, "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
|
"got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="],
|
||||||
|
|
||||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
"grid-index": ["grid-index@1.1.0", "", {}, "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA=="],
|
"grid-index": ["grid-index@1.1.0", "", {}, "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA=="],
|
||||||
|
|
||||||
"hard-rejection": ["hard-rejection@2.1.0", "", {}, "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA=="],
|
"hard-rejection": ["hard-rejection@2.1.0", "", {}, "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
|
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
|
||||||
|
|
||||||
|
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||||
|
|
||||||
|
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
|
||||||
|
|
||||||
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
||||||
|
|
||||||
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
|
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
|
||||||
@@ -325,12 +405,16 @@
|
|||||||
|
|
||||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
|
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||||
|
|
||||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
||||||
|
|
||||||
"json-stringify-pretty-compact": ["json-stringify-pretty-compact@3.0.0", "", {}, "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA=="],
|
"json-stringify-pretty-compact": ["json-stringify-pretty-compact@3.0.0", "", {}, "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA=="],
|
||||||
|
|
||||||
"kdbush": ["kdbush@4.0.2", "", {}, "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="],
|
"kdbush": ["kdbush@4.0.2", "", {}, "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="],
|
||||||
|
|
||||||
|
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||||
|
|
||||||
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||||
@@ -369,6 +453,8 @@
|
|||||||
|
|
||||||
"lodash.templatesettings": ["lodash.templatesettings@4.2.0", "", { "dependencies": { "lodash._reinterpolate": "^3.0.0" } }, "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ=="],
|
"lodash.templatesettings": ["lodash.templatesettings@4.2.0", "", { "dependencies": { "lodash._reinterpolate": "^3.0.0" } }, "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ=="],
|
||||||
|
|
||||||
|
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
"magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
||||||
@@ -379,10 +465,18 @@
|
|||||||
|
|
||||||
"martinez-polygon-clipping": ["martinez-polygon-clipping@0.7.4", "", { "dependencies": { "robust-predicates": "^2.0.4", "splaytree": "^0.1.4", "tinyqueue": "^1.2.0" } }, "sha512-jBEwrKtA0jTagUZj2bnmb4Yg2s4KnJGRePStgI7bAVjtcipKiF39R4LZ2V/UT61jMYWrTcBhPazexeqd6JAVtw=="],
|
"martinez-polygon-clipping": ["martinez-polygon-clipping@0.7.4", "", { "dependencies": { "robust-predicates": "^2.0.4", "splaytree": "^0.1.4", "tinyqueue": "^1.2.0" } }, "sha512-jBEwrKtA0jTagUZj2bnmb4Yg2s4KnJGRePStgI7bAVjtcipKiF39R4LZ2V/UT61jMYWrTcBhPazexeqd6JAVtw=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
"meow": ["meow@9.0.0", "", { "dependencies": { "@types/minimist": "^1.2.0", "camelcase-keys": "^6.2.2", "decamelize": "^1.2.0", "decamelize-keys": "^1.1.0", "hard-rejection": "^2.1.0", "minimist-options": "4.1.0", "normalize-package-data": "^3.0.0", "read-pkg-up": "^7.0.1", "redent": "^3.0.0", "trim-newlines": "^3.0.0", "type-fest": "^0.18.0", "yargs-parser": "^20.2.3" } }, "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ=="],
|
"meow": ["meow@9.0.0", "", { "dependencies": { "@types/minimist": "^1.2.0", "camelcase-keys": "^6.2.2", "decamelize": "^1.2.0", "decamelize-keys": "^1.1.0", "hard-rejection": "^2.1.0", "minimist-options": "4.1.0", "normalize-package-data": "^3.0.0", "read-pkg-up": "^7.0.1", "redent": "^3.0.0", "trim-newlines": "^3.0.0", "type-fest": "^0.18.0", "yargs-parser": "^20.2.3" } }, "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ=="],
|
||||||
|
|
||||||
"merge-options": ["merge-options@3.0.4", "", { "dependencies": { "is-plain-obj": "^2.1.0" } }, "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ=="],
|
"merge-options": ["merge-options@3.0.4", "", { "dependencies": { "is-plain-obj": "^2.1.0" } }, "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
|
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||||
|
|
||||||
"min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
|
"min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
|
||||||
|
|
||||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
@@ -401,6 +495,12 @@
|
|||||||
|
|
||||||
"normalize-package-data": ["normalize-package-data@3.0.3", "", { "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" } }, "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA=="],
|
"normalize-package-data": ["normalize-package-data@3.0.3", "", { "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" } }, "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA=="],
|
||||||
|
|
||||||
|
"normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="],
|
||||||
|
|
||||||
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
|
|
||||||
|
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
|
||||||
|
|
||||||
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||||
|
|
||||||
"p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
"p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||||
@@ -425,6 +525,8 @@
|
|||||||
|
|
||||||
"proxy-compare": ["proxy-compare@3.0.1", "", {}, "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q=="],
|
"proxy-compare": ["proxy-compare@3.0.1", "", {}, "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q=="],
|
||||||
|
|
||||||
|
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||||
|
|
||||||
"quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="],
|
"quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="],
|
||||||
|
|
||||||
"quickselect": ["quickselect@3.0.0", "", {}, "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="],
|
"quickselect": ["quickselect@3.0.0", "", {}, "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="],
|
||||||
@@ -447,8 +549,12 @@
|
|||||||
|
|
||||||
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
||||||
|
|
||||||
|
"resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
|
||||||
|
|
||||||
"resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="],
|
"resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="],
|
||||||
|
|
||||||
|
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
|
||||||
|
|
||||||
"robust-predicates": ["robust-predicates@2.0.4", "", {}, "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg=="],
|
"robust-predicates": ["robust-predicates@2.0.4", "", {}, "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg=="],
|
||||||
|
|
||||||
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
|
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
|
||||||
@@ -487,6 +593,8 @@
|
|||||||
|
|
||||||
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||||
|
|
||||||
|
"subtag": ["subtag@0.5.0", "", {}, "sha512-CaIBcTSb/nyk4xiiSOtZYz1B+F12ZxW8NEp54CdT+84vmh/h4sUnHGC6+KQXUfED8u22PQjCYWfZny8d2ELXwg=="],
|
||||||
|
|
||||||
"suggestions": ["suggestions@1.7.1", "", { "dependencies": { "fuzzy": "^0.1.1", "xtend": "^4.0.0" } }, "sha512-gl5YPAhPYl07JZ5obiD9nTZsg4SyZswAQU/NNtnYiSnFkI3+ZHuXAiEsYm7AaZ71E0LXSFaGVaulGSWN3Gd71A=="],
|
"suggestions": ["suggestions@1.7.1", "", { "dependencies": { "fuzzy": "^0.1.1", "xtend": "^4.0.0" } }, "sha512-gl5YPAhPYl07JZ5obiD9nTZsg4SyZswAQU/NNtnYiSnFkI3+ZHuXAiEsYm7AaZ71E0LXSFaGVaulGSWN3Gd71A=="],
|
||||||
|
|
||||||
"supercluster": ["supercluster@8.0.1", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ=="],
|
"supercluster": ["supercluster@8.0.1", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ=="],
|
||||||
@@ -523,14 +631,24 @@
|
|||||||
|
|
||||||
"validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="],
|
"validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="],
|
||||||
|
|
||||||
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||||
|
|
||||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||||
|
|
||||||
"yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
|
"yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
|
||||||
|
|
||||||
|
"@mapbox/fusspot/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="],
|
||||||
|
|
||||||
|
"@mapbox/mapbox-sdk/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="],
|
||||||
|
|
||||||
|
"clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
|
||||||
|
|
||||||
"decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="],
|
"decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="],
|
||||||
|
|
||||||
|
"http2-wrapper/quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
|
||||||
|
|
||||||
"lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
"lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
"martinez-polygon-clipping/tinyqueue": ["tinyqueue@1.2.3", "", {}, "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA=="],
|
"martinez-polygon-clipping/tinyqueue": ["tinyqueue@1.2.3", "", {}, "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA=="],
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/mapbox-gl-directions": "^4.3.1",
|
"@mapbox/mapbox-gl-directions": "^4.3.1",
|
||||||
|
"@mapbox/mapbox-gl-geocoder": "^5.1.2",
|
||||||
"@types/mapbox-gl": "^3.4.1",
|
"@types/mapbox-gl": "^3.4.1",
|
||||||
|
"csv-parser": "^3.2.0",
|
||||||
"mapbox-gl": "^3.15.0",
|
"mapbox-gl": "^3.15.0",
|
||||||
"next": "15.5.4",
|
"next": "15.5.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
|||||||
139
web/src/app/api/crashes/route.ts
Normal file
139
web/src/app/api/crashes/route.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import csv from 'csv-parser';
|
||||||
|
|
||||||
|
export type CrashData = {
|
||||||
|
id: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
reportDate: string;
|
||||||
|
address: string;
|
||||||
|
ward: string;
|
||||||
|
totalVehicles: number;
|
||||||
|
totalPedestrians: number;
|
||||||
|
totalBicycles: number;
|
||||||
|
fatalDriver: number;
|
||||||
|
fatalPedestrian: number;
|
||||||
|
fatalBicyclist: number;
|
||||||
|
majorInjuriesDriver: number;
|
||||||
|
majorInjuriesPedestrian: number;
|
||||||
|
majorInjuriesBicyclist: number;
|
||||||
|
speedingInvolved: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CrashResponse = {
|
||||||
|
data: CrashData[];
|
||||||
|
pagination: {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasNext: boolean;
|
||||||
|
hasPrev: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const CSV_FILE_PATH = path.join(process.cwd(), 'public', 'Crashes_in_DC.csv');
|
||||||
|
|
||||||
|
// Cache to store parsed CSV data
|
||||||
|
let csvCache: CrashData[] | null = null;
|
||||||
|
let csvCacheTimestamp = 0;
|
||||||
|
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
||||||
|
|
||||||
|
async function loadCsvData(): Promise<CrashData[]> {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Return cached data if it's still valid
|
||||||
|
if (csvCache && (now - csvCacheTimestamp) < CACHE_TTL) {
|
||||||
|
return csvCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const results: CrashData[] = [];
|
||||||
|
|
||||||
|
if (!fs.existsSync(CSV_FILE_PATH)) {
|
||||||
|
reject(new Error('CSV file not found'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.createReadStream(CSV_FILE_PATH)
|
||||||
|
.pipe(csv())
|
||||||
|
.on('data', (row: any) => {
|
||||||
|
// Parse the CSV row and extract relevant fields
|
||||||
|
const latitude = parseFloat(row.LATITUDE);
|
||||||
|
const longitude = parseFloat(row.LONGITUDE);
|
||||||
|
|
||||||
|
// Only include rows with valid coordinates
|
||||||
|
if (!isNaN(latitude) && !isNaN(longitude) && latitude && longitude) {
|
||||||
|
results.push({
|
||||||
|
id: row.OBJECTID || row.CRIMEID || `crash-${results.length}`,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
reportDate: row.REPORTDATE || '',
|
||||||
|
address: row.ADDRESS || '',
|
||||||
|
ward: row.WARD || '',
|
||||||
|
totalVehicles: parseInt(row.TOTAL_VEHICLES) || 0,
|
||||||
|
totalPedestrians: parseInt(row.TOTAL_PEDESTRIANS) || 0,
|
||||||
|
totalBicycles: parseInt(row.TOTAL_BICYCLES) || 0,
|
||||||
|
fatalDriver: parseInt(row.FATAL_DRIVER) || 0,
|
||||||
|
fatalPedestrian: parseInt(row.FATAL_PEDESTRIAN) || 0,
|
||||||
|
fatalBicyclist: parseInt(row.FATAL_BICYCLIST) || 0,
|
||||||
|
majorInjuriesDriver: parseInt(row.MAJORINJURIES_DRIVER) || 0,
|
||||||
|
majorInjuriesPedestrian: parseInt(row.MAJORINJURIES_PEDESTRIAN) || 0,
|
||||||
|
majorInjuriesBicyclist: parseInt(row.MAJORINJURIES_BICYCLIST) || 0,
|
||||||
|
speedingInvolved: parseInt(row.SPEEDING_INVOLVED) || 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('end', () => {
|
||||||
|
// Update cache
|
||||||
|
csvCache = results;
|
||||||
|
csvCacheTimestamp = now;
|
||||||
|
resolve(results);
|
||||||
|
})
|
||||||
|
.on('error', (error: any) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const page = Math.max(1, parseInt(searchParams.get('page') || '1'));
|
||||||
|
const limit = Math.min(10000, Math.max(1, parseInt(searchParams.get('limit') || '100')));
|
||||||
|
|
||||||
|
// Load CSV data
|
||||||
|
const allCrashes = await loadCsvData();
|
||||||
|
|
||||||
|
// Calculate pagination
|
||||||
|
const total = allCrashes.length;
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
const startIndex = (page - 1) * limit;
|
||||||
|
const endIndex = startIndex + limit;
|
||||||
|
|
||||||
|
// Get the page data
|
||||||
|
const pageData = allCrashes.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
const response: CrashResponse = {
|
||||||
|
data: pageData,
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
hasNext: page < totalPages,
|
||||||
|
hasPrev: page > 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return NextResponse.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading crash data:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to load crash data' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
web/src/app/components/CrashDataControls.tsx
Normal file
98
web/src/app/components/CrashDataControls.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { UseCrashDataResult } from '../hooks/useCrashData';
|
||||||
|
|
||||||
|
interface CrashDataControlsProps {
|
||||||
|
crashDataHook: UseCrashDataResult;
|
||||||
|
onDataLoaded?: (dataCount: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CrashDataControls({ crashDataHook, onDataLoaded }: CrashDataControlsProps) {
|
||||||
|
const { data, loading, error, pagination, loadMore, refresh } = crashDataHook;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (onDataLoaded) {
|
||||||
|
onDataLoaded(data.length);
|
||||||
|
}
|
||||||
|
}, [data.length, onDataLoaded]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '10px',
|
||||||
|
right: '10px',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
color: 'white',
|
||||||
|
padding: '12px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
zIndex: 30,
|
||||||
|
fontSize: '14px',
|
||||||
|
minWidth: '200px'
|
||||||
|
}}>
|
||||||
|
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>
|
||||||
|
Crash Data Status
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '6px' }}>
|
||||||
|
Loaded: {data.length.toLocaleString()} crashes
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pagination && (
|
||||||
|
<div style={{ marginBottom: '6px', fontSize: '12px', color: '#ccc' }}>
|
||||||
|
Page {pagination.page} of {pagination.totalPages}
|
||||||
|
<br />
|
||||||
|
Total: {pagination.total.toLocaleString()} crashes
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<div style={{ marginBottom: '8px', color: '#ffff99' }}>
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div style={{ marginBottom: '8px', color: '#ff6666', fontSize: '12px' }}>
|
||||||
|
Error: {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
{pagination?.hasNext && (
|
||||||
|
<button
|
||||||
|
onClick={loadMore}
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
backgroundColor: loading ? '#666' : '#007acc',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '12px',
|
||||||
|
cursor: loading ? 'not-allowed' : 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Load More
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={refresh}
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
backgroundColor: loading ? '#666' : '#28a745',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '12px',
|
||||||
|
cursor: loading ? 'not-allowed' : 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,28 +2,14 @@
|
|||||||
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from "mapbox-gl";
|
||||||
|
import GeocodeInput from './GeocodeInput';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mapRef: React.MutableRefObject<mapboxgl.Map | null>;
|
mapRef: React.MutableRefObject<mapboxgl.Map | null>;
|
||||||
profile?: "mapbox/driving" | "mapbox/walking" | "mapbox/cycling";
|
profile?: "mapbox/driving" | "mapbox/walking" | "mapbox/cycling";
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLngLat(value: string): [number, number] | null {
|
// Routing now uses geocoder-only selection inside the sidebar (no manual coordinate parsing)
|
||||||
// Accept formats like: "lng,lat" or "lat,lng" if clearly parseable
|
|
||||||
if (!value) return null;
|
|
||||||
const parts = value.split(",").map((s) => s.trim());
|
|
||||||
if (parts.length !== 2) return null;
|
|
||||||
const a = Number(parts[0]);
|
|
||||||
const b = Number(parts[1]);
|
|
||||||
if (Number.isFinite(a) && Number.isFinite(b)) {
|
|
||||||
// Heuristic: if abs(a) > 90 then assume it's lng,lat
|
|
||||||
if (Math.abs(a) > 90) return [a, b];
|
|
||||||
if (Math.abs(b) > 90) return [b, a];
|
|
||||||
// otherwise assume input is lng,lat
|
|
||||||
return [a, b];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving" }: Props) {
|
export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving" }: Props) {
|
||||||
// Sidebar supports collapse via a hamburger button in the header
|
// Sidebar supports collapse via a hamburger button in the header
|
||||||
@@ -34,33 +20,86 @@ export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving" }
|
|||||||
const [originCoord, setOriginCoord] = useState<[number, number] | null>(null);
|
const [originCoord, setOriginCoord] = useState<[number, number] | null>(null);
|
||||||
const [destCoord, setDestCoord] = useState<[number, number] | null>(null);
|
const [destCoord, setDestCoord] = useState<[number, number] | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [pickMode, setPickMode] = useState<"origin" | "dest" | null>(null);
|
// custom geocoder inputs + suggestions (we implement our own UI instead of the library)
|
||||||
|
const originQueryRef = useRef<string>("");
|
||||||
|
const destQueryRef = useRef<string>("");
|
||||||
|
const [originQuery, setOriginQuery] = useState("");
|
||||||
|
const [destQuery, setDestQuery] = useState("");
|
||||||
|
const [originSuggestions, setOriginSuggestions] = useState<any[]>([]);
|
||||||
|
const [destSuggestions, setDestSuggestions] = useState<any[]>([]);
|
||||||
|
const originTimer = useRef<number | null>(null);
|
||||||
|
const destTimer = useRef<number | null>(null);
|
||||||
|
const originInputRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const destInputRef = useRef<HTMLDivElement | null>(null);
|
||||||
const mountedRef = useRef(true);
|
const mountedRef = useRef(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => { mountedRef.current = false; };
|
return () => { mountedRef.current = false; };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// We'll implement our own geocoder fetcher and suggestion UI.
|
||||||
|
const fetchSuggestions = async (q: string) => {
|
||||||
|
const token = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || mapboxgl.accessToken || (typeof window !== 'undefined' ? (window as any).NEXT_PUBLIC_MAPBOX_TOKEN : undefined);
|
||||||
|
if (!token) {
|
||||||
|
console.warn('[DirectionsSidebar] Mapbox token missing; suggestions disabled');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!q || q.trim().length === 0) return [];
|
||||||
|
const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(q)}.json?autocomplete=true&limit=6&types=place,locality,address,region,poi&access_token=${token}`;
|
||||||
|
try {
|
||||||
|
console.debug('[DirectionsSidebar] fetchSuggestions url=', url);
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) return [];
|
||||||
|
const data = await res.json();
|
||||||
|
const feats = data.features || [];
|
||||||
|
console.debug('[DirectionsSidebar] fetchSuggestions results=', feats.length);
|
||||||
|
return feats;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[DirectionsSidebar] fetchSuggestions error', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// debounce origin query
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const map = mapRef.current;
|
if (originTimer.current) window.clearTimeout(originTimer.current);
|
||||||
if (!map) return;
|
if (!originQuery) {
|
||||||
|
setOriginSuggestions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
originTimer.current = window.setTimeout(async () => {
|
||||||
|
const features = await fetchSuggestions(originQuery);
|
||||||
|
if (mountedRef.current) setOriginSuggestions(features);
|
||||||
|
}, 250) as unknown as number;
|
||||||
|
return () => { if (originTimer.current) window.clearTimeout(originTimer.current); };
|
||||||
|
}, [originQuery]);
|
||||||
|
|
||||||
function onMapClick(e: mapboxgl.MapMouseEvent) {
|
// debounce dest query
|
||||||
if (!pickMode) return;
|
useEffect(() => {
|
||||||
const lngLat: [number, number] = [e.lngLat.lng, e.lngLat.lat];
|
if (destTimer.current) window.clearTimeout(destTimer.current);
|
||||||
if (pickMode === "origin") {
|
if (!destQuery) {
|
||||||
setOriginCoord(lngLat);
|
setDestSuggestions([]);
|
||||||
setOriginText(`${lngLat[0].toFixed(5)}, ${lngLat[1].toFixed(5)}`);
|
return;
|
||||||
|
}
|
||||||
|
destTimer.current = window.setTimeout(async () => {
|
||||||
|
const features = await fetchSuggestions(destQuery);
|
||||||
|
if (mountedRef.current) setDestSuggestions(features);
|
||||||
|
}, 250) as unknown as number;
|
||||||
|
return () => { if (destTimer.current) window.clearTimeout(destTimer.current); };
|
||||||
|
}, [destQuery]);
|
||||||
|
|
||||||
|
// when collapsed toggles, mount or unmount geocoder controls to keep DOM stable
|
||||||
|
useEffect(() => {
|
||||||
|
// if expanded, ensure the geocoder instances are attached to their containers
|
||||||
|
if (!collapsed) {
|
||||||
|
// nothing to mount for the custom inputs — they are regular DOM inputs rendered below
|
||||||
} else {
|
} else {
|
||||||
setDestCoord(lngLat);
|
// nothing to clear: we are managing suggestions via state
|
||||||
setDestText(`${lngLat[0].toFixed(5)}, ${lngLat[1].toFixed(5)}`);
|
|
||||||
}
|
|
||||||
setPickMode(null);
|
|
||||||
}
|
}
|
||||||
|
}, [collapsed]);
|
||||||
|
|
||||||
map.on("click", onMapClick as any);
|
// note: we no longer listen for map-level geocoder results here because
|
||||||
return () => { if (map) map.off("click", onMapClick as any); };
|
// the sidebar now embeds its own two geocoder controls and captures results directly.
|
||||||
}, [mapRef, pickMode]);
|
|
||||||
|
|
||||||
// helper: remove existing route layers/sources
|
// helper: remove existing route layers/sources
|
||||||
function removeRouteFromMap(map: mapboxgl.Map) {
|
function removeRouteFromMap(map: mapboxgl.Map) {
|
||||||
@@ -95,13 +134,10 @@ export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving" }
|
|||||||
async function handleGetRoute() {
|
async function handleGetRoute() {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
let o = originCoord;
|
const o = originCoord;
|
||||||
let d = destCoord;
|
const d = destCoord;
|
||||||
// if coords not set but text parsable
|
|
||||||
if (!o) o = parseLngLat(originText);
|
|
||||||
if (!d) d = parseLngLat(destText);
|
|
||||||
if (!o || !d) {
|
if (!o || !d) {
|
||||||
alert("Please provide origin and destination coordinates or pick them on the map (click 'Pick on map').\nFormat: lng,lat");
|
alert('Please select both origin and destination using the location search boxes.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +219,9 @@ export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving" }
|
|||||||
setDestCoord(null);
|
setDestCoord(null);
|
||||||
setOriginText("");
|
setOriginText("");
|
||||||
setDestText("");
|
setDestText("");
|
||||||
|
// clear suggestions and inputs
|
||||||
|
setOriginSuggestions([]);
|
||||||
|
setDestSuggestions([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// re-add layers after style change
|
// re-add layers after style change
|
||||||
@@ -276,33 +315,42 @@ export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving" }
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Content — render only when expanded to avoid any collapsed 'strip' */}
|
{/* Content — render only when expanded to avoid any collapsed 'strip' */}
|
||||||
{!collapsed && (
|
<div className={`flex flex-col flex-1 p-4 overflow-auto ${collapsed ? 'hidden' : ''}`}>
|
||||||
<div className="flex flex-col flex-1 p-4 overflow-auto">
|
|
||||||
<div className="flex items-center justify-between mb-3 sticky top-2 z-10">
|
<div className="flex items-center justify-between mb-3 sticky top-2 z-10">
|
||||||
<strong className="text-sm">Directions</strong>
|
<strong className="text-sm">Directions</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3 directions-sidebar-geocoder">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-start gap-2 min-w-0">
|
||||||
<label className="text-sm w-20 flex-shrink-0">Origin</label>
|
<label className="text-sm w-20 flex-shrink-0 pt-2">Origin</label>
|
||||||
<input
|
<div className="flex-1 min-w-0">
|
||||||
className="flex-1 min-w-0 px-3 py-2 rounded-md border border-black/10 bg-transparent text-sm"
|
<div className="p-1">
|
||||||
value={originText}
|
<GeocodeInput
|
||||||
onChange={(e) => setOriginText(e.target.value)}
|
mapRef={mapRef}
|
||||||
placeholder="lng,lat"
|
placeholder="Search origin"
|
||||||
|
value={originQuery}
|
||||||
|
onChange={(v) => { setOriginQuery(v); setOriginText(''); }}
|
||||||
|
onSelect={(f) => { const c = f.center; if (c && c.length === 2) { setOriginCoord([c[0], c[1]]); setOriginText(f.place_name || ''); setOriginQuery(f.place_name || ''); try { const m = mapRef.current; if (m) m.easeTo({ center: c, zoom: 14 }); } catch(e){} } }}
|
||||||
/>
|
/>
|
||||||
<button className="ml-2 px-3 py-1 rounded-md bg-white/5 text-sm flex-shrink-0" onClick={() => setPickMode('origin')}>Pick</button>
|
</div>
|
||||||
|
<div className="mt-2 text-xs text-gray-400 truncate">{originText}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-start gap-2 min-w-0">
|
||||||
<label className="text-sm w-20 flex-shrink-0">Destination</label>
|
<label className="text-sm w-20 flex-shrink-0 pt-2">Destination</label>
|
||||||
<input
|
<div className="flex-1 min-w-0">
|
||||||
className="flex-1 min-w-0 px-3 py-2 rounded-md border border-black/10 bg-transparent text-sm"
|
<div className="p-1">
|
||||||
value={destText}
|
<GeocodeInput
|
||||||
onChange={(e) => setDestText(e.target.value)}
|
mapRef={mapRef}
|
||||||
placeholder="lng,lat"
|
placeholder="Search destination"
|
||||||
|
value={destQuery}
|
||||||
|
onChange={(v) => { setDestQuery(v); setDestText(''); }}
|
||||||
|
onSelect={(f) => { const c = f.center; if (c && c.length === 2) { setDestCoord([c[0], c[1]]); setDestText(f.place_name || ''); setDestQuery(f.place_name || ''); try { const m = mapRef.current; if (m) m.easeTo({ center: c, zoom: 14 }); } catch(e){} } }}
|
||||||
/>
|
/>
|
||||||
<button className="ml-2 px-3 py-1 rounded-md bg-white/5 text-sm flex-shrink-0" onClick={() => setPickMode('dest')}>Pick</button>
|
</div>
|
||||||
|
<div className="mt-2 text-xs text-gray-400 truncate">{destText}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 mt-2">
|
<div className="flex gap-2 mt-2">
|
||||||
@@ -310,10 +358,9 @@ export default function DirectionsSidebar({ mapRef, profile = "mapbox/driving" }
|
|||||||
<button onClick={handleClear} className="px-4 py-2 rounded-lg border border-black/10 bg-transparent text-sm">Clear</button>
|
<button onClick={handleClear} className="px-4 py-2 rounded-lg border border-black/10 bg-transparent text-sm">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{pickMode && <div className="text-sm">Click on the map to set {pickMode}.</div>}
|
{/* pick-on-map mode removed; sidebar uses geocoder-only inputs */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
74
web/src/app/components/GeocodeInput.tsx
Normal file
74
web/src/app/components/GeocodeInput.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import mapboxgl from "mapbox-gl";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mapRef: React.MutableRefObject<mapboxgl.Map | null>;
|
||||||
|
placeholder?: string;
|
||||||
|
value?: string;
|
||||||
|
onChange?: (v: string) => void;
|
||||||
|
onSelect: (feature: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GeocodeInput({ mapRef, placeholder = 'Search', value = '', onChange, onSelect }: Props) {
|
||||||
|
const [query, setQuery] = useState<string>(value);
|
||||||
|
const [suggestions, setSuggestions] = useState<any[]>([]);
|
||||||
|
const timer = useRef<number | null>(null);
|
||||||
|
const mounted = useRef(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => { mounted.current = false; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value !== query) setQuery(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const fetchSuggestions = async (q: string) => {
|
||||||
|
const token = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || mapboxgl.accessToken || undefined;
|
||||||
|
if (!token) return [];
|
||||||
|
if (!q || q.trim().length === 0) return [];
|
||||||
|
const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(q)}.json?autocomplete=true&limit=6&types=place,locality,address,region,poi&access_token=${token}`;
|
||||||
|
try {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) return [];
|
||||||
|
const data = await res.json();
|
||||||
|
return data.features || [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (timer.current) window.clearTimeout(timer.current);
|
||||||
|
if (!query) { setSuggestions([]); return; }
|
||||||
|
timer.current = window.setTimeout(async () => {
|
||||||
|
const feats = await fetchSuggestions(query);
|
||||||
|
if (mounted.current) setSuggestions(feats);
|
||||||
|
}, 250) as unknown as number;
|
||||||
|
return () => { if (timer.current) window.clearTimeout(timer.current); };
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full bg-transparent text-white placeholder-gray-400 rounded-md"
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => { setQuery(e.target.value); onChange && onChange(e.target.value); }}
|
||||||
|
/>
|
||||||
|
{suggestions.length > 0 && (
|
||||||
|
<div className="absolute left-0 right-0 mt-1 bg-[#0b0b0c] border border-black/20 rounded-md overflow-hidden custom-suggestions">
|
||||||
|
{suggestions.map((f: any, i: number) => (
|
||||||
|
<button key={f.id || i} className="w-full text-left px-3 py-2 hover:bg-white/5" onClick={() => { onSelect(f); setSuggestions([]); }}>
|
||||||
|
<div className="font-medium">{f.text}</div>
|
||||||
|
{f.place_name && <div className="text-xs text-gray-400">{f.place_name.replace(f.text, '').replace(/^,\s*/, '')}</div>}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,21 +6,26 @@ export default function Legend() {
|
|||||||
return (
|
return (
|
||||||
<div style={{ position: 'absolute', left: 12, bottom: 12, zIndex: 12 }}>
|
<div style={{ position: 'absolute', left: 12, bottom: 12, zIndex: 12 }}>
|
||||||
<div style={{ background: 'var(--background)', color: 'var(--foreground)', padding: 8, borderRadius: 8, boxShadow: '0 6px 18px rgba(0,0,0,0.12)', border: '1px solid rgba(0,0,0,0.06)', fontSize: 12 }}>
|
<div style={{ background: 'var(--background)', color: 'var(--foreground)', padding: 8, borderRadius: 8, boxShadow: '0 6px 18px rgba(0,0,0,0.12)', border: '1px solid rgba(0,0,0,0.06)', fontSize: 12 }}>
|
||||||
<div style={{ fontSize: 12, fontWeight: 700, marginBottom: 6 }}>Density legend</div>
|
<div style={{ fontSize: 12, fontWeight: 700, marginBottom: 6 }}>Crash Density</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||||
<div style={{ width: 18, height: 12, background: 'rgba(0,120,48,0.0)', border: '1px solid rgba(0,0,0,0.06)' }} />
|
<div style={{ width: 18, height: 12, background: 'rgba(0,0,0,0)', border: '1px solid rgba(0,0,0,0.06)' }} />
|
||||||
<div style={{ width: 18, height: 12, background: 'rgba(34,139,34,0.8)' }} />
|
<div style={{ width: 18, height: 12, background: 'rgba(255,255,0,0.7)' }} />
|
||||||
<div style={{ width: 18, height: 12, background: 'rgba(154,205,50,0.9)' }} />
|
<div style={{ width: 18, height: 12, background: 'rgba(255,165,0,0.8)' }} />
|
||||||
<div style={{ width: 18, height: 12, background: 'rgba(255,215,0,0.95)' }} />
|
<div style={{ width: 18, height: 12, background: 'rgba(255,69,0,0.9)' }} />
|
||||||
<div style={{ width: 18, height: 12, background: 'rgba(255,140,0,0.95)' }} />
|
<div style={{ width: 18, height: 12, background: 'rgba(255,0,0,0.95)' }} />
|
||||||
<div style={{ width: 18, height: 12, background: 'rgba(215,25,28,1)' }} />
|
<div style={{ width: 18, height: 12, background: 'rgba(139,0,0,1)' }} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
<span style={{ fontSize: 11 }}>Low</span>
|
<span style={{ fontSize: 11 }}>Low</span>
|
||||||
<span style={{ fontSize: 11, fontWeight: 700 }}>High</span>
|
<span style={{ fontSize: 11, fontWeight: 700 }}>High</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ marginTop: 8, paddingTop: 6, borderTop: '1px solid rgba(0,0,0,0.06)' }}>
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--foreground-muted)' }}>
|
||||||
|
Real DC crash data (2010+)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,9 +3,19 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
import { generateDCPoints, haversine, PointFeature } from '../lib/mapUtils';
|
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
|
||||||
|
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
|
||||||
|
import { generateDCPoints, haversine, PointFeature, convertCrashDataToGeoJSON } from '../lib/mapUtils';
|
||||||
|
import { useCrashData } from '../hooks/useCrashData';
|
||||||
|
import { CrashData } from '../api/crashes/route';
|
||||||
|
|
||||||
export type PopupData = { lngLat: [number, number]; mag?: number; text?: string; stats?: { count: number; avg?: number; min?: number; max?: number; radiusMeters?: number } } | null;
|
export type PopupData = {
|
||||||
|
lngLat: [number, number];
|
||||||
|
mag?: number;
|
||||||
|
text?: string;
|
||||||
|
crashData?: CrashData;
|
||||||
|
stats?: { count: number; avg?: number; min?: number; max?: number; radiusMeters?: number }
|
||||||
|
} | null;
|
||||||
|
|
||||||
interface MapViewProps {
|
interface MapViewProps {
|
||||||
mapStyleChoice: 'dark' | 'streets';
|
mapStyleChoice: 'dark' | 'streets';
|
||||||
@@ -15,15 +25,109 @@ interface MapViewProps {
|
|||||||
pointsVisible: boolean;
|
pointsVisible: boolean;
|
||||||
onMapReady?: (map: mapboxgl.Map) => void;
|
onMapReady?: (map: mapboxgl.Map) => void;
|
||||||
onPopupCreate?: (p: PopupData) => void; // fires when user clicks features and we want to show popup
|
onPopupCreate?: (p: PopupData) => void; // fires when user clicks features and we want to show popup
|
||||||
|
onGeocoderResult?: (lngLat: [number, number]) => void;
|
||||||
|
useRealCrashData?: boolean; // whether to use real crash data or synthetic data
|
||||||
|
crashData?: CrashData[]; // external crash data to use
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MapView({ mapStyleChoice, heatRadius, heatIntensity, heatVisible, pointsVisible, onMapReady, onPopupCreate }: MapViewProps) {
|
export default function MapView({
|
||||||
|
mapStyleChoice,
|
||||||
|
heatRadius,
|
||||||
|
heatIntensity,
|
||||||
|
heatVisible,
|
||||||
|
pointsVisible,
|
||||||
|
onMapReady,
|
||||||
|
onPopupCreate,
|
||||||
|
onGeocoderResult,
|
||||||
|
useRealCrashData = true,
|
||||||
|
crashData = []
|
||||||
|
}: MapViewProps) {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const mapContainerRef = useRef<HTMLDivElement | null>(null);
|
const mapContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const mapRef = useRef<mapboxgl.Map | null>(null);
|
const mapRef = useRef<mapboxgl.Map | null>(null);
|
||||||
const styleChoiceRef = useRef<'dark' | 'streets'>(mapStyleChoice);
|
const styleChoiceRef = useRef<'dark' | 'streets'>(mapStyleChoice);
|
||||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||||
const dcDataRef = useRef<GeoJSON.FeatureCollection | null>(null);
|
const dcDataRef = useRef<GeoJSON.FeatureCollection | null>(null);
|
||||||
|
const crashDataHook = useCrashData({ autoLoad: false, limit: 10000 }); // Don't auto-load if external data provided
|
||||||
|
|
||||||
|
// Update map data when crash data is loaded
|
||||||
|
useEffect(() => {
|
||||||
|
const activeData = crashData.length > 0 ? crashData : crashDataHook.data;
|
||||||
|
console.log('MapView useEffect: crashData.length =', crashData.length, 'crashDataHook.data.length =', crashDataHook.data.length);
|
||||||
|
if (useRealCrashData && activeData.length > 0) {
|
||||||
|
console.log('Converting crash data to GeoJSON...');
|
||||||
|
dcDataRef.current = convertCrashDataToGeoJSON(activeData);
|
||||||
|
// Update the map source if map is ready
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (map && map.isStyleLoaded()) {
|
||||||
|
console.log('Updating map source with new data...');
|
||||||
|
if (map.getSource('dc-quakes')) {
|
||||||
|
(map.getSource('dc-quakes') as mapboxgl.GeoJSONSource).setData(dcDataRef.current);
|
||||||
|
} else {
|
||||||
|
console.log('Source not found, calling addDataAndLayers');
|
||||||
|
// Call the inner function manually - we need to recreate it here
|
||||||
|
if (dcDataRef.current) {
|
||||||
|
console.log('Adding data and layers, data has', dcDataRef.current.features.length, 'features');
|
||||||
|
if (!map.getSource('dc-quakes')) {
|
||||||
|
console.log('Creating new source');
|
||||||
|
map.addSource('dc-quakes', { type: 'geojson', data: dcDataRef.current });
|
||||||
|
}
|
||||||
|
// Add layers if they don't exist
|
||||||
|
if (!map.getLayer('dc-heat')) {
|
||||||
|
map.addLayer({
|
||||||
|
id: 'dc-heat', type: 'heatmap', source: 'dc-quakes', maxzoom: 15,
|
||||||
|
paint: {
|
||||||
|
'heatmap-weight': ['interpolate', ['linear'], ['get', 'mag'], 0, 0, 6, 1],
|
||||||
|
'heatmap-intensity': heatIntensity,
|
||||||
|
'heatmap-color': [
|
||||||
|
'interpolate',
|
||||||
|
['linear'],
|
||||||
|
['heatmap-density'],
|
||||||
|
0, 'rgba(0,0,0,0)',
|
||||||
|
0.2, 'rgba(255,255,0,0.7)',
|
||||||
|
0.4, 'rgba(255,165,0,0.8)',
|
||||||
|
0.6, 'rgba(255,69,0,0.9)',
|
||||||
|
0.8, 'rgba(255,0,0,0.95)',
|
||||||
|
1, 'rgba(139,0,0,1)'
|
||||||
|
],
|
||||||
|
'heatmap-radius': heatRadius,
|
||||||
|
'heatmap-opacity': ['interpolate', ['linear'], ['zoom'], 7, 1, 12, 0.8]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!map.getLayer('dc-point')) {
|
||||||
|
map.addLayer({
|
||||||
|
id: 'dc-point', type: 'circle', source: 'dc-quakes', minzoom: 12,
|
||||||
|
paint: {
|
||||||
|
'circle-radius': ['interpolate', ['linear'], ['get', 'mag'], 1, 3, 6, 10],
|
||||||
|
'circle-color': [
|
||||||
|
'interpolate',
|
||||||
|
['linear'],
|
||||||
|
['get', 'mag'],
|
||||||
|
1, styleChoiceRef.current === 'dark' ? '#ffff99' : '#ffa500',
|
||||||
|
3, styleChoiceRef.current === 'dark' ? '#ff6666' : '#ff4500',
|
||||||
|
6, styleChoiceRef.current === 'dark' ? '#ff0000' : '#8b0000'
|
||||||
|
] as any,
|
||||||
|
'circle-opacity': ['interpolate', ['linear'], ['zoom'], 12, 0.7, 14, 0.9],
|
||||||
|
'circle-stroke-width': 1,
|
||||||
|
'circle-stroke-color': styleChoiceRef.current === 'dark' ? '#ffffff' : '#000000'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Update layer visibility
|
||||||
|
if (map.getLayer('dc-heat')) {
|
||||||
|
map.setLayoutProperty('dc-heat', 'visibility', heatVisible ? 'visible' : 'none');
|
||||||
|
}
|
||||||
|
if (map.getLayer('dc-point')) {
|
||||||
|
map.setLayoutProperty('dc-point', 'visibility', pointsVisible ? 'visible' : 'none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Map style not loaded yet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [useRealCrashData, crashDataHook.data, crashData, heatRadius, heatIntensity, heatVisible, pointsVisible]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = containerRef.current;
|
const el = containerRef.current;
|
||||||
@@ -56,8 +160,19 @@ export default function MapView({ mapStyleChoice, heatRadius, heatIntensity, hea
|
|||||||
styleChoiceRef.current = mapStyleChoice;
|
styleChoiceRef.current = mapStyleChoice;
|
||||||
// if the dc-point layer exists, update its circle-color to match the style
|
// if the dc-point layer exists, update its circle-color to match the style
|
||||||
if (map.getLayer && map.getLayer('dc-point')) {
|
if (map.getLayer && map.getLayer('dc-point')) {
|
||||||
const color = mapStyleChoice === 'dark' ? '#ffffff' : '#000000';
|
const colorExpression = [
|
||||||
try { map.setPaintProperty('dc-point', 'circle-color', color); } catch (e) {}
|
'interpolate',
|
||||||
|
['linear'],
|
||||||
|
['get', 'mag'],
|
||||||
|
1, mapStyleChoice === 'dark' ? '#ffff99' : '#ffa500',
|
||||||
|
3, mapStyleChoice === 'dark' ? '#ff6666' : '#ff4500',
|
||||||
|
6, mapStyleChoice === 'dark' ? '#ff0000' : '#8b0000'
|
||||||
|
] as any;
|
||||||
|
const strokeColor = mapStyleChoice === 'dark' ? '#ffffff' : '#000000';
|
||||||
|
try {
|
||||||
|
map.setPaintProperty('dc-point', 'circle-color', colorExpression);
|
||||||
|
map.setPaintProperty('dc-point', 'circle-stroke-color', strokeColor);
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
}, [mapStyleChoice]);
|
}, [mapStyleChoice]);
|
||||||
|
|
||||||
@@ -84,7 +199,22 @@ export default function MapView({ mapStyleChoice, heatRadius, heatIntensity, hea
|
|||||||
mapRef.current = new mapboxgl.Map({ container: mapEl, style: styleUrl, center: [-77.0369, 38.9072], zoom: 11, maxBounds: dcBounds });
|
mapRef.current = new mapboxgl.Map({ container: mapEl, style: styleUrl, center: [-77.0369, 38.9072], zoom: 11, maxBounds: dcBounds });
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
|
|
||||||
if (!dcDataRef.current) dcDataRef.current = generateDCPoints(900);
|
// NOTE: geocoder control intentionally removed from map-level UI.
|
||||||
|
// The sidebar provides embedded geocoder inputs; keeping both leads to duplicate controls.
|
||||||
|
|
||||||
|
// Initialize data based on preference
|
||||||
|
const activeData = crashData.length > 0 ? crashData : crashDataHook.data;
|
||||||
|
console.log('Initializing map data, activeData length:', activeData.length);
|
||||||
|
if (useRealCrashData && activeData.length > 0) {
|
||||||
|
console.log('Using real crash data');
|
||||||
|
dcDataRef.current = convertCrashDataToGeoJSON(activeData);
|
||||||
|
} else if (!useRealCrashData) {
|
||||||
|
console.log('Using synthetic data');
|
||||||
|
dcDataRef.current = generateDCPoints(900);
|
||||||
|
} else {
|
||||||
|
console.log('No data available yet, using empty data');
|
||||||
|
dcDataRef.current = { type: 'FeatureCollection' as const, features: [] };
|
||||||
|
}
|
||||||
|
|
||||||
const computeNearbyStats = (center: [number, number], radiusMeters = 500) => {
|
const computeNearbyStats = (center: [number, number], radiusMeters = 500) => {
|
||||||
const data = dcDataRef.current;
|
const data = dcDataRef.current;
|
||||||
@@ -101,11 +231,18 @@ export default function MapView({ mapStyleChoice, heatRadius, heatIntensity, hea
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addDataAndLayers = () => {
|
const addDataAndLayers = () => {
|
||||||
if (!map || !dcDataRef.current) return;
|
if (!map || !dcDataRef.current) {
|
||||||
|
console.log('addDataAndLayers: map or data not ready', !!map, !!dcDataRef.current);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Adding data and layers, data has', dcDataRef.current.features.length, 'features');
|
||||||
|
|
||||||
if (!map.getSource('dc-quakes')) {
|
if (!map.getSource('dc-quakes')) {
|
||||||
|
console.log('Creating new source');
|
||||||
map.addSource('dc-quakes', { type: 'geojson', data: dcDataRef.current });
|
map.addSource('dc-quakes', { type: 'geojson', data: dcDataRef.current });
|
||||||
} else {
|
} else {
|
||||||
|
console.log('Updating existing source');
|
||||||
(map.getSource('dc-quakes') as mapboxgl.GeoJSONSource).setData(dcDataRef.current);
|
(map.getSource('dc-quakes') as mapboxgl.GeoJSONSource).setData(dcDataRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +252,17 @@ export default function MapView({ mapStyleChoice, heatRadius, heatIntensity, hea
|
|||||||
paint: {
|
paint: {
|
||||||
'heatmap-weight': ['interpolate', ['linear'], ['get', 'mag'], 0, 0, 6, 1],
|
'heatmap-weight': ['interpolate', ['linear'], ['get', 'mag'], 0, 0, 6, 1],
|
||||||
'heatmap-intensity': heatIntensity,
|
'heatmap-intensity': heatIntensity,
|
||||||
'heatmap-color': ['interpolate', ['linear'], ['heatmap-density'], 0, 'rgba(0,120,48,0)', 0.2, 'rgba(34,139,34,0.8)', 0.4, 'rgba(154,205,50,0.9)', 0.6, 'rgba(255,215,0,0.95)', 0.8, 'rgba(255,140,0,0.95)', 1, 'rgba(215,25,28,1)'],
|
'heatmap-color': [
|
||||||
|
'interpolate',
|
||||||
|
['linear'],
|
||||||
|
['heatmap-density'],
|
||||||
|
0, 'rgba(0,0,0,0)',
|
||||||
|
0.2, 'rgba(255,255,0,0.7)',
|
||||||
|
0.4, 'rgba(255,165,0,0.8)',
|
||||||
|
0.6, 'rgba(255,69,0,0.9)',
|
||||||
|
0.8, 'rgba(255,0,0,0.95)',
|
||||||
|
1, 'rgba(139,0,0,1)'
|
||||||
|
],
|
||||||
'heatmap-radius': heatRadius,
|
'heatmap-radius': heatRadius,
|
||||||
'heatmap-opacity': ['interpolate', ['linear'], ['zoom'], 7, 1, 12, 0.8]
|
'heatmap-opacity': ['interpolate', ['linear'], ['zoom'], 7, 1, 12, 0.8]
|
||||||
}
|
}
|
||||||
@@ -126,9 +273,18 @@ export default function MapView({ mapStyleChoice, heatRadius, heatIntensity, hea
|
|||||||
map.addLayer({
|
map.addLayer({
|
||||||
id: 'dc-point', type: 'circle', source: 'dc-quakes', minzoom: 12,
|
id: 'dc-point', type: 'circle', source: 'dc-quakes', minzoom: 12,
|
||||||
paint: {
|
paint: {
|
||||||
'circle-radius': ['interpolate', ['linear'], ['get', 'mag'], 1, 2, 6, 8],
|
'circle-radius': ['interpolate', ['linear'], ['get', 'mag'], 1, 3, 6, 10],
|
||||||
'circle-color': styleChoiceRef.current === 'dark' ? '#ffffff' : '#a9a9a9',
|
'circle-color': [
|
||||||
'circle-opacity': ['interpolate', ['linear'], ['zoom'], 12, 0, 14, 1]
|
'interpolate',
|
||||||
|
['linear'],
|
||||||
|
['get', 'mag'],
|
||||||
|
1, styleChoiceRef.current === 'dark' ? '#ffff99' : '#ffa500',
|
||||||
|
3, styleChoiceRef.current === 'dark' ? '#ff6666' : '#ff4500',
|
||||||
|
6, styleChoiceRef.current === 'dark' ? '#ff0000' : '#8b0000'
|
||||||
|
],
|
||||||
|
'circle-opacity': ['interpolate', ['linear'], ['zoom'], 12, 0.7, 14, 0.9],
|
||||||
|
'circle-stroke-width': 1,
|
||||||
|
'circle-stroke-color': styleChoiceRef.current === 'dark' ? '#ffffff' : '#000000'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -144,6 +300,7 @@ export default function MapView({ mapStyleChoice, heatRadius, heatIntensity, hea
|
|||||||
};
|
};
|
||||||
|
|
||||||
map.on('load', () => {
|
map.on('load', () => {
|
||||||
|
console.log('Map loaded, adding initial data and layers');
|
||||||
addDataAndLayers();
|
addDataAndLayers();
|
||||||
// ensure map is fit to DC bounds initially
|
// ensure map is fit to DC bounds initially
|
||||||
try { map.fitBounds(dcBounds, { padding: 20 }); } catch (e) { /* ignore if fitBounds fails */ }
|
try { map.fitBounds(dcBounds, { padding: 20 }); } catch (e) { /* ignore if fitBounds fails */ }
|
||||||
@@ -153,8 +310,20 @@ export default function MapView({ mapStyleChoice, heatRadius, heatIntensity, hea
|
|||||||
if (!feature) return;
|
if (!feature) return;
|
||||||
const coords = (feature.geometry as any).coordinates.slice() as [number, number];
|
const coords = (feature.geometry as any).coordinates.slice() as [number, number];
|
||||||
const mag = feature.properties ? feature.properties.mag : undefined;
|
const mag = feature.properties ? feature.properties.mag : undefined;
|
||||||
|
const crashData = feature.properties ? feature.properties.crashData : undefined;
|
||||||
const stats = computeNearbyStats(coords, 500);
|
const stats = computeNearbyStats(coords, 500);
|
||||||
if (onPopupCreate) onPopupCreate({ lngLat: coords, mag, text: `Magnitude: ${mag ?? 'N/A'}`, stats });
|
|
||||||
|
let text = `Severity: ${mag ?? 'N/A'}`;
|
||||||
|
if (crashData) {
|
||||||
|
text = `Crash Report
|
||||||
|
Date: ${new Date(crashData.reportDate).toLocaleDateString()}
|
||||||
|
Address: ${crashData.address}
|
||||||
|
Vehicles: ${crashData.totalVehicles} | Pedestrians: ${crashData.totalPedestrians} | Bicycles: ${crashData.totalBicycles}
|
||||||
|
Fatalities: ${crashData.fatalDriver + crashData.fatalPedestrian + crashData.fatalBicyclist}
|
||||||
|
Major Injuries: ${crashData.majorInjuriesDriver + crashData.majorInjuriesPedestrian + crashData.majorInjuriesBicyclist}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onPopupCreate) onPopupCreate({ lngLat: coords, mag, crashData, text, stats });
|
||||||
});
|
});
|
||||||
|
|
||||||
map.on('click', 'dc-heat', (e) => {
|
map.on('click', 'dc-heat', (e) => {
|
||||||
@@ -165,11 +334,23 @@ export default function MapView({ mapStyleChoice, heatRadius, heatIntensity, hea
|
|||||||
const f = nearby[0];
|
const f = nearby[0];
|
||||||
const coords = (f.geometry as any).coordinates.slice() as [number, number];
|
const coords = (f.geometry as any).coordinates.slice() as [number, number];
|
||||||
const mag = f.properties ? f.properties.mag : undefined;
|
const mag = f.properties ? f.properties.mag : undefined;
|
||||||
|
const crashData = f.properties ? f.properties.crashData : undefined;
|
||||||
const stats = computeNearbyStats(coords, 500);
|
const stats = computeNearbyStats(coords, 500);
|
||||||
if (onPopupCreate) onPopupCreate({ lngLat: coords, mag, text: `Magnitude: ${mag ?? 'N/A'}`, stats });
|
|
||||||
|
let text = `Severity: ${mag ?? 'N/A'}`;
|
||||||
|
if (crashData) {
|
||||||
|
text = `Crash Report
|
||||||
|
Date: ${new Date(crashData.reportDate).toLocaleDateString()}
|
||||||
|
Address: ${crashData.address}
|
||||||
|
Vehicles: ${crashData.totalVehicles} | Pedestrians: ${crashData.totalPedestrians} | Bicycles: ${crashData.totalBicycles}
|
||||||
|
Fatalities: ${crashData.fatalDriver + crashData.fatalPedestrian + crashData.fatalBicyclist}
|
||||||
|
Major Injuries: ${crashData.majorInjuriesDriver + crashData.majorInjuriesPedestrian + crashData.majorInjuriesBicyclist}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onPopupCreate) onPopupCreate({ lngLat: coords, mag, crashData, text, stats });
|
||||||
} else {
|
} else {
|
||||||
const stats = computeNearbyStats([e.lngLat.lng, e.lngLat.lat], 500);
|
const stats = computeNearbyStats([e.lngLat.lng, e.lngLat.lat], 500);
|
||||||
if (onPopupCreate) onPopupCreate({ lngLat: [e.lngLat.lng, e.lngLat.lat], text: 'Zoom in to see individual points and details', stats });
|
if (onPopupCreate) onPopupCreate({ lngLat: [e.lngLat.lng, e.lngLat.lat], text: 'Zoom in to see individual crash reports and details', stats });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -194,3 +194,105 @@ body {
|
|||||||
|
|
||||||
/* Directions sidebar (left-side, full-height, collapsible) */
|
/* Directions sidebar (left-side, full-height, collapsible) */
|
||||||
/* Directions sidebar styling is now handled via Tailwind classes in the component. Legacy CSS removed. */
|
/* Directions sidebar styling is now handled via Tailwind classes in the component. Legacy CSS removed. */
|
||||||
|
|
||||||
|
/* MapboxGeocoder styling override scoped to the directions sidebar */
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder input[type="text"],
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder .mapboxgl-ctrl-geocoder--input {
|
||||||
|
background: #0f1112; /* match sidebar */
|
||||||
|
color: #e6eef8; /* light text */
|
||||||
|
border: 1px solid rgba(255,255,255,0.06);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder input[type="text"]::placeholder,
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder .mapboxgl-ctrl-geocoder--input::placeholder {
|
||||||
|
color: rgba(230,238,248,0.5);
|
||||||
|
}
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder input[type="text"]:focus,
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder .mapboxgl-ctrl-geocoder--input:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 126, 95, 0.12);
|
||||||
|
border-color: rgba(255,126,95,0.6);
|
||||||
|
}
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder--suggestions,
|
||||||
|
.directions-sidebar-geocoder .suggestions {
|
||||||
|
background: #0b0b0c; /* dropdown bg */
|
||||||
|
color: #e6eef8;
|
||||||
|
border: 1px solid rgba(255,255,255,0.04);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directions-sidebar-geocoder .custom-suggestions { z-index: 60; }
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder--suggestions .suggestion,
|
||||||
|
.directions-sidebar-geocoder .suggestions .suggestion {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder--suggestions .suggestion:hover,
|
||||||
|
.directions-sidebar-geocoder .suggestions .suggestion:hover {
|
||||||
|
background: rgba(255,255,255,0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* custom inline geocoder input and dropdown styling */
|
||||||
|
.directions-sidebar-geocoder input[type="text"] {
|
||||||
|
background: #0f1112;
|
||||||
|
color: #e6eef8;
|
||||||
|
border: 1px solid rgba(255,255,255,0.06);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.directions-sidebar-geocoder .custom-suggestions button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
.directions-sidebar-geocoder .custom-suggestions button:hover { background: rgba(255,255,255,0.03); }
|
||||||
|
|
||||||
|
/* hide the magnifying/search icon inside the embedded geocoder input */
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder .mapboxgl-ctrl-geocoder--icon {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* make the geocoder control and input expand to the container width */
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder .mapboxgl-ctrl-geocoder--input {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Strong overrides to ensure the geocoder input fits into the sidebar layout */
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder {
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder input[type="text"],
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder .mapboxgl-ctrl-geocoder--input {
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
padding: 0.5rem 0.85rem !important;
|
||||||
|
font-size: 0.95rem !important;
|
||||||
|
line-height: 1.2 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder .mapboxgl-ctrl-geocoder--icon,
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder .mapboxgl-ctrl-geocoder--button {
|
||||||
|
/* hide icon and buttons that overlap the input; keep clear if needed */
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.directions-sidebar-geocoder .mapboxgl-ctrl-geocoder--suggestions {
|
||||||
|
width: calc(100% - 0px) !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|||||||
87
web/src/app/hooks/useCrashData.ts
Normal file
87
web/src/app/hooks/useCrashData.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { CrashData, CrashResponse } from '../api/crashes/route';
|
||||||
|
|
||||||
|
export interface UseCrashDataOptions {
|
||||||
|
autoLoad?: boolean;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseCrashDataResult {
|
||||||
|
data: CrashData[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
pagination: CrashResponse['pagination'] | null;
|
||||||
|
loadPage: (page: number) => Promise<void>;
|
||||||
|
loadMore: () => Promise<void>;
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCrashData(options: UseCrashDataOptions = {}): UseCrashDataResult {
|
||||||
|
const { autoLoad = true, limit = 100 } = options;
|
||||||
|
|
||||||
|
const [data, setData] = useState<CrashData[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [pagination, setPagination] = useState<CrashResponse['pagination'] | null>(null);
|
||||||
|
|
||||||
|
const fetchCrashData = useCallback(async (page: number, append: boolean = false) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const response = await fetch(`/api/crashes?page=${page}&limit=${limit}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch crash data: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: CrashResponse = await response.json();
|
||||||
|
|
||||||
|
if (append) {
|
||||||
|
setData(prevData => [...prevData, ...result.data]);
|
||||||
|
} else {
|
||||||
|
setData(result.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPagination(result.pagination);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch crash data';
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error('Error fetching crash data:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [limit]);
|
||||||
|
|
||||||
|
const loadPage = useCallback((page: number) => {
|
||||||
|
return fetchCrashData(page, false);
|
||||||
|
}, [fetchCrashData]);
|
||||||
|
|
||||||
|
const loadMore = useCallback(() => {
|
||||||
|
if (!pagination || !pagination.hasNext || loading) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return fetchCrashData(pagination.page + 1, true);
|
||||||
|
}, [pagination, loading, fetchCrashData]);
|
||||||
|
|
||||||
|
const refresh = useCallback(() => {
|
||||||
|
return fetchCrashData(1, false);
|
||||||
|
}, [fetchCrashData]);
|
||||||
|
|
||||||
|
// Auto-load first page on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoLoad) {
|
||||||
|
loadPage(1);
|
||||||
|
}
|
||||||
|
}, [autoLoad, loadPage]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
pagination,
|
||||||
|
loadPage,
|
||||||
|
loadMore,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
export type PointFeature = GeoJSON.Feature<GeoJSON.Point, { mag: number }>;
|
import { CrashData } from '../api/crashes/route';
|
||||||
|
|
||||||
|
export type PointFeature = GeoJSON.Feature<GeoJSON.Point, { mag: number; crashData: CrashData }>;
|
||||||
|
|
||||||
export const haversine = (a: [number, number], b: [number, number]) => {
|
export const haversine = (a: [number, number], b: [number, number]) => {
|
||||||
const toRad = (v: number) => v * Math.PI / 180;
|
const toRad = (v: number) => v * Math.PI / 180;
|
||||||
@@ -14,6 +16,42 @@ export const haversine = (a: [number, number], b: [number, number]) => {
|
|||||||
return R * c;
|
return R * c;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const convertCrashDataToGeoJSON = (crashes: CrashData[]): GeoJSON.FeatureCollection => {
|
||||||
|
console.log('Converting crash data to GeoJSON:', crashes.length, 'crashes');
|
||||||
|
console.log('Sample crash data:', crashes[0]);
|
||||||
|
|
||||||
|
const features: PointFeature[] = crashes.map((crash) => {
|
||||||
|
// Calculate severity score based on fatalities and major injuries
|
||||||
|
const severityScore = Math.max(1,
|
||||||
|
(crash.fatalDriver + crash.fatalPedestrian + crash.fatalBicyclist) * 3 +
|
||||||
|
(crash.majorInjuriesDriver + crash.majorInjuriesPedestrian + crash.majorInjuriesBicyclist) * 2 +
|
||||||
|
(crash.totalVehicles + crash.totalPedestrians + crash.totalBicycles)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [crash.longitude, crash.latitude]
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
mag: Math.min(6, severityScore), // Cap at 6 for consistent visualization
|
||||||
|
crashData: crash
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const geoJSON = {
|
||||||
|
type: 'FeatureCollection' as const,
|
||||||
|
features
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Generated GeoJSON with', features.length, 'features');
|
||||||
|
console.log('Sample feature:', features[0]);
|
||||||
|
|
||||||
|
return geoJSON;
|
||||||
|
};
|
||||||
|
|
||||||
export const generateDCPoints = (count = 500) => {
|
export const generateDCPoints = (count = 500) => {
|
||||||
const center = { lon: -77.0369, lat: 38.9072 };
|
const center = { lon: -77.0369, lat: 38.9072 };
|
||||||
const features: PointFeature[] = [];
|
const features: PointFeature[] = [];
|
||||||
@@ -31,7 +69,32 @@ export const generateDCPoints = (count = 500) => {
|
|||||||
const lon = center.lon + Math.cos(angle) * radius;
|
const lon = center.lon + Math.cos(angle) * radius;
|
||||||
const lat = center.lat + Math.sin(angle) * radius;
|
const lat = center.lat + Math.sin(angle) * radius;
|
||||||
const mag = Math.round(Math.max(1, Math.abs(randNormal()) * 6));
|
const mag = Math.round(Math.max(1, Math.abs(randNormal()) * 6));
|
||||||
features.push({ type: 'Feature', geometry: { type: 'Point', coordinates: [lon, lat] }, properties: { mag } });
|
|
||||||
|
// Create synthetic crash data for backward compatibility
|
||||||
|
const syntheticCrash: CrashData = {
|
||||||
|
id: `synthetic-${i}`,
|
||||||
|
latitude: lat,
|
||||||
|
longitude: lon,
|
||||||
|
reportDate: new Date().toISOString(),
|
||||||
|
address: `Synthetic Location ${i}`,
|
||||||
|
ward: 'Ward 1',
|
||||||
|
totalVehicles: Math.floor(Math.random() * 3) + 1,
|
||||||
|
totalPedestrians: Math.floor(Math.random() * 2),
|
||||||
|
totalBicycles: Math.floor(Math.random() * 2),
|
||||||
|
fatalDriver: 0,
|
||||||
|
fatalPedestrian: 0,
|
||||||
|
fatalBicyclist: 0,
|
||||||
|
majorInjuriesDriver: Math.floor(Math.random() * 2),
|
||||||
|
majorInjuriesPedestrian: 0,
|
||||||
|
majorInjuriesBicyclist: 0,
|
||||||
|
speedingInvolved: Math.floor(Math.random() * 2),
|
||||||
|
};
|
||||||
|
|
||||||
|
features.push({
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: { type: 'Point', coordinates: [lon, lat] },
|
||||||
|
properties: { mag, crashData: syntheticCrash }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: 'FeatureCollection', features } as GeoJSON.FeatureCollection<GeoJSON.Geometry>;
|
return { type: 'FeatureCollection', features } as GeoJSON.FeatureCollection<GeoJSON.Geometry>;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import PopupOverlay from './components/PopupOverlay';
|
|||||||
import Legend from './components/Legend';
|
import Legend from './components/Legend';
|
||||||
import MapNavigationControl from './components/MapNavigationControl';
|
import MapNavigationControl from './components/MapNavigationControl';
|
||||||
import DirectionsSidebar from './components/DirectionsSidebar';
|
import DirectionsSidebar from './components/DirectionsSidebar';
|
||||||
|
import CrashDataControls from './components/CrashDataControls';
|
||||||
|
import { useCrashData } from './hooks/useCrashData';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const mapRef = useRef<any>(null);
|
const mapRef = useRef<any>(null);
|
||||||
@@ -21,13 +23,16 @@ export default function Home() {
|
|||||||
const [popup, setPopup] = useState<PopupData>(null);
|
const [popup, setPopup] = useState<PopupData>(null);
|
||||||
const [popupVisible, setPopupVisible] = useState(false);
|
const [popupVisible, setPopupVisible] = useState(false);
|
||||||
|
|
||||||
|
// Shared crash data state
|
||||||
|
const crashDataHook = useCrashData({ autoLoad: true, limit: 10000 });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'row' }}>
|
<div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'row' }}>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
<div style={{ flex: 1, position: 'relative', minWidth: 0 }}>
|
||||||
|
{/* Render sidebar as an overlay inside the map container so collapsing doesn't shift layout */}
|
||||||
|
<div style={{ position: 'absolute', left: 0, top: 0, height: '100%', zIndex: 40, pointerEvents: 'auto' }}>
|
||||||
<DirectionsSidebar mapRef={mapRef} profile="mapbox/driving" />
|
<DirectionsSidebar mapRef={mapRef} profile="mapbox/driving" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ flex: 1, position: 'relative', minWidth: 0 }}>
|
|
||||||
<ControlsPanel
|
<ControlsPanel
|
||||||
panelOpen={panelOpen}
|
panelOpen={panelOpen}
|
||||||
onTogglePanel={(next) => { setPanelOpen(next); try { window.localStorage.setItem('map_panel_open', next ? '1' : '0'); } catch (e) {} }}
|
onTogglePanel={(next) => { setPanelOpen(next); try { window.localStorage.setItem('map_panel_open', next ? '1' : '0'); } catch (e) {} }}
|
||||||
@@ -49,12 +54,18 @@ export default function Home() {
|
|||||||
heatIntensity={heatIntensity}
|
heatIntensity={heatIntensity}
|
||||||
heatVisible={heatVisible}
|
heatVisible={heatVisible}
|
||||||
pointsVisible={pointsVisible}
|
pointsVisible={pointsVisible}
|
||||||
|
useRealCrashData={true}
|
||||||
|
crashData={crashDataHook.data}
|
||||||
onMapReady={(m) => { mapRef.current = m; }}
|
onMapReady={(m) => { mapRef.current = m; }}
|
||||||
onPopupCreate={(p) => { setPopupVisible(false); setPopup(p); requestAnimationFrame(() => setPopupVisible(true)); }}
|
onPopupCreate={(p) => { setPopupVisible(false); setPopup(p); requestAnimationFrame(() => setPopupVisible(true)); }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Native Mapbox navigation control (zoom + compass) */}
|
{/* Native Mapbox navigation control (zoom + compass) */}
|
||||||
<MapNavigationControl mapRef={mapRef} position="top-right" />
|
<MapNavigationControl mapRef={mapRef} position="top-right" />
|
||||||
|
|
||||||
|
{/* Crash data loading controls */}
|
||||||
|
<CrashDataControls crashDataHook={crashDataHook} />
|
||||||
|
|
||||||
<Legend />
|
<Legend />
|
||||||
<PopupOverlay popup={popup} popupVisible={popupVisible} mapRef={mapRef} onClose={() => { setPopupVisible(false); setTimeout(() => setPopup(null), 220); }} />
|
<PopupOverlay popup={popup} popupVisible={popupVisible} mapRef={mapRef} onClose={() => { setPopupVisible(false); setTimeout(() => setPopup(null), 220); }} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
19
web/src/types/mapbox-gl-geocoder.d.ts
vendored
Normal file
19
web/src/types/mapbox-gl-geocoder.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
declare module '@mapbox/mapbox-gl-geocoder' {
|
||||||
|
import mapboxgl from 'mapbox-gl';
|
||||||
|
interface GeocoderOptions {
|
||||||
|
accessToken?: string;
|
||||||
|
mapboxgl?: typeof mapboxgl;
|
||||||
|
placeholder?: string;
|
||||||
|
bbox?: number[];
|
||||||
|
proximity?: { longitude: number; latitude: number };
|
||||||
|
countries?: string | string[];
|
||||||
|
types?: string | string[];
|
||||||
|
minLength?: number;
|
||||||
|
}
|
||||||
|
class MapboxGeocoder {
|
||||||
|
constructor(options?: GeocoderOptions);
|
||||||
|
on(event: string, cb: (ev: any) => void): this;
|
||||||
|
off(event: string, cb: (ev: any) => void): this;
|
||||||
|
}
|
||||||
|
export default MapboxGeocoder;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user