adds split gpx function

This commit is contained in:
Christian Beutel
2025-07-20 11:35:21 +02:00
parent 731a4ffb97
commit 634c34ce65
4 changed files with 126 additions and 39 deletions

View File

@@ -55,6 +55,10 @@
segment: number; segment: number;
event: M.MapMouseEvent; event: M.MapMouseEvent;
}) => void; }) => void;
onsegmentclick?: (data: {
segment: number;
event: M.MapMouseEvent;
}) => void;
onselect?: (trail: Trail) => void; onselect?: (trail: Trail) => void;
onunselect?: (trail: Trail) => void; onunselect?: (trail: Trail) => void;
onfullscreen?: () => void; onfullscreen?: () => void;
@@ -87,6 +91,7 @@
clusterTrails = false, clusterTrails = false,
onmarkerdragend, onmarkerdragend,
onsegmentdragend, onsegmentdragend,
onsegmentclick,
onselect, onselect,
onunselect, onunselect,
onfullscreen, onfullscreen,
@@ -408,7 +413,7 @@
activeTrail = trails.findIndex((t) => t.id == trail.id); activeTrail = trails.findIndex((t) => t.id == trail.id);
}; };
layers[id].listener.onMouseMove = moveCrosshairToCursorPosition; layers[id].listener.onMouseMove = moveCrosshairToCursorPosition;
layers[id].listener.onMouseDown = (e) => handDragStart(e, id); layers[id].listener.onMouseDown = (e) => handleDragStart(e, id);
map.on("mouseenter", id, layers[id].listener.onEnter); map.on("mouseenter", id, layers[id].listener.onEnter);
map.on("mouseleave", id, layers[id].listener.onLeave); map.on("mouseleave", id, layers[id].listener.onLeave);
@@ -529,7 +534,7 @@
elevationMarker.setLngLat(e.lngLat); elevationMarker.setLngLat(e.lngLat);
} }
function handDragStart(e: M.MapMouseEvent, id: string) { function handleDragStart(e: M.MapMouseEvent, id: string) {
if ( if (
!drawing || !drawing ||
(e.originalEvent.target as HTMLElement | null)?.classList.contains( (e.originalEvent.target as HTMLElement | null)?.classList.contains(
@@ -549,13 +554,21 @@
} }
map?.on("mousemove", moveElevationMarkerToCursorPosition); map?.on("mousemove", moveElevationMarkerToCursorPosition);
map?.once("mouseup", handleDragEnd); map?.once("mouseup", (e2) => handleDragEnd(e2, e));
} }
function handleDragEnd(e: M.MapMouseEvent) { function handleDragEnd(end: M.MapMouseEvent, start: M.MapMouseEvent) {
map?.off("mousemove", moveElevationMarkerToCursorPosition); map?.off("mousemove", moveElevationMarkerToCursorPosition);
epc?.hideCrosshair(); epc?.hideCrosshair();
onsegmentdragend?.({ segment: draggingSegment!, event: e }); const distanceDragged = Math.sqrt(
Math.pow(end.originalEvent.x - start.originalEvent.x, 2) +
Math.pow(end.originalEvent.y - start.originalEvent.y, 2),
);
if (distanceDragged < 0.5) {
onsegmentclick?.({ segment: draggingSegment!, event: end });
} else {
onsegmentdragend?.({ segment: draggingSegment!, event: end });
}
draggingSegment = null; draggingSegment = null;
} }

View File

@@ -121,7 +121,7 @@
recalculateElevationData = false; recalculateElevationData = false;
crop = false; crop = false;
editRoute = !editRoute; editRoute = !editRoute;
}}><i class="fa fa-pen text-sm"></i></button }}><i class="fa fa-route text-sm"></i></button
> >
<button <button
class="btn-icon" class="btn-icon"

View File

