style: run prettier
All checks were successful
TrafficCue CI / check (push) Successful in 1m34s
TrafficCue CI / build (push) Successful in 10m30s
TrafficCue CI / build-android (push) Successful in 26m47s

This commit is contained in:
2025-10-21 15:46:50 +02:00
parent 4036790a4b
commit 372b31876d
17 changed files with 306 additions and 167 deletions

View File

@ -55,13 +55,22 @@
preferredFuel: "Diesel",
});
const biggerButtonsInDrive = localStore<boolean>("bigger-buttons-in-drive", false);
const shouldUseLargeSize = $derived(biggerButtonsInDrive.current ? isDriving() : false);
const biggerButtonsInDrive = localStore<boolean>(
"bigger-buttons-in-drive",
false,
);
const shouldUseLargeSize = $derived(
biggerButtonsInDrive.current ? isDriving() : false,
);
</script>
<Drawer.Root bind:open>
<Drawer.Trigger
class={buttonVariants({ variant: "secondary", class: "w-full", size: shouldUseLargeSize ? "drive" : "default" })}
class={buttonVariants({
variant: "secondary",
class: "w-full",
size: shouldUseLargeSize ? "drive" : "default",
})}
>
{@render children()}
</Drawer.Trigger>

View File

@ -178,8 +178,13 @@
{/if}
{#if !hideSearch && !!location.speed != false}
<div id="speedometer" style="position: fixed; {mobileView ? `bottom: calc(50px + ${sidebarHeight.current}px + 10px); right: 10px;` : "bottom: 10px; right: 10px;"}">
{(location.speed * 3.6 | 0).toFixed(0)}
<div
id="speedometer"
style="position: fixed; {mobileView
? `bottom: calc(50px + ${sidebarHeight.current}px + 10px); right: 10px;`
: 'bottom: 10px; right: 10px;'}"
>
{((location.speed * 3.6) | 0).toFixed(0)}
</div>
{/if}
{#if getRoadMetadata().current}
@ -187,7 +192,12 @@
{#if meta.maxspeed}
{@const maxspeed = getSpeed(meta.maxspeed)}
{#if maxspeed && maxspeed < 100}
<div id="max-speed" style="position: fixed; {mobileView ? `bottom: calc(50px + ${sidebarHeight.current}px + 10px + 2ch + 20px + 10px); right: 10px;` : "bottom: calc(10px + 2ch + 20px + 10px); right: 10px;"}">
<div
id="max-speed"
style="position: fixed; {mobileView
? `bottom: calc(50px + ${sidebarHeight.current}px + 10px + 2ch + 20px + 10px); right: 10px;`
: 'bottom: calc(10px + 2ch + 20px + 10px); right: 10px;'}"
>
{meta.maxspeed}
</div>
{/if}

View File

@ -41,13 +41,22 @@
}
}
const biggerButtonsInDrive = localStore<boolean>("bigger-buttons-in-drive", false);
const shouldUseLargeSize = $derived(biggerButtonsInDrive.current ? isDriving() : false);
const biggerButtonsInDrive = localStore<boolean>(
"bigger-buttons-in-drive",
false,
);
const shouldUseLargeSize = $derived(
biggerButtonsInDrive.current ? isDriving() : false,
);
</script>
<Drawer.Root bind:open>
<Drawer.Trigger
class={buttonVariants({ variant: "secondary", class: "w-full", size: shouldUseLargeSize ? "drive" : "default" })}
class={buttonVariants({
variant: "secondary",
class: "w-full",
size: shouldUseLargeSize ? "drive" : "default",
})}
>
{@const vehicle = selectedVehicle()?.data ?? DefaultVehicle}
{@const Icon = getVehicleIcon(vehicle.type)}

View File

