feat: Kartenkomponente mit MapLibre und Höhenprofil
This commit is contained in:
parent
616694a7f5
commit
060c4434b9
4 changed files with 500 additions and 1 deletions
201
frontend/package-lock.json
generated
201
frontend/package-lock.json
generated
|
|
@ -10,6 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-svelte": "^0.503.0",
|
"lucide-svelte": "^0.503.0",
|
||||||
|
"maplibre-gl": "^6.2.0",
|
||||||
"mode-watcher": "^1.1.0",
|
"mode-watcher": "^1.1.0",
|
||||||
"pocketbase": "^0.26.0",
|
"pocketbase": "^0.26.0",
|
||||||
"tippy.js": "^6.3.7"
|
"tippy.js": "^6.3.7"
|
||||||
|
|
@ -821,6 +822,92 @@
|
||||||
"svelte": "^5"
|
"svelte": "^5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mapbox/jsonlint-lines-primitives": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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/tiny-sdf": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/unitbezier": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/vector-tile": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-Qf10S1uIHMk20ri/IVBnpS+esUEkVaR5Hftmz88jTInrpmWgPGJfPe3LVjjlE77trLx8tH6qjTG7uWH9hIq/0Q==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/point-geometry": "~1.1.0",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
|
"pbf": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@maplibre/geojson-vt": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-FVMOcmSP/yqol45t7StApEyTL5/vmqBCuFhH9n+fFuINenhaX+YgHHIt1yJ86S8kln3uJLcMvmEU2cfn6E2eCQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"kdbush": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||||
|
"version": "26.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-26.2.1.tgz",
|
||||||
|
"integrity": "sha512-QFKCXkOeSzOr8jF75jm6kySOg+dUvOehPhRi68gcOYPHb7U5JloUq0dJW0Y5/fZV8ygfT0Vp2RWodvq+fyxFWA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/jsonlint-lines-primitives": "^2.0.3",
|
||||||
|
"@mapbox/unitbezier": "^1.0.0",
|
||||||
|
"json-stringify-pretty-compact": "^4.0.0",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
"quickselect": "^3.0.0",
|
||||||
|
"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.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.12.tgz",
|
||||||
|
"integrity": "sha512-ZeK5w2TTeHOajcLaEQs1KZXw2V9wIKo1PmThlxlsHoXsQsYlBqLJzPOd6tJHRtGTChUY3DPPmjXRArYVvAbmZw==",
|
||||||
|
"license": "(MIT OR Apache-2.0)",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/point-geometry": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@maplibre/vt-pbf": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/point-geometry": "^1.1.0",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
|
"pbf": "^5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
|
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
|
||||||
|
|
@ -1869,6 +1956,12 @@
|
||||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||||
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
|
|
@ -2755,6 +2848,12 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/earcut": {
|
||||||
|
"version": "3.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.3.tgz",
|
||||||
|
"integrity": "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
|
@ -3153,6 +3252,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/gl-matrix": {
|
||||||
|
"version": "3.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||||
|
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
|
@ -3583,6 +3688,18 @@
|
||||||
"node": "^22.14.0 || >=24.0.0"
|
"node": "^22.14.0 || >=24.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/kdbush": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/kleur": {
|
"node_modules/kleur": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||||
|
|
@ -3939,6 +4056,38 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/maplibre-gl": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-PaNYtxWmYgIdDHshXsnU3Pho+H9IPme9H6dTjZbFWNazi+Q5QgKgIlu2GiSGVtOZ77/Fz3n5/1/kGM6ETzu0Lg==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/point-geometry": "^1.1.0",
|
||||||
|
"@mapbox/tiny-sdf": "^2.2.0",
|
||||||
|
"@mapbox/unitbezier": "^1.0.0",
|
||||||
|
"@mapbox/vector-tile": "^3.0.0",
|
||||||
|
"@maplibre/geojson-vt": "^6.1.1",
|
||||||
|
"@maplibre/maplibre-gl-style-spec": "^26.2.1",
|
||||||
|
"@maplibre/mlt": "^1.1.12",
|
||||||
|
"@maplibre/vt-pbf": "^4.3.2",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
|
"earcut": "^3.2.3",
|
||||||
|
"gl-matrix": "^3.4.4",
|
||||||
|
"kdbush": "^4.1.0",
|
||||||
|
"murmurhash-js": "^1.0.0",
|
||||||
|
"pbf": "^5.1.2",
|
||||||
|
"potpack": "^2.1.0",
|
||||||
|
"quickselect": "^3.0.0",
|
||||||
|
"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/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|
@ -4020,7 +4169,6 @@
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
|
@ -4359,6 +4507,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/murmurhash-js": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
|
@ -4656,6 +4810,18 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pbf": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"resolve-protobuf-schema": "^2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pbf": "bin/pbf"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
|
@ -4743,6 +4909,12 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/potpack": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/prebuild-install": {
|
"node_modules/prebuild-install": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
|
|
@ -4794,6 +4966,12 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/protocol-buffers-schema": {
|
||||||
|
"version": "3.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz",
|
||||||
|
"integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||||
|
|
@ -4815,6 +4993,12 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/quickselect": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/rc": {
|
"node_modules/rc": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||||
|
|
@ -4892,6 +5076,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resolve-protobuf-schema": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"protocol-buffers-schema": "^3.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/retry": {
|
"node_modules/retry": {
|
||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||||
|
|
@ -5657,6 +5850,12 @@
|
||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyqueue": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/tinyrainbow": {
|
"node_modules/tinyrainbow": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-svelte": "^0.503.0",
|
"lucide-svelte": "^0.503.0",
|
||||||
|
"maplibre-gl": "^6.2.0",
|
||||||
"mode-watcher": "^1.1.0",
|
"mode-watcher": "^1.1.0",
|
||||||
"pocketbase": "^0.26.0",
|
"pocketbase": "^0.26.0",
|
||||||
"tippy.js": "^6.3.7"
|
"tippy.js": "^6.3.7"
|
||||||
|
|
|
||||||
92
frontend/src/lib/components/ElevationProfile.svelte
Normal file
92
frontend/src/lib/components/ElevationProfile.svelte
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Höhenprofil als SVG-Fläche. Beim Überfahren meldet die Komponente den
|
||||||
|
* Index des Punktes; die Elternkomponente reicht ihn an die Karte weiter,
|
||||||
|
* damit dort dieselbe Stelle hervorgehoben wird.
|
||||||
|
*/
|
||||||
|
let {
|
||||||
|
elevation,
|
||||||
|
onhover,
|
||||||
|
height = '120px',
|
||||||
|
}: {
|
||||||
|
elevation: { d: number; ele: number }[]
|
||||||
|
onhover?: (index: number | null) => void
|
||||||
|
height?: string
|
||||||
|
} = $props()
|
||||||
|
|
||||||
|
// Internes Koordinatensystem; das SVG skaliert über viewBox mit.
|
||||||
|
const W = 1000
|
||||||
|
const H = 200
|
||||||
|
|
||||||
|
const stats = $derived.by(() => {
|
||||||
|
if (elevation.length < 2) return null
|
||||||
|
|
||||||
|
const eles = elevation.map((p) => p.ele)
|
||||||
|
const min = Math.min(...eles)
|
||||||
|
const max = Math.max(...eles)
|
||||||
|
const totalD = elevation[elevation.length - 1].d || 1
|
||||||
|
// Bei flachen Strecken wäre die Spanne 0 und die Division ungültig.
|
||||||
|
const span = max - min || 1
|
||||||
|
|
||||||
|
return { min, max, totalD, span }
|
||||||
|
})
|
||||||
|
|
||||||
|
const path = $derived.by(() => {
|
||||||
|
if (!stats) return ''
|
||||||
|
const { min, totalD, span } = stats
|
||||||
|
|
||||||
|
const pts = elevation.map((p) => {
|
||||||
|
const x = (p.d / totalD) * W
|
||||||
|
const y = H - ((p.ele - min) / span) * H
|
||||||
|
return `${x.toFixed(1)},${y.toFixed(1)}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fläche schließen: runter zur Grundlinie und zurück zum Anfang
|
||||||
|
return `M ${pts.join(' L ')} L ${W},${H} L 0,${H} Z`
|
||||||
|
})
|
||||||
|
|
||||||
|
let hoverX = $state<number | null>(null)
|
||||||
|
|
||||||
|
function handleMove(e: MouseEvent) {
|
||||||
|
if (!elevation.length) return
|
||||||
|
|
||||||
|
const rect = (e.currentTarget as SVGElement).getBoundingClientRect()
|
||||||
|
const ratio = (e.clientX - rect.left) / rect.width
|
||||||
|
const idx = Math.round(ratio * (elevation.length - 1))
|
||||||
|
const clamped = Math.max(0, Math.min(elevation.length - 1, idx))
|
||||||
|
|
||||||
|
hoverX = ratio * W
|
||||||
|
onhover?.(clamped)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLeave() {
|
||||||
|
hoverX = null
|
||||||
|
onhover?.(null)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if stats}
|
||||||
|
<div class="w-full" style:height>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 {W} {H}"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
class="w-full h-full"
|
||||||
|
role="img"
|
||||||
|
aria-label="Höhenprofil"
|
||||||
|
onmousemove={handleMove}
|
||||||
|
onmouseleave={handleLeave}
|
||||||
|
>
|
||||||
|
<path d={path} fill="rgb(14 165 233 / 0.2)" stroke="rgb(14 165 233)" stroke-width="2" vector-effect="non-scaling-stroke" />
|
||||||
|
{#if hoverX !== null}
|
||||||
|
<line x1={hoverX} y1="0" x2={hoverX} y2={H} stroke="rgb(14 165 233)" stroke-width="1" vector-effect="non-scaling-stroke" />
|
||||||
|
{/if}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-xs text-muted-foreground mt-1">
|
||||||
|
<span>{Math.round(stats.min)} m</span>
|
||||||
|
<span>{(stats.totalD / 1000).toFixed(1)} km</span>
|
||||||
|
<span>{Math.round(stats.max)} m</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="text-sm text-muted-foreground">Keine Höhendaten vorhanden.</p>
|
||||||
|
{/if}
|
||||||
207
frontend/src/lib/components/TrailMap.svelte
Normal file
207
frontend/src/lib/components/TrailMap.svelte
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Karte für einen Trail. Bewusst ohne Store-Zugriff: Alles kommt über
|
||||||
|
* Props, Ereignisse gehen über Callback-Props zurück. Damit ist die
|
||||||
|
* Komponente in Liste, Detailseite und Setzmodus gleichermaßen nutzbar.
|
||||||
|
*/
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import * as maplibregl from 'maplibre-gl'
|
||||||
|
import type { Map as MapLibreMap, Marker } from 'maplibre-gl'
|
||||||
|
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||||
|
|
||||||
|
type LineGeoJSON = { type: 'LineString'; coordinates: [number, number][] }
|
||||||
|
type MarkerData = {
|
||||||
|
id: string
|
||||||
|
lat: number
|
||||||
|
lng: number
|
||||||
|
color: string
|
||||||
|
resolved: boolean
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
geojson = null,
|
||||||
|
bounds = null,
|
||||||
|
markers = [],
|
||||||
|
status = 'offen',
|
||||||
|
placing = false,
|
||||||
|
highlightIndex = null,
|
||||||
|
onplace,
|
||||||
|
onmarkerclick,
|
||||||
|
height = '420px',
|
||||||
|
}: {
|
||||||
|
geojson?: LineGeoJSON | null
|
||||||
|
bounds?: [[number, number], [number, number]] | null
|
||||||
|
markers?: MarkerData[]
|
||||||
|
status?: 'offen' | 'eingeschraenkt' | 'gesperrt'
|
||||||
|
placing?: boolean
|
||||||
|
highlightIndex?: number | null
|
||||||
|
onplace?: (coords: { lat: number; lng: number }) => void
|
||||||
|
onmarkerclick?: (id: string) => void
|
||||||
|
height?: string
|
||||||
|
} = $props()
|
||||||
|
|
||||||
|
// Kachelquelle als Konstante, damit sie sich später leicht gegen eine
|
||||||
|
// Outdoor-Karte mit Höhenlinien tauschen lässt.
|
||||||
|
const TILES = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'
|
||||||
|
const ATTRIBUTION = '© OpenStreetMap-Mitwirkende'
|
||||||
|
|
||||||
|
// Fallback, wenn ein Trail noch keine Version hat: Hersbruck.
|
||||||
|
const FALLBACK_CENTER: [number, number] = [11.4318, 49.5089]
|
||||||
|
|
||||||
|
const LINE_COLORS = {
|
||||||
|
offen: '#16a34a',
|
||||||
|
eingeschraenkt: '#ca8a04',
|
||||||
|
gesperrt: '#dc2626',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
let container: HTMLDivElement
|
||||||
|
let map = $state<MapLibreMap | null>(null)
|
||||||
|
let ready = $state(false)
|
||||||
|
let markerObjects: Marker[] = []
|
||||||
|
let highlightMarker: Marker | null = null
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const instance = new maplibregl.Map({
|
||||||
|
container,
|
||||||
|
style: {
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
osm: {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: [TILES],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: ATTRIBUTION,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [{ id: 'osm', type: 'raster', source: 'osm' }],
|
||||||
|
},
|
||||||
|
center: FALLBACK_CENTER,
|
||||||
|
zoom: 11,
|
||||||
|
})
|
||||||
|
map = instance
|
||||||
|
|
||||||
|
instance.addControl(new maplibregl.NavigationControl(), 'top-right')
|
||||||
|
instance.addControl(new maplibregl.ScaleControl(), 'bottom-left')
|
||||||
|
|
||||||
|
instance.on('load', () => {
|
||||||
|
ready = true
|
||||||
|
})
|
||||||
|
|
||||||
|
instance.on('click', (e) => {
|
||||||
|
if (placing) onplace?.({ lat: e.lngLat.lat, lng: e.lngLat.lng })
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
instance.remove()
|
||||||
|
map = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Trail-Linie zeichnen bzw. aktualisieren
|
||||||
|
$effect(() => {
|
||||||
|
if (!map || !ready) return
|
||||||
|
|
||||||
|
const data = geojson
|
||||||
|
const color = LINE_COLORS[status] ?? LINE_COLORS.offen
|
||||||
|
const src = map.getSource('trail') as maplibregl.GeoJSONSource | undefined
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
if (src) {
|
||||||
|
src.setData({ type: 'FeatureCollection', features: [] })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const feature = { type: 'Feature' as const, properties: {}, geometry: data }
|
||||||
|
|
||||||
|
if (src) {
|
||||||
|
src.setData(feature)
|
||||||
|
map.setPaintProperty('trail-line', 'line-color', color)
|
||||||
|
} else {
|
||||||
|
map.addSource('trail', { type: 'geojson', data: feature })
|
||||||
|
map.addLayer({
|
||||||
|
id: 'trail-line',
|
||||||
|
type: 'line',
|
||||||
|
source: 'trail',
|
||||||
|
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
||||||
|
paint: { 'line-color': color, 'line-width': 4 },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Kartenausschnitt auf den Track setzen
|
||||||
|
$effect(() => {
|
||||||
|
if (!map || !ready || !bounds) return
|
||||||
|
map.fitBounds(bounds, { padding: 40, duration: 400, maxZoom: 16 })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Marker neu aufbauen — bei überschaubaren Mengen einfacher und
|
||||||
|
// zuverlässiger als einzelne Aktualisierungen.
|
||||||
|
$effect(() => {
|
||||||
|
if (!map || !ready) return
|
||||||
|
|
||||||
|
for (const m of markerObjects) m.remove()
|
||||||
|
markerObjects = []
|
||||||
|
|
||||||
|
for (const data of markers) {
|
||||||
|
const el = document.createElement('button')
|
||||||
|
el.type = 'button'
|
||||||
|
el.title = data.label
|
||||||
|
el.setAttribute('aria-label', data.label)
|
||||||
|
el.style.cssText = `
|
||||||
|
width: 18px; height: 18px; border-radius: 9999px;
|
||||||
|
border: 2px solid white; cursor: pointer; padding: 0;
|
||||||
|
background: ${data.color};
|
||||||
|
opacity: ${data.resolved ? '0.35' : '1'};
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,.4);
|
||||||
|
`
|
||||||
|
el.addEventListener('click', (ev) => {
|
||||||
|
ev.stopPropagation()
|
||||||
|
onmarkerclick?.(data.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
markerObjects.push(
|
||||||
|
new maplibregl.Marker({ element: el })
|
||||||
|
.setLngLat([data.lng, data.lat])
|
||||||
|
.addTo(map),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Stelle aus dem Höhenprofil hervorheben
|
||||||
|
$effect(() => {
|
||||||
|
if (!map || !ready) return
|
||||||
|
|
||||||
|
highlightMarker?.remove()
|
||||||
|
highlightMarker = null
|
||||||
|
|
||||||
|
const coords = geojson?.coordinates
|
||||||
|
if (highlightIndex == null || !coords?.length) return
|
||||||
|
|
||||||
|
const idx = Math.max(0, Math.min(coords.length - 1, highlightIndex))
|
||||||
|
const el = document.createElement('div')
|
||||||
|
el.style.cssText = `
|
||||||
|
width: 12px; height: 12px; border-radius: 9999px;
|
||||||
|
background: #0ea5e9; border: 2px solid white;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,.4);
|
||||||
|
`
|
||||||
|
|
||||||
|
highlightMarker = new maplibregl.Marker({ element: el })
|
||||||
|
.setLngLat(coords[idx])
|
||||||
|
.addTo(map)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={container}
|
||||||
|
style:height
|
||||||
|
class="w-full rounded-lg overflow-hidden border"
|
||||||
|
class:cursor-crosshair={placing}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{#if placing}
|
||||||
|
<p class="text-sm text-muted-foreground mt-2">
|
||||||
|
Klicke auf die Karte, um die Stelle zu markieren.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
Loading…
Reference in a new issue