Compare commits

38 Commits

Author SHA1 Message Date
80791a8f38 feat: show no overtaking sign
Some checks failed
TrafficCue CI / check (push) Failing after 1m39s
TrafficCue CI / build (push) Successful in 10m35s
TrafficCue CI / build-android (push) Successful in 26m54s
2025-10-26 17:21:14 +01:00
59ae422fe3 feat(info): route without successful overpass result
All checks were successful
TrafficCue CI / check (push) Successful in 1m41s
TrafficCue CI / build (push) Successful in 10m37s
TrafficCue CI / build-android (push) Successful in 26m55s
2025-10-25 20:16:25 +02:00
729cc22b37 refactor: clean up logs
Some checks failed
TrafficCue CI / check (push) Successful in 1m35s
TrafficCue CI / build-android (push) Has been cancelled
TrafficCue CI / build (push) Has been cancelled
2025-10-25 20:09:46 +02:00
678e52ddd5 feat(routing): use road max speed for verbal pre instruction distance 2025-10-25 20:08:45 +02:00
1035befc6f style: run prettier
Some checks failed
TrafficCue CI / check (push) Successful in 1m35s
TrafficCue CI / build-android (push) Has been cancelled
TrafficCue CI / build (push) Has been cancelled
2025-10-25 18:49:52 +02:00
1f99976f97 feat: use snapped location by default
Some checks are pending
TrafficCue CI / check (push) Has started running
2025-10-25 18:49:34 +02:00
640a2e8ef6 feat: bias getFeature to lastId
Some checks failed
TrafficCue CI / check (push) Failing after 1m36s
2025-10-22 20:40:24 +02:00
787b3b108b refactor: remove double feature fetch
Some checks failed
TrafficCue CI / check (push) Failing after 1m30s
2025-10-22 10:00:21 +02:00
001011cd23 feat: update transportation class blacklist
Some checks failed
TrafficCue CI / check (push) Failing after 1m34s
2025-10-22 09:53:25 +02:00
aa0bcdd091 feat: initial map matching
Some checks failed
TrafficCue CI / check (push) Failing after 1m38s
2025-10-21 20:08:08 +02:00
5ec74129e2 fix: audio ducking
All checks were successful
TrafficCue CI / check (push) Successful in 1m42s
TrafficCue CI / build (push) Successful in 10m19s
TrafficCue CI / build-android (push) Successful in 25m50s
2025-10-21 17:47:06 +02:00
372b31876d style: run prettier
All checks were successful
TrafficCue CI / check (push) Successful in 1m34s
TrafficCue CI / build (push) Successful in 10m30s
TrafficCue CI / build-android (push) Successful in 26m47s
2025-10-21 15:46:50 +02:00
4036790a4b feat(stores): update saved location functions 2025-10-21 15:46:17 +02:00
621691277e feat: fetch metadata from pmtiles
Some checks failed
TrafficCue CI / check (push) Failing after 1m45s
TrafficCue CI / build (push) Successful in 10m34s
TrafficCue CI / build-android (push) Has been cancelled
2025-10-21 15:33:31 +02:00
4ef2667c4e feat: add speed limit display
Some checks failed
TrafficCue CI / check (push) Failing after 1m53s
TrafficCue CI / build (push) Successful in 10m10s
TrafficCue CI / build-android (push) Successful in 30m40s
2025-10-20 17:51:06 +02:00
b46646a462 feat: add tourism to fetchPOI
Some checks failed
TrafficCue CI / check (push) Failing after 2m11s
TrafficCue CI / build (push) Successful in 9m57s
TrafficCue CI / build-android (push) Successful in 26m47s
2025-10-17 22:21:22 +02:00
171897033e feat: speedometer 2025-10-17 21:05:24 +02:00
c3dbbf2637 feat: increase button size while driving
Some checks failed
TrafficCue CI / check (push) Failing after 1m59s
TrafficCue CI / build (push) Successful in 9m50s
TrafficCue CI / build-android (push) Successful in 27m5s
2025-10-15 17:24:54 +02:00
60e074a70f feat: calendar connections
Some checks failed
TrafficCue CI / check (push) Failing after 1m39s
TrafficCue CI / build (push) Successful in 10m3s
TrafficCue CI / build-android (push) Successful in 25m18s
2025-10-11 16:00:12 +02:00
a0ea5a83a5 feat: caldav client
Some checks failed
TrafficCue CI / check (push) Failing after 2m22s
TrafficCue CI / build (push) Successful in 9m52s
TrafficCue CI / build-android (push) Successful in 26m47s
2025-10-11 15:06:19 +02:00
639b2c1aeb style: run prettier
All checks were successful
TrafficCue CI / check (push) Successful in 1m31s
TrafficCue CI / build (push) Successful in 9m49s
TrafficCue CI / build-android (push) Successful in 23m30s
2025-10-03 19:54:14 +02:00
de06d09d1c feat(location): set bearing on map
Some checks failed
TrafficCue CI / check (push) Failing after 1m29s
TrafficCue CI / build (push) Successful in 9m32s
TrafficCue CI / build-android (push) Has started running
2025-10-03 19:32:19 +02:00
d722364cf7 feat: align location marker to map
Some checks failed
TrafficCue CI / check (push) Failing after 1m30s
TrafficCue CI / build-android (push) Has been cancelled
TrafficCue CI / build (push) Has started running
2025-10-03 19:26:05 +02:00
fd7edc217d fix(vehicles): use store ID for selectedVehicle instead of index
Some checks failed
TrafficCue CI / check (push) Failing after 1m32s
TrafficCue CI / build (push) Successful in 9m35s
TrafficCue CI / build-android (push) Successful in 25m14s
2025-10-03 15:22:00 +02:00
6e11b438a2 feat(stores): location stores
Some checks failed
TrafficCue CI / check (push) Failing after 1m29s
TrafficCue CI / build-android (push) Has been cancelled
TrafficCue CI / build (push) Has been cancelled
2025-10-03 15:17:35 +02:00
46bf44a324 feat: friend system
Some checks failed
TrafficCue CI / check (push) Failing after 1m30s
TrafficCue CI / build (push) Successful in 9m50s
TrafficCue CI / build-android (push) Successful in 24m44s
2025-10-03 14:13:43 +02:00
3b28779b69 feat(routing): increase distance for verbal pre instruction
Some checks failed
TrafficCue CI / check (push) Failing after 1m32s
TrafficCue CI / build (push) Successful in 9m47s
TrafficCue CI / build-android (push) Successful in 23m41s
2025-10-02 21:42:32 +02:00
6f0fbac13e feat(routing): increase tolerance for off-route 2025-10-02 21:42:29 +02:00
920681f68c feat(routing): detect TTS language from valhalla trip 2025-10-02 21:42:23 +02:00
26b1fbc60c feat: update cargo dependencies
Some checks failed
TrafficCue CI / check (push) Failing after 1m28s
TrafficCue CI / build (push) Successful in 9m43s
TrafficCue CI / build-android (push) Successful in 23m36s
2025-10-02 20:32:13 +02:00
3cef8e9e14 fix: bad de translation for sidebar.in-route.left [skip ci] 2025-10-02 19:42:48 +02:00
d740c83b2e fix(tts): set correct parameter for language
Some checks failed
TrafficCue CI / check (push) Failing after 1m29s
TrafficCue CI / build (push) Successful in 9m42s
TrafficCue CI / build-android (push) Successful in 24m1s
2025-10-02 19:41:00 +02:00
99fae9df17 fix: language selector in onboarding
Some checks failed
TrafficCue CI / check (push) Failing after 1m33s
TrafficCue CI / build (push) Successful in 9m49s
TrafficCue CI / build-android (push) Has been cancelled
2025-10-01 07:31:54 +02:00
03838bf714 feat: localize voice guidance
Some checks failed
TrafficCue CI / check (push) Failing after 1m8s
TrafficCue CI / build (push) Successful in 9m46s
TrafficCue CI / build-android (push) Successful in 25m15s
2025-09-30 19:23:46 +02:00
50429fd4a6 fix: disable attribution control
Some checks failed
TrafficCue CI / check (push) Failing after 1m35s
TrafficCue CI / build (push) Successful in 9m44s
TrafficCue CI / build-android (push) Successful in 26m51s
2025-09-30 10:31:57 +02:00
b8ddada94e fix: add text wrap in POI list, increase search radius and exclude street_side parking
Some checks failed
TrafficCue CI / check (push) Failing after 1m34s
TrafficCue CI / build-android (push) Has been cancelled
TrafficCue CI / build (push) Has been cancelled
2025-09-30 10:28:25 +02:00
59d9667109 feat: use X icon for vehicle deletion
Some checks failed
TrafficCue CI / check (push) Failing after 1m35s
TrafficCue CI / build (push) Successful in 11m2s
TrafficCue CI / build-android (push) Has been cancelled
2025-09-30 10:04:51 +02:00
c2ab32c281 feat: deleting vehicles
Some checks failed
TrafficCue CI / check (push) Failing after 1m39s
TrafficCue CI / build-android (push) Has been cancelled
TrafficCue CI / build (push) Has been cancelled
2025-09-30 09:54:52 +02:00
51 changed files with 3039 additions and 859 deletions

317
bun.lock
View File

