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;
}