@ -35,13 +35,15 @@ export const location = $state({
lastUpdate: null as Date | null,
});
const _isDriving = $derived(location.speed > (7 / 3.6));
const _isDriving = $derived(location.speed > 7 / 3.6);
export function isDriving() {
return _isDriving;
}
const roadMetadata: WrappedValue<GeoJSON.GeoJsonProperties> = $state({ current: null });
const roadMetadata: WrappedValue<GeoJSON.GeoJsonProperties> = $state({
current: null,
});
export function getRoadMetadata() {
return roadMetadata;
@ -61,7 +63,10 @@ export function watchLocation() {
location.heading = pos.coords.heading;
location.lastUpdate = new Date();
getMeta({ lat: location.lat, lon: location.lng }, "transportation").then((meta) => {
getMeta(
{ lat: location.lat, lon: location.lng },
"transportation",
).then((meta) => {
roadMetadata.current = meta;
});

View File

@ -1,7 +1,12 @@
<script lang="ts">
import Button from "$lib/components/ui/button/button.svelte";
import * as Card from "$lib/components/ui/card";
import { fetchEvents, type DAVCalendar, type DAVCredentials, type DAVEvent } from "$lib/services/CalDAV";
import {
fetchEvents,
type DAVCalendar,
type DAVCredentials,
type DAVEvent,
} from "$lib/services/CalDAV";
import { search } from "$lib/services/Search";
import { onMount } from "svelte";
import { map, pin } from "../map.svelte";
@ -11,13 +16,14 @@
onMount(async () => {
const calendars = localStorage.getItem("calendars");
if(!calendars) return;
const parsedCalendars: (DAVCalendar & { credentials: DAVCredentials })[] = JSON.parse(calendars);
for(const calendar of parsedCalendars) {
if (!calendars) return;
const parsedCalendars: (DAVCalendar & { credentials: DAVCredentials })[] =
JSON.parse(calendars);
for (const calendar of parsedCalendars) {
const calendarEvents = await fetchEvents(calendar, calendar.credentials);
events.push(...calendarEvents);
}
})
});
</script>
<div id="events">
@ -31,35 +37,40 @@
</Card.Header>
<Card.Content>
<p>
<strong>{m["calendar.start"]()}:</strong> {event.start?.toLocaleString("de-DE") || m["calendar.no-start"]()}
<strong>{m["calendar.start"]()}:</strong>
{event.start?.toLocaleString("de-DE") || m["calendar.no-start"]()}
</p>
<p>
<strong>{m["calendar.end"]()}:</strong> {event.end?.toLocaleString("de-DE") || m["calendar.no-end"]()}
<strong>{m["calendar.end"]()}:</strong>
{event.end?.toLocaleString("de-DE") || m["calendar.no-end"]()}
</p>
<p>
{#if event.location}
<strong>{m["calendar.location"]()}:</strong> {event.location}
{:else}
<strong>{m["calendar.location"]()}:</strong> {m["calendar.no-location"]()}
<strong>{m["calendar.location"]()}:</strong>
{m["calendar.no-location"]()}
{/if}
</p>
</Card.Content>
<Card.Footer>
<Button onclick={async () => {
if(!event.location) return;
const features = await search(event.location, 0, 0);
if(features.length == 0) {
return void alert("Can't find location");
}
const lat = features[0].geometry.coordinates[1];
const lng = features[0].geometry.coordinates[0];
pin.dropPin(lat, lng);
pin.showInfo();
map.value?.flyTo({
center: [lng, lat],
zoom: 19,
});
}}>{m.open()}</Button>
<Button
onclick={async () => {
if (!event.location) return;
const features = await search(event.location, 0, 0);
if (features.length == 0) {
return void alert("Can't find location");
}
const lat = features[0].geometry.coordinates[1];
const lng = features[0].geometry.coordinates[0];
pin.dropPin(lat, lng);
pin.showInfo();
map.value?.flyTo({
center: [lng, lat],
zoom: 19,
});
}}>{m.open()}</Button
>
</Card.Footer>
</Card.Root>
{/each}

View File

@ -74,11 +74,13 @@
const FROM: WorldLocation =
fromLocation == "current"
? { lat: location.lat, lon: location.lng }
: savedLocations.current.find(s => s.name == fromLocation)
: savedLocations.current.find((s) => s.name == fromLocation)
? {
lat: savedLocations.current.find(s => s.name == fromLocation)!.data.lat,
lon: savedLocations.current.find(s => s.name == fromLocation)!.data.lng,
}
lat: savedLocations.current.find((s) => s.name == fromLocation)!
.data.lat,
lon: savedLocations.current.find((s) => s.name == fromLocation)!
.data.lng,
}
: {
lat: parseFloat(fromLocation.split(",")[0]),
lon: parseFloat(fromLocation.split(",")[1]),
@ -86,11 +88,13 @@
const TO: WorldLocation =
toLocation == "current"
? { lat: location.lat, lon: location.lng }
: savedLocations.current.find(s => s.name == toLocation)
: savedLocations.current.find((s) => s.name == toLocation)
? {
lat: savedLocations.current.find(s => s.name == fromLocation)!.data.lat,
lon: savedLocations.current.find(s => s.name == fromLocation)!.data.lng,
}
lat: savedLocations.current.find((s) => s.name == fromLocation)!
.data.lat,
lon: savedLocations.current.find((s) => s.name == fromLocation)!
.data.lng,
}
: {
lat: parseFloat(toLocation.split(",")[0]),
lon: parseFloat(toLocation.split(",")[1]),

View File

@ -8,4 +8,7 @@
{m["sidebar.appearance.header"]()}
</SidebarHeader>
<SettingsToggle text={m["sidebar.appearance.bigger-buttons-in-drive"]()} localStorageKey="bigger-buttons-in-drive" />
<SettingsToggle
text={m["sidebar.appearance.bigger-buttons-in-drive"]()}
localStorageKey="bigger-buttons-in-drive"
/>

View File

@ -6,10 +6,16 @@
import * as Drawer from "$lib/components/ui/drawer";
import { Button } from "$lib/components/ui/button";
import Input from "$lib/components/ui/input/input.svelte";
import { fetchCalendars, findScheme, type AuthScheme, type DAVCalendar, type DAVCredentials } from "$lib/services/CalDAV";
import {
fetchCalendars,
findScheme,
type AuthScheme,
type DAVCalendar,
type DAVCredentials,
} from "$lib/services/CalDAV";
import { onMount } from "svelte";
let calendars: (DAVCalendar & {credentials: DAVCredentials})[] = $state([]);
let calendars: (DAVCalendar & { credentials: DAVCredentials })[] = $state([]);
let calDavDrawerOpen = $state(false);
let calDavLoading = $state(false);
@ -25,15 +31,19 @@
calDavState = m["sidebar.calendar.probing-server"]();
calDavScheme = await findScheme(calDavUrl);
calDavState = m["sidebar.calendar.discovering-calendars"]();
calDavCalendars = await fetchCalendars(calDavUrl, { scheme: calDavScheme, username: calDavUsername, password: calDavPassword });
calDavCalendars = await fetchCalendars(calDavUrl, {
scheme: calDavScheme,
username: calDavUsername,
password: calDavPassword,
});
calDavState = "";
}
onMount(() => {
if(localStorage.getItem("calendars")) {
if (localStorage.getItem("calendars")) {
calendars = JSON.parse(localStorage.getItem("calendars")!);
}
})
});
</script>
<SidebarHeader>
@ -41,15 +51,30 @@
</SidebarHeader>
<Drawer.Root bind:open={calDavDrawerOpen}>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>{m["sidebar.calendar.connect"]()}</Drawer.Title>
</Drawer.Header>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>{m["sidebar.calendar.connect"]()}</Drawer.Title>
</Drawer.Header>
<div class="p-4 pt-0 flex flex-col gap-2">
{#if calDavCalendars}
<Input type="url" placeholder="https://my-caldav-server.com/..." disabled={calDavLoading} bind:value={calDavUrl} />
<Input type="text" placeholder="Username" disabled={calDavLoading} bind:value={calDavUsername} />
<Input type="password" placeholder="Password" disabled={calDavLoading} bind:value={calDavPassword} />
<Input
type="url"
placeholder="https://my-caldav-server.com/..."
disabled={calDavLoading}
bind:value={calDavUrl}
/>
<Input
type="text"
placeholder="Username"
disabled={calDavLoading}
bind:value={calDavUsername}
/>
<Input
type="password"
placeholder="Password"
disabled={calDavLoading}
bind:value={calDavPassword}
/>
{#if calDavState}
<span>{calDavState}</span>
{/if}
@ -59,40 +84,51 @@
<ul class="max-h-48 overflow-y-auto">
{#each calDavCalendars as calendar (calendar.url)}
<li>
<Button class="w-full" variant="secondary" onclick={() => {
if(localStorage.getItem("calendars")) {
const existing = JSON.parse(localStorage.getItem("calendars")!);
existing.push({
name: calendar.name,
url: calendar.url,
credentials: {
username: calDavUsername,
password: calDavPassword,
scheme: calDavScheme
}
});
localStorage.setItem("calendars", JSON.stringify(existing));
} else {
localStorage.setItem("calendars", JSON.stringify([{
name: calendar.name,
url: calendar.url,
credentials: {
username: calDavUsername,
password: calDavPassword,
scheme: calDavScheme
}
}]));
}
calendars.push({
name: calendar.name,
url: calendar.url,
credentials: {
username: calDavUsername,
password: calDavPassword,
scheme: calDavScheme
<Button
class="w-full"
variant="secondary"
onclick={() => {
if (localStorage.getItem("calendars")) {
const existing = JSON.parse(
localStorage.getItem("calendars")!,
);
existing.push({
name: calendar.name,
url: calendar.url,
credentials: {
username: calDavUsername,
password: calDavPassword,
scheme: calDavScheme,
},
});
localStorage.setItem("calendars", JSON.stringify(existing));
} else {
localStorage.setItem(
"calendars",
JSON.stringify([
{
name: calendar.name,
url: calendar.url,
credentials: {
username: calDavUsername,
password: calDavPassword,
scheme: calDavScheme,
},
},
]),
);
}
});
}}>
calendars.push({
name: calendar.name,
url: calendar.url,
credentials: {
username: calDavUsername,
password: calDavPassword,
scheme: calDavScheme,
},
});
}}
>
{calendar.name}
</Button>
</li>
@ -100,33 +136,44 @@
</ul>
{/if}
</div>
<Drawer.Footer>
<Drawer.Footer>
{#if calDavCalendars.length === 0}
<Button onclick={async () => {
calDavLoading = true;
await fetchCalDav().catch((e) => {
calDavState = e;
calDavLoading = false;
})
}}>{m.submit()}</Button>
<Button
onclick={async () => {
calDavLoading = true;
await fetchCalDav().catch((e) => {
calDavState = e;
calDavLoading = false;
});
}}>{m.submit()}</Button
>
{/if}
<Drawer.Close>
<Drawer.Close>
{calDavCalendars.length === 0 ? m.done() : m.cancel()}
</Drawer.Close>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Root>
{#each calendars as calendar (calendar.url)}
<div class="p-2 border rounded mb-2">
<h3 class="font-medium">{calendar.name}</h3>
<Button variant="destructive" size="sm" class="mt-2" onclick={() => {
calendars = calendars.filter(c => c.url !== calendar.url);
localStorage.setItem("calendars", JSON.stringify(calendars));
}}>{m.delete()}</Button>
<Button
variant="destructive"
size="sm"
class="mt-2"
onclick={() => {
calendars = calendars.filter((c) => c.url !== calendar.url);
localStorage.setItem("calendars", JSON.stringify(calendars));
}}>{m.delete()}</Button
>
</div>
{/each}
<SettingsButton text={m["sidebar.calendar.connect"]()} icon={CalendarPlusIcon} onclick={() => {
calDavDrawerOpen = true;
}} />
<SettingsButton
text={m["sidebar.calendar.connect"]()}
icon={CalendarPlusIcon}
onclick={() => {
calDavDrawerOpen = true;
}}
/>

View File

@ -1,6 +1,6 @@
<script>
import {
CalendarSearchIcon,
CalendarSearchIcon,
CloudUploadIcon,
HandIcon,
MapIcon,
@ -62,20 +62,21 @@
onclick={async () => {
const url = prompt("URL?");
if (!url) return;
const scheme = await invoke("dav_find_scheme", { url })
.catch((err) => {
alert("Error fetching scheme: " + err);
});
const scheme = await invoke("dav_find_scheme", { url }).catch((err) => {
alert("Error fetching scheme: " + err);
});
alert("Found scheme: " + scheme);
const username = prompt("Username?");
const password = prompt("Password?");
if (!username || !password) return;
const credentials = { scheme, username, password };
invoke("dav_fetch_calendars", { url, credentials }).then((calendars) => {
alert("Fetched calendars: " + JSON.stringify(calendars));
}).catch((err) => {
alert("Error fetching calendars: " + err);
});
invoke("dav_fetch_calendars", { url, credentials })
.then((calendars) => {
alert("Fetched calendars: " + JSON.stringify(calendars));
})
.catch((err) => {
alert("Error fetching calendars: " + err);
});
}}
/>
<SettingsButton
@ -88,11 +89,16 @@
const password = prompt("Password?");
if (!url || !username || !password) return;
const credentials = { scheme, username, password };
invoke("dav_fetch_events", { calendar: { name: "Calendar", url }, credentials }).then((events) => {
alert("Fetched events: " + JSON.stringify(events));
}).catch((err) => {
alert("Error fetching events: " + err);
});
invoke("dav_fetch_events", {
calendar: { name: "Calendar", url },
credentials,
})
.then((events) => {
alert("Fetched events: " + JSON.stringify(events));
})
.catch((err) => {
alert("Error fetching events: " + err);
});
}}
/>
</section>

View File

@ -11,7 +11,11 @@
onchange,
disabled,
localStorageKey,
value = $bindable(localStorageKey ? localStorage.getItem(localStorageKey) === "true" : false),
value = $bindable(
localStorageKey
? localStorage.getItem(localStorageKey) === "true"
: false,
),
}: {
icon?: Component<IconProps>;
text: string;
@ -23,20 +27,26 @@
</script>
<div class="flex">
<Label
style="width: 100%;"
for="settings-toggle"
>
<Label style="width: 100%;" for="settings-toggle">
{#if Icon}
<Icon />
{/if}
{text}
</Label>
<Switch {disabled} bind:checked={value} onCheckedChange={() => {
if (onchange) onchange();
if (localStorageKey) {
localStorage.setItem(localStorageKey, value ? "true" : "false");
eventTarget.dispatchEvent(new CustomEvent("localStorageChanged", { detail: { key: localStorageKey, value } }));
}
}} id="settings-toggle" />
<Switch
{disabled}
bind:checked={value}
onCheckedChange={() => {
if (onchange) onchange();
if (localStorageKey) {
localStorage.setItem(localStorageKey, value ? "true" : "false");
eventTarget.dispatchEvent(
new CustomEvent("localStorageChanged", {
detail: { key: localStorageKey, value },
}),
);
}
}}
id="settings-toggle"
/>
</div>

View File

@ -8,8 +8,13 @@
} from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
const biggerButtonsInDrive = localStore<boolean>("bigger-buttons-in-drive", false);
const shouldUseLargeSize = $derived(biggerButtonsInDrive.current ? isDriving() : false);
const biggerButtonsInDrive = localStore<boolean>(
"bigger-buttons-in-drive",
false,
);
const shouldUseLargeSize = $derived(
biggerButtonsInDrive.current ? isDriving() : false,
);
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",
@ -32,7 +37,7 @@
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
drive: "h-12 rounded-md px-8 has-[>svg]:px-6"
drive: "h-12 rounded-md px-8 has-[>svg]:px-6",
},
},
defaultVariants: {
@ -69,7 +74,10 @@
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size: shouldUseLargeSize ? "drive" : size }), className)}
class={cn(
buttonVariants({ variant, size: shouldUseLargeSize ? "drive" : size }),
className,
)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? "link" : undefined}
@ -82,7 +90,10 @@
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size: shouldUseLargeSize ? "drive" : size }), className)}
class={cn(
buttonVariants({ variant, size: shouldUseLargeSize ? "drive" : size }),
className,
)}
{type}
{disabled}
{...restProps}

