adds modes of transportation to routing

This commit is contained in:
Christian Beutel
2024-05-02 14:23:32 +02:00
parent b3ed2b1098
commit 2a771b195e
7 changed files with 183 additions and 83 deletions

View File

@@ -56,7 +56,7 @@ services:
UPLOAD_FOLDER: /app/uploads UPLOAD_FOLDER: /app/uploads
UPLOAD_USER: UPLOAD_USER:
UPLOAD_PASSWORD: UPLOAD_PASSWORD:
PUBLIC_VALHALLA_URL: http://localhost:8002 PUBLIC_VALHALLA_URL: https://valhalla1.openstreetmap.de
volumes: volumes:
- ./data/uploads:/app/uploads - ./data/uploads:/app/uploads
ports: ports:

View File

@@ -12,13 +12,13 @@
export let value: any; export let value: any;
export let items: SelectItem[] = []; export let items: SelectItem[] = [];
export let label: string = ""; export let label: string = "";
export let disabled: boolean = false;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
function onChange(target: any) { function onChange(target: any) {
dispatch("change", target?.value) dispatch("change", target?.value);
} }
</script> </script>
<div> <div>
@@ -30,6 +30,8 @@
<select <select
{name} {name}
class="bg-input-background h-10 w-full px-4 border-r-8 border-transparent outline outline-1 outline-input-border rounded-md focus:outline-input-border-focus transition-colors" class="bg-input-background h-10 w-full px-4 border-r-8 border-transparent outline outline-1 outline-input-border rounded-md focus:outline-input-border-focus transition-colors"
class:text-gray-500={disabled}
{disabled}
bind:value bind:value
on:change={(e) => onChange(e.target)} on:change={(e) => onChange(e.target)}
> >

View File

@@ -139,7 +139,7 @@
}); });
</script> </script>
<div id="map-container" class="flex flex-col"> <div id="map-container" class="flex flex-col h-full">
<div <div
id="map" id="map"
class="rounded-xl z-0 basis-full" class="rounded-xl z-0 basis-full"

View File

