diff --git a/src/lib/components/lnv/FullscreenMap.svelte b/src/lib/components/lnv/FullscreenMap.svelte index a6945ff..d8027a3 100644 --- a/src/lib/components/lnv/FullscreenMap.svelte +++ b/src/lib/components/lnv/FullscreenMap.svelte @@ -7,7 +7,7 @@ decodePolyline, routing, } from "$lib/services/navigation/routing.svelte"; - import { location } from "./location.svelte"; + import { getSnappedLocation, location } from "./location.svelte"; import RoutingLayers from "$lib/services/navigation/RoutingLayers.svelte"; import { getPMTilesURL, @@ -137,6 +137,15 @@ pitchAlignment="map" /> {/if} + {#if getSnappedLocation().current} + + {/if} diff --git a/src/lib/components/lnv/location.svelte.ts b/src/lib/components/lnv/location.svelte.ts index e9fb25b..f645cb8 100644 --- a/src/lib/components/lnv/location.svelte.ts +++ b/src/lib/components/lnv/location.svelte.ts @@ -1,7 +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 { getMeta } from "$lib/services/TileMeta"; +import { getFeature, getMeta } from "$lib/services/TileMeta"; +import { lineString, nearestPointOnLine, point } from "@turf/turf"; import { map } from "./map.svelte"; export const location = $state({ @@ -44,11 +45,59 @@ export function isDriving() { const roadMetadata: WrappedValue = $state({ current: null, }); +const roadFeature: WrappedValue = $state({ + current: null, +}); +const snappedLocation: WrappedValue = $state({ + current: null, +}); export function getRoadMetadata() { return roadMetadata; } +export function getRoadFeature() { + return roadFeature; +} + +export function getSnappedLocation() { + return snappedLocation; +} + +function snapLocation() { + const feature = roadFeature.current; + console.log("Snapping location to road feature:", feature); + if (!feature) return; + if (feature.geometry.type === "LineString") { + const loc = nearestPointOnLine(lineString(feature.geometry.coordinates), point([location.lng, location.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 | null = null; + let minDist = Infinity; + for (const coords of feature.geometry.coordinates) { + const loc = nearestPointOnLine(lineString(coords), point([location.lng, location.lat])); + const dist = Math.hypot( + loc.geometry.coordinates[0] - location.lng, + loc.geometry.coordinates[1] - location.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( @@ -63,12 +112,32 @@ export function watchLocation() { location.heading = pos.coords.heading; location.lastUpdate = new Date(); + const blacklist = ["footway", "platform"]; getMeta( { lat: location.lat, lon: location.lng }, "transportation", + (f) => { + if(f.properties) { + return !blacklist.includes(f.properties["subclass"]); + } + return true; + } ).then((meta) => { roadMetadata.current = meta; }); + getFeature( + { lat: location.lat, lon: location.lng }, + "transportation", + (f) => { + if(f.properties) { + return !blacklist.includes(f.properties["subclass"]); + } + return true; + } + ).then((feature) => { + roadFeature.current = feature; + snapLocation(); + }); if (location.locked) { map.value?.flyTo( diff --git a/src/lib/services/TileMeta.ts b/src/lib/services/TileMeta.ts index f8b59ae..4de3692 100644 --- a/src/lib/services/TileMeta.ts +++ b/src/lib/services/TileMeta.ts @@ -19,7 +19,7 @@ function getFeatureDistance(f: GeoJSON.Feature, point: [number, number]) { } } -export async function getMeta(coord: WorldLocation, layer: string) { +export async function getFeature(coord: WorldLocation, layer: string, filter?: (feature: GeoJSON.Feature) => boolean) { const zxy = coordToTile(coord, 14); const tile = await fetchTile(zxy.z, zxy.x, zxy.y); const layerData = tile.layers[layer]; @@ -33,13 +33,18 @@ export async function getMeta(coord: WorldLocation, layer: string) { 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) => { const distA = getFeatureDistance(a, [coord.lon, coord.lat]); const distB = getFeatureDistance(b, [coord.lon, coord.lat]); return distA < distB ? a : b; }); + return nearest; +} + +export async function getMeta(coord: WorldLocation, layer: string, filter?: (feature: GeoJSON.Feature) => boolean) { + const nearest = await getFeature(coord, layer, filter); return nearest ? nearest.properties : null; }