adds height to route drawing
This commit is contained in:
@@ -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}}",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"distance": "Dystans",
|
||||
"documentation": "Dokumentacja",
|
||||
"download-gpx": "Popierz GPX",
|
||||
"draw-on-map": "",
|
||||
"draw-a-route": "",
|
||||
"dutch": "Niderlandzki",
|
||||
"easy": "Łatwy",
|
||||
"edit": "Edytuj",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"distance": "距离",
|
||||
"documentation": "文档",
|
||||
"download-gpx": "下载 GPX",
|
||||
"draw-on-map": "",
|
||||
"draw-a-route": "",
|
||||
"dutch": "荷兰语",
|
||||
"easy": "简单",
|
||||
"edit": "编辑",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
web/src/routes/api/v1/valhalla/height/+server.ts
Normal file
20
web/src/routes/api/v1/valhalla/height/+server.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user