diff --git a/web/package-lock.json b/web/package-lock.json index 2c8cf94..1fb012f 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14,6 +14,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", + "@mapbox/vector-tile": "^2.0.4", "@russss/maplibregl-layer-switcher": "^2.2.3", "@turf/length": "^7.3.5", "@types/express": "^5.0.6", @@ -25,6 +26,7 @@ "eslint-config-prettier": "^10.1.8", "express": "^5.2.1", "maplibre-gl": "^5", + "pbf": "^4.0.2", "prettier": "^3.0.0", "redom": "^3.29.1", "ts-node": "^10.9.1", @@ -68,6 +70,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1667,6 +1670,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" @@ -1679,6 +1683,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -1804,9 +1809,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", "dev": true, "license": "MIT", "dependencies": { @@ -1816,7 +1821,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, @@ -2026,9 +2031,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2046,9 +2048,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2066,9 +2065,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2086,9 +2082,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2106,9 +2099,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2126,9 +2116,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2146,9 +2133,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2172,9 +2156,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2198,9 +2179,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2224,9 +2202,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2250,9 +2225,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2276,9 +2248,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2586,9 +2555,9 @@ } }, "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==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.2.tgz", + "integrity": "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2656,9 +2625,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.138.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", - "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "dev": true, "license": "MIT", "funding": { @@ -2795,9 +2764,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2815,9 +2781,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2835,9 +2798,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2855,9 +2815,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2919,9 +2876,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", - "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -2936,9 +2893,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", - "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -2953,9 +2910,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", - "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -2970,9 +2927,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", - "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -2987,9 +2944,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", - "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ "arm" ], @@ -3004,16 +2961,13 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", - "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3024,16 +2978,13 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", - "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3044,16 +2995,13 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", - "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3064,16 +3012,13 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", - "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3084,16 +3029,13 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", - "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3104,16 +3046,13 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", - "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3124,9 +3063,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", - "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ "arm64" ], @@ -3141,9 +3080,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", - "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ "wasm32" ], @@ -3171,9 +3110,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", - "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ "arm64" ], @@ -3188,9 +3127,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", - "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -3423,9 +3362,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3440,9 +3376,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3457,9 +3390,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3474,9 +3404,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3491,9 +3418,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3508,9 +3432,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3525,9 +3446,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3542,9 +3460,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3559,9 +3474,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3576,9 +3488,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3593,9 +3502,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3610,9 +3516,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3627,9 +3530,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3911,9 +3811,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", - "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.2.tgz", + "integrity": "sha512-d3KvEXBSo/lOAMc2u6fkyDHBvetBHeqD7wm/AcXfLpSOQwlmG9D/aQ0SFswVjv05p7ullQS7Mjohj6/VdbZuTg==", "dev": true, "license": "MIT", "dependencies": { @@ -3945,11 +3845,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.20.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", - "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4003,17 +3904,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz", - "integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", + "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.62.1", - "@typescript-eslint/type-utils": "8.62.1", - "@typescript-eslint/utils": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/type-utils": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -4026,15 +3927,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.62.1", + "@typescript-eslint/parser": "^8.63.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", "dev": true, "license": "MIT", "engines": { @@ -4042,16 +3943,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz", - "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", + "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.62.1", - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", "debug": "^4.4.3" }, "engines": { @@ -4067,14 +3969,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz", - "integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", + "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.62.1", - "@typescript-eslint/types": "^8.62.1", + "@typescript-eslint/tsconfig-utils": "^8.63.0", + "@typescript-eslint/types": "^8.63.0", "debug": "^4.4.3" }, "engines": { @@ -4089,14 +3991,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz", - "integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", + "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1" + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4107,9 +4009,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz", - "integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", + "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", "dev": true, "license": "MIT", "engines": { @@ -4124,15 +4026,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz", - "integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", + "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/utils": "8.62.1", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -4149,9 +4051,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz", - "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", + "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", "dev": true, "license": "MIT", "engines": { @@ -4163,16 +4065,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz", - "integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", + "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.62.1", - "@typescript-eslint/tsconfig-utils": "8.62.1", - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/visitor-keys": "8.62.1", + "@typescript-eslint/project-service": "8.63.0", + "@typescript-eslint/tsconfig-utils": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -4230,16 +4132,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz", - "integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", + "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.62.1", - "@typescript-eslint/types": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1" + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4254,13 +4156,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz", - "integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", + "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/types": "8.63.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -4290,6 +4192,7 @@ "integrity": "sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cac": "^6.7.14", "colorette": "^2.0.20", @@ -4335,6 +4238,7 @@ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4538,9 +4442,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.42", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", - "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4590,9 +4494,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -4614,9 +4518,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", - "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", "dev": true, "funding": [ { @@ -4633,11 +4537,12 @@ } ], "license": "MIT", + "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.10.38", - "caniuse-lite": "^1.0.30001799", - "electron-to-chromium": "^1.5.376", - "node-releases": "^2.0.48", + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -4735,9 +4640,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001800", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", - "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", + "version": "1.0.30001805", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", "dev": true, "funding": [ { @@ -5191,9 +5096,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.387", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.387.tgz", - "integrity": "sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==", + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", "dev": true, "license": "ISC" }, @@ -5396,11 +5301,12 @@ } }, "node_modules/eslint": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", - "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", + "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", "dev": true, "license": "MIT", + "peer": true, "workspaces": [ "packages/*" ], @@ -5843,9 +5749,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -7005,6 +6911,7 @@ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -7300,9 +7207,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7324,9 +7228,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7348,9 +7249,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7372,9 +7270,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7706,9 +7601,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -7742,9 +7637,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.50", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", - "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "dev": true, "license": "MIT", "engines": { @@ -7961,9 +7856,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -8025,9 +7920,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", "dev": true, "funding": [ { @@ -8071,9 +7966,9 @@ } }, "node_modules/prettier": { - "version": "3.9.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", - "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==", + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", "dev": true, "license": "MIT", "bin": { @@ -8413,13 +8308,13 @@ } }, "node_modules/rolldown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", - "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.138.0", + "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -8429,21 +8324,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.4", - "@rolldown/binding-darwin-arm64": "1.1.4", - "@rolldown/binding-darwin-x64": "1.1.4", - "@rolldown/binding-freebsd-x64": "1.1.4", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", - "@rolldown/binding-linux-arm64-gnu": "1.1.4", - "@rolldown/binding-linux-arm64-musl": "1.1.4", - "@rolldown/binding-linux-ppc64-gnu": "1.1.4", - "@rolldown/binding-linux-s390x-gnu": "1.1.4", - "@rolldown/binding-linux-x64-gnu": "1.1.4", - "@rolldown/binding-linux-x64-musl": "1.1.4", - "@rolldown/binding-openharmony-arm64": "1.1.4", - "@rolldown/binding-wasm32-wasi": "1.1.4", - "@rolldown/binding-win32-arm64-msvc": "1.1.4", - "@rolldown/binding-win32-x64-msvc": "1.1.4" + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, "node_modules/rollup": { @@ -8452,6 +8347,7 @@ "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.9" }, @@ -9142,9 +9038,9 @@ } }, "node_modules/terser": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", - "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", + "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9431,6 +9327,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9440,16 +9337,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.62.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz", - "integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", + "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.62.1", - "@typescript-eslint/parser": "8.62.1", - "@typescript-eslint/typescript-estree": "8.62.1", - "@typescript-eslint/utils": "8.62.1" + "@typescript-eslint/eslint-plugin": "8.63.0", + "@typescript-eslint/parser": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9667,16 +9564,17 @@ } }, "node_modules/vite": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", - "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", + "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", + "picomatch": "^4.0.5", "postcss": "^8.5.16", - "rolldown": "~1.1.3", + "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "bin": { @@ -10018,6 +9916,7 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/web/package.json b/web/package.json index 559552b..3b60153 100644 --- a/web/package.json +++ b/web/package.json @@ -25,6 +25,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", + "@mapbox/vector-tile": "^2.0.4", "@russss/maplibregl-layer-switcher": "^2.2.3", "@turf/length": "^7.3.5", "@types/express": "^5.0.6", @@ -36,6 +37,7 @@ "eslint-config-prettier": "^10.1.8", "express": "^5.2.1", "maplibre-gl": "^5", + "pbf": "^4.0.2", "prettier": "^3.0.0", "redom": "^3.29.1", "ts-node": "^10.9.1", diff --git a/web/src/components/map.css b/web/src/components/map.css index ac8326c..341ad2c 100644 --- a/web/src/components/map.css +++ b/web/src/components/map.css @@ -7,6 +7,7 @@ Imported into the root map web component @import './contextmenu.css'; @import '../villages/villages.css'; @import '../export/export.css'; +@import '../search/search.css'; @import '@russss/maplibregl-layer-switcher/layerswitcher.css'; .layer-switcher { diff --git a/web/src/index.ts b/web/src/index.ts index 4497b89..c0d9baf 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -7,6 +7,7 @@ import DistanceMeasure from './components/distancemeasure.ts' import VillagesEditor from './villages/index.ts' import InstallControl from './installcontrol.ts' import ExportControl from './export/export.ts' +import SearchControl from './search/searchcontrol.ts' import { GridPosition } from './grid.ts' import './components/map' import { loadIcons } from './icons.ts' @@ -33,6 +34,7 @@ export class EMFMapApp extends LitElement { const map = mapComponent.map! map.addControl(new DistanceMeasure(), 'top-right') + map.addControl(new SearchControl(), 'top-right') map.addControl(new InstallControl(), 'top-left') map.addControl(new VillagesEditor('villages', 'villages_symbol'), 'top-right') diff --git a/web/src/search/search.css b/web/src/search/search.css new file mode 100644 index 0000000..818c8b7 --- /dev/null +++ b/web/src/search/search.css @@ -0,0 +1,175 @@ +.search-ctrl { + display: flex; + align-items: center; +} + +.search-toggle { + min-width: 30px; + min-height: 30px; + flex: 0 0 auto; + background: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20viewBox%3D%220%200%2024%2024%22%3E%3Ccircle%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%226%22%20fill%3D%22none%22%20stroke%3D%22%23333%22%20stroke-width%3D%222.5%22/%3E%3Cline%20x1%3D%2214.5%22%20y1%3D%2214.5%22%20x2%3D%2220.5%22%20y2%3D%2220.5%22%20stroke%3D%22%23333%22%20stroke-width%3D%222.5%22%20stroke-linecap%3D%22round%22/%3E%3C/svg%3E') + white; + background-repeat: no-repeat; + background-size: 16px 16px; + background-position: center; +} + +/* #fb48c4 is the EMF theme pink (PWA theme_color, search highlight layers) */ +.search-toggle.active { + box-shadow: inset 0 0 0 2px #fb48c4; +} + +.search-toggle:focus-visible, +.search-clear:focus-visible { + outline: 2px solid #fb48c4; + outline-offset: -2px; +} + +.search-input:focus-visible { + box-shadow: inset 0 -2px 0 #fb48c4; +} + +.search-panel { + display: none; +} + +/* Expanded: a bar fixed across the top of the screen, above the site header, + with the results dropdown overlaying the map below it */ +.maplibregl-ctrl-top-right .maplibregl-ctrl.search-ctrl.expanded { + position: fixed; + top: 0; + left: 0; + right: 0; + margin: 0; + border-radius: 0; + z-index: 3; +} + +.search-ctrl.expanded .search-toggle { + border-radius: 0; + min-height: 36px; +} + +.search-ctrl.expanded .search-panel { + display: block; + flex: 1; + min-width: 0; +} + +.search-input-row { + display: flex; + align-items: center; + height: 36px; +} + +.search-input { + flex: 1; + min-width: 0; + height: 100%; + border: none; + outline: none; + background: none; + padding: 0 6px; + /* 16px stops iOS zooming the page when the input is focused */ + font-size: 16px; + font-family: inherit; +} + +/* Scoped under .maplibregl-ctrl-group to out-rank maplibre's 29px button rule, + keeping the 36px touch target */ +.maplibregl-ctrl-group button.search-clear { + flex: 0 0 auto; + width: 36px; + height: 36px; + border: none; + background: none; + border-radius: 4px; + cursor: pointer; + color: #666; + font-size: 14px; +} + +.search-clear:hover { + background-color: #eee; +} + +.search-results { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin: 0; + padding: 0; + list-style: none; + background: white; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + max-height: min(60vh, 480px); + overflow-y: auto; + overscroll-behavior: contain; +} + +.search-results:empty { + display: none; +} + +.search-result { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + min-height: 40px; + padding: 4px 12px; + cursor: pointer; + font-size: 13px; +} + +.search-result:hover, +.search-result.active { + background-color: #fde5f5; +} + +.search-result-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.search-note { + display: block; + padding: 10px 12px; + font-size: 12px; + color: #666; +} + +/* Badge backgrounds are chosen for >= 4.5:1 WCAG AA contrast with white text */ +.search-badge { + flex: 0 0 auto; + padding: 2px 7px; + border-radius: 9px; + font-size: 11px; + color: white; +} + +.search-badge-structure { + background-color: #b0177f; +} + +.search-badge-area { + background-color: #6b6b6b; +} + +.search-badge-camping { + background-color: #5e6f19; +} + +.search-badge-parking { + background-color: #2a62a0; +} + +.search-badge-gate { + background-color: #a85a10; +} + +.search-badge-village { + background-color: #7a4bbd; +} diff --git a/web/src/search/searchcontrol.ts b/web/src/search/searchcontrol.ts new file mode 100644 index 0000000..fbf4ee9 --- /dev/null +++ b/web/src/search/searchcontrol.ts @@ -0,0 +1,350 @@ +import { el, mount } from 'redom' +import maplibregl, { GeoJSONSource, LngLatBounds } from 'maplibre-gl' +import type { SearchCategory, SearchEntry, SearchIndex } from './searchindex.ts' + +const categoryZoom: Record = { + structure: 18, + gate: 17.5, + village: 17.5, + area: 17, + camping: 16.5, + parking: 16.5, +} + +/* Zoom cap when fitting all results at once; matches the widest per-category + zoom in categoryZoom so co-located matches don't over-zoom */ +const MULTI_RESULT_MAX_ZOOM = 17.5 + +const categoryLabel: Record = { + structure: 'venue', + area: 'area', + camping: 'camping', + parking: 'parking', + gate: 'gate', + village: 'village', +} + +const emptyCollection: GeoJSON.FeatureCollection = { type: 'FeatureCollection', features: [] } + +class SearchControl implements maplibregl.IControl { + _map?: maplibregl.Map + _container: HTMLElement + _toggleButton: HTMLButtonElement + _panel: HTMLElement + _input: HTMLInputElement + _clearButton: HTMLButtonElement + _resultsList: HTMLElement + _indexPromise?: Promise + _index?: SearchIndex + _indexError: boolean = false + _searchFn?: typeof import('./searchindex.ts').search + _results: SearchEntry[] = [] + _activeIndex: number = -1 + + constructor() { + this._toggleButton = el('button.search-toggle', { + type: 'button', + 'aria-label': 'Search venues', + 'aria-expanded': 'false', + title: 'Search venues', + }) as HTMLButtonElement + this._input = el('input.search-input', { + type: 'text', + placeholder: 'Search venues…', + enterkeyhint: 'search', + autocapitalize: 'off', + autocorrect: 'off', + spellcheck: 'false', + role: 'combobox', + 'aria-expanded': 'false', + 'aria-autocomplete': 'list', + 'aria-controls': 'search-results-list', + }) as HTMLInputElement + this._clearButton = el('button.search-clear', { + type: 'button', + 'aria-label': 'Clear search', + }) as HTMLButtonElement + this._clearButton.textContent = '✕' + this._resultsList = el('ul.search-results', { role: 'listbox', id: 'search-results-list' }) + this._panel = el( + 'div.search-panel', + el('div.search-input-row', this._input, this._clearButton), + this._resultsList + ) + this._container = el( + 'div.maplibregl-ctrl.maplibregl-ctrl-group.search-ctrl', + this._toggleButton, + this._panel + ) + + // Listeners live on our own elements and are attached once here, so the + // control survives addControl/removeControl/addControl without doubling + this._toggleButton.addEventListener('click', () => this._toggle()) + this._clearButton.addEventListener('click', () => { + this._clear() + this._input.focus() + }) + this._input.addEventListener('input', () => this._renderResults()) + this._input.addEventListener('focus', () => this._renderResults()) + this._input.addEventListener('keydown', (e) => this._onInputKeyDown(e)) + // Escape works from anywhere in the expanded bar, not just the input + this._container.addEventListener('keydown', (e) => { + if (e.key === 'Escape') this._onEscape(e) + }) + // DistanceMeasure cancels measuring on a document-level Escape keyup; + // don't leak Escape presses that belong to the search bar + this._container.addEventListener('keyup', (e) => { + if (e.key === 'Escape') e.stopPropagation() + }) + // Close the dropdown only when focus leaves the whole widget, so tabbing + // between the input and the clear button doesn't dismiss results + this._container.addEventListener('focusout', (e) => { + if (!this._container.contains(e.relatedTarget as Node)) this._hideResults() + }) + // Prevent result taps from blurring the input before their click handler runs + this._resultsList.addEventListener('mousedown', (e) => e.preventDefault()) + } + + onAdd(map: maplibregl.Map) { + this._map = map + + // Hoist above the navigation (+/-) controls, which are added before this one. + // The container isn't attached to the corner element until onAdd returns. + queueMicrotask(() => this._container.parentElement?.prepend(this._container)) + + return this._container + } + + onRemove() { + this._setHighlights([]) + this._collapse() + this._container.parentNode?.removeChild(this._container) + this._map = undefined + } + + _toggle() { + if (this._container.classList.contains('expanded')) { + this._collapse() + } else { + this._expand() + } + } + + _expand() { + this._container.classList.add('expanded') + this._toggleButton.setAttribute('aria-expanded', 'true') + this._toggleButton.setAttribute('aria-label', 'Close search') + this._toggleButton.title = 'Close search' + this._input.focus() + this._ensureIndex() + this._renderResults() + } + + _collapse() { + this._container.classList.remove('expanded') + this._toggleButton.setAttribute('aria-expanded', 'false') + this._toggleButton.setAttribute('aria-label', 'Search venues') + this._toggleButton.title = 'Search venues' + this._input.blur() + } + + _ensureIndex() { + const map = this._map + if (!map) return + this._indexPromise ??= import('./searchindex.ts') + .then(async (module) => { + this._searchFn = module.search + this._indexError = false + this._index = await module.buildIndex(map, () => { + // Villages can merge in after the first render + if (this._container.classList.contains('expanded')) this._renderResults() + }) + this._renderResults() + }) + .catch((e) => { + // Chunk load can fail (e.g. offline before the service worker has + // cached it); reset so the next expansion retries + console.warn('Search: failed to load index', e) + this._indexPromise = undefined + this._indexError = true + this._renderResults() + }) + } + + /* Clear query, results and highlights */ + _clear() { + this._input.value = '' + this._setHighlights([]) + this._renderResults() + } + + _onEscape(e: KeyboardEvent) { + e.preventDefault() + e.stopPropagation() + if (this._input.value) { + this._clear() + this._input.focus() + } else { + this._setHighlights([]) + this._collapse() + this._toggleButton.focus() + } + } + + _onInputKeyDown(e: KeyboardEvent) { + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { + e.preventDefault() + if (this._results.length === 0) return + if (this._activeIndex === -1) { + this._activeIndex = e.key === 'ArrowDown' ? 0 : this._results.length - 1 + } else { + const delta = e.key === 'ArrowDown' ? 1 : -1 + this._activeIndex = (this._activeIndex + delta + this._results.length) % this._results.length + } + this._updateActiveRow() + } else if (e.key === 'Enter') { + e.preventDefault() + if (this._activeIndex >= 0 && this._results[this._activeIndex]) { + this._select(this._results[this._activeIndex]) + } else if (this._results.length === 1) { + this._select(this._results[0]) + } else if (this._results.length > 1) { + this._selectAll() + } + } + } + + _renderResults() { + const query = this._input.value.trim() + this._resultsList.innerHTML = '' + this._activeIndex = -1 + this._input.removeAttribute('aria-activedescendant') + + if (!query) { + this._input.setAttribute('aria-expanded', 'false') + this._results = [] + return + } + + if (this._indexError) { + mount( + this._resultsList, + el('li.search-note', { role: 'presentation' }, 'Search failed to load — close and reopen to retry') + ) + this._results = [] + return + } + + if (!this._index || !this._searchFn) { + mount(this._resultsList, el('li.search-note', { role: 'presentation' }, 'Loading…')) + this._results = [] + return + } + + this._results = this._searchFn(this._index.entries, query) + this._input.setAttribute('aria-expanded', 'true') + + if (this._index.offline) { + mount( + this._resultsList, + el('li.search-note', { role: 'presentation' }, 'Offline — results limited to visible area') + ) + } + + if (this._results.length === 0) { + mount( + this._resultsList, + el( + 'li.search-note', + { role: 'presentation' }, + this._index.entries.length ? 'No results' : 'Search unavailable offline' + ) + ) + return + } + + this._results.forEach((entry, i) => { + const row = el( + 'li.search-result', + { + role: 'option', + id: `search-result-${i}`, + 'aria-selected': 'false', + }, + el('span.search-result-name', entry.displayName), + el('span.search-badge.search-badge-' + entry.category, categoryLabel[entry.category]) + ) + row.addEventListener('click', () => this._select(entry)) + mount(this._resultsList, row) + }) + } + + _updateActiveRow() { + const rows = this._resultsList.querySelectorAll('.search-result') + rows.forEach((row, i) => { + row.classList.toggle('active', i === this._activeIndex) + row.setAttribute('aria-selected', String(i === this._activeIndex)) + }) + if (this._activeIndex >= 0) { + this._input.setAttribute('aria-activedescendant', `search-result-${this._activeIndex}`) + rows[this._activeIndex]?.scrollIntoView({ block: 'nearest' }) + } else { + this._input.removeAttribute('aria-activedescendant') + } + } + + _hideResults() { + // Keep query and highlights; just close the dropdown + this._resultsList.innerHTML = '' + this._activeIndex = -1 + this._input.setAttribute('aria-expanded', 'false') + this._input.removeAttribute('aria-activedescendant') + } + + _select(entry: SearchEntry) { + this._setHighlights([entry]) + this._map?.flyTo({ center: entry.coords, zoom: categoryZoom[entry.category] }) + this._afterSelection() + } + + _selectAll() { + const results = this._results + this._setHighlights(results) + const bounds = results.reduce( + (b, entry) => b.extend(entry.coords), + new LngLatBounds(results[0].coords, results[0].coords) + ) + this._map?.fitBounds(bounds, { + padding: { top: 90, bottom: 50, left: 50, right: 50 }, + maxZoom: MULTI_RESULT_MAX_ZOOM, + }) + this._afterSelection() + } + + _afterSelection() { + // The expanded bar overlays the top of the map, so get out of the way + // once a result is chosen; the query is kept for the next expansion + this._collapse() + } + + _setHighlights(entries: SearchEntry[]) { + const source = this._map?.getSource('search_results') as GeoJSONSource | undefined + if (!source) return + if (entries.length === 0) { + source.setData(emptyCollection) + this._toggleButton.classList.remove('active') + return + } + source.setData({ + type: 'FeatureCollection', + features: entries.map((entry) => ({ + type: 'Feature', + geometry: { type: 'Point', coordinates: entry.coords }, + properties: { name: entry.displayName, category: entry.category }, + })), + }) + this._toggleButton.classList.add('active') + } +} + +export default SearchControl diff --git a/web/src/search/searchindex.ts b/web/src/search/searchindex.ts new file mode 100644 index 0000000..9844da8 --- /dev/null +++ b/web/src/search/searchindex.ts @@ -0,0 +1,255 @@ +import { VectorTile } from '@mapbox/vector-tile' +import Pbf from 'pbf' +import maplibregl, { GeoJSONSource } from 'maplibre-gl' +import { center } from '../style/map_style.ts' + +export type SearchCategory = 'structure' | 'area' | 'camping' | 'parking' | 'gate' | 'village' + +export interface SearchEntry { + displayName: string + normalized: string + category: SearchCategory + coords: [number, number] + importance: number +} + +export interface SearchIndex { + entries: SearchEntry[] + /* True when the site plan tile couldn't be fetched and the index only + covers features loaded in the current viewport */ + offline: boolean +} + +/* Zoom level at which the whole site fits comfortably inside a single + site_plan tile. z11 keeps the site centre well away from every tile edge, + so the venue moving between events can't silently drop features that a + tighter tile would clip. */ +const INDEX_TILE_ZOOM = 11 +const TILE_FETCH_TIMEOUT_MS = 5000 +const DEFAULT_IMPORTANCE = 3 + +function normalize(s: string): string { + return s + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .trim() +} + +function tileForPoint(lng: number, lat: number, z: number): { x: number; y: number } { + const latR = (lat * Math.PI) / 180 + const x = Math.floor(((lng + 180) / 360) * 2 ** z) + const y = Math.floor(((1 - Math.log(Math.tan(latR) + 1 / Math.cos(latR)) / Math.PI) / 2) * 2 ** z) + return { x, y } +} + +/* Village data is attendee-editable, so coordinates can't be trusted to be + well-formed; a bad entry must not break selection or bounds fitting */ +function validCoords(coords: unknown): coords is [number, number] { + return ( + Array.isArray(coords) && + coords.length >= 2 && + Number.isFinite(coords[0]) && + Number.isFinite(coords[1]) && + Math.abs(coords[0]) <= 180 && + Math.abs(coords[1]) <= 90 + ) +} + +function makeEntry( + category: SearchCategory, + displayName: string, + geometry: GeoJSON.Geometry | null | undefined, + importance: number +): SearchEntry | undefined { + if (geometry?.type !== 'Point' || !validCoords(geometry.coordinates)) return undefined + return { + displayName, + normalized: normalize(displayName), + category, + coords: geometry.coordinates as [number, number], + importance, + } +} + +interface LayerExtractor { + sourceLayer: string + category: SearchCategory + displayName: (props: Record) => string | undefined + importance: (props: Record) => number +} + +const constImportance = () => DEFAULT_IMPORTANCE + +// Display names and exclusions mirror the label layers in style/emf.ts +const extractors: LayerExtractor[] = [ + { + sourceLayer: 'structure_centroid', + category: 'structure', + displayName: (props) => (props.name ? String(props.name) : undefined), + importance: (props) => (typeof props.importance === 'number' ? props.importance : 0), + }, + { + sourceLayer: 'areas_event_centroid', + category: 'area', + displayName: (props) => (props.name && props.type !== 'toilets' ? String(props.name) : undefined), + importance: constImportance, + }, + { + sourceLayer: 'areas_camping_centroid', + category: 'camping', + displayName: (props) => { + switch (props.type) { + case 'camping': + return props.name ? 'Camping ' + String(props.name) : undefined + case 'accessible': + return 'Accessible Camping' + case 'vehicles': + return props.name ? 'Live-in Vehicles ' + String(props.name) : undefined + default: + return undefined + } + }, + importance: constImportance, + }, + { + sourceLayer: 'parking_centroid', + category: 'parking', + displayName: (props) => (props.name ? 'Parking: ' + String(props.name) : undefined), + importance: constImportance, + }, + { + sourceLayer: 'gates', + category: 'gate', + displayName: (props) => (props.name ? 'Gate ' + String(props.name) : undefined), + importance: constImportance, + }, +] + +async function sitePlanEntries(map: maplibregl.Map): Promise<{ entries: SearchEntry[]; offline: boolean }> { + try { + const source = map.getStyle().sources['site_plan'] + if (!source || source.type !== 'vector' || !source.tiles?.length) { + throw new Error('site_plan source has no tile URL') + } + const [lng, lat] = center + const { x, y } = tileForPoint(lng, lat, INDEX_TILE_ZOOM) + const url = source.tiles[0] + .replace('{z}', String(INDEX_TILE_ZOOM)) + .replace('{x}', String(x)) + .replace('{y}', String(y)) + + const resp = await fetch(url, { signal: AbortSignal.timeout(TILE_FETCH_TIMEOUT_MS) }) + if (!resp.ok) throw new Error(`tile fetch failed: ${resp.status}`) + const tile = new VectorTile(new Pbf(new Uint8Array(await resp.arrayBuffer()))) + + const entries: SearchEntry[] = [] + for (const extractor of extractors) { + const layer = tile.layers[extractor.sourceLayer] + if (!layer) { + console.warn(`Search: source-layer ${extractor.sourceLayer} missing from site plan tile`) + continue + } + for (let i = 0; i < layer.length; i++) { + const feature = layer.feature(i) + const displayName = extractor.displayName(feature.properties) + if (!displayName) continue + const geometry = feature.toGeoJSON(x, y, INDEX_TILE_ZOOM).geometry + const entry = makeEntry( + extractor.category, + displayName, + geometry, + extractor.importance(feature.properties) + ) + if (entry) entries.push(entry) + } + } + return { entries, offline: false } + } catch (e) { + console.warn('Search: falling back to loaded tiles only', e) + return { entries: viewportEntries(map), offline: true } + } +} + +/* Offline fallback: only sees features in tiles the map has already loaded */ +function viewportEntries(map: maplibregl.Map): SearchEntry[] { + const entries: SearchEntry[] = [] + const seen = new Set() + for (const extractor of extractors) { + for (const feature of map.querySourceFeatures('site_plan', { sourceLayer: extractor.sourceLayer })) { + const displayName = extractor.displayName(feature.properties) + if (!displayName) continue + const entry = makeEntry( + extractor.category, + displayName, + feature.geometry, + extractor.importance(feature.properties) + ) + if (!entry) continue + const key = extractor.category + ':' + entry.normalized + if (seen.has(key)) continue + seen.add(key) + entries.push(entry) + } + } + return entries +} + +async function villageEntries(map: maplibregl.Map): Promise { + try { + const source = map.getSource('villages') as GeoJSONSource | undefined + if (!source) return [] + const data = await source.getData() + if (data.type !== 'FeatureCollection' || !Array.isArray(data.features)) return [] + const entries: SearchEntry[] = [] + for (const feature of data.features) { + const name = feature.properties?.name + if (!name) continue + const entry = makeEntry('village', String(name), feature.geometry, DEFAULT_IMPORTANCE) + if (entry) entries.push(entry) + } + return entries + } catch (e) { + console.warn('Search: villages unavailable', e) + return [] + } +} + +export async function buildIndex(map: maplibregl.Map, onUpdate?: () => void): Promise { + // Start both loads concurrently, but never block on villages: getData() + // does not settle while the villages source is unreachable, so they merge + // in whenever they arrive and onUpdate lets the UI refresh + const villagesPromise = villageEntries(map) + const sitePlan = await sitePlanEntries(map) + const index: SearchIndex = { entries: sitePlan.entries, offline: sitePlan.offline } + villagesPromise.then((villages) => { + if (villages.length === 0) return + for (const village of villages) index.entries.push(village) + onUpdate?.() + }) + return index +} + +function score(entry: SearchEntry, query: string): number { + if (entry.normalized === query) return 4 + if (entry.normalized.startsWith(query)) return 3 + if (entry.normalized.split(/[^\p{L}\p{N}]+/u).some((word) => word.startsWith(query))) return 2 + if (entry.normalized.includes(query)) return 1 + return 0 +} + +export function search(index: SearchEntry[], query: string, limit: number = 20): SearchEntry[] { + const q = normalize(query) + if (!q) return [] + return index + .map((entry) => ({ entry, score: score(entry, q) })) + .filter((item) => item.score > 0) + .sort( + (a, b) => + b.score - a.score || + b.entry.importance - a.entry.importance || + a.entry.displayName.localeCompare(b.entry.displayName) + ) + .slice(0, limit) + .map((item) => item.entry) +} diff --git a/web/src/style/emf.ts b/web/src/style/emf.ts index aab37be..ac25888 100644 --- a/web/src/style/emf.ts +++ b/web/src/style/emf.ts @@ -3,6 +3,8 @@ import { LayerSpecificationWithZIndex } from './types' const backgroundColour = '#DBE8A5' const waterColour = '#2EADD9' +// EMF theme pink — matches the PWA theme_color and the search control accent +const searchHighlightColour = '#FB48C4' const campingColours = { camping: '#AFC944', @@ -1158,4 +1160,38 @@ export const layers: LayerSpecificationWithZIndex[] = [ 'icon-opacity': ['case', ['boolean', ['feature-state', 'grist-selected'], false], 1, 0], }, },*/ + { + id: 'search-results-halo', + type: 'circle', + source: 'search_results', + paint: { + 'circle-radius': ['interpolate', ['linear'], ['zoom'], 14, 12, 19, 26], + 'circle-color': searchHighlightColour, + 'circle-opacity': 0.25, + 'circle-stroke-color': searchHighlightColour, + 'circle-stroke-width': 3, + 'circle-stroke-opacity': 0.9, + }, + }, + { + id: 'search-results-label', + type: 'symbol', + source: 'search_results', + layout: { + 'icon-image': 'marker', + 'icon-anchor': 'bottom', + 'icon-allow-overlap': true, + 'text-field': '{name}', + 'text-font': ['Raleway SemiBold'], + 'text-size': 13, + 'text-anchor': 'top', + 'text-offset': [0, 0.4], + 'text-optional': true, + }, + paint: { + 'text-color': 'rgba(50, 50, 50, 1)', + 'text-halo-color': 'rgba(255, 255, 255, 0.9)', + 'text-halo-width': 2, + }, + }, ] diff --git a/web/src/style/map_style.ts b/web/src/style/map_style.ts index d2cfc07..1b2511a 100644 --- a/web/src/style/map_style.ts +++ b/web/src/style/map_style.ts @@ -70,6 +70,10 @@ const style: StyleSpecification = { type: 'geojson', data: { type: 'FeatureCollection', features: [] }, }, + search_results: { + type: 'geojson', + data: { type: 'FeatureCollection', features: [] }, + }, }, glyphs: 'https://map.emfcamp.org/tiles/font/{fontstack}/{range}', layers: layers