From 8d963a59b3c683391d2c4bbb427d4a1d933fbd40 Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Thu, 10 Sep 2026 09:17:18 -0400 Subject: [PATCH] Apply dependabot bumps; migrate to maplibre-gl 6 Named imports, Vite worker URL via setWorkerUrl, and the single missing-image resolver replacing the styleimagemissing listeners. --- package-lock.json | 309 +++++++++++++--------------- package.json | 6 +- src/features/map/MapView.tsx | 4 +- src/features/map/PacketPathMap.tsx | 14 +- src/features/map/maplibre-worker.ts | 6 + src/features/map/node-icons.ts | 2 +- src/features/map/useMapLibre.ts | 28 +-- src/features/map/useMapNodes.ts | 15 +- 8 files changed, 183 insertions(+), 201 deletions(-) create mode 100644 src/features/map/maplibre-worker.ts diff --git a/package-lock.json b/package-lock.json index 5586260..a57c914 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,10 @@ "@tanstack/react-query": "^5.100.11", "@tanstack/react-virtual": "^3.13.25", "echarts": "6.1.0", - "maplibre-gl": "^5.24.0", + "maplibre-gl": "^6.9.0", "react": "^19.2.6", "react-dom": "^19.2.6", - "react-router-dom": "^7.15.1" + "react-router-dom": "^7.18.2" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -36,7 +36,7 @@ "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", "vite": "^8.0.12", - "vitest": "^4.1.7" + "vitest": "^4.1.11" } }, "node_modules/@adobe/css-tools": { @@ -821,11 +821,12 @@ } }, "node_modules/@mapbox/jsonlint-lines-primitives": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", - "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.3.tgz", + "integrity": "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg==", + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 22" } }, "node_modules/@mapbox/point-geometry": { @@ -841,47 +842,38 @@ "license": "BSD-2-Clause" }, "node_modules/@mapbox/unitbezier": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", - "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz", + "integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==", "license": "BSD-2-Clause" }, "node_modules/@mapbox/vector-tile": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.5.tgz", - "integrity": "sha512-pXj8m7KTsqZt+1jsE0xIpGvqTSbblfkuEJL/NJmNePMtEwxO8V3XMDo9WMSfDeqHvCtBI9Lmt4mGcGR10zecmw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-3.0.0.tgz", + "integrity": "sha512-Qf10S1uIHMk20ri/IVBnpS+esUEkVaR5Hftmz88jTInrpmWgPGJfPe3LVjjlE77trLx8tH6qjTG7uWH9hIq/0Q==", "license": "BSD-3-Clause", "dependencies": { "@mapbox/point-geometry": "~1.1.0", "@types/geojson": "^7946.0.16", - "pbf": "^4.0.2" - } - }, - "node_modules/@mapbox/whoots-js": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", - "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", - "license": "ISC", - "engines": { - "node": ">=6.0.0" + "pbf": "^5.0.0" } }, "node_modules/@maplibre/geojson-vt": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.0.tgz", - "integrity": "sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.1.tgz", + "integrity": "sha512-FVMOcmSP/yqol45t7StApEyTL5/vmqBCuFhH9n+fFuINenhaX+YgHHIt1yJ86S8kln3uJLcMvmEU2cfn6E2eCQ==", "license": "ISC", "dependencies": { - "kdbush": "^4.0.2" + "kdbush": "^4.1.0" } }, "node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "24.10.0", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.10.0.tgz", - "integrity": "sha512-lichxSiagMEBBrqHF0trtMQH9RKh+9jUlIJl0qW0QHvt2H/tbvUWdE+ZzI2Jd0/pT7j/iavLonlPu7EQ/ixTOw==", + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-26.4.2.tgz", + "integrity": "sha512-6J0vZqMZvRAJKtdWJdDGHEh1YJ2ZHG08/GOur8gCArYhO8ZkM//OXqtI2AAEz4jc2G+Wq4MfcePg8qNK5TZ4Kg==", "license": "ISC", "dependencies": { - "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.3", "@mapbox/unitbezier": "^1.0.0", "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", @@ -894,16 +886,10 @@ "gl-style-validate": "dist/gl-style-validate.mjs" } }, - "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz", - "integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==", - "license": "BSD-2-Clause" - }, "node_modules/@maplibre/mlt": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.11.tgz", - "integrity": "sha512-dKvjKdITw9d0y3ndGkSqLUEpWCizMtdq8NB06cHohH/JZ2sJoM7dClR9wzJLUWykjbw9RXDFmhjjNBnNW27mzw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.2.1.tgz", + "integrity": "sha512-5n5dgolE2EYxwCKgx8vlwURCB8A+kyfJJyThlYimjlGgOcOe2Bhw9VxxnnHC91OC9PgHg+nVdgVPTYPkIo62vg==", "license": "(MIT OR Apache-2.0)", "dependencies": { "@mapbox/point-geometry": "^1.1.0" @@ -920,18 +906,6 @@ "pbf": "^5.1.0" } }, - "node_modules/@maplibre/vt-pbf/node_modules/pbf": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.0.tgz", - "integrity": "sha512-Wv0yo0+uZepnoNEKsquhar1F18LogB8oeEikIhUXG16udbiXG7JecHGySwoo6kuMgjmbQYzdrTZlO+/K9t8eZg==", - "license": "BSD-3-Clause", - "dependencies": { - "resolve-protobuf-schema": "^2.1.0" - }, - "bin": { - "pbf": "bin/pbf" - } - }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -2077,16 +2051,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", - "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -2095,13 +2069,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", - "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.8", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2122,9 +2096,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", - "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -2135,13 +2109,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", - "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.8", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -2149,14 +2123,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", - "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.8", - "@vitest/utils": "4.1.8", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2165,9 +2139,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", - "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -2175,13 +2149,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", - "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.8", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -2285,9 +2259,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.35", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz", - "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==", + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2298,32 +2272,31 @@ } }, "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", "license": "MIT", "dependencies": { "require-from-string": "^2.0.2" } }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", "dev": true, "funding": [ { @@ -2341,11 +2314,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" @@ -2355,9 +2328,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001797", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", - "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, "funding": [ { @@ -2523,9 +2496,9 @@ "peer": true }, "node_modules/earcut": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", - "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.3.tgz", + "integrity": "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==", "license": "ISC" }, "node_modules/echarts": { @@ -2539,9 +2512,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.371", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", - "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==", + "version": "1.5.426", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.426.tgz", + "integrity": "sha512-2Gcq6inCQs/AqfHP3f5ftCzk+pqeW2VlA1LgmPqEj2hfBO8HiZRspNYkD4HzFBe4u6lGqca4BspFr6Ix4Q400g==", "dev": true, "license": "ISC" }, @@ -3515,27 +3488,26 @@ } }, "node_modules/maplibre-gl": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz", - "integrity": "sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-6.9.0.tgz", + "integrity": "sha512-vFMwMK0Zs+NM/rOMSdtu8bO30DIexhBEVi5KC6f70/XtI+L/K2wC3LsDCAXFZ4s8ik5gAuDugfCNbpllpdJ9bA==", "license": "BSD-3-Clause", "dependencies": { - "@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/point-geometry": "^1.1.0", - "@mapbox/tiny-sdf": "^2.1.0", - "@mapbox/unitbezier": "^0.0.1", - "@mapbox/vector-tile": "^2.0.4", - "@mapbox/whoots-js": "^3.1.0", - "@maplibre/geojson-vt": "^6.1.0", - "@maplibre/maplibre-gl-style-spec": "^24.8.1", - "@maplibre/mlt": "^1.1.8", - "@maplibre/vt-pbf": "^4.3.0", + "@mapbox/tiny-sdf": "^2.2.0", + "@mapbox/unitbezier": "^1.0.0", + "@mapbox/vector-tile": "^3.0.0", + "@maplibre/geojson-vt": "^6.1.1", + "@maplibre/maplibre-gl-style-spec": "^26.4.2", + "@maplibre/mlt": "^1.2.1", + "@maplibre/vt-pbf": "^4.3.2", "@types/geojson": "^7946.0.16", - "earcut": "^3.0.2", + "bidi-js": "^1.1.0", + "earcut": "^3.2.3", "gl-matrix": "^3.4.4", - "kdbush": "^4.0.2", + "kdbush": "^4.1.0", "murmurhash-js": "^1.0.0", - "pbf": "^4.0.1", + "pbf": "^5.1.2", "potpack": "^2.1.0", "quickselect": "^3.0.0", "tinyqueue": "^3.0.0" @@ -3604,9 +3576,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -3630,9 +3602,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.47", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", - "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", "dev": true, "license": "MIT", "engines": { @@ -3744,9 +3716,9 @@ "license": "MIT" }, "node_modules/pbf": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.2.tgz", - "integrity": "sha512-J0ajxARhZfpUEebxYs1vhMGMuLSXtBe1e+fFPDrf2uA2hgo+UshKfNUWOz92HJNz6/NFEXseQPddnHkTreWRqg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.2.tgz", + "integrity": "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w==", "license": "BSD-3-Clause", "dependencies": { "resolve-protobuf-schema": "^2.1.0" @@ -3776,9 +3748,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -3796,7 +3768,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3888,9 +3860,9 @@ "peer": true }, "node_modules/react-router": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", - "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -3910,12 +3882,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz", - "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", "license": "MIT", "dependencies": { - "react-router": "7.17.0" + "react-router": "7.18.2" }, "engines": { "node": ">=20.0.0" @@ -3943,7 +3915,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4163,9 +4134,9 @@ "license": "ISC" }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -4289,9 +4260,9 @@ } }, "node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", "dev": true, "license": "MIT", "engines": { @@ -4306,9 +4277,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", "dev": true, "funding": [ { @@ -4425,19 +4396,19 @@ } }, "node_modules/vitest": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", - "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.8", - "@vitest/mocker": "4.1.8", - "@vitest/pretty-format": "4.1.8", - "@vitest/runner": "4.1.8", - "@vitest/snapshot": "4.1.8", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -4465,12 +4436,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.8", - "@vitest/browser-preview": "4.1.8", - "@vitest/browser-webdriverio": "4.1.8", - "@vitest/coverage-istanbul": "4.1.8", - "@vitest/coverage-v8": "4.1.8", - "@vitest/ui": "4.1.8", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index 4cb16de..45276a2 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,10 @@ "@tanstack/react-query": "^5.100.11", "@tanstack/react-virtual": "^3.13.25", "echarts": "6.1.0", - "maplibre-gl": "^5.24.0", + "maplibre-gl": "^6.9.0", "react": "^19.2.6", "react-dom": "^19.2.6", - "react-router-dom": "^7.15.1" + "react-router-dom": "^7.18.2" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -44,6 +44,6 @@ "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", "vite": "^8.0.12", - "vitest": "^4.1.7" + "vitest": "^4.1.11" } } diff --git a/src/features/map/MapView.tsx b/src/features/map/MapView.tsx index 7871077..d89ff2a 100644 --- a/src/features/map/MapView.tsx +++ b/src/features/map/MapView.tsx @@ -184,7 +184,7 @@ export function MapView({ wsManager, selectedNodeId, onSelectNode }: MapViewProp }, [iatas, selectedIatas]); const borderData = useMapBordersData(borderIatas, borders); - const { containerRef, mapRef, isReady, error } = useMapLibre(styleId, fitPoints, handleStyleError, initialCamera); + const { containerRef, mapRef, isReady, error, nodeIconResolverRef } = useMapLibre(styleId, fitPoints, handleStyleError, initialCamera); const isDark = resolveMapStyle(styleId).dark; // drives marker theming + maplibre control chrome // Snapshot the current view (live camera + settings) into deep-link params for the copy button. @@ -205,7 +205,7 @@ export function MapView({ wsManager, selectedNodeId, onSelectNode }: MapViewProp return { tab: "Map", ...buildMapParams(snapshot) }; }, [mapRef, clustered, typeFilter, neighborLines, styleId, packetFlow, borders]); - useMapNodes(mapRef, isReady, geojson, isDark, themeKey, clustered, onSelectNode, selectedNodeId, packetFlow, focusIds, `${regionKey}:${typeFilter}`); + useMapNodes(mapRef, nodeIconResolverRef, isReady, geojson, isDark, themeKey, clustered, onSelectNode, selectedNodeId, packetFlow, focusIds, `${regionKey}:${typeFilter}`); useMapNeighbors(mapRef, isReady, neighborEdges, themeKey); useMapBorders(mapRef, isReady, borderData, themeKey); useMapPacketFlow(mapRef, isReady, packetFlow, wsManager, themeKey, regionKey); diff --git a/src/features/map/PacketPathMap.tsx b/src/features/map/PacketPathMap.tsx index 87d6cea..b421868 100644 --- a/src/features/map/PacketPathMap.tsx +++ b/src/features/map/PacketPathMap.tsx @@ -1,8 +1,8 @@ // src/features/map/PacketPathMap.tsx import { useEffect, useRef, useState } from "react"; -import maplibregl from "maplibre-gl"; +import { Map as MapLibreMap, NavigationControl, AttributionControl, Popup, LngLatBounds } from "maplibre-gl"; +import "./maplibre-worker"; import type { - Map as MapLibreMap, GeoJSONSource, LineLayerSpecification, CircleLayerSpecification, @@ -38,7 +38,7 @@ export function PacketPathMap({ paths, selectedKey, styleId }: { // build the map once (styleId is read at creation; the popup doesn't hot-swap basemaps) useEffect(() => { if (mapRef.current || !containerRef.current) return; - const map = new maplibregl.Map({ + const map = new MapLibreMap({ container: containerRef.current, style: resolveMapStyle(styleId).url, center: DEFAULT_CENTER, @@ -46,8 +46,8 @@ export function PacketPathMap({ paths, selectedKey, styleId }: { attributionControl: false, }); mapRef.current = map; - map.addControl(new maplibregl.NavigationControl({ showCompass: false }), "top-right"); - map.addControl(new maplibregl.AttributionControl({ compact: true })); + map.addControl(new NavigationControl({ showCompass: false }), "top-right"); + map.addControl(new AttributionControl({ compact: true })); // start the attribution as a bare (i) instead of the wide expanded bar it pops open on load — // on mobile that bar overlaps the observer list beneath the map (same trick as useMapLibre) const attrib = map.getContainer().querySelector(".maplibregl-ctrl-attrib"); @@ -69,7 +69,7 @@ export function PacketPathMap({ paths, selectedKey, styleId }: { coords.textContent = `${lat.toFixed(5)}, ${lng.toFixed(5)}`; el.append(name, coords); // a fresh popup per click; closeOnClick removes the previous one on the same click - new maplibregl.Popup({ closeButton: false, closeOnClick: true, offset: 10 }) + new Popup({ closeButton: false, closeOnClick: true, offset: 10 }) .setLngLat([lng, lat]).setDOMContent(el).addTo(map); }); const onLoad = () => setReady(true); @@ -124,7 +124,7 @@ export function PacketPathMap({ paths, selectedKey, styleId }: { (map.getSource(NODE_SOURCE) as GeoJSONSource).setData(points); if (bounds.length) { - const b = bounds.reduce((acc, p) => acc.extend(p), new maplibregl.LngLatBounds(bounds[0], bounds[0])); + const b = bounds.reduce((acc, p) => acc.extend(p), new LngLatBounds(bounds[0], bounds[0])); map.fitBounds(b, { padding: 60, maxZoom: IATA_ZOOM }); } }, [ready, paths, selectedKey]); diff --git a/src/features/map/maplibre-worker.ts b/src/features/map/maplibre-worker.ts new file mode 100644 index 0000000..6d090f9 --- /dev/null +++ b/src/features/map/maplibre-worker.ts @@ -0,0 +1,6 @@ +import { setWorkerUrl } from "maplibre-gl"; +import workerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url"; + +// maplibre 6 can't find its worker through Vite's module graph on its own; without this the map +// silently never fetches vector tiles. Import once from anything that constructs a Map. +setWorkerUrl(workerUrl); diff --git a/src/features/map/node-icons.ts b/src/features/map/node-icons.ts index af49be6..628c9ba 100644 --- a/src/features/map/node-icons.ts +++ b/src/features/map/node-icons.ts @@ -8,7 +8,7 @@ import { } from "./types"; // Marker icons: per-type SVG + cluster hexagon, recolored and rasterized to maplibre images (unknown -// type = canvas ring). Async, so provided lazily via styleimagemissing in useMapNodes; re-colors on theme. +// type = canvas ring). Async, so provided lazily via the missing-image resolver in useMapNodes; re-colors on theme. // Glyph SVGs as raw text per type (+ observer variant). Marker style follows the basemap: filled // Glitch on dark, hollow Wireframe on light — which is exactly what isDark encodes. import.meta.glob diff --git a/src/features/map/useMapLibre.ts b/src/features/map/useMapLibre.ts index 08a8fea..cca1a69 100644 --- a/src/features/map/useMapLibre.ts +++ b/src/features/map/useMapLibre.ts @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from "react"; -import maplibregl from "maplibre-gl"; -import type { Map as MapLibreMap, RasterDEMSourceSpecification } from "maplibre-gl"; +import { Map as MapLibreMap, NavigationControl, ScaleControl, AttributionControl, LngLatBounds } from "maplibre-gl"; +import type { RasterDEMSourceSpecification } from "maplibre-gl"; +import "./maplibre-worker"; import { DEM_TILES, DEM_ATTRIBUTION, @@ -78,6 +79,7 @@ export function useMapLibre( const lastGoodStyleIdRef = useRef(styleId); // last style that loaded; the revert target on a failed swap const hasLoadedRef = useRef(false); // a style has loaded at least once (distinguishes initial-load failure) const swapPendingRef = useRef(false); // a setStyle() basemap swap is in flight (awaiting style.load) + const nodeIconResolverRef = useRef<((id: string) => Promise) | null>(null); // filled by useMapNodes const onStyleErrorRef = useRef(onStyleError); const lastFitKeyRef = useRef(null); // last applied fit target; skips redundant re-fits const skipInitialFitRef = useRef(!!initialCamera); // let a deep-link camera win over the first fit @@ -103,7 +105,7 @@ export function useMapLibre( // open at the deep-link camera if one was given, else the default view; the fit effect frames the // selection once the style is ready (unless a deep-link camera suppresses that first fit) - const map = new maplibregl.Map({ + const map = new MapLibreMap({ container, style: resolveMapStyle(styleIdRef.current).url, center: initialCameraRef.current?.center ?? DEFAULT_CENTER, @@ -116,9 +118,9 @@ export function useMapLibre( mapRef.current = map; lastStyleIdRef.current = styleIdRef.current; - map.addControl(new maplibregl.NavigationControl({ visualizePitch: true }), "top-right"); - map.addControl(new maplibregl.ScaleControl({ unit: "metric" }), "bottom-left"); - map.addControl(new maplibregl.AttributionControl({ compact: true })); // bottom-right + map.addControl(new NavigationControl({ visualizePitch: true }), "top-right"); + map.addControl(new ScaleControl({ unit: "metric" }), "bottom-left"); + map.addControl(new AttributionControl({ compact: true })); // bottom-right // maplibre pops the compact attribution open the first time the basemap credit loads (it tacks // on .maplibregl-compact-show). Mark it .maplibregl-compact up front so it skips that and stays // a bare (i) on load — clicking it still opens the credit. @@ -141,14 +143,16 @@ export function useMapLibre( // "circle-11"), so maplibre warns on every load. Hand it a transparent 1x1 for anything that // isn't ours and the noise goes away — a missing icon already draws nothing, so the map looks // identical. Our own markers all start with "node-" and are rasterized by useMapNodes, so we - // leave those alone. This lives here (not in useMapNodes) so it's listening before the base + // leave those alone. This lives here (not in useMapNodes) so it's installed before the base // style's first paint, when those icons are first requested. - map.on("styleimagemissing", (e) => { - if (!e.id.startsWith("node-") && !map.hasImage(e.id)) map.addImage(e.id, new ImageData(1, 1)); + // maplibre allows one resolver per map, so node icons route through a slot useMapNodes fills. + map.setMissingStyleImageResolver((id) => { + if (id.startsWith("node-")) return nodeIconResolverRef.current?.(id); + if (!map.hasImage(id)) map.addImage(id, new ImageData(1, 1)); }); map.on("error", (e) => { - const err = e as { error?: Error; sourceId?: string; tile?: unknown }; + const err = e as unknown as { error?: Error; sourceId?: string; tile?: unknown }; // A single tile/source failure (one basemap or DEM tile timing out / 403 / a momentary network // blip) is transient and non-fatal — the rest of the map stays usable — so never blank the map // for it. maplibre tags tile/source errors with a tile/sourceId; style-level errors have neither. @@ -212,7 +216,7 @@ export function useMapLibre( const bounds = fitPoints.reduce( (b, p) => b.extend(p), - new maplibregl.LngLatBounds(fitPoints[0], fitPoints[0]), + new LngLatBounds(fitPoints[0], fitPoints[0]), ); map.fitBounds(bounds, { padding: 48, @@ -222,5 +226,5 @@ export function useMapLibre( }); }, [fitPoints, isReady]); - return { containerRef, mapRef, isReady, error }; + return { containerRef, mapRef, isReady, error, nodeIconResolverRef }; } diff --git a/src/features/map/useMapNodes.ts b/src/features/map/useMapNodes.ts index c4a7807..f674755 100644 --- a/src/features/map/useMapNodes.ts +++ b/src/features/map/useMapNodes.ts @@ -98,6 +98,7 @@ const SPIDER_LEAVES_LAYOUT: SymbolLayerSpecification["layout"] = { // labels at high zoom). Like useMapLibre, the imperative work re-adds itself after every style switch. export function useMapNodes( mapRef: React.RefObject, + nodeIconResolverRef: React.RefObject<((id: string) => Promise) | null>, isReady: boolean, geojson: NodeFC, isDark: boolean, @@ -281,8 +282,9 @@ export function useMapNodes( }, [mapRef, isReady, isDark, clustered, themeKey]); // Supply and re-color the marker images. SVG glyphs rasterize async, so they're provided both - // proactively here and lazily on styleimagemissing. Re-runs on a theme/basemap/DPR change to - // re-rasterize; a basemap switch also drops the images via setStyle, which this then restores. + // proactively here and lazily through the map's missing-image resolver. Re-runs on a theme/ + // basemap/DPR change to re-rasterize; a basemap switch also drops the images via setStyle, which + // this then restores. useEffect(() => { const map = mapRef.current; if (!map || !isReady) return; @@ -297,12 +299,11 @@ export function useMapNodes( .catch(() => { /* an icon failed to rasterize; the layer simply draws nothing for that id */ }); - const onMissing = (e: { id: string }) => provide(e.id); - map.on("styleimagemissing", onMissing); + nodeIconResolverRef.current = provide; // A symbol won't draw until its icon is in, and adding one late doesn't redraw tiles that // already laid out — that's the "markers only show after I pan/zoom" bug. So once every icon // is ready, nudge the source to lay the markers out again (setData reloads the whole source). - // styleimagemissing still covers anything asked for before we get here. + // The resolver still covers anything asked for before we get here. Promise.all(MAP_ICON_IDS.map(provide)).then(() => { if (cancelled || mapRef.current !== map) return; const src = map.getSource(NODES_SOURCE_ID) as GeoJSONSource | undefined; @@ -310,9 +311,9 @@ export function useMapNodes( }); return () => { cancelled = true; - map.off("styleimagemissing", onMissing); + nodeIconResolverRef.current = null; }; - }, [mapRef, isReady, isDark, themeKey, dpr]); + }, [mapRef, nodeIconResolverRef, isReady, isDark, themeKey, dpr]); // Reflect the shared selection as a ring (mirrors the table's row highlight). Its own effect so // changing the selection doesn't rebuild the source/layers.