adds routing engine

This commit is contained in:
Christian Beutel
2024-04-21 00:34:57 +02:00
parent 13f0f5ca8a
commit 9af7ddeffd
15 changed files with 237 additions and 25 deletions

View File

@@ -71,7 +71,7 @@ services:
volumes: volumes:
- ./data/valhalla:/custom_files - ./data/valhalla:/custom_files
environment: environment:
- tile_urls=https://download.geofabrik.de/europe/germany-latest.osm.pbf - tile_urls=https://download.geofabrik.de/europe/germany-latest.osm.pbf https://download.geofabrik.de/europe/germany/bayern/oberbayern-latest.osm.pbf
- use_tiles_ignore_pbf=True - use_tiles_ignore_pbf=True
- force_rebuild=False - force_rebuild=False
- force_rebuild_elevation=False - force_rebuild_elevation=False

View File

@@ -14,13 +14,14 @@
export let map: Map | null = null; export let map: Map | null = null;
export let options = {}; export let options = {};
export let graticule: AutoGraticule | null = null; export let graticule: AutoGraticule | null = null;
export let crosshair: boolean = false;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let L: any; let L: any;
let controlElevation: any; let controlElevation: any;
$: if (trail?.expand.gpx_data && controlElevation) { $: if (trail?.expand.gpx_data !== undefined && controlElevation) {
controlElevation.clear(); controlElevation.clear();
controlElevation.load(trail.expand.gpx_data); controlElevation.load(trail.expand.gpx_data);
} }
@@ -31,7 +32,9 @@
await import("leaflet.awesome-markers"); await import("leaflet.awesome-markers");
//@ts-ignore //@ts-ignore
await import("$lib/vendor/leaflet-elevation/src/index.js"); await import("$lib/vendor/leaflet-elevation/src/index.js");
const AutoGraticule = (await import("$lib/vendor/leaflet-graticule/leaflet-auto-graticule")).default; const AutoGraticule = (
await import("$lib/vendor/leaflet-graticule/leaflet-auto-graticule")
).default;
map = L.map("map", { preferCanvas: true }).setView( map = L.map("map", { preferCanvas: true }).setView(
[trail?.lat ?? 0, trail?.lon ?? 0], [trail?.lat ?? 0, trail?.lon ?? 0],
@@ -43,6 +46,10 @@
dispatch("zoomend", map); dispatch("zoomend", map);
}); });
map!.on("click", function (e) {
dispatch("click", e);
});
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap contributors", attribution: "© OpenStreetMap contributors",
}).addTo(map); }).addTo(map);
@@ -99,7 +106,7 @@
iconColor: "white", iconColor: "white",
}), }),
}, },
graticule: false graticule: false,
}; };
const elevation_options = Object.assign( const elevation_options = Object.assign(
@@ -119,12 +126,17 @@
graticule = new AutoGraticule(); graticule = new AutoGraticule();
graticule.addTo(map!); graticule.addTo(map!);
} }
}); });
</script> </script>
<div id="map-container" class="flex flex-col"> <div id="map-container" class="flex flex-col">
<div id="map" class="rounded-xl z-0 basis-full"></div> <div
id="map"
class="rounded-xl z-0 basis-full"
style={crosshair
? "position: relative; outline-style: none;cursor: crosshair !important"
: "position: relative; outline-style: none;"}
></div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<slot /> <slot />
<div class="basis-[300px] flex-grow flex-shrink-0" id="elevation"></div> <div class="basis-[300px] flex-grow flex-shrink-0" id="elevation"></div>

View File

