This repository has been archived on 2025-11-09. You can view files and clone it, but cannot push or open issues or pull requests.
Files
trafficcue-client/src/lib/components/lnv/location.svelte.ts
Jannik 729cc22b37
Some checks failed
TrafficCue CI / check (push) Successful in 1m35s
TrafficCue CI / build-android (push) Has been cancelled
TrafficCue CI / build (push) Has been cancelled
refactor: clean up logs
2025-10-25 20:09:46 +02:00

317 lines
8.6 KiB
TypeScript

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<GeoJSON.GeoJsonProperties> = $state({
current: null,
});
const roadFeature: WrappedValue<GeoJSON.Feature | null> = $state({
current: null,
});
const rawLocation: WrappedValue<WorldLocation | null> = $state({
current: null,
});
const snappedLocation: WrappedValue<WorldLocation | null> = $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<GeoJSON.Point> | 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)