diff --git a/bun.lock b/bun.lock index 159989d..e918094 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "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", @@ -19,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", @@ -206,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=="], @@ -1000,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=="], @@ -1222,8 +1224,14 @@ "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=="], @@ -1242,6 +1250,12 @@ "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=="], diff --git a/package.json b/package.json index f96ed30..907cb28 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "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", @@ -55,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", diff --git a/src/lib/components/lnv/Sidebar.svelte b/src/lib/components/lnv/Sidebar.svelte index 7044a90..1f2f184 100644 --- a/src/lib/components/lnv/Sidebar.svelte +++ b/src/lib/components/lnv/Sidebar.svelte @@ -182,8 +182,8 @@ {(location.speed * 3.6 | 0).toFixed(0)} {/if} -{#if getRoadMetadata()} - {@const meta = getRoadMetadata()!} +{#if getRoadMetadata().current} + {@const meta = getRoadMetadata().current!} {#if meta.maxspeed} {@const maxspeed = getSpeed(meta.maxspeed)} {#if maxspeed && maxspeed < 100} diff --git a/src/lib/components/lnv/location.svelte.ts b/src/lib/components/lnv/location.svelte.ts index 6b089d3..3e9417e 100644 --- a/src/lib/components/lnv/location.svelte.ts +++ b/src/lib/components/lnv/location.svelte.ts @@ -1,6 +1,7 @@ import { LNV_SERVER } from "$lib/services/hosts"; import { routing } from "$lib/services/navigation/routing.svelte"; -import { getTransportationMeta } from "$lib/services/TileMeta"; +import type { WrappedValue } from "$lib/services/stores.svelte"; +import { getMeta } from "$lib/services/TileMeta"; import { map } from "./map.svelte"; export const location = $state({ @@ -40,7 +41,7 @@ export function isDriving() { return _isDriving; } -const roadMetadata = $derived(getTransportationMeta({ lat: location.lat, lon: location.lng })); +const roadMetadata: WrappedValue = $state({ current: null }); export function getRoadMetadata() { return roadMetadata; @@ -59,6 +60,10 @@ export function watchLocation() { location.available = true; location.heading = pos.coords.heading; location.lastUpdate = new Date(); + + getMeta({ lat: location.lat, lon: location.lng }, "transportation").then((meta) => { + roadMetadata.current = meta; + }); if (location.locked) { map.value?.flyTo( diff --git a/src/lib/services/TileMeta.ts b/src/lib/services/TileMeta.ts index 3c845ad..a55f26f 100644 --- a/src/lib/services/TileMeta.ts +++ b/src/lib/services/TileMeta.ts @@ -1,5 +1,8 @@ -import { map } from "$lib/components/lnv/map.svelte"; 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"; function getFeatureDistance(f: GeoJSON.Feature, point: [number, number]) { if (f.geometry.type === "LineString") { @@ -16,11 +19,17 @@ function getFeatureDistance(f: GeoJSON.Feature, point: [number, number]) { } } -export function getTransportationMeta(coord: WorldLocation) { - if(!map.value) return null; - const features = map.value.querySourceFeatures("openmaptiles", { - sourceLayer: "transportation" - }); +export async function getMeta(coord: WorldLocation, layer: string) { + 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"); if(filtered.length === 0) return null; const nearest = filtered.reduce((a, b) => { @@ -47,3 +56,74 @@ export function getSpeed(maxspeed: string): number | null { 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 { + data: Map; + size: number; + + constructor(size: number) { + this.data = new Map(); + 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(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) { + console.log(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; +}