adds modes of transportation to routing
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -62,49 +82,4 @@ 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;
|
|
||||||
};
|
|
||||||
@@ -29,7 +29,7 @@ export async function gpx2trail(gpxString: string) {
|
|||||||
const totals = gpx.getTotals()
|
const totals = gpx.getTotals()
|
||||||
|
|
||||||
const points = gpx.trk?.at(0)?.trkseg?.at(0)?.trkpt
|
const points = gpx.trk?.at(0)?.trkseg?.at(0)?.trkpt
|
||||||
const startPoint = points?.at(0);
|
const startPoint = points?.at(0);
|
||||||
if (startPoint) {
|
if (startPoint) {
|
||||||
trail.lat = startPoint.$.lat
|
trail.lat = startPoint.$.lat
|
||||||
trail.lon = startPoint.$.lon
|
trail.lon = startPoint.$.lon
|
||||||
|
|||||||
80
web/src/lib/util/polyline_util.ts
Normal file
80
web/src/lib/util/polyline_util.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user