adds height to route drawing

This commit is contained in:
Christian Beutel
2024-04-23 01:39:20 +02:00
parent 8d7ed16904
commit 8a3894106f
18 changed files with 176 additions and 96 deletions

View File

@@ -40,7 +40,7 @@
"distance": "Distanz", "distance": "Distanz",
"documentation": "Dokumentation", "documentation": "Dokumentation",
"download-gpx": "GPX herunterladen", "download-gpx": "GPX herunterladen",
"draw-on-map": "", "draw-a-route": "Route zeichnen",
"dutch": "Niederländisch", "dutch": "Niederländisch",
"easy": "Einfach", "easy": "Einfach",
"edit": "Bearbeiten", "edit": "Bearbeiten",
@@ -129,7 +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": "", "stop-drawing": "Zeichnen beenden",
"summit-book": "Gipfelbuch", "summit-book": "Gipfelbuch",
"text": "Text", "text": "Text",
"trail": "{n, plural, =1 {Route} other {Routen}}", "trail": "{n, plural, =1 {Route} other {Routen}}",

View File

@@ -40,7 +40,7 @@
"distance": "Distance", "distance": "Distance",
"documentation": "Documentation", "documentation": "Documentation",
"download-gpx": "Download GPX", "download-gpx": "Download GPX",
"draw-on-map": "Draw on map", "draw-a-route": "Draw a route",
"dutch": "Dutch", "dutch": "Dutch",
"easy": "Easy", "easy": "Easy",
"edit": "Edit", "edit": "Edit",

View File

@@ -40,7 +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": "", "draw-a-route": "",
"dutch": "Néerlandais", "dutch": "Néerlandais",
"easy": "Facile", "easy": "Facile",
"edit": "Editer", "edit": "Editer",

View File

@@ -40,7 +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": "", "draw-a-route": "",
"dutch": "Holland", "dutch": "Holland",
"easy": "Könnyű", "easy": "Könnyű",
"edit": "Szerkesztés", "edit": "Szerkesztés",

View File

@@ -40,7 +40,7 @@
"distance": "Afstand", "distance": "Afstand",
"documentation": "Documentatie", "documentation": "Documentatie",
"download-gpx": "GPX-bestand downloaden", "download-gpx": "GPX-bestand downloaden",
"draw-on-map": "", "draw-a-route": "",
"dutch": "Nederlands", "dutch": "Nederlands",
"easy": "Makkelijk", "easy": "Makkelijk",
"edit": "Bewerken", "edit": "Bewerken",

View File

@@ -40,7 +40,7 @@
"distance": "Dystans", "distance": "Dystans",
"documentation": "Dokumentacja", "documentation": "Dokumentacja",
"download-gpx": "Popierz GPX", "download-gpx": "Popierz GPX",
"draw-on-map": "", "draw-a-route": "",
"dutch": "Niderlandzki", "dutch": "Niderlandzki",
"easy": "Łatwy", "easy": "Łatwy",
"edit": "Edytuj", "edit": "Edytuj",

View File

@@ -40,7 +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": "", "draw-a-route": "",
"dutch": "Holandês", "dutch": "Holandês",
"easy": "Fácil", "easy": "Fácil",
"edit": "Editar", "edit": "Editar",

View File

@@ -40,7 +40,7 @@
"distance": "距离", "distance": "距离",
"documentation": "文档", "documentation": "文档",
"download-gpx": "下载 GPX", "download-gpx": "下载 GPX",
"draw-on-map": "", "draw-a-route": "",
"dutch": "荷兰语", "dutch": "荷兰语",
"easy": "简单", "easy": "简单",
"edit": "编辑", "edit": "编辑",

View File