@@ -38,7 +38,7 @@
const tabs = [ const tabs = [
$_("description"), $_("description"),
$_("waypoints"), $_("waypoints", { values: { n: 2 } }),
$_("photos"), $_("photos"),
$_("summit-book"), $_("summit-book"),
...($currentUser ? [$_("comment", { values: { n: 2 } })] : []), ...($currentUser ? [$_("comment", { values: { n: 2 } })] : []),

View File

@@ -40,6 +40,7 @@
"distance": "Distanz", "distance": "Distanz",
"documentation": "Dokumentation", "documentation": "Dokumentation",
"download-gpx": "GPX herunterladen", "download-gpx": "GPX herunterladen",
"draw-on-map": "",
"dutch": "Niederländisch", "dutch": "Niederländisch",
"easy": "Einfach", "easy": "Einfach",
"edit": "Bearbeiten", "edit": "Bearbeiten",
@@ -128,6 +129,7 @@
"show-on-map": "Auf der Karte anzeigen", "show-on-map": "Auf der Karte anzeigen",
"slogan": "Speichere deine Abenteuer!", "slogan": "Speichere deine Abenteuer!",
"sort": "Sortieren", "sort": "Sortieren",
"stop-drawing": "",
"summit-book": "Gipfelbuch", "summit-book": "Gipfelbuch",
"text": "Text", "text": "Text",
"trail": "{n, plural, =1 {Route} other {Routen}}", "trail": "{n, plural, =1 {Route} other {Routen}}",
@@ -136,7 +138,7 @@
"upload-file": "Datei hochladen", "upload-file": "Datei hochladen",
"upload-gpx": "GPX hochladen", "upload-gpx": "GPX hochladen",
"username": "Nutzername", "username": "Nutzername",
"waypoints": "Wegpunkte", "waypoints": "{n, plural, =1 {Wegpunkt} other {Wegpunkte}}",
"welcome_to": "Willkommen bei", "welcome_to": "Willkommen bei",
"wrong-username-or-password": "Falscher Nutzername oder falsches Passwort" "wrong-username-or-password": "Falscher Nutzername oder falsches Passwort"
} }

View File

@@ -40,6 +40,7 @@
"distance": "Distance", "distance": "Distance",
"documentation": "Documentation", "documentation": "Documentation",
"download-gpx": "Download GPX", "download-gpx": "Download GPX",
"draw-on-map": "Draw on map",
"dutch": "Dutch", "dutch": "Dutch",
"easy": "Easy", "easy": "Easy",
"edit": "Edit", "edit": "Edit",
@@ -128,6 +129,7 @@
"show-on-map": "Show on map", "show-on-map": "Show on map",
"slogan": "Save your adventures!", "slogan": "Save your adventures!",
"sort": "Sort", "sort": "Sort",
"stop-drawing": "Stop drawing",
"summit-book": "Summit Book", "summit-book": "Summit Book",
"text": "Text", "text": "Text",
"trail": "{n, plural, =1 {Trail} other {Trails}}", "trail": "{n, plural, =1 {Trail} other {Trails}}",
@@ -136,7 +138,7 @@
"upload-file": "Upload file", "upload-file": "Upload file",
"upload-gpx": "Upload GPX", "upload-gpx": "Upload GPX",
"username": "Username", "username": "Username",
"waypoints": "Waypoints", "waypoints": "{n, plural, =1 {Waypoint} other {Waypoints}}",
"welcome_to": "Welcome to", "welcome_to": "Welcome to",
"wrong-username-or-password": "Wrong username or password" "wrong-username-or-password": "Wrong username or password"
} }

View File

@@ -40,6 +40,7 @@
"distance": "Distance", "distance": "Distance",
"documentation": "Documentation", "documentation": "Documentation",
"download-gpx": "Télécharger le GPX", "download-gpx": "Télécharger le GPX",
"draw-on-map": "",
"dutch": "Néerlandais", "dutch": "Néerlandais",
"easy": "Facile", "easy": "Facile",
"edit": "Editer", "edit": "Editer",
@@ -128,6 +129,7 @@
"show-on-map": "Voir sur la carte", "show-on-map": "Voir sur la carte",
"slogan": "Sauvegarder vos aventures !", "slogan": "Sauvegarder vos aventures !",
"sort": "Trier", "sort": "Trier",
"stop-drawing": "",
"summit-book": "Livre du sommet", "summit-book": "Livre du sommet",
"text": "Texte", "text": "Texte",
"trail": "{n, plural, =1 {Itinéraire} other {Itinéraires}}", "trail": "{n, plural, =1 {Itinéraire} other {Itinéraires}}",

View File

@@ -40,6 +40,7 @@
"distance": "Távolság", "distance": "Távolság",
"documentation": "Dokumentáció", "documentation": "Dokumentáció",
"download-gpx": "GPX letöltése", "download-gpx": "GPX letöltése",
"draw-on-map": "",
"dutch": "Holland", "dutch": "Holland",
"easy": "Könnyű", "easy": "Könnyű",
"edit": "Szerkesztés", "edit": "Szerkesztés",
@@ -128,6 +129,7 @@
"show-on-map": "Mutatás térképen", "show-on-map": "Mutatás térképen",
"slogan": "Mentse el a kalandjait!", "slogan": "Mentse el a kalandjait!",
"sort": "Rendezés", "sort": "Rendezés",
"stop-drawing": "",
"summit-book": "Csúcspont könyv", "summit-book": "Csúcspont könyv",
"text": "Szöveg", "text": "Szöveg",
"trail": "{n, plural, =1 {Útvonal} other {Útvonalak}}", "trail": "{n, plural, =1 {Útvonal} other {Útvonalak}}",

