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]); console.log("lastId:", lastId, "a.id:", a.id, "b.id:", b.id); const STAY_BIAS = getBias(); if (lastId && String(a.id) == lastId) { console.log("Applying stay bias to B"); distB += STAY_BIAS; } if (lastId && String(b.id) == lastId) { console.log("Applying stay bias to A"); distA += STAY_BIAS; } console.log("Distances:", distA, distB); return distA < distB ? a : b; }); console.log("ID: ", nearest.id); 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 = { "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 { 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; }