@ -6,8 +6,10 @@
"dependencies": {
"@diffusionstudio/vits-web": "^1.0.3",
"@eslint/js": "^9.29.0",
"@mapbox/vector-tile": "^2.0.4",
"@tauri-apps/plugin-fs": "~2",
"@tauri-apps/plugin-upload": "~2",
"@turf/turf": "^7.2.0",
"@types/sql.js": "^1.4.9",
"buffer": "^6.0.3",
"eslint": "^9.29.0",
@ -18,6 +20,7 @@
"libsodium-wrappers": "^0.7.15",
"opening_hours": "^3.8.0",
"pako": "^2.1.0",
"pbf": "^4.0.1",
"pmtiles": "^4.3.0",
"sql.js": "^1.13.0",
"svelte-maplibre-gl": "^0.1.8",
@ -38,7 +41,7 @@
"@types/libsodium-wrappers": "^0.7.14",
"@types/node": "^22.15.24",
"@types/pako": "^2.0.3",
"bits-ui": "^2.8.6",
"bits-ui": "^2.11.0",
"clsx": "^2.1.1",
"eslint-config-prettier": "^10.1.5",
"git-format-staged": "^3.1.1",
@ -205,13 +208,13 @@
"@mapbox/jsonlint-lines-primitives": ["@mapbox/jsonlint-lines-primitives@2.0.2", "", {}, "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ=="],
"@mapbox/point-geometry": ["@mapbox/point-geometry@0.1.0", "", {}, "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ=="],
"@mapbox/point-geometry": ["@mapbox/point-geometry@1.1.0", "", {}, "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="],
"@mapbox/tiny-sdf": ["@mapbox/tiny-sdf@2.0.6", "", {}, "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA=="],
"@mapbox/unitbezier": ["@mapbox/unitbezier@0.0.1", "", {}, "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw=="],
"@mapbox/vector-tile": ["@mapbox/vector-tile@1.3.1", "", { "dependencies": { "@mapbox/point-geometry": "~0.1.0" } }, "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw=="],
"@mapbox/vector-tile": ["@mapbox/vector-tile@2.0.4", "", { "dependencies": { "@mapbox/point-geometry": "~1.1.0", "@types/geojson": "^7946.0.16", "pbf": "^4.0.1" } }, "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg=="],
"@mapbox/whoots-js": ["@mapbox/whoots-js@3.1.0", "", {}, "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q=="],
@ -373,6 +376,238 @@
"@tsconfig/svelte": ["@tsconfig/svelte@5.0.4", "", {}, "sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q=="],
"@turf/along": ["@turf/along@7.2.0", "", { "dependencies": { "@turf/bearing": "^7.2.0", "@turf/destination": "^7.2.0", "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-Cf+d2LozABdb0TJoIcJwFKB+qisJY4nMUW9z6PAuZ9UCH7AR//hy2Z06vwYCKFZKP4a7DRPkOMBadQABCyoYuw=="],
"@turf/angle": ["@turf/angle@7.2.0", "", { "dependencies": { "@turf/bearing": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/rhumb-bearing": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-b28rs1NO8Dt/MXadFhnpqH7GnEWRsl+xF5JeFtg9+eM/+l/zGrdliPYMZtAj12xn33w22J1X4TRprAI0rruvVQ=="],
"@turf/area": ["@turf/area@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-zuTTdQ4eoTI9nSSjerIy4QwgvxqwJVciQJ8tOPuMHbXJ9N/dNjI7bU8tasjhxas/Cx3NE9NxVHtNpYHL0FSzoA=="],
"@turf/bbox": ["@turf/bbox@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-wzHEjCXlYZiDludDbXkpBSmv8Zu6tPGLmJ1sXQ6qDwpLE1Ew3mcWqt8AaxfTP5QwDNQa3sf2vvgTEzNbPQkCiA=="],
"@turf/bbox-clip": ["@turf/bbox-clip@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-q6RXTpqeUQAYLAieUL1n3J6ukRGsNVDOqcYtfzaJbPW+0VsAf+1cI16sN700t0sekbeU1DH/RRVAHhpf8+36wA=="],
"@turf/bbox-polygon": ["@turf/bbox-polygon@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-Aj4G1GAAy26fmOqMjUk0Z+Lcax5VQ9g1xYDbHLQWXvfTsaueBT+RzdH6XPnZ/seEEnZkio2IxE8V5af/osupgA=="],
"@turf/bearing": ["@turf/bearing@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-Jm0Xt3GgHjRrWvBtAGvgfnADLm+4exud2pRlmCYx8zfiKuNXQFkrcTZcOiJOgTfG20Agq28iSh15uta47jSIbg=="],
"@turf/bezier-spline": ["@turf/bezier-spline@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-7BPkc3ufYB9KLvcaTpTsnpXzh9DZoENxCS0Ms9XUwuRXw45TpevwUpOsa3atO76iKQ5puHntqFO4zs8IUxBaaA=="],
"@turf/boolean-clockwise": ["@turf/boolean-clockwise@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-0fJeFSARxy6ealGBM4Gmgpa1o8msQF87p2Dx5V6uSqzT8VPDegX1NSWl4b7QgXczYa9qv7IAABttdWP0K7Q7eQ=="],
"@turf/boolean-concave": ["@turf/boolean-concave@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-v3dTN04dfO6VqctQj1a+pjDHb6+/Ev90oAR2QjJuAntY4ubhhr7vKeJdk/w+tWNSMKULnYwfe65Du3EOu3/TeA=="],
"@turf/boolean-contains": ["@turf/boolean-contains@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/boolean-point-on-line": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-dgRQm4uVO5XuLee4PLVH7CFQZKdefUBMIXTPITm2oRIDmPLJKHDOFKQTNkGJ73mDKKBR2lmt6eVH3br6OYrEYg=="],
"@turf/boolean-crosses": ["@turf/boolean-crosses@7.2.0", "", { "dependencies": { "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/line-intersect": "^7.2.0", "@turf/polygon-to-line": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-9GyM4UUWFKQOoNhHVSfJBf5XbPy8Fxfz9djjJNAnm/IOl8NmFUSwFPAjKlpiMcr6yuaAoc9R/1KokS9/eLqPvA=="],
"@turf/boolean-disjoint": ["@turf/boolean-disjoint@7.2.0", "", { "dependencies": { "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/line-intersect": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/polygon-to-line": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-xdz+pYKkLMuqkNeJ6EF/3OdAiJdiHhcHCV0ykX33NIuALKIEpKik0+NdxxNsZsivOW6keKwr61SI+gcVtHYcnQ=="],
"@turf/boolean-equal": ["@turf/boolean-equal@7.2.0", "", { "dependencies": { "@turf/clean-coords": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "geojson-equality-ts": "^1.0.2", "tslib": "^2.8.1" } }, "sha512-TmjKYLsxXqEmdDtFq3QgX4aSogiISp3/doeEtDOs3NNSR8susOtBEZkmvwO6DLW+g/rgoQJIBR6iVoWiRqkBxw=="],
"@turf/boolean-intersects": ["@turf/boolean-intersects@7.2.0", "", { "dependencies": { "@turf/boolean-disjoint": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-GLRyLQgK3F14drkK5Qi9Mv7Z9VT1bgQUd9a3DB3DACTZWDSwfh8YZUFn/HBwRkK8dDdgNEXaavggQHcPi1k9ow=="],
"@turf/boolean-overlap": ["@turf/boolean-overlap@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/line-intersect": "^7.2.0", "@turf/line-overlap": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "geojson-equality-ts": "^1.0.2", "tslib": "^2.8.1" } }, "sha512-ieM5qIE4anO+gUHIOvEN7CjyowF+kQ6v20/oNYJCp63TVS6eGMkwgd+I4uMzBXfVW66nVHIXjODdUelU+Xyctw=="],
"@turf/boolean-parallel": ["@turf/boolean-parallel@7.2.0", "", { "dependencies": { "@turf/clean-coords": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/line-segment": "^7.2.0", "@turf/rhumb-bearing": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-iOtuzzff8nmwv05ROkSvyeGLMrfdGkIi+3hyQ+DH4IVyV37vQbqR5oOJ0Nt3Qq1Tjrq9fvF8G3OMdAv3W2kY9w=="],
"@turf/boolean-point-in-polygon": ["@turf/boolean-point-in-polygon@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "point-in-polygon-hao": "^1.1.0", "tslib": "^2.8.1" } }, "sha512-lvEOjxeXIp+wPXgl9kJA97dqzMfNexjqHou+XHVcfxQgolctoJiRYmcVCWGpiZ9CBf/CJha1KmD1qQoRIsjLaA=="],
"@turf/boolean-point-on-line": ["@turf/boolean-point-on-line@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-H/bXX8+2VYeSyH8JWrOsu8OGmeA9KVZfM7M6U5/fSqGsRHXo9MyYJ94k39A9kcKSwI0aWiMXVD2UFmiWy8423Q=="],
"@turf/boolean-touches": ["@turf/boolean-touches@7.2.0", "", { "dependencies": { "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/boolean-point-on-line": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-8qb1CO+cwFATGRGFgTRjzL9aibfsbI91pdiRl7KIEkVdeN/H9k8FDrUA1neY7Yq48IaciuwqjbbojQ16FD9b0w=="],
"@turf/boolean-valid": ["@turf/boolean-valid@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/boolean-crosses": "^7.2.0", "@turf/boolean-disjoint": "^7.2.0", "@turf/boolean-overlap": "^7.2.0", "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/boolean-point-on-line": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/line-intersect": "^7.2.0", "@types/geojson": "^7946.0.10", "geojson-polygon-self-intersections": "^1.2.1", "tslib": "^2.8.1" } }, "sha512-xb7gdHN8VV6ivPJh6rPpgxmAEGReiRxqY+QZoEZVGpW2dXcmU1BdY6FA6G/cwvggXAXxJBREoANtEDgp/0ySbA=="],
"@turf/boolean-within": ["@turf/boolean-within@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/boolean-point-on-line": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-zB3AiF59zQZ27Dp1iyhp9mVAKOFHat8RDH45TZhLY8EaqdEPdmLGvwMFCKfLryQcUDQvmzP8xWbtUR82QM5C4g=="],
"@turf/buffer": ["@turf/buffer@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/center": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/jsts": "^2.7.1", "@turf/meta": "^7.2.0", "@turf/projection": "^7.2.0", "@types/geojson": "^7946.0.10", "d3-geo": "1.7.1" } }, "sha512-QH1FTr5Mk4z1kpQNztMD8XBOZfpOXPOtlsxaSAj2kDIf5+LquA6HtJjZrjUngnGtzG5+XwcfyRL4ImvLnFjm5Q=="],
"@turf/center": ["@turf/center@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-UTNp9abQ2kuyRg5gCIGDNwwEQeF3NbpYsd1Q0KW9lwWuzbLVNn0sOwbxjpNF4J2HtMOs5YVOcqNvYyuoa2XrXw=="],
"@turf/center-mean": ["@turf/center-mean@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-NaW6IowAooTJ35O198Jw3U4diZ6UZCCeJY+4E+WMLpks3FCxMDSHEfO2QjyOXQMGWZnVxVelqI5x9DdniDbQ+A=="],
"@turf/center-median": ["@turf/center-median@7.2.0", "", { "dependencies": { "@turf/center-mean": "^7.2.0", "@turf/centroid": "^7.2.0", "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-/CgVyHNG4zAoZpvkl7qBCe4w7giWNVtLyTU5PoIfg1vWM4VpYw+N7kcBBH46bbzvVBn0vhmZr586r543EwdC/A=="],
"@turf/center-of-mass": ["@turf/center-of-mass@7.2.0", "", { "dependencies": { "@turf/centroid": "^7.2.0", "@turf/convex": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-ij3pmG61WQPHGTQvOziPOdIgwTMegkYTwIc71Gl7xn4C0vWH6KLDSshCphds9xdWSXt2GbHpUs3tr4XGntHkEQ=="],
"@turf/centroid": ["@turf/centroid@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-yJqDSw25T7P48au5KjvYqbDVZ7qVnipziVfZ9aSo7P2/jTE7d4BP21w0/XLi3T/9bry/t9PR1GDDDQljN4KfDw=="],
"@turf/circle": ["@turf/circle@7.2.0", "", { "dependencies": { "@turf/destination": "^7.2.0", "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-1AbqBYtXhstrHmnW6jhLwsv7TtmT0mW58Hvl1uZXEDM1NCVXIR50yDipIeQPjrCuJ/Zdg/91gU8+4GuDCAxBGA=="],
"@turf/clean-coords": ["@turf/clean-coords@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-+5+J1+D7wW7O/RDXn46IfCHuX1gIV1pIAQNSA7lcDbr3HQITZj334C4mOGZLEcGbsiXtlHWZiBtm785Vg8i+QQ=="],
"@turf/clone": ["@turf/clone@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-JlGUT+/5qoU5jqZmf6NMFIoLDY3O7jKd53Up+zbpJ2vzUp6QdwdNzwrsCeONhynWM13F0MVtPXH4AtdkrgFk4g=="],
"@turf/clusters": ["@turf/clusters@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-sKOrIKHHtXAuTKNm2USnEct+6/MrgyzMW42deZ2YG2RRKWGaaxHMFU2Yw71Yk4DqStOqTIBQpIOdrRuSOwbuQw=="],
"@turf/clusters-dbscan": ["@turf/clusters-dbscan@7.2.0", "", { "dependencies": { "@turf/clone": "^7.2.0", "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "rbush": "^3.0.1", "tslib": "^2.8.1" } }, "sha512-VWVUuDreev56g3/BMlnq/81yzczqaz+NVTypN5CigGgP67e+u/CnijphiuhKjtjDd/MzGjXgEWBJc26Y6LYKAw=="],
"@turf/clusters-kmeans": ["@turf/clusters-kmeans@7.2.0", "", { "dependencies": { "@turf/clone": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "skmeans": "0.9.7", "tslib": "^2.8.1" } }, "sha512-BxQdK8jc8Mwm9yoClCYkktm4W004uiQGqb/i/6Y7a8xqgJITWDgTu/cy//wOxAWPk4xfe6MThjnqkszWW8JdyQ=="],
"@turf/collect": ["@turf/collect@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "rbush": "^3.0.1", "tslib": "^2.8.1" } }, "sha512-zRVGDlYS8Bx/Zz4vnEUyRg4dmqHhkDbW/nIUIJh657YqaMj1SFi4Iv2i9NbcurlUBDJFkpuOhCvvEvAdskJ8UA=="],
"@turf/combine": ["@turf/combine@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-VEjm3IvnbMt3IgeRIhCDhhQDbLqCU1/5uN1+j1u6fyA095pCizPThGp4f/COSzC3t1s/iiV+fHuDsB6DihHffQ=="],
"@turf/concave": ["@turf/concave@7.2.0", "", { "dependencies": { "@turf/clone": "^7.2.0", "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/tin": "^7.2.0", "@types/geojson": "^7946.0.10", "topojson-client": "3.x", "topojson-server": "3.x", "tslib": "^2.8.1" } }, "sha512-cpaDDlumK762kdadexw5ZAB6g/h2pJdihZ+e65lbQVe3WukJHAANnIEeKsdFCuIyNKrwTz2gWu5ws+OpjP48Yw=="],
"@turf/convex": ["@turf/convex@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "concaveman": "^1.2.1", "tslib": "^2.8.1" } }, "sha512-HsgHm+zHRE8yPCE/jBUtWFyaaBmpXcSlyHd5/xsMhSZRImFzRzBibaONWQo7xbKZMISC3Nc6BtUjDi/jEVbqyA=="],
"@turf/destination": ["@turf/destination@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-8DUxtOO0Fvrh1xclIUj3d9C5WS20D21F5E+j+X9Q+ju6fcM4huOqTg5ckV1DN2Pg8caABEc5HEZJnGch/5YnYQ=="],
"@turf/difference": ["@turf/difference@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "polyclip-ts": "^0.16.8", "tslib": "^2.8.1" } }, "sha512-NHKD1v3s8RX+9lOpvHJg6xRuJOKiY3qxHhz5/FmE0VgGqnCkE7OObqWZ5SsXG+Ckh0aafs5qKhmDdDV/gGi6JA=="],
"@turf/dissolve": ["@turf/dissolve@7.2.0", "", { "dependencies": { "@turf/flatten": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "polyclip-ts": "^0.16.8", "tslib": "^2.8.1" } }, "sha512-gPG5TE3mAYuZqBut8tPYCKwi4hhx5Cq0ALoQMB9X0hrVtFIKrihrsj98XQM/5pL/UIpAxQfwisQvy6XaOFaoPA=="],
"@turf/distance": ["@turf/distance@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-HBjjXIgEcD/wJYjv7/6OZj5yoky2oUvTtVeIAqO3lL80XRvoYmVg6vkOIu6NswkerwLDDNT9kl7+BFLJoHbh6Q=="],
"@turf/distance-weight": ["@turf/distance-weight@7.2.0", "", { "dependencies": { "@turf/centroid": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-NeoyV0fXDH+7nIoNtLjAoH9XL0AS1pmTIyDxEE6LryoDTsqjnuR0YQxIkLCCWDqECoqaOmmBqpeWONjX5BwWCg=="],
"@turf/ellipse": ["@turf/ellipse@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/rhumb-destination": "^7.2.0", "@turf/transform-rotate": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-/Y75S5hE2+xjnTw4dXpQ5r/Y2HPM4xrwkPRCCQRpuuboKdEvm42azYmh7isPnMnBTVcmGb9UmGKj0HHAbiwt1g=="],
"@turf/envelope": ["@turf/envelope@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/bbox-polygon": "^7.2.0", "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-xOMtDeNKHwUuDfzQeoSNmdabsP0/IgVDeyzitDe/8j9wTeW+MrKzVbGz7627PT3h6gsO+2nUv5asfKtUbmTyHA=="],
"@turf/explode": ["@turf/explode@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-jyMXg93J1OI7/65SsLE1k9dfQD3JbcPNMi4/O3QR2Qb3BAs2039oFaSjtW+YqhMqVC4V3ZeKebMcJ8h9sK1n+A=="],
"@turf/flatten": ["@turf/flatten@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-q38Qsqr4l7mxp780zSdn0gp/WLBX+sa+gV6qIbDQ1HKCrrPK8QQJmNx7gk1xxEXVot6tq/WyAPysCQdX+kLmMA=="],
"@turf/flip": ["@turf/flip@7.2.0", "", { "dependencies": { "@turf/clone": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-X0TQ0U/UYh4tyXdLO5itP1sO2HOvfrZC0fYSWmTfLDM14jEPkEK8PblofznfBygL+pIFtOS2is8FuVcp5XxYpQ=="],
"@turf/geojson-rbush": ["@turf/geojson-rbush@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "rbush": "^3.0.1" } }, "sha512-ST8fLv+EwxVkDgsmhHggM0sPk2SfOHTZJkdgMXVFT7gB9o4lF8qk4y4lwvCCGIfFQAp2yv/PN5EaGMEKutk6xw=="],
"@turf/great-circle": ["@turf/great-circle@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10" } }, "sha512-n30OiADyOKHhor0aXNgYfXQYXO3UtsOKmhQsY1D89/Oh1nCIXG/1ZPlLL9ZoaRXXBTUBjh99a+K8029NQbGDhw=="],
"@turf/helpers": ["@turf/helpers@7.2.0", "", { "dependencies": { "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-cXo7bKNZoa7aC7ydLmUR02oB3IgDe7MxiPuRz3cCtYQHn+BJ6h1tihmamYDWWUlPHgSNF0i3ATc4WmDECZafKw=="],
"@turf/hex-grid": ["@turf/hex-grid@7.2.0", "", { "dependencies": { "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/intersect": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-Yo2yUGxrTCQfmcVsSjDt0G3Veg8YD26WRd7etVPD9eirNNgXrIyZkbYA7zVV/qLeRWVmYIKRXg1USWl7ORQOGA=="],
"@turf/interpolate": ["@turf/interpolate@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/centroid": "^7.2.0", "@turf/clone": "^7.2.0", "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/hex-grid": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/point-grid": "^7.2.0", "@turf/square-grid": "^7.2.0", "@turf/triangle-grid": "^7.2.0", "@types/geojson": "^7946.0.10" } }, "sha512-Ifgjm1SEo6XujuSAU6lpRMvoJ1SYTreil1Rf5WsaXj16BQJCedht/4FtWCTNhSWTwEz2motQ1WNrjTCuPG94xA=="],
"@turf/intersect": ["@turf/intersect@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "polyclip-ts": "^0.16.8", "tslib": "^2.8.1" } }, "sha512-81GMzKS9pKqLPa61qSlFxLFeAC8XbwyCQ9Qv4z6o5skWk1qmMUbEHeMqaGUTEzk+q2XyhZ0sju1FV4iLevQ/aw=="],
"@turf/invariant": ["@turf/invariant@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-kV4u8e7Gkpq+kPbAKNC21CmyrXzlbBgFjO1PhrHPgEdNqXqDawoZ3i6ivE3ULJj2rSesCjduUaC/wyvH/sNr2Q=="],
"@turf/isobands": ["@turf/isobands@7.2.0", "", { "dependencies": { "@turf/area": "^7.2.0", "@turf/bbox": "^7.2.0", "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/explode": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "marchingsquares": "^1.3.3", "tslib": "^2.8.1" } }, "sha512-lYoHeRieFzpBp29Jh19QcDIb0E+dzo/K5uwZuNga4wxr6heNU0AfkD4ByAHYIXHtvmp4m/JpSKq/2N6h/zvBkg=="],
"@turf/isolines": ["@turf/isolines@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "marchingsquares": "^1.3.3", "tslib": "^2.8.1" } }, "sha512-4ZXKxvA/JKkxAXixXhN3UVza5FABsdYgOWXyYm3L5ryTPJVOYTVSSd9A+CAVlv9dZc3YdlsqMqLTXNOOre/kwg=="],
"@turf/jsts": ["@turf/jsts@2.7.2", "", { "dependencies": { "jsts": "2.7.1" } }, "sha512-zAezGlwWHPyU0zxwcX2wQY3RkRpwuoBmhhNE9HY9kWhFDkCxZ3aWK5URKwa/SWKJbj9aztO+8vtdiBA28KVJFg=="],
"@turf/kinks": ["@turf/kinks@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-BtxDxGewJR0Q5WR9HKBSxZhirFX+GEH1rD7/EvgDsHS8e1Y5/vNQQUmXdURjdPa4StzaUBsWRU5T3A356gLbPA=="],
"@turf/length": ["@turf/length@7.2.0", "", { "dependencies": { "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-LBmYN+iCgVtWNLsckVnpQIJENqIIPO63mogazMp23lrDGfWXu07zZQ9ZinJVO5xYurXNhc/QI2xxoqt2Xw90Ig=="],
"@turf/line-arc": ["@turf/line-arc@7.2.0", "", { "dependencies": { "@turf/circle": "^7.2.0", "@turf/destination": "^7.2.0", "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-kfWzA5oYrTpslTg5fN50G04zSypiYQzjZv3FLjbZkk6kta5fo4JkERKjTeA8x4XNojb+pfmjMBB0yIh2w2dDRw=="],
"@turf/line-chunk": ["@turf/line-chunk@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/length": "^7.2.0", "@turf/line-slice-along": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10" } }, "sha512-1ODyL5gETtWSL85MPI0lgp/78vl95M39gpeBxePXyDIqx8geDP9kXfAzctuKdxBoR4JmOVM3NT7Fz7h+IEkC+g=="],
"@turf/line-intersect": ["@turf/line-intersect@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "sweepline-intersections": "^1.5.0", "tslib": "^2.8.1" } }, "sha512-GhCJVEkc8EmggNi85EuVLoXF5T5jNVxmhIetwppiVyJzMrwkYAkZSYB3IBFYGUUB9qiNFnTwungVSsBV/S8ZiA=="],
"@turf/line-offset": ["@turf/line-offset@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10" } }, "sha512-1+OkYueDCbnEWzbfBh3taVr+3SyM2bal5jfnSEuDiLA6jnlScgr8tn3INo+zwrUkPFZPPAejL1swVyO5TjUahw=="],
"@turf/line-overlap": ["@turf/line-overlap@7.2.0", "", { "dependencies": { "@turf/boolean-point-on-line": "^7.2.0", "@turf/geojson-rbush": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/line-segment": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/nearest-point-on-line": "^7.2.0", "@types/geojson": "^7946.0.10", "fast-deep-equal": "^3.1.3", "tslib": "^2.8.1" } }, "sha512-NNn7/jg53+N10q2Kyt66bEDqN3101iW/1zA5FW7J6UbKApDFkByh+18YZq1of71kS6oUYplP86WkDp16LFpqqw=="],
"@turf/line-segment": ["@turf/line-segment@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-E162rmTF9XjVN4rINJCd15AdQGCBlNqeWN3V0YI1vOUpZFNT2ii4SqEMCcH2d+5EheHLL8BWVwZoOsvHZbvaWA=="],
"@turf/line-slice": ["@turf/line-slice@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/nearest-point-on-line": "^7.2.0", "@types/geojson": "^7946.0.10" } }, "sha512-bHotzZIaU1GPV3RMwttYpDrmcvb3X2i1g/WUttPZWtKrEo2VVAkoYdeZ2aFwtogERYS4quFdJ/TDzAtquBC8WQ=="],
"@turf/line-slice-along": ["@turf/line-slice-along@7.2.0", "", { "dependencies": { "@turf/bearing": "^7.2.0", "@turf/destination": "^7.2.0", "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10" } }, "sha512-4/gPgP0j5Rp+1prbhXqn7kIH/uZTmSgiubUnn67F8nb9zE+MhbRglhSlRYEZxAVkB7VrGwjyolCwvrROhjHp2A=="],
"@turf/line-split": ["@turf/line-split@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/geojson-rbush": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/line-intersect": "^7.2.0", "@turf/line-segment": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/nearest-point-on-line": "^7.2.0", "@turf/square": "^7.2.0", "@turf/truncate": "^7.2.0", "@types/geojson": "^7946.0.10" } }, "sha512-yJTZR+c8CwoKqdW/aIs+iLbuFwAa3Yan+EOADFQuXXIUGps3bJUXx/38rmowNoZbHyP1np1+OtrotyHu5uBsfQ=="],
"@turf/line-to-polygon": ["@turf/line-to-polygon@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/clone": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-iKpJqc7EYc5NvlD4KaqrKKO6mXR7YWO/YwtW60E2FnsF/blnsy9OfAOcilYHgH3S/V/TT0VedC7DW7Kgjy2EIA=="],
"@turf/mask": ["@turf/mask@7.2.0", "", { "dependencies": { "@turf/clone": "^7.2.0", "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "polyclip-ts": "^0.16.8", "tslib": "^2.8.1" } }, "sha512-ulJ6dQqXC0wrjIoqFViXuMUdIPX5Q6GPViZ3kGfeVijvlLM7kTFBsZiPQwALSr5nTQg4Ppf3FD0Jmg8IErPrgA=="],
"@turf/meta": ["@turf/meta@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10" } }, "sha512-igzTdHsQc8TV1RhPuOLVo74Px/hyPrVgVOTgjWQZzt3J9BVseCdpfY/0cJBdlSRI4S/yTmmHl7gAqjhpYH5Yaw=="],
"@turf/midpoint": ["@turf/midpoint@7.2.0", "", { "dependencies": { "@turf/bearing": "^7.2.0", "@turf/destination": "^7.2.0", "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-AMn5S9aSrbXdE+Q4Rj+T5nLdpfpn+mfzqIaEKkYI021HC0vb22HyhQHsQbSeX+AWcS4CjD1hFsYVcgKI+5qCfw=="],
"@turf/moran-index": ["@turf/moran-index@7.2.0", "", { "dependencies": { "@turf/distance-weight": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-Aexh1EmXVPJhApr9grrd120vbalIthcIsQ3OAN2Tqwf+eExHXArJEJqGBo9IZiQbIpFJeftt/OvUvlI8BeO1bA=="],
"@turf/nearest-neighbor-analysis": ["@turf/nearest-neighbor-analysis@7.2.0", "", { "dependencies": { "@turf/area": "^7.2.0", "@turf/bbox": "^7.2.0", "@turf/bbox-polygon": "^7.2.0", "@turf/centroid": "^7.2.0", "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/nearest-point": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-LmP/crXb7gilgsL0wL9hsygqc537W/a1W5r9XBKJT4SKdqjoXX5APJatJfd3nwXbRIqwDH0cDA9/YyFjBPlKnA=="],
"@turf/nearest-point": ["@turf/nearest-point@7.2.0", "", { "dependencies": { "@turf/clone": "^7.2.0", "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-0wmsqXZ8CGw4QKeZmS+NdjYTqCMC+HXZvM3XAQIU6k6laNLqjad2oS4nDrtcRs/nWDvcj1CR+Io7OiQ6sbpn5Q=="],
"@turf/nearest-point-on-line": ["@turf/nearest-point-on-line@7.2.0", "", { "dependencies": { "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-UOhAeoDPVewBQV+PWg1YTMQcYpJsIqfW5+EuZ5vJl60XwUa0+kqB/eVfSLNXmHENjKKIlEt9Oy9HIDF4VeWmXA=="],
"@turf/nearest-point-to-line": ["@turf/nearest-point-to-line@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/point-to-line-distance": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-EorU7Qj30A7nAjh++KF/eTPDlzwuuV4neBz7tmSTB21HKuXZAR0upJsx6M2X1CSyGEgNsbFB0ivNKIvymRTKBw=="],
"@turf/planepoint": ["@turf/planepoint@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-8Vno01tvi5gThUEKBQ46CmlEKDAwVpkl7stOPFvJYlA1oywjAL4PsmgwjXgleZuFtXQUPBNgv5a42Pf438XP4g=="],
"@turf/point-grid": ["@turf/point-grid@7.2.0", "", { "dependencies": { "@turf/boolean-within": "^7.2.0", "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-ai7lwBV2FREPW3XiUNohT4opC1hd6+F56qZe20xYhCTkTD9diWjXHiNudQPSmVAUjgMzQGasblQQqvOdL+bJ3Q=="],
"@turf/point-on-feature": ["@turf/point-on-feature@7.2.0", "", { "dependencies": { "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/center": "^7.2.0", "@turf/explode": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/nearest-point": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-ksoYoLO9WtJ/qI8VI9ltF+2ZjLWrAjZNsCsu8F7nyGeCh4I8opjf4qVLytFG44XA2qI5yc6iXDpyv0sshvP82Q=="],
"@turf/point-to-line-distance": ["@turf/point-to-line-distance@7.2.0", "", { "dependencies": { "@turf/bearing": "^7.2.0", "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/nearest-point-on-line": "^7.2.0", "@turf/projection": "^7.2.0", "@turf/rhumb-bearing": "^7.2.0", "@turf/rhumb-distance": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-fB9Rdnb5w5+t76Gho2dYDkGe20eRrFk8CXi4v1+l1PC8YyLXO+x+l3TrtT8HzL/dVaZeepO6WUIsIw3ditTOPg=="],
"@turf/point-to-polygon-distance": ["@turf/point-to-polygon-distance@7.2.0", "", { "dependencies": { "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/point-to-line-distance": "^7.2.0", "@turf/polygon-to-line": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-w+WYuINgTiFjoZemQwOaQSje/8Kq+uqJOynvx7+gleQPHyWQ3VtTodtV4LwzVzXz8Sf7Mngx1Jcp2SNai5CJYA=="],
"@turf/points-within-polygon": ["@turf/points-within-polygon@7.2.0", "", { "dependencies": { "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-jRKp8/mWNMzA+hKlQhxci97H5nOio9tp14R2SzpvkOt+cswxl+NqTEi1hDd2XetA7tjU0TSoNjEgVY8FfA0S6w=="],
"@turf/polygon-smooth": ["@turf/polygon-smooth@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-KCp9wF2IEynvGXVhySR8oQ2razKP0zwg99K+fuClP21pSKCFjAPaihPEYq6e8uI/1J7ibjL5++6EMl+LrUTrLg=="],
"@turf/polygon-tangents": ["@turf/polygon-tangents@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/boolean-within": "^7.2.0", "@turf/explode": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/nearest-point": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-AHUUPmOjiQDrtP/ODXukHBlUG0C/9I1je7zz50OTfl2ZDOdEqFJQC3RyNELwq07grTXZvg5TS5wYx/Y7nsm47g=="],
"@turf/polygon-to-line": ["@turf/polygon-to-line@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-9jeTN3LiJ933I5sd4K0kwkcivOYXXm1emk0dHorwXeSFSHF+nlYesEW3Hd889wb9lZd7/SVLMUeX/h39mX+vCA=="],
"@turf/polygonize": ["@turf/polygonize@7.2.0", "", { "dependencies": { "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/envelope": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-U9v+lBhUPDv+nsg/VcScdiqCB59afO6CHDGrwIl2+5i6Ve+/KQKjpTV/R+NqoC1iMXAEq3brY6HY8Ukp/pUWng=="],
"@turf/projection": ["@turf/projection@7.2.0", "", { "dependencies": { "@turf/clone": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-/qke5vJScv8Mu7a+fU3RSChBRijE6EVuFHU3RYihMuYm04Vw8dBMIs0enEpoq0ke/IjSbleIrGQNZIMRX9EwZQ=="],
"@turf/quadrat-analysis": ["@turf/quadrat-analysis@7.2.0", "", { "dependencies": { "@turf/area": "^7.2.0", "@turf/bbox": "^7.2.0", "@turf/bbox-polygon": "^7.2.0", "@turf/centroid": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/point-grid": "^7.2.0", "@turf/random": "^7.2.0", "@turf/square-grid": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-fDQh3+ldYNxUqS6QYlvJ7GZLlCeDZR6tD3ikdYtOsSemwW1n/4gm2xcgWJqy3Y0uszBwxc13IGGY7NGEjHA+0w=="],
"@turf/random": ["@turf/random@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-fNXs5mOeXsrirliw84S8UCNkpm4RMNbefPNsuCTfZEXhcr1MuHMzq4JWKb4FweMdN1Yx2l/xcytkO0s71cJ50w=="],
"@turf/rectangle-grid": ["@turf/rectangle-grid@7.2.0", "", { "dependencies": { "@turf/boolean-intersects": "^7.2.0", "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-f0o5ifvy0Ml/nHDJzMNcuSk4h11aa3BfvQNnYQhLpuTQu03j/ICZNlzKTLxwjcUqvxADUifty7Z9CX5W6zky4A=="],
"@turf/rewind": ["@turf/rewind@7.2.0", "", { "dependencies": { "@turf/boolean-clockwise": "^7.2.0", "@turf/clone": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-SZpRAZiZsE22+HVz6pEID+ST25vOdpAMGk5NO1JeqzhpMALIkIGnkG+xnun2CfYHz7wv8/Z0ADiAvei9rkcQYA=="],
"@turf/rhumb-bearing": ["@turf/rhumb-bearing@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-jbdexlrR8X2ZauUciHx3tRwG+BXoMXke4B8p8/IgDlAfIrVdzAxSQN89FMzIKnjJ/kdLjo9bFGvb92bu31Etug=="],
"@turf/rhumb-destination": ["@turf/rhumb-destination@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-U9OLgLAHlH4Wfx3fBZf3jvnkDjdTcfRan5eI7VPV1+fQWkOteATpzkiRjCvSYK575GljVwWBjkKca8LziGWitQ=="],
"@turf/rhumb-distance": ["@turf/rhumb-distance@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-NsijTPON1yOc9tirRPEQQuJ5aQi7pREsqchQquaYKbHNWsexZjcDi4wnw2kM3Si4XjmgynT+2f7aXH7FHarHzw=="],
"@turf/sample": ["@turf/sample@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-f+ZbcbQJ9glQ/F26re8LadxO0ORafy298EJZe6XtbctRTJrNus6UNAsl8+GYXFqMnXM22tbTAznnJX3ZiWNorA=="],
"@turf/sector": ["@turf/sector@7.2.0", "", { "dependencies": { "@turf/circle": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/line-arc": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-zL06MjbbMG4DdpiNz+Q9Ax8jsCekt3R76uxeWShulAGkyDB5smdBOUDoRwxn05UX7l4kKv4Ucq2imQXhxKFd1w=="],
"@turf/shortest-path": ["@turf/shortest-path@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/bbox-polygon": "^7.2.0", "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/clean-coords": "^7.2.0", "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/transform-scale": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-6fpx8feZ2jMSaeRaFdqFShGWkNb+veUOeyLFSHA/aRD9n/e9F2pWZoRbQWKbKTpcKFJ2FnDEqCZnh/GrcAsqWA=="],
"@turf/simplify": ["@turf/simplify@7.2.0", "", { "dependencies": { "@turf/clean-coords": "^7.2.0", "@turf/clone": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-9YHIfSc8BXQfi5IvEMbCeQYqNch0UawIGwbboJaoV8rodhtk6kKV2wrpXdGqk/6Thg6/RWvChJFKVVTjVrULyQ=="],
"@turf/square": ["@turf/square@7.2.0", "", { "dependencies": { "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-9pMoAGFvqzCDOlO9IRSSBCGXKbl8EwMx6xRRBMKdZgpS0mZgfm9xiptMmx/t1m4qqHIlb/N+3MUF7iMBx6upcA=="],
"@turf/square-grid": ["@turf/square-grid@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/rectangle-grid": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-EmzGXa90hz+tiCOs9wX+Lak6pH0Vghb7QuX6KZej+pmWi3Yz7vdvQLmy/wuN048+wSkD5c8WUo/kTeNDe7GnmA=="],
"@turf/standard-deviational-ellipse": ["@turf/standard-deviational-ellipse@7.2.0", "", { "dependencies": { "@turf/center-mean": "^7.2.0", "@turf/ellipse": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/points-within-polygon": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-+uC0pR2nRjm90JvMXe/2xOCZsYV2II1ZZ2zmWcBWv6bcFXBspcxk2QfCC3k0bj6jDapELzoQgnn3cG5lbdQV2w=="],
"@turf/tag": ["@turf/tag@7.2.0", "", { "dependencies": { "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/clone": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-TAFvsbp5TCBqXue8ui+CtcLsPZ6NPC88L8Ad6Hb/R6VAi21qe0U42WJHQYXzWmtThoTNwxi+oKSeFbRDsr0FIA=="],
"@turf/tesselate": ["@turf/tesselate@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "earcut": "^2.2.4", "tslib": "^2.8.1" } }, "sha512-zHGcG85aOJJu1seCm+CYTJ3UempX4Xtyt669vFG6Hbr/Hc7ii6STQ2ysFr7lJwFtU9uyYhphVrrgwIqwglvI/Q=="],
"@turf/tin": ["@turf/tin@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-y24Vt3oeE6ZXvyLJamP0Ke02rPlDGE9gF7OFADnR0mT+2uectb0UTIBC3kKzON80TEAlA3GXpKFkCW5Fo/O/Kg=="],
"@turf/transform-rotate": ["@turf/transform-rotate@7.2.0", "", { "dependencies": { "@turf/centroid": "^7.2.0", "@turf/clone": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/rhumb-bearing": "^7.2.0", "@turf/rhumb-destination": "^7.2.0", "@turf/rhumb-distance": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-EMCj0Zqy3cF9d3mGRqDlYnX2ZBXe3LgT+piDR0EuF5c5sjuKErcFcaBIsn/lg1gp4xCNZFinkZ3dsFfgGHf6fw=="],
"@turf/transform-scale": ["@turf/transform-scale@7.2.0", "", { "dependencies": { "@turf/bbox": "^7.2.0", "@turf/center": "^7.2.0", "@turf/centroid": "^7.2.0", "@turf/clone": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/rhumb-bearing": "^7.2.0", "@turf/rhumb-destination": "^7.2.0", "@turf/rhumb-distance": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-HYB+pw938eeI8s1/zSWFy6hq+t38fuUaBb0jJsZB1K9zQ1WjEYpPvKF/0//80zNPlyxLv3cOkeBucso3hzI07A=="],
"@turf/transform-translate": ["@turf/transform-translate@7.2.0", "", { "dependencies": { "@turf/clone": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/rhumb-destination": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-zAglR8MKCqkzDTjGMIQgbg/f+Q3XcKVzr9cELw5l9CrS1a0VTSDtBZLDm0kWx0ankwtam7ZmI2jXyuQWT8Gbug=="],
"@turf/triangle-grid": ["@turf/triangle-grid@7.2.0", "", { "dependencies": { "@turf/distance": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/intersect": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-4gcAqWKh9hg6PC5nNSb9VWyLgl821cwf9yR9yEzQhEFfwYL/pZONBWCO1cwVF23vSYMSMm+/TwqxH4emxaArfw=="],
"@turf/truncate": ["@turf/truncate@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-jyFzxYbPugK4XjV5V/k6Xr3taBjjvo210IbPHJXw0Zh7Y6sF+hGxeRVtSuZ9VP/6oRyqAOHKUrze+OOkPqBgUg=="],
"@turf/turf": ["@turf/turf@7.2.0", "", { "dependencies": { "@turf/along": "^7.2.0", "@turf/angle": "^7.2.0", "@turf/area": "^7.2.0", "@turf/bbox": "^7.2.0", "@turf/bbox-clip": "^7.2.0", "@turf/bbox-polygon": "^7.2.0", "@turf/bearing": "^7.2.0", "@turf/bezier-spline": "^7.2.0", "@turf/boolean-clockwise": "^7.2.0", "@turf/boolean-concave": "^7.2.0", "@turf/boolean-contains": "^7.2.0", "@turf/boolean-crosses": "^7.2.0", "@turf/boolean-disjoint": "^7.2.0", "@turf/boolean-equal": "^7.2.0", "@turf/boolean-intersects": "^7.2.0", "@turf/boolean-overlap": "^7.2.0", "@turf/boolean-parallel": "^7.2.0", "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/boolean-point-on-line": "^7.2.0", "@turf/boolean-touches": "^7.2.0", "@turf/boolean-valid": "^7.2.0", "@turf/boolean-within": "^7.2.0", "@turf/buffer": "^7.2.0", "@turf/center": "^7.2.0", "@turf/center-mean": "^7.2.0", "@turf/center-median": "^7.2.0", "@turf/center-of-mass": "^7.2.0", "@turf/centroid": "^7.2.0", "@turf/circle": "^7.2.0", "@turf/clean-coords": "^7.2.0", "@turf/clone": "^7.2.0", "@turf/clusters": "^7.2.0", "@turf/clusters-dbscan": "^7.2.0", "@turf/clusters-kmeans": "^7.2.0", "@turf/collect": "^7.2.0", "@turf/combine": "^7.2.0", "@turf/concave": "^7.2.0", "@turf/convex": "^7.2.0", "@turf/destination": "^7.2.0", "@turf/difference": "^7.2.0", "@turf/dissolve": "^7.2.0", "@turf/distance": "^7.2.0", "@turf/distance-weight": "^7.2.0", "@turf/ellipse": "^7.2.0", "@turf/envelope": "^7.2.0", "@turf/explode": "^7.2.0", "@turf/flatten": "^7.2.0", "@turf/flip": "^7.2.0", "@turf/geojson-rbush": "^7.2.0", "@turf/great-circle": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/hex-grid": "^7.2.0", "@turf/interpolate": "^7.2.0", "@turf/intersect": "^7.2.0", "@turf/invariant": "^7.2.0", "@turf/isobands": "^7.2.0", "@turf/isolines": "^7.2.0", "@turf/kinks": "^7.2.0", "@turf/length": "^7.2.0", "@turf/line-arc": "^7.2.0", "@turf/line-chunk": "^7.2.0", "@turf/line-intersect": "^7.2.0", "@turf/line-offset": "^7.2.0", "@turf/line-overlap": "^7.2.0", "@turf/line-segment": "^7.2.0", "@turf/line-slice": "^7.2.0", "@turf/line-slice-along": "^7.2.0", "@turf/line-split": "^7.2.0", "@turf/line-to-polygon": "^7.2.0", "@turf/mask": "^7.2.0", "@turf/meta": "^7.2.0", "@turf/midpoint": "^7.2.0", "@turf/moran-index": "^7.2.0", "@turf/nearest-neighbor-analysis": "^7.2.0", "@turf/nearest-point": "^7.2.0", "@turf/nearest-point-on-line": "^7.2.0", "@turf/nearest-point-to-line": "^7.2.0", "@turf/planepoint": "^7.2.0", "@turf/point-grid": "^7.2.0", "@turf/point-on-feature": "^7.2.0", "@turf/point-to-line-distance": "^7.2.0", "@turf/point-to-polygon-distance": "^7.2.0", "@turf/points-within-polygon": "^7.2.0", "@turf/polygon-smooth": "^7.2.0", "@turf/polygon-tangents": "^7.2.0", "@turf/polygon-to-line": "^7.2.0", "@turf/polygonize": "^7.2.0", "@turf/projection": "^7.2.0", "@turf/quadrat-analysis": "^7.2.0", "@turf/random": "^7.2.0", "@turf/rectangle-grid": "^7.2.0", "@turf/rewind": "^7.2.0", "@turf/rhumb-bearing": "^7.2.0", "@turf/rhumb-destination": "^7.2.0", "@turf/rhumb-distance": "^7.2.0", "@turf/sample": "^7.2.0", "@turf/sector": "^7.2.0", "@turf/shortest-path": "^7.2.0", "@turf/simplify": "^7.2.0", "@turf/square": "^7.2.0", "@turf/square-grid": "^7.2.0", "@turf/standard-deviational-ellipse": "^7.2.0", "@turf/tag": "^7.2.0", "@turf/tesselate": "^7.2.0", "@turf/tin": "^7.2.0", "@turf/transform-rotate": "^7.2.0", "@turf/transform-scale": "^7.2.0", "@turf/transform-translate": "^7.2.0", "@turf/triangle-grid": "^7.2.0", "@turf/truncate": "^7.2.0", "@turf/union": "^7.2.0", "@turf/unkink-polygon": "^7.2.0", "@turf/voronoi": "^7.2.0", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-G1kKBu4hYgoNoRJgnpJohNuS7bLnoWHZ2G/4wUMym5xOSiYah6carzdTEsMoTsauyi7ilByWHx5UHwbjjCVcBw=="],
"@turf/union": ["@turf/union@7.2.0", "", { "dependencies": { "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "polyclip-ts": "^0.16.8", "tslib": "^2.8.1" } }, "sha512-Xex/cfKSmH0RZRWSJl4RLlhSmEALVewywiEXcu0aIxNbuZGTcpNoI0h4oLFrE/fUd0iBGFg/EGLXRL3zTfpg6g=="],
"@turf/unkink-polygon": ["@turf/unkink-polygon@7.2.0", "", { "dependencies": { "@turf/area": "^7.2.0", "@turf/boolean-point-in-polygon": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/meta": "^7.2.0", "@types/geojson": "^7946.0.10", "rbush": "^3.0.1", "tslib": "^2.8.1" } }, "sha512-dFPfzlIgkEr15z6oXVxTSWshWi51HeITGVFtl1GAKGMtiXJx1uMqnfRsvljqEjaQu/4AzG1QAp3b+EkSklQSiQ=="],
"@turf/voronoi": ["@turf/voronoi@7.2.0", "", { "dependencies": { "@turf/clone": "^7.2.0", "@turf/helpers": "^7.2.0", "@turf/invariant": "^7.2.0", "@types/d3-voronoi": "^1.1.12", "@types/geojson": "^7946.0.10", "d3-voronoi": "1.1.2", "tslib": "^2.8.1" } }, "sha512-3K6N0LtJsWTXxPb/5N2qD9e8f4q8+tjTbGV3lE3v8x06iCnNlnuJnqM5NZNPpvgvCatecBkhClO3/3RndE61Fw=="],
"@types/d3-voronoi": ["@types/d3-voronoi@1.1.12", "", {}, "sha512-DauBl25PKZZ0WVJr42a6CNvI6efsdzofl9sajqZr2Gf5Gu733WkDdUGiPkUHXiUvYGzNNlFQde2wdZdfQPG+yw=="],
"@types/emscripten": ["@types/emscripten@1.40.1", "", {}, "sha512-sr53lnYkQNhjHNN0oJDdUm5564biioI5DuOpycufDVK7D3y+GR3oUswe2rlwY1nPNyusHbrJ9WoTyIHl4/Bpwg=="],
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
@ -441,7 +676,9 @@
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"bits-ui": ["bits-ui@2.9.2", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.29.1", "svelte-toolbelt": "^0.9.3", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-GGbyr4oVKtHin//Q0AhlygkasmfWt328VjsnmB3sP+h8Sh+Eyghm+1AQ8o+xQMDCYbdL35JZ9UZGTZYTMar4Uw=="],
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"bits-ui": ["bits-ui@2.11.5", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.31.1", "svelte-toolbelt": "^0.10.4", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-d7b6HrrCUeK261c777agFz0G5lx13RMA0DT022e4SRuIjI3bZ8ci53YxIZ2/jpXTmeAeqeShyC+Mgibh9OeW9A=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
@ -471,6 +708,8 @@
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"concaveman": ["concaveman@1.2.1", "", { "dependencies": { "point-in-polygon": "^1.1.0", "rbush": "^3.0.1", "robust-predicates": "^2.0.4", "tinyqueue": "^2.0.3" } }, "sha512-PwZYKaM/ckQSa8peP5JpVr7IMJ4Nn/MHIaWUjP4be+KoZ7Botgs8seAZGpmaOM+UZXawcdYRao/px9ycrCihHw=="],
"consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
@ -479,6 +718,12 @@
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"d3-array": ["d3-array@1.2.4", "", {}, "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="],
"d3-geo": ["d3-geo@1.7.1", "", { "dependencies": { "d3-array": "1" } }, "sha512-O4AempWAr+P5qbk2bC2FuN/sDW4z+dN2wDf9QV3bxQt4M5HfOEeXLgJ/UKQW0+o1Dj8BE+L5kiDbdWUMjsmQpw=="],
"d3-voronoi": ["d3-voronoi@1.1.2", "", {}, "sha512-RhGS1u2vavcO7ay7ZNAPo4xeDh/VYeGof3x5ZLJBQgYhLegxr3s5IykvWmJ94FTU6mcbtp4sloqZ54mP6R4Utw=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"dedent": ["dedent@1.5.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg=="],
@ -555,6 +800,10 @@
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"geojson-equality-ts": ["geojson-equality-ts@1.0.2", "", { "dependencies": { "@types/geojson": "^7946.0.14" } }, "sha512-h3Ryq+0mCSN/7yLs0eDgrZhvc9af23o/QuC4aTiuuzP/MRCtd6mf5rLsLRY44jX0RPUfM8c4GqERQmlUxPGPoQ=="],
"geojson-polygon-self-intersections": ["geojson-polygon-self-intersections@1.2.1", "", { "dependencies": { "rbush": "^2.0.1" } }, "sha512-/QM1b5u2d172qQVO//9CGRa49jEmclKEsYOQmWP9ooEjj63tBM51m2805xsbxkzlEELQ2REgTf700gUhhlegxA=="],
"geojson-vt": ["geojson-vt@4.0.2", "", {}, "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A=="],
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
@ -629,6 +878,8 @@
"jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],
"jsts": ["jsts@2.7.1", "", {}, "sha512-x2wSZHEBK20CY+Wy+BPE7MrFQHW6sIsdaGUMEqmGAio+3gFzQaBYPwLRonUfQf9Ak8pBieqj9tUofX1+WtAEIg=="],
"jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
"jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
@ -703,6 +954,8 @@
"maplibre-gl": ["maplibre-gl@5.5.0", "", { "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/point-geometry": "^0.1.0", "@mapbox/tiny-sdf": "^2.0.6", "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", "@maplibre/maplibre-gl-style-spec": "^23.2.2", "@types/geojson": "^7946.0.16", "@types/geojson-vt": "3.2.5", "@types/mapbox__point-geometry": "^0.1.4", "@types/mapbox__vector-tile": "^1.3.4", "@types/pbf": "^3.0.5", "@types/supercluster": "^7.1.3", "earcut": "^3.0.1", "geojson-vt": "^4.0.2", "gl-matrix": "^3.4.3", "global-prefix": "^4.0.0", "kdbush": "^4.0.2", "murmurhash-js": "^1.0.0", "pbf": "^3.3.0", "potpack": "^2.0.0", "quickselect": "^3.0.0", "supercluster": "^8.0.1", "tinyqueue": "^3.0.0", "vt-pbf": "^3.1.3" } }, "sha512-p8AOPuzzqn1ZA9gcXxKw0IED715we/2Owa/YUr6PANmgMvNMe/JG+V/C1hRra43Wm62Biz+Aa8AgbOLJimA8tA=="],
"marchingsquares": ["marchingsquares@1.3.3", "", {}, "sha512-gz6nNQoVK7Lkh2pZulrT4qd4347S/toG9RXH2pyzhLgkL5mLkBoqgv4EvAGXcV0ikDW72n/OQb3Xe8bGagQZCg=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
@ -749,7 +1002,7 @@
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"pbf": ["pbf@3.3.0", "", { "dependencies": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q=="],
"pbf": ["pbf@4.0.1", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
@ -759,6 +1012,12 @@
"pmtiles": ["pmtiles@4.3.0", "", { "dependencies": { "fflate": "^0.8.2" } }, "sha512-wnzQeSiYT/MyO63o7AVxwt7+uKqU0QUy2lHrivM7GvecNy0m1A4voVyGey7bujnEW5Hn+ZzLdvHPoFaqrOzbPA=="],
"point-in-polygon": ["point-in-polygon@1.1.0", "", {}, "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw=="],
"point-in-polygon-hao": ["point-in-polygon-hao@1.2.4", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ=="],
"polyclip-ts": ["polyclip-ts@0.16.8", "", { "dependencies": { "bignumber.js": "^9.1.0", "splaytree-ts": "^1.0.2" } }, "sha512-JPtKbDRuPEuAjuTdhR62Gph7Is2BS1Szx69CFOO3g71lpJDFo78k4tFyi+qFOMVPePEzdSKkpGU3NBXPHHjvKQ=="],
"postcss": ["postcss@8.5.4", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w=="],
"postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="],
@ -787,6 +1046,8 @@
"quickselect": ["quickselect@3.0.0", "", {}, "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="],
"rbush": ["rbush@3.0.1", "", { "dependencies": { "quickselect": "^2.0.0" } }, "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="],
@ -797,11 +1058,13 @@
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
"rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"runed": ["runed@0.29.2", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA=="],
"runed": ["runed@0.31.1", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-v3czcTnO+EJjiPvD4dwIqfTdHLZ8oH0zJheKqAHh9QMViY7Qb29UlAMRpX7ZtHh7AFqV60KmfxaJ9QMy+L1igQ=="],
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
@ -815,8 +1078,12 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"skmeans": ["skmeans@0.9.7", "", {}, "sha512-hNj1/oZ7ygsfmPZ7ZfN5MUBRoGg1gtpnImuJBgLO0ljQ67DtJuiQaiYdS4lUA6s0KCwnPhGivtC/WRwIZLkHyg=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"splaytree-ts": ["splaytree-ts@1.0.2", "", {}, "sha512-0kGecIZNIReCSiznK3uheYB8sbstLjCZLiwcQwbmLhgHJj2gz6OnSPkVzJQCMnmEz1BQ4gPK59ylhBoEWOhGNA=="],
"sql.js": ["sql.js@1.13.0", "", {}, "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA=="],
"sqlite-wasm-kysely": ["sqlite-wasm-kysely@0.3.0", "", { "dependencies": { "@sqlite.org/sqlite-wasm": "^3.48.0-build2" }, "peerDependencies": { "kysely": "*" } }, "sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg=="],
@ -839,7 +1106,9 @@
"svelte-maplibre-gl": ["svelte-maplibre-gl@0.1.8", "", { "peerDependencies": { "@deck.gl/core": "^9.1.0", "@deck.gl/layers": "^9.1.0", "@deck.gl/mapbox": "^9.1.0", "maplibre-contour": ">=0.1.0", "maplibre-gl": "^5.0.0 || ^4.0.0", "pmtiles": "^4.0.0", "svelte": ">=5.0.0", "terra-draw": "^1.0.0", "terra-draw-maplibre-gl-adapter": "^1.0.3" } }, "sha512-YvMo25q/rpNDNE4iBvOuYYt+E+6jT+PBLxX7vR20LE5ZD2K3cLV9cR34S4SX7w81E00lP7InD2+CvFr7T0vBxg=="],
"svelte-toolbelt": ["svelte-toolbelt@0.9.3", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.29.0", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw=="],
"svelte-toolbelt": ["svelte-toolbelt@0.10.5", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.29.0", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-8e+eWTgxw1aiLxhDE8Rb1X6AoLitqpJz+WhAul2W7W58C8KoLoJQf1TgQdFPBiCPJ0Jg5y0Zi1uyua9em4VS0w=="],
"sweepline-intersections": ["sweepline-intersections@1.5.0", "", { "dependencies": { "tinyqueue": "^2.0.0" } }, "sha512-AoVmx72QHpKtItPu72TzFL+kcYjd67BPLDoR0LarIk+xyaRg+pDTMFXndIEvZf9xEKnJv6JdhgRMnocoG0D3AQ=="],
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
@ -867,6 +1136,10 @@
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"topojson-client": ["topojson-client@3.1.0", "", { "dependencies": { "commander": "2" }, "bin": { "topo2geo": "bin/topo2geo", "topomerge": "bin/topomerge", "topoquantize": "bin/topoquantize" } }, "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw=="],
"topojson-server": ["topojson-server@3.0.1", "", { "dependencies": { "commander": "2" }, "bin": { "geo2topo": "bin/geo2topo" } }, "sha512-/VS9j/ffKr2XAOjlZ9CgyyeLmgJ9dMwq6Y0YEON8O7p/tGGk+dCWnrE03zEdu7i4L7YsFZLEPZPzCvcB7lEEXw=="],
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@ -941,22 +1214,52 @@
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"concaveman/robust-predicates": ["robust-predicates@2.0.4", "", {}, "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg=="],
"concaveman/tinyqueue": ["tinyqueue@2.0.3", "", {}, "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"geojson-polygon-self-intersections/rbush": ["rbush@2.0.2", "", { "dependencies": { "quickselect": "^1.0.1" } }, "sha512-XBOuALcTm+O/H8G90b6pzu6nX6v2zCKiFG4BJho8a+bY6AER6t8uQUZdi5bomQc0AprCWhEGa7ncAbbRap0bRA=="],
"global-prefix/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
"maplibre-gl/@mapbox/point-geometry": ["@mapbox/point-geometry@0.1.0", "", {}, "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ=="],
"maplibre-gl/@mapbox/vector-tile": ["@mapbox/vector-tile@1.3.1", "", { "dependencies": { "@mapbox/point-geometry": "~0.1.0" } }, "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw=="],
"maplibre-gl/earcut": ["earcut@3.0.1", "", {}, "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw=="],
"maplibre-gl/pbf": ["pbf@3.3.0", "", { "dependencies": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"rbush/quickselect": ["quickselect@2.0.0", "", {}, "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw=="],
"svelte-toolbelt/runed": ["runed@0.29.2", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA=="],
"sweepline-intersections/tinyqueue": ["tinyqueue@2.0.3", "", {}, "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="],
"tailwind-variants/tailwind-merge": ["tailwind-merge@3.0.2", "", {}, "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw=="],
"topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"topojson-server/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"vaul-svelte/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
"vaul-svelte/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
"vt-pbf/@mapbox/point-geometry": ["@mapbox/point-geometry@0.1.0", "", {}, "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ=="],
"vt-pbf/@mapbox/vector-tile": ["@mapbox/vector-tile@1.3.1", "", { "dependencies": { "@mapbox/point-geometry": "~0.1.0" } }, "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw=="],
"vt-pbf/pbf": ["pbf@3.3.0", "", { "dependencies": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"geojson-polygon-self-intersections/rbush/quickselect": ["quickselect@1.1.1", "", {}, "sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ=="],
"global-prefix/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
}
}

View File

@ -1,6 +1,11 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"language": "Deutsch - German",
"language": {
"name": "Deutsch - German",
"tts": "de",
"speechSynthesis": "de-DE",
"valhalla": "de"
},
"saved": {
"home": "Heim",
"school": "Schule",
@ -101,7 +106,7 @@
"ask-question": "Stellen Sie eine Frage zu diesem Ort ..."
},
"in-route": {
"left": "links",
"left": "übrig",
"end-trip": "Route beenden",
"share-code": "Code teilen",
"stop-sharing": "Standortfreigabe beenden",
@ -155,5 +160,9 @@
"poi": {
"fuel": "Tankstelle",
"parking": "Parken"
},
"routing": {
"off-route": "Sie sind von der Route abgekommen",
"back-on-route": "Sie sind wieder auf der Route"
}
}

View File

@ -1,6 +1,11 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"language": "English",
"language": {
"name": "English",
"tts": "en",
"speechSynthesis": "en-US",
"valhalla": "en"
},
"save": "Save",
"cancel": "Cancel",
"loading": "Loading...",
@ -73,7 +78,8 @@
"settings": {
"header": "Settings",
"general": "General",
"map": "Map"
"map": "Map",
"connections": "Connections"
},
"info": {
"dropped": "Dropped Pin",
@ -143,6 +149,18 @@
"header": "Nearby Points of Interest",
"no-poi": "No points of interest found nearby.",
"loading": "Loading nearby points of interest..."
},
"calendar": {
"header": "Calendar",
"add": "Add Calendar",
"connect": "Connect Calendar",
"probing-server": "Probing server...",
"discovering-calendars": "Discovering calendars...",
"choose": "Choose calendars to add:"
},
"appearance": {
"header": "Appearance",
"bigger-buttons-in-drive": "Bigger Buttons while Driving"
}
},
"unsave": "Unsave",
@ -155,5 +173,21 @@
"poi": {
"fuel": "Fuel Station",
"parking": "Parking"
},
"routing": {
"off-route": "You went off route",
"back-on-route": "You are back on route"
},
"open": "Open",
"submit": "Submit",
"done": "Done",
"delete": "Delete",
"calendar": {
"location": "Location",
"no-location": "No location",
"start": "Start",
"end": "End",
"no-start": "No start",
"no-end": "No end"
}
}

View File

@ -13,7 +13,7 @@
"@types/libsodium-wrappers": "^0.7.14",
"@types/node": "^22.15.24",
"@types/pako": "^2.0.3",
"bits-ui": "^2.8.6",
"bits-ui": "^2.11.0",
"clsx": "^2.1.1",
"eslint-config-prettier": "^10.1.5",
"git-format-staged": "^3.1.1",
@ -42,8 +42,10 @@
"dependencies": {
"@diffusionstudio/vits-web": "^1.0.3",
"@eslint/js": "^9.29.0",
"@mapbox/vector-tile": "^2.0.4",
"@tauri-apps/plugin-fs": "~2",
"@tauri-apps/plugin-upload": "~2",
"@turf/turf": "^7.2.0",
"@types/sql.js": "^1.4.9",
"buffer": "^6.0.3",
"eslint": "^9.29.0",
@ -54,6 +56,7 @@
"libsodium-wrappers": "^0.7.15",
"opening_hours": "^3.8.0",
"pako": "^2.1.0",
"pbf": "^4.0.1",
"pmtiles": "^4.3.0",
"sql.js": "^1.13.0",
"svelte-maplibre-gl": "^0.1.8",

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

1421
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -27,3 +27,10 @@ tauri-plugin-keep-screen-on = "0.1.2"
tauri-plugin-upload = "2"
tauri-plugin-fs = "2"
tauri-plugin-tts = { git = "https://github.com/cfpwastaken/tauri-plugin-tts.git" }
tauri-plugin-duck = { git = "https://git.picoscratch.de/trafficcue/tauri-plugin-duck.git" }
reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls"] }
diqwest = "3.1.0"
minidom = "0.17.0"
icalendar = { version = "0.17.5", features = ["chrono-tz"] }
anyhow = "1.0.100"
chrono = "0.4.42"

View File

@ -36,6 +36,7 @@
]
},
"upload:allow-download",
"tts:allow-speak"
"tts:allow-speak",
"duck:default"
]
}

389
src-tauri/src/dav.rs Normal file
View File

@ -0,0 +1,389 @@
use std::fmt::Display;
use diqwest::WithDigestAuth;
use icalendar::{Calendar};
use minidom::Element;
use reqwest::{
header::{CONTENT_TYPE, USER_AGENT}, Client, Method, RequestBuilder, Url
};
use serde::{Deserialize, Serialize};
static PRINCIPAL_BODY: &str = r#"
<d:propfind xmlns:d="DAV:">
<d:prop>
<d:current-user-principal />
</d:prop>
</d:propfind>
"#;
static HOMESET_BODY: &str = r#"
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:prop>
<c:calendar-home-set />
</d:prop>
</d:propfind>
"#;
static CAL_BODY: &str = r#"
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" >
<d:prop>
<d:displayname />
<d:resourcetype />
<c:supported-calendar-component-set />
</d:prop>
</d:propfind>
"#;
static EVENT_BODY: &str = r#"
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:prop>
<d:getetag />
<c:calendar-data />
</d:prop>
<c:filter>
<c:comp-filter name="VCALENDAR">
<c:comp-filter name="VEVENT" >
<c:time-range start="{start}" end="{end}" />
</c:comp-filter>
</c:comp-filter>
</c:filter>
</c:calendar-query>
"#;
pub fn find_elems(root: &Element, tag: String) -> Vec<&Element> {
let mut elems: Vec<&Element> = Vec::new();
for el in root.children() {
if el.name() == tag {
elems.push(el);
} else {
let ret = find_elems(el, tag.clone());
elems.extend(ret);
}
}
elems
}
pub fn find_elem(root: &Element, tag: String) -> Option<&Element> {
if root.name() == tag {
return Some(root);
}
for el in root.children() {
if el.name() == tag {
return Some(el);
} else {
let ret = find_elem(el, tag.clone());
if ret.is_some() {
return ret;
}
}
}
None
}
#[derive(Clone, Serialize, Deserialize)]
pub enum AuthScheme {
Basic,
Digest,
}
impl std::str::FromStr for AuthScheme {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"basic" => Ok(AuthScheme::Basic),
"digest" => Ok(AuthScheme::Digest),
_ => Err(format!("Invalid auth scheme: {}", s)),
}
}
}
impl Display for AuthScheme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuthScheme::Basic => write!(f, "Basic"),
AuthScheme::Digest => write!(f, "Digest"),
}
}
}
pub trait ReqwestAuth {
fn send_authed(self, scheme: &AuthScheme, username: &str, password: &str) -> impl std::future::Future<Output = anyhow::Result<reqwest::Response>> + Send;
}
impl ReqwestAuth for RequestBuilder {
fn send_authed(self, scheme: &AuthScheme, username: &str, password: &str) -> impl std::future::Future<Output = anyhow::Result<reqwest::Response>> + Send {
async move {
let res = match scheme {
AuthScheme::Basic => self.basic_auth(username, Some(password)).send().await?,
AuthScheme::Digest => self.send_with_digest_auth(username, password).await?,
};
Ok(res)
}
}
}
pub async fn request(
client: &Client,
method: Method,
url: Url,
body: String,
depth: u8,
credentials: &DAVCredentials,
) -> anyhow::Result<Element> {
let res = client
.request(method, url)
.header("Depth", depth.to_string())
.header(USER_AGENT, "TrafficCue")
.header(CONTENT_TYPE, "application/xml")
.body(body)
.send_authed(&credentials.scheme, &credentials.username, &credentials.password)
.await?;
let status = res.status();
if !status.is_success() {
Err(anyhow::anyhow!("Request failed with status: {}", status))?;
}
let text = res.text().await?;
let root: Element = text.parse()?;
Ok(root)
}
#[derive(Clone, Serialize, Deserialize)]
pub struct DAVCredentials {
pub scheme: AuthScheme,
pub username: String,
pub password: String,
}
impl DAVCredentials {
pub async fn find_scheme(url: &Url) -> anyhow::Result<AuthScheme> {
let client = Client::new();
let res = client
.request(Method::from_bytes(b"PROPFIND").expect("Invalid method"), url.clone())
.header(USER_AGENT, "TrafficCue")
.send()
.await;
if res.is_err() {
return Err(anyhow::anyhow!("Failed to connect to server"));
}
let res = res.unwrap();
if res.status().is_success() {
return Err(anyhow::anyhow!("Server did not require authentication"));
}
let headers = res.headers();
if let Some(www_auth) = headers.get("www-authenticate") {
let www_auth = www_auth.to_str().unwrap_or("");
if www_auth.to_lowercase().contains("digest") {
return Ok(AuthScheme::Digest);
} else if www_auth.to_lowercase().contains("basic") {
return Ok(AuthScheme::Basic);
}
}
Err(anyhow::anyhow!("Could not determine authentication scheme"))
}
}
/// Client for interacting with a CalDAV server.
/// ## Example
/// ```
/// let client = DAVClient::new("https://cal.example.com/.well-known/caldav", DAVCredentials {
/// username: "user".into(),
/// password: "pass".into(),
/// });
/// client.init().await?;
/// let calendars = client.get_calendars().await?;
/// for cal in calendars {
/// println!("{}", cal);
/// let events = cal.get_events(&client.credentials).await?;
/// for event in events {
/// println!("Event:\n{}", event);
/// }
/// }
/// ```
pub struct DAVClient {
pub url: Url,
pub credentials: DAVCredentials,
client: Client,
principal_url: Option<Url>,
cal_url: Option<Url>,
}
impl DAVClient {
/// **Ensure the URL is actually the CalDAV endpoint, and not just the base URL.**
pub fn new(string_url: &str, credentials: DAVCredentials) -> Self {
let url = Url::parse(string_url).expect("Invalid URL");
Self {
url,
credentials,
client: Client::new(),
principal_url: None,
cal_url: None,
}
}
pub async fn init(&mut self) -> anyhow::Result<()> {
self.get_principal().await?;
self.get_cal_homeset().await?;
Ok(())
}
pub async fn _propfind(
&mut self,
url: Url,
body: String,
depth: u8,
) -> anyhow::Result<Element> {
let method = Method::from_bytes(b"PROPFIND").expect("Invalid method");
let root = request(
&self.client,
method,
url,
body,
depth,
&self.credentials,
).await?;
Ok(root)
}
pub async fn get_principal(&mut self) -> anyhow::Result<()> {
let root: Element = self
._propfind(self.url.clone(), PRINCIPAL_BODY.to_string(), 0)
.await?;
let principal = find_elem(&root, "current-user-principal".to_string())
.ok_or(anyhow::anyhow!("No principal found"))?;
let principal_href =
find_elem(principal, "href".to_string()).ok_or(anyhow::anyhow!("No href found"))?;
let h_str = principal_href.text();
let mut p_url = self.url.clone();
p_url.set_path(&h_str);
self.principal_url = Some(p_url);
Ok(())
}
pub async fn get_cal_homeset(&mut self) -> anyhow::Result<()> {
if self.principal_url.is_none() {
return Err(anyhow::anyhow!("Principal URL not set"));
}
let root: Element = self
._propfind(
self.principal_url.clone().unwrap(),
HOMESET_BODY.to_string(),
0,
)
.await?;
let homeset = find_elem(&root, "calendar-home-set".to_string())
.ok_or(anyhow::anyhow!("No calendar-home-set found"))?;
let href =
find_elem(homeset, "href".to_string()).ok_or(anyhow::anyhow!("No href found"))?;
let h_str = href.text();
let mut cal_url = self.url.clone();
cal_url.set_path(&h_str);
self.cal_url = Some(cal_url.clone());
Ok(())
}
pub async fn get_calendars(&mut self) -> anyhow::Result<Vec<DAVCalendar>> {
if self.cal_url.is_none() {
return Err(anyhow::anyhow!("Calendar URL not set"));
}
let root: Element = self
._propfind(self.cal_url.clone().unwrap(), CAL_BODY.to_string(), 1)
.await?;
let reps = find_elems(&root, "response".to_string());
let mut calendars = Vec::new();
for rep in reps {
let displayname = find_elem(rep, "displayname".to_string())
.ok_or(anyhow::anyhow!("No displayname found"))?
.text();
if displayname == "" {
continue;
}
let resourcetype = find_elem(rep, "resourcetype".to_string())
.ok_or(anyhow::anyhow!("No resourcetype found"))?;
let is_calendar = find_elem(resourcetype, "calendar".to_string()).is_some();
if !is_calendar {
continue;
}
let url_base = self.cal_url.as_ref().unwrap();
let mut href = url_base.clone();
href.set_path(
&find_elem(rep, "href".to_string())
.ok_or(anyhow::anyhow!("No href found"))?
.text(),
);
calendars.push(DAVCalendar {
name: displayname,
url: href,
});
}
Ok(calendars)
}
}
/// Represents a CalDAV calendar.
/// Can be used to fetch events.
#[derive(Serialize, Deserialize, Clone)]
pub struct DAVCalendar {
pub name: String,
pub url: Url,
}
impl Display for DAVCalendar {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.name, self.url)
}
}
impl DAVCalendar {
pub fn new(name: &str, url: Url) -> Self {
Self {
name: name.to_string(),
url,
}
}
pub async fn _report(
&mut self,
credentials: &DAVCredentials,
body: String,
) -> anyhow::Result<Element> {
let client = Client::new();
let method = Method::from_bytes(b"REPORT").expect("Invalid method");
let root = request(
&client,
method,
self.url.clone(),
body,
1,
credentials,
).await?;
Ok(root)
}
pub async fn get_events(
&mut self,
credentials: &DAVCredentials,
) -> anyhow::Result<Vec<Calendar>> {
let mut events = Vec::new();
let time = chrono::Utc::now();
let start = time.format("%Y%m%dT000000Z").to_string();
let end = time.format("%Y%m%dT235959Z").to_string();
let body = EVENT_BODY.replace("{start}", &start).replace("{end}", &end);
let root = self._report(credentials, body).await?;
let datas = find_elems(&root, "calendar-data".to_string());
for data in datas {
let etext = data.text();
let cal: Calendar = etext.parse().unwrap();
events.push(cal);
}
Ok(events)
}
}

View File

@ -0,0 +1,64 @@
use chrono::Local;
use icalendar::{CalendarComponent, Component, DatePerhapsTime, EventLike};
use crate::dav::{DAVClient, DAVCalendar, DAVCredentials};
#[tauri::command]
pub async fn dav_find_scheme(url: &str) -> Result<String, String> {
let url = reqwest::Url::parse(url).map_err(|e| format!("Invalid URL: {}", e))?;
let scheme = DAVCredentials::find_scheme(&url).await.map_err(|e| format!("Failed to determine authentication scheme: {}", e))?;
Ok(scheme.to_string())
}
#[tauri::command]
pub async fn dav_fetch_calendars(url: &str, credentials: DAVCredentials) -> Result<Vec<DAVCalendar>, String> {
let mut client = DAVClient::new(url, credentials);
client.init().await.map_err(|e| format!("Failed to initialize DAV client: {}", e))?;
let calendars = client.get_calendars().await.map_err(|e| format!("Failed to fetch calendars: {}", e))?;
Ok(calendars)
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DAVEvent {
summary: String,
start: Option<String>,
end: Option<String>,
description: Option<String>,
location: Option<String>,
}
pub fn dateperhapstime_to_string(d: DatePerhapsTime) -> Option<String> {
match d {
DatePerhapsTime::DateTime(dt) => dt.try_into_utc(),
DatePerhapsTime::Date(d) => d
.and_hms_opt(0, 0, 0)
.and_then(|dt| dt.and_local_timezone(Local).earliest())
.map(|dt| dt.to_utc()),
}.map(|dt| dt.to_rfc3339())
}
#[tauri::command]
pub async fn dav_fetch_events(calendar: DAVCalendar, credentials: DAVCredentials) -> Result<Vec<DAVEvent>, String> {
let events = calendar.clone().get_events(&credentials).await.map_err(|e| format!("Failed to fetch events: {}", e))?;
let mut res: Vec<DAVEvent> = Vec::new();
for event in events {
for component in &event.components {
if let CalendarComponent::Event(event) = component {
let location = event.get_location();
if location.is_none() {
continue;
}
let start_at = event.get_start().and_then(dateperhapstime_to_string);
let end_at = event.get_end().and_then(dateperhapstime_to_string);
res.push(DAVEvent {
summary: event.get_summary().unwrap_or("No summary").to_string(),
start: start_at,
end: end_at,
description: event.get_description().map(|s| s.to_string()),
location: location.map(|s| s.to_string()),
});
}
}
}
Ok(res)
}

View File

@ -1,3 +1,6 @@
mod dav;
mod dav_commands;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@ -5,6 +8,7 @@ pub fn run() {
.plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_keep_screen_on::init())
.plugin(tauri_plugin_tts::init())
.plugin(tauri_plugin_duck::init())
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
@ -15,6 +19,7 @@ pub fn run() {
}
Ok(())
})
.invoke_handler(tauri::generate_handler![dav_commands::dav_find_scheme, dav_commands::dav_fetch_calendars, dav_commands::dav_fetch_events])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -21,6 +21,8 @@
import Input from "../ui/input/input.svelte";
import EvConnectorSelect from "./EVConnectorSelect.svelte";
import { m } from "$lang/messages";
import { localStore } from "$lib/services/localStore.svelte";
import { isDriving } from "./location.svelte";
let open = $state(false);
@ -52,11 +54,23 @@
fuelType: "diesel",
preferredFuel: "Diesel",
});
const biggerButtonsInDrive = localStore<boolean>(
"bigger-buttons-in-drive",
false,
);
const shouldUseLargeSize = $derived(
biggerButtonsInDrive.current ? isDriving() : false,
);
</script>
<Drawer.Root bind:open>
<Drawer.Trigger
class={buttonVariants({ variant: "secondary", class: "w-full p-5" })}
class={buttonVariants({
variant: "secondary",
class: "w-full",
size: shouldUseLargeSize ? "drive" : "default",
})}
>
{@render children()}
</Drawer.Trigger>

View File

@ -8,7 +8,6 @@
routing,
} from "$lib/services/navigation/routing.svelte";
import { location } from "./location.svelte";
import { saved } from "$lib/saved.svelte";
import RoutingLayers from "$lib/services/navigation/RoutingLayers.svelte";
import {
getPMTilesURL,
@ -20,6 +19,7 @@
import HazardMarker from "./HazardMarker.svelte";
import { hazards } from "./hazards.svelte";
import RequiresCapability from "./RequiresCapability.svelte";
import MapLocationMarkers from "./MapLocationMarkers.svelte";
onMount(() => {
window.addEventListener("resize", map.updateMapPadding);
@ -28,9 +28,6 @@
let locationDot: HTMLDivElement | undefined = $state();
let locationAccuracyCircle: HTMLDivElement | undefined = $state();
let homeMarker: HTMLImageElement | undefined = $state();
let workMarker: HTMLImageElement | undefined = $state();
let schoolMarker: HTMLImageElement | undefined = $state();
const DEBUG_POINTS = false; // Set to true to show debug points on the map
</script>
@ -45,6 +42,7 @@
bind:map={map.value}
bind:zoom={map.zoom}
padding={map.padding}
attributionControl={false}
onload={async () => {
map.updateMapPadding();
location.locked = true;
@ -129,58 +127,18 @@
<Marker
lnglat={{ lat: location.lat, lng: location.lng }}
element={locationDot}
rotationAlignment="map"
pitchAlignment="map"
/>
<Marker
lnglat={{ lat: location.lat, lng: location.lng }}
element={locationAccuracyCircle}
rotationAlignment="map"
pitchAlignment="map"
/>
{/if}
{#if saved.home}
<img
src={map.zoom > 9 ? "/img/saved/home.png" : "/img/saved/small.png"}
alt="Home Marker"
bind:this={homeMarker}
style="width: 32px;"
/>
<Marker
lnglat={{
lat: saved.home.lat,
lng: saved.home.lon,
}}
element={homeMarker}
/>
{/if}
{#if saved.school}
<img
src={map.zoom > 9 ? "/img/saved/school.png" : "/img/saved/small.png"}
alt="School Marker"
bind:this={schoolMarker}
style="width: 32px;"
/>
<Marker
lnglat={{
lat: saved.school.lat,
lng: saved.school.lon,
}}
element={schoolMarker}
/>
{/if}
{#if saved.work}
<img
src={map.zoom > 9 ? "/img/saved/work.png" : "/img/saved/small.png"}
alt="Work Marker"
bind:this={workMarker}
style="width: 32px;"
/>
<Marker
lnglat={{
lat: saved.work.lat,
lng: saved.work.lon,
}}
element={workMarker}
/>
{/if}
<MapLocationMarkers />
<RequiresCapability capability="hazards">
{#each hazards as hazard (hazard.latitude + "-" + hazard.longitude)}

View File

@ -0,0 +1,25 @@
<script lang="ts">
import { MAP_ICONS, type Location } from "$lib/saved.svelte";
import { Marker } from "svelte-maplibre-gl";
import { map } from "./map.svelte";
let { store }: { store: Location } = $props();
let marker: HTMLImageElement | undefined = $state();
</script>
<img
src={map.zoom > 9
? `/img/saved/${MAP_ICONS[store.icon ?? "small.png"]}`
: "/img/saved/small.png"}
alt="Work Marker"
bind:this={marker}
style="width: 32px;"
/>
<Marker
lnglat={{
lat: store.lat,
lng: store.lng,
}}
element={marker}
/>

View File

@ -0,0 +1,11 @@
<script lang="ts">
import type { Location } from "$lib/saved.svelte";
import { stores } from "$lib/services/stores.svelte";
import MapLocationMarker from "./MapLocationMarker.svelte";
const locations = stores<Location>("location");
</script>
{#each locations.current as location (location.data.lat + "-" + location.data.lng)}
<MapLocationMarker store={location.data} />
{/each}

View File

@ -14,6 +14,7 @@
import RequiresCapability from "./RequiresCapability.svelte";
import {
advertiseRemoteLocation,
getRoadMetadata,
location,
remoteLocation,
} from "./location.svelte";
@ -27,6 +28,7 @@
import Progressbar from "../Progressbar.svelte";
import { postHazard } from "$lib/services/lnv";
import { fetchHazards } from "./hazards.svelte";
import { getSpeed } from "$lib/services/TileMeta";
const views: Record<string, string> = {
main: "MainSidebar",
@ -44,6 +46,8 @@
"onboarding-vehicles": "onboarding/OnboardingVehiclesSidebar",
"nearby-poi": "NearbyPOISidebar",
licenses: "settings/LicensesSidebar",
calendar: "settings/CalendarSidebar",
appearance: "settings/AppearanceSidebar",
};
const fullscreen: Record<string, boolean> = {
@ -62,6 +66,8 @@
"onboarding-vehicles": true,
"nearby-poi": false,
licenses: true,
calendar: true,
appearance: true,
};
let isDragging = false;
@ -170,6 +176,47 @@
<Input class="h-10" placeholder="Search..." bind:value={searchbar.text} />
</div>
{/if}
{#if !hideSearch && !!location.speed != false}
<div
id="speedometer"
style="position: fixed; {mobileView
? `bottom: calc(50px + ${sidebarHeight.current}px + 10px); right: 10px;`
: 'bottom: 10px; right: 10px;'}"
>
{((location.speed * 3.6) | 0).toFixed(0)}
</div>
{/if}
{#if getRoadMetadata().current}
{@const meta = getRoadMetadata().current!}
{#if meta.maxspeed}
{@const maxspeed = getSpeed(meta.maxspeed)}
{#if maxspeed && maxspeed < 100}
<div
id="max-speed"
style="position: fixed; {mobileView
? `bottom: calc(50px + ${sidebarHeight.current}px + 10px + 2ch + 20px + 10px); right: 10px;`
: 'bottom: calc(10px + 2ch + 20px + 10px); right: 10px;'}"
>
{meta.maxspeed}
</div>
{/if}
{/if}
{#if meta.overtaking}
{@const overtaking = meta.overtaking}
{#if overtaking && overtaking == "no"}
<img
alt="No Overtaking"
src="/img/no-overtaking.png"
id="overtaking"
style="position: fixed; {mobileView
? `bottom: calc(50px + ${sidebarHeight.current}px + 10px + (2ch + 20px + 10px) * 2); right: 10px;`
: 'bottom: calc(10px + (2ch + 20px + 10px) * 2); right: 10px;'}"
/>
{/if}
{/if}
{/if}
<div
id="sidebar"
class={mobileView ? "mobileView" : ""}
@ -394,4 +441,32 @@
border-bottom-right-radius: 0;
padding-bottom: calc(40px + env(safe-area-inset-bottom));
}
#speedometer,
#max-speed,
#overtaking {
z-index: 30;
background-color: hsla(0, 0%, 5%, 0.6);
backdrop-filter: blur(5px);
aspect-ratio: 1 / 1;
font-size: 1.5rem;
font-weight: bold;
padding: 10px;
border-radius: 10px;
text-align: center;
width: calc(2ch + 20px);
height: calc(2ch + 20px);
}
#max-speed {
border-radius: 50%;
border: 5px solid #FA5959;
padding: 5px;
}
#overtaking {
padding: 0;
background: none;
backdrop-filter: none;
}
</style>

View File

@ -6,6 +6,7 @@
PlusCircleIcon,
TractorIcon,
TruckIcon,
XIcon,
} from "@lucide/svelte";
import Button, { buttonVariants } from "../ui/button/button.svelte";
import {
@ -17,6 +18,9 @@
} from "$lib/vehicles/vehicles.svelte";
import AddVehicleDrawer from "./AddVehicleDrawer.svelte";
import { m } from "$lang/messages";
import { updateStore } from "$lib/services/stores.svelte";
import { localStore } from "$lib/services/localStore.svelte";
import { isDriving } from "./location.svelte";
let open = $state(false);
@ -36,13 +40,25 @@
return TractorIcon; // Default icon if no match
}
}
const biggerButtonsInDrive = localStore<boolean>(
"bigger-buttons-in-drive",
false,
);
const shouldUseLargeSize = $derived(
biggerButtonsInDrive.current ? isDriving() : false,
);
</script>
<Drawer.Root bind:open>
<Drawer.Trigger
class={buttonVariants({ variant: "secondary", class: "w-full p-5" })}
class={buttonVariants({
variant: "secondary",
class: "w-full",
size: shouldUseLargeSize ? "drive" : "default",
})}
>
{@const vehicle = selectedVehicle() ?? DefaultVehicle}
{@const vehicle = selectedVehicle()?.data ?? DefaultVehicle}
{@const Icon = getVehicleIcon(vehicle.type)}
<Icon />
{vehicle.name}
@ -56,25 +72,42 @@
</Drawer.Header>
<div class="p-4 pt-0 flex flex-col gap-2">
{#each vehicles.current as vehicle (vehicle.name)}
<Button
variant={selectedVehicle() === vehicle.data ? "default" : "secondary"}
class="w-full p-5"
onclick={() => {
selectVehicle(vehicle.data);
open = false;
}}
>
{@const Icon = getVehicleIcon(vehicle.data.type)}
<Icon />
{vehicle.data.name}
</Button>
<div class="flex items-center gap-2">
<Button
variant={selectedVehicle()?.data === vehicle.data
? "default"
: "secondary"}
class="w-[calc(100%-48px-8px)]"
onclick={() => {
selectVehicle(vehicle.data);
open = false;
}}
>
{@const Icon = getVehicleIcon(vehicle.data.type)}
<Icon />
{vehicle.data.name}
</Button>
<Button
variant="destructive"
class="w-[48px]"
onclick={() => {
if (
!confirm(
"Are you sure you want to delete this vehicle? This action cannot be undone.",
)
)
return;
updateStore({ type: "vehicle", name: vehicle.name }, null);
}}
>
<XIcon />
</Button>
</div>
{/each}
<AddVehicleDrawer>
<Button variant="secondary" class="w-full p-5">
<PlusCircleIcon />
{m["vehicles.selector.add"]()}
</Button>
<PlusCircleIcon />
{m["vehicles.selector.add"]()}
</AddVehicleDrawer>
</div>
</Drawer.Content>

View File

@ -1,5 +1,8 @@
import { LNV_SERVER } from "$lib/services/hosts";
import { routing } from "$lib/services/navigation/routing.svelte";
import type { WrappedValue } from "$lib/services/stores.svelte";
import { getFeature } from "$lib/services/TileMeta";
import { lineString, nearestPointOnLine, point } from "@turf/turf";
import { map } from "./map.svelte";
export const location = $state({
@ -20,7 +23,7 @@ export const location = $state({
center: [location.lng, location.lat],
zoom: 16,
duration: 1000,
// bearing: location.heading !== null ? location.heading : undefined
bearing: location.heading != null ? location.heading : 0,
},
{
reason: "location",
@ -31,29 +34,139 @@ export const location = $state({
advertiser: null as WebSocket | null,
code: null as string | null,
lastUpdate: null as Date | null,
useSnapped: true,
});
const _isDriving = $derived(location.speed > 7 / 3.6);
export function isDriving() {
return _isDriving;
}
const roadMetadata: WrappedValue<GeoJSON.GeoJsonProperties> = $state({
current: null,
});
const roadFeature: WrappedValue<GeoJSON.Feature | null> = $state({
current: null,
});
const rawLocation: WrappedValue<WorldLocation | null> = $state({
current: null,
});
const snappedLocation: WrappedValue<WorldLocation | null> = $state({
current: null,
});
let lastFeatureId: string | null = null;
export function getRoadMetadata() {
return roadMetadata;
}
export function getRoadFeature() {
return roadFeature;
}
export function getSnappedLocation() {
return snappedLocation;
}
function snapLocation() {
const feature = roadFeature.current;
if (!feature) return;
if (!rawLocation.current) return;
if (feature.geometry.type === "LineString") {
const loc = nearestPointOnLine(
lineString(feature.geometry.coordinates),
point([rawLocation.current.lon, rawLocation.current.lat]),
);
snappedLocation.current = {
lat: loc.geometry.coordinates[1],
lon: loc.geometry.coordinates[0],
};
} else if (feature.geometry.type === "MultiLineString") {
// Find nearest point across all parts
let nearestLoc: GeoJSON.Feature<GeoJSON.Point> | null = null;
let minDist = Infinity;
for (const coords of feature.geometry.coordinates) {
const loc = nearestPointOnLine(
lineString(coords),
point([rawLocation.current.lon, rawLocation.current.lat]),
);
const dist = Math.hypot(
loc.geometry.coordinates[0] - rawLocation.current.lon,
loc.geometry.coordinates[1] - rawLocation.current.lat,
);
if (dist < minDist) {
minDist = dist;
nearestLoc = loc;
}
}
if (nearestLoc) {
snappedLocation.current = {
lat: nearestLoc.geometry.coordinates[1],
lon: nearestLoc.geometry.coordinates[0],
};
}
}
}
export function watchLocation() {
if (navigator.geolocation) {
navigator.geolocation.watchPosition(
(pos) => {
if (location.provider !== "gps") return;
// console.log("Geolocation update:", pos)
location.lat = pos.coords.latitude;
location.lng = pos.coords.longitude;
rawLocation.current = {
lat: pos.coords.latitude,
lon: pos.coords.longitude,
};
if (!location.useSnapped) {
location.lat = pos.coords.latitude;
location.lng = pos.coords.longitude;
}
location.accuracy = pos.coords.accuracy;
location.speed = pos.coords.speed || 0;
location.available = true;
location.heading = pos.coords.heading;
location.lastUpdate = new Date();
const blacklist = [
"path",
"track",
"raceway",
"busway",
"bus_guideway",
"ferry",
];
getFeature(
{ lat: rawLocation.current.lat, lon: rawLocation.current.lon },
"transportation",
{
filter: (f) => {
if (f.properties) {
return !blacklist.includes(f.properties["class"]);
}
return true;
},
lastId: lastFeatureId || undefined,
},
).then((feature) => {
roadFeature.current = feature;
roadMetadata.current = feature ? feature.properties : null;
lastFeatureId = feature ? String(feature.id) : null;
snapLocation();
if (location.useSnapped && snappedLocation.current) {
location.lat = snappedLocation.current.lat;
location.lng = snappedLocation.current.lon;
}
});
if (location.locked) {
map.value?.flyTo(
{
center: [location.lng, location.lat],
zoom: 16,
duration: 1000,
// bearing: location.heading !== null ? location.heading : undefined
bearing: location.heading != null ? location.heading : 0,
},
{
reason: "location",

View File

@ -0,0 +1,85 @@
<script lang="ts">
import Button from "$lib/components/ui/button/button.svelte";
import * as Card from "$lib/components/ui/card";
import {
fetchEvents,
type DAVCalendar,
type DAVCredentials,
type DAVEvent,
} from "$lib/services/CalDAV";
import { search } from "$lib/services/Search";
import { onMount } from "svelte";
import { map, pin } from "../map.svelte";
import { m } from "$lang/messages";
let events: DAVEvent[] = $state([]);
onMount(async () => {
const calendars = localStorage.getItem("calendars");
if (!calendars) return;
const parsedCalendars: (DAVCalendar & { credentials: DAVCredentials })[] =
JSON.parse(calendars);
for (const calendar of parsedCalendars) {
const calendarEvents = await fetchEvents(calendar, calendar.credentials);
events.push(...calendarEvents);
}
});
</script>
<div id="events">
{#each events as event ((event.start + "" || "") + (event.end + "" || ""))}
<Card.Root>
<Card.Header>
<Card.Title>{event.summary}</Card.Title>
{#if event.description}
<Card.Description>{event.description}</Card.Description>
{/if}
</Card.Header>
<Card.Content>
<p>
<strong>{m["calendar.start"]()}:</strong>
{event.start?.toLocaleString("de-DE") || m["calendar.no-start"]()}
</p>
<p>
<strong>{m["calendar.end"]()}:</strong>
{event.end?.toLocaleString("de-DE") || m["calendar.no-end"]()}
</p>
<p>
{#if event.location}
<strong>{m["calendar.location"]()}:</strong> {event.location}
{:else}
<strong>{m["calendar.location"]()}:</strong>
{m["calendar.no-location"]()}
{/if}
</p>
</Card.Content>
<Card.Footer>
<Button
onclick={async () => {
if (!event.location) return;
const features = await search(event.location, 0, 0);
if (features.length == 0) {
return void alert("Can't find location");
}
const lat = features[0].geometry.coordinates[1];
const lng = features[0].geometry.coordinates[0];
pin.dropPin(lat, lng);
pin.showInfo();
map.value?.flyTo({
center: [lng, lat],
zoom: 19,
});
}}>{m.open()}</Button
>
</Card.Footer>
</Card.Root>
{/each}
</div>
<style>
#events {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
</style>

View File

@ -0,0 +1,59 @@
<script lang="ts">
import { m } from "$lang/messages";
import Button from "$lib/components/ui/button/button.svelte";
import { circInOut } from "svelte/easing";
import { fly } from "svelte/transition";
import { map, pin } from "../map.svelte";
import { MapPinIcon } from "@lucide/svelte";
import { stores } from "$lib/services/stores.svelte";
import { ICONS, type Location } from "$lib/saved.svelte";
const locations = stores<Location>("location");
function getName(name: string) {
if (name == "home") {
return m["saved.home"]();
} else if (name == "work") {
return m["saved.work"]();
} else if (name == "school") {
return m["saved.school"]();
}
return name;
}
</script>
<div
id="saved"
class="mt-2 mb-2"
in:fly={{ y: 20, duration: 200, easing: circInOut }}
>
{#each locations.current as location (location.data.lat + "-" + location.data.lng)}
<Button
variant="secondary"
class="flex-1"
onclick={() => {
const { lat, lng } = location.data;
pin.dropPin(lat, lng);
pin.showInfo();
map.value?.flyTo({
center: [lng, lat],
zoom: 19,
});
}}
>
{@const Icon = ICONS[location.data.icon ?? "pin"] ?? MapPinIcon}
<Icon />
{getName(location.data.name)}
</Button>
{/each}
</div>
<style>
#saved {
display: flex;
gap: 0.5rem;
/* justify-content: space-evenly; */
width: 100%;
max-width: 100%;
}
</style>

View File

@ -22,12 +22,12 @@
import Reviews from "../info/Reviews.svelte";
import MapAi from "../info/MapAI.svelte";
import RequiresCapability from "../RequiresCapability.svelte";
import { saved, saveLocations } from "$lib/saved.svelte";
import { getDeveloperToggle } from "./settings/developer.svelte";
import InternetAccess from "../info/InternetAccess.svelte";
import RestaurantInfo from "../info/RestaurantInfo.svelte";
import { m } from "$lang/messages";
import { onMount } from "svelte";
import { updateStore } from "$lib/services/stores.svelte";
// let { feature }: { feature: Feature } = $props();
@ -99,6 +99,18 @@
>
{m["sidebar.info.dropped"]()}
</SidebarHeader>
<div id="actions">
<Button
onclick={() => {
view.switch("route", {
to: lat + "," + lng,
});
}}
>
<RouteIcon />
{m["sidebar.info.route"]()}
</Button>
</div>
<p>{m.loading()}</p>
{:then res}
{#if res.elements.length === 0}
@ -185,12 +197,15 @@
<Button
variant="outline"
onclick={() => {
// localStorage.setItem(
// "saved.home",
// JSON.stringify({ lat, lon: lng }),
// );
saved.home = { lat, lon: lng };
saveLocations();
updateStore(
{ name: "home", type: "location" },
{
lat,
lng,
name: "home",
icon: "home",
},
);
}}
>
<HomeIcon />
@ -199,8 +214,15 @@
<Button
variant="outline"
onclick={() => {
saved.school = { lat, lon: lng };
saveLocations();
updateStore(
{ name: "school", type: "location" },
{
lat,
lng,
name: "school",
icon: "school",
},
);
}}
>
<SchoolIcon />
@ -209,12 +231,15 @@
<Button
variant="outline"
onclick={() => {
// localStorage.setItem(
// "saved.work",
// JSON.stringify({ lat, lon: lng }),
// );
saved.work = { lat, lon: lng };
saveLocations();
updateStore(
{ name: "work", type: "location" },
{
lat,
lng,
name: "work",
icon: "work",
},
);
}}
>
<BriefcaseIcon />
@ -292,6 +317,18 @@
>
Dropped Pin
</SidebarHeader>
<div id="actions">
<Button
onclick={() => {
view.switch("route", {
to: lat + "," + lng,
});
}}
>
<RouteIcon />
{m["sidebar.info.route"]()}
</Button>
</div>
<p>Error: {err.message}</p>
{/await}

View File

@ -1,108 +1,18 @@
<script lang="ts">
import {
BriefcaseIcon,
DownloadIcon,
FuelIcon,
HomeIcon,
ParkingSquareIcon,
SchoolIcon,
} from "@lucide/svelte";
import { DownloadIcon, FuelIcon, ParkingSquareIcon } from "@lucide/svelte";
import { Button } from "../../ui/button";
import { fly } from "svelte/transition";
import { circInOut } from "svelte/easing";
import { map, pin } from "../map.svelte";
import VehicleSelector from "../VehicleSelector.svelte";
import Post from "../Post.svelte";
import RequiresCapability from "../RequiresCapability.svelte";
import { saved } from "$lib/saved.svelte";
import { m } from "$lang/messages";
import { view } from "../view.svelte";
import * as Card from "$lib/components/ui/card";
import SavedRoutes from "../main/SavedRoutes.svelte";
import SavedLocations from "../main/SavedLocations.svelte";
import Calendar from "../main/Calendar.svelte";
</script>
<div
id="saved"
class="mt-2 mb-2"
in:fly={{ y: 20, duration: 200, easing: circInOut }}
>
<Button
variant="secondary"
class="flex-1"
onclick={() => {
const loc = saved.home;
if (!loc) {
alert(m["saved.no-location"]({ name: m["saved.home"]() }));
return;
}
const { lat, lon } = loc;
if (!lat || !lon) {
alert(m["saved.no-location"]({ name: m["saved.home"]() }));
return;
}
pin.dropPin(lat, lon);
pin.showInfo();
map.value?.flyTo({
center: [lon, lat],
zoom: 19,
});
}}
>
<HomeIcon />
{m["saved.home"]()}
</Button>
<Button
variant="secondary"
class="flex-1"
onclick={() => {
console.log(saved);
const loc = saved.school;
if (!loc) {
alert(m["saved.no-location"]({ name: m["saved.school"]() }));
return;
}
const { lat, lon } = loc;
if (!lat || !lon) {
alert(m["saved.no-location"]({ name: m["saved.school"]() }));
return;
}
pin.dropPin(lat, lon);
pin.showInfo();
map.value?.flyTo({
center: [lon, lat],
zoom: 19,
});
}}
>
<SchoolIcon />
{m["saved.school"]()}
</Button>
<Button
variant="secondary"
class="flex-1"
onclick={() => {
const loc = saved.work;
if (!loc) {
alert(m["saved.no-location"]({ name: m["saved.work"]() }));
return;
}
const { lat, lon } = loc;
if (!lat || !lon) {
alert(m["saved.no-location"]({ name: m["saved.work"]() }));
return;
}
pin.dropPin(lat, lon);
pin.showInfo();
map.value?.flyTo({
center: [lon, lat],
zoom: 19,
});
}}
>
<BriefcaseIcon />
{m["saved.work"]()}
</Button>
</div>
<SavedLocations />
<VehicleSelector />
@ -112,7 +22,6 @@
>
<Button
variant="secondary"
size="lg"
style="flex: 1;"
onclick={() => {
view.switch("nearby-poi", {
@ -126,11 +35,10 @@
</Button>
<Button
variant="secondary"
size="lg"
style="flex: 1;"
onclick={() => {
view.switch("nearby-poi", {
tags: "amenity=parking",
tags: "amenity=parking,parking!=street_side",
});
}}
>
@ -140,6 +48,8 @@
</Button>
</div>
<Calendar />
{#if !window.__TAURI__}
<Card.Root style="margin-top: 1rem;">
<Card.Header>
@ -170,13 +80,3 @@
<Post />
</div>
</RequiresCapability>
<style>
#saved {
display: flex;
gap: 0.5rem;
/* justify-content: space-evenly; */
width: 100%;
max-width: 100%;
}
</style>

View File

@ -18,7 +18,7 @@
return;
}
view.loading = true;
fetchNearbyPOI(location.lat, location.lng, tags.split(","), 500).then(
fetchNearbyPOI(location.lat, location.lng, tags.split(","), 5000).then(
(results) => {
pois = results.elements.sort((a, b) => {
const distA = distanceTo(
@ -89,7 +89,7 @@
});
}}
>
<div class="font-bold">
<div class="font-bold text-wrap">
{poi.tags.name ?? poi.tags.brand ?? m.unnamed()}
</div>
<div class="text-sm">

View File

@ -17,7 +17,7 @@
selectedVehicle,
} from "$lib/vehicles/vehicles.svelte";
import { location } from "../location.svelte";
import { saved } from "$lib/saved.svelte";
import { savedLocations } from "$lib/saved.svelte";
import { m } from "$lang/messages";
import LocationSelect from "../LocationSelect.svelte";
@ -74,8 +74,13 @@
const FROM: WorldLocation =
fromLocation == "current"
? { lat: location.lat, lon: location.lng }
: saved[fromLocation]
? saved[fromLocation]
: savedLocations.current.find((s) => s.name == fromLocation)
? {
lat: savedLocations.current.find((s) => s.name == fromLocation)!
.data.lat,
lon: savedLocations.current.find((s) => s.name == fromLocation)!
.data.lng,
}
: {
lat: parseFloat(fromLocation.split(",")[0]),
lon: parseFloat(fromLocation.split(",")[1]),
@ -83,16 +88,21 @@
const TO: WorldLocation =
toLocation == "current"
? { lat: location.lat, lon: location.lng }
: saved[toLocation]
? saved[toLocation]
: savedLocations.current.find((s) => s.name == toLocation)
? {
lat: savedLocations.current.find((s) => s.name == fromLocation)!
.data.lat,
lon: savedLocations.current.find((s) => s.name == fromLocation)!
.data.lng,
}
: {
lat: parseFloat(toLocation.split(",")[0]),
lon: parseFloat(toLocation.split(",")[1]),
};
const req = createValhallaRequest(selectedVehicle() ?? DefaultVehicle, [
FROM,
TO,
]);
const req = createValhallaRequest(
selectedVehicle()?.data ?? DefaultVehicle,
[FROM, TO],
);
const res = await fetchRoute(ROUTING_SERVER, req);
routes = [res.trip];
if (res.alternates) {

View File

@ -5,7 +5,13 @@
import { getAuthURL, getOIDCUser } from "$lib/services/oidc";
import * as Avatar from "$lib/components/ui/avatar";
import { m } from "$lang/messages";
import { refreshToken, uploadID } from "$lib/services/lnv";
import {
fetchMyUser,
followUser,
refreshToken,
unfollowUser,
uploadID,
} from "$lib/services/lnv";
interface OIDCUser {
sub: string;
@ -25,6 +31,8 @@
);
}
});
let testInput = "";
</script>
{#if !user}
@ -74,6 +82,26 @@
refreshToken();
}}>refresh</button
>
{#await fetchMyUser() then u}
<span><b>Followers:</b> {u.followers}</span>
<span><b>Following:</b> {u.following}</span>
<span><b>Reviews:</b> {u.reviewsCount}</span>
<span><b>Hazards:</b> {u.hazardsCount}</span>
{/await}
<input type="text" bind:value={testInput} />
<button
onclick={async () => {
alert(await followUser(testInput).catch(alert));
}}>Follow</button
>
<button
onclick={async () => {
alert(await unfollowUser(testInput).catch(alert));
}}>Unfollow</button
>
<pre>{user.sub}</pre>
{JSON.stringify(user, null, 2)}
{/if}

View File

@ -16,7 +16,7 @@
<div id="languages">
{#each locales as locale, _index (locale)}
<SettingsButton
text={m.language({}, { locale })}
text={m["language.name"]({}, { locale })}
icon={LanguagesIcon}
onclick={() => {
setOnboardingState("vehicles");

View File

@ -12,10 +12,8 @@
</h1>
<AddVehicleDrawer>
<Button variant="secondary" class="w-full p-5">
<PlusCircleIcon />
{m["vehicles.selector.add"]()}
</Button>
<PlusCircleIcon />
{m["vehicles.selector.add"]()}
</AddVehicleDrawer>
<Button

View File

@ -0,0 +1,21 @@
<script>
import { m } from "$lang/messages";
import { BeakerIcon } from "@lucide/svelte";
import { location } from "../../location.svelte";
import SidebarHeader from "../SidebarHeader.svelte";
import SettingsToggle from "./SettingsToggle.svelte";
</script>
<SidebarHeader>
{m["sidebar.appearance.header"]()}
</SidebarHeader>
<SettingsToggle
text={m["sidebar.appearance.bigger-buttons-in-drive"]()}
localStorageKey="bigger-buttons-in-drive"
/>
<SettingsToggle
text="Use snapped location (experimental)"
bind:value={location.useSnapped}
icon={BeakerIcon}
/>

View File

@ -0,0 +1,179 @@
<script lang="ts">
import { m } from "$lang/messages";
import { CalendarPlusIcon } from "@lucide/svelte";
import SidebarHeader from "../SidebarHeader.svelte";
import SettingsButton from "./SettingsButton.svelte";
import * as Drawer from "$lib/components/ui/drawer";
import { Button } from "$lib/components/ui/button";
import Input from "$lib/components/ui/input/input.svelte";
import {
fetchCalendars,
findScheme,
type AuthScheme,
type DAVCalendar,
type DAVCredentials,
} from "$lib/services/CalDAV";
import { onMount } from "svelte";
let calendars: (DAVCalendar & { credentials: DAVCredentials })[] = $state([]);
let calDavDrawerOpen = $state(false);
let calDavLoading = $state(false);
let calDavUrl = $state("");
let calDavUsername = $state("");
let calDavPassword = $state("");
let calDavCalendars: DAVCalendar[] = $state([]);
let calDavScheme: AuthScheme;
let calDavState = $state("");
async function fetchCalDav() {
calDavState = m["sidebar.calendar.probing-server"]();
calDavScheme = await findScheme(calDavUrl);
calDavState = m["sidebar.calendar.discovering-calendars"]();
calDavCalendars = await fetchCalendars(calDavUrl, {
scheme: calDavScheme,
username: calDavUsername,
password: calDavPassword,
});
calDavState = "";
}
onMount(() => {
if (localStorage.getItem("calendars")) {
calendars = JSON.parse(localStorage.getItem("calendars")!);
}
});
</script>
<SidebarHeader>
{m["sidebar.calendar.header"]()}
</SidebarHeader>
<Drawer.Root bind:open={calDavDrawerOpen}>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>{m["sidebar.calendar.connect"]()}</Drawer.Title>
</Drawer.Header>
<div class="p-4 pt-0 flex flex-col gap-2">
{#if calDavCalendars}
<Input
type="url"
placeholder="https://my-caldav-server.com/..."
disabled={calDavLoading}
bind:value={calDavUrl}
/>
<Input
type="text"
placeholder="Username"
disabled={calDavLoading}
bind:value={calDavUsername}
/>
<Input
type="password"
placeholder="Password"
disabled={calDavLoading}
bind:value={calDavPassword}
/>
{#if calDavState}
<span>{calDavState}</span>
{/if}
{/if}
{#if calDavCalendars.length > 0}
<h3 class="font-medium pt-4">{m["sidebar.calendar.choose"]()}</h3>
<ul class="max-h-48 overflow-y-auto">
{#each calDavCalendars as calendar (calendar.url)}
<li>
<Button
class="w-full"
variant="secondary"
onclick={() => {
if (localStorage.getItem("calendars")) {
const existing = JSON.parse(
localStorage.getItem("calendars")!,
);
existing.push({
name: calendar.name,
url: calendar.url,
credentials: {
username: calDavUsername,
password: calDavPassword,
scheme: calDavScheme,
},
});
localStorage.setItem("calendars", JSON.stringify(existing));
} else {
localStorage.setItem(
"calendars",
JSON.stringify([
{
name: calendar.name,
url: calendar.url,
credentials: {
username: calDavUsername,
password: calDavPassword,
scheme: calDavScheme,
},
},
]),
);
}
calendars.push({
name: calendar.name,
url: calendar.url,
credentials: {
username: calDavUsername,
password: calDavPassword,
scheme: calDavScheme,
},
});
}}
>
{calendar.name}
</Button>
</li>
{/each}
</ul>
{/if}
</div>
<Drawer.Footer>
{#if calDavCalendars.length === 0}
<Button
onclick={async () => {
calDavLoading = true;
await fetchCalDav().catch((e) => {
calDavState = e;
calDavLoading = false;
});
}}>{m.submit()}</Button
>
{/if}
<Drawer.Close>
{calDavCalendars.length === 0 ? m.done() : m.cancel()}
</Drawer.Close>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Root>
{#each calendars as calendar (calendar.url)}
<div class="p-2 border rounded mb-2">
<h3 class="font-medium">{calendar.name}</h3>
<Button
variant="destructive"
size="sm"
class="mt-2"
onclick={() => {
calendars = calendars.filter((c) => c.url !== calendar.url);
localStorage.setItem("calendars", JSON.stringify(calendars));
}}>{m.delete()}</Button
>
</div>
{/each}
<SettingsButton
text={m["sidebar.calendar.connect"]()}
icon={CalendarPlusIcon}
onclick={() => {
calDavDrawerOpen = true;
}}
/>

View File

@ -1,5 +1,6 @@
<script>
import {
CalendarSearchIcon,
CloudUploadIcon,
HandIcon,
MapIcon,
@ -24,6 +25,7 @@
syncStores,
updateStore,
} from "$lib/services/stores.svelte";
import { invoke } from "@tauri-apps/api/core";
const dev = getDeveloperToggle();
@ -54,6 +56,51 @@
await downloadPMTiles(url, name);
}}
/>
<SettingsButton
icon={CalendarSearchIcon}
text="Fetch calendars"
onclick={async () => {
const url = prompt("URL?");
if (!url) return;
const scheme = await invoke("dav_find_scheme", { url }).catch((err) => {
alert("Error fetching scheme: " + err);
});
alert("Found scheme: " + scheme);
const username = prompt("Username?");
const password = prompt("Password?");
if (!username || !password) return;
const credentials = { scheme, username, password };
invoke("dav_fetch_calendars", { url, credentials })
.then((calendars) => {
alert("Fetched calendars: " + JSON.stringify(calendars));
})
.catch((err) => {
alert("Error fetching calendars: " + err);
});
}}
/>
<SettingsButton
icon={CalendarSearchIcon}
text="Fetch CalDAV events"
onclick={async () => {
const url = prompt("URL?");
const scheme = prompt("Scheme? (basic, digest)");
const username = prompt("Username?");
const password = prompt("Password?");
if (!url || !username || !password) return;
const credentials = { scheme, username, password };
invoke("dav_fetch_events", {
calendar: { name: "Calendar", url },
credentials,
})
.then((events) => {
alert("Fetched events: " + JSON.stringify(events));
})
.catch((err) => {
alert("Error fetching events: " + err);
});
}}
/>
</section>
<section>

View File

@ -13,10 +13,10 @@
<div id="languages">
{#each locales as locale, _index (locale)}
{#if locale == getLocale()}
<SettingsButton text={m.language()} icon={CheckIcon} disabled />
<SettingsButton text={m["language.name"]()} icon={CheckIcon} disabled />
{:else}
<SettingsButton
text={m.language({}, { locale })}
text={m["language.name"]({}, { locale })}
icon={LanguagesIcon}
onclick={() => {
setLocale(locale);

View File

@ -24,7 +24,7 @@
<Button
variant="secondary"
style="width: 100%; height: 40px;"
style="width: 100%;"
{disabled}
onclick={() => {
if (viewName) view.switch(viewName);

View File

@ -1,5 +1,6 @@
<script>
import {
CalendarIcon,
CodeIcon,
InfoIcon,
LanguagesIcon,
@ -24,6 +25,20 @@
text={m["sidebar.language.header"]()}
view="language"
/>
<SettingsButton
icon={PaintbrushIcon}
text={m["sidebar.appearance.header"]()}
view="appearance"
/>
</section>
<section>
<h2>{m["sidebar.settings.connections"]()}</h2>
<SettingsButton
icon={CalendarIcon}
text={m["sidebar.calendar.header"]()}
view="calendar"
/>
</section>
<section>

View File

@ -0,0 +1,52 @@
<script lang="ts">
import type { IconProps } from "@lucide/svelte";
import type { Component } from "svelte";
import Switch from "$lib/components/ui/switch/switch.svelte";
import { Label } from "$lib/components/ui/label";
import { eventTarget } from "$lib/services/localStore.svelte";
let {
icon: Icon,
text,
onchange,
disabled,
localStorageKey,
value = $bindable(
localStorageKey
? localStorage.getItem(localStorageKey) === "true"
: false,
),
}: {
icon?: Component<IconProps>;
text: string;
onchange?: () => void;
disabled?: boolean;
value?: boolean;
localStorageKey?: string;
} = $props();
</script>
<div class="flex">
<Label style="width: 100%;" for="settings-toggle">
{#if Icon}
<Icon />
{/if}
{text}
</Label>
<Switch
{disabled}
bind:checked={value}
onCheckedChange={() => {
if (onchange) onchange();
if (localStorageKey) {
localStorage.setItem(localStorageKey, value ? "true" : "false");
eventTarget.dispatchEvent(
new CustomEvent("localStorageChanged", {
detail: { key: localStorageKey, value },
}),
);
}
}}
id="settings-toggle"
/>
</div>

View File

@ -1,4 +1,6 @@
<script lang="ts" module>
import { isDriving } from "$lib/components/lnv/location.svelte";
import { localStore } from "$lib/services/localStore.svelte";
import { cn, type WithElementRef } from "$lib/utils.js";
import type {
HTMLAnchorAttributes,
@ -6,6 +8,14 @@
} from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
const biggerButtonsInDrive = localStore<boolean>(
"bigger-buttons-in-drive",
false,
);
const shouldUseLargeSize = $derived(
biggerButtonsInDrive.current ? isDriving() : false,
);
export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
@ -27,6 +37,7 @@
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
drive: "h-12 rounded-md px-8 has-[>svg]:px-6",
},
},
defaultVariants: {
@ -63,7 +74,10 @@
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
class={cn(
buttonVariants({ variant, size: shouldUseLargeSize ? "drive" : size }),
className,
)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? "link" : undefined}
@ -76,7 +90,10 @@
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
class={cn(
buttonVariants({ variant, size: shouldUseLargeSize ? "drive" : size }),
className,
)}
{type}
{disabled}
{...restProps}

View File

@ -0,0 +1,7 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};

View File

@ -0,0 +1,20 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: LabelPrimitive.RootProps = $props();
</script>
<LabelPrimitive.Root
bind:ref
data-slot="label"
class={cn(
"flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
className,
)}
{...restProps}
/>

View File

@ -0,0 +1,7 @@
import Root from "./switch.svelte";
export {
Root,
//
Root as Switch,
};

View File

@ -0,0 +1,29 @@
<script lang="ts">
import { Switch as SwitchPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
checked = $bindable(false),
...restProps
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
</script>
<SwitchPrimitive.Root
bind:ref
bind:checked
data-slot="switch"
class={cn(
"data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 shadow-xs peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent outline-none transition-all focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...restProps}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
class={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitive.Root>

View File

@ -1,20 +1,57 @@
import type { Component } from "svelte";
import { reverseGeocode } from "./services/Search";
import {
BriefcaseIcon,
HomeIcon,
MapPinIcon,
SchoolIcon,
type IconProps,
} from "@lucide/svelte";
import { stores, storeSnapshot } from "./services/stores.svelte";
/**
* @deprecated Use stores instead.
*/
export const saved: Record<string, WorldLocation> = $state(
JSON.parse(localStorage.getItem("saved") ?? "{}"),
);
export const savedLocations = stores<Location>("location");
/**
* @deprecated Use stores instead.
*/
export function saveLocations() {
localStorage.setItem("saved", JSON.stringify(saved));
}
export async function geocode(name: string) {
const loc = saved[name];
const loc = await storeSnapshot<Location>({ name, type: "location" });
if (!loc) return;
const geocode = await reverseGeocode(loc);
const geocode = await reverseGeocode({ lat: loc.lat, lon: loc.lng });
if (geocode.length == 0) {
return;
}
const feature = geocode[0];
return `${feature.properties.street}${feature.properties.housenumber ? " " + feature.properties.housenumber : ""}, ${feature.properties.city}`;
}
export const ICONS: Record<string, Component<IconProps>> = {
home: HomeIcon,
work: BriefcaseIcon,
school: SchoolIcon,
pin: MapPinIcon,
};
export const MAP_ICONS: Record<string, string> = {
home: "home.png",
school: "school.png",
work: "work.png",
}; // TODO: add generic pin icon
export interface Location {
lat: number;
lng: number;
name: string;
icon?: string;
}

View File

@ -0,0 +1,47 @@
import { invoke } from "@tauri-apps/api/core";
export type AuthScheme = "Digest" | "Basic";
export interface DAVCredentials {
scheme: AuthScheme;
username: string;
password: string;
}
export interface DAVCalendar {
name: string;
url: string;
}
export interface DAVEvent {
summary: string;
start?: Date;
end?: Date;
description?: string;
location?: string;
}
export async function findScheme(url: string): Promise<AuthScheme> {
const scheme = await invoke("dav_find_scheme", { url });
return scheme as AuthScheme;
}
export async function fetchCalendars(
url: string,
credentials: DAVCredentials,
): Promise<DAVCalendar[]> {
const calendars = await invoke("dav_fetch_calendars", { url, credentials });
return calendars as DAVCalendar[];
}
export async function fetchEvents(
calendar: DAVCalendar,
credentials: DAVCredentials,
): Promise<DAVEvent[]> {
const events = await invoke("dav_fetch_events", { calendar, credentials });
return (events as DAVEvent[]).map((e) => {
if (e.start) e.start = new Date(e.start);
if (e.end) e.end = new Date(e.end);
return e;
}) as DAVEvent[];
}

View File

@ -47,6 +47,8 @@ export async function fetchPOI(lat: number, lon: number, radius: number) {
(
node(around:${radius}, ${lat}, ${lon})["amenity"]["name"];
way(around:${radius}, ${lat}, ${lon})["amenity"]["name"];
node(around:${radius}, ${lat}, ${lon})["tourism"]["name"];
way(around:${radius}, ${lat}, ${lon})["tourism"]["name"];
relation(around:${radius}, ${lat}, ${lon})["amenity"]["name"];
node(around:${radius}, ${lat}, ${lon})["shop"]["name"];
way(around:${radius}, ${lat}, ${lon})["shop"]["name"];

View File

@ -0,0 +1,171 @@
import { lineString, pointToLineDistance } from "@turf/turf";
import { FSSource, hasPMTiles } from "./OfflineTiles";
import { PMTiles } from "pmtiles";
import { VectorTile } from "@mapbox/vector-tile";
import Protobuf from "pbf";
import { location } from "$lib/components/lnv/location.svelte";
function getFeatureDistance(f: GeoJSON.Feature, point: [number, number]) {
if (f.geometry.type === "LineString") {
return pointToLineDistance(point, lineString(f.geometry.coordinates), {
units: "meters",
});
} else if (f.geometry.type === "MultiLineString") {
// Compute the min distance across all parts
return Math.min(
...f.geometry.coordinates.map((coords) =>
pointToLineDistance(point, lineString(coords), { units: "meters" }),
),
);
} else {
return Infinity;
}
}
interface GetFeatureOptions {
lastId?: string;
filter?: (feature: GeoJSON.Feature) => boolean;
}
function getBias() {
if (!location.speed) return 5;
return Math.max(5, Math.min(15, location.speed * 0.5)); // Bias increases with speed, min 5, max 15, 0.5 per km/h
}
export async function getFeature(
coord: WorldLocation,
layer: string,
{ lastId, filter }: GetFeatureOptions = {},
) {
const zxy = coordToTile(coord, 14);
const tile = await fetchTile(zxy.z, zxy.x, zxy.y);
const layerData = tile.layers[layer];
if (!layerData) return null;
const features: GeoJSON.Feature[] = [];
for (let i = 0; i < layerData.length; i++) {
const feature = layerData.feature(i);
features.push(feature.toGeoJSON(zxy.x, zxy.y, zxy.z));
}
const filtered = features
.filter(
(f) =>
f.geometry.type === "LineString" ||
f.geometry.type == "MultiLineString",
)
.filter((f) => (filter ? filter(f) : true));
if (filtered.length === 0) return null;
const nearest = filtered.reduce((a, b) => {
let distA = getFeatureDistance(a, [coord.lon, coord.lat]);
let distB = getFeatureDistance(b, [coord.lon, coord.lat]);
const STAY_BIAS = getBias();
// TODO: don't bias if feature is continuing straight (split ways vs turn)
if (lastId && String(a.id) == lastId) {
distB += STAY_BIAS;
}
if (lastId && String(b.id) == lastId) {
distA += STAY_BIAS;
}
return distA < distB ? a : b;
});
return nearest;
}
export async function getMeta(
coord: WorldLocation,
layer: string,
options?: GetFeatureOptions,
) {
const nearest = await getFeature(coord, layer, options);
return nearest ? nearest.properties : null;
}
const IMPLICIT_SPEEDS: Record<string, number> = {
"DE:urban": 50,
"DE:rural": 100, // TODO: 80 (hgv weight > 3.5t, or trailer), 60 (weight > 7.5t)
"DE:living_street": 7,
"DE:bicycle_road": 30,
};
export function getSpeed(maxspeed: string): number | null {
if (!isNaN(parseInt(maxspeed))) return parseInt(maxspeed);
if (maxspeed.endsWith(" mph")) {
const val = parseInt(maxspeed.replace(" mph", ""));
if (!isNaN(val)) return Math.round(val * 1.60934); // Convert to km/h
}
if (maxspeed === "walk") return 7; // https://wiki.openstreetmap.org/wiki/Proposed_features/maxspeed_walk
return IMPLICIT_SPEEDS[maxspeed as keyof typeof IMPLICIT_SPEEDS] || null;
}
function coordToTile(coord: WorldLocation, zoom: number) {
const z = zoom;
const x = Math.floor(((coord.lon + 180) / 360) * Math.pow(2, z));
const y = Math.floor(
((1 -
Math.log(
Math.tan((coord.lat * Math.PI) / 180) +
1 / Math.cos((coord.lat * Math.PI) / 180),
) /
Math.PI) /
2) *
Math.pow(2, z),
);
return { z, x, y };
}
class Cache<T> {
data: Map<string, T>;
size: number;
constructor(size: number) {
this.data = new Map<string, T>();
this.size = size;
}
get(key: string): T | undefined {
if (this.data.has(key)) {
const value = this.data.get(key)!;
this.data.delete(key);
this.data.set(key, value);
return value;
}
return this.data.get(key);
}
has(key: string): boolean {
return this.data.has(key);
}
set(key: string, value: T): void {
this.data.set(key, value);
if (this.data.size > this.size) {
const firstKey = this.data.keys().next().value;
if (firstKey) {
this.data.delete(firstKey);
}
}
}
}
const tileCache = new Cache<VectorTile>(5);
export async function fetchTile(z: number, x: number, y: number) {
const cacheKey = `${z}/${x}/${y}`;
if (tileCache.has(cacheKey)) {
return tileCache.get(cacheKey)!;
}
const pmtiles = (await hasPMTiles("tiles"))
? new PMTiles(new FSSource("tiles"))
: new PMTiles("https://trafficcue-tiles.picoscratch.de/germany.pmtiles");
const tile = await pmtiles.getZxy(z, x, y);
if (!tile) {
throw new Error(`Tile not found: z${z} x${x} y${y}`);
}
const data = tile.data;
const vectorTile = new VectorTile(new Protobuf(data));
tileCache.set(cacheKey, vectorTile);
return vectorTile;
}

View File

@ -9,4 +9,4 @@ export const LNV_SERVER =
? "http://localhost:3000/api"
: location.hostname.includes("staging")
? "https://staging-trafficcue-api.picoscratch.de/api"
: "https://trafficcue-api.picoscratch.de/api";
: "https://staging-trafficcue-api.picoscratch.de/api";

View File

@ -280,3 +280,48 @@ export async function ping() {
const res = await fetch(LNV_SERVER + "/ping").catch(() => ({ ok: false }));
return res.ok;
}
export interface UserInfo {
username: string;
createdAt: string;
reviewsCount: number;
hazardsCount: number;
followers: number;
following: number;
}
export async function fetchMyUser(): Promise<UserInfo> {
const res = await authFetch(LNV_SERVER + "/user/me");
if (!res.ok) {
throw new Error(`Failed to fetch user: ${res.statusText}`);
}
return await res.json();
}
export async function followUser(username: string) {
const res = await authFetch(LNV_SERVER + "/user/follow", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username }),
});
if (!res.ok) {
throw new Error(`Failed to follow user: ${res.statusText}`);
}
return await res.json();
}
export async function unfollowUser(username: string) {
const res = await authFetch(LNV_SERVER + "/user/unfollow", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username }),
});
if (!res.ok) {
throw new Error(`Failed to unfollow user: ${res.statusText}`);
}
return await res.json();
}

View File

@ -0,0 +1,34 @@
import type { WrappedValue } from "$lib/services/stores.svelte";
export const eventTarget = new EventTarget();
export function localStore<T>(key: string, defaultValue: T): WrappedValue<T> {
const storedValue = localStorage.getItem(key);
const initialValue =
storedValue !== null ? JSON.parse(storedValue) : defaultValue;
const state = $state<WrappedValue<T>>({ current: initialValue });
eventTarget.addEventListener("localStorageChanged", (event) => {
const customEvent = event as CustomEvent;
console.log("localStorageChanged event received", customEvent.detail);
if (customEvent.detail.key === key) {
const newValue = customEvent.detail.value as T;
console.log(`localStore: ${key} updated from another tab`, newValue);
if (JSON.stringify(state.current) === JSON.stringify(newValue)) return;
state.current = newValue;
}
});
return {
get current() {
return state.current;
},
set current(newValue: T) {
state.current = newValue;
eventTarget.dispatchEvent(
new CustomEvent("localStorageChanged", {
detail: { key, value: newValue },
}),
);
localStorage.setItem(key, JSON.stringify(newValue));
},
};
}

View File

@ -1,5 +1,7 @@
import { duck, unduck } from "tauri-plugin-duck-api";
import { invoke } from "@tauri-apps/api/core";
import { m } from "$lang/messages";
import { locales, type Locale } from "$lang/runtime";
export let tts: "tauri" | "web" | null = null;
@ -13,25 +15,39 @@ export async function initTTS() {
}
}
export default async function say(text: string) {
export function findLocaleForValhallaLanguage(language: string) {
for (const locale of locales) {
if (m["language.valhalla"]({ language }, { locale }) === language) {
return locale;
}
}
}
export default async function say(text: string, language?: Locale) {
if (!tts) {
// alert("TTS not initialized");
// console.error("TTS not initialized");
await initTTS();
// return;
}
duck();
await duck();
await new Promise((resolve) => setTimeout(resolve, 500)); // wait a bit for ducking to take effect
if (tts !== "web") {
try {
await invoke("plugin:tts|speak", { text });
await invoke("plugin:tts|speak", {
text,
language: language
? m["language.tts"]({}, { locale: language })
: m["language.tts"](),
});
} catch (e) {
console.error("Error speaking text", e);
alert(e);
}
} else {
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = "de-DE";
utterance.lang = m["language.speechSynthesis"]();
window.speechSynthesis.speak(utterance);
}
unduck();
await unduck();
}

View File

@ -1,10 +1,12 @@
import { location } from "$lib/components/lnv/location.svelte";
import { map } from "$lib/components/lnv/map.svelte";
import say from "./TTS";
import say, { findLocaleForValhallaLanguage } from "./TTS";
import type { ValhallaRequest } from "./ValhallaRequest";
import type { LngLatBoundsLike } from "maplibre-gl";
import { generateVoiceGuidance } from "./VoiceGuidance";
import { keepScreenOn } from "tauri-plugin-keep-screen-on-api";
import { m } from "$lang/messages";
import { getFeature, getSpeed } from "../TileMeta";
export const routing = $state({
geojson: {
@ -149,6 +151,28 @@ export async function startRoute(trip: Trip) {
let hasAnnouncedPreInstruction = false;
const USE_LANDMARK_INSTRUCTIONS = false;
let maneuverSpeed = { idx: -1, speed: 50 };
async function getSpeedForManeuver(idx: number) {
if (maneuverSpeed.idx === idx) {
return maneuverSpeed.speed;
}
if (!routing.currentTrip) return 50;
const esi = routing.currentTrip!.legs[0].maneuvers[idx].end_shape_index;
const shape = routing.currentTrip!.legs[0].shape;
const polyline = decodePolyline(shape);
const point = polyline[esi];
const feature = await getFeature(point, "transportation");
if (!feature || !feature.properties) return 50;
const speed = getSpeed(feature.properties.maxspeed || "50") || 50;
maneuverSpeed = { idx, speed };
return speed;
}
async function tickRoute() {
const trip = routing.currentTrip;
const info = routing.currentTripInfo;
@ -177,17 +201,17 @@ async function tickRoute() {
}
// Check if the user is on the route
if (!isOnShape(loc, polyline)) {
if (!isOnShape(loc, polyline, 30)) {
console.log("Off route!");
if (!info.isOffRoute) {
say("You went off route.");
say(m["routing.off-route"]());
}
info.isOffRoute = true;
// TODO: Implement re-routing logic
return;
} else {
if (info.isOffRoute) {
say("You are back on route.");
say(m["routing.back-on-route"]());
}
info.isOffRoute = false;
}
@ -200,7 +224,7 @@ async function tickRoute() {
// console.log("Distance to end of current maneuver: ", distanceToEnd, " meters");
// console.log("Speed: ", location.speed, " km/h");
const verbalDistance = verbalPreInstructionDistance(
location.speed || 50, // Assuming location has a speed property
await getSpeedForManeuver(routing.currentTripInfo.maneuverIdx),
);
if (distanceToEnd <= verbalDistance) {
hasAnnouncedPreInstruction = true;
@ -212,7 +236,8 @@ async function tickRoute() {
// currentManeuver.verbal_pre_transition_instruction,
instruction,
);
say(instruction);
const locale = findLocaleForValhallaLanguage(trip.language);
say(instruction, locale);
}
}
@ -244,7 +269,8 @@ async function tickRoute() {
"[Verbal instruction] ",
currentManeuver.verbal_post_transition_instruction,
);
say(currentManeuver.verbal_post_transition_instruction);
const locale = findLocaleForValhallaLanguage(trip.language);
say(currentManeuver.verbal_post_transition_instruction, locale);
}
}
@ -265,7 +291,7 @@ async function tickRoute() {
}
function verbalPreInstructionDistance(speed: number): number {
return Math.min(speed, 30) * 2.222 + 37.144;
return (Math.min(speed, 30) * 2.222 + 37.144) * 2;
}
export function stopNavigation() {
@ -300,9 +326,10 @@ function isOnLine(
location: WorldLocation,
from: WorldLocation,
to: WorldLocation,
toleranceMeters = 12,
) {
// Convert the 12-meter tolerance to degrees (approximation)
const tolerance = 12 / 111320; // 1 degree latitude ≈ 111.32 km
// Convert the tolerance to degrees (approximation)
const tolerance = toleranceMeters / 111320; // 1 degree latitude ≈ 111.32 km
// Calculate the vector components
const dx = to.lon - from.lon;
@ -344,10 +371,13 @@ function isOnPoint(location: WorldLocation, point: WorldLocation) {
return distance <= tolerance;
}
function isOnShape(location: WorldLocation, shape: WorldLocation[]) {
// Check if the location is on the line between from and to (3 meter tolerance)
function isOnShape(
location: WorldLocation,
shape: WorldLocation[],
toleranceMeters = 12,
) {
for (let i = 0; i < shape.length - 1; i++) {
if (isOnLine(location, shape[i], shape[i + 1])) {
if (isOnLine(location, shape[i], shape[i + 1], toleranceMeters)) {
return true;
}
}

View File

@ -203,6 +203,18 @@ export async function hasStore(info: StoreInfo) {
return store !== undefined;
}
export async function storeSnapshot<T extends object>(info: StoreInfo) {
const db = await getDB();
const store = await db.getFromIndex("stores", "by-name-and-type", [
info.name,
info.type,
]);
if (!store) {
return null;
}
return JSON.parse(store.data) as T;
}
// export async function store<T extends object>(info: StoreInfo) {
// const store = await db.getFromIndex("stores", "by-name-and-type", [info.name, info.type]);
// if (!store) {

View File

@ -1,3 +1,4 @@
import { m } from "$lang/messages";
import type {
ValhallaCosting,
ValhallaCostingOptions,
@ -63,7 +64,7 @@ export function createValhallaRequest(
costing,
units: "kilometers",
alternates: 2,
language: "de-DE",
language: m["language.valhalla"](),
costing_options: costingOptions,
turn_lanes: true,
};

View File

@ -78,16 +78,11 @@ interface StateValue<T> {
v: T;
}
export const vehicles: WrappedValue<StoreValue<Vehicle>[]> = stores("vehicle");
export const selectedVehicleIdx: StateValue<number | null> = $state({
v: localStorage.getItem("selectedVehicle")
? parseInt(localStorage.getItem("selectedVehicle")!)
: null,
export const selectedVehicleId: StateValue<string | null> = $state({
v: localStorage.getItem("selectedVehicle") ?? null,
});
export const selectedVehicle: () => Vehicle | null = () => {
return (
vehicles.current[selectedVehicleIdx.v !== null ? selectedVehicleIdx.v : 0]
?.data ?? null
);
export const selectedVehicle: () => StoreValue<Vehicle> | null = () => {
return vehicles.current.find((v) => v.id === selectedVehicleId.v) ?? null;
};
// export function setVehicles(_vehicles: Vehicle[]) {
@ -104,18 +99,14 @@ export async function addVehicle(vehicle: Vehicle) {
export function selectVehicle(vehicle: Vehicle | null) {
if (vehicle == null) {
selectedVehicleIdx.v = null;
selectedVehicleId.v = null;
} else {
selectedVehicleIdx.v = vehicles.current.findIndex(
(v) => v.name === vehicle.name,
);
if (selectedVehicleIdx.v === -1) {
selectedVehicleIdx.v = null;
}
selectedVehicleId.v =
vehicles.current.find((v) => v.name === vehicle.name)?.id ?? null;
}
localStorage.setItem(
"selectedVehicle",
selectedVehicleIdx.v !== null ? selectedVehicleIdx.v.toString() : "",
selectedVehicleId.v !== null ? selectedVehicleId.v.toString() : "",
);
}