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",
"documentation": "Dokumentation",
"download-gpx": "GPX herunterladen",
"draw-on-map": "",
"draw-a-route": "Route zeichnen",
"dutch": "Niederländisch",
"easy": "Einfach",
"edit": "Bearbeiten",
@@ -129,7 +129,7 @@
"show-on-map": "Auf der Karte anzeigen",
"slogan": "Speichere deine Abenteuer!",
"sort": "Sortieren",
"stop-drawing": "",
"stop-drawing": "Zeichnen beenden",
"summit-book": "Gipfelbuch",
"text": "Text",
"trail": "{n, plural, =1 {Route} other {Routen}}",

View File

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

View File

@@ -40,7 +40,7 @@
"distance": "Distance",
"documentation": "Documentation",
"download-gpx": "Télécharger le GPX",
"draw-on-map": "",
"draw-a-route": "",
"dutch": "Néerlandais",
"easy": "Facile",
"edit": "Editer",

View File

@@ -40,7 +40,7 @@
"distance": "Távolság",
"documentation": "Dokumentáció",
"download-gpx": "GPX letöltése",
"draw-on-map": "",
"draw-a-route": "",
"dutch": "Holland",
"easy": "Könnyű",
"edit": "Szerkesztés",

View File

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

View File

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

View File

@@ -40,7 +40,7 @@
"distance": "Distância",
"documentation": "Documentação",
"download-gpx": "Baixar GPX",
"draw-on-map": "",
"draw-a-route": "",
"dutch": "Holandês",
"easy": "Fácil",
"edit": "Editar",

View File

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

View File

@@ -1,9 +1,9 @@
import * as xml2js from 'isomorphic-xml2js';
import Metadata from './metadata';
import Waypoint from './waypoint';
import Route from './route';
import Track from './track';
import { removeEmpty, allDatesToISOString } from './utils';
import { allDatesToISOString, calculateDistance, removeEmpty } from './utils';
import Waypoint from './waypoint';
const defaultAttributes = {
version: '1.1',
@@ -72,9 +72,59 @@ export default class GPX {
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> {
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) => {
if (err) {
reject(err);
@@ -98,4 +148,4 @@ export default class GPX {
allDatesToISOString(gpx);
return builder.buildObject(gpx);
}
}
}

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: {
legs: {
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 TrackSegment from "$lib/models/gpx/track-segment";
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";
@@ -20,13 +20,21 @@ export async function calculateRouteBetween(startLat: number, startLon: number,
"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) })
let r = await fetch("/api/v1/valhalla/route", { method: "POST", body: JSON.stringify(requestBody) })
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
const response: ValhallaResponse = await r.json();
const points = decodeShape(response.trip.legs[0].shape);
const waypoints = points.map((p) => new Waypoint({ $: { lat: p[0], lon: p[1] } }))
const routeResponse: ValhallaRouteResponse = await r.json();
const shape = routeResponse.trip.legs[0].shape
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
}

View File

@@ -3,19 +3,7 @@ import { Trail } from "$lib/models/trail";
import { Waypoint } from "$lib/models/waypoint";
import GeoJsonToGpx from "$lib/vendor/geoJSONToGPX";
import { kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON";
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;
}
import cryptoRandomString from "crypto-random-string";
export async function gpx2trail(gpxString: string) {
const gpx = await GPX.parse(gpxString);
@@ -30,65 +18,34 @@ export async function gpx2trail(gpxString: string) {
trail.description = gpx.metadata?.desc;
for (const wpt of gpx.wpt ?? []) {
for (const wpt of gpx.wpt ?? []) {
const wp = new Waypoint(wpt.$.lat ?? 0, wpt.$.lon ?? 0);
wp.name = wp.name
wp.description = wp.description;
wp.id = cryptoRandomString({ length: 15 });
wp.name = wpt.name
wp.description = wpt.desc;
trail.expand.waypoints.push(wp);
}
let totalElevationGain = 0;
let totalDuration = 0;
let totalDistance = 0;
const totals = gpx.getTotals()
for (const track of gpx.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();
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);
const points = gpx.trk?.at(0)?.trkseg?.at(0)?.trkpt
const startPoint = points?.at(0);
if (startPoint) {
trail.lat = startPoint.$.lat
trail.lon = startPoint.$.lon
}
trail.duration = totalDuration / 1000 / 60
trail.elevation_gain = totalElevationGain;
trail.distance = totalDistance
const startTime = points?.at(0)?.time;
const endTime = points?.at((points?.length ?? 1) - 1)?.time
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
}

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 'MultiLineString': return _.each(geom.coordinates, (coords, i) => this._addGeoJSONData(coords, d.properties, i));
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;
}
$form.expand.waypoints = [];
$form.waypoints = [];
clearWaypoints();
clearRoute();
var reader = new FileReader();
reader.readAsText(selectedFile);
@@ -205,6 +204,10 @@
try {
$form = await gpx2trail(gpxData);
$form.expand.gpx_data = gpxData;
for (const waypoint of $form.expand.waypoints) {
saveWaypoint(waypoint);
}
} catch (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) {
waypoint.marker?.openPopup();
}
@@ -266,13 +277,13 @@
if (drawingActive) {
if (index == 0) {
deleteFromRoute(index);
$form.expand.gpx_data = route.toString();
updateTrailWithRouteData();
const nextWaypoint = $form.expand.waypoints[index];
nextWaypoint.icon = "circle-half-stroke";
saveWaypoint(nextWaypoint);
} else if (index == $form.expand.waypoints.length) {
deleteFromRoute(index - 1);
$form.expand.gpx_data = route.toString();
updateTrailWithRouteData();
if ($form.expand.waypoints.length > 1) {
const previousWaypoint = $form.expand.waypoints[index - 1];
@@ -296,7 +307,6 @@
$form.expand.waypoints[editedWaypointIndex] = savedWaypoint;
} else {
savedWaypoint.id = cryptoRandomString({ length: 15 });
$form.expand.waypoints = [...$form.expand.waypoints, savedWaypoint];
}
const marker = createMarkerFromWaypoint(L, savedWaypoint, (event) => {
@@ -374,9 +384,11 @@
}
function startDrawing() {
$form.expand.gpx_data = "";
clearRoute();
drawingActive = true;
if (!route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.length) {
$form.expand.gpx_data = "";
clearWaypoints();
}
}
function stopDrawing() {
@@ -410,7 +422,7 @@
previousWaypoint.icon = "circle";
saveWaypoint(previousWaypoint);
}
$form.expand.gpx_data = route.toString();
updateTrailWithRouteData();
} catch (e) {
console.error(e);
show_toast({
@@ -451,6 +463,14 @@
if (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();
}
</script>
@@ -475,12 +495,19 @@
on:click={openFileBrowser}>{$_("upload-file")}</Button
>
{#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
class="btn-primary"
type="button"
on:click={drawingActive ? stopDrawing : startDrawing}
>
{drawingActive ? $_("stop-drawing") : $_("draw-on-map")}</button
{drawingActive
? $_("stop-drawing")
: $_("draw-a-route")}</button
>
{/if}
<input
@@ -693,10 +720,11 @@
trail={$form}
crosshair={drawingActive}
options={{
hotline: false,
hotline: true,
autofitBounds: false,
trkStart: false,
trkEnd: false,
speed: false,
}}
bind:map
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("height", this._height())
.attr('class', 'gap')
.attr('fill-opacity', '0.8')
.attr('fill-opacity', '0.0')
.attr("fill", 'black'); // hide = black (mask)
});
},