@@ -3,6 +3,7 @@ import type Track from "$lib/models/gpx/track";
import TrackSegment from "$lib/models/gpx/track-segment"; import TrackSegment from "$lib/models/gpx/track-segment";
import Waypoint from "$lib/models/gpx/waypoint"; import Waypoint from "$lib/models/gpx/waypoint";
import { type ValhallaAnchor, type ValhallaHeightResponse, type ValhallaRouteResponse } from "$lib/models/valhalla"; import { type ValhallaAnchor, type ValhallaHeightResponse, type ValhallaRouteResponse } from "$lib/models/valhalla";
import { decodePolyline, encodePolyline } from "$lib/util/polyline_util";
import { ClientResponseError } from "pocketbase"; import { ClientResponseError } from "pocketbase";
@@ -19,26 +20,45 @@ export function setRoute(newRoute: GPX) {
route = newRoute route = newRoute
} }
export async function calculateRouteBetween(startLat: number, startLon: number, endLat: number, endLon: number) { export async function calculateRouteBetween(startLat: number, startLon: number, endLat: number, endLon: number, costing: string = "pedestrian", autoRoute: boolean = true) {
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 } }
}
let r = await fetch("/api/v1/valhalla/route", { method: "POST", body: JSON.stringify(requestBody) })
if (!r.ok) { let shape;
throw new ClientResponseError(await r.json()) if (autoRoute) {
} let costingBody;
const routeResponse: ValhallaRouteResponse = await r.json(); switch (costing) {
const shape = routeResponse.trip.legs[0].shape case "bicycle":
r = await fetch("/api/v1/valhalla/height", { method: "POST", body: JSON.stringify({ encoded_polyline: shape }) }) costingBody = { "costing": "bicycle", "costing_options": { "bicycle": { "bicycle_type": "Hybrid", "use_roads": 0.5, "use_hills": 0.5, "avoid_bad_surfaces": 0.5, "use_ferry": 0 } } }
break;
case "auto":
costingBody = { "costing": "auto", "costing_options": { "auto": { "use_ferry": 0 } } }
default:
costingBody = { "costing": costing, "costing_options": { costing: { "max_hiking_difficulty": 6, "use_ferry": 0 } } }
break;
}
const requestBody = {
"directions_type": "none",
"locations": [{ "lat": startLat, "lon": startLon }, { "lat": endLat, "lon": endLon }],
...costingBody
}
if (!r.ok) { let r = await fetch("/api/v1/valhalla/route", { method: "POST", body: JSON.stringify(requestBody) })
throw new ClientResponseError(await r.json())
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
const routeResponse: ValhallaRouteResponse = await r.json();
shape = routeResponse.trip.legs[0].shape
} else {
shape = encodePolyline([[startLat, startLon], [endLat, endLon]])
} }
const heightResponse: ValhallaHeightResponse = await r.json()
const points = decodeShape(shape); const r2 = await fetch("/api/v1/valhalla/height", { method: "POST", body: JSON.stringify({ encoded_polyline: shape }) })
if (!r2.ok) {
throw new ClientResponseError(await r2.json())
}
const heightResponse: ValhallaHeightResponse = await r2.json()
const points = decodePolyline(shape);
const waypoints = points.map((p, i) => new Waypoint({ $: { lat: p[0], lon: p[1] }, ele: heightResponse.height[i] })) const waypoints = points.map((p, i) => new Waypoint({ $: { lat: p[0], lon: p[1] }, ele: heightResponse.height[i] }))
return waypoints return waypoints
@@ -63,48 +83,3 @@ export async function editRoute(index: number, waypoints: Waypoint[]) {
export function deleteFromRoute(index: number) { export function deleteFromRoute(index: number) {
route.trk?.at(0)?.trkseg?.splice(index, 1); route.trk?.at(0)?.trkseg?.splice(index, 1);
} }
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

@@ -0,0 +1,80 @@
function py2_round(value: number) {
return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1);
}
function encode(current: number, previous: number, factor: number) {
current = py2_round(current * factor);
previous = py2_round(previous * factor);
var coordinate = (current - previous) * 2;
if (coordinate < 0) {
coordinate = -coordinate - 1
}
var output = '';
while (coordinate >= 0x20) {
output += String.fromCharCode((0x20 | (coordinate & 0x1f)) + 63);
coordinate /= 32;
}
output += String.fromCharCode((coordinate | 0) + 63);
return output;
}
export function decodePolyline(str: 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 < str.length) {
byte = null;
shift = 0;
result = 0;
do {
byte = str.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 = str.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;
};
export function encodePolyline(coordinates: number[][], precision: number = 6) {
if (!coordinates.length) { return ''; }
var factor = Math.pow(10, precision),
output = encode(coordinates[0][0], 0, factor) + encode(coordinates[0][1], 0, factor);
for (var i = 1; i < coordinates.length; i++) {
var a = coordinates[i], b = coordinates[i - 1];
output += encode(a[0], b[0], factor);
output += encode(a[1], b[1], factor);
}
return output;
};

View File

@@ -60,6 +60,8 @@
import type { DivIcon, LatLng, LeafletMouseEvent, Map } from "leaflet"; import type { DivIcon, LatLng, LeafletMouseEvent, Map } from "leaflet";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { backInOut } from "svelte/easing";
import { fade, scale } from "svelte/transition";
import { array, number, object, string } from "yup"; import { array, number, object, string } from "yup";
export let data: { trail: Trail }; export let data: { trail: Trail };
@@ -98,6 +100,15 @@
description: string().optional(), description: string().optional(),
}); });
const modesOfTransport = [
{ text: "Hiking", value: "pedestrian" },
{ text: "Cycling", value: "bicycle" },
{ text: "Driving", value: "auto" },
];
let selectedModeOfTransport = modesOfTransport[0].value;
let autoRouting = true;
const { form, errors, handleChange, handleSubmit } = createForm<Trail>({ const { form, errors, handleChange, handleSubmit } = createForm<Trail>({
initialValues: data.trail, initialValues: data.trail,
validationSchema: trailSchema, validationSchema: trailSchema,
@@ -141,6 +152,20 @@
}); });
} }
if (
(!$form.lat || !$form.lon) &&
route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)
) {
$form.lat = route.trk
?.at(0)
?.trkseg?.at(0)
?.trkpt?.at(0)?.$.lat;
$form.lon = route.trk
?.at(0)
?.trkseg?.at(0)
?.trkpt?.at(0)?.$.lon;
}
if (!submittedTrail.id) { if (!submittedTrail.id) {
const createdTrail = await trails_create( const createdTrail = await trails_create(
submittedTrail, submittedTrail,
@@ -180,8 +205,6 @@
L = (await import("leaflet")).default; L = (await import("leaflet")).default;
await import("leaflet.awesome-markers"); await import("leaflet.awesome-markers");
// $form.lat = 47.74604748696788;
// $form.lon = 11.588988304138185;
clearAnchorMarker(); clearAnchorMarker();
clearRoute(); clearRoute();
@@ -446,6 +469,8 @@
previousAnchor.lon, previousAnchor.lon,
e.latlng.lat, e.latlng.lat,
e.latlng.lng, e.latlng.lng,
selectedModeOfTransport,
autoRouting,
); );
appendToRoute(routeWaypoints); appendToRoute(routeWaypoints);
addAnchor(e.latlng.lat, e.latlng.lng); addAnchor(e.latlng.lat, e.latlng.lng);
@@ -537,6 +562,8 @@
anchor.lon, anchor.lon,
nextAnchor.lat, nextAnchor.lat,
nextAnchor.lon, nextAnchor.lon,
selectedModeOfTransport,
autoRouting,
); );
} }
if (anchorIndex > 0) { if (anchorIndex > 0) {
@@ -546,6 +573,8 @@
previousAnchor.lon, previousAnchor.lon,
anchor.lat, anchor.lat,
anchor.lon, anchor.lon,
selectedModeOfTransport,
autoRouting,
); );
} }
if (nextRouteSegment) { if (nextRouteSegment) {
@@ -808,19 +837,33 @@
{loading}>{$_("save-trail")}</Button {loading}>{$_("save-trail")}</Button
> >
</form> </form>
<MapWithElevation <div class="relative">
trail={$form} {#if drawingActive}
crosshair={drawingActive} <div
options={{ class="absolute top-0 left-16 z-50 p-4 my-2 rounded-xl bg-background space-y-4"
autofitBounds: !drawingActive, in:scale={{ easing: backInOut }}
mapTooltip: !drawingActive, out:scale={{ easing: backInOut }}
speed: !drawingActive, >
speedFactor: drawingActive ? 0 : 1, <Toggle bind:value={autoRouting} label="Enable auto-routing"
showStartEnd: !drawingActive, ></Toggle>
}} <Select items={modesOfTransport} bind:value={selectedModeOfTransport} disabled={!autoRouting}
bind:map ></Select>
on:click={(e) => handleMapClick(e.detail)} </div>
></MapWithElevation> {/if}
<MapWithElevation
trail={$form}
crosshair={drawingActive}
options={{
autofitBounds: !drawingActive,
mapTooltip: !drawingActive,
speed: !drawingActive,
speedFactor: drawingActive ? 0 : 1,
showStartEnd: !drawingActive,
}}
bind:map
on:click={(e) => handleMapClick(e.detail)}
></MapWithElevation>
</div>
</main> </main>
<WaypointModal <WaypointModal
bind:openModal={openWaypointModal} bind:openModal={openWaypointModal}