@@ -1,9 +1,9 @@
import * as xml2js from 'isomorphic-xml2js'; import * as xml2js from 'isomorphic-xml2js';
import Metadata from './metadata'; import Metadata from './metadata';
import Waypoint from './waypoint';
import Route from './route'; import Route from './route';
import Track from './track'; import Track from './track';
import { removeEmpty, allDatesToISOString } from './utils'; import { allDatesToISOString, calculateDistance, removeEmpty } from './utils';
import Waypoint from './waypoint';
const defaultAttributes = { const defaultAttributes = {
version: '1.1', version: '1.1',
@@ -72,9 +72,59 @@ export default class GPX {
removeEmpty(this); removeEmpty(this);
} }
getTotals() {
let totalElevationGain = 0;
let totalDuration = 0;
let totalDistance = 0;
for (const track of this.trk ?? []) {
for (const segment of track.trkseg ?? []) {
const points = segment.trkpt ?? [];
if (points.length >= 2) {
const startTime = points[0].time;
const endTime = points[points.length - 1].time
if (startTime && endTime) {
totalDuration += endTime.getTime() - startTime.getTime();
}
}
const pointLength = points.length
for (let i = 1; i < pointLength; i++) {
const prevPoint = points[i - 1];
const point = points[i];
const elevation = point.ele ?? 0
const previousElevation = prevPoint.ele ?? 0
const elevationDiff = elevation - previousElevation;
if (elevationDiff > 0) {
totalElevationGain += elevationDiff;
}
const distance = calculateDistance(
prevPoint.$.lat ?? 0,
prevPoint.$.lon ?? 0,
point.$.lat ?? 0,
point.$.lon ?? 0,
);
totalDistance += distance;
}
}
}
return { distance: totalDistance, elevationGain: totalElevationGain, duration: totalDuration }
}
static parse(gpxString: string): Promise<GPX | Error> { static parse(gpxString: string): Promise<GPX | Error> {
return new Promise<GPX | Error>((resolve, reject) => xml2js.parseString(gpxString, { return new Promise<GPX | Error>((resolve, reject) => xml2js.parseString(gpxString, {
explicitArray: false explicitArray: false,
attrValueProcessors: [(str: string | number) => {
if (!isNaN(Number(str))) {
str = Number.isInteger(Number(str)) ? parseInt(String(str), 10) : parseFloat(String(str));
}
return str;
}
]
}, (err, xml) => { }, (err, xml) => {
if (err) { if (err) {
reject(err); reject(err);

View File

@@ -20,4 +20,17 @@ function allDatesToISOString(obj: Record<string, any>) {
}); });
} }
export { removeEmpty, allDatesToISOString }; function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371; // Radius of the Earth in km
const dLat = (lat2 - lat1) * (Math.PI / 180); // Convert degrees to radians
const dLon = (lon2 - lon1) * (Math.PI / 180);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * (Math.PI / 180))) * Math.cos((lat2 * (Math.PI / 180))) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distance = R * c * 1000; // Distance in km
return distance;
}
export { removeEmpty, allDatesToISOString, calculateDistance };

View File

@@ -1,4 +1,4 @@
interface ValhallaResponse { interface ValhallaRouteResponse {
trip: { trip: {
legs: { legs: {
shape: string; shape: string;
@@ -6,4 +6,8 @@ interface ValhallaResponse {
}; };
} }
export { type ValhallaResponse } interface ValhallaHeightResponse {
height: number[];
}
export { type ValhallaRouteResponse, type ValhallaHeightResponse }

View File

@@ -2,7 +2,7 @@ import GPX from "$lib/models/gpx/gpx";
import type Track from "$lib/models/gpx/track"; 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 ValhallaResponse } from "$lib/models/valhalla"; import { type ValhallaHeightResponse, type ValhallaRouteResponse } from "$lib/models/valhalla";
import { ClientResponseError } from "pocketbase"; import { ClientResponseError } from "pocketbase";
@@ -20,13 +20,21 @@ export async function calculateRouteBetween(startLat: number, startLon: number,
"locations": [{ "lat": startLat, "lon": startLon }, { "lat": endLat, "lon": endLon }], "locations": [{ "lat": startLat, "lon": startLon }, { "lat": endLat, "lon": endLon }],
"costing": "pedestrian", "costing_options": { "pedestrian": { "max_hiking_difficulty": 6, "use_ferry": 0 } } "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) }) let r = await fetch("/api/v1/valhalla/route", { method: "POST", body: JSON.stringify(requestBody) })
if (!r.ok) { if (!r.ok) {
throw new ClientResponseError(await r.json()) throw new ClientResponseError(await r.json())
} }
const response: ValhallaResponse = await r.json(); const routeResponse: ValhallaRouteResponse = await r.json();
const points = decodeShape(response.trip.legs[0].shape); const shape = routeResponse.trip.legs[0].shape
const waypoints = points.map((p) => new Waypoint({ $: { lat: p[0], lon: p[1] } })) r = await fetch("/api/v1/valhalla/height", { method: "POST", body: JSON.stringify({ encoded_polyline: shape }) })
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
const heightResponse: ValhallaHeightResponse = await r.json()
const points = decodeShape(shape);
const waypoints = points.map((p, i) => new Waypoint({ $: { lat: p[0], lon: p[1] }, ele: heightResponse.height[i] }))
return waypoints return waypoints
} }

View File

@@ -3,19 +3,7 @@ import { Trail } from "$lib/models/trail";
import { Waypoint } from "$lib/models/waypoint"; import { Waypoint } from "$lib/models/waypoint";
import GeoJsonToGpx from "$lib/vendor/geoJSONToGPX"; import GeoJsonToGpx from "$lib/vendor/geoJSONToGPX";
import { kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON"; import { kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON";
import cryptoRandomString from "crypto-random-string";
function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371; // Radius of the Earth in km
const dLat = (lat2 - lat1) * (Math.PI / 180); // Convert degrees to radians
const dLon = (lon2 - lon1) * (Math.PI / 180);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * (Math.PI / 180))) * Math.cos((lat2 * (Math.PI / 180))) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distance = R * c * 1000; // Distance in km
return distance;
}
export async function gpx2trail(gpxString: string) { export async function gpx2trail(gpxString: string) {
const gpx = await GPX.parse(gpxString); const gpx = await GPX.parse(gpxString);
@@ -32,63 +20,32 @@ export async function gpx2trail(gpxString: string) {
for (const wpt of gpx.wpt ?? []) { for (const wpt of gpx.wpt ?? []) {
const wp = new Waypoint(wpt.$.lat ?? 0, wpt.$.lon ?? 0); const wp = new Waypoint(wpt.$.lat ?? 0, wpt.$.lon ?? 0);
wp.name = wp.name wp.id = cryptoRandomString({ length: 15 });
wp.description = wp.description; wp.name = wpt.name
wp.description = wpt.desc;
trail.expand.waypoints.push(wp); trail.expand.waypoints.push(wp);
} }
let totalElevationGain = 0; const totals = gpx.getTotals()
let totalDuration = 0;
let totalDistance = 0;
for (const track of gpx.trk ?? []) { const points = gpx.trk?.at(0)?.trkseg?.at(0)?.trkpt
for (const segment of track.trkseg ?? []) { const startPoint = points?.at(0);
const points = segment.trkpt ?? [];
if (points.length >= 2) {
const startTime = points[0].time;
const endTime = points[points.length - 1].time
if (startTime && endTime) {
totalDuration += endTime.getTime() - startTime.getTime();
if (!trail.date) {
trail.date = startTime.toISOString()
.substring(0, 10);
}
}
}
const pointLength = points.length
for (let i = 1; i < pointLength; i++) {
const prevPoint = points[i - 1];
const point = points[i];
const elevation = point.ele ?? 0
const previousElevation = prevPoint.ele ?? 0
const elevationDiff = elevation - previousElevation;
if (elevationDiff > 0) {
totalElevationGain += elevationDiff;
}
const distance = calculateDistance(
prevPoint.$.lat ?? 0,
prevPoint.$.lon ?? 0,
point.$.lat ?? 0,
point.$.lon ?? 0,
);
totalDistance += distance;
}
}
}
const startPoint = gpx.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0);
if (startPoint) { if (startPoint) {
trail.lat = startPoint.$.lat trail.lat = startPoint.$.lat
trail.lon = startPoint.$.lon trail.lon = startPoint.$.lon
} }
trail.duration = totalDuration / 1000 / 60 const startTime = points?.at(0)?.time;
trail.elevation_gain = totalElevationGain; const endTime = points?.at((points?.length ?? 1) - 1)?.time
trail.distance = totalDistance
if (startTime && endTime && !trail.date) {
trail.date = startTime.toISOString()
.substring(0, 10);
}
trail.duration = totals.duration / 1000 / 60
trail.elevation_gain = totals.elevationGain;
trail.distance = totals.distance
return trail return trail
} }

View File

@@ -325,7 +325,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
case 'LineString': return this._addGeoJSONData(geom.coordinates, d.properties); case 'LineString': return this._addGeoJSONData(geom.coordinates, d.properties);
case 'MultiLineString': return _.each(geom.coordinates, (coords, i) => this._addGeoJSONData(coords, d.properties, i)); case 'MultiLineString': return _.each(geom.coordinates, (coords, i) => this._addGeoJSONData(coords, d.properties, i));
case 'Point': case 'Point':
default: return console.warn('Unsopperted GeoJSON feature geometry type:' + geom.type); default: return console.warn('Unsupported GeoJSON feature geometry type:' + geom.type);
} }
} }
} }

