feat: location marker with remote location support
This commit is contained in:
@ -10,7 +10,7 @@
|
|||||||
Protocol,
|
Protocol,
|
||||||
} from "svelte-maplibre-gl";
|
} from "svelte-maplibre-gl";
|
||||||
import { view } from "./sidebar.svelte";
|
import { view } from "./sidebar.svelte";
|
||||||
import { geolocate, map, pin } from "./map.svelte";
|
import { map, pin } from "./map.svelte";
|
||||||
import {
|
import {
|
||||||
drawAllRoutes,
|
drawAllRoutes,
|
||||||
fetchRoute,
|
fetchRoute,
|
||||||
@ -18,11 +18,15 @@
|
|||||||
} from "$lib/services/navigation/routing.svelte";
|
} from "$lib/services/navigation/routing.svelte";
|
||||||
import { createValhallaRequest } from "$lib/vehicles/ValhallaVehicles";
|
import { createValhallaRequest } from "$lib/vehicles/ValhallaVehicles";
|
||||||
import { ROUTING_SERVER } from "$lib/services/hosts";
|
import { ROUTING_SERVER } from "$lib/services/hosts";
|
||||||
|
import { location } from "./location.svelte";
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener("resize", map.updateMapPadding);
|
window.addEventListener("resize", map.updateMapPadding);
|
||||||
map.updateMapPadding();
|
map.updateMapPadding();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let locationDot: HTMLDivElement | undefined = $state();
|
||||||
|
let locationAccuracyCircle: HTMLDivElement | undefined = $state();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Protocol
|
<Protocol
|
||||||
@ -60,6 +64,7 @@
|
|||||||
padding={map.padding}
|
padding={map.padding}
|
||||||
onload={async () => {
|
onload={async () => {
|
||||||
map.updateMapPadding();
|
map.updateMapPadding();
|
||||||
|
location.locked = true;
|
||||||
}}
|
}}
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
if (view.current.type == "main" || view.current.type == "info") {
|
if (view.current.type == "main" || view.current.type == "info") {
|
||||||
@ -67,9 +72,15 @@
|
|||||||
pin.showInfo();
|
pin.showInfo();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onmove={(e) => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (e.reason !== "location") {
|
||||||
|
location.locked = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<!-- <Hash /> -->
|
<!-- <Hash /> -->
|
||||||
<GeolocateControl
|
<!-- <GeolocateControl
|
||||||
positionOptions={{
|
positionOptions={{
|
||||||
enableHighAccuracy: true,
|
enableHighAccuracy: true,
|
||||||
}}
|
}}
|
||||||
@ -84,7 +95,7 @@
|
|||||||
};
|
};
|
||||||
// $inspect(`Geolocation: ${e.coords.latitude}, ${e.coords.longitude} (Speed: ${speed} km/h, Accuracy: ${accuracy} m)`);
|
// $inspect(`Geolocation: ${e.coords.latitude}, ${e.coords.longitude} (Speed: ${speed} km/h, Accuracy: ${accuracy} m)`);
|
||||||
}}
|
}}
|
||||||
/>
|
/> -->
|
||||||
{#if pin.isDropped}
|
{#if pin.isDropped}
|
||||||
<Marker lnglat={{ lat: pin.lat, lng: pin.lng }} />
|
<Marker lnglat={{ lat: pin.lat, lng: pin.lng }} />
|
||||||
{/if}
|
{/if}
|
||||||
@ -177,4 +188,17 @@
|
|||||||
></LineLayer>
|
></LineLayer>
|
||||||
</GeoJSONSource>
|
</GeoJSONSource>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if location.available}
|
||||||
|
<div class="maplibregl-user-location-dot" bind:this={locationDot}></div>
|
||||||
|
<div class="maplibregl-user-location-accuracy-circle" bind:this={locationAccuracyCircle}></div>
|
||||||
|
<Marker
|
||||||
|
lnglat={{ lat: location.lat, lng: location.lng }}
|
||||||
|
element={locationDot}
|
||||||
|
/>
|
||||||
|
<Marker
|
||||||
|
lnglat={{ lat: location.lat, lng: location.lng }}
|
||||||
|
element={locationAccuracyCircle}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</MapLibre>
|
</MapLibre>
|
||||||
|
|||||||
@ -8,10 +8,12 @@
|
|||||||
import { map } from "./map.svelte";
|
import { map } from "./map.svelte";
|
||||||
import TripSidebar from "./sidebar/TripSidebar.svelte";
|
import TripSidebar from "./sidebar/TripSidebar.svelte";
|
||||||
import Input from "../ui/input/input.svelte";
|
import Input from "../ui/input/input.svelte";
|
||||||
import { HomeIcon, SettingsIcon, UserIcon } from "@lucide/svelte";
|
import { EllipsisIcon, HomeIcon, SettingsIcon, UserIcon } from "@lucide/svelte";
|
||||||
import Button from "../ui/button/button.svelte";
|
import Button from "../ui/button/button.svelte";
|
||||||
import { search, type Feature } from "$lib/services/Search";
|
import { search, type Feature } from "$lib/services/Search";
|
||||||
import SearchSidebar from "./sidebar/SearchSidebar.svelte";
|
import SearchSidebar from "./sidebar/SearchSidebar.svelte";
|
||||||
|
import { advertiseRemoteLocation, location, remoteLocation } from "./location.svelte";
|
||||||
|
import * as Popover from "../ui/popover";
|
||||||
|
|
||||||
const views: {[key: string]: Component<any>} = {
|
const views: {[key: string]: Component<any>} = {
|
||||||
main: MainSidebar,
|
main: MainSidebar,
|
||||||
@ -112,6 +114,40 @@
|
|||||||
<button>
|
<button>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</button>
|
</button>
|
||||||
|
<!-- <button onclick={() => {
|
||||||
|
location.toggleLock();
|
||||||
|
}}>
|
||||||
|
L
|
||||||
|
</button> -->
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger>
|
||||||
|
<button>
|
||||||
|
<EllipsisIcon />
|
||||||
|
</button>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Button variant="outline" onclick={() => {
|
||||||
|
location.toggleLock();
|
||||||
|
}}>
|
||||||
|
{location.locked ? "Unlock Location" : "Lock Location"}
|
||||||
|
</Button>
|
||||||
|
{#if location.code}
|
||||||
|
<span>Advertise code: {location.code}</span>
|
||||||
|
{/if}
|
||||||
|
<Button variant="outline" onclick={() => {
|
||||||
|
advertiseRemoteLocation();
|
||||||
|
}}>
|
||||||
|
Advertise Location
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onclick={() => {
|
||||||
|
remoteLocation(prompt("Code?") || "");
|
||||||
|
}}>
|
||||||
|
Join Remote Location
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
183
src/lib/components/lnv/location.svelte.ts
Normal file
183
src/lib/components/lnv/location.svelte.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import { LNV_SERVER } from "$lib/services/hosts"
|
||||||
|
import { routing } from "$lib/services/navigation/routing.svelte"
|
||||||
|
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 : undefined
|
||||||
|
}, {
|
||||||
|
reason: "location"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
advertiser: null as WebSocket | null,
|
||||||
|
code: null as string | null,
|
||||||
|
lastUpdate: null as Date | null
|
||||||
|
})
|
||||||
|
|
||||||
|
export function watchLocation() {
|
||||||
|
if(navigator.geolocation) {
|
||||||
|
navigator.geolocation.watchPosition((pos) => {
|
||||||
|
if(location.provider !== "gps") return;
|
||||||
|
// console.log("Geolocation update:", pos)
|
||||||
|
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()
|
||||||
|
|
||||||
|
if (location.locked) {
|
||||||
|
map.value?.flyTo({
|
||||||
|
center: [location.lng, location.lat],
|
||||||
|
zoom: 16,
|
||||||
|
duration: 1000,
|
||||||
|
// bearing: location.heading !== null ? location.heading : undefined
|
||||||
|
}, {
|
||||||
|
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)
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { reverseGeocode } from "$lib/services/Search";
|
import { reverseGeocode } from "$lib/services/Search";
|
||||||
import { view } from "./sidebar.svelte";
|
import { view } from "./sidebar.svelte";
|
||||||
|
|
||||||
export const geolocate = $state({
|
// export const geolocate = $state({
|
||||||
currentLocation: null as WorldLocation | null,
|
// currentLocation: null as WorldLocation | null,
|
||||||
})
|
// })
|
||||||
|
|
||||||
export const map = $state({
|
export const map = $state({
|
||||||
value: undefined as maplibregl.Map | undefined,
|
value: undefined as maplibregl.Map | undefined,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { geolocate, map } from "$lib/components/lnv/map.svelte";
|
import { location } from "$lib/components/lnv/location.svelte";
|
||||||
|
import { map } from "$lib/components/lnv/map.svelte";
|
||||||
import type { ValhallaRequest } from "./ValhallaRequest";
|
import type { ValhallaRequest } from "./ValhallaRequest";
|
||||||
import type { LngLatBoundsLike } from "maplibre-gl";
|
import type { LngLatBoundsLike } from "maplibre-gl";
|
||||||
|
|
||||||
@ -204,7 +205,11 @@ export function stopNavigation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getUserLocation(): WorldLocation {
|
function getUserLocation(): WorldLocation {
|
||||||
return geolocate.currentLocation!;
|
// return geolocate.currentLocation!;
|
||||||
|
return {
|
||||||
|
lat: location.lat,
|
||||||
|
lon: location.lng
|
||||||
|
}
|
||||||
// const lnglat = window.geolocate._userLocationDotMarker.getLngLat();
|
// const lnglat = window.geolocate._userLocationDotMarker.getLngLat();
|
||||||
// return { lat: lnglat.lat, lon: lnglat.lng };
|
// return { lat: lnglat.lat, lon: lnglat.lng };
|
||||||
// console.log(map.value!)
|
// console.log(map.value!)
|
||||||
|
|||||||
Reference in New Issue
Block a user