import { LNV_SERVER } from "$lib/services/hosts"; import { routing } from "$lib/services/navigation/routing.svelte"; import type { WrappedValue } from "$lib/services/stores.svelte"; import { getFeature } from "$lib/services/TileMeta"; import { lineString, nearestPointOnLine, point } from "@turf/turf"; import { map } from "./map.svelte"; export const location = $state({ available: false, lat: 0, lng: 0, accuracy: 0, speed: 0, heading: null as number | null, provider: "gps" as "gps" | "remote" | "simulated", locked: true, toggleLock: () => { location.locked = !location.locked; console.log("Location lock toggled:", location.locked); if (location.locked) { map.value?.flyTo( { center: [location.lng, location.lat], zoom: 16, duration: 1000, bearing: location.heading != null ? location.heading : 0, }, { reason: "location", }, ); } }, advertiser: null as WebSocket | null, code: null as string | null, lastUpdate: null as Date | null, useSnapped: true, }); const _isDriving = $derived(location.speed > 7 / 3.6); export function isDriving() { return _isDriving; } const roadMetadata: WrappedValue = $state({ current: null, }); const roadFeature: WrappedValue = $state({ current: null, }); const rawLocation: WrappedValue = $state({ current: null, }); const snappedLocation: WrappedValue = $state({ current: null, }); let lastFeatureId: string | null = null; export function getRoadMetadata() { return roadMetadata; } export function getRoadFeature() { return roadFeature; } export function getSnappedLocation() { return snappedLocation; } function snapLocation() { const feature = roadFeature.current; if (!feature) return; if (!rawLocation.current) return; if (feature.geometry.type === "LineString") { const loc = nearestPointOnLine( lineString(feature.geometry.coordinates), point([rawLocation.current.lon, rawLocation.current.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([rawLocation.current.lon, rawLocation.current.lat]), ); const dist = Math.hypot( loc.geometry.coordinates[0] - rawLocation.current.lon, loc.geometry.coordinates[1] - rawLocation.current.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( (pos) => { if (location.provider !== "gps") return; // console.log("Geolocation update:", pos) rawLocation.current = { lat: pos.coords.latitude, lon: pos.coords.longitude, }; if (!location.useSnapped) { location.lat = pos.coords.latitude; location.lng = pos.coords.longitude; } location.accuracy = pos.coords.accuracy; location.speed = pos.coords.speed || 0; location.available = true; location.heading = pos.coords.heading; location.lastUpdate = new Date(); const blacklist = [ "path", "track", "raceway", "busway", "bus_guideway", "ferry", ]; getFeature( { lat: rawLocation.current.lat, lon: rawLocation.current.lon }, "transportation", { filter: (f) => { if (f.properties) { return !blacklist.includes(f.properties["class"]); } return true; }, lastId: lastFeatureId || undefined, }, ).then((feature) => { roadFeature.current = feature; roadMetadata.current = feature ? feature.properties : null; lastFeatureId = feature ? String(feature.id) : null; snapLocation(); if (location.useSnapped && snappedLocation.current) { location.lat = snappedLocation.current.lat; location.lng = snappedLocation.current.lon; } }); if (location.locked) { map.value?.flyTo( { center: [location.lng, location.lat], zoom: 16, duration: 1000, bearing: location.heading != null ? location.heading : 0, }, { reason: "location", }, ); } // console.log(location.advertiser); if (location.advertiser) { location.advertiser.send( JSON.stringify({ type: "location", location: { lat: location.lat, lng: location.lng, accuracy: location.accuracy, speed: location.speed, heading: location.heading, }, route: { trip: routing.currentTrip, info: routing.currentTripInfo, geojson: routing.geojson, }, }), ); } }, (err) => { console.error("Geolocation error:", err); }, { enableHighAccuracy: true, }, ); } } let checkRunning = false; if (!checkRunning) { setInterval(() => { checkRunning = true; if (location.provider !== "gps") return; // If the last update was more than 5 seconds ago, recall watchPosition // console.log("Checking location update status") if ( location.lastUpdate && new Date().getTime() - location.lastUpdate.getTime() > 10000 ) { console.warn("Location update is stale, rewatching position"); watchLocation(); } }, 1000); checkRunning = true; } watchLocation(); export function advertiseRemoteLocation(code?: string) { const ws = new WebSocket( `${LNV_SERVER.replace("https", "wss").replace("http", "ws")}/ws`, ); ws.addEventListener("open", () => { console.log( "WebSocket connection established for remote location advertisement", ); ws.send(JSON.stringify({ type: "advertise", code })); }); ws.addEventListener("message", (event) => { const data = JSON.parse(event.data); console.log("WebSocket message received:", data); if (data.type === "advertising") { console.log("Advertising code:", data.code); // alert(`You are now advertising your location with code: ${data.code}`); location.locked = true; // Lock the location when advertising location.advertiser = ws; // Store the WebSocket for sending location updates location.code = data.code; // Store the advertising code } else if (data.type === "error") { console.error("WebSocket error:", data.message); } }); } export function remoteLocation(code: string) { // Open websocket connection // Use LNV_SERVER, change to ws or wss based on protocol const ws = new WebSocket( `${LNV_SERVER.replace("https", "wss").replace("http", "ws")}/ws`, ); ws.addEventListener("open", () => { console.log("WebSocket connection established for remote location"); ws.send(JSON.stringify({ type: "subscribe", code })); location.provider = "remote"; location.code = code; }); ws.addEventListener("message", (event) => { const data = JSON.parse(event.data); if (data.type === "location") { console.log("Remote location update:", data.location); location.lat = data.location.lat; location.lng = data.location.lng; location.accuracy = data.location.accuracy; location.speed = data.location.speed || 0; location.available = true; location.heading = data.location.heading || null; routing.currentTrip = data.route.trip || null; routing.currentTripInfo = data.route.info || null; routing.geojson = data.route.geojson || null; if (location.locked) { map.value?.flyTo( { center: [location.lng, location.lat], zoom: 16, duration: 1000, // bearing: location.heading !== null ? location.heading : undefined }, { reason: "location", }, ); } } }); } // setInterval(() => { // // Move location towards heading (if available) at speed // // if (location.provider !== "simulated" || location.locked) return; // if (location.heading !== null && location.speed > 0) { // const rad = (location.heading * Math.PI) / 180 // const distance = location.speed / 3600 // Convert speed from km/h to km/s // location.lat += (distance * Math.cos(rad)) / 111.32 // Approximate conversion from degrees to km // location.lng += (distance * Math.sin(rad)) / (111.32 * Math.cos((location.lat * Math.PI) / 180)) // Adjust for longitude // location.accuracy = 5 // Simulated accuracy // location.available = true // map.value?.flyTo({ // center: [location.lng, location.lat], // zoom: 16, // duration: 1000 // }) // } // }, 100)