View File

@@ -40,6 +40,7 @@
"distance": "Afstand", "distance": "Afstand",
"documentation": "Documentatie", "documentation": "Documentatie",
"download-gpx": "GPX-bestand downloaden", "download-gpx": "GPX-bestand downloaden",
"draw-on-map": "",
"dutch": "Nederlands", "dutch": "Nederlands",
"easy": "Makkelijk", "easy": "Makkelijk",
"edit": "Bewerken", "edit": "Bewerken",
@@ -128,6 +129,7 @@
"show-on-map": "Tonen op kaart", "show-on-map": "Tonen op kaart",
"slogan": "Bewaar je avonturen!", "slogan": "Bewaar je avonturen!",
"sort": "Sorteren", "sort": "Sorteren",
"stop-drawing": "",
"summit-book": "Bergtopboek", "summit-book": "Bergtopboek",
"text": "Tekst", "text": "Tekst",
"trail": "{n, plural, =1 {wandelroute} other {wandelroutes}}", "trail": "{n, plural, =1 {wandelroute} other {wandelroutes}}",

View File

@@ -40,6 +40,7 @@
"distance": "Dystans", "distance": "Dystans",
"documentation": "Dokumentacja", "documentation": "Dokumentacja",
"download-gpx": "Popierz GPX", "download-gpx": "Popierz GPX",
"draw-on-map": "",
"dutch": "Niderlandzki", "dutch": "Niderlandzki",
"easy": "Łatwy", "easy": "Łatwy",
"edit": "Edytuj", "edit": "Edytuj",
@@ -128,6 +129,7 @@
"show-on-map": "Pokaż na mapie", "show-on-map": "Pokaż na mapie",
"slogan": "Zapisz swoją wyprawę!", "slogan": "Zapisz swoją wyprawę!",
"sort": "Sortowanie", "sort": "Sortowanie",
"stop-drawing": "",
"summit-book": "Logbook", "summit-book": "Logbook",
"text": "Tekst", "text": "Tekst",
"trail": "{n, plural, =1 {Szlak} other {Szlaki}}", "trail": "{n, plural, =1 {Szlak} other {Szlaki}}",

View File

@@ -40,6 +40,7 @@
"distance": "Distância", "distance": "Distância",
"documentation": "Documentação", "documentation": "Documentação",
"download-gpx": "Baixar GPX", "download-gpx": "Baixar GPX",
"draw-on-map": "",
"dutch": "Holandês", "dutch": "Holandês",
"easy": "Fácil", "easy": "Fácil",
"edit": "Editar", "edit": "Editar",
@@ -128,6 +129,7 @@
"show-on-map": "Mostrar no mapa", "show-on-map": "Mostrar no mapa",
"slogan": "Guarde as suas aventuras!", "slogan": "Guarde as suas aventuras!",
"sort": "Ordenar", "sort": "Ordenar",
"stop-drawing": "",
"summit-book": "Livro da cimeira", "summit-book": "Livro da cimeira",
"text": "Texto", "text": "Texto",
"trail": "{n, plural, =1 {Percurso} other {Percursos}}", "trail": "{n, plural, =1 {Percurso} other {Percursos}}",

View File

@@ -40,6 +40,7 @@
"distance": "距离", "distance": "距离",
"documentation": "文档", "documentation": "文档",
"download-gpx": "下载 GPX", "download-gpx": "下载 GPX",
"draw-on-map": "",
"dutch": "荷兰语", "dutch": "荷兰语",
"easy": "简单", "easy": "简单",
"edit": "编辑", "edit": "编辑",
@@ -128,6 +129,7 @@
"show-on-map": "地图中展示", "show-on-map": "地图中展示",
"slogan": "保存你的冒险!", "slogan": "保存你的冒险!",
"sort": "排序", "sort": "排序",
"stop-drawing": "",
"summit-book": "详细日程", "summit-book": "详细日程",
"text": "文本", "text": "文本",
"trail": "{n, plural, =1 {路线} other {路线}}", "trail": "{n, plural, =1 {路线} other {路线}}",

View File

