feat: get nearby POIs
This commit is contained in:
@ -5,7 +5,8 @@
|
|||||||
"home": "Heim",
|
"home": "Heim",
|
||||||
"school": "Schule",
|
"school": "Schule",
|
||||||
"work": "Arbeit",
|
"work": "Arbeit",
|
||||||
"no-location": "Kein {name} Speicherort gespeichert."
|
"no-location": "Kein {name} Speicherort gespeichert.",
|
||||||
|
"saved-routes": "Gespeicherte Routen"
|
||||||
},
|
},
|
||||||
"location": {
|
"location": {
|
||||||
"unlock": "Standort entsperren",
|
"unlock": "Standort entsperren",
|
||||||
@ -136,6 +137,11 @@
|
|||||||
"description": "TrafficCue ist auf Android verfügbar. Holen Sie es sich für das beste Erlebnis!",
|
"description": "TrafficCue ist auf Android verfügbar. Holen Sie es sich für das beste Erlebnis!",
|
||||||
"button": "Runterladen"
|
"button": "Runterladen"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"nearby-poi": {
|
||||||
|
"header": "POIs in der Nähe",
|
||||||
|
"no-poi": "Keine POIs in der Nähe gefunden.",
|
||||||
|
"loading": "POIs in der Nähe werden geladen …"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"location-selector": {
|
"location-selector": {
|
||||||
@ -143,5 +149,10 @@
|
|||||||
"searching": "Suchen...",
|
"searching": "Suchen...",
|
||||||
"no-results": "Keine Orte gefunden."
|
"no-results": "Keine Orte gefunden."
|
||||||
},
|
},
|
||||||
"unsave": "Löschen"
|
"unsave": "Löschen",
|
||||||
|
"unnamed": "Unbenannt",
|
||||||
|
"poi": {
|
||||||
|
"fuel": "Tankstelle",
|
||||||
|
"parking": "Parken"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -137,6 +137,11 @@
|
|||||||
"description": "TrafficCue is available on Android. Get it for the best experience!",
|
"description": "TrafficCue is available on Android. Get it for the best experience!",
|
||||||
"button": "Get it"
|
"button": "Get it"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"nearby-poi": {
|
||||||
|
"header": "Nearby Points of Interest",
|
||||||
|
"no-poi": "No points of interest found nearby.",
|
||||||
|
"loading": "Loading nearby points of interest..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"unsave": "Unsave",
|
"unsave": "Unsave",
|
||||||
@ -144,5 +149,10 @@
|
|||||||
"current": "Current Location",
|
"current": "Current Location",
|
||||||
"searching": "Searching...",
|
"searching": "Searching...",
|
||||||
"no-results": "No locations found."
|
"no-results": "No locations found."
|
||||||
|
},
|
||||||
|
"unnamed": "Unnamed",
|
||||||
|
"poi": {
|
||||||
|
"fuel": "Fuel Station",
|
||||||
|
"parking": "Parking"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,7 @@
|
|||||||
language: "settings/LanguageSidebar",
|
language: "settings/LanguageSidebar",
|
||||||
onboarding: "onboarding/OnboardingSidebar",
|
onboarding: "onboarding/OnboardingSidebar",
|
||||||
"onboarding-vehicles": "onboarding/OnboardingVehiclesSidebar",
|
"onboarding-vehicles": "onboarding/OnboardingVehiclesSidebar",
|
||||||
|
"nearby-poi": "NearbyPOISidebar",
|
||||||
};
|
};
|
||||||
|
|
||||||
const fullscreen: Record<string, boolean> = {
|
const fullscreen: Record<string, boolean> = {
|
||||||
@ -55,6 +56,7 @@
|
|||||||
language: true,
|
language: true,
|
||||||
onboarding: true,
|
onboarding: true,
|
||||||
"onboarding-vehicles": true,
|
"onboarding-vehicles": true,
|
||||||
|
"nearby-poi": false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
@ -62,7 +64,7 @@
|
|||||||
let startHeight = 0;
|
let startHeight = 0;
|
||||||
let sidebarHeight = new Tween(200, {
|
let sidebarHeight = new Tween(200, {
|
||||||
duration: 500,
|
duration: 500,
|
||||||
easing: quintOut
|
easing: quintOut,
|
||||||
});
|
});
|
||||||
let lastSidebarHeight = 0;
|
let lastSidebarHeight = 0;
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
import {
|
import {
|
||||||
BriefcaseIcon,
|
BriefcaseIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
FuelIcon,
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
|
ParkingSquareIcon,
|
||||||
SchoolIcon,
|
SchoolIcon,
|
||||||
} from "@lucide/svelte";
|
} from "@lucide/svelte";
|
||||||
import { Button } from "../../ui/button";
|
import { Button } from "../../ui/button";
|
||||||
@ -104,6 +106,40 @@
|
|||||||
|
|
||||||
<VehicleSelector />
|
<VehicleSelector />
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="display: flex; gap: 0.5rem; justify-content: space-evenly;"
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
|
style="flex: 1;"
|
||||||
|
onclick={() => {
|
||||||
|
view.switch("nearby-poi", {
|
||||||
|
tags: "amenity=fuel",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FuelIcon />
|
||||||
|
<!-- TODO: hide if no space -->
|
||||||
|
{m["poi.fuel"]()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
|
style="flex: 1;"
|
||||||
|
onclick={() => {
|
||||||
|
view.switch("nearby-poi", {
|
||||||
|
tags: "amenity=parking",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ParkingSquareIcon />
|
||||||
|
<!-- TODO: hide if no space -->
|
||||||
|
{m["poi.parking"]()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if !window.__TAURI__}
|
{#if !window.__TAURI__}
|
||||||
<Card.Root style="margin-top: 1rem;">
|
<Card.Root style="margin-top: 1rem;">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
|
|||||||
110
src/lib/components/lnv/sidebar/NearbyPOISidebar.svelte
Normal file
110
src/lib/components/lnv/sidebar/NearbyPOISidebar.svelte
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { m } from "$lang/messages";
|
||||||
|
import Button from "$lib/components/ui/button/button.svelte";
|
||||||
|
import { fetchNearbyPOI, type OverpassElement } from "$lib/services/Overpass";
|
||||||
|
import { location } from "../location.svelte";
|
||||||
|
import { map, pin } from "../map.svelte";
|
||||||
|
import SidebarHeader from "./SidebarHeader.svelte";
|
||||||
|
|
||||||
|
let { tags }: { tags?: string } = $props();
|
||||||
|
|
||||||
|
let pois: OverpassElement[] = $state([]);
|
||||||
|
let loading = $state(true);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!tags) {
|
||||||
|
loading = false;
|
||||||
|
pois = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchNearbyPOI(location.lat, location.lng, tags.split(","), 500).then(
|
||||||
|
(results) => {
|
||||||
|
pois = results.elements.sort((a, b) => {
|
||||||
|
const distA = distanceTo(
|
||||||
|
a.lat ?? a.center?.lat ?? 0.0,
|
||||||
|
a.lon ?? a.center?.lon ?? 0.0,
|
||||||
|
);
|
||||||
|
const distB = distanceTo(
|
||||||
|
b.lat ?? b.center?.lat ?? 0.0,
|
||||||
|
b.lon ?? b.center?.lon ?? 0.0,
|
||||||
|
);
|
||||||
|
return distA - distB;
|
||||||
|
});
|
||||||
|
loading = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// returns meters
|
||||||
|
function distanceTo(lat: number, lon: number) {
|
||||||
|
const R = 6371e3; // metres
|
||||||
|
const φ1 = (location.lat * Math.PI) / 180; // φ, λ in radians
|
||||||
|
const φ2 = (lat * Math.PI) / 180;
|
||||||
|
const Δφ = ((lat - location.lat) * Math.PI) / 180;
|
||||||
|
const Δλ = ((lon - location.lng) * Math.PI) / 180;
|
||||||
|
|
||||||
|
const a =
|
||||||
|
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
|
||||||
|
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
|
||||||
|
return R * c; // in metres
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SidebarHeader>
|
||||||
|
{#if tags}
|
||||||
|
{tags.split(",")[0].split("=")[1]?.replace(/_/g, " ") ?? "POIs"}
|
||||||
|
{:else}
|
||||||
|
{m["sidebar.nearby-poi.header"]()}
|
||||||
|
{/if}
|
||||||
|
</SidebarHeader>
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="text-sm text-muted-foreground">{m.loading()}</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-2 p-2">
|
||||||
|
{#if pois.length === 0}
|
||||||
|
<div class="text-sm text-muted-foreground">
|
||||||
|
{m["sidebar.nearby-poi.no-poi"]()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#each pois as poi (poi.id)}
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
class="w-full flex flex-col items-start h-auto"
|
||||||
|
onclick={() => {
|
||||||
|
pin.dropPin(
|
||||||
|
poi.lat ?? poi.center?.lat ?? 0.0,
|
||||||
|
poi.lon ?? poi.center?.lon ?? 0.0,
|
||||||
|
);
|
||||||
|
pin.showInfo();
|
||||||
|
map.value?.flyTo({
|
||||||
|
center: [
|
||||||
|
poi.lon ?? poi.center?.lon ?? 0.0,
|
||||||
|
poi.lat ?? poi.center?.lat ?? 0.0,
|
||||||
|
],
|
||||||
|
zoom: 19,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="font-bold">
|
||||||
|
{poi.tags.name ?? poi.tags.brand ?? m.unnamed()}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
{#if poi.tags.amenity}
|
||||||
|
<span class="capitalize">{poi.tags.amenity}</span>
|
||||||
|
{/if}
|
||||||
|
<span>
|
||||||
|
{Math.round(
|
||||||
|
distanceTo(
|
||||||
|
poi.lat ?? poi.center?.lat ?? 0.0,
|
||||||
|
poi.lon ?? poi.center?.lon ?? 0.0,
|
||||||
|
),
|
||||||
|
)}m
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
Reference in New Issue
Block a user