@@ -1,12 +1,14 @@
import GPX from "$lib/models/gpx/gpx"; 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 { haversineDistance } from "$lib/models/gpx/utils";
import Waypoint from "$lib/models/gpx/waypoint"; import Waypoint from "$lib/models/gpx/waypoint";
import { type RoutingOptions, type ValhallaAnchor, type ValhallaHeightResponse, type ValhallaRouteResponse } from "$lib/models/valhalla"; import { type RoutingOptions, type ValhallaAnchor, type ValhallaHeightResponse, type ValhallaRouteResponse } from "$lib/models/valhalla";
import { APIError } from "$lib/util/api_util"; import { APIError } from "$lib/util/api_util";
import { decodePolyline, encodePolyline } from "$lib/util/polyline_util"; import { decodePolyline, encodePolyline } from "$lib/util/polyline_util";
import { get } from "svelte/store"; import type { LngLat } from "maplibre-gl";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { get } from "svelte/store";
const emtpyTrack: Track = { trkseg: [] } const emtpyTrack: Track = { trkseg: [] }
@@ -138,7 +140,7 @@ export function reverseRoute() {
._content.getElementsByTagName("h5")[0]; ._content.getElementsByTagName("h5")[0];
if (anchorPopupHeading) { if (anchorPopupHeading) {
anchorPopupHeading.textContent = anchorPopupHeading.textContent =
get(_)("valhallaStore.route-point") + " #" + (i + 1); get(_)("route-point") + " #" + (i + 1);
} }
}); });
} }
@@ -161,6 +163,31 @@ export async function recalculateHeight() {
await valhallaStore.route.correctElevation(); await valhallaStore.route.correctElevation();
} }
export async function splitSegment(index: number, pos: LngLat) {
let seg = valhallaStore.route.trk?.at(0)?.trkseg?.at(index);
if (!seg || !seg.trkpt) {
return;
}
const points = seg.trkpt;
let bestSplitIndex: number = 0
let minDistance = Infinity
for (let i = 1; i < points.length; i++) {
const pt = points[i]
const dist = haversineDistance(pt.$.lat!, pt.$.lon!, pos.lat, pos.lng);
if (dist < minDistance) {
bestSplitIndex = i
}
}
const firstSegmentPoints = [...points.slice(0, index)];
const secondSegmentPoints = [...points.slice(index)];
seg.trkpt = firstSegmentPoints;
insertIntoRoute(secondSegmentPoints, index)
}
export function normalizeRouteTime() { export function normalizeRouteTime() {
let currentTime = new Date(); let currentTime = new Date();

View File

@@ -47,6 +47,7 @@
resetRoute, resetRoute,
reverseRoute, reverseRoute,
setRoute, setRoute,
splitSegment,
} from "$lib/stores/valhalla_store.svelte.js"; } from "$lib/stores/valhalla_store.svelte.js";
import { waypoint } from "$lib/stores/waypoint_store"; import { waypoint } from "$lib/stores/waypoint_store";
import { getFileURL } from "$lib/util/file_util"; import { getFileURL } from "$lib/util/file_util";
@@ -93,7 +94,7 @@
import { onMount, untrack } from "svelte"; import { onMount, untrack } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { backInOut } from "svelte/easing"; import { backInOut } from "svelte/easing";
import { slide } from "svelte/transition"; import { fly, slide } from "svelte/transition";
import { z } from "zod"; import { z } from "zod";
let { data } = $props(); let { data } = $props();
@@ -124,7 +125,7 @@
let cropStartMarker: FontawesomeMarker; let cropStartMarker: FontawesomeMarker;
let cropEndMarker: FontawesomeMarker; let cropEndMarker: FontawesomeMarker;
let flatRoute: GPXWaypoint[] = $derived(valhallaStore.route.flatten()) let flatRoute: GPXWaypoint[] = $derived(valhallaStore.route.flatten());
let croppedGPX: GPX | null = null; let croppedGPX: GPX | null = null;
@@ -411,12 +412,16 @@
function initCropMarkers() { function initCropMarkers() {
const routeStartPoint: M.LngLatLike = [ const routeStartPoint: M.LngLatLike = [
valhallaStore.route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)?.$.lon ?? 0, valhallaStore.route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)?.$
valhallaStore.route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)?.$.lat ?? 0, .lon ?? 0,
valhallaStore.route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)?.$
.lat ?? 0,
]; ];
const routeEndPoint: M.LngLatLike = [ const routeEndPoint: M.LngLatLike = [
valhallaStore.route.trk?.at(-1)?.trkseg?.at(-1)?.trkpt?.at(-1)?.$.lon ?? 0, valhallaStore.route.trk?.at(-1)?.trkseg?.at(-1)?.trkpt?.at(-1)?.$
valhallaStore.route.trk?.at(-1)?.trkseg?.at(-1)?.trkpt?.at(-1)?.$.lat ?? 0, .lon ?? 0,
valhallaStore.route.trk?.at(-1)?.trkseg?.at(-1)?.trkpt?.at(-1)?.$
.lat ?? 0,
]; ];
if (!cropStartMarker || !cropEndMarker) { if (!cropStartMarker || !cropEndMarker) {
cropStartMarker = new FontawesomeMarker( cropStartMarker = new FontawesomeMarker(
@@ -611,6 +616,7 @@
for (const anchor of valhallaStore.anchors) { for (const anchor of valhallaStore.anchors) {
anchor.marker?.remove(); anchor.marker?.remove();
} }
toggleCropMarkers(false);
if (valhallaStore.route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)) { if (valhallaStore.route.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0)) {
$formData.lat = valhallaStore.route.trk $formData.lat = valhallaStore.route.trk
@@ -649,7 +655,11 @@
} else { } else {
const anchorCount = valhallaStore.anchors.length; const anchorCount = valhallaStore.anchors.length;
if (anchorCount == 0) { if (anchorCount == 0) {
addAnchor(e.lngLat.lat, e.lngLat.lng, valhallaStore.anchors.length); addAnchor(
e.lngLat.lat,
e.lngLat.lng,
valhallaStore.anchors.length,
);
} else { } else {
await addAnchorAndRecalculate(e.lngLat.lat, e.lngLat.lng); await addAnchorAndRecalculate(e.lngLat.lat, e.lngLat.lng);
} }
@@ -657,7 +667,8 @@
} }
async function addAnchorAndRecalculate(lat: number, lon: number) { async function addAnchorAndRecalculate(lat: number, lon: number) {
const previousAnchor = valhallaStore.anchors[valhallaStore.anchors.length - 1]; const previousAnchor =
valhallaStore.anchors[valhallaStore.anchors.length - 1];
const anchor = addAnchor(lat, lon, valhallaStore.anchors.length); const anchor = addAnchor(lat, lon, valhallaStore.anchors.length);
const markerText = startAnchorLoading(anchor); const markerText = startAnchorLoading(anchor);
try { try {
@@ -699,10 +710,14 @@
lon, lon,
index + 1, index + 1,
() => { () => {
removeAnchor(valhallaStore.anchors.findIndex((a) => a.id == anchor.id)); removeAnchor(
valhallaStore.anchors.findIndex((a) => a.id == anchor.id),
);
}, },
() => { () => {
const thisAnchor = valhallaStore.anchors.find((a) => a.id == anchor.id); const thisAnchor = valhallaStore.anchors.find(
(a) => a.id == anchor.id,
);
addAnchorAndRecalculate( addAnchorAndRecalculate(
thisAnchor?.lat ?? lat, thisAnchor?.lat ?? lat,
thisAnchor?.lon ?? lon, thisAnchor?.lon ?? lon,
@@ -794,7 +809,9 @@
} }
async function recalculateRoute(anchorIndex: number) { async function recalculateRoute(anchorIndex: number) {
const markerText = startAnchorLoading(valhallaStore.anchors[anchorIndex]); const markerText = startAnchorLoading(
valhallaStore.anchors[anchorIndex],
);
const anchor = valhallaStore.anchors[anchorIndex]; const anchor = valhallaStore.anchors[anchorIndex];
if (!anchor) { if (!anchor) {
@@ -860,21 +877,8 @@
data.segment + 1, data.segment + 1,
); );
const markerText = startAnchorLoading(anchor); const markerText = startAnchorLoading(anchor);
updateFollowingAnchors(data.segment);
for (let i = data.segment + 2; i < valhallaStore.anchors.length; i++) {
const anchor = valhallaStore.anchors[i];
const markerIcon = anchor.marker?.getElement();
if (markerIcon) {
const markerText = markerIcon.textContent ?? "0";
const markerIndex = parseInt(markerText);
const newIndex = markerIndex + 1;
markerIcon.textContent = newIndex + "";
anchor
.marker!.getPopup()
._content.getElementsByTagName("h5")[0].textContent =
$_("route-point") + " #" + newIndex;
}
}
const previousAnchor = valhallaStore.anchors[data.segment]; const previousAnchor = valhallaStore.anchors[data.segment];
const nextAnchor = valhallaStore.anchors[data.segment + 2]; const nextAnchor = valhallaStore.anchors[data.segment + 2];
@@ -910,6 +914,37 @@
} }
} }
function updateFollowingAnchors(segment: number) {
for (let i = segment + 2; i < valhallaStore.anchors.length; i++) {
const anchor = valhallaStore.anchors[i];
const markerIcon = anchor.marker?.getElement();
if (markerIcon) {
const markerText = markerIcon.textContent ?? "0";
const markerIndex = parseInt(markerText);
const newIndex = markerIndex + 1;
markerIcon.textContent = newIndex + "";
anchor
.marker!.getPopup()
._content.getElementsByTagName("h5")[0].textContent =
$_("route-point") + " #" + newIndex;
}
}
}
async function handleSegmentClick(data: {
segment: number;
event: M.MapMouseEvent;
}) {
addAnchor(
data.event.lngLat.lat,
data.event.lngLat.lng,
data.segment + 1,
);
splitSegment(data.segment, data.event.lngLat);
updateFollowingAnchors(data.segment);
}
function reverseTrail() { function reverseTrail() {
reverseRoute(); reverseRoute();
@@ -935,20 +970,27 @@
} else { } else {
cropStartMarker?.setOpacity("0"); cropStartMarker?.setOpacity("0");
cropEndMarker?.setOpacity("0"); cropEndMarker?.setOpacity("0");
const totals = valhallaStore.route.features;
$formData.distance = totals.distance;
$formData.duration = totals.duration / 1000;
$formData.elevation_gain = totals.elevationGain;
$formData.elevation_loss = totals.elevationLoss;
} }
} }
function updateCropMarkers(range: [start: number, end: number]) { function updateCropMarkers(range: [start: number, end: number]) {
const [start, end] = range; const [start, end] = range;
const targetStartDistance = valhallaStore.route.features.distance * (start / 100); const targetStartDistance =
valhallaStore.route.features.distance * (start / 100);
const [startLon, startLat, startIndex] = getCoordinateAtDistance( const [startLon, startLat, startIndex] = getCoordinateAtDistance(
flatRoute, flatRoute,
valhallaStore.route.features.cumulativeDistance, valhallaStore.route.features.cumulativeDistance,
targetStartDistance, targetStartDistance,
); );
const targetEndDistance = valhallaStore.route.features.distance * (end / 100); const targetEndDistance =
valhallaStore.route.features.distance * (end / 100);
const [endLon, endLat, endIndex] = getCoordinateAtDistance( const [endLon, endLat, endIndex] = getCoordinateAtDistance(
flatRoute, flatRoute,
valhallaStore.route.features.cumulativeDistance, valhallaStore.route.features.cumulativeDistance,
@@ -958,7 +1000,11 @@
cropStartMarker.setLngLat([startLon, startLat]); cropStartMarker.setLngLat([startLon, startLat]);
cropEndMarker.setLngLat([endLon, endLat]); cropEndMarker.setLngLat([endLon, endLat]);
croppedGPX = cropGPX(flatRoute[startIndex], flatRoute[endIndex], valhallaStore.route); croppedGPX = cropGPX(
flatRoute[startIndex],
flatRoute[endIndex],
valhallaStore.route,
);
const totals = croppedGPX.features; const totals = croppedGPX.features;
$formData.distance = totals.distance; $formData.distance = totals.distance;
$formData.duration = totals.duration / 1000; $formData.duration = totals.duration / 1000;
@@ -1425,8 +1471,8 @@
<div class="relative"> <div class="relative">
{#if drawingActive} {#if drawingActive}
<div <div
in:slide={{ easing: backInOut, axis: "x" }} in:fly={{ easing: backInOut, x: -30 }}
out:slide={{ easing: backInOut, axis: "x" }} out:fly={{ easing: backInOut, x: -30 }}
class="absolute top-8 left-2 z-50" class="absolute top-8 left-2 z-50"
> >
<RouteEditor <RouteEditor
@@ -1450,6 +1496,7 @@
activeTrail={0} activeTrail={0}
bind:map bind:map
onclick={(target) => handleMapClick(target)} onclick={(target) => handleMapClick(target)}
onsegmentclick={(data) => handleSegmentClick(data)}
onsegmentdragend={(data) => handleSegmentDragEnd(data)} onsegmentdragend={(data) => handleSegmentDragEnd(data)}
mapOptions={{ preserveDrawingBuffer: true }} mapOptions={{ preserveDrawingBuffer: true }}
></MapWithElevationMaplibre> ></MapWithElevationMaplibre>