@@ -0,0 +1,31 @@
interface ValhallaResponse {
trip: {
legs: {
shape: string[];
}[];
};
}
class ValhallaRoute {
type: "Feature";
properties: {};
geometry: {
type: "LineString";
coordinates: number[][];
};
constructor() {
this.type = "Feature"
this.properties = {}
this.geometry = {
type: "LineString",
coordinates: []
}
}
toString() {
return JSON.stringify(this);
}
}
export {type ValhallaResponse, ValhallaRoute}

View File

@@ -0,0 +1,79 @@
import { ValhallaRoute, type ValhallaResponse } from "$lib/models/valhalla";
import { ClientResponseError } from "pocketbase";
export const route: ValhallaRoute = new ValhallaRoute();
export function clearRoute() {
route.geometry.coordinates = [];
}
export async function calculateRouteBetween(startLat: number, startLon: number, endLat: number, endLon: number) {
const requestBody = {
"directions_type": "none",
"locations": [{ "lat": startLat, "lon": startLon }, { "lat": endLat, "lon": endLon }],
"costing": "pedestrian", "costing_options": { "pedestrian": { "max_hiking_difficulty": 6, "use_ferry": 0 } }
}
const r = await fetch("/api/v1/valhalla", { method: "POST", body: JSON.stringify(requestBody) })
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
const response: ValhallaResponse = await r.json();
appendToRoute(response);
}
function appendToRoute(valhallaResponse: any) {
const routeGeometry = decodeShape(valhallaResponse.trip.legs[0].shape);
for (const point of routeGeometry) {
route.geometry.coordinates.push([point[1], point[0]])
}
}
function decodeShape(shape: string, precision: number = 6) {
var index = 0,
lat = 0,
lng = 0,
coordinates = [],
shift = 0,
result = 0,
byte = null,
latitude_change,
longitude_change,
factor = Math.pow(10, precision);
while (index < shape.length) {
byte = null;
shift = 0;
result = 0;
do {
byte = shape.charCodeAt(index++) - 63;
result |= (byte & 0x1f) << shift;
shift += 5;
} while (byte >= 0x20);
latitude_change = ((result & 1) ? ~(result >> 1) : (result >> 1));
shift = result = 0;
do {
byte = shape.charCodeAt(index++) - 63;
result |= (byte & 0x1f) << shift;
shift += 5;
} while (byte >= 0x20);
longitude_change = ((result & 1) ? ~(result >> 1) : (result >> 1));
lat += latitude_change;
lng += longitude_change;
coordinates.push([lat / factor, lng / factor]);
}
return coordinates;
};

View File

