From e96b544a75c86e7809fbdc0549749c3f0ebd23a4 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Fri, 30 Jan 2026 21:01:24 +0100 Subject: [PATCH 01/15] switch to maplibre, but laggy --- .github/workflows/deploy.yml | 2 +- README.md | 46 +- website/.env.example | 2 +- website/package-lock.json | 1210 +++-------------- website/package.json | 9 +- ...x-satellite.png => maptiler-satellite.png} | Bin ...{mapbox-outdoors.png => maptiler-topo.png} | Bin website/src/lib/assets/layers.ts | 31 +- website/src/lib/components/Logo.svelte | 8 +- .../src/lib/components/docs/DocsLayers.svelte | 4 +- .../elevation-profile/elevation-profile.ts | 6 +- .../embedding/EmbeddingPlayground.svelte | 14 +- .../src/lib/components/embedding/embedding.ts | 12 +- .../file-list/FileListNodeLabel.svelte | 25 +- website/src/lib/components/map/Map.svelte | 72 +- .../map/custom-control/CustomControl.svelte | 2 +- .../map/custom-control/custom-control.ts | 2 +- .../components/map/gpx-layer/GPXLayers.svelte | 7 +- .../map/gpx-layer/TrackpointPopup.svelte | 1 + .../map/gpx-layer/distance-markers.ts | 76 +- .../map/gpx-layer/gpx-layer-popup.ts | 3 +- .../lib/components/map/gpx-layer/gpx-layer.ts | 145 +- .../components/map/gpx-layer/gpx-layers.ts | 2 + .../map/gpx-layer/start-end-markers.ts | 10 +- .../map/layer-control/CustomLayers.svelte | 9 +- .../map/layer-control/LayerControl.svelte | 117 +- .../layer-control/LayerControlSettings.svelte | 4 +- .../map/layer-control/OverpassPopup.svelte | 45 +- .../map/layer-control/extension-api.ts | 2 +- .../map/layer-control/overpass-layer.ts | 37 +- website/src/lib/components/map/map-popup.ts | 16 +- website/src/lib/components/map/map.ts | 214 ++- .../StreetViewControl.svelte | 2 +- .../map/street-view-control/google.ts | 7 +- .../map/street-view-control/mapillary.ts | 24 +- website/src/lib/components/map/style.ts | 221 +++ .../components/toolbar/ToolbarItemMenu.svelte | 6 +- .../lib/components/toolbar/tools/Clean.svelte | 24 +- .../components/toolbar/tools/Elevation.svelte | 7 +- .../toolbar/tools/reduce/utils.svelte.ts | 24 +- .../toolbar/tools/routing/Routing.svelte | 2 +- .../toolbar/tools/routing/routing-controls.ts | 16 +- .../toolbar/tools/scissors/split-controls.ts | 35 +- .../toolbar/tools/waypoint/Waypoint.svelte | 6 +- website/src/lib/docs/en/home/funding.mdx | 2 +- website/src/lib/docs/en/home/mapbox.mdx | 5 - website/src/lib/docs/en/home/maptiler.mdx | 2 + website/src/lib/docs/en/integration.mdx | 2 +- website/src/lib/docs/en/map-controls.mdx | 4 +- website/src/lib/docs/en/toolbar/elevation.mdx | 4 +- website/src/lib/logic/bounds.ts | 8 +- website/src/lib/logic/file-actions.ts | 2 +- website/src/lib/utils.ts | 64 +- website/src/locales/en.json | 13 +- website/src/routes/[[language]]/+page.svelte | 18 +- website/src/routes/[[language]]/+page.ts | 2 +- website/static/mapbox-logo-black.svg | 38 - website/static/mapbox-logo-white.svg | 42 - website/static/maptiler-logo-dark.svg | 47 + website/static/maptiler-logo.svg | 45 + 60 files changed, 1059 insertions(+), 1746 deletions(-) rename website/src/lib/assets/img/home/{mapbox-satellite.png => maptiler-satellite.png} (100%) rename website/src/lib/assets/img/home/{mapbox-outdoors.png => maptiler-topo.png} (100%) create mode 100644 website/src/lib/components/map/style.ts delete mode 100644 website/src/lib/docs/en/home/mapbox.mdx create mode 100644 website/src/lib/docs/en/home/maptiler.mdx delete mode 100644 website/static/mapbox-logo-black.svg delete mode 100644 website/static/mapbox-logo-white.svg create mode 100644 website/static/maptiler-logo-dark.svg create mode 100644 website/static/maptiler-logo.svg diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e3dcb61ce..d354c062b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,7 +31,7 @@ jobs: - name: Create env file run: | touch website/.env - echo PUBLIC_MAPBOX_TOKEN=${{ secrets.PUBLIC_MAPBOX_TOKEN }} >> website/.env + echo PUBLIC_MAPTILER_KEY=${{ secrets.PUBLIC_MAPTILER_KEY }} >> website/.env cat website/.env - name: Build website diff --git a/README.md b/README.md index 04be098d3..f177dd5b6 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ Any help is greatly appreciated! The code is split into two parts: -- `gpx`: a Typescript library for parsing and manipulating GPX files, -- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application. +- `gpx`: a Typescript library for parsing and manipulating GPX files, +- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application. You will need [Node.js](https://nodejs.org/) to build and run these two parts. @@ -42,11 +42,11 @@ npm run build ### Running the website -To be able to load the map, you will need to create your own Mapbox access token and store it in a `.env` file in the `website` directory. +To be able to load the map, you will need to create your own MapTiler key and store it in a `.env` file in the `website` directory. ```bash cd website -echo PUBLIC_MAPBOX_TOKEN={YOUR_MAPBOX_TOKEN} >> .env +echo PUBLIC_MAPTILER_KEY={YOUR_MAPTILER_KEY} >> .env npm install npm run dev ``` @@ -55,25 +55,25 @@ npm run dev This project has been made possible thanks to the following open source projects: -- Development: - - [Svelte](https://github.com/sveltejs/svelte) and [SvelteKit](https://github.com/sveltejs/kit) — seamless development experience - - [MDsveX](https://github.com/pngwn/MDsveX) — allowing a Markdown-based documentation -- Design: - - [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte) — beautiful components - - [@lucide/svelte](https://github.com/lucide-icons/lucide/tree/main/packages/svelte) — beautiful icons - - [tailwindcss](https://github.com/tailwindlabs/tailwindcss) — easy styling - - [Chart.js](https://github.com/chartjs/Chart.js) — beautiful and fast charts -- Logic: - - [immer](https://github.com/immerjs/immer) — complex state management - - [Dexie.js](https://github.com/dexie/Dexie.js) — IndexedDB wrapper - - [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) — fast GPX file parsing - - [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree -- Mapping: - - [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) — beautiful and fast interactive maps - - [brouter](https://github.com/abrensch/brouter) — routing engine - - [OpenStreetMap](https://www.openstreetmap.org) — map data used by Mapbox and brouter -- Search: - - [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation +- Development: + - [Svelte](https://github.com/sveltejs/svelte) and [SvelteKit](https://github.com/sveltejs/kit) — seamless development experience + - [MDsveX](https://github.com/pngwn/MDsveX) — allowing a Markdown-based documentation +- Design: + - [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte) — beautiful components + - [@lucide/svelte](https://github.com/lucide-icons/lucide/tree/main/packages/svelte) — beautiful icons + - [tailwindcss](https://github.com/tailwindlabs/tailwindcss) — easy styling + - [Chart.js](https://github.com/chartjs/Chart.js) — beautiful and fast charts +- Logic: + - [immer](https://github.com/immerjs/immer) — complex state management + - [Dexie.js](https://github.com/dexie/Dexie.js) — IndexedDB wrapper + - [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) — fast GPX file parsing + - [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree +- Mapping: + - [MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js) — beautiful and fast interactive maps + - [brouter](https://github.com/abrensch/brouter) — routing engine + - [OpenStreetMap](https://www.openstreetmap.org) — map data used by most of the map layers, and by the routing engine +- Search: + - [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation ## License diff --git a/website/.env.example b/website/.env.example index 4d6b95bd3..dc5ca3183 100644 --- a/website/.env.example +++ b/website/.env.example @@ -1 +1 @@ -PUBLIC_MAPBOX_TOKEN=YOUR_MAPBOX_TOKEN \ No newline at end of file +PUBLIC_MAPTILER_KEY=YOUR_MAPTILER_KEY \ No newline at end of file diff --git a/website/package-lock.json b/website/package-lock.json index a4e6d3904..caddfe48f 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -10,9 +10,9 @@ "dependencies": { "@docsearch/js": "^3.9.0", "@internationalized/date": "^3.8.2", - "@mapbox/mapbox-gl-geocoder": "^5.0.3", "@mapbox/sphericalmercator": "^2.0.1", "@mapbox/tilebelt": "^2.0.2", + "@maplibre/maplibre-gl-geocoder": "^1.9.4", "@types/mapbox__sphericalmercator": "^1.2.3", "chart.js": "^4.5.1", "chartjs-plugin-zoom": "^2.2.0", @@ -22,9 +22,8 @@ "gpx": "file:../gpx", "immer": "^10.1.1", "jszip": "^3.10.1", - "mapbox-gl": "^3.17.0", "mapillary-js": "^4.1.2", - "png.js": "^0.2.1", + "maplibre-gl": "^5.16.0", "sanitize-html": "^2.17.0", "sortablejs": "^1.15.6", "tailwind-merge": "^3.3.0" @@ -40,7 +39,6 @@ "@types/events": "^3.0.3", "@types/file-saver": "^2.0.7", "@types/mapbox__tilebelt": "^1.0.4", - "@types/mapbox-gl": "^3.4.1", "@types/node": "^22.15.30", "@types/png.js": "^0.2.3", "@types/sanitize-html": "^2.16.0", @@ -329,27 +327,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@docsearch/css": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.9.0.tgz", @@ -1635,13 +1612,29 @@ "svelte": "^5" } }, - "node_modules/@mapbox/fusspot": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@mapbox/fusspot/-/fusspot-0.4.0.tgz", - "integrity": "sha512-6sys1vUlhNCqMvJOqPEPSi0jc9tg7aJ//oG1A16H3PXoIt9whtNngD7UzBHUVTH15zunR/vRvMtGNVsogm1KzA==", + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "license": "ISC", "dependencies": { - "is-plain-obj": "^1.1.0", - "xtend": "^4.0.1" + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, + "node_modules/@mapbox/geojson-rewind/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@mapbox/jsonlint-lines-primitives": { @@ -1652,71 +1645,12 @@ "node": ">= 0.6" } }, - "node_modules/@mapbox/mapbox-gl-geocoder": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-geocoder/-/mapbox-gl-geocoder-5.0.3.tgz", - "integrity": "sha512-aeu2ZM+UKoMUGqqKy4UVVEKsIaNj2KSsiQ4p4YbNSAjZj2vcP33KSod+DPeRwhvoY+MU6KgyvdZ/1xdmH+C62g==", - "dependencies": { - "@mapbox/mapbox-sdk": "^0.16.1", - "events": "^3.3.0", - "lodash.debounce": "^4.0.6", - "nanoid": "^3.1.31", - "subtag": "^0.5.0", - "suggestions": "^1.6.0", - "xtend": "^4.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@mapbox/mapbox-gl-supported": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-3.0.0.tgz", - "integrity": "sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg==" - }, - "node_modules/@mapbox/mapbox-sdk": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@mapbox/mapbox-sdk/-/mapbox-sdk-0.16.1.tgz", - "integrity": "sha512-dyZrmg+UL/Gp5mGG3CDbcwGSUMYYrfbd9hdp0rcA3pHSf3A9eYoXO9nFiIk6SzBwBVMzHENJz84ZHdqM0MDncQ==", - "dependencies": { - "@mapbox/fusspot": "^0.4.0", - "@mapbox/parse-mapbox-token": "^0.2.0", - "@mapbox/polyline": "^1.0.0", - "eventemitter3": "^3.1.0", - "form-data": "^3.0.0", - "got": "^11.8.5", - "is-plain-obj": "^1.1.0", - "xtend": "^4.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@mapbox/parse-mapbox-token": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@mapbox/parse-mapbox-token/-/parse-mapbox-token-0.2.0.tgz", - "integrity": "sha512-BjeuG4sodYaoTygwXIuAWlZV6zUv4ZriYAQhXikzx+7DChycMUQ9g85E79Htat+AsBg+nStFALehlOhClYm5cQ==", - "dependencies": { - "base-64": "^0.1.0" - } - }, "node_modules/@mapbox/point-geometry": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", "license": "ISC" }, - "node_modules/@mapbox/polyline": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-1.2.1.tgz", - "integrity": "sha512-sn0V18O3OzW4RCcPoUIVDWvEGQaBNH9a0y5lgqrf5hUycyw1CzrhEoxV5irzrMNXKCkw1xRsZXcaVbsVZggHXA==", - "dependencies": { - "meow": "^9.0.0" - }, - "bin": { - "polyline": "bin/polyline.bin.js" - } - }, "node_modules/@mapbox/sphericalmercator": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@mapbox/sphericalmercator/-/sphericalmercator-2.0.1.tgz", @@ -1729,9 +1663,10 @@ "license": "MIT" }, "node_modules/@mapbox/tiny-sdf": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz", - "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==" + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz", + "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==", + "license": "BSD-2-Clause" }, "node_modules/@mapbox/unitbezier": { "version": "0.0.1", @@ -1769,6 +1704,87 @@ "node": ">=6.0.0" } }, + "node_modules/@maplibre/geojson-vt": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz", + "integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==", + "license": "ISC" + }, + "node_modules/@maplibre/maplibre-gl-geocoder": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-geocoder/-/maplibre-gl-geocoder-1.9.4.tgz", + "integrity": "sha512-ss0NMpjUgK1/8YrrikrAtdda41jERiGg+XqwPkj52AhwvQTLZEnZSU7IhqdyuE1FZ/QhlzAauMbyzJUTTxDscw==", + "license": "ISC", + "dependencies": { + "events": "^3.3.0", + "lodash.debounce": "^4.0.6", + "subtag": "^0.5.0", + "suggestions-list": "^0.0.2", + "xtend": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "maplibre-gl": ">=4.0.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "24.4.1", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.4.1.tgz", + "integrity": "sha512-UKhA4qv1h30XT768ccSv5NjNCX+dgfoq2qlLVmKejspPcSQTYD4SrVucgqegmYcKcmwf06wcNAa/kRd0NHWbUg==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "rw": "^1.3.3", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/mlt": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.2.tgz", + "integrity": "sha512-SQKdJ909VGROkA6ovJgtHNs9YXV4YXUPS+VaZ50I2Mt951SLlUm2Cv34x5Xwc1HiFlsd3h2Yrs5cn7xzqBmENw==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0" + } + }, + "node_modules/@maplibre/vt-pbf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.2.1.tgz", + "integrity": "sha512-IxZBGq/+9cqf2qdWlFuQ+ZfoMhWpxDUGQZ/poPHOJBvwMUT1GuxLo6HgYTou+xxtsOsjfbcjI8PZaPCtmt97rA==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/vector-tile": "^2.0.4", + "@maplibre/geojson-vt": "^5.0.4", + "@types/geojson": "^7946.0.16", + "@types/supercluster": "^7.1.3", + "pbf": "^4.0.1", + "supercluster": "^8.0.1" + } + }, + "node_modules/@maplibre/vt-pbf/node_modules/pbf": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", + "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2147,17 +2163,6 @@ "win32" ] }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, "node_modules/@sveltejs/acorn-typescript": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", @@ -2277,17 +2282,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@tailwindcss/node": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.8.tgz", @@ -2565,17 +2559,6 @@ "vite": "^5.2.0 || ^6" } }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -2628,6 +2611,7 @@ "version": "3.2.5", "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", + "license": "MIT", "dependencies": { "@types/geojson": "*" } @@ -2638,31 +2622,12 @@ "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", "license": "MIT" }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/mapbox__point-geometry": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", - "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", - "license": "MIT" - }, "node_modules/@types/mapbox__sphericalmercator": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/mapbox__sphericalmercator/-/mapbox__sphericalmercator-1.2.3.tgz", @@ -2677,16 +2642,6 @@ "@types/geojson": "*" } }, - "node_modules/@types/mapbox-gl": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz", - "integrity": "sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -2697,25 +2652,16 @@ "@types/unist": "*" } }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==" - }, "node_modules/@types/node": { "version": "22.15.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==" - }, "node_modules/@types/pako": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.7.tgz", @@ -2742,14 +2688,6 @@ "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-3.0.4.tgz", "integrity": "sha512-knSt9cCW8jj1ZSFcFeBZaX++OucmfPxxHiRwTahZfJlnQsek7O0bazTJHWD2RVj9LEoejUYF2de3/stf+QXcXw==" }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/sanitize-html": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.16.0.tgz", @@ -3142,14 +3080,6 @@ "node": ">= 0.4" } }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/asn1.js": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", @@ -3180,11 +3110,6 @@ "util": "^0.12.5" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3215,11 +3140,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3525,31 +3445,6 @@ "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", "dev": true }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -3607,38 +3502,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dependencies": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-keys/node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "engines": { - "node": ">=8" - } - }, "node_modules/camelize": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", @@ -3688,11 +3551,6 @@ "chart.js": ">=3.2.0" } }, - "node_modules/cheap-ruler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz", - "integrity": "sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==" - }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -3713,17 +3571,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3776,17 +3623,6 @@ "simple-swizzle": "^0.2.2" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3905,11 +3741,6 @@ "node": "*" } }, - "node_modules/csscolorparser": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", - "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==" - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3940,62 +3771,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4010,14 +3785,6 @@ "node": ">=0.10.0" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "engines": { - "node": ">=10" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -4052,14 +3819,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4202,9 +3961,9 @@ } }, "node_modules/earcut": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz", - "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", "license": "ISC" }, "node_modules/eastasianwidth": { @@ -4243,14 +4002,6 @@ "dev": true, "license": "MIT" }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -4286,14 +4037,6 @@ "xtend": "~4.0.0" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -4663,11 +4406,6 @@ "individual": "^3.0.0" } }, - "node_modules/eventemitter3": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", - "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" - }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -4842,19 +4580,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4873,6 +4598,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4888,7 +4614,8 @@ "node_modules/geojson-vt": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", - "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==" + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", + "license": "ISC" }, "node_modules/get-intrinsic": { "version": "1.3.0", @@ -4927,20 +4654,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-tsconfig": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", @@ -5045,30 +4758,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, "node_modules/gpx": { "resolved": "../gpx", "link": true @@ -5086,11 +4775,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/grid-index": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", - "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==" - }, "node_modules/hammerjs": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", @@ -5099,14 +4783,6 @@ "node": ">=0.8.0" } }, - "node_modules/hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "engines": { - "node": ">=6" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -5182,6 +4858,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -5200,17 +4877,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -5229,23 +4895,6 @@ "entities": "^4.4.0" } }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, "node_modules/https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", @@ -5332,14 +4981,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "engines": { - "node": ">=8" - } - }, "node_modules/individual": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/individual/-/individual-3.0.0.tgz", @@ -5373,11 +5014,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -5394,6 +5030,7 @@ "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, "dependencies": { "hasown": "^2.0.2" }, @@ -5483,14 +5120,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -5570,11 +5199,6 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -5591,12 +5215,8 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -5611,6 +5231,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -5668,18 +5294,11 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "dependencies": { "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -5966,11 +5585,6 @@ "node": ">=10" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -5995,7 +5609,8 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -6011,25 +5626,6 @@ "node": ">=0.6" } }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/lucide-static": { "version": "0.513.0", "resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-0.513.0.tgz", @@ -6057,69 +5653,6 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mapbox-gl": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.17.0.tgz", - "integrity": "sha512-nCrDKRlr5di6xUksUDslNWwxroJ5yv1hT8pyVFtcpWJOOKsYQxF/wOFTMie8oxMnXeFkrz1Tl1TwA1XN1yX0KA==", - "license": "SEE LICENSE IN LICENSE.txt", - "workspaces": [ - "src/style-spec", - "test/build/vite", - "test/build/webpack", - "test/build/typings" - ], - "dependencies": { - "@mapbox/jsonlint-lines-primitives": "^2.0.2", - "@mapbox/mapbox-gl-supported": "^3.0.0", - "@mapbox/point-geometry": "^1.1.0", - "@mapbox/tiny-sdf": "^2.0.6", - "@mapbox/unitbezier": "^0.0.1", - "@mapbox/vector-tile": "^2.0.4", - "@mapbox/whoots-js": "^3.1.0", - "@types/geojson": "^7946.0.16", - "@types/geojson-vt": "^3.2.5", - "@types/mapbox__point-geometry": "^0.1.4", - "@types/pbf": "^3.0.5", - "@types/supercluster": "^7.1.3", - "cheap-ruler": "^4.0.0", - "csscolorparser": "~1.0.3", - "earcut": "^3.0.1", - "geojson-vt": "^4.0.2", - "gl-matrix": "^3.4.4", - "grid-index": "^1.1.0", - "kdbush": "^4.0.2", - "martinez-polygon-clipping": "^0.7.4", - "murmurhash-js": "^1.0.0", - "pbf": "^4.0.1", - "potpack": "^2.0.0", - "quickselect": "^3.0.0", - "supercluster": "^8.0.1", - "tinyqueue": "^3.0.0" - } - }, - "node_modules/mapbox-gl/node_modules/pbf": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", - "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==", - "license": "BSD-3-Clause", - "dependencies": { - "resolve-protobuf-schema": "^2.1.0" - }, - "bin": { - "pbf": "bin/pbf" - } - }, "node_modules/mapillary-js": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/mapillary-js/-/mapillary-js-4.1.2.tgz", @@ -6155,6 +5688,56 @@ "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" }, + "node_modules/maplibre-gl": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.16.0.tgz", + "integrity": "sha512-/VDY89nr4jgLJyzmhy325cG6VUI02WkZ/UfVuDbG/piXzo6ODnM+omDFIwWY8tsEsBG26DNDmNMn3Y2ikHsBiA==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/tiny-sdf": "^2.0.7", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^2.0.4", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^24.4.1", + "@maplibre/mlt": "^1.1.2", + "@maplibre/vt-pbf": "^4.2.0", + "@types/geojson": "^7946.0.16", + "@types/geojson-vt": "3.2.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.2", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.4", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^4.0.1", + "potpack": "^2.1.0", + "quickselect": "^3.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, + "node_modules/maplibre-gl/node_modules/pbf": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", + "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/martinez-polygon-clipping": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.7.4.tgz", @@ -6208,42 +5791,6 @@ "svelte": "^3.56.0 || ^4.0.0 || ^5.0.0-next.120" } }, - "node_modules/meow": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", - "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", - "dependencies": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize": "^1.2.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6285,33 +5832,6 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "engines": { - "node": ">=4" - } - }, "node_modules/min-document": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", @@ -6320,14 +5840,6 @@ "dom-walk": "^0.1.0" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "engines": { - "node": ">=4" - } - }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -6355,17 +5867,13 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/minipass": { @@ -6519,31 +6027,6 @@ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", "dev": true }, - "node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6601,14 +6084,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6632,14 +6107,6 @@ "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", "dev": true }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "engines": { - "node": ">=8" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6670,14 +6137,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -6749,23 +6208,6 @@ "node": ">= 0.10" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parse-srcset": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", @@ -6781,6 +6223,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "engines": { "node": ">=8" } @@ -6797,7 +6240,8 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-scurry": { "version": "2.0.0", @@ -6884,11 +6328,6 @@ "node": ">=10" } }, - "node_modules/png.js": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/png.js/-/png.js-0.2.1.tgz", - "integrity": "sha512-EXQ+wk98iL3KuuPY6bUcfPeM1CZCK8/exKoTxY4H6EjU2hMYtV5ofx7gxmjL1mnJpcvexXexKuC3c5U7f7KVFg==" - }, "node_modules/polylabel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz", @@ -7033,9 +6472,10 @@ } }, "node_modules/potpack": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", - "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" }, "node_modules/preact": { "version": "10.24.1", @@ -7136,15 +6576,6 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7199,17 +6630,6 @@ } ] }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/quickselect": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", @@ -7247,124 +6667,6 @@ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" }, - "node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" - }, - "node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "engines": { - "node": ">=8" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -7379,22 +6681,11 @@ "node": ">= 6" } }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -7407,11 +6698,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -7439,17 +6725,6 @@ "protocol-buffers-schema": "^3.3.1" } }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -7554,6 +6829,12 @@ "svelte": "^5.7.0" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -7627,6 +6908,7 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7892,34 +7174,6 @@ "node": ">=0.10.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.20", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", - "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==" - }, "node_modules/splaytree": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/splaytree/-/splaytree-0.1.4.tgz", @@ -8065,17 +7319,6 @@ "node": ">=8" } }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8102,12 +7345,14 @@ "node_modules/subtag": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/subtag/-/subtag-0.5.0.tgz", - "integrity": "sha512-CaIBcTSb/nyk4xiiSOtZYz1B+F12ZxW8NEp54CdT+84vmh/h4sUnHGC6+KQXUfED8u22PQjCYWfZny8d2ELXwg==" + "integrity": "sha512-CaIBcTSb/nyk4xiiSOtZYz1B+F12ZxW8NEp54CdT+84vmh/h4sUnHGC6+KQXUfED8u22PQjCYWfZny8d2ELXwg==", + "license": "ISC" }, - "node_modules/suggestions": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/suggestions/-/suggestions-1.7.1.tgz", - "integrity": "sha512-gl5YPAhPYl07JZ5obiD9nTZsg4SyZswAQU/NNtnYiSnFkI3+ZHuXAiEsYm7AaZ71E0LXSFaGVaulGSWN3Gd71A==", + "node_modules/suggestions-list": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/suggestions-list/-/suggestions-list-0.0.2.tgz", + "integrity": "sha512-Yw0fdq14c6RQWQIfE1/8WEi9Dp8rjyCD6FhYA/Tit2/ADbE9Y4ADG4ezlvivsa8Civ5nz++pyVVBMjOMlgIUJw==", + "license": "ISC", "dependencies": { "fuzzy": "^0.1.1", "xtend": "^4.0.0" @@ -8137,6 +7382,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -8598,14 +7844,6 @@ "node": ">=6" } }, - "node_modules/trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "engines": { - "node": ">=8" - } - }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -8691,6 +7929,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, "license": "MIT" }, "node_modules/unist-util-is": { @@ -8795,15 +8034,6 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/vaul-svelte": { "version": "1.0.0-next.7", "resolved": "https://registry.npmjs.org/vaul-svelte/-/vaul-svelte-1.0.0-next.7.tgz", @@ -9172,11 +8402,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, "node_modules/x-is-array": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/x-is-array/-/x-is-array-0.1.0.tgz", @@ -9195,11 +8420,6 @@ "node": ">=0.4" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/yaml": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", @@ -9215,14 +8435,6 @@ "node": ">= 14.6" } }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/website/package.json b/website/package.json index c03e5d40d..37a9bbcca 100644 --- a/website/package.json +++ b/website/package.json @@ -23,10 +23,9 @@ "@types/eslint": "^9.6.1", "@types/events": "^3.0.3", "@types/file-saver": "^2.0.7", + "@types/mapbox__sphericalmercator": "^1.2.3", "@types/mapbox__tilebelt": "^1.0.4", - "@types/mapbox-gl": "^3.4.1", "@types/node": "^22.15.30", - "@types/png.js": "^0.2.3", "@types/sanitize-html": "^2.16.0", "@types/sortablejs": "^1.15.8", "@typescript-eslint/eslint-plugin": "^8.33.1", @@ -62,10 +61,9 @@ "dependencies": { "@docsearch/js": "^3.9.0", "@internationalized/date": "^3.8.2", - "@mapbox/mapbox-gl-geocoder": "^5.0.3", "@mapbox/sphericalmercator": "^2.0.1", "@mapbox/tilebelt": "^2.0.2", - "@types/mapbox__sphericalmercator": "^1.2.3", + "@maplibre/maplibre-gl-geocoder": "^1.9.4", "chart.js": "^4.5.1", "chartjs-plugin-zoom": "^2.2.0", "clsx": "^2.1.1", @@ -74,9 +72,8 @@ "gpx": "file:../gpx", "immer": "^10.1.1", "jszip": "^3.10.1", - "mapbox-gl": "^3.17.0", "mapillary-js": "^4.1.2", - "png.js": "^0.2.1", + "maplibre-gl": "^5.16.0", "sanitize-html": "^2.17.0", "sortablejs": "^1.15.6", "tailwind-merge": "^3.3.0" diff --git a/website/src/lib/assets/img/home/mapbox-satellite.png b/website/src/lib/assets/img/home/maptiler-satellite.png similarity index 100% rename from website/src/lib/assets/img/home/mapbox-satellite.png rename to website/src/lib/assets/img/home/maptiler-satellite.png diff --git a/website/src/lib/assets/img/home/mapbox-outdoors.png b/website/src/lib/assets/img/home/maptiler-topo.png similarity index 100% rename from website/src/lib/assets/img/home/mapbox-outdoors.png rename to website/src/lib/assets/img/home/maptiler-topo.png diff --git a/website/src/lib/assets/layers.ts b/website/src/lib/assets/layers.ts index 44b1cf72e..ff6813171 100644 --- a/website/src/lib/assets/layers.ts +++ b/website/src/lib/assets/layers.ts @@ -22,15 +22,18 @@ import { Binoculars, Toilet, } from 'lucide-static'; -import { type RasterDEMSourceSpecification, type StyleSpecification } from 'mapbox-gl'; +import { type RasterDEMSourceSpecification, type StyleSpecification } from 'maplibre-gl'; import ignFrTopo from './custom/ign-fr-topo.json'; import ignFrPlan from './custom/ign-fr-plan.json'; import ignFrSatellite from './custom/ign-fr-satellite.json'; import bikerouterGravel from './custom/bikerouter-gravel.json'; +export const maptilerKeyPlaceHolder = 'MAPTILER_KEY'; + export const basemaps: { [key: string]: string | StyleSpecification } = { - mapboxOutdoors: 'mapbox://styles/mapbox/outdoors-v12', - mapboxSatellite: 'mapbox://styles/mapbox/satellite-streets-v12', + maptilerTopo: `https://api.maptiler.com/maps/topo-v4/style.json?key=${maptilerKeyPlaceHolder}`, + maptilerOutdoors: `https://api.maptiler.com/maps/outdoor-v4/style.json?key=${maptilerKeyPlaceHolder}`, + maptilerSatellite: `https://api.maptiler.com/maps/hybrid-v4/style.json?key=${maptilerKeyPlaceHolder}`, openStreetMap: { version: 8, sources: { @@ -773,8 +776,9 @@ export type LayerTreeType = { [key: string]: LayerTreeType | boolean }; export const basemapTree: LayerTreeType = { basemaps: { world: { - mapboxOutdoors: true, - mapboxSatellite: true, + maptilerTopo: true, + maptilerOutdoors: true, + maptilerSatellite: true, openStreetMap: true, openTopoMap: true, openHikingMap: true, @@ -907,7 +911,7 @@ export const overpassTree: LayerTreeType = { }; // Default basemap used -export const defaultBasemap = 'mapboxOutdoors'; +export const defaultBasemap = 'maptilerTopo'; // Default overlays used (none) export const defaultOverlays: LayerTreeType = { @@ -996,8 +1000,9 @@ export const defaultOverpassQueries: LayerTreeType = { export const defaultBasemapTree: LayerTreeType = { basemaps: { world: { - mapboxOutdoors: true, - mapboxSatellite: true, + maptilerTopo: true, + maptilerOutdoors: true, + maptilerSatellite: true, openStreetMap: true, openTopoMap: true, openHikingMap: true, @@ -1136,7 +1141,7 @@ export type CustomLayer = { maxZoom: number; layerType: 'basemap' | 'overlay'; resourceType: 'raster' | 'vector'; - value: string | {}; + value: string | maplibregl.StyleSpecification; }; type OverpassQueryData = { @@ -1455,11 +1460,9 @@ export const overpassQueryData: Record = { }; export const terrainSources: { [key: string]: RasterDEMSourceSpecification } = { - 'mapbox-dem': { + 'maptiler-dem': { type: 'raster-dem', - url: 'mapbox://mapbox.mapbox-terrain-dem-v1', - tileSize: 512, - maxzoom: 14, + url: `https://api.maptiler.com/tiles/terrain-rgb-v2/tiles.json?key=${maptilerKeyPlaceHolder}`, }, mapterhorn: { type: 'raster-dem', @@ -1467,4 +1470,4 @@ export const terrainSources: { [key: string]: RasterDEMSourceSpecification } = { }, }; -export const defaultTerrainSource = 'mapbox-dem'; +export const defaultTerrainSource = 'maptiler-dem'; diff --git a/website/src/lib/components/Logo.svelte b/website/src/lib/components/Logo.svelte index f06d9351c..c23a18bb6 100644 --- a/website/src/lib/components/Logo.svelte +++ b/website/src/lib/components/Logo.svelte @@ -8,7 +8,7 @@ ...others }: { iconOnly?: boolean; - company?: 'gpx.studio' | 'mapbox' | 'github' | 'crowdin' | 'facebook' | 'reddit'; + company?: 'gpx.studio' | 'maptiler' | 'github' | 'crowdin' | 'facebook' | 'reddit'; [key: string]: any; } = $props(); @@ -19,10 +19,10 @@ alt="Logo of gpx.studio." {...others} /> -{:else if company === 'mapbox'} +{:else if company === 'maptiler'} Logo of Mapbox. {:else if company === 'github'} diff --git a/website/src/lib/components/docs/DocsLayers.svelte b/website/src/lib/components/docs/DocsLayers.svelte index 4c5abadc7..ba6e46ba3 100644 --- a/website/src/lib/components/docs/DocsLayers.svelte +++ b/website/src/lib/components/docs/DocsLayers.svelte @@ -1,10 +1,10 @@
- + url.length > 0), ids: driveIds.split(',').filter((id) => id.length > 0), elevation: { @@ -102,8 +102,8 @@
- - + + diff --git a/website/src/lib/components/embedding/embedding.ts b/website/src/lib/components/embedding/embedding.ts index 027fb4ea4..d68cc2c06 100644 --- a/website/src/lib/components/embedding/embedding.ts +++ b/website/src/lib/components/embedding/embedding.ts @@ -1,8 +1,8 @@ -import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public'; +import { PUBLIC_MAPTILER_KEY } from '$env/static/public'; import { basemaps } from '$lib/assets/layers'; export type EmbeddingOptions = { - token: string; + key: string; files: string[]; ids: string[]; basemap: string; @@ -26,10 +26,10 @@ export type EmbeddingOptions = { }; export const defaultEmbeddingOptions = { - token: '', + key: '', files: [], ids: [], - basemap: 'mapboxOutdoors', + basemap: 'maptilerTopo', elevation: { show: true, height: 170, @@ -107,7 +107,7 @@ export function getURLForGoogleDriveFile(fileId: string): string { export function convertOldEmbeddingOptions(options: URLSearchParams): any { let newOptions: any = { - token: PUBLIC_MAPBOX_TOKEN, + key: PUBLIC_MAPTILER_KEY, files: [], ids: [], }; @@ -123,7 +123,7 @@ export function convertOldEmbeddingOptions(options: URLSearchParams): any { if (options.has('source')) { let basemap = options.get('source')!; if (basemap === 'satellite') { - newOptions.basemap = 'mapboxSatellite'; + newOptions.basemap = 'maptilerSatellite'; } else if (basemap === 'otm') { newOptions.basemap = 'openTopoMap'; } else if (basemap === 'ohm') { diff --git a/website/src/lib/components/file-list/FileListNodeLabel.svelte b/website/src/lib/components/file-list/FileListNodeLabel.svelte index 78d970fe3..16b0c6724 100644 --- a/website/src/lib/components/file-list/FileListNodeLabel.svelte +++ b/website/src/lib/components/file-list/FileListNodeLabel.svelte @@ -34,11 +34,10 @@ import { editStyle } from '$lib/components/file-list/style/utils.svelte'; import { getSymbolKey, symbols } from '$lib/assets/symbols'; import { selection, copied, cut } from '$lib/logic/selection'; - import { map } from '$lib/components/map/map'; import { fileActions, pasteSelection } from '$lib/logic/file-actions'; import { allHidden } from '$lib/logic/hidden'; import { boundsManager } from '$lib/logic/bounds'; - import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers'; + import { gpxColors, gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers'; import { fileStateCollection } from '$lib/logic/file-state'; import { waypointPopup } from '$lib/components/map/gpx-layer/gpx-layer-popup'; import { allowedPastes } from './sortable-file-list'; @@ -58,19 +57,11 @@ let singleSelection = $derived($selection.size === 1); - let nodeColors: string[] = $state([]); - - $effect.pre(() => { + let nodeColors: string[] = $derived.by(() => { let colors: string[] = []; - if (node && $map) { + if (node) { if (node instanceof GPXFile) { - let defaultColor = undefined; - - let layer = gpxLayers.getLayer(item.getFileId()); - if (layer) { - defaultColor = layer.layerColor; - } - + let defaultColor = $gpxColors.get(item.getFileId()); let style = node.getStyle(defaultColor); colors = style.color; } else if (node instanceof Track) { @@ -83,14 +74,14 @@ colors.push(style['gpx_style:color']); } if (colors.length === 0) { - let layer = gpxLayers.getLayer(item.getFileId()); - if (layer) { - colors.push(layer.layerColor); + let defaultColor = $gpxColors.get(item.getFileId()); + if (defaultColor) { + colors.push(defaultColor); } } } } - nodeColors = colors; + return colors; }); let symbolKey = $derived(node instanceof Waypoint ? getSymbolKey(node.sym) : undefined); diff --git a/website/src/lib/components/map/Map.svelte b/website/src/lib/components/map/Map.svelte index d4b65e4a0..0d4f3626d 100644 --- a/website/src/lib/components/map/Map.svelte +++ b/website/src/lib/components/map/Map.svelte @@ -1,30 +1,25 @@ @@ -84,6 +85,7 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an diff --git a/website/src/lib/logic/statistics.ts b/website/src/lib/logic/statistics.ts index 1193c21a0..45322ce9c 100644 --- a/website/src/lib/logic/statistics.ts +++ b/website/src/lib/logic/statistics.ts @@ -1,5 +1,5 @@ import { selection } from '$lib/logic/selection'; -import { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx'; +import { GPXGlobalStatistics, GPXStatisticsGroup, type Coordinates } from 'gpx'; import { fileStateCollection, GPXFileState } from '$lib/logic/file-state'; import { ListFileItem, @@ -82,6 +82,8 @@ export const gpxStatistics = new SelectedGPXStatistics(); export const slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined> = writable(undefined); +export const hoveredPoint: Writable = writable(null); + gpxStatistics.subscribe(() => { slicedGPXStatistics.set(undefined); }); diff --git a/website/src/lib/utils.ts b/website/src/lib/utils.ts index 5237a6bdc..5e335ed65 100644 --- a/website/src/lib/utils.ts +++ b/website/src/lib/utils.ts @@ -197,6 +197,18 @@ export function getElevation( ); } +export function loadSVGIcon(map: maplibregl.Map, id: string, svg: string) { + if (!map.hasImage(id)) { + let icon = new Image(100, 100); + icon.onload = () => { + if (!map.hasImage(id)) { + map.addImage(id, icon); + } + }; + icon.src = 'data:image/svg+xml,' + encodeURIComponent(svg); + } +} + export function isMac() { return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0; } diff --git a/website/src/routes/[[language]]/+page.svelte b/website/src/routes/[[language]]/+page.svelte index 9a0a4f7bb..3268ca8b4 100644 --- a/website/src/routes/[[language]]/+page.svelte +++ b/website/src/routes/[[language]]/+page.svelte @@ -35,6 +35,7 @@ let gpxStatistics = writable(exampleGPXFile.getStatistics()); let slicedGPXStatistics = writable(undefined); + let hoveredPoint = writable(null); let additionalDatasets = writable(['speed', 'atemp']); let elevationFill = writable(undefined); @@ -197,6 +198,7 @@ diff --git a/website/src/routes/[[language]]/app/+page.svelte b/website/src/routes/[[language]]/app/+page.svelte index 56f0098d5..6c5002e18 100644 --- a/website/src/routes/[[language]]/app/+page.svelte +++ b/website/src/routes/[[language]]/app/+page.svelte @@ -16,7 +16,7 @@ import { loadFiles } from '$lib/logic/file-actions'; import { onDestroy, onMount } from 'svelte'; import { page } from '$app/state'; - import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics'; + import { gpxStatistics, hoveredPoint, slicedGPXStatistics } from '$lib/logic/statistics'; import { getURLForGoogleDriveFile } from '$lib/components/embedding/embedding'; import { db } from '$lib/db'; import { fileStateCollection } from '$lib/logic/file-state'; @@ -140,6 +140,7 @@ From 2189c76eddf03c6004631f62a80471193d10e973 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sun, 1 Feb 2026 17:46:24 +0100 Subject: [PATCH 07/15] renaming --- website/src/lib/components/toolbar/tools/routing/routing.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/lib/components/toolbar/tools/routing/routing.ts b/website/src/lib/components/toolbar/tools/routing/routing.ts index 6c8d9df6d..17006c814 100644 --- a/website/src/lib/components/toolbar/tools/routing/routing.ts +++ b/website/src/lib/components/toolbar/tools/routing/routing.ts @@ -6,7 +6,7 @@ import { get } from 'svelte/store'; const { routing, routingProfile, privateRoads } = settings; -export const brouterProfiles: { [key: string]: string } = { +export const routingProfiles: { [key: string]: string } = { bike: 'Trekking-dry', racing_bike: 'fastbike', gravel_bike: 'gravel', @@ -19,7 +19,7 @@ export const brouterProfiles: { [key: string]: string } = { export function route(points: Coordinates[]): Promise { if (get(routing)) { - return getRoute(points, brouterProfiles[get(routingProfile)], get(privateRoads)); + return getRoute(points, routingProfiles[get(routingProfile)], get(privateRoads)); } else { return getIntermediatePoints(points); } From dba01e1826d7ba824d33d7d71ef720f567f333cb Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sun, 1 Feb 2026 18:06:16 +0100 Subject: [PATCH 08/15] finish renaming --- .../src/lib/components/toolbar/tools/routing/Routing.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/lib/components/toolbar/tools/routing/Routing.svelte b/website/src/lib/components/toolbar/tools/routing/Routing.svelte index 9263e22ae..e97564d64 100644 --- a/website/src/lib/components/toolbar/tools/routing/Routing.svelte +++ b/website/src/lib/components/toolbar/tools/routing/Routing.svelte @@ -21,7 +21,7 @@ SquareArrowUpLeft, SquareArrowOutDownRight, } from '@lucide/svelte'; - import { brouterProfiles } from '$lib/components/toolbar/tools/routing/routing'; + import { routingProfiles } from '$lib/components/toolbar/tools/routing/routing'; import { i18n } from '$lib/i18n.svelte'; import { slide } from 'svelte/transition'; import { @@ -167,7 +167,7 @@ {i18n._(`toolbar.routing.activities.${$routingProfile}`)} - {#each Object.keys(brouterProfiles) as profile} + {#each Object.keys(routingProfiles) as profile} {i18n._( `toolbar.routing.activities.${profile}` From bfd0d90abc7e3122bda4238e54e9b0da6a4ed196 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sun, 1 Feb 2026 18:45:40 +0100 Subject: [PATCH 09/15] validate settings --- .../map/layer-control/CustomLayers.svelte | 17 +- .../layer-control/LayerControlSettings.svelte | 4 +- .../lib/components/map/layer-control/utils.ts | 2 - website/src/lib/components/map/style.ts | 4 +- website/src/lib/logic/settings.ts | 179 ++++++++++++++++-- 5 files changed, 168 insertions(+), 38 deletions(-) diff --git a/website/src/lib/components/map/layer-control/CustomLayers.svelte b/website/src/lib/components/map/layer-control/CustomLayers.svelte index e8214c807..9789b67ad 100644 --- a/website/src/lib/components/map/layer-control/CustomLayers.svelte +++ b/website/src/lib/components/map/layer-control/CustomLayers.svelte @@ -20,9 +20,8 @@ import { i18n } from '$lib/i18n.svelte'; import { defaultBasemap, type CustomLayer } from '$lib/assets/layers'; import { onMount } from 'svelte'; - import { customBasemapUpdate, isSelected, remove } from './utils'; + import { remove } from './utils'; import { settings } from '$lib/logic/settings'; - import { map } from '$lib/components/map/map'; import { dndzone } from 'svelte-dnd-action'; const { @@ -129,8 +128,8 @@ ], }; } - $customLayers[layerId] = layer; addLayer(layerId); + $customLayers[layerId] = layer; selectedLayerId = undefined; setDataFromSelectedLayer(); } @@ -153,9 +152,7 @@ return $tree; }); - if ($currentBasemap === layerId) { - $customBasemapUpdate++; - } else { + if ($currentBasemap !== layerId) { $currentBasemap = layerId; } @@ -171,14 +168,6 @@ return $tree; }); - if ($map && $currentOverlays && isSelected($currentOverlays, layerId)) { - try { - $map.removeImport(layerId); - } catch (e) { - // No reliable way to check if the map is ready to remove sources and layers - } - } - currentOverlays.update(($overlays) => { if (!$overlays.overlays.hasOwnProperty('custom')) { $overlays.overlays['custom'] = {}; diff --git a/website/src/lib/components/map/layer-control/LayerControlSettings.svelte b/website/src/lib/components/map/layer-control/LayerControlSettings.svelte index 15f377379..4dc0e2efc 100644 --- a/website/src/lib/components/map/layer-control/LayerControlSettings.svelte +++ b/website/src/lib/components/map/layer-control/LayerControlSettings.svelte @@ -167,11 +167,11 @@ {#if isSelected($selectedOverlayTree, selectedOverlay)} {#if $isLayerFromExtension(selectedOverlay)} {$getLayerName(selectedOverlay)} + {:else if $customLayers.hasOwnProperty(selectedOverlay)} + {$customLayers[selectedOverlay].name} {:else} {i18n._(`layers.label.${selectedOverlay}`)} {/if} - {:else if $customLayers.hasOwnProperty(selectedOverlay)} - {$customLayers[selectedOverlay].name} {/if} {/if} diff --git a/website/src/lib/components/map/layer-control/utils.ts b/website/src/lib/components/map/layer-control/utils.ts index 6f173ff60..fdfd1c872 100644 --- a/website/src/lib/components/map/layer-control/utils.ts +++ b/website/src/lib/components/map/layer-control/utils.ts @@ -76,5 +76,3 @@ export function removeAll(node: LayerTreeType, ids: string[]) { }); return node; } - -export const customBasemapUpdate = writable(0); diff --git a/website/src/lib/components/map/style.ts b/website/src/lib/components/map/style.ts index 274ebad6d..399a1b02d 100644 --- a/website/src/lib/components/map/style.ts +++ b/website/src/lib/components/map/style.ts @@ -7,7 +7,7 @@ import { overlays, terrainSources, } from '$lib/assets/layers'; -import { customBasemapUpdate, getLayers } from '$lib/components/map/layer-control/utils'; +import { getLayers } from '$lib/components/map/layer-control/utils'; import { i18n } from '$lib/i18n.svelte'; const { currentBasemap, currentOverlays, customLayers, opacities, terrainSource } = settings; @@ -52,10 +52,10 @@ export class StyleManager { } }); currentBasemap.subscribe(() => this.updateBasemap()); - customBasemapUpdate.subscribe(() => this.updateBasemap()); currentOverlays.subscribe(() => this.updateOverlays()); opacities.subscribe(() => this.updateOverlays()); terrainSource.subscribe(() => this.updateTerrain()); + customLayers.subscribe(() => this.updateBasemap()); } updateBasemap() { diff --git a/website/src/lib/logic/settings.ts b/website/src/lib/logic/settings.ts index c86df6ac0..3ec53e5c9 100644 --- a/website/src/lib/logic/settings.ts +++ b/website/src/lib/logic/settings.ts @@ -1,6 +1,7 @@ import { type Database } from '$lib/db'; import { liveQuery } from 'dexie'; import { + basemaps, defaultBasemap, defaultBasemapTree, defaultOpacities, @@ -9,7 +10,10 @@ import { defaultOverpassQueries, defaultOverpassTree, defaultTerrainSource, + overlays, + overpassQueryData, type CustomLayer, + type LayerTreeType, } from '$lib/assets/layers'; import { browser } from '$app/environment'; import { get, writable, type Writable } from 'svelte/store'; @@ -19,10 +23,12 @@ export class Setting { private _subscription: { unsubscribe: () => void } | null = null; private _key: string; private _value: Writable; + private _validator?: (value: V) => V; - constructor(key: string, initial: V) { + constructor(key: string, initial: V, validator?: (value: V) => V) { this._key = key; this._value = writable(initial); + this._validator = validator; } connectToDatabase(db: Database) { @@ -36,6 +42,9 @@ export class Setting { this._value.set(value); } } else { + if (this._validator) { + value = this._validator(value); + } this._value.set(value); } first = false; @@ -73,11 +82,13 @@ export class SettingInitOnFirstRead { private _key: string; private _value: Writable; private _initial: V; + private _validator?: (value: V) => V; - constructor(key: string, initial: V) { + constructor(key: string, initial: V, validator?: (value: V) => V) { this._key = key; this._value = writable(undefined); this._initial = initial; + this._validator = validator; } connectToDatabase(db: Database) { @@ -93,6 +104,9 @@ export class SettingInitOnFirstRead { this._value.set(value); } } else { + if (this._validator) { + value = this._validator(value); + } this._value.set(value); } first = false; @@ -128,37 +142,166 @@ export class SettingInitOnFirstRead { } } +function getValueValidator(allowed: V[], fallback: V) { + const dict = new Set(allowed); + return (value: V) => (dict.has(value) ? value : fallback); +} + +function getArrayValidator(allowed: V[]) { + const dict = new Set(allowed); + return (value: V[]) => value.filter((v) => dict.has(v)); +} + +function getLayerValidator(allowed: Record, fallback: string) { + return (layer: string) => + allowed.hasOwnProperty(layer) || + layer.startsWith('custom-') || + layer.startsWith('extension-') + ? layer + : fallback; +} + +function filterLayerTree(t: LayerTreeType, allowed: Record): LayerTreeType { + const filtered: LayerTreeType = {}; + Object.entries(t).forEach(([key, value]) => { + if (typeof value === 'object') { + filtered[key] = filterLayerTree(value, allowed); + } else if ( + allowed.hasOwnProperty(key) || + key.startsWith('custom-') || + key.startsWith('extension-') + ) { + filtered[key] = value; + } + }); + return filtered; +} + +function getLayerTreeValidator(allowed: Record) { + return (value: LayerTreeType) => filterLayerTree(value, allowed); +} + +type DistanceUnits = 'metric' | 'imperial' | 'nautical'; +type VelocityUnits = 'speed' | 'pace'; +type TemperatureUnits = 'celsius' | 'fahrenheit'; +type AdditionalDataset = 'speed' | 'hr' | 'cad' | 'atemp' | 'power'; +type ElevationFill = 'slope' | 'surface' | undefined; +type RoutingProfile = + | 'bike' + | 'racing_bike' + | 'gravel_bike' + | 'mountain_bike' + | 'foot' + | 'motorcycle' + | 'water' + | 'railway'; +type TerrainSource = 'maptiler-dem' | 'mapterhorn'; +type StreetViewSource = 'mapillary' | 'google'; + export const settings = { - distanceUnits: new Setting<'metric' | 'imperial' | 'nautical'>('distanceUnits', 'metric'), - velocityUnits: new Setting<'speed' | 'pace'>('velocityUnits', 'speed'), - temperatureUnits: new Setting<'celsius' | 'fahrenheit'>('temperatureUnits', 'celsius'), + distanceUnits: new Setting( + 'distanceUnits', + 'metric', + getValueValidator(['metric', 'imperial', 'nautical'], 'metric') + ), + velocityUnits: new Setting( + 'velocityUnits', + 'speed', + getValueValidator(['speed', 'pace'], 'speed') + ), + temperatureUnits: new Setting( + 'temperatureUnits', + 'celsius', + getValueValidator(['celsius', 'fahrenheit'], 'celsius') + ), elevationProfile: new Setting('elevationProfile', true), - additionalDatasets: new Setting('additionalDatasets', []), - elevationFill: new Setting<'slope' | 'surface' | undefined>('elevationFill', undefined), + additionalDatasets: new Setting( + 'additionalDatasets', + [], + getArrayValidator(['speed', 'hr', 'cad', 'atemp', 'power']) + ), + elevationFill: new Setting( + 'elevationFill', + undefined, + getValueValidator(['slope', 'surface', undefined], undefined) + ), treeFileView: new Setting('fileView', false), minimizeRoutingMenu: new Setting('minimizeRoutingMenu', false), routing: new Setting('routing', true), - routingProfile: new Setting('routingProfile', 'bike'), + routingProfile: new Setting( + 'routingProfile', + 'bike', + getValueValidator( + [ + 'bike', + 'racing_bike', + 'gravel_bike', + 'mountain_bike', + 'foot', + 'motorcycle', + 'water', + 'railway', + ], + 'bike' + ) + ), privateRoads: new Setting('privateRoads', false), - currentBasemap: new Setting('currentBasemap', defaultBasemap), - previousBasemap: new Setting('previousBasemap', defaultBasemap), - selectedBasemapTree: new Setting('selectedBasemapTree', defaultBasemapTree), - currentOverlays: new SettingInitOnFirstRead('currentOverlays', defaultOverlays), - previousOverlays: new Setting('previousOverlays', defaultOverlays), - selectedOverlayTree: new Setting('selectedOverlayTree', defaultOverlayTree), + currentBasemap: new Setting( + 'currentBasemap', + defaultBasemap, + getLayerValidator(basemaps, defaultBasemap) + ), + previousBasemap: new Setting( + 'previousBasemap', + defaultBasemap, + getLayerValidator(Object.keys(basemaps), defaultBasemap) + ), + selectedBasemapTree: new Setting( + 'selectedBasemapTree', + defaultBasemapTree, + getLayerTreeValidator(basemaps) + ), + currentOverlays: new SettingInitOnFirstRead( + 'currentOverlays', + defaultOverlays, + getLayerTreeValidator(overlays) + ), + previousOverlays: new Setting( + 'previousOverlays', + defaultOverlays, + getLayerTreeValidator(overlays) + ), + selectedOverlayTree: new Setting( + 'selectedOverlayTree', + defaultOverlayTree, + getLayerTreeValidator(overlays) + ), currentOverpassQueries: new SettingInitOnFirstRead( 'currentOverpassQueries', - defaultOverpassQueries + defaultOverpassQueries, + getLayerTreeValidator(overpassQueryData) + ), + selectedOverpassTree: new Setting( + 'selectedOverpassTree', + defaultOverpassTree, + getLayerTreeValidator(overpassQueryData) ), - selectedOverpassTree: new Setting('selectedOverpassTree', defaultOverpassTree), opacities: new Setting('opacities', defaultOpacities), customLayers: new Setting>('customLayers', {}), customBasemapOrder: new Setting('customBasemapOrder', []), customOverlayOrder: new Setting('customOverlayOrder', []), - terrainSource: new Setting('terrainSource', defaultTerrainSource), + terrainSource: new Setting( + 'terrainSource', + defaultTerrainSource, + getValueValidator(['maptiler-dem', 'mapterhorn'], defaultTerrainSource) + ), directionMarkers: new Setting('directionMarkers', false), distanceMarkers: new Setting('distanceMarkers', false), - streetViewSource: new Setting('streetViewSource', 'mapillary'), + streetViewSource: new Setting( + 'streetViewSource', + 'mapillary', + getValueValidator(['mapillary', 'google'], 'mapillary') + ), fileOrder: new Setting('fileOrder', []), defaultOpacity: new Setting('defaultOpacity', 0.7), defaultWidth: new Setting('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5), From b8c1500aad55ebb923b926c790f423714db839d6 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Mon, 2 Feb 2026 21:50:01 +0100 Subject: [PATCH 10/15] fix layer filtering in event manager --- .../components/map/map-layer-event-manager.ts | 27 +++++++++++++------ website/src/lib/logic/statistics-tree.ts | 10 +++---- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/website/src/lib/components/map/map-layer-event-manager.ts b/website/src/lib/components/map/map-layer-event-manager.ts index 29412098c..b87a43874 100644 --- a/website/src/lib/components/map/map-layer-event-manager.ts +++ b/website/src/lib/components/map/map-layer-event-manager.ts @@ -142,9 +142,9 @@ export class MapLayerEventManager { } private _handleMouseMove(e: maplibregl.MapMouseEvent) { - const layerIds = this._filterLayersContainingCoordinate( + const layerIds = this._filterLayersIntersectingBounds( Object.keys(this._listeners), - e.lngLat + this._getBounds(e.point) ); const features = layerIds.length > 0 @@ -228,11 +228,11 @@ export class MapLayerEventManager { } private _handleTouchStart(e: maplibregl.MapTouchEvent) { - const layerIds = this._filterLayersContainingCoordinate( + const layerIds = this._filterLayersIntersectingBounds( Object.keys(this._listeners).filter( (layerId) => this._listeners[layerId].touchstarts.length > 0 ), - e.lngLat + this._getBounds(e.point) ); if (layerIds.length === 0) return; const features = this._map.queryRenderedFeatures(e.points[0], { layers: layerIds }); @@ -258,17 +258,28 @@ export class MapLayerEventManager { }); } - private _filterLayersContainingCoordinate( + private _getBounds(point: maplibregl.Point) { + const delta = 30; + return new maplibregl.LngLatBounds( + this._map.unproject([point.x - delta, point.y + delta]), + this._map.unproject([point.x + delta, point.y - delta]) + ); + } + + private _filterLayersIntersectingBounds( layerIds: string[], - lngLat: maplibregl.LngLat + bounds: maplibregl.LngLatBounds ): string[] { let result = layerIds.filter((layerId) => { if (!this._map.getLayer(layerId)) return false; const fileId = layerId.replace('-waypoints', ''); if (fileId === layerId) { - return fileStateCollection.getStatistics(fileId)?.inBBox(lngLat) ?? true; + return fileStateCollection.getStatistics(fileId)?.intersectsBBox(bounds) ?? true; } else { - return fileStateCollection.getStatistics(fileId)?.inWaypointBBox(lngLat) ?? true; + return ( + fileStateCollection.getStatistics(fileId)?.intersectsWaypointBBox(bounds) ?? + true + ); } }); return result; diff --git a/website/src/lib/logic/statistics-tree.ts b/website/src/lib/logic/statistics-tree.ts index 972491192..38d6a6a0c 100644 --- a/website/src/lib/logic/statistics-tree.ts +++ b/website/src/lib/logic/statistics-tree.ts @@ -49,7 +49,7 @@ export class GPXStatisticsTree { return statistics; } - inBBox(coordinates: { lat: number; lng: number }): boolean { + intersectsBBox(bounds: maplibregl.LngLatBounds): boolean { for (let key in this.statistics) { const stats = this.statistics[key]; if (stats instanceof GPXStatistics) { @@ -57,18 +57,18 @@ export class GPXStatisticsTree { stats.global.bounds.southWest, stats.global.bounds.northEast ); - if (!bbox.isEmpty() && bbox.contains(coordinates)) { + if (!bbox.isEmpty() && bbox.intersects(bounds)) { return true; } - } else if (stats.inBBox(coordinates)) { + } else if (stats.intersectsBBox(bounds)) { return true; } } return false; } - inWaypointBBox(coordinates: { lat: number; lng: number }): boolean { - return !this.wptBounds.isEmpty() && this.wptBounds.contains(coordinates); + intersectsWaypointBBox(bounds: maplibregl.LngLatBounds): boolean { + return !this.wptBounds.isEmpty() && this.wptBounds.intersects(bounds); } } export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree }; From 1137e851ce7aa6fa8eb92e51a3a58a8b50e8b9a9 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 11 Feb 2026 18:31:08 +0100 Subject: [PATCH 11/15] remove company support section until clarified --- website/src/lib/docs/en/home/maptiler.mdx | 2 -- website/src/routes/[[language]]/+page.svelte | 20 -------------------- website/src/routes/[[language]]/+page.ts | 1 - 3 files changed, 23 deletions(-) delete mode 100644 website/src/lib/docs/en/home/maptiler.mdx diff --git a/website/src/lib/docs/en/home/maptiler.mdx b/website/src/lib/docs/en/home/maptiler.mdx deleted file mode 100644 index 2f9c67c15..000000000 --- a/website/src/lib/docs/en/home/maptiler.mdx +++ /dev/null @@ -1,2 +0,0 @@ -MapTiler is the company that provides some of the beautiful maps on this website. -This partnership allows **gpx.studio** to benefit from MapTiler tools at discounted prices, greatly contributing to the financial viability of the project and enabling us to offer the best possible user experience. diff --git a/website/src/routes/[[language]]/+page.svelte b/website/src/routes/[[language]]/+page.svelte index 3268ca8b4..c826c31f6 100644 --- a/website/src/routes/[[language]]/+page.svelte +++ b/website/src/routes/[[language]]/+page.svelte @@ -29,7 +29,6 @@ data: { fundingModule: Promise; translationModule: Promise; - maptilerModule: Promise; }; } = $props(); @@ -272,23 +271,4 @@
-
-
-
-
- ❤️ {i18n._('homepage.supported_by')} -
- - - -
- {#await data.maptilerModule then maptilerModule} - - {/await} -
-
diff --git a/website/src/routes/[[language]]/+page.ts b/website/src/routes/[[language]]/+page.ts index a8a51dc9b..64f864778 100644 --- a/website/src/routes/[[language]]/+page.ts +++ b/website/src/routes/[[language]]/+page.ts @@ -9,6 +9,5 @@ export async function load({ params }) { return { fundingModule: getModule(language, 'funding'), translationModule: getModule(language, 'translation'), - maptilerModule: getModule(language, 'maptiler'), }; } From 88abd72a41125199fff3f692dc632bc52422e576 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sat, 14 Feb 2026 14:35:35 +0100 Subject: [PATCH 12/15] layer instead of markers for routing controls --- .../components/map/CoordinatesPopup.svelte | 4 + .../lib/components/map/gpx-layer/gpx-layer.ts | 13 +- .../map/gpx-layer/start-end-markers.ts | 60 +- .../components/map/map-layer-event-manager.ts | 66 +- website/src/lib/components/map/style.ts | 1 + .../toolbar/tools/routing/routing-controls.ts | 1260 ++++++++++------- website/src/lib/logic/map-cursor.ts | 6 +- website/src/lib/utils.ts | 4 +- 8 files changed, 824 insertions(+), 590 deletions(-) diff --git a/website/src/lib/components/map/CoordinatesPopup.svelte b/website/src/lib/components/map/CoordinatesPopup.svelte index f49be578f..6aac880b1 100644 --- a/website/src/lib/components/map/CoordinatesPopup.svelte +++ b/website/src/lib/components/map/CoordinatesPopup.svelte @@ -5,6 +5,10 @@ map.onLoad((map_) => { map_.on('contextmenu', (e) => { + if (map_.queryRenderedFeatures(e.point, { layers: ['routing-controls'] }).length) { + // Clicked on routing control, ignoring + return; + } trackpointPopup?.setItem({ item: new TrackPoint({ attributes: { diff --git a/website/src/lib/components/map/gpx-layer/gpx-layer.ts b/website/src/lib/components/map/gpx-layer/gpx-layer.ts index ee222435f..1efebd505 100644 --- a/website/src/lib/components/map/gpx-layer/gpx-layer.ts +++ b/website/src/lib/components/map/gpx-layer/gpx-layer.ts @@ -287,6 +287,7 @@ export class GPXLayer { _map.addSource(this.fileId + '-waypoints', { type: 'geojson', data: this.currentWaypointData, + promoteId: 'waypointIndex', }); } @@ -645,7 +646,17 @@ export class GPXLayer { | GeoJSONSource | undefined; if (waypointSource) { - waypointSource.setData(this.currentWaypointData!); + waypointSource.updateData({ + update: [ + { + id: this.draggedWaypointIndex, + newGeometry: { + type: 'Point', + coordinates: [e.lngLat.lng, e.lngLat.lat], + }, + }, + ], + }); } } diff --git a/website/src/lib/components/map/gpx-layer/start-end-markers.ts b/website/src/lib/components/map/gpx-layer/start-end-markers.ts index 59e38a762..10323e762 100644 --- a/website/src/lib/components/map/gpx-layer/start-end-markers.ts +++ b/website/src/lib/components/map/gpx-layer/start-end-markers.ts @@ -50,39 +50,41 @@ export class StartEndMarkers { const slicedStatistics = get(slicedGPXStatistics); const hovered = get(hoveredPoint); const hidden = get(allHidden); - if (statistics.global.length > 0 && tool !== Tool.ROUTING && !hidden) { - const start = statistics - .getTrackPoint(slicedStatistics?.[1] ?? 0)! - .trkpt.getCoordinates(); - const end = statistics - .getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)! - .trkpt.getCoordinates(); + if (!hidden) { const data: GeoJSON.FeatureCollection = { type: 'FeatureCollection', - features: [ - { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [start.lon, start.lat], - }, - properties: { - icon: 'start-marker', - }, - }, - { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [end.lon, end.lat], - }, - properties: { - icon: 'end-marker', - }, - }, - ], + features: [], }; + if (statistics.global.length > 0 && tool !== Tool.ROUTING) { + const start = statistics + .getTrackPoint(slicedStatistics?.[1] ?? 0)! + .trkpt.getCoordinates(); + const end = statistics + .getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)! + .trkpt.getCoordinates(); + data.features.push({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [start.lon, start.lat], + }, + properties: { + icon: 'start-marker', + }, + }); + data.features.push({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [end.lon, end.lat], + }, + properties: { + icon: 'end-marker', + }, + }); + } + if (hovered) { data.features.push({ type: 'Feature', diff --git a/website/src/lib/components/map/map-layer-event-manager.ts b/website/src/lib/components/map/map-layer-event-manager.ts index b87a43874..a30fb9708 100644 --- a/website/src/lib/components/map/map-layer-event-manager.ts +++ b/website/src/lib/components/map/map-layer-event-manager.ts @@ -142,21 +142,7 @@ export class MapLayerEventManager { } private _handleMouseMove(e: maplibregl.MapMouseEvent) { - const layerIds = this._filterLayersIntersectingBounds( - Object.keys(this._listeners), - this._getBounds(e.point) - ); - const features = - layerIds.length > 0 - ? this._map.queryRenderedFeatures(e.point, { layers: layerIds }) - : []; - const featuresByLayer: Record = {}; - features.forEach((f) => { - if (!featuresByLayer[f.layer.id]) { - featuresByLayer[f.layer.id] = []; - } - featuresByLayer[f.layer.id].push(f); - }); + const featuresByLayer = this._getRenderedFeaturesByLayer(e); Object.keys(this._listeners).forEach((layerId) => { const features = featuresByLayer[layerId] || []; const listener = this._listeners[layerId]; @@ -183,7 +169,6 @@ export class MapLayerEventManager { listener.mouseleaves.forEach((l) => l(event)); } } - listener.features = features; } if (features.length > 0 && listener.mousemoves.length > 0) { const event = new maplibregl.MapMouseEvent('mousemove', e.target, e.originalEvent, { @@ -191,15 +176,19 @@ export class MapLayerEventManager { }); listener.mousemoves.forEach((l) => l(event)); } + listener.features = features; }); } private _handleMouseClick(type: string, e: maplibregl.MapMouseEvent) { - Object.values(this._listeners).forEach((listener) => { - if (listener.features.length > 0) { + const featuresByLayer = this._getRenderedFeaturesByLayer(e); + Object.keys(this._listeners).forEach((layerId) => { + const features = featuresByLayer[layerId] || []; + const listener = this._listeners[layerId]; + if (features.length > 0) { if (type === 'click' && listener.clicks.length > 0) { const event = new maplibregl.MapMouseEvent('click', e.target, e.originalEvent, { - features: listener.features, + features: features, }); listener.clicks.forEach((l) => l(event)); } else if (type === 'contextmenu' && listener.contextmenus.length > 0) { @@ -208,7 +197,7 @@ export class MapLayerEventManager { e.target, e.originalEvent, { - features: listener.features, + features: features, } ); listener.contextmenus.forEach((l) => l(event)); @@ -218,7 +207,7 @@ export class MapLayerEventManager { e.target, e.originalEvent, { - features: listener.features, + features: features, } ); listener.mousedowns.forEach((l) => l(event)); @@ -228,21 +217,7 @@ export class MapLayerEventManager { } private _handleTouchStart(e: maplibregl.MapTouchEvent) { - const layerIds = this._filterLayersIntersectingBounds( - Object.keys(this._listeners).filter( - (layerId) => this._listeners[layerId].touchstarts.length > 0 - ), - this._getBounds(e.point) - ); - if (layerIds.length === 0) return; - const features = this._map.queryRenderedFeatures(e.points[0], { layers: layerIds }); - const featuresByLayer: Record = {}; - features.forEach((f) => { - if (!featuresByLayer[f.layer.id]) { - featuresByLayer[f.layer.id] = []; - } - featuresByLayer[f.layer.id].push(f); - }); + const featuresByLayer = this._getRenderedFeaturesByLayer(e); Object.keys(this._listeners).forEach((layerId) => { const features = featuresByLayer[layerId] || []; const listener = this._listeners[layerId]; @@ -284,4 +259,23 @@ export class MapLayerEventManager { }); return result; } + + private _getRenderedFeaturesByLayer(e: maplibregl.MapMouseEvent | maplibregl.MapTouchEvent) { + const layerIds = this._filterLayersIntersectingBounds( + Object.keys(this._listeners), + this._getBounds(e.point) + ); + const features = + layerIds.length > 0 + ? this._map.queryRenderedFeatures(e.point, { layers: layerIds }) + : []; + const featuresByLayer: Record = {}; + features.forEach((f) => { + if (!featuresByLayer[f.layer.id]) { + featuresByLayer[f.layer.id] = []; + } + featuresByLayer[f.layer.id].push(f); + }); + return featuresByLayer; + } } diff --git a/website/src/lib/components/map/style.ts b/website/src/lib/components/map/style.ts index 399a1b02d..f695a9995 100644 --- a/website/src/lib/components/map/style.ts +++ b/website/src/lib/components/map/style.ts @@ -29,6 +29,7 @@ export const ANCHOR_LAYER_KEY = { interactions: 'interactions-end', overpass: 'overpass-end', waypoints: 'waypoints-end', + routingControls: 'routing-controls-end', }; const anchorLayers: maplibregl.LayerSpecification[] = Object.values(ANCHOR_LAYER_KEY).map((id) => ({ id: id, diff --git a/website/src/lib/components/toolbar/tools/routing/routing-controls.ts b/website/src/lib/components/toolbar/tools/routing/routing-controls.ts index 76ac8ce47..d26fcea5c 100644 --- a/website/src/lib/components/toolbar/tools/routing/routing-controls.ts +++ b/website/src/lib/components/toolbar/tools/routing/routing-controls.ts @@ -1,6 +1,11 @@ import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx'; import { get, writable, type Readable } from 'svelte/store'; -import maplibregl from 'maplibre-gl'; +import maplibregl, { + type MapMouseEvent, + type GeoJSONSource, + type MapLayerMouseEvent, + type MapLayerTouchEvent, +} from 'maplibre-gl'; import { route } from './routing'; import { toast } from 'svelte-sonner'; import { @@ -8,7 +13,7 @@ import { ListTrackItem, ListTrackSegmentItem, } from '$lib/components/file-list/file-list'; -import { getClosestLinePoint } from '$lib/utils'; +import { getClosestLinePoint, loadSVGIcon } from '$lib/utils'; import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { settings } from '$lib/logic/settings'; @@ -18,32 +23,48 @@ import { streetViewEnabled } from '$lib/components/map/street-view-control/utils import { fileActionManager } from '$lib/logic/file-action-manager'; import { i18n } from '$lib/i18n.svelte'; import { map } from '$lib/components/map/map'; +import { ANCHOR_LAYER_KEY } from '$lib/components/map/style'; const { streetViewSource } = settings; export const canChangeStart = writable(false); -function stopPropagation(e: any) { - e.stopPropagation(); -} +type AnchorProperties = { + trackIndex: number; + segmentIndex: number; + pointIndex: number; + anchorIndex: number; + minZoom: number; +}; +type Anchor = GeoJSON.Feature; export class RoutingControls { active: boolean = false; fileId: string = ''; file: Readable; - anchors: AnchorWithMarker[] = []; - shownAnchors: AnchorWithMarker[] = []; + anchors: GeoJSON.Feature[] = []; popup: maplibregl.Popup; popupElement: HTMLElement; - temporaryAnchor: AnchorWithMarker; - lastDragEvent = 0; fileUnsubscribe: () => void = () => {}; unsubscribes: Function[] = []; - toggleAnchorsForZoomLevelAndBoundsBinded: () => void = - this.toggleAnchorsForZoomLevelAndBounds.bind(this); - showTemporaryAnchorBinded: (e: any) => void = this.showTemporaryAnchor.bind(this); - updateTemporaryAnchorBinded: (e: any) => void = this.updateTemporaryAnchor.bind(this); - appendAnchorBinded: (e: maplibregl.MapMouseEvent) => void = this.appendAnchor.bind(this); + appendAnchorBinded: (e: MapMouseEvent) => void = this.appendAnchor.bind(this); + + draggedAnchorIndex: number | null = null; + draggingStartingPosition: maplibregl.Point = new maplibregl.Point(0, 0); + onMouseEnterBinded: () => void = this.onMouseEnter.bind(this); + onMouseLeaveBinded: () => void = this.onMouseLeave.bind(this); + onClickBinded: (e: MapLayerMouseEvent) => void = this.onClick.bind(this); + onMouseDownBinded: (e: MapLayerMouseEvent) => void = this.onMouseDown.bind(this); + onTouchStartBinded: (e: MapLayerTouchEvent) => void = this.onTouchStart.bind(this); + onMouseMoveBinded: (e: MapLayerMouseEvent | MapLayerTouchEvent) => void = + this.onMouseMove.bind(this); + onMouseUpBinded: (e: MapLayerMouseEvent | MapLayerTouchEvent) => void = + this.onMouseUp.bind(this); + + temporaryAnchor: GeoJSON.Feature | null = null; + showTemporaryAnchorBinded: (e: MapLayerMouseEvent) => void = + this.showTemporaryAnchor.bind(this); + updateTemporaryAnchorBinded: (e: MapMouseEvent) => void = this.updateTemporaryAnchor.bind(this); constructor( fileId: string, @@ -56,15 +77,6 @@ export class RoutingControls { this.popup = popup; this.popupElement = popupElement; - let point = new TrackPoint({ - attributes: { - lat: 0, - lon: 0, - }, - }); - this.temporaryAnchor = this.createAnchor(point, new TrackSegment(), 0, 0); - this.temporaryAnchor.marker.getElement().classList.remove('z-10'); // Show below the other markers - this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this))); this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this))); } @@ -101,54 +113,98 @@ export class RoutingControls { this.active = true; - map_.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded); + this.loadIcons(); + map_.on('click', this.appendAnchorBinded); layerEventManager.on('mousemove', this.fileId, this.showTemporaryAnchorBinded); - layerEventManager.on('click', this.fileId, stopPropagation); this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this)); } updateControls() { - // Update the markers when the file changes - let file = get(this.file)?.file; - if (!file) { + const map_ = get(map); + const layerEventManager = map.layerEventManager; + const file = get(this.file)?.file; + if (!map_ || !layerEventManager || !file) { return; } - let anchorIndex = 0; + this.anchors = []; + file.forEachSegment((segment, trackIndex, segmentIndex) => { if ( get(selection).hasAnyParent( new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) ) ) { - for (let point of segment.trkpt) { - // Update the existing anchors (could be improved by matching the existing anchors with the new ones?) + for (let i = 0; i < segment.trkpt.length; i++) { + const point = segment.trkpt[i]; if (point._data.anchor) { - if (anchorIndex < this.anchors.length) { - this.anchors[anchorIndex].point = point; - this.anchors[anchorIndex].segment = segment; - this.anchors[anchorIndex].trackIndex = trackIndex; - this.anchors[anchorIndex].segmentIndex = segmentIndex; - this.anchors[anchorIndex].marker.setLngLat(point.getCoordinates()); - } else { - this.anchors.push( - this.createAnchor(point, segment, trackIndex, segmentIndex) - ); - } - anchorIndex++; + this.anchors.push({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [point.getLongitude(), point.getLatitude()], + }, + properties: { + trackIndex: trackIndex, + segmentIndex: segmentIndex, + pointIndex: i, + anchorIndex: this.anchors.length, + minZoom: point._data.zoom, + }, + }); } } } }); - while (anchorIndex < this.anchors.length) { - // Remove the extra anchors - this.anchors.pop()?.marker.remove(); - } + try { + let source = map_.getSource('routing-controls') as maplibregl.GeoJSONSource | undefined; + if (source) { + source.setData({ + type: 'FeatureCollection', + features: this.anchors, + }); + } else { + map_.addSource('routing-controls', { + type: 'geojson', + data: { + type: 'FeatureCollection', + features: this.anchors, + }, + promoteId: 'anchorIndex', + }); + } - this.toggleAnchorsForZoomLevelAndBounds(); + if (!map_.getLayer('routing-controls')) { + map_.addLayer( + { + id: 'routing-controls', + type: 'symbol', + source: 'routing-controls', + layout: { + 'icon-image': 'routing-control', + 'icon-size': 0.25, + 'icon-padding': 0, + 'icon-allow-overlap': true, + }, + filter: ['<=', ['get', 'minZoom'], ['zoom']], + }, + ANCHOR_LAYER_KEY.routingControls + ); + + layerEventManager.on('mouseenter', 'routing-controls', this.onMouseEnterBinded); + layerEventManager.on('mouseleave', 'routing-controls', this.onMouseLeaveBinded); + layerEventManager.on('click', 'routing-controls', this.onClickBinded); + layerEventManager.on('contextmenu', 'routing-controls', this.onClickBinded); + layerEventManager.on('mousedown', 'routing-controls', this.onMouseDownBinded); + layerEventManager.on('touchstart', 'routing-controls', this.onTouchStartBinded); + } + } catch (e) { + // No reliable way to check if the map is ready to add sources and layers + return; + } } remove() { @@ -157,371 +213,174 @@ export class RoutingControls { this.active = false; - for (let anchor of this.anchors) { - anchor.marker.remove(); - } - map_?.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded); map_?.off('click', this.appendAnchorBinded); layerEventManager?.off('mousemove', this.fileId, this.showTemporaryAnchorBinded); - layerEventManager?.off('click', this.fileId, stopPropagation); map_?.off('mousemove', this.updateTemporaryAnchorBinded); - this.temporaryAnchor.marker.remove(); + + try { + layerEventManager?.off('mouseenter', 'routing-controls', this.onMouseEnterBinded); + layerEventManager?.off('mouseleave', 'routing-controls', this.onMouseLeaveBinded); + layerEventManager?.off('click', 'routing-controls', this.onClickBinded); + layerEventManager?.off('contextmenu', 'routing-controls', this.onClickBinded); + layerEventManager?.off('mousedown', 'routing-controls', this.onMouseDownBinded); + layerEventManager?.off('touchstart', 'routing-controls', this.onTouchStartBinded); + + if (map_?.getLayer('routing-controls')) { + map_?.removeLayer('routing-controls'); + } + + if (map_?.getSource('routing-controls')) { + map_?.removeSource('routing-controls'); + } + } catch (e) { + // No reliable way to check if the map is ready to remove sources and layers + } + + this.popup.remove(); this.fileUnsubscribe(); } - createAnchor( - point: TrackPoint, - segment: TrackSegment, - trackIndex: number, - segmentIndex: number - ): AnchorWithMarker { - let element = document.createElement('div'); - element.className = `h-5 w-5 xs:h-4 xs:w-4 md:h-3 md:w-3 rounded-full bg-white border-2 border-black cursor-pointer`; - - let marker = new maplibregl.Marker({ - draggable: true, - className: 'z-10', - element, - }).setLngLat(point.getCoordinates()); - - let anchor = { - point, - segment, - trackIndex, - segmentIndex, - marker, - inZoom: false, - }; - - marker.on('dragstart', (e) => { - this.lastDragEvent = Date.now(); - mapCursor.notify(MapCursorState.TRACKPOINT_DRAGGING, true); - element.classList.remove('cursor-pointer'); - element.classList.add('cursor-grabbing'); - }); - marker.on('dragend', (e) => { - this.lastDragEvent = Date.now(); - mapCursor.notify(MapCursorState.TRACKPOINT_DRAGGING, false); - element.classList.remove('cursor-grabbing'); - element.classList.add('cursor-pointer'); - this.moveAnchor(anchor); - }); - let handleAnchorClick = this.handleClickForAnchor(anchor, marker); - marker.getElement().addEventListener('click', handleAnchorClick); - marker.getElement().addEventListener('contextmenu', handleAnchorClick); - - return anchor; - } - - handleClickForAnchor(anchor: Anchor, marker: maplibregl.Marker) { - return (e: any) => { - e.preventDefault(); - e.stopPropagation(); - - if (Date.now() - this.lastDragEvent < 100) { - // Prevent click event during drag - return; - } - - if (marker === this.temporaryAnchor.marker) { - this.turnIntoPermanentAnchor(); - return; - } - - if (e.shiftKey) { - this.deleteAnchor(anchor); - return; - } - - canChangeStart.update(() => { - if (anchor.point._data.index === 0) { - return false; - } - let segment = anchor.segment; - if ( - distance( - segment.trkpt[0].getCoordinates(), - segment.trkpt[segment.trkpt.length - 1].getCoordinates() - ) > 1000 - ) { - return false; - } - return true; - }); - - marker.setPopup(this.popup); - marker.togglePopup(); - - let deleteThisAnchor = this.getDeleteAnchor(anchor); - this.popupElement.addEventListener('delete', deleteThisAnchor); // Register the delete event for this anchor - let startLoopAtThisAnchor = this.getStartLoopAtAnchor(anchor); - this.popupElement.addEventListener('change-start', startLoopAtThisAnchor); // Register the start loop event for this anchor - this.popup.once('close', () => { - this.popupElement.removeEventListener('delete', deleteThisAnchor); - this.popupElement.removeEventListener('change-start', startLoopAtThisAnchor); - }); - }; - } - - toggleAnchorsForZoomLevelAndBounds() { - const map_ = get(map); - if (!map_) { - return; - } - - // Show markers only if they are in the current zoom level and bounds - this.shownAnchors.splice(0, this.shownAnchors.length); - - let center = map_.getCenter(); - let bottomLeft = map_.unproject([0, map_.getCanvas().height]); - let topRight = map_.unproject([map_.getCanvas().width, 0]); - let diagonal = bottomLeft.distanceTo(topRight); - - let zoom = map_.getZoom(); - this.anchors.forEach((anchor) => { - anchor.inZoom = anchor.point._data.zoom <= zoom; - if (anchor.inZoom && center.distanceTo(anchor.marker.getLngLat()) < diagonal) { - anchor.marker.addTo(map_); - this.shownAnchors.push(anchor); - } else { - anchor.marker.remove(); - } - }); - } - - showTemporaryAnchor(e: any) { - const map_ = get(map); - if (!map_) { - return; - } - - if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) { - // Do not not change the source point if it is already being dragged - return; - } - - if (get(streetViewEnabled)) { - return; - } - - if ( - !get(selection).hasAnyParent( - new ListTrackSegmentItem( - this.fileId, - e.features[0].properties.trackIndex, - e.features[0].properties.segmentIndex - ) - ) - ) { - return; - } - - if (this.temporaryAnchorCloseToOtherAnchor(e)) { - return; - } - - this.temporaryAnchor.point.setCoordinates({ - lat: e.lngLat.lat, - lon: e.lngLat.lng, - }); - this.temporaryAnchor.marker.setLngLat(e.lngLat).addTo(map_); - - map_.on('mousemove', this.updateTemporaryAnchorBinded); - } - - updateTemporaryAnchor(e: any) { - const map_ = get(map); - if (!map_) { - return; - } - - if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) { - // Do not hide if it is being dragged, and stop listening for mousemove - map_.off('mousemove', this.updateTemporaryAnchorBinded); - return; - } - - if ( - e.point.dist(map_.project(this.temporaryAnchor.point.getCoordinates())) > 20 || - this.temporaryAnchorCloseToOtherAnchor(e) - ) { - // Hide if too far from the layer - this.temporaryAnchor.marker.remove(); - map_.off('mousemove', this.updateTemporaryAnchorBinded); - return; - } - - this.temporaryAnchor.marker.setLngLat(e.lngLat); // Update the position of the temporary anchor - } - - temporaryAnchorCloseToOtherAnchor(e: any) { - const map_ = get(map); - if (!map_) { - return false; - } - - for (let anchor of this.shownAnchors) { - if (e.point.dist(map_.project(anchor.marker.getLngLat())) < 10) { - return true; - } - } - return false; - } - - async moveAnchor(anchorWithMarker: AnchorWithMarker) { + async moveAnchor(anchor: Anchor, coordinates: Coordinates) { // Move the anchor and update the route from and to the neighbouring anchors - let coordinates = { - lat: anchorWithMarker.marker.getLngLat().lat, - lon: anchorWithMarker.marker.getLngLat().lng, - }; - - let anchor = anchorWithMarker as Anchor; - if (anchorWithMarker === this.temporaryAnchor) { + if (anchor === this.temporaryAnchor) { // Temporary anchor, need to find the closest point of the segment and create an anchor for it - this.temporaryAnchor.marker.remove(); - anchor = this.getPermanentAnchor(); + anchor = this.getPermanentAnchor(this.temporaryAnchor); + this.removeTemporaryAnchor(); } + const file = get(this.file)?.file; + if (!file) { + return; + } + + const segment = file.getSegment( + anchor.properties.trackIndex, + anchor.properties.segmentIndex + ); + const initialAnchorCoordinates = + segment.trkpt[anchor.properties.pointIndex].getCoordinates(); let [previousAnchor, nextAnchor] = this.getNeighbouringAnchors(anchor); let anchors = []; - let targetCoordinates = []; + let targetTrackpoints = []; if (previousAnchor !== null) { anchors.push(previousAnchor); - targetCoordinates.push(previousAnchor.point.getCoordinates()); + targetTrackpoints.push(segment.trkpt[previousAnchor.properties.pointIndex]); } anchors.push(anchor); - targetCoordinates.push(coordinates); + targetTrackpoints.push( + new TrackPoint({ + attributes: coordinates, + }) + ); if (nextAnchor !== null) { anchors.push(nextAnchor); - targetCoordinates.push(nextAnchor.point.getCoordinates()); + targetTrackpoints.push(segment.trkpt[nextAnchor.properties.pointIndex]); } - let success = await this.routeBetweenAnchors(anchors, targetCoordinates); + let success = await this.routeBetweenAnchors(anchors, targetTrackpoints); - if (!success) { + if (!success && anchor.properties.anchorIndex != this.anchors.length) { // Route failed, revert the anchor to the previous position - anchorWithMarker.marker.setLngLat(anchorWithMarker.point.getCoordinates()); + this.moveAnchorFeature(anchor.properties.anchorIndex, initialAnchorCoordinates); } } - getPermanentAnchor(): Anchor { - let file = get(this.file)?.file; - - // Find the point closest to the temporary anchor - let minDetails: any = { distance: Number.MAX_VALUE }; - let minAnchor = this.temporaryAnchor as Anchor; - file?.forEachSegment((segment, trackIndex, segmentIndex) => { - if ( - get(selection).hasAnyParent( - new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) - ) - ) { - let details: any = {}; - let closest = getClosestLinePoint( - segment.trkpt, - this.temporaryAnchor.point, - details - ); - if (details.distance < minDetails.distance) { - minDetails = details; - minAnchor = { - point: closest, - segment, - trackIndex, - segmentIndex, - }; - } - } - }); - - if (minAnchor.point._data.anchor) { - minAnchor.point = minAnchor.point.clone(); - if (minDetails.before) { - minAnchor.point._data.index = minAnchor.point._data.index + 0.5; - } else { - minAnchor.point._data.index = minAnchor.point._data.index - 0.5; - } + getPermanentAnchor(anchor: Anchor): Anchor { + const file = get(this.file)?.file; + if (!file) { + return anchor; } + const segment = file.getSegment( + anchor.properties.trackIndex, + anchor.properties.segmentIndex + ); + // Find the point closest to the temporary anchor + const anchorPoint = new TrackPoint({ + attributes: { + lon: anchor.geometry.coordinates[0], + lat: anchor.geometry.coordinates[1], + }, + }); + let details: any = {}; + let closest = getClosestLinePoint(segment.trkpt, anchorPoint, details); - return minAnchor; + let permanentAnchor: Anchor = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [closest.getLongitude(), closest.getLatitude()], + }, + properties: { + trackIndex: anchor.properties.trackIndex, + segmentIndex: anchor.properties.segmentIndex, + pointIndex: closest._data.index, + anchorIndex: this.anchors.length, + minZoom: 0, + }, + }; + + return permanentAnchor; } turnIntoPermanentAnchor() { - let file = get(this.file)?.file; - + const file = get(this.file)?.file; + if (!file || !this.temporaryAnchor) { + return; + } + const segment = file.getSegment( + this.temporaryAnchor.properties.trackIndex, + this.temporaryAnchor.properties.segmentIndex + ); // Find the point closest to the temporary anchor - let minDetails: any = { distance: Number.MAX_VALUE }; - let minInfo = { - point: this.temporaryAnchor.point, - trackIndex: -1, - segmentIndex: -1, - trkptIndex: -1, + const anchorPoint = new TrackPoint({ + attributes: { + lon: this.temporaryAnchor.geometry.coordinates[0], + lat: this.temporaryAnchor.geometry.coordinates[1], + }, + }); + let details: any = {}; + getClosestLinePoint(segment.trkpt, anchorPoint, details); + + let before = details.before ? details.index : details.index - 1; + + let projectedPt = projectedPoint( + segment.trkpt[before], + segment.trkpt[before + 1], + anchorPoint + ); + let ratio = + distance(segment.trkpt[before], projectedPt) / + distance(segment.trkpt[before], segment.trkpt[before + 1]); + + let point = segment.trkpt[before].clone(); + point.setCoordinates(projectedPt); + point.ele = + (1 - ratio) * (segment.trkpt[before].ele ?? 0) + + ratio * (segment.trkpt[before + 1].ele ?? 0); + point.time = + segment.trkpt[before].time && segment.trkpt[before + 1].time + ? new Date( + (1 - ratio) * segment.trkpt[before].time.getTime() + + ratio * segment.trkpt[before + 1].time!.getTime() + ) + : undefined; + point._data = { + anchor: true, + zoom: 0, }; - file?.forEachSegment((segment, trackIndex, segmentIndex) => { - if ( - get(selection).hasAnyParent( - new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) - ) - ) { - let details: any = {}; - getClosestLinePoint(segment.trkpt, this.temporaryAnchor.point, details); - if (details.distance < minDetails.distance) { - minDetails = details; - let before = details.before ? details.index : details.index - 1; + const trackIndex = this.temporaryAnchor!.properties.trackIndex; + const segmentIndex = this.temporaryAnchor!.properties.segmentIndex; + fileActionManager.applyToFile(this.fileId, (file) => + file.replaceTrackPoints(trackIndex, segmentIndex, before + 1, before, [point]) + ); - let projectedPt = projectedPoint( - segment.trkpt[before], - segment.trkpt[before + 1], - this.temporaryAnchor.point - ); - let ratio = - distance(segment.trkpt[before], projectedPt) / - distance(segment.trkpt[before], segment.trkpt[before + 1]); - - let point = segment.trkpt[before].clone(); - point.setCoordinates(projectedPt); - point.ele = - (1 - ratio) * (segment.trkpt[before].ele ?? 0) + - ratio * (segment.trkpt[before + 1].ele ?? 0); - point.time = - segment.trkpt[before].time && segment.trkpt[before + 1].time - ? new Date( - (1 - ratio) * segment.trkpt[before].time.getTime() + - ratio * segment.trkpt[before + 1].time!.getTime() - ) - : undefined; - point._data = { - anchor: true, - zoom: 0, - }; - - minInfo = { - point, - trackIndex, - segmentIndex, - trkptIndex: before + 1, - }; - } - } - }); - - if (minInfo.trackIndex !== -1) { - fileActionManager.applyToFile(this.fileId, (file) => - file.replaceTrackPoints( - minInfo.trackIndex, - minInfo.segmentIndex, - minInfo.trkptIndex, - minInfo.trkptIndex - 1, - [minInfo.point] - ) - ); - } + this.temporaryAnchor = null; } getDeleteAnchor(anchor: Anchor) { @@ -537,36 +396,56 @@ export class RoutingControls { if (previousAnchor === null && nextAnchor === null) { // Only one point, remove it fileActionManager.applyToFile(this.fileId, (file) => - file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, []) + file.replaceTrackPoints( + anchor.properties.trackIndex, + anchor.properties.segmentIndex, + 0, + 0, + [] + ) ); } else if (previousAnchor === null && nextAnchor !== null) { // First point, remove trackpoints until nextAnchor fileActionManager.applyToFile(this.fileId, (file) => file.replaceTrackPoints( - anchor.trackIndex, - anchor.segmentIndex, + anchor.properties.trackIndex, + anchor.properties.segmentIndex, 0, - nextAnchor.point._data.index - 1, + nextAnchor.properties.pointIndex - 1, [] ) ); } else if (nextAnchor === null && previousAnchor !== null) { // Last point, remove trackpoints from previousAnchor fileActionManager.applyToFile(this.fileId, (file) => { - let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex); + const segment = file.getSegment( + anchor.properties.trackIndex, + anchor.properties.segmentIndex + ); file.replaceTrackPoints( - anchor.trackIndex, - anchor.segmentIndex, - previousAnchor.point._data.index + 1, + anchor.properties.trackIndex, + anchor.properties.segmentIndex, + previousAnchor.properties.pointIndex + 1, segment.trkpt.length - 1, [] ); }); } else if (previousAnchor !== null && nextAnchor !== null) { // Route between previousAnchor and nextAnchor + const file = get(this.file)?.file; + if (!file) { + return; + } + const segment = file.getSegment( + anchor.properties.trackIndex, + anchor.properties.segmentIndex + ); this.routeBetweenAnchors( [previousAnchor, nextAnchor], - [previousAnchor.point.getCoordinates(), nextAnchor.point.getCoordinates()] + [ + segment.trkpt[previousAnchor.properties.pointIndex], + segment.trkpt[nextAnchor.properties.pointIndex], + ] ); } } @@ -578,30 +457,37 @@ export class RoutingControls { startLoopAtAnchor(anchor: Anchor) { this.popup.remove(); - let fileWithStats = get(this.file); + const fileWithStats = get(this.file); if (!fileWithStats) { return; } - let speed = fileWithStats.statistics.getStatisticsFor( - new ListTrackSegmentItem(this.fileId, anchor.trackIndex, anchor.segmentIndex) + const speed = fileWithStats.statistics.getStatisticsFor( + new ListTrackSegmentItem( + this.fileId, + anchor.properties.trackIndex, + anchor.properties.segmentIndex + ) ).global.speed.moving; - let segment = anchor.segment; + const segment = fileWithStats.file.getSegment( + anchor.properties.trackIndex, + anchor.properties.segmentIndex + ); fileActionManager.applyToFile(this.fileId, (file) => { file.replaceTrackPoints( - anchor.trackIndex, - anchor.segmentIndex, + anchor.properties.trackIndex, + anchor.properties.segmentIndex, segment.trkpt.length, segment.trkpt.length - 1, - segment.trkpt.slice(0, anchor.point._data.index), + segment.trkpt.slice(0, anchor.properties.pointIndex), speed > 0 ? speed : undefined ); file.crop( - anchor.point._data.index, - anchor.point._data.index + segment.trkpt.length - 1, - [anchor.trackIndex], - [anchor.segmentIndex] + anchor.properties.pointIndex, + anchor.properties.pointIndex + segment.trkpt.length - 1, + [anchor.properties.trackIndex], + [anchor.properties.segmentIndex] ); }); } @@ -611,7 +497,10 @@ export class RoutingControls { if (get(streetViewEnabled) && get(streetViewSource) === 'google') { return; } - + if (e.target.queryRenderedFeatures(e.point, { layers: ['routing-controls'] }).length) { + // Clicked on routing control, ignoring + return; + } this.appendAnchorWithCoordinates({ lat: e.lngLat.lat, lon: e.lngLat.lng, @@ -620,21 +509,120 @@ export class RoutingControls { async appendAnchorWithCoordinates(coordinates: Coordinates) { // Add a new anchor to the end of the last segment - let selected = selection.getOrderedSelection(); - if (selected.length === 0 || selected[selected.length - 1].getFileId() !== this.fileId) { + let newAnchorPoint = new TrackPoint({ + attributes: coordinates, + }); + + if (this.anchors.length == 0) { + this.routeBetweenAnchors( + [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [ + newAnchorPoint.getLongitude(), + newAnchorPoint.getLatitude(), + ], + }, + properties: { + trackIndex: 0, + segmentIndex: 0, + pointIndex: 0, + anchorIndex: 0, + minZoom: 0, + }, + }, + ], + [newAnchorPoint] + ); return; } - let item = selected[selected.length - 1]; let lastAnchor = this.anchors[this.anchors.length - 1]; - let newPoint = new TrackPoint({ - attributes: coordinates, - }); - newPoint._data.anchor = true; - newPoint._data.zoom = 0; + const file = get(this.file)?.file; + if (!file) { + return; + } - if (!lastAnchor) { + const segment = file.getSegment( + lastAnchor.properties.trackIndex, + lastAnchor.properties.segmentIndex + ); + const lastAnchorPoint = segment.trkpt[lastAnchor.properties.pointIndex]; + + let newAnchor: Anchor = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [newAnchorPoint.getLongitude(), newAnchorPoint.getLatitude()], + }, + properties: { + trackIndex: lastAnchor.properties.trackIndex, + segmentIndex: lastAnchor.properties.segmentIndex, + pointIndex: segment.trkpt.length - 1, // Do as if the point was the last point in the segment + anchorIndex: 0, + minZoom: 0, + }, + }; + + await this.routeBetweenAnchors([lastAnchor, newAnchor], [lastAnchorPoint, newAnchorPoint]); + } + + getNeighbouringAnchors(anchor: Anchor): [Anchor | null, Anchor | null] { + let previousAnchor: Anchor | null = null; + let nextAnchor: Anchor | null = null; + + const zoom = get(map)?.getZoom() ?? 20; + + for (let i = 0; i < this.anchors.length; i++) { + if ( + this.anchors[i].properties.segmentIndex === anchor.properties.segmentIndex && + zoom >= this.anchors[i].properties.minZoom + ) { + if (this.anchors[i].properties.pointIndex < anchor.properties.pointIndex) { + if ( + !previousAnchor || + this.anchors[i].properties.pointIndex > previousAnchor.properties.pointIndex + ) { + previousAnchor = this.anchors[i]; + } + } else if (this.anchors[i].properties.pointIndex > anchor.properties.pointIndex) { + if ( + !nextAnchor || + this.anchors[i].properties.pointIndex < nextAnchor.properties.pointIndex + ) { + nextAnchor = this.anchors[i]; + } + } + } + } + + return [previousAnchor, nextAnchor]; + } + + async routeBetweenAnchors( + anchors: Anchor[], + targetTrackPoints: TrackPoint[] + ): Promise { + const fileWithStats = get(this.file); + if (!fileWithStats) { + return false; + } + + if (anchors.length <= 1) { + // Only one anchor, update the point in the segment + targetTrackPoints[0]._data.anchor = true; + targetTrackPoints[0]._data.zoom = 0; + let selected = selection.getOrderedSelection(); + if ( + selected.length === 0 || + selected[selected.length - 1].getFileId() !== this.fileId + ) { + return false; + } + let item = selected[selected.length - 1]; fileActionManager.applyToFile(this.fileId, (file) => { let trackIndex = file.trk.length > 0 ? file.trk.length - 1 : 0; if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) { @@ -649,86 +637,22 @@ export class RoutingControls { } if (file.trk.length === 0) { let track = new Track(); - track.replaceTrackPoints(0, 0, 0, [newPoint]); + track.replaceTrackPoints(0, 0, 0, targetTrackPoints); file.replaceTracks(0, 0, [track]); } else if (file.trk[trackIndex].trkseg.length === 0) { let segment = new TrackSegment(); - segment.replaceTrackPoints(0, 0, [newPoint]); + segment.replaceTrackPoints(0, 0, targetTrackPoints); file.replaceTrackSegments(trackIndex, 0, 0, [segment]); } else { - file.replaceTrackPoints(trackIndex, segmentIndex, 0, 0, [newPoint]); + file.replaceTrackPoints(trackIndex, segmentIndex, 0, 0, targetTrackPoints); } }); - return; - } - - newPoint._data.index = lastAnchor.segment.trkpt.length - 1; // Do as if the point was the last point in the segment - let newAnchor = { - point: newPoint, - segment: lastAnchor.segment, - trackIndex: lastAnchor.trackIndex, - segmentIndex: lastAnchor.segmentIndex, - }; - - await this.routeBetweenAnchors( - [lastAnchor, newAnchor], - [lastAnchor.point.getCoordinates(), newAnchor.point.getCoordinates()] - ); - } - - getNeighbouringAnchors(anchor: Anchor): [Anchor | null, Anchor | null] { - let previousAnchor: Anchor | null = null; - let nextAnchor: Anchor | null = null; - - for (let i = 0; i < this.anchors.length; i++) { - if (this.anchors[i].segment === anchor.segment && this.anchors[i].inZoom) { - if (this.anchors[i].point._data.index < anchor.point._data.index) { - if ( - !previousAnchor || - this.anchors[i].point._data.index > previousAnchor.point._data.index - ) { - previousAnchor = this.anchors[i]; - } - } else if (this.anchors[i].point._data.index > anchor.point._data.index) { - if ( - !nextAnchor || - this.anchors[i].point._data.index < nextAnchor.point._data.index - ) { - nextAnchor = this.anchors[i]; - } - } - } - } - - return [previousAnchor, nextAnchor]; - } - - async routeBetweenAnchors( - anchors: Anchor[], - targetCoordinates: Coordinates[] - ): Promise { - let segment = anchors[0].segment; - - let fileWithStats = get(this.file); - if (!fileWithStats) { - return false; - } - - if (anchors.length === 1) { - // Only one anchor, update the point in the segment - fileActionManager.applyToFile(this.fileId, (file) => - file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, 0, 0, [ - new TrackPoint({ - attributes: targetCoordinates[0], - }), - ]) - ); return true; } let response: TrackPoint[]; try { - response = await route(targetCoordinates); + response = await route(targetTrackPoints.map((trkpt) => trkpt.getCoordinates())); } catch (e: any) { if (e.message.includes('from-position not mapped in existing datafile')) { toast.error(i18n._('toolbar.routing.error.from')); @@ -744,47 +668,46 @@ export class RoutingControls { return false; } - if (anchors[0].point._data.index === 0) { - // First anchor is the first point of the segment - anchors[0].point = response[0]; // replace the first anchor - anchors[0].point._data.index = 0; - } else if ( - anchors[0].point._data.index === segment.trkpt.length - 1 && - distance(anchors[0].point.getCoordinates(), response[0].getCoordinates()) < 1 + const segment = fileWithStats.file.getSegment( + anchors[0].properties.trackIndex, + anchors[0].properties.segmentIndex + ); + + if ( + anchors[0].properties.pointIndex !== 0 && + (anchors[0].properties.pointIndex !== segment.trkpt.length - 1 || + distance(targetTrackPoints[0].getCoordinates(), response[0].getCoordinates()) > 1) ) { - // First anchor is the last point of the segment, and the new point is close enough - anchors[0].point = response[0]; // replace the first anchor - anchors[0].point._data.index = segment.trkpt.length - 1; - } else { - anchors[0].point = anchors[0].point.clone(); // Clone the anchor to assign new properties - response.splice(0, 0, anchors[0].point); // Insert it in the response to keep it + response.splice(0, 0, targetTrackPoints[0].clone()); // Keep the current first anchor } - if (anchors[anchors.length - 1].point._data.index === segment.trkpt.length - 1) { - // Last anchor is the last point of the segment - anchors[anchors.length - 1].point = response[response.length - 1]; // replace the last anchor - anchors[anchors.length - 1].point._data.index = segment.trkpt.length - 1; - } else { - anchors[anchors.length - 1].point = anchors[anchors.length - 1].point.clone(); // Clone the anchor to assign new properties - response.push(anchors[anchors.length - 1].point); // Insert it in the response to keep it + if (anchors[anchors.length - 1].properties.pointIndex !== segment.trkpt.length - 1) { + response.push(targetTrackPoints[anchors.length - 1].clone()); // Keep the current last anchor } + let anchorTrackPoints = [response[0], response[response.length - 1]]; for (let i = 1; i < anchors.length - 1; i++) { - // Find the closest point to the intermediate anchor - // and transfer the marker to that point - anchors[i].point = getClosestLinePoint(response.slice(1, -1), targetCoordinates[i]); + // Find the closest point to the intermediate anchor, which will become an anchor + anchorTrackPoints.push( + getClosestLinePoint(response.slice(1, -1), targetTrackPoints[i]) + ); } - anchors.forEach((anchor) => { - anchor.point._data.anchor = true; - anchor.point._data.zoom = 0; // Make these anchors permanent + anchorTrackPoints.forEach((trkpt) => { + // Turn them into permanent anchors + trkpt._data.anchor = true; + trkpt._data.zoom = 0; }); - let stats = fileWithStats.statistics.getStatisticsFor( - new ListTrackSegmentItem(this.fileId, anchors[0].trackIndex, anchors[0].segmentIndex) + const stats = fileWithStats.statistics.getStatisticsFor( + new ListTrackSegmentItem( + this.fileId, + anchors[0].properties.trackIndex, + anchors[0].properties.segmentIndex + ) ); let speed: number | undefined = undefined; - let startTime = anchors[0].point.time; + let startTime = segment.trkpt[anchors[0].properties.pointIndex].time; if (stats.global.speed.moving > 0) { let replacingDistance = 0; @@ -792,9 +715,9 @@ export class RoutingControls { replacingDistance += distance(response[i - 1].getCoordinates(), response[i].getCoordinates()) / 1000; } - let startAnchorStats = stats.getTrackPoint(anchors[0].point._data.index)!; + let startAnchorStats = stats.getTrackPoint(anchors[0].properties.pointIndex)!; let endAnchorStats = stats.getTrackPoint( - anchors[anchors.length - 1].point._data.index + anchors[anchors.length - 1].properties.pointIndex )!; let replacedDistance = @@ -817,7 +740,7 @@ export class RoutingControls { if (startTime === undefined) { // Replacing the first point - let endIndex = anchors[anchors.length - 1].point._data.index; + let endIndex = anchors[anchors.length - 1].properties.pointIndex; startTime = new Date( (segment.trkpt[endIndex].time?.getTime() ?? 0) - (replacingTime + endAnchorStats.time.total - endAnchorStats.time.moving) * @@ -828,10 +751,10 @@ export class RoutingControls { fileActionManager.applyToFile(this.fileId, (file) => file.replaceTrackPoints( - anchors[0].trackIndex, - anchors[0].segmentIndex, - anchors[0].point._data.index, - anchors[anchors.length - 1].point._data.index, + anchors[0].properties.trackIndex, + anchors[0].properties.segmentIndex, + anchors[0].properties.pointIndex, + anchors[anchors.length - 1].properties.pointIndex, response, speed, startTime @@ -845,18 +768,315 @@ export class RoutingControls { this.remove(); this.unsubscribes.forEach((unsubscribe) => unsubscribe()); } + + loadIcons() { + const _map = get(map); + if (!_map) { + return; + } + + loadSVGIcon( + _map, + 'routing-control', + ` + + `, + 60 + ); + } + + onMouseEnter() { + mapCursor.notify(MapCursorState.ANCHOR_HOVER, true); + } + + onMouseLeave() { + if (this.temporaryAnchor !== null) { + return; + } + mapCursor.notify(MapCursorState.ANCHOR_HOVER, false); + } + + onClick(e: MapLayerMouseEvent) { + e.preventDefault(); + + if (this.temporaryAnchor !== null) { + this.turnIntoPermanentAnchor(); + return; + } + + const anchor = this.anchors[e.features![0].properties.anchorIndex]; + if (e.originalEvent.shiftKey) { + this.deleteAnchor(anchor); + return; + } + + canChangeStart.update(() => { + if (anchor.properties.pointIndex === 0) { + return false; + } + const segment = get(this.file)?.file.getSegment( + anchor.properties.trackIndex, + anchor.properties.segmentIndex + ); + if ( + !segment || + distance( + segment.trkpt[0].getCoordinates(), + segment.trkpt[segment.trkpt.length - 1].getCoordinates() + ) > 1000 + ) { + return false; + } + return true; + }); + + this.popup.setLngLat(e.lngLat); + this.popup.addTo(e.target); + + let deleteThisAnchor = this.getDeleteAnchor(anchor); + this.popupElement.addEventListener('delete', deleteThisAnchor); // Register the delete event for this anchor + let startLoopAtThisAnchor = this.getStartLoopAtAnchor(anchor); + this.popupElement.addEventListener('change-start', startLoopAtThisAnchor); // Register the start loop event for this anchor + this.popup.once('close', () => { + this.popupElement.removeEventListener('delete', deleteThisAnchor); + this.popupElement.removeEventListener('change-start', startLoopAtThisAnchor); + }); + } + + onMouseDown(e: MapLayerMouseEvent) { + const _map = get(map); + if (!_map) { + return; + } + + e.preventDefault(); + _map.dragPan.disable(); + + this.draggedAnchorIndex = e.features![0].properties.anchorIndex; + this.draggingStartingPosition = e.point; + + _map.on('mousemove', this.onMouseMoveBinded); + _map.once('mouseup', this.onMouseUpBinded); + } + + onTouchStart(e: MapLayerTouchEvent) { + if (e.points.length !== 1) { + return; + } + const _map = get(map); + if (!_map) { + return; + } + + this.draggedAnchorIndex = e.features![0].properties.anchorIndex; + this.draggingStartingPosition = e.point; + + e.preventDefault(); + _map.dragPan.disable(); + + _map.on('touchmove', this.onMouseMoveBinded); + _map.once('touchend', this.onMouseUpBinded); + } + + onMouseMove(e: MapLayerMouseEvent | MapLayerTouchEvent) { + if (this.draggedAnchorIndex === null || e.point.equals(this.draggingStartingPosition)) { + return; + } + + mapCursor.notify(MapCursorState.ANCHOR_DRAGGING, true); + + this.moveAnchorFeature(this.draggedAnchorIndex, { + lat: e.lngLat.lat, + lon: e.lngLat.lng, + }); + } + + onMouseUp(e: MapLayerMouseEvent | MapLayerTouchEvent) { + mapCursor.notify(MapCursorState.ANCHOR_DRAGGING, false); + + const _map = get(map); + if (!_map) { + return; + } + + _map.dragPan.enable(); + + _map.off('mousemove', this.onMouseMoveBinded); + _map.off('touchmove', this.onMouseMoveBinded); + + if (this.draggedAnchorIndex === null) { + return; + } + if (e.point.equals(this.draggingStartingPosition)) { + this.draggedAnchorIndex = null; + return; + } + + if (this.draggedAnchorIndex === this.anchors.length) { + if (this.temporaryAnchor) { + this.moveAnchor(this.temporaryAnchor, { + lat: e.lngLat.lat, + lon: e.lngLat.lng, + }); + } + } else { + this.moveAnchor(this.anchors[this.draggedAnchorIndex], { + lat: e.lngLat.lat, + lon: e.lngLat.lng, + }); + } + + this.draggedAnchorIndex = null; + } + + showTemporaryAnchor(e: MapLayerMouseEvent) { + const map_ = get(map); + if (!map_) { + return; + } + + if (this.draggedAnchorIndex !== null) { + // Do not not change the source point if it is already being dragged + return; + } + + if (get(streetViewEnabled)) { + return; + } + + if ( + !get(selection).hasAnyParent( + new ListTrackSegmentItem( + this.fileId, + e.features![0].properties.trackIndex, + e.features![0].properties.segmentIndex + ) + ) + ) { + return; + } + + if (this.temporaryAnchorCloseToOtherAnchor(e)) { + return; + } + + this.temporaryAnchor = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [e.lngLat.lng, e.lngLat.lat], + }, + properties: { + trackIndex: e.features![0].properties.trackIndex, + segmentIndex: e.features![0].properties.segmentIndex, + pointIndex: 0, + anchorIndex: this.anchors.length, + minZoom: 0, + }, + }; + + this.addTemporaryAnchor(); + mapCursor.notify(MapCursorState.ANCHOR_HOVER, true); + + map_.on('mousemove', this.updateTemporaryAnchorBinded); + } + + updateTemporaryAnchor(e: MapMouseEvent) { + const map_ = get(map); + if (!map_ || !this.temporaryAnchor) { + return; + } + + if (this.draggedAnchorIndex !== null) { + // Do not hide if it is being dragged, and stop listening for mousemove + map_.off('mousemove', this.updateTemporaryAnchorBinded); + return; + } + + if ( + e.point.dist( + map_.project(this.temporaryAnchor.geometry.coordinates as [number, number]) + ) > 20 || + this.temporaryAnchorCloseToOtherAnchor(e) + ) { + // Hide if too far from the layer + this.removeTemporaryAnchor(); + return; + } + + // Update the position of the temporary anchor + this.moveAnchorFeature(this.anchors.length, { + lat: e.lngLat.lat, + lon: e.lngLat.lng, + }); + } + + temporaryAnchorCloseToOtherAnchor(e: any) { + const map_ = get(map); + if (!map_) { + return false; + } + + const zoom = map_.getZoom(); + for (let anchor of this.anchors) { + if ( + zoom >= anchor.properties.minZoom && + e.point.dist(map_.project(anchor.geometry.coordinates as [number, number])) < 10 + ) { + return true; + } + } + return false; + } + + moveAnchorFeature(anchorIndex: number, coordinates: Coordinates) { + let source = get(map)?.getSource('routing-controls') as GeoJSONSource | undefined; + if (source) { + source.updateData({ + update: [ + { + id: anchorIndex, + newGeometry: { + type: 'Point', + coordinates: [coordinates.lon, coordinates.lat], + }, + }, + ], + }); + } + } + + addTemporaryAnchor() { + if (!this.temporaryAnchor) { + return; + } + let source = get(map)?.getSource('routing-controls') as GeoJSONSource | undefined; + if (source) { + if (this.temporaryAnchor) { + source.updateData({ + add: [this.temporaryAnchor], + }); + } + } + } + + removeTemporaryAnchor() { + if (!this.temporaryAnchor) { + return; + } + const map_ = get(map); + let source = map_?.getSource('routing-controls') as GeoJSONSource | undefined; + if (source) { + if (this.temporaryAnchor) { + source.updateData({ + remove: [this.temporaryAnchor.properties.anchorIndex], + }); + } + } + map_?.off('mousemove', this.updateTemporaryAnchorBinded); + mapCursor.notify(MapCursorState.ANCHOR_HOVER, false); + this.temporaryAnchor = null; + } } export const routingControls: Map = new Map(); - -type Anchor = { - segment: TrackSegment; - trackIndex: number; - segmentIndex: number; - point: TrackPoint; -}; - -type AnchorWithMarker = Anchor & { - marker: maplibregl.Marker; - inZoom: boolean; -}; diff --git a/website/src/lib/logic/map-cursor.ts b/website/src/lib/logic/map-cursor.ts index 1c5f61f2d..d83565756 100644 --- a/website/src/lib/logic/map-cursor.ts +++ b/website/src/lib/logic/map-cursor.ts @@ -7,7 +7,8 @@ export enum MapCursorState { TOOL_WITH_CROSSHAIR, WAYPOINT_HOVER, WAYPOINT_DRAGGING, - TRACKPOINT_DRAGGING, + ANCHOR_HOVER, + ANCHOR_DRAGGING, SCISSORS, SPLIT_CONTROL, MAPILLARY_HOVER, @@ -20,7 +21,8 @@ const cursorStyles = { [MapCursorState.LAYER_HOVER]: 'pointer', [MapCursorState.WAYPOINT_HOVER]: 'pointer', [MapCursorState.WAYPOINT_DRAGGING]: 'grabbing', - [MapCursorState.TRACKPOINT_DRAGGING]: 'grabbing', + [MapCursorState.ANCHOR_HOVER]: 'pointer', + [MapCursorState.ANCHOR_DRAGGING]: 'grabbing', [MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair', [MapCursorState.SCISSORS]: scissorsCursor, [MapCursorState.SPLIT_CONTROL]: 'pointer', diff --git a/website/src/lib/utils.ts b/website/src/lib/utils.ts index 5e335ed65..a030374a1 100644 --- a/website/src/lib/utils.ts +++ b/website/src/lib/utils.ts @@ -197,9 +197,9 @@ export function getElevation( ); } -export function loadSVGIcon(map: maplibregl.Map, id: string, svg: string) { +export function loadSVGIcon(map: maplibregl.Map, id: string, svg: string, size: number = 100) { if (!map.hasImage(id)) { - let icon = new Image(100, 100); + let icon = new Image(size, size); icon.onload = () => { if (!map.hasImage(id)) { map.addImage(id, icon); From d6c9fb102571d6415a1d25b5580ec77239452f11 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sat, 14 Feb 2026 15:05:23 +0100 Subject: [PATCH 13/15] split routing controls in zoom-specific layers to improve performance --- .../components/map/CoordinatesPopup.svelte | 8 +- .../toolbar/tools/routing/routing-controls.ts | 159 +++++++++++------- .../toolbar/tools/routing/simplify.ts | 10 +- 3 files changed, 111 insertions(+), 66 deletions(-) diff --git a/website/src/lib/components/map/CoordinatesPopup.svelte b/website/src/lib/components/map/CoordinatesPopup.svelte index 6aac880b1..53628d94c 100644 --- a/website/src/lib/components/map/CoordinatesPopup.svelte +++ b/website/src/lib/components/map/CoordinatesPopup.svelte @@ -5,7 +5,13 @@ map.onLoad((map_) => { map_.on('contextmenu', (e) => { - if (map_.queryRenderedFeatures(e.point, { layers: ['routing-controls'] }).length) { + if ( + map_.queryRenderedFeatures(e.point, { + layers: map_ + .getLayersOrder() + .filter((layerId) => layerId.startsWith('routing-controls')), + }).length + ) { // Clicked on routing control, ignoring return; } diff --git a/website/src/lib/components/toolbar/tools/routing/routing-controls.ts b/website/src/lib/components/toolbar/tools/routing/routing-controls.ts index d26fcea5c..acbbcc6c0 100644 --- a/website/src/lib/components/toolbar/tools/routing/routing-controls.ts +++ b/website/src/lib/components/toolbar/tools/routing/routing-controls.ts @@ -24,6 +24,7 @@ import { fileActionManager } from '$lib/logic/file-action-manager'; import { i18n } from '$lib/i18n.svelte'; import { map } from '$lib/components/map/map'; import { ANCHOR_LAYER_KEY } from '$lib/components/map/style'; +import { MAX_ANCHOR_ZOOM, MIN_ANCHOR_ZOOM } from './simplify'; const { streetViewSource } = settings; export const canChangeStart = writable(false); @@ -41,6 +42,13 @@ export class RoutingControls { active: boolean = false; fileId: string = ''; file: Readable; + layers: Map< + number, + { + id: string; + anchors: GeoJSON.Feature[]; + } + > = new Map(); anchors: GeoJSON.Feature[] = []; popup: maplibregl.Popup; popupElement: HTMLElement; @@ -74,6 +82,12 @@ export class RoutingControls { ) { this.fileId = fileId; this.file = file; + for (let zoom = MIN_ANCHOR_ZOOM; zoom <= MAX_ANCHOR_ZOOM; zoom++) { + this.layers.set(zoom, { + id: `routing-controls-${zoom}`, + anchors: [], + }); + } this.popup = popup; this.popupElement = popupElement; @@ -129,6 +143,7 @@ export class RoutingControls { return; } + this.layers.forEach((layer) => (layer.anchors = [])); this.anchors = []; file.forEachSegment((segment, trackIndex, segmentIndex) => { @@ -140,7 +155,7 @@ export class RoutingControls { for (let i = 0; i < segment.trkpt.length; i++) { const point = segment.trkpt[i]; if (point._data.anchor) { - this.anchors.push({ + const anchor: Anchor = { type: 'Feature', geometry: { type: 'Point', @@ -153,58 +168,62 @@ export class RoutingControls { anchorIndex: this.anchors.length, minZoom: point._data.zoom, }, - }); + }; + this.layers.get(point._data.zoom)?.anchors.push(anchor); + this.anchors.push(anchor); } } } }); - try { - let source = map_.getSource('routing-controls') as maplibregl.GeoJSONSource | undefined; - if (source) { - source.setData({ - type: 'FeatureCollection', - features: this.anchors, - }); - } else { - map_.addSource('routing-controls', { - type: 'geojson', - data: { + this.layers.forEach((layer, zoom) => { + try { + let source = map_.getSource(layer.id) as maplibregl.GeoJSONSource | undefined; + if (source) { + source.setData({ type: 'FeatureCollection', - features: this.anchors, - }, - promoteId: 'anchorIndex', - }); - } - - if (!map_.getLayer('routing-controls')) { - map_.addLayer( - { - id: 'routing-controls', - type: 'symbol', - source: 'routing-controls', - layout: { - 'icon-image': 'routing-control', - 'icon-size': 0.25, - 'icon-padding': 0, - 'icon-allow-overlap': true, + features: layer.anchors, + }); + } else { + map_.addSource(layer.id, { + type: 'geojson', + data: { + type: 'FeatureCollection', + features: layer.anchors, }, - filter: ['<=', ['get', 'minZoom'], ['zoom']], - }, - ANCHOR_LAYER_KEY.routingControls - ); + promoteId: 'anchorIndex', + }); + } - layerEventManager.on('mouseenter', 'routing-controls', this.onMouseEnterBinded); - layerEventManager.on('mouseleave', 'routing-controls', this.onMouseLeaveBinded); - layerEventManager.on('click', 'routing-controls', this.onClickBinded); - layerEventManager.on('contextmenu', 'routing-controls', this.onClickBinded); - layerEventManager.on('mousedown', 'routing-controls', this.onMouseDownBinded); - layerEventManager.on('touchstart', 'routing-controls', this.onTouchStartBinded); + if (!map_.getLayer(layer.id)) { + map_.addLayer( + { + id: layer.id, + type: 'symbol', + source: layer.id, + layout: { + 'icon-image': 'routing-control', + 'icon-size': 0.25, + 'icon-padding': 0, + 'icon-allow-overlap': true, + }, + minzoom: zoom, + }, + ANCHOR_LAYER_KEY.routingControls + ); + + layerEventManager.on('mouseenter', layer.id, this.onMouseEnterBinded); + layerEventManager.on('mouseleave', layer.id, this.onMouseLeaveBinded); + layerEventManager.on('click', layer.id, this.onClickBinded); + layerEventManager.on('contextmenu', layer.id, this.onClickBinded); + layerEventManager.on('mousedown', layer.id, this.onMouseDownBinded); + layerEventManager.on('touchstart', layer.id, this.onTouchStartBinded); + } + } catch (e) { + // No reliable way to check if the map is ready to add sources and layers + return; } - } catch (e) { - // No reliable way to check if the map is ready to add sources and layers - return; - } + }); } remove() { @@ -217,24 +236,26 @@ export class RoutingControls { layerEventManager?.off('mousemove', this.fileId, this.showTemporaryAnchorBinded); map_?.off('mousemove', this.updateTemporaryAnchorBinded); - try { - layerEventManager?.off('mouseenter', 'routing-controls', this.onMouseEnterBinded); - layerEventManager?.off('mouseleave', 'routing-controls', this.onMouseLeaveBinded); - layerEventManager?.off('click', 'routing-controls', this.onClickBinded); - layerEventManager?.off('contextmenu', 'routing-controls', this.onClickBinded); - layerEventManager?.off('mousedown', 'routing-controls', this.onMouseDownBinded); - layerEventManager?.off('touchstart', 'routing-controls', this.onTouchStartBinded); + this.layers.forEach((layer) => { + try { + layerEventManager?.off('mouseenter', layer.id, this.onMouseEnterBinded); + layerEventManager?.off('mouseleave', layer.id, this.onMouseLeaveBinded); + layerEventManager?.off('click', layer.id, this.onClickBinded); + layerEventManager?.off('contextmenu', layer.id, this.onClickBinded); + layerEventManager?.off('mousedown', layer.id, this.onMouseDownBinded); + layerEventManager?.off('touchstart', layer.id, this.onTouchStartBinded); - if (map_?.getLayer('routing-controls')) { - map_?.removeLayer('routing-controls'); - } + if (map_?.getLayer(layer.id)) { + map_?.removeLayer(layer.id); + } - if (map_?.getSource('routing-controls')) { - map_?.removeSource('routing-controls'); + if (map_?.getSource(layer.id)) { + map_?.removeSource(layer.id); + } + } catch (e) { + // No reliable way to check if the map is ready to remove sources and layers } - } catch (e) { - // No reliable way to check if the map is ready to remove sources and layers - } + }); this.popup.remove(); @@ -497,7 +518,14 @@ export class RoutingControls { if (get(streetViewEnabled) && get(streetViewSource) === 'google') { return; } - if (e.target.queryRenderedFeatures(e.point, { layers: ['routing-controls'] }).length) { + if ( + e.target.queryRenderedFeatures(e.point, { + layers: this.layers + .values() + .map((layer) => layer.id) + .toArray(), + }).length + ) { // Clicked on routing control, ignoring return; } @@ -578,6 +606,7 @@ export class RoutingControls { for (let i = 0; i < this.anchors.length; i++) { if ( + this.anchors[i].properties.trackIndex === anchor.properties.trackIndex && this.anchors[i].properties.segmentIndex === anchor.properties.segmentIndex && zoom >= this.anchors[i].properties.minZoom ) { @@ -1030,7 +1059,11 @@ export class RoutingControls { } moveAnchorFeature(anchorIndex: number, coordinates: Coordinates) { - let source = get(map)?.getSource('routing-controls') as GeoJSONSource | undefined; + const anchor = + anchorIndex === this.anchors.length ? this.temporaryAnchor : this.anchors[anchorIndex]; + let source = get(map)?.getSource( + this.layers.get(anchor?.properties.minZoom ?? MIN_ANCHOR_ZOOM)?.id ?? '' + ) as GeoJSONSource | undefined; if (source) { source.updateData({ update: [ @@ -1050,7 +1083,7 @@ export class RoutingControls { if (!this.temporaryAnchor) { return; } - let source = get(map)?.getSource('routing-controls') as GeoJSONSource | undefined; + let source = get(map)?.getSource('routing-controls-0') as GeoJSONSource | undefined; if (source) { if (this.temporaryAnchor) { source.updateData({ @@ -1065,7 +1098,7 @@ export class RoutingControls { return; } const map_ = get(map); - let source = map_?.getSource('routing-controls') as GeoJSONSource | undefined; + let source = map_?.getSource('routing-controls-0') as GeoJSONSource | undefined; if (source) { if (this.temporaryAnchor) { source.updateData({ diff --git a/website/src/lib/components/toolbar/tools/routing/simplify.ts b/website/src/lib/components/toolbar/tools/routing/simplify.ts index f36d42274..189ea3faa 100644 --- a/website/src/lib/components/toolbar/tools/routing/simplify.ts +++ b/website/src/lib/components/toolbar/tools/routing/simplify.ts @@ -2,15 +2,21 @@ import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from 'gpx'; const earthRadius = 6371008.8; +export const MIN_ANCHOR_ZOOM = 0; +export const MAX_ANCHOR_ZOOM = 22; + export function getZoomLevelForDistance(latitude: number, distance?: number): number { if (distance === undefined) { - return 0; + return MIN_ANCHOR_ZOOM; } const rad = Math.PI / 180; const lat = latitude * rad; - return Math.min(22, Math.max(0, Math.log2((earthRadius * Math.cos(lat)) / distance))); + return Math.min( + MAX_ANCHOR_ZOOM, + Math.max(MIN_ANCHOR_ZOOM, Math.round(Math.log2((earthRadius * Math.cos(lat)) / distance))) + ); } export function updateAnchorPoints(file: GPXFile) { From 091f6a3ed072a7d68015637d2fce1417d934c9f7 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Tue, 17 Feb 2026 21:12:04 +0100 Subject: [PATCH 14/15] adapt routing control size to canvas width --- .../lib/components/toolbar/tools/routing/routing-controls.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/lib/components/toolbar/tools/routing/routing-controls.ts b/website/src/lib/components/toolbar/tools/routing/routing-controls.ts index acbbcc6c0..0b0d54024 100644 --- a/website/src/lib/components/toolbar/tools/routing/routing-controls.ts +++ b/website/src/lib/components/toolbar/tools/routing/routing-controls.ts @@ -810,7 +810,7 @@ export class RoutingControls { ` `, - 60 + _map.getCanvasContainer().offsetWidth > 1000 ? 50 : 80 ); } From c9ca75e2e8b732e2f9f6af4923dc7b62b08d166c Mon Sep 17 00:00:00 2001 From: vcoppe Date: Tue, 17 Feb 2026 22:24:14 +0100 Subject: [PATCH 15/15] small ui improvements --- website/src/lib/components/GPXStatistics.svelte | 6 +++--- .../elevation-profile/ElevationProfile.svelte | 2 +- website/src/lib/components/map/Map.svelte | 11 ++++++++++- website/src/routes/[[language]]/app/+page.svelte | 12 ++++++++++-- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/website/src/lib/components/GPXStatistics.svelte b/website/src/lib/components/GPXStatistics.svelte index ecc38577b..40838010f 100644 --- a/website/src/lib/components/GPXStatistics.svelte +++ b/website/src/lib/components/GPXStatistics.svelte @@ -31,13 +31,13 @@ diff --git a/website/src/lib/components/elevation-profile/ElevationProfile.svelte b/website/src/lib/components/elevation-profile/ElevationProfile.svelte index 11db47106..3d353a017 100644 --- a/website/src/lib/components/elevation-profile/ElevationProfile.svelte +++ b/website/src/lib/components/elevation-profile/ElevationProfile.svelte @@ -64,7 +64,7 @@ }); -
+
{#if showControls} diff --git a/website/src/lib/components/map/Map.svelte b/website/src/lib/components/map/Map.svelte index 0d4f3626d..9b5d539db 100644 --- a/website/src/lib/components/map/Map.svelte +++ b/website/src/lib/components/map/Map.svelte @@ -129,12 +129,21 @@ @apply relative; @apply top-0; @apply left-0; - @apply my-2; @apply w-[29px]; } + div :global(.maplibregl-ctrl-geocoder--icon-loading) { + @apply -mt-1; + @apply mb-0; + } + + div :global(.maplibregl-ctrl-geocoder--icon-close) { + @apply my-0; + } + div :global(.maplibregl-ctrl-geocoder--input) { @apply relative; + @apply h-8; @apply w-64; @apply py-0; @apply pl-2; diff --git a/website/src/routes/[[language]]/app/+page.svelte b/website/src/routes/[[language]]/app/+page.svelte index 6c5002e18..5cec7b973 100644 --- a/website/src/routes/[[language]]/app/+page.svelte +++ b/website/src/routes/[[language]]/app/+page.svelte @@ -30,6 +30,11 @@ elevationFill, } = settings; + let bottomPanelWidth: number | undefined = $state(); + let bottomPanelOrientation = $derived( + bottomPanelWidth && bottomPanelWidth >= 540 && $elevationProfile ? 'horizontal' : 'vertical' + ); + onMount(async () => { settings.connectToDatabase(db); fileStateCollection.connectToDatabase(db).then(() => { @@ -127,14 +132,17 @@ /> {/if}
{#if $elevationProfile}