View File

@@ -0,0 +1,20 @@
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) {
return error(400, "PUBLIC_VALHALLA_URL not set")
}
try {
const r = await event.fetch(env.PUBLIC_VALHALLA_URL + '/height', { 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 || 500, e)
}
}

View File

@@ -182,9 +182,8 @@
return; return;
} }
$form.expand.waypoints = []; clearWaypoints();
$form.waypoints = []; clearRoute();
var reader = new FileReader(); var reader = new FileReader();
reader.readAsText(selectedFile); reader.readAsText(selectedFile);
@@ -205,6 +204,10 @@
try { try {
$form = await gpx2trail(gpxData); $form = await gpx2trail(gpxData);
$form.expand.gpx_data = gpxData; $form.expand.gpx_data = gpxData;
for (const waypoint of $form.expand.waypoints) {
saveWaypoint(waypoint);
}
} catch (e) { } catch (e) {
console.log(e); console.log(e);
@@ -234,6 +237,14 @@
}; };
} }
function clearWaypoints() {
for (const waypoint of $form.expand.waypoints) {
waypoint.marker?.remove();
}
$form.expand.waypoints = [];
$form.waypoints = [];
}
function openMarkerPopup(waypoint: Waypoint) { function openMarkerPopup(waypoint: Waypoint) {
waypoint.marker?.openPopup(); waypoint.marker?.openPopup();
} }
@@ -266,13 +277,13 @@
if (drawingActive) { if (drawingActive) {
if (index == 0) { if (index == 0) {
deleteFromRoute(index); deleteFromRoute(index);
$form.expand.gpx_data = route.toString(); updateTrailWithRouteData();
const nextWaypoint = $form.expand.waypoints[index]; const nextWaypoint = $form.expand.waypoints[index];
nextWaypoint.icon = "circle-half-stroke"; nextWaypoint.icon = "circle-half-stroke";
saveWaypoint(nextWaypoint); saveWaypoint(nextWaypoint);
} else if (index == $form.expand.waypoints.length) { } else if (index == $form.expand.waypoints.length) {
deleteFromRoute(index - 1); deleteFromRoute(index - 1);
$form.expand.gpx_data = route.toString(); updateTrailWithRouteData();
if ($form.expand.waypoints.length > 1) { if ($form.expand.waypoints.length > 1) {
const previousWaypoint = $form.expand.waypoints[index - 1]; const previousWaypoint = $form.expand.waypoints[index - 1];
@@ -296,7 +307,6 @@
$form.expand.waypoints[editedWaypointIndex] = savedWaypoint; $form.expand.waypoints[editedWaypointIndex] = savedWaypoint;
} else { } else {
savedWaypoint.id = cryptoRandomString({ length: 15 }); savedWaypoint.id = cryptoRandomString({ length: 15 });
$form.expand.waypoints = [...$form.expand.waypoints, savedWaypoint]; $form.expand.waypoints = [...$form.expand.waypoints, savedWaypoint];
} }
const marker = createMarkerFromWaypoint(L, savedWaypoint, (event) => { const marker = createMarkerFromWaypoint(L, savedWaypoint, (event) => {
@@ -374,9 +384,11 @@
} }
function startDrawing() { function startDrawing() {
$form.expand.gpx_data = "";
clearRoute();
drawingActive = true; drawingActive = true;
if (!route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.length) {
$form.expand.gpx_data = "";
clearWaypoints();
}
} }
function stopDrawing() { function stopDrawing() {
@@ -410,7 +422,7 @@
previousWaypoint.icon = "circle"; previousWaypoint.icon = "circle";
saveWaypoint(previousWaypoint); saveWaypoint(previousWaypoint);
} }
$form.expand.gpx_data = route.toString(); updateTrailWithRouteData();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
show_toast({ show_toast({
@@ -451,6 +463,14 @@
if (previousWaypoints) { if (previousWaypoints) {
editRoute(waypointIndex - 1, previousWaypoints); editRoute(waypointIndex - 1, previousWaypoints);
} }
updateTrailWithRouteData();
}
function updateTrailWithRouteData() {
const totals = route.getTotals();
$form.distance = totals.distance;
$form.duration = totals.duration;
$form.elevation_gain = totals.elevationGain;
$form.expand.gpx_data = route.toString(); $form.expand.gpx_data = route.toString();
} }
</script> </script>
@@ -475,12 +495,19 @@
on:click={openFileBrowser}>{$_("upload-file")}</Button on:click={openFileBrowser}>{$_("upload-file")}</Button
> >
{#if PUBLIC_VALHALLA_URL} {#if PUBLIC_VALHALLA_URL}
<div class="flex gap-4 items-center w-full">
<hr class="basis-full border-input-border" />
<span class="text-gray-500 uppercase">{$_("or")}</span>
<hr class="basis-full border-input-border" />
</div>
<button <button
class="btn-primary" class="btn-primary"
type="button" type="button"
on:click={drawingActive ? stopDrawing : startDrawing} on:click={drawingActive ? stopDrawing : startDrawing}
> >
{drawingActive ? $_("stop-drawing") : $_("draw-on-map")}</button {drawingActive
? $_("stop-drawing")
: $_("draw-a-route")}</button
> >
{/if} {/if}
<input <input
@@ -693,10 +720,11 @@
trail={$form} trail={$form}
crosshair={drawingActive} crosshair={drawingActive}
options={{ options={{
hotline: false, hotline: true,
autofitBounds: false, autofitBounds: false,
trkStart: false, trkStart: false,
trkEnd: false, trkEnd: false,
speed: false,
}} }}
bind:map bind:map
on:click={(e) => handleMapClick(e.detail)} on:click={(e) => handleMapClick(e.detail)}

View File

@@ -531,7 +531,7 @@ export var Chart = L.Control.Elevation.Chart = L.Class.extend({
.attr("width", x2 - x1 ) .attr("width", x2 - x1 )
.attr("height", this._height()) .attr("height", this._height())
.attr('class', 'gap') .attr('class', 'gap')
.attr('fill-opacity', '0.8') .attr('fill-opacity', '0.0')
.attr("fill", 'black'); // hide = black (mask) .attr("fill", 'black'); // hide = black (mask)
}); });
}, },