adds routing engine
This commit is contained in:
@@ -71,7 +71,7 @@ services:
|
||||
volumes:
|
||||
- ./data/valhalla:/custom_files
|
||||
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
|
||||
- force_rebuild=False
|
||||
- force_rebuild_elevation=False
|
||||
|
||||
@@ -14,13 +14,14 @@
|
||||
export let map: Map | null = null;
|
||||
export let options = {};
|
||||
export let graticule: AutoGraticule | null = null;
|
||||
export let crosshair: boolean = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let L: any;
|
||||
let controlElevation: any;
|
||||
|
||||
$: if (trail?.expand.gpx_data && controlElevation) {
|
||||
$: if (trail?.expand.gpx_data !== undefined && controlElevation) {
|
||||
controlElevation.clear();
|
||||
controlElevation.load(trail.expand.gpx_data);
|
||||
}
|
||||
@@ -31,7 +32,9 @@
|
||||
await import("leaflet.awesome-markers");
|
||||
//@ts-ignore
|
||||
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(
|
||||
[trail?.lat ?? 0, trail?.lon ?? 0],
|
||||
@@ -43,6 +46,10 @@
|
||||
dispatch("zoomend", map);
|
||||
});
|
||||
|
||||
map!.on("click", function (e) {
|
||||
dispatch("click", e);
|
||||
});
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
}).addTo(map);
|
||||
@@ -99,7 +106,7 @@
|
||||
iconColor: "white",
|
||||
}),
|
||||
},
|
||||
graticule: false
|
||||
graticule: false,
|
||||
};
|
||||
|
||||
const elevation_options = Object.assign(
|
||||
@@ -115,16 +122,21 @@
|
||||
markers.push(marker);
|
||||
}
|
||||
|
||||
if(elevation_options.graticule) {
|
||||
if (elevation_options.graticule) {
|
||||
graticule = new AutoGraticule();
|
||||
graticule.addTo(map!);
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<slot />
|
||||
<div class="basis-[300px] flex-grow flex-shrink-0" id="elevation"></div>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
const tabs = [
|
||||
$_("description"),
|
||||
$_("waypoints"),
|
||||
$_("waypoints", { values: { n: 2 } }),
|
||||
$_("photos"),
|
||||
$_("summit-book"),
|
||||
...($currentUser ? [$_("comment", { values: { n: 2 } })] : []),
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"distance": "Distanz",
|
||||
"documentation": "Dokumentation",
|
||||
"download-gpx": "GPX herunterladen",
|
||||
"draw-on-map": "",
|
||||
"dutch": "Niederländisch",
|
||||
"easy": "Einfach",
|
||||
"edit": "Bearbeiten",
|
||||
@@ -128,6 +129,7 @@
|
||||
"show-on-map": "Auf der Karte anzeigen",
|
||||
"slogan": "Speichere deine Abenteuer!",
|
||||
"sort": "Sortieren",
|
||||
"stop-drawing": "",
|
||||
"summit-book": "Gipfelbuch",
|
||||
"text": "Text",
|
||||
"trail": "{n, plural, =1 {Route} other {Routen}}",
|
||||
@@ -136,7 +138,7 @@
|
||||
"upload-file": "Datei hochladen",
|
||||
"upload-gpx": "GPX hochladen",
|
||||
"username": "Nutzername",
|
||||
"waypoints": "Wegpunkte",
|
||||
"waypoints": "{n, plural, =1 {Wegpunkt} other {Wegpunkte}}",
|
||||
"welcome_to": "Willkommen bei",
|
||||
"wrong-username-or-password": "Falscher Nutzername oder falsches Passwort"
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"distance": "Distance",
|
||||
"documentation": "Documentation",
|
||||
"download-gpx": "Download GPX",
|
||||
"draw-on-map": "Draw on map",
|
||||
"dutch": "Dutch",
|
||||
"easy": "Easy",
|
||||
"edit": "Edit",
|
||||
@@ -128,6 +129,7 @@
|
||||
"show-on-map": "Show on map",
|
||||
"slogan": "Save your adventures!",
|
||||
"sort": "Sort",
|
||||
"stop-drawing": "Stop drawing",
|
||||
"summit-book": "Summit Book",
|
||||
"text": "Text",
|
||||
"trail": "{n, plural, =1 {Trail} other {Trails}}",
|
||||
@@ -136,7 +138,7 @@
|
||||
"upload-file": "Upload file",
|
||||
"upload-gpx": "Upload GPX",
|
||||
"username": "Username",
|
||||
"waypoints": "Waypoints",
|
||||
"waypoints": "{n, plural, =1 {Waypoint} other {Waypoints}}",
|
||||
"welcome_to": "Welcome to",
|
||||
"wrong-username-or-password": "Wrong username or password"
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"distance": "Distance",
|
||||
"documentation": "Documentation",
|
||||
"download-gpx": "Télécharger le GPX",
|
||||
"draw-on-map": "",
|
||||
"dutch": "Néerlandais",
|
||||
"easy": "Facile",
|
||||
"edit": "Editer",
|
||||
@@ -128,6 +129,7 @@
|
||||
"show-on-map": "Voir sur la carte",
|
||||
"slogan": "Sauvegarder vos aventures !",
|
||||
"sort": "Trier",
|
||||
"stop-drawing": "",
|
||||
"summit-book": "Livre du sommet",
|
||||
"text": "Texte",
|
||||
"trail": "{n, plural, =1 {Itinéraire} other {Itinéraires}}",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"distance": "Távolság",
|
||||
"documentation": "Dokumentáció",
|
||||
"download-gpx": "GPX letöltése",
|
||||
"draw-on-map": "",
|
||||
"dutch": "Holland",
|
||||
"easy": "Könnyű",
|
||||
"edit": "Szerkesztés",
|
||||
@@ -128,6 +129,7 @@
|
||||
"show-on-map": "Mutatás térképen",
|
||||
"slogan": "Mentse el a kalandjait!",
|
||||
"sort": "Rendezés",
|
||||
"stop-drawing": "",
|
||||
"summit-book": "Csúcspont könyv",
|
||||
"text": "Szöveg",
|
||||
"trail": "{n, plural, =1 {Útvonal} other {Útvonalak}}",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"distance": "Afstand",
|
||||
"documentation": "Documentatie",
|
||||
"download-gpx": "GPX-bestand downloaden",
|
||||
"draw-on-map": "",
|
||||
"dutch": "Nederlands",
|
||||
"easy": "Makkelijk",
|
||||
"edit": "Bewerken",
|
||||
@@ -128,6 +129,7 @@
|
||||
"show-on-map": "Tonen op kaart",
|
||||
"slogan": "Bewaar je avonturen!",
|
||||
"sort": "Sorteren",
|
||||
"stop-drawing": "",
|
||||
"summit-book": "Bergtopboek",
|
||||
"text": "Tekst",
|
||||
"trail": "{n, plural, =1 {wandelroute} other {wandelroutes}}",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"distance": "Dystans",
|
||||
"documentation": "Dokumentacja",
|
||||
"download-gpx": "Popierz GPX",
|
||||
"draw-on-map": "",
|
||||
"dutch": "Niderlandzki",
|
||||
"easy": "Łatwy",
|
||||
"edit": "Edytuj",
|
||||
@@ -128,6 +129,7 @@
|
||||
"show-on-map": "Pokaż na mapie",
|
||||
"slogan": "Zapisz swoją wyprawę!",
|
||||
"sort": "Sortowanie",
|
||||
"stop-drawing": "",
|
||||
"summit-book": "Logbook",
|
||||
"text": "Tekst",
|
||||
"trail": "{n, plural, =1 {Szlak} other {Szlaki}}",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"distance": "Distância",
|
||||
"documentation": "Documentação",
|
||||
"download-gpx": "Baixar GPX",
|
||||
"draw-on-map": "",
|
||||
"dutch": "Holandês",
|
||||
"easy": "Fácil",
|
||||
"edit": "Editar",
|
||||
@@ -128,6 +129,7 @@
|
||||
"show-on-map": "Mostrar no mapa",
|
||||
"slogan": "Guarde as suas aventuras!",
|
||||
"sort": "Ordenar",
|
||||
"stop-drawing": "",
|
||||
"summit-book": "Livro da cimeira",
|
||||
"text": "Texto",
|
||||
"trail": "{n, plural, =1 {Percurso} other {Percursos}}",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"distance": "距离",
|
||||
"documentation": "文档",
|
||||
"download-gpx": "下载 GPX",
|
||||
"draw-on-map": "",
|
||||
"dutch": "荷兰语",
|
||||
"easy": "简单",
|
||||
"edit": "编辑",
|
||||
@@ -128,6 +129,7 @@
|
||||
"show-on-map": "地图中展示",
|
||||
"slogan": "保存你的冒险!",
|
||||
"sort": "排序",
|
||||
"stop-drawing": "",
|
||||
"summit-book": "详细日程",
|
||||
"text": "文本",
|
||||
"trail": "{n, plural, =1 {路线} other {路线}}",
|
||||
|
||||
31
web/src/lib/models/valhalla.ts
Normal file
31
web/src/lib/models/valhalla.ts
Normal 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}
|
||||
79
web/src/lib/stores/valhalla_store.ts
Normal file
79
web/src/lib/stores/valhalla_store.ts
Normal 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;
|
||||
};
|
||||
@@ -1,16 +1,20 @@
|
||||
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) {
|
||||
const data = await event.request.json()
|
||||
if(!env.PUBLIC_VALHALLA_URL) {
|
||||
if (!env.PUBLIC_VALHALLA_URL) {
|
||||
return error(400, "PUBLIC_VALHALLA_URL not set")
|
||||
}
|
||||
try {
|
||||
const r = await event.fetch(env.PUBLIC_VALHALLA_URL + '/route', data);
|
||||
return json(r);
|
||||
const r = await event.fetch(env.PUBLIC_VALHALLA_URL + '/route', { method: "POST", body: JSON.stringify(data) });
|
||||
const response = await r.json();
|
||||
if (!r.ok) {
|
||||
throw error(r.status as NumericRange<400,500>, response);
|
||||
}
|
||||
return json(response);
|
||||
} catch (e: any) {
|
||||
throw error(e.status, e)
|
||||
throw error(e.status || 500, e)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { PUBLIC_VALHALLA_URL } from "$env/static/public";
|
||||
import Button from "$lib/components/base/button.svelte";
|
||||
import Datepicker from "$lib/components/base/datepicker.svelte";
|
||||
import Select from "$lib/components/base/select.svelte";
|
||||
@@ -30,6 +31,11 @@
|
||||
trails_create,
|
||||
trails_update,
|
||||
} from "$lib/stores/trail_store";
|
||||
import {
|
||||
calculateRouteBetween,
|
||||
clearRoute,
|
||||
route,
|
||||
} from "$lib/stores/valhalla_store";
|
||||
import { waypoint } from "$lib/stores/waypoint_store";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import {
|
||||
@@ -41,7 +47,7 @@
|
||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||
import cryptoRandomString from "crypto-random-string";
|
||||
import type { Map } from "leaflet";
|
||||
import type { LatLng, LeafletMouseEvent, Map } from "leaflet";
|
||||
import { onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { array, number, object, string } from "yup";
|
||||
@@ -63,6 +69,8 @@
|
||||
|
||||
let gpxFile: File | Blob | null = null;
|
||||
|
||||
let drawingActive = false;
|
||||
|
||||
const trailSchema = object<Trail>({
|
||||
id: string().optional(),
|
||||
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>
|
||||
|
||||
<svelte:head>
|
||||
@@ -342,9 +392,21 @@
|
||||
on:submit={handleSubmit}
|
||||
>
|
||||
<h3 class="text-xl font-semibold">{$_("pick-a-trail")}</h3>
|
||||
<button class="btn-primary" type="button" on:click={openFileBrowser}
|
||||
>{$_("upload-file")}</button
|
||||
<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
|
||||
type="file"
|
||||
name="gpx"
|
||||
@@ -459,7 +521,9 @@
|
||||
<Toggle name="public" label={$_("public")} bind:value={$form.public}
|
||||
></Toggle>
|
||||
<hr class="border-separator" />
|
||||
<h3 class="text-xl font-semibold">{$_("waypoints")}</h3>
|
||||
<h3 class="text-xl font-semibold">
|
||||
{$_("waypoints", { values: { n: 2 } })}
|
||||
</h3>
|
||||
<ul>
|
||||
{#each $form.expand.waypoints ?? [] as waypoint, i}
|
||||
<li on:mouseenter={() => openMarkerPopup(waypoint)}>
|
||||
@@ -549,7 +613,13 @@
|
||||
{loading}>{$_("save-trail")}</Button
|
||||
>
|
||||
</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>
|
||||
<WaypointModal
|
||||
bind:openModal={openWaypointModal}
|
||||
@@ -563,11 +633,11 @@
|
||||
></ListSelectModal>
|
||||
|
||||
<style>
|
||||
#map {
|
||||
:global(#map) {
|
||||
height: calc(400px);
|
||||
}
|
||||
@media only screen and (min-width: 768px) {
|
||||
#map,
|
||||
:global(#map),
|
||||
form {
|
||||
height: calc(100vh - 124px);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user