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

This commit is contained in:
2025-10-21 15:33:31 +02:00
parent 4ef2667c4e
commit 621691277e
5 changed files with 114 additions and 13 deletions

View File

@@ -182,8 +182,8 @@
{(location.speed * 3.6 | 0).toFixed(0)}
</div>
{/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}

View File

@@ -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<GeoJSON.GeoJsonProperties> = $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(

View File

@@ -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<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) {
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;
}