@@ -1,5 +1,5 @@
import { error, json, type RequestEvent } from "@sveltejs/kit"; import { env } from '$env/dynamic/public';
import { env } from '$env/dynamic/public' import { error, json, type NumericRange, type RequestEvent } from "@sveltejs/kit";
export async function POST(event: RequestEvent) { export async function POST(event: RequestEvent) {
@@ -8,9 +8,13 @@ export async function POST(event: RequestEvent) {
return error(400, "PUBLIC_VALHALLA_URL not set") return error(400, "PUBLIC_VALHALLA_URL not set")
} }
try { try {
const r = await event.fetch(env.PUBLIC_VALHALLA_URL + '/route', data); const r = await event.fetch(env.PUBLIC_VALHALLA_URL + '/route', { method: "POST", body: JSON.stringify(data) });
return json(r); const response = await r.json();
if (!r.ok) {
throw error(r.status as NumericRange<400,500>, response);
}
return json(response);
} catch (e: any) { } catch (e: any) {
throw error(e.status, e) throw error(e.status || 500, e)
} }
} }

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { PUBLIC_VALHALLA_URL } from "$env/static/public";
import Button from "$lib/components/base/button.svelte"; import Button from "$lib/components/base/button.svelte";
import Datepicker from "$lib/components/base/datepicker.svelte"; import Datepicker from "$lib/components/base/datepicker.svelte";
import Select from "$lib/components/base/select.svelte"; import Select from "$lib/components/base/select.svelte";
@@ -30,6 +31,11 @@
trails_create, trails_create,
trails_update, trails_update,
} from "$lib/stores/trail_store"; } from "$lib/stores/trail_store";
import {
calculateRouteBetween,
clearRoute,
route,
} from "$lib/stores/valhalla_store";
import { waypoint } from "$lib/stores/waypoint_store"; import { waypoint } from "$lib/stores/waypoint_store";
import { getFileURL } from "$lib/util/file_util"; import { getFileURL } from "$lib/util/file_util";
import { import {
@@ -41,7 +47,7 @@
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util"; import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
import { createForm } from "$lib/vendor/svelte-form-lib"; import { createForm } from "$lib/vendor/svelte-form-lib";
import cryptoRandomString from "crypto-random-string"; import cryptoRandomString from "crypto-random-string";
import type { Map } from "leaflet"; import type { LatLng, LeafletMouseEvent, Map } from "leaflet";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { array, number, object, string } from "yup"; import { array, number, object, string } from "yup";
@@ -63,6 +69,8 @@
let gpxFile: File | Blob | null = null; let gpxFile: File | Blob | null = null;
let drawingActive = false;
const trailSchema = object<Trail>({ const trailSchema = object<Trail>({
id: string().optional(), id: string().optional(),
name: string().required($_("required")), name: string().required($_("required")),
@@ -327,6 +335,48 @@
}); });
} }
} }
function startDrawing() {
$form.expand.gpx_data = "";
clearRoute();
drawingActive = true;
}
function stopDrawing() {
drawingActive = false;
}
async function handleMapClick(e: LeafletMouseEvent) {
if (!drawingActive) {
return;
}
const waypointCount = $form.expand.waypoints.length;
const wp = new Waypoint(e.latlng.lat, e.latlng.lng, {
name: `${$_("waypoints", { values: { n: 1 } })} ${waypointCount + 1}`,
icon: waypointCount ? undefined : "circle-half-stroke",
});
saveWaypoint(wp);
if (waypointCount >= 1) {
const previousWaypoint = $form.expand.waypoints[waypointCount - 1];
try {
await calculateRouteBetween(
previousWaypoint.lat,
previousWaypoint.lon,
e.latlng.lat,
e.latlng.lng,
);
$form.expand.gpx_data = route.toString();
} catch (e) {
show_toast({
text: "Error calculating route",
icon: "close",
type: "error",
});
}
}
}
</script> </script>
<svelte:head> <svelte:head>
@@ -342,9 +392,21 @@
on:submit={handleSubmit} on:submit={handleSubmit}
> >
<h3 class="text-xl font-semibold">{$_("pick-a-trail")}</h3> <h3 class="text-xl font-semibold">{$_("pick-a-trail")}</h3>
<button class="btn-primary" type="button" on:click={openFileBrowser} <Button
>{$_("upload-file")}</button primary={true}
type="button"
disabled={drawingActive}
on:click={openFileBrowser}>{$_("upload-file")}</Button
> >
{#if PUBLIC_VALHALLA_URL}
<button
class="btn-primary"
type="button"
on:click={drawingActive ? stopDrawing : startDrawing}
>
{drawingActive ? $_("stop-drawing") : $_("draw-on-map")}</button
>
{/if}
<input <input
type="file" type="file"
name="gpx" name="gpx"
@@ -459,7 +521,9 @@
<Toggle name="public" label={$_("public")} bind:value={$form.public} <Toggle name="public" label={$_("public")} bind:value={$form.public}
></Toggle> ></Toggle>
<hr class="border-separator" /> <hr class="border-separator" />
<h3 class="text-xl font-semibold">{$_("waypoints")}</h3> <h3 class="text-xl font-semibold">
{$_("waypoints", { values: { n: 2 } })}
</h3>
<ul> <ul>
{#each $form.expand.waypoints ?? [] as waypoint, i} {#each $form.expand.waypoints ?? [] as waypoint, i}
<li on:mouseenter={() => openMarkerPopup(waypoint)}> <li on:mouseenter={() => openMarkerPopup(waypoint)}>
@@ -549,7 +613,13 @@
{loading}>{$_("save-trail")}</Button {loading}>{$_("save-trail")}</Button
> >
</form> </form>
<MapWithElevation trail={$form} bind:map></MapWithElevation> <MapWithElevation
trail={$form}
crosshair={drawingActive}
options={{ hotline: false }}
bind:map
on:click={(e) => handleMapClick(e.detail)}
></MapWithElevation>
</main> </main>
<WaypointModal <WaypointModal
bind:openModal={openWaypointModal} bind:openModal={openWaypointModal}
@@ -563,11 +633,11 @@
></ListSelectModal> ></ListSelectModal>
<style> <style>
#map { :global(#map) {
height: calc(400px); height: calc(400px);
} }
@media only screen and (min-width: 768px) { @media only screen and (min-width: 768px) {
#map, :global(#map),
form { form {
height: calc(100vh - 124px); height: calc(100vh - 124px);
} }