This repository has been archived on 2025-11-09. You can view files and clone it, but cannot push or open issues or pull requests.
Files
trafficcue-client/src/lib/components/lnv/sidebar/InfoSidebar.svelte
Jannik 59ae422fe3
All checks were successful
TrafficCue CI / check (push) Successful in 1m41s
TrafficCue CI / build (push) Successful in 10m37s
TrafficCue CI / build-android (push) Successful in 26m55s
feat(info): route without successful overpass result
2025-10-25 20:16:25 +02:00

342 lines
7.9 KiB
Svelte

<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { POIIcons } from "$lib/POIIcons";
import {
BriefcaseIcon,
EllipsisIcon,
GlobeIcon,
HomeIcon,
MailIcon,
PhoneIcon,
RouteIcon,
SchoolIcon,
} from "@lucide/svelte";
import { pin } from "../map.svelte";
import SidebarHeader from "./SidebarHeader.svelte";
import { fetchPOI, type OverpassElement } from "$lib/services/Overpass";
import OpeningHours from "../info/OpeningHours.svelte";
import Badge from "$lib/components/ui/badge/badge.svelte";
import FuelStation from "../info/FuelStation.svelte";
import { view } from "../view.svelte";
import * as Popover from "$lib/components/ui/popover";
import Reviews from "../info/Reviews.svelte";
import MapAi from "../info/MapAI.svelte";
import RequiresCapability from "../RequiresCapability.svelte";
import { getDeveloperToggle } from "./settings/developer.svelte";
import InternetAccess from "../info/InternetAccess.svelte";
import RestaurantInfo from "../info/RestaurantInfo.svelte";
import { m } from "$lang/messages";
import { onMount } from "svelte";
import { updateStore } from "$lib/services/stores.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();
const dev = getDeveloperToggle();
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 {
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 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[] {
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)
);
});
}
onMount(() => {
view.loading = true;
});
</script>
{#await fetchPOI(lat, lng, 20).then((r) => {
view.loading = false;
return r;
})}
<SidebarHeader
onback={() => {
pin.liftPin();
}}
>
{m["sidebar.info.dropped"]()}
</SidebarHeader>
<div id="actions">
<Button
onclick={() => {
view.switch("route", {
to: lat + "," + lng,
});
}}
>
<RouteIcon />
{m["sidebar.info.route"]()}
</Button>
</div>
<p>{m.loading()}</p>
{:then res}
{#if res.elements.length === 0}
<SidebarHeader
onback={() => {
pin.liftPin();
}}
>
{m["sidebar.info.dropped"]()}
</SidebarHeader>
<span style="color: #acacac;">&copy; OpenStreetMap</span>
<pre>{JSON.stringify(res, null, 2)}</pre>
{:else}
{@const elements = sortByDistance(res.elements, lat, lng)}
{@const tags = elements[0].tags}
{@const firstElement = elements[0]}
{@const ellat = firstElement.center?.lat || firstElement.lat!}
{@const ellng = firstElement.center?.lon || firstElement.lon!}
<SidebarHeader
onback={() => {
pin.liftPin();
}}
>
{#if getIcon(tags)}
{@const Icon = getIcon(tags)}
<Icon />
{/if}
{tags.name ||
(tags["addr:street"]
? tags["addr:street"] + " " + tags["addr:housenumber"]
: "")}
</SidebarHeader>
<div id="actions">
<Button
onclick={() => {
view.switch("route", {
to: lat + "," + lng,
});
}}
>
<RouteIcon />
{m["sidebar.info.route"]()}
</Button>
{#if tags.email || tags["contact:email"]}
<Button
variant="secondary"
href={`mailto:${tags.email || tags["contact:email"]}`}
target="_blank"
>
<MailIcon />
{m["sidebar.info.email"]()}
</Button>
{/if}
{#if tags.website || tags["contact:website"]}
<Button
variant="secondary"
href={tags.website || tags["contact:website"]}
target="_blank"
>
<GlobeIcon />
{m["sidebar.info.website"]()}
</Button>
{/if}
{#if tags.phone || tags["contact:phone"]}
<Button
variant="secondary"
href={`tel:${tags.phone || tags["contact:phone"]}`}
target="_blank"
>
<PhoneIcon />
{m["sidebar.info.call"]()}
</Button>
{/if}
<Popover.Root>
<Popover.Trigger>
<Button variant="secondary">
<EllipsisIcon />
{m.more()}
</Button>
</Popover.Trigger>
<Popover.Content>
<div class="flex flex-col gap-2">
<Button
variant="outline"
onclick={() => {
updateStore(
{ name: "home", type: "location" },
{
lat,
lng,
name: "home",
icon: "home",
},
);
}}
>
<HomeIcon />
{m["sidebar.info.set-as"]({ name: m["saved.home"]() })}
</Button>
<Button
variant="outline"
onclick={() => {
updateStore(
{ name: "school", type: "location" },
{
lat,
lng,
name: "school",
icon: "school",
},
);
}}
>
<SchoolIcon />
{m["sidebar.info.set-as"]({ name: m["saved.school"]() })}
</Button>
<Button
variant="outline"
onclick={() => {
updateStore(
{ name: "work", type: "location" },
{
lat,
lng,
name: "work",
icon: "work",
},
);
}}
>
<BriefcaseIcon />
{m["sidebar.info.set-as"]({ name: m["saved.work"]() })}
</Button>
{#if dev.current == "true"}
<Button
variant="outline"
onclick={() => {
alert(JSON.stringify(elements, null, 3));
console.log(elements);
}}
>
Show raw data
</Button>
{/if}
</div>
</Popover.Content>
</Popover.Root>
</div>
<RequiresCapability capability="ai">
<MapAi lat={ellat} lon={ellng} />
</RequiresCapability>
<!--
"addr:city": "Hagen",
"addr:housenumber": "12",
"addr:postcode": "58135",
"addr:street": -->
<p class="mt-2">{tags["addr:street"]} {tags["addr:housenumber"]}</p>
<p>{tags["addr:postcode"]} {tags["addr:city"]}</p>
{#if tags.opening_hours}
<OpeningHours hours={tags.opening_hours} {lat} lon={lng} />
{/if}
{#if tags.internet_access}
<InternetAccess {tags} />
{/if}
<RestaurantInfo {tags} />
{#if tags.amenity == "fuel"}
<FuelStation {tags} {lat} {lng} />
{/if}
<!-- any payment:* tag -->
{#if Object.keys(tags).some((key) => key.startsWith("payment:"))}
<h3 class="text-lg font-bold mt-2">
{m["sidebar.info.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] (key)}
<Badge
>{key.replace("payment:", "")}{value == "yes"
? ""
: ": " + value}</Badge
>
{/each}
</ul>
{/if}
<RequiresCapability capability="reviews">
<Reviews lat={ellat} lng={ellng} />
</RequiresCapability>
<span style="color: #acacac;">&copy; OpenStreetMap</span>
{/if}
{:catch err}
<SidebarHeader
onback={() => {
pin.liftPin();
}}
>
Dropped Pin
</SidebarHeader>
<div id="actions">
<Button
onclick={() => {
view.switch("route", {
to: lat + "," + lng,
});
}}
>
<RouteIcon />
{m["sidebar.info.route"]()}
</Button>
</div>
<p>Error: {err.message}</p>
{/await}
<style>
#actions {
display: flex;
gap: 0.5rem;
overflow-y: scroll;
}
</style>