feat: add hazards
Some checks failed
TrafficCue CI / check (push) Failing after 57s
TrafficCue CI / build (push) Successful in 1m41s
TrafficCue CI / build-android (push) Successful in 14m24s

This commit is contained in:
2025-09-18 13:04:34 +02:00
parent 2aed03642d
commit 93514ae5b4
7 changed files with 102 additions and 3 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -17,6 +17,8 @@
} from "$lib/services/OfflineTiles"; } from "$lib/services/OfflineTiles";
import { layers, worldLayers } from "$lib/mapLayers"; import { layers, worldLayers } from "$lib/mapLayers";
import { PMTilesProtocol } from "svelte-maplibre-gl/pmtiles"; import { PMTilesProtocol } from "svelte-maplibre-gl/pmtiles";
import HazardMarker from "./HazardMarker.svelte";
import { hazards } from "./hazards.svelte";
onMount(() => { onMount(() => {
window.addEventListener("resize", map.updateMapPadding); window.addEventListener("resize", map.updateMapPadding);
@ -178,4 +180,9 @@
element={workMarker} element={workMarker}
/> />
{/if} {/if}
<!-- <HazardMarker hazard="bumpy-road" lat={51.347447} lon={7.4028181} /> -->
{#each hazards as hazard (hazard.latitude + "-" + hazard.longitude)}
<HazardMarker hazard={hazard.type} lat={hazard.latitude} lon={hazard.longitude} />
{/each}
</MapLibre> </MapLibre>

View File

@ -0,0 +1,16 @@
<script lang="ts">
import { Marker } from "svelte-maplibre-gl";
import { map } from "./map.svelte";
let { hazard, lat, lon }: { hazard: string, lat: number, lon: number } = $props();
let img: HTMLImageElement | undefined = $state();
</script>
<img src="/img/hazards/{hazard}.png" alt="{hazard} Marker" bind:this={img} style="width: 32px; {map.zoom < 15 ? "display: none;" : ""}" />
<Marker
lnglat={{
lat: lat,
lng: lon,
}}
element={img}
/>

View File

@ -25,6 +25,8 @@
import { Tween } from "svelte/motion"; import { Tween } from "svelte/motion";
import { quintOut } from "svelte/easing"; import { quintOut } from "svelte/easing";
import Progressbar from "../Progressbar.svelte"; import Progressbar from "../Progressbar.svelte";
import { postHazard } from "$lib/services/lnv";
import { fetchHazards } from "./hazards.svelte";
const views: Record<string, string> = { const views: Record<string, string> = {
main: "MainSidebar", main: "MainSidebar",
@ -282,6 +284,16 @@
> >
{m["location.join"]()} {m["location.join"]()}
</Button> </Button>
<Button
variant="outline"
onclick={async () => {
await postHazard({ lat: location.lat, lon: location.lng }, "bumpy-road");
alert("Thanks for your report!");
await fetchHazards();
}}
>
Report Bumpy Road
</Button>
</div> </div>
</Popover.Content> </Popover.Content>
</Popover.Root> </Popover.Root>

View File

@ -0,0 +1,22 @@
import { getHazards, type Hazard } from "$lib/services/lnv";
import { location } from "./location.svelte";
export const hazards: Hazard[] = $state([]);
export async function fetchHazards() {
if(!location.available) return;
const newHazards = await getHazards({ lat: location.lat, lon: location.lng }, 100); // TODO: get radius from server config
hazards.splice(0, hazards.length, ...newHazards);
}
setInterval(() => {
fetchHazards();
}, 1000 * 60 * 5) // Every 5 minutes
// TODO: get from server config
const initalFetch = setInterval(() => {
if(location.available) {
fetchHazards();
clearInterval(initalFetch);
}
})

View File

@ -6,7 +6,7 @@ export const SEARCH_SERVER = "https://photon.komoot.io/";
export const OVERPASS_SERVER = "https://overpass-api.de/api/interpreter"; export const OVERPASS_SERVER = "https://overpass-api.de/api/interpreter";
export const LNV_SERVER = export const LNV_SERVER =
location.hostname == "localhost" && location.protocol == "http:" location.hostname == "localhost" && location.protocol == "http:"
? "http://localhost:3000/api" ? "https://staging-trafficcue-api.picoscratch.de/api"
: location.hostname.includes("staging") : location.hostname.includes("staging")
? "https://staging-trafficcue-api.picoscratch.de/api" ? "https://staging-trafficcue-api.picoscratch.de/api"
: "https://trafficcue-api.picoscratch.de/api"; : "https://trafficcue-api.picoscratch.de/api";

View File

@ -108,20 +108,20 @@ export async function authFetch(
params?: RequestInit, params?: RequestInit,
): ReturnType<typeof fetch> { ): ReturnType<typeof fetch> {
let res = await fetch(url, { let res = await fetch(url, {
...params,
headers: { headers: {
Authorization: "Bearer " + localStorage.getItem("lnv-token"), Authorization: "Bearer " + localStorage.getItem("lnv-token"),
}, },
...params,
}); });
if (res.status != 401) { if (res.status != 401) {
return res; return res;
} }
await refreshToken(); await refreshToken();
res = await fetch(url, { res = await fetch(url, {
...params,
headers: { headers: {
Authorization: "Bearer " + localStorage.getItem("lnv-token"), Authorization: "Bearer " + localStorage.getItem("lnv-token"),
}, },
...params,
}); });
if (res.status == 401) { if (res.status == 401) {
console.error("Server is misconfigured."); console.error("Server is misconfigured.");
@ -230,3 +230,45 @@ export async function isSaved(data: Trip) {
if (filtered.length == 0) return false; if (filtered.length == 0) return false;
return filtered[0].name; return filtered[0].name;
} }
export interface Hazard {
user_id: string;
latitude: number;
longitude: number;
type: string;
}
export async function getHazards(location: WorldLocation, radius = 50) {
// if (!(await hasCapability("hazards"))) {
// throw new Error("Hazards capability is not available");
// }
const res = await fetch(
LNV_SERVER + `/hazards?lat=${location.lat}&lon=${location.lon}&radius=${radius}`,
);
if (!res.ok) {
throw new Error(`Failed to fetch hazards: ${res.statusText}`);
}
const data = await res.json();
return data as Hazard[];
}
export async function postHazard(location: WorldLocation, type: string) {
// if (!(await hasCapability("hazards"))) {
// throw new Error("Hazards capability is not available");
// }
const res = await authFetch(LNV_SERVER + `/hazards`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
lat: location.lat,
lon: location.lon,
type,
}),
});
if (!res.ok) {
throw new Error(`Failed to post hazard: ${res.statusText}`);
}
return await res.json();
}