View File

@ -14,7 +14,7 @@
data-slot="label"
class={cn(
"flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
className
className,
)}
{...restProps}
/>

View File

@ -16,14 +16,14 @@
data-slot="switch"
class={cn(
"data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 shadow-xs peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent outline-none transition-all focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
className,
)}
{...restProps}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
class={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitive.Root>

View File

@ -22,18 +22,24 @@ export interface DAVEvent {
}
export async function findScheme(url: string): Promise<AuthScheme> {
const scheme = await invoke("dav_find_scheme", { url });
const scheme = await invoke("dav_find_scheme", { url });
return scheme as AuthScheme;
}
export async function fetchCalendars(url: string, credentials: DAVCredentials): Promise<DAVCalendar[]> {
export async function fetchCalendars(
url: string,
credentials: DAVCredentials,
): Promise<DAVCalendar[]> {
const calendars = await invoke("dav_fetch_calendars", { url, credentials });
return calendars as DAVCalendar[];
}
export async function fetchEvents(calendar: DAVCalendar, credentials: DAVCredentials): Promise<DAVEvent[]> {
export async function fetchEvents(
calendar: DAVCalendar,
credentials: DAVCredentials,
): Promise<DAVEvent[]> {
const events = await invoke("dav_fetch_events", { calendar, credentials });
return (events as DAVEvent[]).map(e => {
return (events as DAVEvent[]).map((e) => {
if (e.start) e.start = new Date(e.start);
if (e.end) e.end = new Date(e.end);
return e;

View File

@ -11,8 +11,8 @@ function getFeatureDistance(f: GeoJSON.Feature, point: [number, number]) {
// Compute the min distance across all parts
return Math.min(
...f.geometry.coordinates.map((coords) =>
pointToLineDistance(point, lineString(coords))
)
pointToLineDistance(point, lineString(coords)),
),
);
} else {
return Infinity;
@ -30,8 +30,11 @@ export async function getMeta(coord: WorldLocation, layer: string) {
const feature = layerData.feature(i);
features.push(feature.toGeoJSON(zxy.x, zxy.y, zxy.z));
}
const filtered = features.filter((f) => f.geometry.type === "LineString" || f.geometry.type == "MultiLineString");
if(filtered.length === 0) return null;
const filtered = features.filter(
(f) =>
f.geometry.type === "LineString" || f.geometry.type == "MultiLineString",
);
if (filtered.length === 0) return null;
const nearest = filtered.reduce((a, b) => {
const distA = getFeatureDistance(a, [coord.lon, coord.lat]);
const distB = getFeatureDistance(b, [coord.lon, coord.lat]);
@ -44,16 +47,16 @@ const IMPLICIT_SPEEDS: Record<string, number> = {
"DE:urban": 50,
"DE:rural": 100, // TODO: 80 (hgv weight > 3.5t, or trailer), 60 (weight > 7.5t)
"DE:living_street": 7,
"DE:bicycle_road": 30
}
"DE:bicycle_road": 30,
};
export function getSpeed(maxspeed: string): number | null {
if(!isNaN(parseInt(maxspeed))) return parseInt(maxspeed);
if(maxspeed.endsWith(" mph")) {
if (!isNaN(parseInt(maxspeed))) return parseInt(maxspeed);
if (maxspeed.endsWith(" mph")) {
const val = parseInt(maxspeed.replace(" mph", ""));
if(!isNaN(val)) return Math.round(val * 1.60934); // Convert to km/h
if (!isNaN(val)) return Math.round(val * 1.60934); // Convert to km/h
}
if(maxspeed === "walk") return 7; // https://wiki.openstreetmap.org/wiki/Proposed_features/maxspeed_walk
if (maxspeed === "walk") return 7; // https://wiki.openstreetmap.org/wiki/Proposed_features/maxspeed_walk
return IMPLICIT_SPEEDS[maxspeed as keyof typeof IMPLICIT_SPEEDS] || null;
}
@ -64,11 +67,11 @@ function coordToTile(coord: WorldLocation, zoom: number) {
((1 -
Math.log(
Math.tan((coord.lat * Math.PI) / 180) +
1 / Math.cos((coord.lat * Math.PI) / 180)
1 / Math.cos((coord.lat * Math.PI) / 180),
) /
Math.PI) /
2) *
Math.pow(2, z)
Math.pow(2, z),
);
return { z, x, y };
}
@ -117,7 +120,7 @@ export async function fetchTile(z: number, x: number, y: number) {
const pmtiles = (await hasPMTiles("tiles"))
? new PMTiles(new FSSource("tiles"))
: new PMTiles("https://trafficcue-tiles.picoscratch.de/germany.pmtiles");
const tile = (await pmtiles.getZxy(z, x, y));
const tile = await pmtiles.getZxy(z, x, y);
if (!tile) {
console.log(tile);
throw new Error(`Tile not found: z${z} x${x} y${y}`);

View File

@ -4,15 +4,16 @@ export const eventTarget = new EventTarget();
export function localStore<T>(key: string, defaultValue: T): WrappedValue<T> {
const storedValue = localStorage.getItem(key);
const initialValue = storedValue !== null ? JSON.parse(storedValue) : defaultValue;
const initialValue =
storedValue !== null ? JSON.parse(storedValue) : defaultValue;
const state = $state<WrappedValue<T>>({ current: initialValue });
eventTarget.addEventListener("localStorageChanged", (event) => {
const customEvent = event as CustomEvent;
console.log("localStorageChanged event received", customEvent.detail);
if(customEvent.detail.key === key) {
if (customEvent.detail.key === key) {
const newValue = customEvent.detail.value as T;
console.log(`localStore: ${key} updated from another tab`, newValue);
if(JSON.stringify(state.current) === JSON.stringify(newValue)) return;
if (JSON.stringify(state.current) === JSON.stringify(newValue)) return;
state.current = newValue;
}
});
@ -22,7 +23,11 @@ export function localStore<T>(key: string, defaultValue: T): WrappedValue<T> {
},
set current(newValue: T) {
state.current = newValue;
eventTarget.dispatchEvent(new CustomEvent("localStorageChanged", { detail: { key, value: newValue } }));
eventTarget.dispatchEvent(
new CustomEvent("localStorageChanged", {
detail: { key, value: newValue },
}),
);
localStorage.setItem(key, JSON.stringify(newValue));
},
};