style: add eslint and prettier
This commit is contained in:
@@ -1,4 +1,17 @@
|
||||
import { BriefcaseMedicalIcon, CarIcon, ChefHatIcon, CroissantIcon, DrillIcon, FlameIcon, FuelIcon, HamburgerIcon, PackageIcon, SchoolIcon, SquareParkingIcon, StoreIcon } from "@lucide/svelte";
|
||||
import {
|
||||
BriefcaseMedicalIcon,
|
||||
CarIcon,
|
||||
ChefHatIcon,
|
||||
CroissantIcon,
|
||||
DrillIcon,
|
||||
FlameIcon,
|
||||
FuelIcon,
|
||||
HamburgerIcon,
|
||||
PackageIcon,
|
||||
SchoolIcon,
|
||||
SquareParkingIcon,
|
||||
StoreIcon,
|
||||
} from "@lucide/svelte";
|
||||
import type { Component } from "svelte";
|
||||
|
||||
export const POIIcons: Record<string, Component> = {
|
||||
@@ -14,5 +27,5 @@ export const POIIcons: Record<string, Component> = {
|
||||
"shop=kiosk": StoreIcon,
|
||||
"amenity=restaurant": ChefHatIcon,
|
||||
"amenity=fast_food": HamburgerIcon,
|
||||
"shop=bakery": CroissantIcon
|
||||
"shop=bakery": CroissantIcon,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
<script lang="ts">
|
||||
import * as Drawer from "$lib/components/ui/drawer/index.js";
|
||||
import { BikeIcon, CarIcon, PlusCircleIcon, SaveIcon, TractorIcon, TruckIcon, XIcon } from "@lucide/svelte";
|
||||
import * as Drawer from "$lib/components/ui/drawer/index.js";
|
||||
import {
|
||||
BikeIcon,
|
||||
CarIcon,
|
||||
SaveIcon,
|
||||
TractorIcon,
|
||||
TruckIcon,
|
||||
XIcon,
|
||||
} from "@lucide/svelte";
|
||||
import Button, { buttonVariants } from "../ui/button/button.svelte";
|
||||
import { DefaultVehicle, isValidFuel, selectVehicle, setVehicles, vehicles, type Vehicle, type VehicleType } from "$lib/vehicles/vehicles.svelte";
|
||||
import {
|
||||
isValidFuel,
|
||||
selectVehicle,
|
||||
setVehicles,
|
||||
vehicles,
|
||||
type Vehicle,
|
||||
type VehicleType,
|
||||
} from "$lib/vehicles/vehicles.svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import * as Select from "../ui/select";
|
||||
import Input from "../ui/input/input.svelte";
|
||||
@@ -36,25 +50,31 @@
|
||||
actualMaxSpeed: 45,
|
||||
emissionClass: "euro_5",
|
||||
fuelType: "diesel",
|
||||
preferredFuel: "Diesel"
|
||||
preferredFuel: "Diesel",
|
||||
});
|
||||
</script>
|
||||
|
||||
<Drawer.Root bind:open={open}>
|
||||
<Drawer.Trigger class={buttonVariants({ variant: "secondary", class: "w-full p-5" })}>
|
||||
|
||||
<Drawer.Root bind:open>
|
||||
<Drawer.Trigger
|
||||
class={buttonVariants({ variant: "secondary", class: "w-full p-5" })}
|
||||
>
|
||||
{@render children()}
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header>
|
||||
<Drawer.Title>Add Vehicle</Drawer.Title>
|
||||
</Drawer.Header>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header>
|
||||
<Drawer.Title>Add Vehicle</Drawer.Title>
|
||||
</Drawer.Header>
|
||||
<div class="p-4 pt-0 flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<Select.Root type="single" bind:value={vehicle.type}>
|
||||
<Select.Trigger class="w-[180px]">
|
||||
{@const Icon = getVehicleIcon(vehicle.type)}
|
||||
<Icon />
|
||||
{vehicle.type === "car" ? "Car" : vehicle.type === "motor_scooter" ? "Moped" : "?"}
|
||||
{vehicle.type === "car"
|
||||
? "Car"
|
||||
: vehicle.type === "motor_scooter"
|
||||
? "Moped"
|
||||
: "?"}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="car">
|
||||
@@ -110,7 +130,11 @@
|
||||
<div class="flex gap-2">
|
||||
<Select.Root type="single" bind:value={vehicle.fuelType}>
|
||||
<Select.Trigger class="w-full">
|
||||
{vehicle.fuelType === "diesel" ? "Diesel" : vehicle.fuelType === "petrol" ? "Petrol" : "Electric"}
|
||||
{vehicle.fuelType === "diesel"
|
||||
? "Diesel"
|
||||
: vehicle.fuelType === "petrol"
|
||||
? "Petrol"
|
||||
: "Electric"}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="diesel">Diesel</Select.Item>
|
||||
@@ -137,33 +161,38 @@
|
||||
</div>
|
||||
</div>
|
||||
<Drawer.Footer>
|
||||
<Button onclick={() => {
|
||||
open = false;
|
||||
if (vehicle.name.trim() === "") {
|
||||
alert("Please enter a vehicle name.");
|
||||
return;
|
||||
}
|
||||
if (vehicle.legalMaxSpeed <= 0 || vehicle.actualMaxSpeed <= 0) {
|
||||
alert("Please enter valid speeds.");
|
||||
return;
|
||||
}
|
||||
if(!isValidFuel(vehicle)) {
|
||||
alert("Please select a valid fuel type and preferred fuel.");
|
||||
return;
|
||||
}
|
||||
setVehicles([...vehicles, vehicle]);
|
||||
selectVehicle(vehicle);
|
||||
location.reload(); // TODO
|
||||
}}>
|
||||
<Button
|
||||
onclick={() => {
|
||||
open = false;
|
||||
if (vehicle.name.trim() === "") {
|
||||
alert("Please enter a vehicle name.");
|
||||
return;
|
||||
}
|
||||
if (vehicle.legalMaxSpeed <= 0 || vehicle.actualMaxSpeed <= 0) {
|
||||
alert("Please enter valid speeds.");
|
||||
return;
|
||||
}
|
||||
if (!isValidFuel(vehicle)) {
|
||||
alert("Please select a valid fuel type and preferred fuel.");
|
||||
return;
|
||||
}
|
||||
setVehicles([...vehicles, vehicle]);
|
||||
selectVehicle(vehicle);
|
||||
location.reload(); // TODO
|
||||
}}
|
||||
>
|
||||
<SaveIcon />
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="secondary" onclick={() => {
|
||||
open = false;
|
||||
}}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={() => {
|
||||
open = false;
|
||||
}}
|
||||
>
|
||||
<XIcon />
|
||||
Cancel
|
||||
</Button>
|
||||
</Drawer.Footer>
|
||||
</Drawer.Content>
|
||||
</Drawer.Root>
|
||||
</Drawer.Content>
|
||||
</Drawer.Root>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import * as Select from "../ui/select";
|
||||
</script>
|
||||
|
||||
{#each EVConnectors as connector}
|
||||
{#each EVConnectors as connector (connector)}
|
||||
<Select.Item value={connector}>
|
||||
{connector}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import * as Popover from "$lib/components/ui/popover/index.js";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
|
||||
const frameworks = [
|
||||
{
|
||||
value: "sveltekit",
|
||||
@@ -29,15 +29,13 @@
|
||||
label: "Astro",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
let open = $state(false);
|
||||
let value = $state("");
|
||||
let triggerRef = $state<HTMLButtonElement>(null!);
|
||||
|
||||
const selectedValue = $derived(
|
||||
value === "location" ? "My Location" : value
|
||||
);
|
||||
|
||||
|
||||
const selectedValue = $derived(value === "location" ? "My Location" : value);
|
||||
|
||||
// We want to refocus the trigger button when the user selects
|
||||
// an item from the list so users can continue navigating the
|
||||
// rest of the form with the keyboard.
|
||||
@@ -51,7 +49,7 @@
|
||||
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Trigger bind:ref={triggerRef}>
|
||||
{#snippet child({ props }: { props: Record<string, any> })}
|
||||
{#snippet child({ props }: { props: Record<string, unknown> })}
|
||||
<Button
|
||||
variant="outline"
|
||||
class="justify-between"
|
||||
@@ -66,46 +64,46 @@
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-[200px] p-0">
|
||||
<Command.Root>
|
||||
<Command.Input placeholder="Search..." />
|
||||
<Command.List>
|
||||
<Command.Empty>No location found.</Command.Empty>
|
||||
<Command.Group>
|
||||
<Command.Item
|
||||
value={"location"}
|
||||
onSelect={() => {
|
||||
value = "location";
|
||||
closeAndFocusTrigger();
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
class={cn(
|
||||
"mr-2 size-4",
|
||||
value !== "location" && "text-transparent"
|
||||
)}
|
||||
/>
|
||||
My Location
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group>
|
||||
{#each frameworks as framework}
|
||||
<Command.Item
|
||||
value={framework.value}
|
||||
onSelect={() => {
|
||||
value = framework.value;
|
||||
closeAndFocusTrigger();
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
class={cn(
|
||||
"mr-2 size-4",
|
||||
value !== framework.value && "text-transparent"
|
||||
)}
|
||||
/>
|
||||
{framework.label}
|
||||
</Command.Item>
|
||||
{/each}
|
||||
</Command.Group>
|
||||
</Command.List>
|
||||
<Command.Input placeholder="Search..." />
|
||||
<Command.List>
|
||||
<Command.Empty>No location found.</Command.Empty>
|
||||
<Command.Group>
|
||||
<Command.Item
|
||||
value="location"
|
||||
onSelect={() => {
|
||||
value = "location";
|
||||
closeAndFocusTrigger();
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
class={cn(
|
||||
"mr-2 size-4",
|
||||
value !== "location" && "text-transparent",
|
||||
)}
|
||||
/>
|
||||
My Location
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group>
|
||||
{#each frameworks as framework (framework.value)}
|
||||
<Command.Item
|
||||
value={framework.value}
|
||||
onSelect={() => {
|
||||
value = framework.value;
|
||||
closeAndFocusTrigger();
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
class={cn(
|
||||
"mr-2 size-4",
|
||||
value !== framework.value && "text-transparent",
|
||||
)}
|
||||
/>
|
||||
{framework.label}
|
||||
</Command.Item>
|
||||
{/each}
|
||||
</Command.Group>
|
||||
</Command.List>
|
||||
</Command.Root>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Popover.Root>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
let name = $derived(maneuverTypes[maneuver] || "none");
|
||||
</script>
|
||||
|
||||
<img src="/img/maneuver/{name}.svg" alt={name}>
|
||||
<img src="/img/maneuver/{name}.svg" alt={name} />
|
||||
|
||||
<style>
|
||||
img {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
GeoJSONSource,
|
||||
GeolocateControl,
|
||||
Hash,
|
||||
LineLayer,
|
||||
MapLibre,
|
||||
Marker,
|
||||
@@ -11,13 +9,7 @@
|
||||
} from "svelte-maplibre-gl";
|
||||
import { view } from "./sidebar.svelte";
|
||||
import { map, pin } from "./map.svelte";
|
||||
import {
|
||||
drawAllRoutes,
|
||||
fetchRoute,
|
||||
routing,
|
||||
} from "$lib/services/navigation/routing.svelte";
|
||||
import { createValhallaRequest } from "$lib/vehicles/ValhallaVehicles";
|
||||
import { ROUTING_SERVER } from "$lib/services/hosts";
|
||||
import { routing } from "$lib/services/navigation/routing.svelte";
|
||||
import { location } from "./location.svelte";
|
||||
|
||||
onMount(() => {
|
||||
@@ -33,7 +25,9 @@
|
||||
scheme="tiles"
|
||||
loadFn={async (params) => {
|
||||
console.log(params.url);
|
||||
const url = params.url.replace("tiles://", "").replace("tiles.openfreemap.org/", "");
|
||||
const url = params.url
|
||||
.replace("tiles://", "")
|
||||
.replace("tiles.openfreemap.org/", "");
|
||||
const path = url.split("/")[0];
|
||||
if (path == "natural_earth") {
|
||||
const t = await fetch("https://tiles.openfreemap.org/" + url);
|
||||
@@ -73,7 +67,7 @@
|
||||
}
|
||||
}}
|
||||
onmove={(e) => {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error - not typed
|
||||
if (e.reason !== "location") {
|
||||
location.locked = false;
|
||||
}
|
||||
@@ -191,7 +185,10 @@
|
||||
|
||||
{#if location.available}
|
||||
<div class="maplibregl-user-location-dot" bind:this={locationDot}></div>
|
||||
<div class="maplibregl-user-location-accuracy-circle" bind:this={locationAccuracyCircle}></div>
|
||||
<div
|
||||
class="maplibregl-user-location-accuracy-circle"
|
||||
bind:this={locationAccuracyCircle}
|
||||
></div>
|
||||
<Marker
|
||||
lnglat={{ lat: location.lat, lng: location.lng }}
|
||||
element={locationDot}
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
import { hasCapability, type Capabilities } from "$lib/services/lnv";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let { capability, children }: {
|
||||
let {
|
||||
capability,
|
||||
children,
|
||||
}: {
|
||||
capability: Capabilities[number];
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
@@ -12,6 +15,6 @@
|
||||
{#if has}
|
||||
{@render children()}
|
||||
{/if}
|
||||
{:catch error}
|
||||
{:catch _error}
|
||||
<!-- user is likely offline -->
|
||||
{/await}
|
||||
{/await}
|
||||
|
||||
@@ -1,37 +1,52 @@
|
||||
<script lang="ts">
|
||||
import LanesDisplay from "$lib/services/navigation/LanesDisplay.svelte";
|
||||
import { decodePolyline, routing } from "$lib/services/navigation/routing.svelte";
|
||||
import {
|
||||
decodePolyline,
|
||||
routing,
|
||||
} from "$lib/services/navigation/routing.svelte";
|
||||
import { location } from "./location.svelte";
|
||||
import ManeuverIcon from "./ManeuverIcon.svelte";
|
||||
|
||||
// Helper: Haversine distance in meters
|
||||
function haversine(a: { lat: number; lon: number }, b: { lat: number; lon: number }) {
|
||||
function haversine(
|
||||
a: { lat: number; lon: number },
|
||||
b: { lat: number; lon: number },
|
||||
) {
|
||||
const R = 6371000;
|
||||
const toRad = (d: number) => d * Math.PI / 180;
|
||||
const toRad = (d: number) => (d * Math.PI) / 180;
|
||||
const dLat = toRad(b.lat - a.lat);
|
||||
const dLon = toRad(b.lon - a.lon);
|
||||
const lat1 = toRad(a.lat);
|
||||
const lat2 = toRad(b.lat);
|
||||
|
||||
const aVal = Math.sin(dLat / 2) ** 2 +
|
||||
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
|
||||
const aVal =
|
||||
Math.sin(dLat / 2) ** 2 +
|
||||
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
|
||||
return 2 * R * Math.atan2(Math.sqrt(aVal), Math.sqrt(1 - aVal));
|
||||
}
|
||||
|
||||
// Helper: Project point onto segment AB, return projected point and distance along segment
|
||||
function projectPointToSegment(p: WorldLocation, a: WorldLocation, b: WorldLocation) {
|
||||
const toRad = (deg: number) => deg * Math.PI / 180;
|
||||
const toDeg = (rad: number) => rad * 180 / Math.PI;
|
||||
function projectPointToSegment(
|
||||
p: WorldLocation,
|
||||
a: WorldLocation,
|
||||
b: WorldLocation,
|
||||
) {
|
||||
const toRad = (deg: number) => (deg * Math.PI) / 180;
|
||||
const toDeg = (rad: number) => (rad * 180) / Math.PI;
|
||||
|
||||
const lat1 = toRad(a.lat), lon1 = toRad(a.lon);
|
||||
const lat2 = toRad(b.lat), lon2 = toRad(b.lon);
|
||||
const lat3 = toRad(p.lat), lon3 = toRad(p.lon);
|
||||
const lat1 = toRad(a.lat),
|
||||
lon1 = toRad(a.lon);
|
||||
const lat2 = toRad(b.lat),
|
||||
lon2 = toRad(b.lon);
|
||||
const lat3 = toRad(p.lat),
|
||||
lon3 = toRad(p.lon);
|
||||
|
||||
const dLon = lon2 - lon1;
|
||||
const dLat = lat2 - lat1;
|
||||
|
||||
const t = ((lat3 - lat1) * dLat + (lon3 - lon1) * dLon) /
|
||||
(dLat * dLat + dLon * dLon);
|
||||
const t =
|
||||
((lat3 - lat1) * dLat + (lon3 - lon1) * dLon) /
|
||||
(dLat * dLat + dLon * dLon);
|
||||
|
||||
// Clamp to [0,1]
|
||||
const clampedT = Math.max(0, Math.min(1, t));
|
||||
@@ -49,11 +64,13 @@
|
||||
|
||||
// const point = $derived(decodePolyline(routing.currentTrip?.legs[0].shape || "")[routing.currentTripInfo.currentManeuver?.end_shape_index || 0]);
|
||||
// const distance = $derived(Math.sqrt(Math.pow(point.lat - location.lat, 2) + Math.pow(point.lon - location.lng, 2)) * 111000); // Approximate conversion to meters
|
||||
const shape = $derived(decodePolyline(routing.currentTrip?.legs[0].shape || ""));
|
||||
const shape = $derived(
|
||||
decodePolyline(routing.currentTrip?.legs[0].shape || ""),
|
||||
);
|
||||
const maneuver = $derived(routing.currentTripInfo.currentManeuver);
|
||||
|
||||
const distance = $derived.by(() => {
|
||||
const lat = location.lat;
|
||||
const lat = location.lat;
|
||||
const lon = location.lng;
|
||||
if (!shape || shape.length === 0 || !maneuver) return 0;
|
||||
const start = shape[maneuver.begin_shape_index];
|
||||
@@ -61,7 +78,7 @@
|
||||
if (!start || !end) return 0;
|
||||
const projected = projectPointToSegment({ lat, lon }, start, end);
|
||||
return projected.distToUser;
|
||||
});
|
||||
});
|
||||
|
||||
const roundDistance = $derived.by(() => {
|
||||
const dist = Math.round(distance);
|
||||
@@ -76,24 +93,27 @@
|
||||
} else {
|
||||
return Math.round(dist / 5000) * 5000;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const distanceText = $derived.by(() => {
|
||||
const dist = roundDistance;
|
||||
if (dist < 1000) return `${dist} m`;
|
||||
return `${(dist / 1000)} km`;
|
||||
})
|
||||
return `${dist / 1000} km`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="fixed top-4 left-4 z-50 w-[calc(100%-32px)] bg-background/60 text-white rounded-lg overflow-hidden" style="backdrop-filter: blur(5px);">
|
||||
<div
|
||||
class="fixed top-4 left-4 z-50 w-[calc(100%-32px)] bg-background/60 text-white rounded-lg overflow-hidden"
|
||||
style="backdrop-filter: blur(5px);"
|
||||
>
|
||||
<div class="p-2 flex gap-2">
|
||||
<ManeuverIcon maneuver={routing.currentTripInfo.currentManeuver?.type ?? 0} />
|
||||
<ManeuverIcon
|
||||
maneuver={routing.currentTripInfo.currentManeuver?.type ?? 0}
|
||||
/>
|
||||
<div class="flex gap-1 flex-col">
|
||||
<span class="text-xl font-bold">{distanceText}</span>
|
||||
<span>{routing.currentTripInfo.currentManeuver?.instruction}</span>
|
||||
</div>
|
||||
</div>
|
||||
<LanesDisplay
|
||||
lanes={routing.currentTripInfo.currentManeuver?.lanes}
|
||||
/>
|
||||
</div>
|
||||
<LanesDisplay lanes={routing.currentTripInfo.currentManeuver?.lanes} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount, type Component } from "svelte";
|
||||
import { type Component } from "svelte";
|
||||
import InvalidSidebar from "./sidebar/InvalidSidebar.svelte";
|
||||
import { searchbar, view } from "./sidebar.svelte";
|
||||
import MainSidebar from "./sidebar/MainSidebar.svelte";
|
||||
@@ -8,18 +8,28 @@
|
||||
import { map } from "./map.svelte";
|
||||
import TripSidebar from "./sidebar/TripSidebar.svelte";
|
||||
import Input from "../ui/input/input.svelte";
|
||||
import { EllipsisIcon, HomeIcon, SettingsIcon, UserIcon } from "@lucide/svelte";
|
||||
import {
|
||||
EllipsisIcon,
|
||||
HomeIcon,
|
||||
SettingsIcon,
|
||||
UserIcon,
|
||||
} from "@lucide/svelte";
|
||||
import Button from "../ui/button/button.svelte";
|
||||
import { search, type Feature } from "$lib/services/Search";
|
||||
import SearchSidebar from "./sidebar/SearchSidebar.svelte";
|
||||
import RequiresCapability from "./RequiresCapability.svelte";
|
||||
import UserSidebar from "./sidebar/UserSidebar.svelte";
|
||||
import { advertiseRemoteLocation, location, remoteLocation } from "./location.svelte";
|
||||
import {
|
||||
advertiseRemoteLocation,
|
||||
location,
|
||||
remoteLocation,
|
||||
} from "./location.svelte";
|
||||
import * as Popover from "../ui/popover";
|
||||
import { routing } from "$lib/services/navigation/routing.svelte";
|
||||
import InRouteSidebar from "./sidebar/InRouteSidebar.svelte";
|
||||
|
||||
const views: {[key: string]: Component<any>} = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const views: Record<string, Component<any>> = {
|
||||
main: MainSidebar,
|
||||
info: InfoSidebar,
|
||||
route: RouteSidebar,
|
||||
@@ -41,12 +51,14 @@
|
||||
$effect(() => {
|
||||
const newValue = getter(); // read here to subscribe to it
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => value = newValue, delay);
|
||||
timer = setTimeout(() => (value = newValue), delay);
|
||||
return () => clearTimeout(timer);
|
||||
});
|
||||
return () => value;
|
||||
}
|
||||
|
||||
// TODO: implement loading state
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
let loading = $state(false);
|
||||
|
||||
let searchText = $derived.by(debounce(() => searchbar.text, 300));
|
||||
@@ -54,20 +66,20 @@
|
||||
let mobileView = $derived(window.innerWidth < 768 || routing.currentTrip);
|
||||
|
||||
$effect(() => {
|
||||
if(!searchText) {
|
||||
if (!searchText) {
|
||||
searchResults = [];
|
||||
if(view.current.type == "search") view.switch("main");
|
||||
if (view.current.type == "search") view.switch("main");
|
||||
return;
|
||||
}
|
||||
if (searchText.length > 0) {
|
||||
loading = true;
|
||||
search(searchText, 0, 0).then(results => {
|
||||
search(searchText, 0, 0).then((results) => {
|
||||
searchResults = results;
|
||||
loading = false;
|
||||
view.switch("search", {
|
||||
results: searchResults,
|
||||
query: searchText
|
||||
})
|
||||
query: searchText,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
searchResults = [];
|
||||
@@ -77,37 +89,48 @@
|
||||
|
||||
{#if !routing.currentTrip}
|
||||
<div id="floating-search" class={mobileView ? "mobileView" : ""}>
|
||||
<Input class="h-10"
|
||||
placeholder="Search..." bind:value={searchbar.text} />
|
||||
<Input class="h-10" placeholder="Search..." bind:value={searchbar.text} />
|
||||
</div>
|
||||
{/if}
|
||||
<div id="sidebar" class={mobileView ? "mobileView" : ""} style={(mobileView ? `height: ${sidebarHeight}px;` : "") + (routing.currentTrip ? "bottom: 0 !important;" : "")}>
|
||||
<div
|
||||
id="sidebar"
|
||||
class={mobileView ? "mobileView" : ""}
|
||||
style={(mobileView ? `height: ${sidebarHeight}px;` : "") +
|
||||
(routing.currentTrip ? "bottom: 0 !important;" : "")}
|
||||
>
|
||||
{#if mobileView}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
||||
<div role="button" id="grabber" style="height: 10px; cursor: grab; display: flex; justify-content: center; align-items: center; touch-action: none;" ontouchstart={(e) => {
|
||||
isDragging = true;
|
||||
startY = e.touches[0].clientY;
|
||||
startHeight = sidebarHeight;
|
||||
}} ontouchmove={(e) => {
|
||||
if(!isDragging) return;
|
||||
e.preventDefault();
|
||||
const deltaY = e.touches[0].clientY - startY;
|
||||
let newHeight = Math.max(100, startHeight - deltaY);
|
||||
<div
|
||||
role="button"
|
||||
id="grabber"
|
||||
style="height: 10px; cursor: grab; display: flex; justify-content: center; align-items: center; touch-action: none;"
|
||||
ontouchstart={(e) => {
|
||||
isDragging = true;
|
||||
startY = e.touches[0].clientY;
|
||||
startHeight = sidebarHeight;
|
||||
}}
|
||||
ontouchmove={(e) => {
|
||||
if (!isDragging) return;
|
||||
e.preventDefault();
|
||||
const deltaY = e.touches[0].clientY - startY;
|
||||
let newHeight = Math.max(100, startHeight - deltaY);
|
||||
|
||||
const snapPoint = 200;
|
||||
const snapThreshold = 20;
|
||||
if (Math.abs(newHeight - snapPoint) < snapThreshold) {
|
||||
newHeight = snapPoint;
|
||||
}
|
||||
sidebarHeight = newHeight;
|
||||
const snapPoint = 200;
|
||||
const snapThreshold = 20;
|
||||
if (Math.abs(newHeight - snapPoint) < snapThreshold) {
|
||||
newHeight = snapPoint;
|
||||
}
|
||||
sidebarHeight = newHeight;
|
||||
|
||||
map.updateMapPadding();
|
||||
}} ontouchend={() => {
|
||||
if(!isDragging) return;
|
||||
isDragging = false;
|
||||
}}>
|
||||
<div style="height: 8px; background-color: #acacac; width: 40%; border-radius: 15px;"></div>
|
||||
map.updateMapPadding();
|
||||
}}
|
||||
ontouchend={() => {
|
||||
if (!isDragging) return;
|
||||
isDragging = false;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style="height: 8px; background-color: #acacac; width: 40%; border-radius: 15px;"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -123,9 +146,11 @@
|
||||
<HomeIcon />
|
||||
</button>
|
||||
<RequiresCapability capability="auth">
|
||||
<button onclick={async () => {
|
||||
view.switch("user");
|
||||
}}>
|
||||
<button
|
||||
onclick={async () => {
|
||||
view.switch("user");
|
||||
}}
|
||||
>
|
||||
<UserIcon />
|
||||
</button>
|
||||
</RequiresCapability>
|
||||
@@ -145,22 +170,31 @@
|
||||
</Popover.Trigger>
|
||||
<Popover.Content>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Button variant="outline" onclick={() => {
|
||||
location.toggleLock();
|
||||
}}>
|
||||
<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();
|
||||
}}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
advertiseRemoteLocation();
|
||||
}}
|
||||
>
|
||||
Advertise Location
|
||||
</Button>
|
||||
<Button variant="outline" onclick={() => {
|
||||
remoteLocation(prompt("Code?") || "");
|
||||
}}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
remoteLocation(prompt("Code?") || "");
|
||||
}}
|
||||
>
|
||||
Join Remote Location
|
||||
</Button>
|
||||
</div>
|
||||
@@ -255,4 +289,4 @@
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
<script lang="ts">
|
||||
import * as Drawer from "$lib/components/ui/drawer/index.js";
|
||||
import { BikeIcon, CarIcon, PlusCircleIcon, TractorIcon, TruckIcon } from "@lucide/svelte";
|
||||
import * as Drawer from "$lib/components/ui/drawer/index.js";
|
||||
import {
|
||||
BikeIcon,
|
||||
CarIcon,
|
||||
PlusCircleIcon,
|
||||
TractorIcon,
|
||||
TruckIcon,
|
||||
} from "@lucide/svelte";
|
||||
import Button, { buttonVariants } from "../ui/button/button.svelte";
|
||||
import { DefaultVehicle, selectedVehicle, selectVehicle, vehicles, type VehicleType } from "$lib/vehicles/vehicles.svelte";
|
||||
import {
|
||||
DefaultVehicle,
|
||||
selectedVehicle,
|
||||
selectVehicle,
|
||||
vehicles,
|
||||
type VehicleType,
|
||||
} from "$lib/vehicles/vehicles.svelte";
|
||||
import AddVehicleDrawer from "./AddVehicleDrawer.svelte";
|
||||
|
||||
let open = $state(false);
|
||||
@@ -24,28 +36,39 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer.Root bind:open={open}>
|
||||
<Drawer.Trigger class={buttonVariants({ variant: "secondary", class: "w-full p-5" })}>
|
||||
|
||||
<Drawer.Root bind:open>
|
||||
<Drawer.Trigger
|
||||
class={buttonVariants({ variant: "secondary", class: "w-full p-5" })}
|
||||
>
|
||||
{@const vehicle = selectedVehicle() ?? DefaultVehicle}
|
||||
{@const Icon = getVehicleIcon(vehicle.type)}
|
||||
<Icon />
|
||||
{vehicle.name}
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header>
|
||||
<Drawer.Title>Vehicle Selector</Drawer.Title>
|
||||
<Drawer.Description>Select your vehicle to customize routing just for you.</Drawer.Description>
|
||||
</Drawer.Header>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header>
|
||||
<Drawer.Title>Vehicle Selector</Drawer.Title>
|
||||
<Drawer.Description
|
||||
>Select your vehicle to customize routing just for you.</Drawer.Description
|
||||
>
|
||||
</Drawer.Header>
|
||||
<div class="p-4 pt-0 flex flex-col gap-2">
|
||||
{#each vehicles as vehicle}
|
||||
<Button variant={selectedVehicle() === vehicle ? "default" : "secondary"} class="w-full p-5" onclick={() => {selectVehicle(vehicle); open = false;}}>
|
||||
{#each vehicles as vehicle (vehicle.name)}
|
||||
<Button
|
||||
variant={selectedVehicle() === vehicle ? "default" : "secondary"}
|
||||
class="w-full p-5"
|
||||
onclick={() => {
|
||||
selectVehicle(vehicle);
|
||||
open = false;
|
||||
}}
|
||||
>
|
||||
{@const Icon = getVehicleIcon(vehicle.type)}
|
||||
<Icon />
|
||||
{vehicle.name}
|
||||
</Button>
|
||||
{/each}
|
||||
|
||||
|
||||
<AddVehicleDrawer>
|
||||
<Button variant="secondary" class="w-full p-5">
|
||||
<PlusCircleIcon />
|
||||
@@ -53,5 +76,5 @@
|
||||
</Button>
|
||||
</AddVehicleDrawer>
|
||||
</div>
|
||||
</Drawer.Content>
|
||||
</Drawer.Root>
|
||||
</Drawer.Content>
|
||||
</Drawer.Root>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script>
|
||||
import Badge from "$lib/components/ui/badge/badge.svelte";
|
||||
import { getStations } from "$lib/services/MTSK";
|
||||
import Badge from "$lib/components/ui/badge/badge.svelte";
|
||||
import { getStations } from "$lib/services/MTSK";
|
||||
|
||||
let { tags, lat, lng } = $props();
|
||||
</script>
|
||||
|
||||
<h3 class="text-lg font-bold mt-2">Fuel Types</h3>
|
||||
<ul class="flex gap-2 flex-wrap">
|
||||
{#each Object.entries(tags).filter(([key]) => key.startsWith("fuel:")) as [key, tag]}
|
||||
{#each Object.entries(tags).filter( ([key]) => key.startsWith("fuel:"), ) as [key, tag] (key)}
|
||||
<!-- <li>{key.replace("fuel:", "")}: {tag}</li> -->
|
||||
<Badge>
|
||||
{key.replace("fuel:", "")}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Input from "$lib/components/ui/input/input.svelte";
|
||||
import { LNV_SERVER } from "$lib/services/hosts";
|
||||
import { ai } from "$lib/services/lnv";
|
||||
import { SparklesIcon } from "@lucide/svelte";
|
||||
|
||||
|
||||
let { lat, lon } = $props();
|
||||
let question = $state("");
|
||||
|
||||
@@ -12,7 +11,7 @@
|
||||
const chunks = res.split("\n");
|
||||
let text = "";
|
||||
for (const chunk of chunks) {
|
||||
if(chunk.startsWith("0:")) {
|
||||
if (chunk.startsWith("0:")) {
|
||||
text += JSON.parse(chunk.substring(2).trim());
|
||||
}
|
||||
}
|
||||
@@ -33,9 +32,11 @@
|
||||
{/await}
|
||||
<Input
|
||||
type="text"
|
||||
value={""}
|
||||
placeholder="Ask a question about this place..." onchange={(e) => {
|
||||
question = (e.target! as any).value;
|
||||
}} />
|
||||
value=""
|
||||
placeholder="Ask a question about this place..."
|
||||
onchange={(e) => {
|
||||
question = (e.target! as HTMLInputElement).value;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
import Badge from "$lib/components/ui/badge/badge.svelte";
|
||||
import opening_hours from "opening_hours";
|
||||
|
||||
let { hours, lat, lon }: { hours: string, lat: number, lon: number } = $props();
|
||||
let { hours, lat, lon }: { hours: string; lat: number; lon: number } =
|
||||
$props();
|
||||
|
||||
const oh = $derived.by(() => {
|
||||
return new opening_hours(hours, {
|
||||
lat, lon, address: {
|
||||
lat,
|
||||
lon,
|
||||
address: {
|
||||
country_code: "de", // Default to Germany, can be overridden if needed
|
||||
state: "NRW", // Default to North Rhine-Westphalia, can be overridden if needed
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<h3 class="text-lg font-bold mt-2">
|
||||
@@ -24,4 +27,4 @@
|
||||
</h3>
|
||||
|
||||
<p>{hours}</p>
|
||||
<!-- todo -->
|
||||
<!-- todo -->
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
import { getReviews, postReview } from "$lib/services/lnv";
|
||||
import Stars from "./Stars.svelte";
|
||||
|
||||
let { lat, lng }: { lat: number, lng: number } = $props();
|
||||
let { lat, lng }: { lat: number; lng: number } = $props();
|
||||
</script>
|
||||
|
||||
<h3 class="text-lg font-bold mt-2">Reviews</h3>
|
||||
{#await getReviews({lat, lon: lng}) then reviews}
|
||||
{#await getReviews({ lat, lon: lng }) then reviews}
|
||||
{#if reviews.length > 0}
|
||||
<ul class="list-disc pl-5">
|
||||
{#each reviews as review}
|
||||
{#each reviews as review (review)}
|
||||
<li class="flex justify-center gap-2 mb-2 flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar.Root>
|
||||
@@ -27,20 +27,27 @@
|
||||
{:else}
|
||||
<p>No reviews available.</p>
|
||||
{/if}
|
||||
<Button variant="secondary" onclick={async () => {
|
||||
const rating = prompt("Enter your rating (1-5):");
|
||||
const comment = prompt("Enter your review comment:");
|
||||
if (rating && comment) {
|
||||
console.log(`Rating: ${rating}, Comment: ${comment}`);
|
||||
await postReview({ lat, lon: lng }, {
|
||||
rating: parseInt(rating, 10),
|
||||
comment
|
||||
})
|
||||
alert("Thank you for your review!");
|
||||
} else {
|
||||
alert("Review submission cancelled.");
|
||||
}
|
||||
}} disabled>Write a review</Button><br>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={async () => {
|
||||
const rating = prompt("Enter your rating (1-5):");
|
||||
const comment = prompt("Enter your review comment:");
|
||||
if (rating && comment) {
|
||||
console.log(`Rating: ${rating}, Comment: ${comment}`);
|
||||
await postReview(
|
||||
{ lat, lon: lng },
|
||||
{
|
||||
rating: parseInt(rating, 10),
|
||||
comment,
|
||||
},
|
||||
);
|
||||
alert("Thank you for your review!");
|
||||
} else {
|
||||
alert("Review submission cancelled.");
|
||||
}
|
||||
}}
|
||||
disabled>Write a review</Button
|
||||
><br />
|
||||
{:catch error}
|
||||
<p>Error loading reviews: {error.message}</p>
|
||||
{/await}
|
||||
{/await}
|
||||
|
||||
@@ -40,4 +40,4 @@
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
<StarIcon class="fill-white" />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LNV_SERVER } from "$lib/services/hosts"
|
||||
import { routing } from "$lib/services/navigation/routing.svelte"
|
||||
import { map } from "./map.svelte"
|
||||
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,
|
||||
@@ -12,100 +12,117 @@ export const location = $state({
|
||||
provider: "gps" as "gps" | "remote" | "simulated",
|
||||
locked: true,
|
||||
toggleLock: () => {
|
||||
location.locked = !location.locked
|
||||
console.log("Location lock toggled:", location.locked)
|
||||
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({
|
||||
map.value?.flyTo(
|
||||
{
|
||||
center: [location.lng, location.lat],
|
||||
zoom: 16,
|
||||
duration: 1000,
|
||||
// bearing: location.heading !== null ? location.heading : undefined
|
||||
}, {
|
||||
reason: "location"
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
reason: "location",
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
advertiser: null as WebSocket | null,
|
||||
code: null as string | null,
|
||||
lastUpdate: null as Date | null,
|
||||
});
|
||||
|
||||
// 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
|
||||
})
|
||||
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) {
|
||||
if (!checkRunning) {
|
||||
setInterval(() => {
|
||||
checkRunning = true;
|
||||
if(location.provider !== "gps") return;
|
||||
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")
|
||||
if (
|
||||
location.lastUpdate &&
|
||||
new Date().getTime() - location.lastUpdate.getTime() > 10000
|
||||
) {
|
||||
console.warn("Location update is stale, rewatching position");
|
||||
watchLocation();
|
||||
}
|
||||
}, 1000)
|
||||
}, 1000);
|
||||
checkRunning = true;
|
||||
}
|
||||
|
||||
watchLocation()
|
||||
watchLocation();
|
||||
|
||||
export function advertiseRemoteLocation(code?: string) {
|
||||
const ws = new WebSocket(
|
||||
`${LNV_SERVER.replace("https", "wss").replace("http", "ws")}/ws`
|
||||
`${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 }))
|
||||
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);
|
||||
@@ -127,40 +144,43 @@ 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`
|
||||
`${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
|
||||
})
|
||||
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)
|
||||
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
|
||||
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"
|
||||
})
|
||||
map.value?.flyTo(
|
||||
{
|
||||
center: [location.lng, location.lat],
|
||||
zoom: 16,
|
||||
duration: 1000,
|
||||
// bearing: location.heading !== null ? location.heading : undefined
|
||||
},
|
||||
{
|
||||
reason: "location",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// setInterval(() => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { routing } from "$lib/services/navigation/routing.svelte";
|
||||
import { reverseGeocode } from "$lib/services/Search";
|
||||
import { view } from "./sidebar.svelte";
|
||||
|
||||
// export const geolocate = $state({
|
||||
@@ -9,27 +8,31 @@ import { view } from "./sidebar.svelte";
|
||||
export const map = $state({
|
||||
value: undefined as maplibregl.Map | undefined,
|
||||
updateMapPadding: () => {
|
||||
if(document.querySelector<HTMLDivElement>("#sidebar") == null) {
|
||||
if (document.querySelector<HTMLDivElement>("#sidebar") == null) {
|
||||
map._setPadding({
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0
|
||||
left: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.log("Updating map padding");
|
||||
if (window.innerWidth < 768 || routing.currentTrip) {
|
||||
const calculatedSidebarHeight = document.querySelector<HTMLDivElement>("#sidebar")!.getBoundingClientRect().height;
|
||||
const calculatedSidebarHeight = document
|
||||
.querySelector<HTMLDivElement>("#sidebar")!
|
||||
.getBoundingClientRect().height;
|
||||
map._setPadding({
|
||||
top: routing.currentTrip ? 64 : 0,
|
||||
right: 0,
|
||||
bottom: calculatedSidebarHeight,
|
||||
left: 0
|
||||
left: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const calculatedSidebarWidth = document.querySelector<HTMLDivElement>("#sidebar")!.getBoundingClientRect().width;
|
||||
const calculatedSidebarWidth = document
|
||||
.querySelector<HTMLDivElement>("#sidebar")!
|
||||
.getBoundingClientRect().width;
|
||||
map._setPadding({
|
||||
top: 0,
|
||||
right: 0,
|
||||
@@ -41,14 +44,19 @@ export const map = $state({
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0
|
||||
left: 0,
|
||||
},
|
||||
_setPadding: (_padding: { top: number, right: number, bottom: number, left: number }) => {
|
||||
_setPadding: (_padding: {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
}) => {
|
||||
map.padding = _padding;
|
||||
if (map.value) {
|
||||
map.value.setPadding(map.padding);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const pin = $state({
|
||||
@@ -66,12 +74,12 @@ export const pin = $state({
|
||||
pin.lng = 0;
|
||||
},
|
||||
showInfo: async () => {
|
||||
if(!pin.isDropped) return;
|
||||
if (!pin.isDropped) return;
|
||||
// const res = await reverseGeocode({ lat: pin.lat, lon: pin.lng });
|
||||
// if(res.length > 0) {
|
||||
// const feature = res[0];
|
||||
// view.switch("info", { feature });
|
||||
// const feature = res[0];
|
||||
// view.switch("info", { feature });
|
||||
// }
|
||||
view.switch("info", { lat: pin.lat, lng: pin.lng });
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type View = {
|
||||
export interface View {
|
||||
type: string;
|
||||
props?: Record<string, any>;
|
||||
props?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const view = $state({
|
||||
@@ -13,14 +13,14 @@ export const view = $state({
|
||||
view.current = { type: "main" } as View; // Reset to main view if history is empty
|
||||
}
|
||||
},
|
||||
switch: (to: string, props?: Record<string, any>) => {
|
||||
switch: (to: string, props?: Record<string, unknown>) => {
|
||||
if (view.current.type !== to) {
|
||||
view.history.push(view.current);
|
||||
}
|
||||
view.current = { type: to, props } as View;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const searchbar = $state({
|
||||
text: ""
|
||||
})
|
||||
text: "",
|
||||
});
|
||||
|
||||
@@ -1,36 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { decodePolyline, routing, stopNavigation } from "$lib/services/navigation/routing.svelte";
|
||||
import { advertiseRemoteLocation, location } from "../location.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import {
|
||||
decodePolyline,
|
||||
routing,
|
||||
stopNavigation,
|
||||
} from "$lib/services/navigation/routing.svelte";
|
||||
import { advertiseRemoteLocation, location } from "../location.svelte";
|
||||
|
||||
// Helper: Haversine distance in meters
|
||||
function haversine(a: { lat: number; lon: number }, b: { lat: number; lon: number }) {
|
||||
function haversine(
|
||||
a: { lat: number; lon: number },
|
||||
b: { lat: number; lon: number },
|
||||
) {
|
||||
const R = 6371000;
|
||||
const toRad = (d: number) => d * Math.PI / 180;
|
||||
const toRad = (d: number) => (d * Math.PI) / 180;
|
||||
const dLat = toRad(b.lat - a.lat);
|
||||
const dLon = toRad(b.lon - a.lon);
|
||||
const lat1 = toRad(a.lat);
|
||||
const lat2 = toRad(b.lat);
|
||||
|
||||
const aVal = Math.sin(dLat / 2) ** 2 +
|
||||
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
|
||||
const aVal =
|
||||
Math.sin(dLat / 2) ** 2 +
|
||||
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
|
||||
return 2 * R * Math.atan2(Math.sqrt(aVal), Math.sqrt(1 - aVal));
|
||||
}
|
||||
|
||||
// Helper: Project point onto segment AB, return projected point and distance along segment
|
||||
function projectPointToSegment(p: WorldLocation, a: WorldLocation, b: WorldLocation) {
|
||||
const toRad = (deg: number) => deg * Math.PI / 180;
|
||||
const toDeg = (rad: number) => rad * 180 / Math.PI;
|
||||
function projectPointToSegment(
|
||||
p: WorldLocation,
|
||||
a: WorldLocation,
|
||||
b: WorldLocation,
|
||||
) {
|
||||
const toRad = (deg: number) => (deg * Math.PI) / 180;
|
||||
const toDeg = (rad: number) => (rad * 180) / Math.PI;
|
||||
|
||||
const lat1 = toRad(a.lat), lon1 = toRad(a.lon);
|
||||
const lat2 = toRad(b.lat), lon2 = toRad(b.lon);
|
||||
const lat3 = toRad(p.lat), lon3 = toRad(p.lon);
|
||||
const lat1 = toRad(a.lat),
|
||||
lon1 = toRad(a.lon);
|
||||
const lat2 = toRad(b.lat),
|
||||
lon2 = toRad(b.lon);
|
||||
const lat3 = toRad(p.lat),
|
||||
lon3 = toRad(p.lon);
|
||||
|
||||
const dLon = lon2 - lon1;
|
||||
const dLat = lat2 - lat1;
|
||||
|
||||
const t = ((lat3 - lat1) * dLat + (lon3 - lon1) * dLon) /
|
||||
(dLat * dLat + dLon * dLon);
|
||||
const t =
|
||||
((lat3 - lat1) * dLat + (lon3 - lon1) * dLon) /
|
||||
(dLat * dLat + dLon * dLon);
|
||||
|
||||
// Clamp to [0,1]
|
||||
const clampedT = Math.max(0, Math.min(1, t));
|
||||
@@ -46,37 +62,43 @@
|
||||
};
|
||||
}
|
||||
|
||||
const shape = $derived(decodePolyline(routing.currentTrip?.legs[0].shape || ""));
|
||||
const maneuver = $derived(routing.currentTripInfo.currentManeuver);
|
||||
const shape = $derived(
|
||||
decodePolyline(routing.currentTrip?.legs[0].shape || ""),
|
||||
);
|
||||
// const maneuver = $derived(routing.currentTripInfo.currentManeuver);
|
||||
|
||||
const fullDistance = $derived.by(() => {
|
||||
const lat = location.lat;
|
||||
const lon = location.lng;
|
||||
if (!shape.length) return 0;
|
||||
const lat = location.lat;
|
||||
const lon = location.lng;
|
||||
if (!shape.length) return 0;
|
||||
|
||||
// 1️⃣ find projection onto any segment of the full shape
|
||||
let best = { idx: 0, proj: shape[0], dist: Infinity };
|
||||
for (let i = 0; i < shape.length - 1; i++) {
|
||||
const a = shape[i];
|
||||
const b = shape[i + 1];
|
||||
const proj = projectPointToSegment({ lat, lon }, a, b);
|
||||
if (proj.distToUser < best.dist) {
|
||||
best = { idx: i, proj: { lat: proj.lat, lon: proj.lon }, dist: proj.distToUser };
|
||||
}
|
||||
}
|
||||
// 1️⃣ find projection onto any segment of the full shape
|
||||
let best = { idx: 0, proj: shape[0], dist: Infinity };
|
||||
for (let i = 0; i < shape.length - 1; i++) {
|
||||
const a = shape[i];
|
||||
const b = shape[i + 1];
|
||||
const proj = projectPointToSegment({ lat, lon }, a, b);
|
||||
if (proj.distToUser < best.dist) {
|
||||
best = {
|
||||
idx: i,
|
||||
proj: { lat: proj.lat, lon: proj.lon },
|
||||
dist: proj.distToUser,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ sum from the projection point to the very last point
|
||||
let total = 0;
|
||||
// from projection → next vertex
|
||||
total += haversine(best.proj, shape[best.idx + 1]);
|
||||
// 2️⃣ sum from the projection point to the very last point
|
||||
let total = 0;
|
||||
// from projection → next vertex
|
||||
total += haversine(best.proj, shape[best.idx + 1]);
|
||||
|
||||
// then each remaining segment
|
||||
for (let j = best.idx + 1; j < shape.length - 1; j++) {
|
||||
total += haversine(shape[j], shape[j + 1]);
|
||||
}
|
||||
// then each remaining segment
|
||||
for (let j = best.idx + 1; j < shape.length - 1; j++) {
|
||||
total += haversine(shape[j], shape[j + 1]);
|
||||
}
|
||||
|
||||
return total;
|
||||
});
|
||||
return total;
|
||||
});
|
||||
|
||||
const roundFullDistance = $derived.by(() => {
|
||||
const dist = Math.round(fullDistance);
|
||||
@@ -91,40 +113,50 @@
|
||||
} else {
|
||||
return Math.round(dist / 5000) * 5000;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const fullDistanceText = $derived.by(() => {
|
||||
const dist = roundFullDistance;
|
||||
if (dist < 1000) return `${dist} m`;
|
||||
return `${(dist / 1000)} km`;
|
||||
})
|
||||
return `${dist / 1000} km`;
|
||||
});
|
||||
</script>
|
||||
|
||||
{fullDistanceText} left
|
||||
|
||||
<Button onclick={() => {
|
||||
location.toggleLock();
|
||||
}}>LOCK</Button>
|
||||
<Button
|
||||
onclick={() => {
|
||||
location.toggleLock();
|
||||
}}>LOCK</Button
|
||||
>
|
||||
|
||||
<Button onclick={() => {
|
||||
stopNavigation();
|
||||
}}>End Trip</Button>
|
||||
<Button
|
||||
onclick={() => {
|
||||
stopNavigation();
|
||||
}}>End Trip</Button
|
||||
>
|
||||
|
||||
<div class="flex flex-col gap-2 mt-5">
|
||||
{#if location.code}
|
||||
<span>Share Code: {location.code}</span>
|
||||
<Button variant="secondary" onclick={() => {
|
||||
location.advertiser?.close();
|
||||
location.advertiser = null;
|
||||
location.code = null;
|
||||
}}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={() => {
|
||||
location.advertiser?.close();
|
||||
location.advertiser = null;
|
||||
location.code = null;
|
||||
}}
|
||||
>
|
||||
Stop Sharing Location
|
||||
</Button>
|
||||
{:else}
|
||||
<Button variant="secondary" onclick={() => {
|
||||
advertiseRemoteLocation();
|
||||
}}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={() => {
|
||||
advertiseRemoteLocation();
|
||||
}}
|
||||
>
|
||||
Share Trip Status & Location
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { POIIcons } from "$lib/POIIcons";
|
||||
import { OVERPASS_SERVER } from "$lib/services/hosts";
|
||||
import { BriefcaseIcon, EllipsisIcon, GlobeIcon, HomeIcon, MailIcon, PhoneIcon, RouteIcon } from "@lucide/svelte";
|
||||
import {
|
||||
BriefcaseIcon,
|
||||
EllipsisIcon,
|
||||
GlobeIcon,
|
||||
HomeIcon,
|
||||
MailIcon,
|
||||
PhoneIcon,
|
||||
RouteIcon,
|
||||
} from "@lucide/svelte";
|
||||
import { pin } from "../map.svelte";
|
||||
import SidebarHeader from "./SidebarHeader.svelte";
|
||||
import { fetchPOI, type OverpassElement } from "$lib/services/Overpass";
|
||||
@@ -13,57 +20,78 @@
|
||||
import * as Popover from "$lib/components/ui/popover";
|
||||
import Reviews from "../info/Reviews.svelte";
|
||||
import MapAi from "../info/MapAI.svelte";
|
||||
import { hasCapability } from "$lib/services/lnv";
|
||||
import RequiresCapability from "../RequiresCapability.svelte";
|
||||
|
||||
// let { feature }: { feature: Feature } = $props();
|
||||
|
||||
// let Icon = $derived(POIIcons[feature.properties.osm_key + "=" + feature.properties.osm_value]);
|
||||
|
||||
let { lat, lng }: { lat: number, lng: number } = $props();
|
||||
let { lat, lng }: { lat: number; lng: number } = $props();
|
||||
|
||||
function getIcon(tags: Record<string, string>): typeof POIIcons[keyof typeof POIIcons] | null {
|
||||
const key = Object.keys(tags).find(k => k.startsWith("amenity") || k.startsWith("shop"));
|
||||
function getIcon(
|
||||
tags: Record<string, string>,
|
||||
): (typeof POIIcons)[keyof typeof POIIcons] | null {
|
||||
const key = Object.keys(tags).find(
|
||||
(k) => k.startsWith("amenity") || k.startsWith("shop"),
|
||||
);
|
||||
if (key && POIIcons[key + "=" + tags[key]]) {
|
||||
return POIIcons[key + "=" + tags[key]];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDistance(aLat: number, aLon: number, lat: number, lon: number): number {
|
||||
function getDistance(
|
||||
aLat: number,
|
||||
aLon: number,
|
||||
lat: number,
|
||||
lon: number,
|
||||
): number {
|
||||
const R = 6371e3; // Earth radius in meters
|
||||
const φ1 = lat * Math.PI / 180;
|
||||
const φ2 = aLat * Math.PI / 180;
|
||||
const Δφ = (aLat - lat) * Math.PI / 180;
|
||||
const Δλ = (aLon - lon) * Math.PI / 180;
|
||||
const φ1 = (lat * Math.PI) / 180;
|
||||
const φ2 = (aLat * Math.PI) / 180;
|
||||
const Δφ = ((aLat - lat) * Math.PI) / 180;
|
||||
const Δλ = ((aLon - lon) * Math.PI) / 180;
|
||||
|
||||
const a = Math.sin(Δφ/2)**2 + Math.cos(φ1)*Math.cos(φ2)*Math.sin(Δλ/2)**2;
|
||||
const a =
|
||||
Math.sin(Δφ / 2) ** 2 +
|
||||
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2;
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c;
|
||||
}
|
||||
|
||||
function sortByDistance(elements: OverpassElement[], lat: number, lng: number): OverpassElement[] {
|
||||
function sortByDistance(
|
||||
elements: OverpassElement[],
|
||||
lat: number,
|
||||
lng: number,
|
||||
): OverpassElement[] {
|
||||
return elements.sort((a: OverpassElement, b: OverpassElement) => {
|
||||
const aLoc = a.center || a;
|
||||
const bLoc = b.center || b;
|
||||
return getDistance(aLoc.lat!, aLoc.lon!, lat, lng) - getDistance(bLoc.lat!, bLoc.lon!, lat, lng);
|
||||
return (
|
||||
getDistance(aLoc.lat!, aLoc.lon!, lat, lng) -
|
||||
getDistance(bLoc.lat!, bLoc.lon!, lat, lng)
|
||||
);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await fetchPOI(lat, lng, 20)}
|
||||
<SidebarHeader onback={() => {
|
||||
pin.liftPin();
|
||||
}}>
|
||||
<SidebarHeader
|
||||
onback={() => {
|
||||
pin.liftPin();
|
||||
}}
|
||||
>
|
||||
Dropped Pin
|
||||
</SidebarHeader>
|
||||
<p>Loading...</p>
|
||||
{:then res}
|
||||
{#if res.elements.length === 0}
|
||||
<SidebarHeader onback={() => {
|
||||
pin.liftPin();
|
||||
}}>
|
||||
<SidebarHeader
|
||||
onback={() => {
|
||||
pin.liftPin();
|
||||
}}
|
||||
>
|
||||
Dropped Pin
|
||||
</SidebarHeader>
|
||||
<span style="color: #acacac;">© OpenStreetMap</span>
|
||||
@@ -75,38 +103,57 @@
|
||||
{@const ellat = firstElement.center?.lat || firstElement.lat!}
|
||||
{@const ellng = firstElement.center?.lon || firstElement.lon!}
|
||||
|
||||
<SidebarHeader onback={() => {
|
||||
pin.liftPin();
|
||||
}}>
|
||||
<SidebarHeader
|
||||
onback={() => {
|
||||
pin.liftPin();
|
||||
}}
|
||||
>
|
||||
{#if getIcon(tags)}
|
||||
{@const Icon = getIcon(tags)}
|
||||
<Icon />
|
||||
{/if}
|
||||
{tags.name || (tags["addr:street"] ? (tags["addr:street"] + " " + tags["addr:housenumber"]) : "")}
|
||||
{tags.name ||
|
||||
(tags["addr:street"]
|
||||
? tags["addr:street"] + " " + tags["addr:housenumber"]
|
||||
: "")}
|
||||
</SidebarHeader>
|
||||
<div id="actions">
|
||||
<Button onclick={() => {
|
||||
view.switch("route", {
|
||||
to: lat + "," + lng,
|
||||
})
|
||||
}}>
|
||||
<Button
|
||||
onclick={() => {
|
||||
view.switch("route", {
|
||||
to: lat + "," + lng,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<RouteIcon />
|
||||
Route
|
||||
</Button>
|
||||
{#if tags.email || tags["contact:email"]}
|
||||
<Button variant="secondary" href={`mailto:${tags.email || tags["contact:email"]}`} target="_blank">
|
||||
<Button
|
||||
variant="secondary"
|
||||
href={`mailto:${tags.email || tags["contact:email"]}`}
|
||||
target="_blank"
|
||||
>
|
||||
<MailIcon />
|
||||
Email
|
||||
</Button>
|
||||
{/if}
|
||||
{#if tags.website || tags["contact:website"]}
|
||||
<Button variant="secondary" href={tags.website || tags["contact:website"]} target="_blank">
|
||||
<Button
|
||||
variant="secondary"
|
||||
href={tags.website || tags["contact:website"]}
|
||||
target="_blank"
|
||||
>
|
||||
<GlobeIcon />
|
||||
Website
|
||||
</Button>
|
||||
{/if}
|
||||
{#if tags.phone || tags["contact:phone"]}
|
||||
<Button variant="secondary" href={`tel:${tags.phone || tags["contact:phone"]}`} target="_blank">
|
||||
<Button
|
||||
variant="secondary"
|
||||
href={`tel:${tags.phone || tags["contact:phone"]}`}
|
||||
target="_blank"
|
||||
>
|
||||
<PhoneIcon />
|
||||
Call
|
||||
</Button>
|
||||
@@ -120,15 +167,27 @@
|
||||
</Popover.Trigger>
|
||||
<Popover.Content>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Button variant="outline" onclick={() => {
|
||||
localStorage.setItem("saved.home", JSON.stringify({ lat, lon: lng }));
|
||||
}}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
localStorage.setItem(
|
||||
"saved.home",
|
||||
JSON.stringify({ lat, lon: lng }),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<HomeIcon />
|
||||
Set as Home
|
||||
</Button>
|
||||
<Button variant="outline" onclick={() => {
|
||||
localStorage.setItem("saved.work", JSON.stringify({ lat, lon: lng }));
|
||||
}}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
localStorage.setItem(
|
||||
"saved.work",
|
||||
JSON.stringify({ lat, lon: lng }),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<BriefcaseIcon />
|
||||
Set as Work
|
||||
</Button>
|
||||
@@ -160,12 +219,11 @@
|
||||
{/if}
|
||||
|
||||
<!-- any payment:* tag -->
|
||||
{#if Object.keys(tags).some(key => key.startsWith("payment:"))}
|
||||
{#if Object.keys(tags).some((key) => key.startsWith("payment:"))}
|
||||
<h3 class="text-lg font-bold mt-2">Payment Methods</h3>
|
||||
<ul style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
||||
{#each Object.entries(tags).filter(([key]) => key.startsWith("payment:")) as [key, value]}
|
||||
<!-- <li>{key.replace("payment:", "")}: {value}</li> -->
|
||||
<Badge>{key.replace("payment:", "")}</Badge>
|
||||
{#each Object.entries(tags).filter( ([key]) => key.startsWith("payment:"), ) as [key, value] (key)}
|
||||
<Badge>{key.replace("payment:", "")}: {value}</Badge>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
@@ -175,13 +233,15 @@
|
||||
</RequiresCapability>
|
||||
|
||||
<span style="color: #acacac;">© OpenStreetMap</span>
|
||||
|
||||
|
||||
<pre>{JSON.stringify(elements, null, 2)}</pre>
|
||||
{/if}
|
||||
{:catch err}
|
||||
<SidebarHeader onback={() => {
|
||||
pin.liftPin();
|
||||
}}>
|
||||
<SidebarHeader
|
||||
onback={() => {
|
||||
pin.liftPin();
|
||||
}}
|
||||
>
|
||||
Dropped Pin
|
||||
</SidebarHeader>
|
||||
<p>Error: {err.message}</p>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
<h1>Error</h1>
|
||||
<p>Invalid sidebar configuration.</p>
|
||||
<p>Invalid sidebar configuration.</p>
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { BriefcaseIcon, HomeIcon } from "@lucide/svelte";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { fly } from "svelte/transition";
|
||||
import { circInOut } from "svelte/easing";
|
||||
import { search, type Feature } from "$lib/services/Search";
|
||||
import { view } from "../sidebar.svelte";
|
||||
import { map, pin } from "../map.svelte";
|
||||
|
||||
function debounce<T>(getter: () => T, delay: number): () => T | undefined {
|
||||
let value = $state<T>();
|
||||
let timer: NodeJS.Timeout;
|
||||
$effect(() => {
|
||||
const newValue = getter(); // read here to subscribe to it
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => value = newValue, delay);
|
||||
return () => clearTimeout(timer);
|
||||
});
|
||||
return () => value;
|
||||
}
|
||||
|
||||
let typedText = $state("");
|
||||
let loading = $state(false);
|
||||
|
||||
let searchText = $derived.by(debounce(() => typedText, 300));
|
||||
let searchResults: Feature[] = $state([]);
|
||||
|
||||
$effect(() => {
|
||||
if(!searchText) {
|
||||
searchResults = [];
|
||||
return;
|
||||
}
|
||||
if (searchText.length > 0) {
|
||||
loading = true;
|
||||
search(searchText, 0, 0).then(results => {
|
||||
searchResults = results;
|
||||
loading = false;
|
||||
});
|
||||
} else {
|
||||
searchResults = [];
|
||||
}
|
||||
});
|
||||
|
||||
$inspect("searchText", searchText);
|
||||
</script>
|
||||
|
||||
<div id="search-progress" style="min-height: calc(3px + 3px); width: 100%; min-height: 3ch;">
|
||||
{#if loading}
|
||||
LOADING
|
||||
{/if}
|
||||
</div>
|
||||
<Input placeholder="Search..." bind:value={typedText} class="mb-2" />
|
||||
{#if searchResults.length == 0}
|
||||
<div id="saved" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
const home = localStorage.getItem("saved.home");
|
||||
if(!home) {
|
||||
alert("No home location saved.");
|
||||
return;
|
||||
}
|
||||
const {lat, lon} = JSON.parse(home);
|
||||
pin.dropPin(lat, lon);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
<HomeIcon />
|
||||
Home
|
||||
</Button>
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
const work = localStorage.getItem("saved.work");
|
||||
if(!work) {
|
||||
alert("No home location saved.");
|
||||
return;
|
||||
}
|
||||
const {lat, lon} = JSON.parse(work);
|
||||
pin.dropPin(lat, lon);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
<BriefcaseIcon />
|
||||
Work
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div id="results" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
|
||||
{#each searchResults as result}
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
// view.switch("info", { feature: result });
|
||||
pin.dropPin(result.geometry.coordinates[1], result.geometry.coordinates[0]);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [result.geometry.coordinates[0], result.geometry.coordinates[1]],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
{result.properties.name}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
#saved {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
/* justify-content: space-evenly; */
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -6,41 +6,53 @@
|
||||
import { map, pin } from "../map.svelte";
|
||||
import VehicleSelector from "../VehicleSelector.svelte";
|
||||
import Post from "../Post.svelte";
|
||||
import RequiresCapability from "../RequiresCapability.svelte";
|
||||
import RequiresCapability from "../RequiresCapability.svelte";
|
||||
</script>
|
||||
|
||||
<div id="saved" class="mt-2 mb-2" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
const home = localStorage.getItem("saved.home");
|
||||
if(!home) {
|
||||
alert("No home location saved.");
|
||||
return;
|
||||
}
|
||||
const {lat, lon} = JSON.parse(home);
|
||||
pin.dropPin(lat, lon);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
<div
|
||||
id="saved"
|
||||
class="mt-2 mb-2"
|
||||
in:fly={{ y: 20, duration: 200, easing: circInOut }}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="flex-1"
|
||||
onclick={() => {
|
||||
const home = localStorage.getItem("saved.home");
|
||||
if (!home) {
|
||||
alert("No home location saved.");
|
||||
return;
|
||||
}
|
||||
const { lat, lon } = JSON.parse(home);
|
||||
pin.dropPin(lat, lon);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 19,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<HomeIcon />
|
||||
Home
|
||||
</Button>
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
const work = localStorage.getItem("saved.work");
|
||||
if(!work) {
|
||||
alert("No work location saved.");
|
||||
return;
|
||||
}
|
||||
const {lat, lon} = JSON.parse(work);
|
||||
pin.dropPin(lat, lon);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="flex-1"
|
||||
onclick={() => {
|
||||
const work = localStorage.getItem("saved.work");
|
||||
if (!work) {
|
||||
alert("No work location saved.");
|
||||
return;
|
||||
}
|
||||
const { lat, lon } = JSON.parse(work);
|
||||
pin.dropPin(lat, lon);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 19,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<BriefcaseIcon />
|
||||
Work
|
||||
</Button>
|
||||
@@ -51,7 +63,7 @@
|
||||
<RequiresCapability capability="post">
|
||||
<div>
|
||||
<h2>In your area</h2>
|
||||
|
||||
|
||||
<Post />
|
||||
</div>
|
||||
</RequiresCapability>
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { CircleArrowDown, CircleDotIcon, StarIcon } from "@lucide/svelte";
|
||||
import LocationSelect from "../LocationSelect.svelte";
|
||||
import Input from "$lib/components/ui/input/input.svelte";
|
||||
import SidebarHeader from "./SidebarHeader.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { createValhallaRequest } from "$lib/vehicles/ValhallaVehicles";
|
||||
import { drawAllRoutes, fetchRoute, removeAllRoutes, zoomToPoints } from "$lib/services/navigation/routing.svelte";
|
||||
import {
|
||||
drawAllRoutes,
|
||||
fetchRoute,
|
||||
removeAllRoutes,
|
||||
zoomToPoints,
|
||||
} from "$lib/services/navigation/routing.svelte";
|
||||
import { ROUTING_SERVER } from "$lib/services/hosts";
|
||||
import { map } from "../map.svelte";
|
||||
import { view } from "../sidebar.svelte";
|
||||
import { DefaultVehicle, selectedVehicle } from "$lib/vehicles/vehicles.svelte";
|
||||
import {
|
||||
DefaultVehicle,
|
||||
selectedVehicle,
|
||||
} from "$lib/vehicles/vehicles.svelte";
|
||||
|
||||
let { from, to }: {
|
||||
from?: string,
|
||||
to?: string
|
||||
let {
|
||||
from,
|
||||
to,
|
||||
}: {
|
||||
from?: string;
|
||||
to?: string;
|
||||
} = $props();
|
||||
|
||||
let fromLocation = $state(from || "");
|
||||
@@ -29,13 +39,18 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<SidebarHeader onback={() => {
|
||||
removeAllRoutes();
|
||||
}}>
|
||||
<SidebarHeader
|
||||
onback={() => {
|
||||
removeAllRoutes();
|
||||
}}
|
||||
>
|
||||
Route
|
||||
</SidebarHeader>
|
||||
|
||||
<span>Driving with <strong>{(selectedVehicle() ?? DefaultVehicle).name}</strong></span>
|
||||
<span
|
||||
>Driving with <strong>{(selectedVehicle() ?? DefaultVehicle).name}</strong
|
||||
></span
|
||||
>
|
||||
<div class="flex flex-col gap-2 w-full mb-2">
|
||||
<div class="flex gap-2 items-center w-full">
|
||||
<CircleDotIcon />
|
||||
@@ -49,39 +64,51 @@
|
||||
<Input bind:value={toLocation} />
|
||||
</div>
|
||||
</div>
|
||||
<Button onclick={async () => {
|
||||
const FROM: WorldLocation = fromLocation == "home" ? JSON.parse(localStorage.getItem("saved.home")!)
|
||||
: fromLocation == "work" ? JSON.parse(localStorage.getItem("saved.work")!)
|
||||
: {
|
||||
lat: parseFloat(fromLocation.split(",")[0]),
|
||||
lon: parseFloat(fromLocation.split(",")[1])
|
||||
};
|
||||
const TO: WorldLocation = toLocation == "home" ? JSON.parse(localStorage.getItem("saved.home")!)
|
||||
: toLocation == "work" ? JSON.parse(localStorage.getItem("saved.work")!)
|
||||
: {
|
||||
lat: parseFloat(toLocation.split(",")[0]),
|
||||
lon: parseFloat(toLocation.split(",")[1])
|
||||
};
|
||||
const req = createValhallaRequest(selectedVehicle() ?? DefaultVehicle, [FROM, TO]);
|
||||
const res = await fetchRoute(ROUTING_SERVER, req);
|
||||
routes = [
|
||||
res.trip,
|
||||
];
|
||||
for(const alternate of res.alternates) {
|
||||
if(alternate.trip) {
|
||||
routes.push(alternate.trip);
|
||||
<Button
|
||||
onclick={async () => {
|
||||
const FROM: WorldLocation =
|
||||
fromLocation == "home"
|
||||
? JSON.parse(localStorage.getItem("saved.home")!)
|
||||
: fromLocation == "work"
|
||||
? JSON.parse(localStorage.getItem("saved.work")!)
|
||||
: {
|
||||
lat: parseFloat(fromLocation.split(",")[0]),
|
||||
lon: parseFloat(fromLocation.split(",")[1]),
|
||||
};
|
||||
const TO: WorldLocation =
|
||||
toLocation == "home"
|
||||
? JSON.parse(localStorage.getItem("saved.home")!)
|
||||
: toLocation == "work"
|
||||
? JSON.parse(localStorage.getItem("saved.work")!)
|
||||
: {
|
||||
lat: parseFloat(toLocation.split(",")[0]),
|
||||
lon: parseFloat(toLocation.split(",")[1]),
|
||||
};
|
||||
const req = createValhallaRequest(selectedVehicle() ?? DefaultVehicle, [
|
||||
FROM,
|
||||
TO,
|
||||
]);
|
||||
const res = await fetchRoute(ROUTING_SERVER, req);
|
||||
routes = [res.trip];
|
||||
for (const alternate of res.alternates) {
|
||||
if (alternate.trip) {
|
||||
routes.push(alternate.trip);
|
||||
}
|
||||
}
|
||||
}
|
||||
drawAllRoutes(routes);
|
||||
zoomToPoints(FROM, TO, map.value!);
|
||||
}}>Calculate</Button>
|
||||
drawAllRoutes(routes);
|
||||
zoomToPoints(FROM, TO, map.value!);
|
||||
}}>Calculate</Button
|
||||
>
|
||||
|
||||
{#if routes}
|
||||
<div class="mt-2 flex gap-2 flex-col">
|
||||
{#each routes as route, i (route?.summary?.length)}
|
||||
<Button variant="secondary" onclick={() => {
|
||||
view.switch("trip", { route });
|
||||
}}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={() => {
|
||||
view.switch("trip", { route });
|
||||
}}
|
||||
>
|
||||
{#if i == 0}
|
||||
<StarIcon />
|
||||
{/if}
|
||||
@@ -89,4 +116,4 @@
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -7,29 +7,48 @@
|
||||
import SidebarHeader from "./SidebarHeader.svelte";
|
||||
import { searchbar } from "../sidebar.svelte";
|
||||
|
||||
let { results, query }: {
|
||||
results: Feature[],
|
||||
query: string
|
||||
let {
|
||||
results,
|
||||
query,
|
||||
}: {
|
||||
results: Feature[];
|
||||
query: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SidebarHeader onback={() => {
|
||||
searchbar.text = "";
|
||||
}}>
|
||||
Search Results
|
||||
<SidebarHeader
|
||||
onback={() => {
|
||||
searchbar.text = "";
|
||||
}}
|
||||
>
|
||||
Search Results for "{query}"
|
||||
</SidebarHeader>
|
||||
<div id="results" class="mt-2" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
|
||||
{#each results as result}
|
||||
<Button variant="secondary" class="flex-1" onclick={() => {
|
||||
// view.switch("info", { feature: result });
|
||||
pin.dropPin(result.geometry.coordinates[1], result.geometry.coordinates[0]);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [result.geometry.coordinates[0], result.geometry.coordinates[1]],
|
||||
zoom: 19
|
||||
});
|
||||
}}>
|
||||
<div
|
||||
id="results"
|
||||
class="mt-2"
|
||||
in:fly={{ y: 20, duration: 200, easing: circInOut }}
|
||||
>
|
||||
{#each results as result (result.properties.osm_id)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="flex-1"
|
||||
onclick={() => {
|
||||
// view.switch("info", { feature: result });
|
||||
pin.dropPin(
|
||||
result.geometry.coordinates[1],
|
||||
result.geometry.coordinates[0],
|
||||
);
|
||||
pin.showInfo();
|
||||
map.value?.flyTo({
|
||||
center: [
|
||||
result.geometry.coordinates[0],
|
||||
result.geometry.coordinates[1],
|
||||
],
|
||||
zoom: 19,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{result.properties.name}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,18 +3,26 @@
|
||||
import type { Snippet } from "svelte";
|
||||
import { view } from "../sidebar.svelte";
|
||||
|
||||
let { children, onback }: {
|
||||
children: Snippet,
|
||||
onback?: () => void,
|
||||
let {
|
||||
children,
|
||||
onback,
|
||||
}: {
|
||||
children: Snippet;
|
||||
onback?: () => void;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
<Button variant="outline" onclick={() => {
|
||||
view.back();
|
||||
if (onback) {
|
||||
onback();
|
||||
}
|
||||
}}><</Button>
|
||||
<h2 class="text-lg font-bold flex gap-2 items-center">{@render children?.()}</h2>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
view.back();
|
||||
if (onback) {
|
||||
onback();
|
||||
}
|
||||
}}><</Button
|
||||
>
|
||||
<h2 class="text-lg font-bold flex gap-2 items-center">
|
||||
{@render children?.()}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import SidebarHeader from "./SidebarHeader.svelte";
|
||||
import { drawRoute, removeAllRoutes, startRoute } from "$lib/services/navigation/routing.svelte";
|
||||
import {
|
||||
drawRoute,
|
||||
removeAllRoutes,
|
||||
startRoute,
|
||||
} from "$lib/services/navigation/routing.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { RouteIcon, SaveIcon, SendIcon } from "@lucide/svelte";
|
||||
import { map } from "../map.svelte";
|
||||
import { map } from "../map.svelte";
|
||||
|
||||
let { route }: {
|
||||
route: Trip
|
||||
let {
|
||||
route,
|
||||
}: {
|
||||
route: Trip;
|
||||
} = $props();
|
||||
|
||||
onMount(() => {
|
||||
removeAllRoutes();
|
||||
drawRoute(route);
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<SidebarHeader onback={() => {
|
||||
removeAllRoutes();
|
||||
}}>
|
||||
<SidebarHeader
|
||||
onback={() => {
|
||||
removeAllRoutes();
|
||||
}}
|
||||
>
|
||||
Trip Details
|
||||
</SidebarHeader>
|
||||
|
||||
<div id="actions" class="flex gap-2">
|
||||
<Button onclick={async () => {
|
||||
await startRoute(route);
|
||||
requestAnimationFrame(() => {
|
||||
map.updateMapPadding();
|
||||
})
|
||||
}}>
|
||||
<Button
|
||||
onclick={async () => {
|
||||
await startRoute(route);
|
||||
requestAnimationFrame(() => {
|
||||
map.updateMapPadding();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<RouteIcon />
|
||||
Start Navigation
|
||||
</Button>
|
||||
@@ -43,9 +53,9 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 mt-2">
|
||||
{#each route.legs[0].maneuvers as maneuver}
|
||||
{#each route.legs[0].maneuvers as maneuver (maneuver)}
|
||||
<li>
|
||||
{maneuver.instruction}
|
||||
</li>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,49 +5,60 @@
|
||||
import { getAuthURL, getOIDCUser } from "$lib/services/oidc";
|
||||
import * as Avatar from "$lib/components/ui/avatar";
|
||||
|
||||
let user: any = $state(null);
|
||||
interface OIDCUser {
|
||||
sub: string;
|
||||
preferred_username: string;
|
||||
name?: string;
|
||||
picture?: string;
|
||||
}
|
||||
|
||||
let user: OIDCUser | null = $state(null);
|
||||
|
||||
onMount(() => {
|
||||
if(!localStorage.getItem("lnv-token")) {
|
||||
if (!localStorage.getItem("lnv-token")) {
|
||||
user = null;
|
||||
} else {
|
||||
user = JSON.parse(atob((localStorage.getItem("lnv-id") || "").split(".")[1]));
|
||||
user = JSON.parse(
|
||||
atob((localStorage.getItem("lnv-id") || "").split(".")[1]),
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !user}
|
||||
<SidebarHeader>
|
||||
User
|
||||
</SidebarHeader>
|
||||
<SidebarHeader>User</SidebarHeader>
|
||||
|
||||
<Button onclick={async () => {
|
||||
const auth = await getAuthURL();
|
||||
// localStorage.setItem("lnv-codeVerifier", auth.codeVerifier);
|
||||
// localStorage.setItem("lnv-oidcstate", auth.state);
|
||||
const popup = window.open(auth.url, "Login", "width=500,height=600");
|
||||
window.addEventListener("message", async (e) => {
|
||||
if(e.origin !== window.location.origin) return;
|
||||
<Button
|
||||
onclick={async () => {
|
||||
const auth = await getAuthURL();
|
||||
// localStorage.setItem("lnv-codeVerifier", auth.codeVerifier);
|
||||
// localStorage.setItem("lnv-oidcstate", auth.state);
|
||||
const popup = window.open(auth.url, "Login", "width=500,height=600");
|
||||
window.addEventListener("message", async (e) => {
|
||||
if (e.origin !== window.location.origin) return;
|
||||
|
||||
const { code, state } = e.data;
|
||||
console.log("Received data from popup:", e.data);
|
||||
if(!code || !state) {
|
||||
console.error("Invalid response from popup");
|
||||
return;
|
||||
}
|
||||
popup?.close();
|
||||
if(state !== auth.state) {
|
||||
alert("State mismatch. Please try again.");
|
||||
return;
|
||||
}
|
||||
const { code, state } = e.data;
|
||||
console.log("Received data from popup:", e.data);
|
||||
if (!code || !state) {
|
||||
console.error("Invalid response from popup");
|
||||
return;
|
||||
}
|
||||
popup?.close();
|
||||
if (state !== auth.state) {
|
||||
alert("State mismatch. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await getOIDCUser(code, auth.codeVerifier);
|
||||
localStorage.setItem("lnv-id", token.id_token);
|
||||
localStorage.setItem("lnv-token", token.access_token);
|
||||
localStorage.setItem("lnv-refresh", token.refresh_token);
|
||||
user = JSON.parse(atob((localStorage.getItem("lnv-id") || "").split(".")[1]));
|
||||
})
|
||||
}}>Login</Button>
|
||||
const token = await getOIDCUser(code, auth.codeVerifier);
|
||||
localStorage.setItem("lnv-id", token.id_token);
|
||||
localStorage.setItem("lnv-token", token.access_token);
|
||||
localStorage.setItem("lnv-refresh", token.refresh_token);
|
||||
user = JSON.parse(
|
||||
atob((localStorage.getItem("lnv-id") || "").split(".")[1]),
|
||||
);
|
||||
});
|
||||
}}>Login</Button
|
||||
>
|
||||
{:else}
|
||||
<SidebarHeader>
|
||||
<Avatar.Root>
|
||||
@@ -58,4 +69,4 @@
|
||||
</SidebarHeader>
|
||||
<pre>{user.sub}</pre>
|
||||
{JSON.stringify(user, null, 2)}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<AvatarPrimitive.Fallback
|
||||
bind:ref
|
||||
data-slot="avatar-fallback"
|
||||
class={cn("bg-muted flex size-full items-center justify-center rounded-full", className)}
|
||||
class={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<AvatarPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="avatar"
|
||||
class={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
|
||||
class={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
||||
destructive:
|
||||
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
|
||||
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
import type {
|
||||
HTMLAnchorAttributes,
|
||||
HTMLButtonAttributes,
|
||||
} from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
|
||||
outline:
|
||||
"bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
|
||||
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-action"
|
||||
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||
class={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-content"
|
||||
class={cn("px-6", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
data-slot="card-header"
|
||||
class={cn(
|
||||
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
data-slot="card"
|
||||
class={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { Command as CommandPrimitive, Dialog as DialogPrimitive } from "bits-ui";
|
||||
import type {
|
||||
Command as CommandPrimitive,
|
||||
Dialog as DialogPrimitive,
|
||||
} from "bits-ui";
|
||||
import type { Snippet } from "svelte";
|
||||
import Command from "./command.svelte";
|
||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||
|
||||
@@ -11,13 +11,16 @@
|
||||
}: CommandPrimitive.InputProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex h-9 items-center gap-2 border-b px-3" data-slot="command-input-wrapper">
|
||||
<div
|
||||
class="flex h-9 items-center gap-2 border-b px-3"
|
||||
data-slot="command-input-wrapper"
|
||||
>
|
||||
<SearchIcon class="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
class={cn(
|
||||
"placeholder:text-muted-foreground outline-hidden flex h-10 w-full rounded-md bg-transparent py-3 text-sm disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
bind:ref
|
||||
{...restProps}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
data-slot="command-item"
|
||||
class={cn(
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
data-slot="command-item"
|
||||
class={cn(
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<CommandPrimitive.List
|
||||
bind:ref
|
||||
data-slot="command-list"
|
||||
class={cn("max-h-[300px] scroll-py-1 overflow-y-auto overflow-x-hidden", className)}
|
||||
class={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-y-auto overflow-x-hidden",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
data-slot="command"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
data-slot="dialog-content"
|
||||
class={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-footer"
|
||||
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
class={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
data-slot="dialog-overlay"
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.CloseProps = $props();
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.CloseProps =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Close bind:ref data-slot="drawer-close" {...restProps} />
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
|
||||
@@ -9,4 +9,9 @@
|
||||
}: DrawerPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
|
||||
<DrawerPrimitive.NestedRoot
|
||||
{shouldScaleBackground}
|
||||
bind:open
|
||||
bind:activeSnapPoint
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
data-slot="drawer-overlay"
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.TriggerProps = $props();
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.TriggerProps =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Trigger bind:ref data-slot="drawer-trigger" {...restProps} />
|
||||
|
||||
@@ -9,4 +9,9 @@
|
||||
}: DrawerPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
|
||||
<DrawerPrimitive.Root
|
||||
{shouldScaleBackground}
|
||||
bind:open
|
||||
bind:activeSnapPoint
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
||||
import type {
|
||||
HTMLInputAttributes,
|
||||
HTMLInputTypeAttribute,
|
||||
} from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, "type"> &
|
||||
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||
(
|
||||
| { type: "file"; files?: FileList }
|
||||
| { type?: InputType; files?: undefined }
|
||||
)
|
||||
>;
|
||||
|
||||
let {
|
||||
@@ -27,7 +33,7 @@
|
||||
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
@@ -42,7 +48,7 @@
|
||||
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{align}
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-popover-content-transform-origin) outline-hidden z-50 w-72 rounded-md border p-4 shadow-md",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -23,14 +23,14 @@
|
||||
data-slot="select-content"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--bits-select-content-available-height) origin-(--bits-select-content-transform-origin) relative z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
class={cn(
|
||||
"h-(--bits-select-anchor-height) min-w-(--bits-select-anchor-width) w-full scroll-my-1 p-1"
|
||||
"h-(--bits-select-anchor-height) min-w-(--bits-select-anchor-width) w-full scroll-my-1 p-1",
|
||||
)}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
|
||||
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Group data-slot="select-group" {...restProps} />
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
data-slot="select-item"
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 shadow-xs flex w-fit select-none items-center justify-between gap-2 whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
data-slot="separator"
|
||||
class={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LNV_SERVER } from "./hosts";
|
||||
import { hasCapability } from "./lnv";
|
||||
|
||||
type Station = {
|
||||
interface Station {
|
||||
id: string;
|
||||
name: string;
|
||||
brand: string;
|
||||
@@ -18,13 +18,13 @@ type Station = {
|
||||
postCode: number;
|
||||
}
|
||||
|
||||
type StationsResponse = {
|
||||
interface StationsResponse {
|
||||
ok: boolean;
|
||||
license: "CC BY 4.0 - https:\/\/creativecommons.tankerkoenig.de";
|
||||
license: "CC BY 4.0 - https://creativecommons.tankerkoenig.de";
|
||||
data: "MTS-K";
|
||||
status: string;
|
||||
stations: Station[];
|
||||
};
|
||||
}
|
||||
|
||||
type StationDetails = {
|
||||
openingTimes: StationOpeningTime[];
|
||||
@@ -32,37 +32,49 @@ type StationDetails = {
|
||||
wholeDay: boolean;
|
||||
} & Station;
|
||||
|
||||
type StationOpeningTime = {
|
||||
interface StationOpeningTime {
|
||||
text: string;
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
}
|
||||
|
||||
type StationDetailsResponse = {
|
||||
interface StationDetailsResponse {
|
||||
ok: boolean;
|
||||
license: "CC BY 4.0 - https:\/\/creativecommons.tankerkoenig.de";
|
||||
license: "CC BY 4.0 - https://creativecommons.tankerkoenig.de";
|
||||
data: "MTS-K";
|
||||
status: string;
|
||||
station: StationDetails;
|
||||
};
|
||||
|
||||
export async function getStations(lat: number, lon: number): Promise<StationsResponse> {
|
||||
if(!await hasCapability("fuel")) {
|
||||
throw new Error("Fuel capability is not available");
|
||||
}
|
||||
return await fetch(`${LNV_SERVER}/fuel/list?lat=${lat}&lng=${lon}&rad=1&sort=dist&type=all`).then(res => res.json());
|
||||
}
|
||||
|
||||
export async function getPrices(id: string) { // TODO: add type
|
||||
if(!await hasCapability("fuel")) {
|
||||
export async function getStations(
|
||||
lat: number,
|
||||
lon: number,
|
||||
): Promise<StationsResponse> {
|
||||
if (!(await hasCapability("fuel"))) {
|
||||
throw new Error("Fuel capability is not available");
|
||||
}
|
||||
return await fetch(`${LNV_SERVER}/fuel/prices?ids=${id}`).then(res => res.json());
|
||||
return await fetch(
|
||||
`${LNV_SERVER}/fuel/list?lat=${lat}&lng=${lon}&rad=1&sort=dist&type=all`,
|
||||
).then((res) => res.json());
|
||||
}
|
||||
|
||||
export async function getStationDetails(id: string): Promise<StationDetailsResponse> {
|
||||
if(!await hasCapability("fuel")) {
|
||||
export async function getPrices(id: string) {
|
||||
// TODO: add type
|
||||
if (!(await hasCapability("fuel"))) {
|
||||
throw new Error("Fuel capability is not available");
|
||||
}
|
||||
return await fetch(`${LNV_SERVER}/fuel/detail?id=${id}`).then(res => res.json());
|
||||
return await fetch(`${LNV_SERVER}/fuel/prices?ids=${id}`).then((res) =>
|
||||
res.json(),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getStationDetails(
|
||||
id: string,
|
||||
): Promise<StationDetailsResponse> {
|
||||
if (!(await hasCapability("fuel"))) {
|
||||
throw new Error("Fuel capability is not available");
|
||||
}
|
||||
return await fetch(`${LNV_SERVER}/fuel/detail?id=${id}`).then((res) =>
|
||||
res.json(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { OVERPASS_SERVER } from "./hosts";
|
||||
|
||||
export type OverpassResult = {
|
||||
export interface OverpassResult {
|
||||
elements: OverpassElement[];
|
||||
};
|
||||
}
|
||||
|
||||
export type OverpassElement = {
|
||||
export interface OverpassElement {
|
||||
type: "node" | "way" | "relation";
|
||||
id: number;
|
||||
tags: Record<string, string>;
|
||||
@@ -15,7 +15,7 @@ export type OverpassElement = {
|
||||
lat: number; // Only for relations
|
||||
lon: number; // Only for relations
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
[out:json];
|
||||
@@ -40,11 +40,7 @@ out geom;
|
||||
out geom;
|
||||
*/
|
||||
|
||||
export async function fetchPOI(
|
||||
lat: number,
|
||||
lon: number,
|
||||
radius: number,
|
||||
) {
|
||||
export async function fetchPOI(lat: number, lon: number, radius: number) {
|
||||
return await fetch(OVERPASS_SERVER, {
|
||||
method: "POST",
|
||||
body: `[out:json];
|
||||
@@ -60,6 +56,6 @@ export async function fetchPOI(
|
||||
node(around:${radius}, ${lat}, ${lon})["amenity"="parking"];
|
||||
way(around:${radius}, ${lat}, ${lon})["amenity"="parking"];
|
||||
);
|
||||
out center tags;`
|
||||
}).then(res => res.json() as Promise<OverpassResult>);
|
||||
}
|
||||
out center tags;`,
|
||||
}).then((res) => res.json() as Promise<OverpassResult>);
|
||||
}
|
||||
|
||||
@@ -2,38 +2,50 @@
|
||||
import { SEARCH_SERVER } from "./hosts";
|
||||
// import { Capacitor } from "@capacitor/core";
|
||||
|
||||
export type Feature = {
|
||||
type: "Feature",
|
||||
export interface Feature {
|
||||
type: "Feature";
|
||||
geometry: {
|
||||
coordinates: [number, number],
|
||||
type: "Point"
|
||||
},
|
||||
coordinates: [number, number];
|
||||
type: "Point";
|
||||
};
|
||||
properties: {
|
||||
osm_key: string;
|
||||
osm_value: string;
|
||||
osm_id: number,
|
||||
city: string,
|
||||
country: string,
|
||||
name: string,
|
||||
street: string,
|
||||
housenumber: string,
|
||||
type: string,
|
||||
osm_id: number;
|
||||
city: string;
|
||||
country: string;
|
||||
name: string;
|
||||
street: string;
|
||||
housenumber: string;
|
||||
type: string;
|
||||
// There is more, but not needed atm
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function searchPlaces(query: string, lat: number, lon: number): Promise<Feature[]> {
|
||||
const res = await fetch(SEARCH_SERVER + "/api/?q=" + query + "&lat=" + lat + "&lon=" + lon).then((res) => res.json());
|
||||
export async function searchPlaces(
|
||||
query: string,
|
||||
lat: number,
|
||||
lon: number,
|
||||
): Promise<Feature[]> {
|
||||
const res = await fetch(
|
||||
SEARCH_SERVER + "/api/?q=" + query + "&lat=" + lat + "&lon=" + lon,
|
||||
).then((res) => res.json());
|
||||
return res.features;
|
||||
}
|
||||
|
||||
export async function reverseGeocode(coord: WorldLocation): Promise<Feature[]> {
|
||||
const res = await fetch(SEARCH_SERVER + "/reverse?lat=" + coord.lat + "&lon=" + coord.lon).then((res) => res.json());
|
||||
const res = await fetch(
|
||||
SEARCH_SERVER + "/reverse?lat=" + coord.lat + "&lon=" + coord.lon,
|
||||
).then((res) => res.json());
|
||||
return res.features;
|
||||
}
|
||||
|
||||
export async function search(query: string, lat: number, lon: number): Promise<Feature[]> {
|
||||
if(query.startsWith("@")) {
|
||||
export async function search(
|
||||
query: string,
|
||||
lat: number,
|
||||
lon: number,
|
||||
): Promise<Feature[]> {
|
||||
if (query.startsWith("@")) {
|
||||
// if(Capacitor.isNativePlatform()) {
|
||||
// return await searchContacts(query, lat, lon);
|
||||
// }
|
||||
|
||||
@@ -4,4 +4,7 @@ export const ROUTING_SERVER = "https://valhalla1.openstreetmap.de/";
|
||||
// export const ROUTING_SERVER = "https://routing.map.picoscratch.de";
|
||||
export const SEARCH_SERVER = "https://photon.komoot.io/";
|
||||
export const OVERPASS_SERVER = "https://overpass-api.de/api/interpreter";
|
||||
export const LNV_SERVER = location.hostname == "localhost" ? "http://localhost:3000/api" : "https://trafficcue-api.picoscratch.de/api";
|
||||
export const LNV_SERVER =
|
||||
location.hostname == "localhost"
|
||||
? "http://localhost:3000/api"
|
||||
: "https://trafficcue-api.picoscratch.de/api";
|
||||
|
||||
@@ -2,7 +2,11 @@ import { LNV_SERVER } from "./hosts";
|
||||
|
||||
export type Capabilities = ("auth" | "reviews" | "ai" | "fuel" | "post")[];
|
||||
export let capabilities: Capabilities = [];
|
||||
export let oidcConfig: { AUTH_URL: string; CLIENT_ID: string; TOKEN_URL: string } | null = null;
|
||||
export let oidcConfig: {
|
||||
AUTH_URL: string;
|
||||
CLIENT_ID: string;
|
||||
TOKEN_URL: string;
|
||||
} | null = null;
|
||||
|
||||
export async function fetchConfig() {
|
||||
const res = await fetch(LNV_SERVER + "/config");
|
||||
@@ -10,7 +14,12 @@ export async function fetchConfig() {
|
||||
throw new Error(`Failed to fetch capabilities: ${res.statusText}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
return data as { name: string; version: string; capabilities: Capabilities; oidc?: { AUTH_URL: string; CLIENT_ID: string; TOKEN_URL: string } };
|
||||
return data as {
|
||||
name: string;
|
||||
version: string;
|
||||
capabilities: Capabilities;
|
||||
oidc?: { AUTH_URL: string; CLIENT_ID: string; TOKEN_URL: string };
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCapabilities() {
|
||||
@@ -30,28 +39,32 @@ export async function getOIDCConfig() {
|
||||
oidcConfig = {
|
||||
AUTH_URL: config.oidc.AUTH_URL,
|
||||
CLIENT_ID: config.oidc.CLIENT_ID,
|
||||
TOKEN_URL: config.oidc.TOKEN_URL
|
||||
TOKEN_URL: config.oidc.TOKEN_URL,
|
||||
};
|
||||
}
|
||||
return oidcConfig;
|
||||
}
|
||||
|
||||
export async function hasCapability(capability: Capabilities[number]): Promise<boolean> {
|
||||
export async function hasCapability(
|
||||
capability: Capabilities[number],
|
||||
): Promise<boolean> {
|
||||
const caps = await getCapabilities();
|
||||
return caps.includes(capability);
|
||||
}
|
||||
|
||||
export type Review = {
|
||||
export interface Review {
|
||||
user_id: string;
|
||||
username: string;
|
||||
rating: number;
|
||||
comment: string;
|
||||
}
|
||||
export async function getReviews(location: WorldLocation) {
|
||||
if(!await hasCapability("reviews")) {
|
||||
if (!(await hasCapability("reviews"))) {
|
||||
throw new Error("Reviews capability is not available");
|
||||
}
|
||||
const res = await fetch(LNV_SERVER + `/reviews?lat=${location.lat}&lon=${location.lon}`);
|
||||
const res = await fetch(
|
||||
LNV_SERVER + `/reviews?lat=${location.lat}&lon=${location.lon}`,
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch reviews: ${res.statusText}`);
|
||||
}
|
||||
@@ -59,8 +72,11 @@ export async function getReviews(location: WorldLocation) {
|
||||
return data as Review[];
|
||||
}
|
||||
|
||||
export async function postReview(location: WorldLocation, review: Omit<Review, 'user_id' | 'username'>) {
|
||||
if(!await hasCapability("reviews")) {
|
||||
export async function postReview(
|
||||
location: WorldLocation,
|
||||
review: Omit<Review, "user_id" | "username">,
|
||||
) {
|
||||
if (!(await hasCapability("reviews"))) {
|
||||
throw new Error("Reviews capability is not available");
|
||||
}
|
||||
const token = localStorage.getItem("lnv-token");
|
||||
@@ -71,13 +87,13 @@ export async function postReview(location: WorldLocation, review: Omit<Review, '
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...review,
|
||||
lat: location.lat,
|
||||
lon: location.lon
|
||||
})
|
||||
lon: location.lon,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to post review: ${res.statusText}`);
|
||||
@@ -86,7 +102,7 @@ export async function postReview(location: WorldLocation, review: Omit<Review, '
|
||||
}
|
||||
|
||||
export async function ai(query: string, location?: WorldLocation) {
|
||||
if(!await hasCapability("ai")) {
|
||||
if (!(await hasCapability("ai"))) {
|
||||
throw new Error("AI capability is not available");
|
||||
}
|
||||
const res = await fetch(LNV_SERVER + `/ai`, {
|
||||
@@ -96,8 +112,8 @@ export async function ai(query: string, location?: WorldLocation) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: query,
|
||||
coords: location
|
||||
})
|
||||
coords: location,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to get AI response: ${res.statusText}`);
|
||||
|
||||
@@ -2,26 +2,30 @@
|
||||
let { lane }: { lane: Lane } = $props();
|
||||
const knownDirections = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024];
|
||||
|
||||
async function fetchImage(bit: number) {
|
||||
if (knownDirections.includes(bit)) {
|
||||
return await fetch(`/img/lanes/${bit}.svg`).then(res => res.text());
|
||||
} else {
|
||||
return `<span>${bit}</span>`;
|
||||
}
|
||||
}
|
||||
async function fetchImage(bit: number) {
|
||||
if (knownDirections.includes(bit)) {
|
||||
return await fetch(`/img/lanes/${bit}.svg`).then((res) => res.text());
|
||||
} else {
|
||||
return `<span>${bit}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function loadImage(node: HTMLElement, bit: number) {
|
||||
fetchImage(bit).then(img => {
|
||||
node.innerHTML = img;
|
||||
});
|
||||
}
|
||||
function loadImage(node: HTMLElement, bit: number) {
|
||||
fetchImage(bit).then((img) => {
|
||||
node.innerHTML = img;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="lane">
|
||||
{#each Array(10).fill(0).map((_, i) => 1 << i) as bit}
|
||||
{#each Array(10)
|
||||
.fill(0)
|
||||
.map((_, i) => 1 << i) as bit (bit)}
|
||||
{#if lane.directions & bit}
|
||||
<div
|
||||
class="lane-image {lane.valid & bit ? 'valid' : ''} {lane.active & bit ? 'active' : ''}"
|
||||
class="lane-image {lane.valid & bit ? 'valid' : ''} {lane.active & bit
|
||||
? 'active'
|
||||
: ''}"
|
||||
use:loadImage={bit}
|
||||
></div>
|
||||
{/if}
|
||||
@@ -67,4 +71,4 @@
|
||||
font-weight: bold;
|
||||
color: #cc2c2c;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function displayLane(lane: Lane) {
|
||||
// Check if the bit is in the known directions
|
||||
let img = "";
|
||||
if (knownDirections.includes(bit)) {
|
||||
img = await fetch(`/img/lanes/${bit}.svg`).then(res => res.text());
|
||||
img = await fetch(`/img/lanes/${bit}.svg`).then((res) => res.text());
|
||||
} else {
|
||||
img = `<span>${bit}</span>`;
|
||||
}
|
||||
@@ -32,4 +32,4 @@ export async function displayLane(lane: Lane) {
|
||||
}
|
||||
}
|
||||
return laneDiv;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
{#if lanes}
|
||||
<div id="lanes">
|
||||
{#each lanes as lane}
|
||||
{#each lanes as lane (lane)}
|
||||
<LaneDisplay {lane} />
|
||||
{/each}
|
||||
</div>
|
||||
@@ -19,4 +19,4 @@
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -43,4 +43,4 @@ export const maneuverTypes = [
|
||||
"escalatorEnter", // ???
|
||||
"buildingEnter", // ???
|
||||
"buildingExit", // ???
|
||||
];
|
||||
];
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
/* eslint-disable no-empty */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/prefer-for-of */
|
||||
/* eslint-disable prefer-const */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
|
||||
import maplibregl from "maplibre-gl";
|
||||
// import { maneuverTypes } from "./Maneuver";
|
||||
import { hideRouteStatus, updateRouteStatus } from "../../components/routestatus";
|
||||
import { NavigationLayer, removeAllNavigationLayers, updateNavigationLayer } from "./NavigationLayers";
|
||||
import {
|
||||
hideRouteStatus,
|
||||
updateRouteStatus,
|
||||
} from "../../components/routestatus";
|
||||
import {
|
||||
NavigationLayer,
|
||||
removeAllNavigationLayers,
|
||||
updateNavigationLayer,
|
||||
} from "./NavigationLayers";
|
||||
import { updateMapPadding } from "../../main";
|
||||
import say from "../TTS";
|
||||
import { ROUTING_SERVER } from "../servers";
|
||||
import { createValhallaRequest } from "./ValhallaRequest";
|
||||
import { Vehicle } from "../../components/vehicles";
|
||||
import { KeepAwake } from "@capacitor-community/keep-awake";
|
||||
import { getCurrentViewName, getSidebarView } from "../../components/sidebar/SidebarRegistry";
|
||||
import {
|
||||
getCurrentViewName,
|
||||
getSidebarView,
|
||||
} from "../../components/sidebar/SidebarRegistry";
|
||||
// import { displayLane } from "./LaneDisplay";
|
||||
|
||||
export async function fetchRoute(vehicle: Vehicle, from: WorldLocation, to: WorldLocation): Promise<RouteResult> {
|
||||
export async function fetchRoute(
|
||||
vehicle: Vehicle,
|
||||
from: WorldLocation,
|
||||
to: WorldLocation,
|
||||
): Promise<RouteResult> {
|
||||
// const req = {
|
||||
// locations: [
|
||||
// from,
|
||||
@@ -35,7 +54,7 @@ export async function fetchRoute(vehicle: Vehicle, from: WorldLocation, to: Worl
|
||||
const res = await fetch(
|
||||
ROUTING_SERVER + "/route?json=" + JSON.stringify(req),
|
||||
).then((res) => res.json());
|
||||
|
||||
|
||||
console.log(res);
|
||||
return res;
|
||||
} catch (e) {
|
||||
@@ -65,7 +84,11 @@ function drawRoute(trip: Trip, name: NavigationLayer) {
|
||||
updateNavigationLayer(name, geometry);
|
||||
}
|
||||
|
||||
export async function findRoute(vehicle: Vehicle, from: WorldLocation, to: WorldLocation): Promise<Trip[]> {
|
||||
export async function findRoute(
|
||||
vehicle: Vehicle,
|
||||
from: WorldLocation,
|
||||
to: WorldLocation,
|
||||
): Promise<Trip[]> {
|
||||
fromMarker = new maplibregl.Marker()
|
||||
.setLngLat([from.lon, from.lat])
|
||||
.addTo(window.glmap);
|
||||
@@ -75,10 +98,10 @@ export async function findRoute(vehicle: Vehicle, from: WorldLocation, to: World
|
||||
const route = await fetchRoute(vehicle, from, to);
|
||||
|
||||
let routes = [route.trip];
|
||||
if(route.alternates) {
|
||||
for(let i = 0; i < route.alternates.length; i++) {
|
||||
if (route.alternates) {
|
||||
for (let i = 0; i < route.alternates.length; i++) {
|
||||
routes.push(route.alternates[i].trip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawAllRoutes(routes);
|
||||
@@ -117,7 +140,11 @@ function getUserLocation(): WorldLocation {
|
||||
let pastRoute: WorldLocation[] = [];
|
||||
|
||||
// Check if the location is on the line between from and to (3 meter tolerance)
|
||||
function isOnLine(location: WorldLocation, from: WorldLocation, to: WorldLocation) {
|
||||
function isOnLine(
|
||||
location: WorldLocation,
|
||||
from: WorldLocation,
|
||||
to: WorldLocation,
|
||||
) {
|
||||
// Convert the 6-meter tolerance to degrees (approximation)
|
||||
const tolerance = 6 / 111320; // 1 degree latitude ≈ 111.32 km
|
||||
|
||||
@@ -126,7 +153,9 @@ function isOnLine(location: WorldLocation, from: WorldLocation, to: WorldLocatio
|
||||
const dy = to.lat - from.lat;
|
||||
|
||||
// Calculate the projection of the location onto the line segment
|
||||
const t = ((location.lon - from.lon) * dx + (location.lat - from.lat) * dy) / (dx * dx + dy * dy);
|
||||
const t =
|
||||
((location.lon - from.lon) * dx + (location.lat - from.lat) * dy) /
|
||||
(dx * dx + dy * dy);
|
||||
|
||||
// Clamp t to the range [0, 1] to ensure the projection is on the segment
|
||||
const clampedT = Math.max(0, Math.min(1, t));
|
||||
@@ -139,7 +168,8 @@ function isOnLine(location: WorldLocation, from: WorldLocation, to: WorldLocatio
|
||||
|
||||
// Calculate the distance from the location to the closest point
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(location.lon - closestPoint.lon, 2) + Math.pow(location.lat - closestPoint.lat, 2)
|
||||
Math.pow(location.lon - closestPoint.lon, 2) +
|
||||
Math.pow(location.lat - closestPoint.lat, 2),
|
||||
);
|
||||
|
||||
// Check if the distance is within the tolerance
|
||||
@@ -175,9 +205,9 @@ export async function startNavigation(trip: Trip) {
|
||||
|
||||
// @ts-ignore The types are not correct
|
||||
int = setInterval(() => {
|
||||
if(instructionIdx != 0) {
|
||||
if (instructionIdx != 0) {
|
||||
// Only continue if the user location is at the end shape index of the current maneuver
|
||||
if(currentManeuver == null) {
|
||||
if (currentManeuver == null) {
|
||||
return;
|
||||
}
|
||||
const bgi = currentManeuver.begin_shape_index;
|
||||
@@ -212,7 +242,7 @@ export async function startNavigation(trip: Trip) {
|
||||
updateRouteStatus({
|
||||
time: trip.summary.time,
|
||||
distance: trip.summary.length,
|
||||
currentManeuver
|
||||
currentManeuver,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,7 +258,7 @@ export async function startNavigation(trip: Trip) {
|
||||
// pastRoute.push(...polyline.slice(0, bgi + 1));
|
||||
pastRoute = polyline.slice(0, bgi + 1);
|
||||
|
||||
updateNavigationLayer("route-past", pastRoute.flat())
|
||||
updateNavigationLayer("route-past", pastRoute.flat());
|
||||
|
||||
// Remove from shape begin to end from the route line
|
||||
const newShape = polyline.slice(bgi);
|
||||
@@ -242,11 +272,14 @@ export async function startNavigation(trip: Trip) {
|
||||
return;
|
||||
}
|
||||
const maneuver = trip.legs[0].maneuvers[instructionIdx];
|
||||
updateRouteStatus({
|
||||
time: trip.summary.time,
|
||||
distance: trip.summary.length,
|
||||
currentManeuver: trip.legs[0].maneuvers[instructionIdx],
|
||||
}, maneuver.lanes);
|
||||
updateRouteStatus(
|
||||
{
|
||||
time: trip.summary.time,
|
||||
distance: trip.summary.length,
|
||||
currentManeuver: trip.legs[0].maneuvers[instructionIdx],
|
||||
},
|
||||
maneuver.lanes,
|
||||
);
|
||||
currentManeuver = maneuver;
|
||||
|
||||
// document.querySelector<HTMLDivElement>("#lanes")!.innerHTML = "";
|
||||
@@ -266,22 +299,27 @@ export async function startNavigation(trip: Trip) {
|
||||
if (instructionIdx > 0) {
|
||||
const prevManeuver = trip.legs[0].maneuvers[instructionIdx - 1];
|
||||
if (prevManeuver.verbal_post_transition_instruction) {
|
||||
console.log("Saying: " + prevManeuver.verbal_post_transition_instruction);
|
||||
console.log(
|
||||
"Saying: " + prevManeuver.verbal_post_transition_instruction,
|
||||
);
|
||||
say(prevManeuver.verbal_post_transition_instruction);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
updateRouteStatus({
|
||||
time: trip.summary.time,
|
||||
distance: trip.summary.length,
|
||||
currentManeuver: trip.legs[0].maneuvers[0],
|
||||
}, trip.legs[0].maneuvers[0].lanes);
|
||||
updateRouteStatus(
|
||||
{
|
||||
time: trip.summary.time,
|
||||
distance: trip.summary.length,
|
||||
currentManeuver: trip.legs[0].maneuvers[0],
|
||||
},
|
||||
trip.legs[0].maneuvers[0].lanes,
|
||||
);
|
||||
currentTrip = trip;
|
||||
}
|
||||
|
||||
export async function stopNavigation() {
|
||||
if(int) clearInterval(int);
|
||||
if (int) clearInterval(int);
|
||||
await KeepAwake.allowSleep();
|
||||
hideRouteStatus();
|
||||
document.querySelector<HTMLBodyElement>("body")!.classList.remove("isInTrip");
|
||||
@@ -320,7 +358,7 @@ export function decodePolyline(encoded: string): WorldLocation[] {
|
||||
shift += 5;
|
||||
} while (byte >= 0x20);
|
||||
|
||||
let deltaLat = (result & 1) ? ~(result >> 1) : (result >> 1);
|
||||
let deltaLat = result & 1 ? ~(result >> 1) : result >> 1;
|
||||
lat += deltaLat;
|
||||
|
||||
shift = 0;
|
||||
@@ -331,7 +369,7 @@ export function decodePolyline(encoded: string): WorldLocation[] {
|
||||
shift += 5;
|
||||
} while (byte >= 0x20);
|
||||
|
||||
let deltaLng = (result & 1) ? ~(result >> 1) : (result >> 1);
|
||||
let deltaLng = result & 1 ? ~(result >> 1) : result >> 1;
|
||||
lng += deltaLng;
|
||||
|
||||
// Convert the latitude and longitude to decimal format with six digits of precision
|
||||
|
||||
@@ -8,7 +8,7 @@ export type ValhallaCosting =
|
||||
| "motor_scooter"
|
||||
| "multimodal"
|
||||
| "pedestrian";
|
||||
export type ValhallaRequest = {
|
||||
export interface ValhallaRequest {
|
||||
locations: WorldLocation[];
|
||||
costing: ValhallaCosting;
|
||||
units: "miles" | "kilometers";
|
||||
@@ -16,8 +16,8 @@ export type ValhallaRequest = {
|
||||
alternates: number;
|
||||
costing_options: ValhallaCostingOptions;
|
||||
turn_lanes: boolean;
|
||||
};
|
||||
export type GeneralCostingOptions = {
|
||||
}
|
||||
export interface GeneralCostingOptions {
|
||||
/**
|
||||
* A penalty applied when transitioning between roads that do not have consistent
|
||||
* naming - in other words, no road names in common. This penalty can be used to
|
||||
@@ -59,7 +59,7 @@ export type GeneralCostingOptions = {
|
||||
* @default 0 for trucks, 15 for cars, buses, motor scooters and motorcycles
|
||||
*/
|
||||
service_penalty?: number;
|
||||
};
|
||||
}
|
||||
export type AutomobileCostingOptions = {
|
||||
/**
|
||||
* A penalty applied when a gate or bollard with access=private is encountered.
|
||||
@@ -226,7 +226,7 @@ export type AutomobileCostingOptions = {
|
||||
*/
|
||||
hierarchy_limits?: void;
|
||||
} & GeneralCostingOptions;
|
||||
export type OtherCostingOptions = {
|
||||
export interface OtherCostingOptions {
|
||||
/**
|
||||
* The height of the vehicle (in meters).
|
||||
* @default 1.9 for car, bus, taxi and 4.11 for truck
|
||||
@@ -267,7 +267,7 @@ export type OtherCostingOptions = {
|
||||
* @default false
|
||||
*/
|
||||
include_hot?: boolean;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* The type of the bicycle.
|
||||
* Road: a road-style bicycle with narrow tires that is generally lightweight and designed for speed on paved surfaces.
|
||||
@@ -276,7 +276,7 @@ export type OtherCostingOptions = {
|
||||
* Mountain: a mountain bicycle suitable for most surfaces but generally heavier and slower on paved surfaces.
|
||||
*/
|
||||
export type BicycleType = "Road" | "Hybrid" | "City" | "Mountain";
|
||||
export type BicycleCostingOptions = {
|
||||
export interface BicycleCostingOptions {
|
||||
/**
|
||||
* @default "Hybrid"
|
||||
*/
|
||||
@@ -367,9 +367,8 @@ export type BicycleCostingOptions = {
|
||||
* @default false
|
||||
*/
|
||||
shortest?: boolean;
|
||||
|
||||
};
|
||||
export type BikeshareCostingOptions = {}; // TODO
|
||||
}
|
||||
export type BikeshareCostingOptions = unknown; // TODO
|
||||
export type MotorScooterCostingOptions = {
|
||||
/**
|
||||
* A rider's propensity to use primary roads.
|
||||
@@ -398,9 +397,9 @@ export type MotorScooterCostingOptions = {
|
||||
*/
|
||||
use_hills?: boolean;
|
||||
} & AutomobileCostingOptions;
|
||||
export type MultimodalCostingOptions = {}; // TODO
|
||||
export type PedestrianCostingOptions = {}; // TODO
|
||||
export type TruckCostingOptions = {
|
||||
export type MultimodalCostingOptions = unknown; // TODO
|
||||
export type PedestrianCostingOptions = unknown; // TODO
|
||||
export interface TruckCostingOptions {
|
||||
/**
|
||||
* The length of the truck (in meters).
|
||||
* @default 21.64
|
||||
@@ -448,8 +447,8 @@ export type TruckCostingOptions = {
|
||||
* @default 0
|
||||
*/
|
||||
use_truck_route?: boolean;
|
||||
};
|
||||
export type ValhallaCostingOptions = {
|
||||
}
|
||||
export interface ValhallaCostingOptions {
|
||||
auto?: AutomobileCostingOptions & OtherCostingOptions;
|
||||
bicycle?: BicycleCostingOptions;
|
||||
bus?: AutomobileCostingOptions & OtherCostingOptions;
|
||||
@@ -459,4 +458,4 @@ export type ValhallaCostingOptions = {
|
||||
motor_scooter?: MotorScooterCostingOptions;
|
||||
multimodal?: MultimodalCostingOptions;
|
||||
pedestrian?: PedestrianCostingOptions;
|
||||
};
|
||||
}
|
||||
|
||||
23
src/lib/services/navigation/navigation.d.ts
vendored
23
src/lib/services/navigation/navigation.d.ts
vendored
@@ -1,15 +1,18 @@
|
||||
type Language = "de-DE" | "en-US";
|
||||
type WorldLocation = { lat: number; lon: number };
|
||||
interface WorldLocation {
|
||||
lat: number;
|
||||
lon: number;
|
||||
}
|
||||
type Units = "kilometers" | "miles";
|
||||
|
||||
type RouteResult = {
|
||||
interface RouteResult {
|
||||
alternates?: {
|
||||
trip: Trip;
|
||||
}[];
|
||||
trip: Trip;
|
||||
}
|
||||
|
||||
type Trip = {
|
||||
interface Trip {
|
||||
language: Language;
|
||||
legs: Leg[];
|
||||
status: number;
|
||||
@@ -17,16 +20,16 @@ type Trip = {
|
||||
summary: Summary;
|
||||
units: Units;
|
||||
locations: WorldLocation[];
|
||||
};
|
||||
}
|
||||
|
||||
type Leg = {
|
||||
interface Leg {
|
||||
maneuvers: Maneuver[];
|
||||
shape: string;
|
||||
summary: Summary;
|
||||
locations: WorldLocation[];
|
||||
}
|
||||
|
||||
type Summary = {
|
||||
interface Summary {
|
||||
cost: number;
|
||||
has_ferry: boolean;
|
||||
has_highway: boolean;
|
||||
@@ -55,13 +58,13 @@ type Summary = {
|
||||
* 512 = MergeToLeft
|
||||
* 1024 = MergeToRight
|
||||
*/
|
||||
type Lane = {
|
||||
interface Lane {
|
||||
directions: number;
|
||||
valid: number;
|
||||
active: number;
|
||||
};
|
||||
}
|
||||
|
||||
type Maneuver = {
|
||||
interface Maneuver {
|
||||
bearing_after: number;
|
||||
begin_shape_index: number;
|
||||
cost: number;
|
||||
@@ -78,4 +81,4 @@ type Maneuver = {
|
||||
verbal_pre_transition_instruction: string;
|
||||
verbal_succinct_transition_instruction: string;
|
||||
lanes?: Lane[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ export const routing = $state({
|
||||
int: null as NodeJS.Timeout | null,
|
||||
isOffRoute: false,
|
||||
currentManeuver: null as Maneuver | null,
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
export function resetRouting() {
|
||||
routing.geojson.route = null;
|
||||
@@ -30,9 +30,10 @@ export function resetRouting() {
|
||||
|
||||
export async function fetchRoute(server: string, request: ValhallaRequest) {
|
||||
try {
|
||||
const res = await fetch(server + "/route?json=" + JSON.stringify(request))
|
||||
.then((res) => res.json());
|
||||
|
||||
const res = await fetch(
|
||||
server + "/route?json=" + JSON.stringify(request),
|
||||
).then((res) => res.json());
|
||||
|
||||
console.log(res);
|
||||
return res;
|
||||
} catch (error) {
|
||||
@@ -65,9 +66,9 @@ function geometryToGeoJSON(polyline: WorldLocation[]): GeoJSON.Feature {
|
||||
}
|
||||
|
||||
export function decodePolyline(encoded: string): WorldLocation[] {
|
||||
let points = [];
|
||||
const points = [];
|
||||
let index = 0;
|
||||
let len = encoded.length;
|
||||
const len = encoded.length;
|
||||
let lat = 0;
|
||||
let lng = 0;
|
||||
|
||||
@@ -81,7 +82,7 @@ export function decodePolyline(encoded: string): WorldLocation[] {
|
||||
shift += 5;
|
||||
} while (byte >= 0x20);
|
||||
|
||||
let deltaLat = (result & 1) ? ~(result >> 1) : (result >> 1);
|
||||
const deltaLat = result & 1 ? ~(result >> 1) : result >> 1;
|
||||
lat += deltaLat;
|
||||
|
||||
shift = 0;
|
||||
@@ -92,7 +93,7 @@ export function decodePolyline(encoded: string): WorldLocation[] {
|
||||
shift += 5;
|
||||
} while (byte >= 0x20);
|
||||
|
||||
let deltaLng = (result & 1) ? ~(result >> 1) : (result >> 1);
|
||||
const deltaLng = result & 1 ? ~(result >> 1) : result >> 1;
|
||||
lng += deltaLng;
|
||||
|
||||
// Convert the latitude and longitude to decimal format with six digits of precision
|
||||
@@ -108,8 +109,8 @@ export function decodePolyline(encoded: string): WorldLocation[] {
|
||||
export function drawAllRoutes(trips: Trip[]) {
|
||||
routing.geojson.routePast = null;
|
||||
routing.geojson.route = tripToGeoJSON(trips[0]);
|
||||
if(trips[1]) routing.geojson.al0 = tripToGeoJSON(trips[1]);
|
||||
if(trips[2]) routing.geojson.al1 = tripToGeoJSON(trips[2]);
|
||||
if (trips[1]) routing.geojson.al0 = tripToGeoJSON(trips[1]);
|
||||
if (trips[2]) routing.geojson.al1 = tripToGeoJSON(trips[2]);
|
||||
}
|
||||
|
||||
export function drawRoute(trip: Trip) {
|
||||
@@ -119,7 +120,9 @@ export function drawRoute(trip: Trip) {
|
||||
function drawCurrentTrip() {
|
||||
if (!routing.currentTrip) return;
|
||||
routing.geojson.route = geometryToGeoJSON(routing.currentTripInfo.route);
|
||||
routing.geojson.routePast = geometryToGeoJSON(routing.currentTripInfo.past.flat());
|
||||
routing.geojson.routePast = geometryToGeoJSON(
|
||||
routing.currentTripInfo.past.flat(),
|
||||
);
|
||||
}
|
||||
|
||||
export async function startRoute(trip: Trip) {
|
||||
@@ -131,7 +134,8 @@ export async function startRoute(trip: Trip) {
|
||||
routing.currentTripInfo.isOffRoute = false;
|
||||
|
||||
drawRoute(trip);
|
||||
routing.currentTripInfo.currentManeuver = routing.currentTrip.legs[0].maneuvers[0];
|
||||
routing.currentTripInfo.currentManeuver =
|
||||
routing.currentTrip.legs[0].maneuvers[0];
|
||||
|
||||
routing.currentTripInfo.int = setInterval(tickRoute, 500);
|
||||
}
|
||||
@@ -143,7 +147,8 @@ async function tickRoute() {
|
||||
const info = routing.currentTripInfo;
|
||||
if (!trip) return;
|
||||
|
||||
const currentManeuver = trip.legs[0].maneuvers[routing.currentTripInfo.maneuverIdx];
|
||||
const currentManeuver =
|
||||
trip.legs[0].maneuvers[routing.currentTripInfo.maneuverIdx];
|
||||
if (!currentManeuver) {
|
||||
// No more maneuvers, stop navigation
|
||||
stopNavigation();
|
||||
@@ -155,14 +160,14 @@ async function tickRoute() {
|
||||
const polyline = decodePolyline(trip.legs[0].shape);
|
||||
|
||||
// Check if the user location is on the last point of the entire route
|
||||
if(isOnPoint(location, polyline[polyline.length - 1])) {
|
||||
if (isOnPoint(location, polyline[polyline.length - 1])) {
|
||||
console.log("Reached destination!");
|
||||
stopNavigation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the user is on the route
|
||||
if(!isOnShape(location, polyline)) {
|
||||
if (!isOnShape(location, polyline)) {
|
||||
console.log("Off route!");
|
||||
info.isOffRoute = true;
|
||||
// TODO: Implement re-routing logic
|
||||
@@ -171,18 +176,24 @@ async function tickRoute() {
|
||||
info.isOffRoute = false;
|
||||
}
|
||||
|
||||
if (currentManeuver.verbal_pre_transition_instruction && !hasAnnouncedPreInstruction) {
|
||||
if (
|
||||
currentManeuver.verbal_pre_transition_instruction &&
|
||||
!hasAnnouncedPreInstruction
|
||||
) {
|
||||
const distanceToEnd = calculateDistance(location, polyline[bgi]);
|
||||
// console.log("Distance to end of current maneuver: ", distanceToEnd, " meters");
|
||||
if (distanceToEnd <= 100) {
|
||||
hasAnnouncedPreInstruction = true;
|
||||
console.log("[Verbal instruction] ", currentManeuver.verbal_pre_transition_instruction);
|
||||
console.log(
|
||||
"[Verbal instruction] ",
|
||||
currentManeuver.verbal_pre_transition_instruction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the user is past the current maneuver
|
||||
// Checks if the user is still on the current maneuver's polyline
|
||||
if(!isOnShape(location, polyline.slice(bgi))) {
|
||||
if (!isOnShape(location, polyline.slice(bgi))) {
|
||||
return; // User is not on the current maneuver's polyline, do not update
|
||||
}
|
||||
|
||||
@@ -196,15 +207,25 @@ async function tickRoute() {
|
||||
// announce the "verbal_post_transition_instruction"
|
||||
if (currentManeuver.verbal_post_transition_instruction) {
|
||||
hasAnnouncedPreInstruction = false;
|
||||
const distanceToEnd = calculateDistance(location, polyline[trip.legs[0].maneuvers[routing.currentTripInfo.maneuverIdx + 1].begin_shape_index]);
|
||||
const distanceToEnd = calculateDistance(
|
||||
location,
|
||||
polyline[
|
||||
trip.legs[0].maneuvers[routing.currentTripInfo.maneuverIdx + 1]
|
||||
.begin_shape_index
|
||||
],
|
||||
);
|
||||
if (distanceToEnd >= 200) {
|
||||
console.log("[Verbal instruction] ", currentManeuver.verbal_post_transition_instruction);
|
||||
console.log(
|
||||
"[Verbal instruction] ",
|
||||
currentManeuver.verbal_post_transition_instruction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Advance to the next maneuver
|
||||
info.maneuverIdx++;
|
||||
if(info.maneuverIdx >= trip.legs[0].maneuvers.length) { // No more maneuvers
|
||||
if (info.maneuverIdx >= trip.legs[0].maneuvers.length) {
|
||||
// No more maneuvers
|
||||
stopNavigation();
|
||||
return;
|
||||
}
|
||||
@@ -228,8 +249,8 @@ function getUserLocation(): WorldLocation {
|
||||
// return geolocate.currentLocation!;
|
||||
return {
|
||||
lat: location.lat,
|
||||
lon: location.lng
|
||||
}
|
||||
lon: location.lng,
|
||||
};
|
||||
// const lnglat = window.geolocate._userLocationDotMarker.getLngLat();
|
||||
// return { lat: lnglat.lat, lon: lnglat.lng };
|
||||
// console.log(map.value!)
|
||||
@@ -239,7 +260,11 @@ function getUserLocation(): WorldLocation {
|
||||
// }
|
||||
}
|
||||
|
||||
function isOnLine(location: WorldLocation, from: WorldLocation, to: WorldLocation) {
|
||||
function isOnLine(
|
||||
location: WorldLocation,
|
||||
from: WorldLocation,
|
||||
to: WorldLocation,
|
||||
) {
|
||||
// Convert the 12-meter tolerance to degrees (approximation)
|
||||
const tolerance = 12 / 111320; // 1 degree latitude ≈ 111.32 km
|
||||
|
||||
@@ -248,7 +273,9 @@ function isOnLine(location: WorldLocation, from: WorldLocation, to: WorldLocatio
|
||||
const dy = to.lat - from.lat;
|
||||
|
||||
// Calculate the projection of the location onto the line segment
|
||||
const t = ((location.lon - from.lon) * dx + (location.lat - from.lat) * dy) / (dx * dx + dy * dy);
|
||||
const t =
|
||||
((location.lon - from.lon) * dx + (location.lat - from.lat) * dy) /
|
||||
(dx * dx + dy * dy);
|
||||
|
||||
// Clamp t to the range [0, 1] to ensure the projection is on the segment
|
||||
const clampedT = Math.max(0, Math.min(1, t));
|
||||
@@ -261,7 +288,8 @@ function isOnLine(location: WorldLocation, from: WorldLocation, to: WorldLocatio
|
||||
|
||||
// Calculate the distance from the location to the closest point
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(location.lon - closestPoint.lon, 2) + Math.pow(location.lat - closestPoint.lat, 2)
|
||||
Math.pow(location.lon - closestPoint.lon, 2) +
|
||||
Math.pow(location.lat - closestPoint.lat, 2),
|
||||
);
|
||||
|
||||
// Check if the distance is within the tolerance
|
||||
@@ -273,7 +301,8 @@ function isOnPoint(location: WorldLocation, point: WorldLocation) {
|
||||
const tolerance = 6 / 111320; // 1 degree latitude ≈ 111.32 km
|
||||
// Calculate the distance from the location to the point
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(location.lon - point.lon, 2) + Math.pow(location.lat - point.lat, 2)
|
||||
Math.pow(location.lon - point.lon, 2) +
|
||||
Math.pow(location.lat - point.lat, 2),
|
||||
);
|
||||
// Check if the distance is within the tolerance
|
||||
return distance <= tolerance;
|
||||
@@ -289,34 +318,47 @@ function isOnShape(location: WorldLocation, shape: WorldLocation[]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function calculateDistance(point1: WorldLocation, point2: WorldLocation): number {
|
||||
function calculateDistance(
|
||||
point1: WorldLocation,
|
||||
point2: WorldLocation,
|
||||
): number {
|
||||
const R = 6371000; // Earth's radius in meters
|
||||
const lat1 = point1.lat * (Math.PI / 180);
|
||||
const lat2 = point2.lat * (Math.PI / 180);
|
||||
const deltaLat = (point2.lat - point1.lat) * (Math.PI / 180);
|
||||
const deltaLon = (point2.lon - point1.lon) * (Math.PI / 180);
|
||||
|
||||
const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
|
||||
Math.cos(lat1) * Math.cos(lat2) *
|
||||
Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
|
||||
const a =
|
||||
Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
|
||||
Math.cos(lat1) *
|
||||
Math.cos(lat2) *
|
||||
Math.sin(deltaLon / 2) *
|
||||
Math.sin(deltaLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c; // Distance in meters
|
||||
}
|
||||
|
||||
export function zoomToPoints(from: WorldLocation, to: WorldLocation, map: maplibregl.Map) {
|
||||
const getBoundingBox = (point1: [number, number], point2: [number, number]): LngLatBoundsLike => {
|
||||
export function zoomToPoints(
|
||||
from: WorldLocation,
|
||||
to: WorldLocation,
|
||||
map: maplibregl.Map,
|
||||
) {
|
||||
const getBoundingBox = (
|
||||
point1: [number, number],
|
||||
point2: [number, number],
|
||||
): LngLatBoundsLike => {
|
||||
const [lng1, lat1] = point1;
|
||||
const [lng2, lat2] = point2;
|
||||
|
||||
|
||||
const sw = [Math.min(lng1, lng2), Math.min(lat1, lat2)] as [number, number];
|
||||
const ne = [Math.max(lng1, lng2), Math.max(lat1, lat2)] as [number, number];
|
||||
|
||||
|
||||
return [sw, ne];
|
||||
};
|
||||
|
||||
map.fitBounds(getBoundingBox([from.lon, from.lat], [to.lon, to.lat]), {
|
||||
padding: 40
|
||||
map.fitBounds(getBoundingBox([from.lon, from.lat], [to.lon, to.lat]), {
|
||||
padding: 40,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { getOIDCConfig, hasCapability } from "./lnv";
|
||||
|
||||
export async function getAuthURL() {
|
||||
if(!await hasCapability("auth")) {
|
||||
if (!(await hasCapability("auth"))) {
|
||||
throw new Error("Server does not support OIDC authentication");
|
||||
}
|
||||
const oidcConfig = await getOIDCConfig();
|
||||
@@ -17,8 +17,7 @@ export async function getAuthURL() {
|
||||
const state = generateRandomString(16);
|
||||
|
||||
return {
|
||||
url:
|
||||
`${AUTH_URL}?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${window.location.origin}/login/callback&scope=openid%20profile&code_challenge=${pkce.codeChallenge}&code_challenge_method=S256&state=${state}`,
|
||||
url: `${AUTH_URL}?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${window.location.origin}/login/callback&scope=openid%20profile&code_challenge=${pkce.codeChallenge}&code_challenge_method=S256&state=${state}`,
|
||||
codeVerifier: pkce.codeVerifier,
|
||||
state,
|
||||
};
|
||||
@@ -41,7 +40,9 @@ function generateRandomString(length: number) {
|
||||
|
||||
window.crypto.getRandomValues(array);
|
||||
|
||||
return Array.from(array, (dec) => ("0" + dec.toString(16)).substr(-2)).join("");
|
||||
return Array.from(array, (dec) => ("0" + dec.toString(16)).substr(-2)).join(
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
// Encodes a string to base64url (no padding)
|
||||
@@ -60,7 +61,7 @@ async function sha256(input: string | undefined): Promise<ArrayBuffer> {
|
||||
}
|
||||
|
||||
export async function getOIDCUser(code: string, codeVerifier: string) {
|
||||
if(!await hasCapability("auth")) {
|
||||
if (!(await hasCapability("auth"))) {
|
||||
throw new Error("Server does not support OIDC authentication");
|
||||
}
|
||||
const oidcConfig = await getOIDCConfig();
|
||||
@@ -81,8 +82,8 @@ export async function getOIDCUser(code: string, codeVerifier: string) {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: params
|
||||
}).then(res => res.json());
|
||||
body: params,
|
||||
}).then((res) => res.json());
|
||||
|
||||
return res;
|
||||
// return JSON.parse(atob(id_token.split(".")[1]));
|
||||
|
||||
@@ -8,6 +8,10 @@ export function cn(...inputs: ClassValue[]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||
export type WithoutChildren<T> = T extends { children?: any }
|
||||
? Omit<T, "children">
|
||||
: T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & {
|
||||
ref?: U | null;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { ValhallaCosting, ValhallaCostingOptions, ValhallaRequest } from "$lib/services/navigation/ValhallaRequest";
|
||||
import type {
|
||||
ValhallaCosting,
|
||||
ValhallaCostingOptions,
|
||||
ValhallaRequest,
|
||||
} from "$lib/services/navigation/ValhallaRequest";
|
||||
import type { Vehicle } from "./vehicles.svelte";
|
||||
|
||||
function getVehicleCosting(vehicle: Vehicle): ValhallaCosting {
|
||||
@@ -17,31 +21,43 @@ function getVehicleCosting(vehicle: Vehicle): ValhallaCosting {
|
||||
}
|
||||
}
|
||||
|
||||
export function createValhallaRequest(vehicle: Vehicle, locations: WorldLocation[]): ValhallaRequest {
|
||||
export function createValhallaRequest(
|
||||
vehicle: Vehicle,
|
||||
locations: WorldLocation[],
|
||||
): ValhallaRequest {
|
||||
const costing = getVehicleCosting(vehicle);
|
||||
let costingOptions: ValhallaCostingOptions = costing == "auto" ? {
|
||||
auto: {
|
||||
top_speed: vehicle.legalMaxSpeed,
|
||||
fixed_speed: vehicle.actualMaxSpeed
|
||||
}
|
||||
} : costing == "motor_scooter" ? {
|
||||
motor_scooter: {
|
||||
top_speed: vehicle.legalMaxSpeed,
|
||||
fixed_speed: vehicle.actualMaxSpeed
|
||||
}
|
||||
} : costing == "truck" ? {
|
||||
truck: {
|
||||
top_speed: vehicle.legalMaxSpeed,
|
||||
fixed_speed: vehicle.actualMaxSpeed,
|
||||
length: vehicle.length,
|
||||
weight: vehicle.weight,
|
||||
axle_load: vehicle.axisLoad
|
||||
}
|
||||
} : costing == "bicycle" ? {
|
||||
bicycle: {
|
||||
cycling_speed: vehicle.actualMaxSpeed
|
||||
}
|
||||
} : {};
|
||||
const costingOptions: ValhallaCostingOptions =
|
||||
costing == "auto"
|
||||
? {
|
||||
auto: {
|
||||
top_speed: vehicle.legalMaxSpeed,
|
||||
fixed_speed: vehicle.actualMaxSpeed,
|
||||
},
|
||||
}
|
||||
: costing == "motor_scooter"
|
||||
? {
|
||||
motor_scooter: {
|
||||
top_speed: vehicle.legalMaxSpeed,
|
||||
fixed_speed: vehicle.actualMaxSpeed,
|
||||
},
|
||||
}
|
||||
: costing == "truck"
|
||||
? {
|
||||
truck: {
|
||||
top_speed: vehicle.legalMaxSpeed,
|
||||
fixed_speed: vehicle.actualMaxSpeed,
|
||||
length: vehicle.length,
|
||||
weight: vehicle.weight,
|
||||
axle_load: vehicle.axisLoad,
|
||||
},
|
||||
}
|
||||
: costing == "bicycle"
|
||||
? {
|
||||
bicycle: {
|
||||
cycling_speed: vehicle.actualMaxSpeed,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
return {
|
||||
locations,
|
||||
costing,
|
||||
@@ -49,6 +65,6 @@ export function createValhallaRequest(vehicle: Vehicle, locations: WorldLocation
|
||||
alternates: 2,
|
||||
language: "de-DE",
|
||||
costing_options: costingOptions,
|
||||
turn_lanes: true
|
||||
}
|
||||
turn_lanes: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,19 +6,42 @@ bicycle, prefer cycleways and bicycle lanes = bicycle
|
||||
truck, prioritizes truck routes = truck
|
||||
motor_scooter = motor scooter, moped, lkfz
|
||||
*/
|
||||
export const VehicleTypes = ["car", "truck", "motorcycle", "bicycle", "motor_scooter"] as const;
|
||||
export type VehicleType = typeof VehicleTypes[number];
|
||||
export const FuelTypes = ["petrol", "diesel", "electric"] as const;
|
||||
export type FuelType = typeof FuelTypes[number];
|
||||
export const EVConnectors = [
|
||||
"Type 2", "CCS", "CHAdeMO", "Tesla Supercharger", "Type 1", "Type 3",
|
||||
"SEV 1011 (Type 13)", "SEV 1011 (Type 15)", "SEV 1011 (Type 23)", "SEV 1011 (Type 25)",
|
||||
"CEE (red)", "CEE (blue)", "Schuko", "CCS Type 2", "Other"
|
||||
export const VehicleTypes = [
|
||||
"car",
|
||||
"truck",
|
||||
"motorcycle",
|
||||
"bicycle",
|
||||
"motor_scooter",
|
||||
] as const;
|
||||
export type EVConnector = typeof EVConnectors[number];
|
||||
export const PreferredFuels = ["Super", "Super E10", "Diesel", ...EVConnectors] as const;
|
||||
export type PreferredFuel = typeof PreferredFuels[number];
|
||||
export type Vehicle = {
|
||||
export type VehicleType = (typeof VehicleTypes)[number];
|
||||
export const FuelTypes = ["petrol", "diesel", "electric"] as const;
|
||||
export type FuelType = (typeof FuelTypes)[number];
|
||||
export const EVConnectors = [
|
||||
"Type 2",
|
||||
"CCS",
|
||||
"CHAdeMO",
|
||||
"Tesla Supercharger",
|
||||
"Type 1",
|
||||
"Type 3",
|
||||
"SEV 1011 (Type 13)",
|
||||
"SEV 1011 (Type 15)",
|
||||
"SEV 1011 (Type 23)",
|
||||
"SEV 1011 (Type 25)",
|
||||
"CEE (red)",
|
||||
"CEE (blue)",
|
||||
"Schuko",
|
||||
"CCS Type 2",
|
||||
"Other",
|
||||
] as const;
|
||||
export type EVConnector = (typeof EVConnectors)[number];
|
||||
export const PreferredFuels = [
|
||||
"Super",
|
||||
"Super E10",
|
||||
"Diesel",
|
||||
...EVConnectors,
|
||||
] as const;
|
||||
export type PreferredFuel = (typeof PreferredFuels)[number];
|
||||
export interface Vehicle {
|
||||
name: string;
|
||||
legalMaxSpeed: number;
|
||||
actualMaxSpeed: number;
|
||||
@@ -31,7 +54,7 @@ export type Vehicle = {
|
||||
emissionClass: string;
|
||||
fuelType: FuelType;
|
||||
preferredFuel: PreferredFuel;
|
||||
};
|
||||
}
|
||||
|
||||
export const DefaultVehicle: Vehicle = {
|
||||
name: "Default Vehicle",
|
||||
@@ -40,49 +63,64 @@ export const DefaultVehicle: Vehicle = {
|
||||
type: "motor_scooter",
|
||||
emissionClass: "Euro 4",
|
||||
fuelType: "diesel",
|
||||
preferredFuel: "Diesel"
|
||||
}
|
||||
preferredFuel: "Diesel",
|
||||
};
|
||||
|
||||
type StateValue<T> = {v: T};
|
||||
export const vehicles: Vehicle[] = $state(localStorage.getItem("vehicles") ? JSON.parse(localStorage.getItem("vehicles")!) : []);
|
||||
interface StateValue<T> {
|
||||
v: T;
|
||||
}
|
||||
export const vehicles: Vehicle[] = $state(
|
||||
localStorage.getItem("vehicles")
|
||||
? JSON.parse(localStorage.getItem("vehicles")!)
|
||||
: [],
|
||||
);
|
||||
export const selectedVehicleIdx: StateValue<number | null> = $state({
|
||||
v: localStorage.getItem("selectedVehicle") ? parseInt(localStorage.getItem("selectedVehicle")!) : null
|
||||
v: localStorage.getItem("selectedVehicle")
|
||||
? parseInt(localStorage.getItem("selectedVehicle")!)
|
||||
: null,
|
||||
});
|
||||
export const selectedVehicle: () => Vehicle | null = () => {
|
||||
return vehicles[selectedVehicleIdx.v !== null ? selectedVehicleIdx.v : 0] || null
|
||||
return (
|
||||
vehicles[selectedVehicleIdx.v !== null ? selectedVehicleIdx.v : 0] || null
|
||||
);
|
||||
};
|
||||
|
||||
export function setVehicles(_vehicles: Vehicle[]) {
|
||||
// vehicles = _vehicles;
|
||||
// Hack to update without reassigning the array
|
||||
vehicles.length = 0;
|
||||
_vehicles.forEach(vehicle => vehicles.push(vehicle));
|
||||
_vehicles.forEach((vehicle) => vehicles.push(vehicle));
|
||||
localStorage.setItem("vehicles", JSON.stringify(vehicles));
|
||||
}
|
||||
|
||||
export function selectVehicle(vehicle: Vehicle | null) {
|
||||
if(vehicle == null) {
|
||||
if (vehicle == null) {
|
||||
selectedVehicleIdx.v = null;
|
||||
} else {
|
||||
selectedVehicleIdx.v = vehicles.findIndex(v => v.name === vehicle.name);
|
||||
if(selectedVehicleIdx.v === -1) {
|
||||
selectedVehicleIdx.v = vehicles.findIndex((v) => v.name === vehicle.name);
|
||||
if (selectedVehicleIdx.v === -1) {
|
||||
selectedVehicleIdx.v = null;
|
||||
}
|
||||
}
|
||||
localStorage.setItem("selectedVehicle", selectedVehicleIdx.v !== null ? selectedVehicleIdx.v.toString() : "");
|
||||
localStorage.setItem(
|
||||
"selectedVehicle",
|
||||
selectedVehicleIdx.v !== null ? selectedVehicleIdx.v.toString() : "",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the vehicle uses the correct preferred fuel type
|
||||
*/
|
||||
export function isValidFuel(vehicle: Vehicle): boolean {
|
||||
if(vehicle.fuelType == "petrol") {
|
||||
return vehicle.preferredFuel == "Super" || vehicle.preferredFuel == "Super E10";
|
||||
if (vehicle.fuelType == "petrol") {
|
||||
return (
|
||||
vehicle.preferredFuel == "Super" || vehicle.preferredFuel == "Super E10"
|
||||
);
|
||||
}
|
||||
if(vehicle.fuelType == "diesel") {
|
||||
if (vehicle.fuelType == "diesel") {
|
||||
return vehicle.preferredFuel == "Diesel";
|
||||
}
|
||||
if(vehicle.fuelType == "electric") {
|
||||
if (vehicle.fuelType == "electric") {
|
||||
return EVConnectors.includes(vehicle.preferredFuel as EVConnector);
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
export function checkWebGL() {
|
||||
if(window.WebGLRenderingContext) {
|
||||
if (window.WebGLRenderingContext) {
|
||||
const canvas = document.createElement("canvas");
|
||||
try {
|
||||
const ctx = canvas.getContext("webgl2") || canvas.getContext("webgl");
|
||||
if(ctx && typeof ctx.getParameter == "function") {
|
||||
if (ctx && typeof ctx.getParameter == "function") {
|
||||
return true;
|
||||
}
|
||||
} catch(e) {
|
||||
} catch (_e) {
|
||||
// Supported, but disabled
|
||||
alert("WebGL is supported but disabled in your browser. Please enable it in your settings.")
|
||||
alert(
|
||||
"WebGL is supported but disabled in your browser. Please enable it in your settings.",
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// WebGL is not supported
|
||||
alert("WebGL is not supported in your browser. Please try a different browser.");
|
||||
alert(
|
||||
"WebGL is not supported in your browser. Please try a different browser.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user