speed improvements for map (tbc)

This commit is contained in:
Christian Beutel
2025-08-02 22:02:59 +02:00
parent 396e64b5a6
commit 1c10e90db7
14 changed files with 230 additions and 113 deletions

View File

@@ -89,11 +89,8 @@
$data.distance = undefined;
return;
}
const gpxObject = await GPX.parse(trailData);
if (gpxObject instanceof Error) {
throw gpxObject;
}
const gpxObject = GPX.parse(trailData);
const totals = gpxObject.features;
$data.duration = totals.duration / 1000;

View File

@@ -58,7 +58,7 @@
};
}
trail = (await gpx2trail(log.expand.gpx_data!)).trail;
trail = gpx2trail(log.expand.gpx_data!).trail;
trail.id = log.id;
trail.expand!.gpx_data = log.expand.gpx_data;

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import { page } from "$app/state";
import directionCaret from "$lib/assets/svgs/caret-right-solid.svg";
import GPX from "$lib/models/gpx/gpx";
import type { Trail } from "$lib/models/trail";
import type { Waypoint } from "$lib/models/waypoint";
import { theme } from "$lib/stores/theme_store";
import { fetchGPX } from "$lib/stores/trail_store";
import { findStartAndEndPoints } from "$lib/util/geojson_util";
import { toGeoJson } from "$lib/util/gpx_util";
import {
createMarkerFromWaypoint,
createPopupFromTrail,
@@ -15,24 +15,17 @@
import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control";
import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule";
import {
baseMapStyles,
overlays,
pois,
} from "$lib/vendor/maplibre-layer-manager/layers";
import { baseMapStyles } from "$lib/vendor/maplibre-layer-manager/layers";
import { LayerManager } from "$lib/vendor/maplibre-layer-manager/maplibre-layer-manager";
import {
StyleSwitcherControl,
type StyleSwitcherControlOptions,
} from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
import { StyleSwitcherControl } from "$lib/vendor/maplibre-style-switcher/style-switcher-control";
import type { Feature, FeatureCollection, GeoJSON } from "geojson";
import * as M from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import { onDestroy, onMount, untrack } from "svelte";
import type { RadioItem } from "../base/radio_group.svelte";
interface Props {
trails?: Trail[];
gpx?: GPX;
waypoints?: Waypoint[];
markers?: M.Marker[];
map?: M.Map | null;
@@ -160,10 +153,10 @@
toggleEpcTheme();
});
$effect(() => {
if (drawing && map) {
startDrawing();
} else if (!drawing && map) {
stopDrawing();
if (drawing) {
untrack(() => startDrawing());
} else {
untrack(() => stopDrawing());
}
});
$effect(() => {
@@ -203,9 +196,11 @@
let cD: FeatureCollection = { type: "FeatureCollection", features: [] };
let r: GeoJSON[] = [];
trails.forEach((t) => {
if (t.expand?.gpx_data) {
r.push(toGeoJson(t.expand.gpx_data) as GeoJSON);
for (const t of trails) {
if (t.expand?.gpx) {
r.push(t.expand.gpx.toGeoJSON());
} else if (t.expand?.gpx_data) {
r.push(GPX.parse(t.expand.gpx_data).toGeoJSON());
}
if (clusterTrails && t.lat !== null && t.lon !== null) {
cD.features.push({
@@ -220,7 +215,7 @@
},
} as Feature);
}
});
}
return [r, cD];
}
@@ -653,9 +648,9 @@
clusterPopup = createPopupFromTrail(trail);
clusterPopup.setLngLat([trail.lon!, trail.lat!]).addTo(map);
const geojson = await fetchGPX(trail);
const gpx = await fetchGPX(trail);
addClusterHighlightLayer(toGeoJson(geojson));
addClusterHighlightLayer(GPX.parse(gpx).toGeoJSON());
clusterPopup.on("close", () => {
unHighlightCluster(false);
@@ -814,7 +809,7 @@
}
function showWaypoints() {
if (!map) {
if (!map || drawing) {
return;
}
@@ -1030,23 +1025,23 @@
type: "hillshade",
});
}
trails.forEach((t, i) => {
const layerId = t.id!;
if (map?.getLayer(layerId)) {
return;
}
addTrailLayer(t, layerId, i, data[i]);
});
if (
activeTrail !== null &&
!map?.getLayer("direction-carets")
) {
addCaretLayer(trails[activeTrail].id!);
}
} catch (e) {}
}
trails.forEach((t, i) => {
const layerId = t.id!;
if (map?.getLayer(layerId)) {
return;
}
addTrailLayer(t, layerId, i, data[i]);
});
if (
activeTrail !== null &&
trails[activeTrail] &&
!map?.getLayer("direction-carets")
) {
addCaretLayer(trails[activeTrail].id!);
}
});
map.on("moveend", (e) => {

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import * as M from "maplibre-gl";
import { fromFile, toGeoJson } from "$lib/util/gpx_util";
import GPX from "$lib/models/gpx/gpx";
import { fromFile } from "$lib/util/gpx_util";
import { onMount } from "svelte";
interface Props {
trailFile: File | undefined | null;
@@ -76,7 +77,12 @@
const sourceId = "trail-picker-geojson-source";
const layerId = "trail-picker-geojson-layer";
const geojson = toGeoJson(trailData);
const gpx = GPX.parse(trailData)
if(gpx instanceof Error) {
throw gpx;
}
const geojson = gpx.toGeoJSON()
if (layer) {
map.removeLayer(layerId);

View File

@@ -10,6 +10,9 @@ import geohash from "ngeohash"
import { encodePolyline } from '$lib/util/polyline_util';
import { APIError } from '$lib/util/api_util';
import type { ValhallaHeightResponse } from '../valhalla';
import { bbox, splitMultiLineStringToLineStrings } from '$lib/util/geojson_util';
import { browser } from '$app/environment';
import xmldom from 'xmldom';
const defaultAttributes = {
version: '1.1',
@@ -210,31 +213,65 @@ export default class GPX {
this.features = this.getTotals()
}
static parse(gpxString: string): Promise<GPX | Error> {
static parse(gpxString: string): GPX {
const sanitizedGPX = gpxString.replace(/\sxmlns=""/g, '').replace(/<!--[\s\S]*?-->/g, '');
return new Promise<GPX | Error>((resolve, reject) => xml2js.parseString(sanitizedGPX, {
explicitArray: false,
attrValueProcessors: [(str: string) => {
if (str.length && !isNaN(Number(str))) {
return Number.isInteger(Number(str)) ? parseInt(String(str), 10) : parseFloat(String(str));
return (function () {
let data = null, error = null;
xml2js.parseString(sanitizedGPX, {
explicitArray: false,
attrValueProcessors: [(str: string) => {
if (str.length && !isNaN(Number(str))) {
return Number.isInteger(Number(str)) ? parseInt(String(str), 10) : parseFloat(String(str));
}
return str;
}
return str;
}
]
}, async (err, xml) => {
if (err) {
reject(err);
}
const gpx = new GPX({
$: xml.gpx.$,
metadata: xml.gpx.metadata,
wpt: xml.gpx.wpt,
rte: xml.gpx.rte,
trk: xml.gpx.trk
]
}, (err, xml) => {
error = err;
data = new GPX({
$: xml.gpx.$,
metadata: xml.gpx.metadata,
wpt: xml.gpx.wpt,
rte: xml.gpx.rte,
trk: xml.gpx.trk
});
});
resolve(gpx)
}));
if (error) {
throw error
};
return data;
}()) as unknown as GPX;
}
toGeoJSON(): GeoJSON.FeatureCollection {
const features: GeoJSON.Feature[] = [];
if (this.wpt) {
for (const wpt of this.wpt) {
features.push(wpt.toGeoJSON());
}
}
if (this.rte) {
for (const rte of this.rte) {
features.push(rte.toGeoJSON());
}
}
if (this.trk) {
for (const trk of this.trk) {
features.push(...trk.toGeoJSON());
}
}
let geojson: GeoJSON.FeatureCollection = {
type: "FeatureCollection",
features
};
geojson.bbox = bbox(geojson)
return geojson
}
toString(options?: xml2js.BuilderOptions) {

View File

@@ -42,4 +42,24 @@ export default class Route {
this.rtept = (object.rtept as Waypoint[]).map(rtept => new Waypoint(rtept))
}
}
toGeoJSON(): GeoJSON.Feature {
const coordinates = (this.rtept || []).map(pt =>
[pt.$.lon ?? 0, pt.$.lat ?? 0, pt.ele ?? 0]
);
return {
type: "Feature",
geometry: {
type: "LineString",
coordinates
},
properties: {
name: this.name,
desc: this.desc,
type: this.type,
number: this.number
}
};
}
}

View File

@@ -1,3 +1,4 @@
import type Track from './track';
import Waypoint from './waypoint';
export default class TrackSegment {
@@ -12,4 +13,38 @@ export default class TrackSegment {
}
this.extensions = object.extensions;
}
toGeoJSON(
track: Track,
segmentId: number,
featureId: number
): GeoJSON.Feature {
const coordinates = (this.trkpt || []).map(pt => [
pt.$.lon ?? 0,
pt.$.lat ?? 0,
pt.ele ?? 0,
]);
const times = (this.trkpt || []).map(pt => pt.time?.toISOString() ?? null);
return {
type: "Feature",
geometry: {
type: "LineString",
coordinates,
},
properties: {
name: track.name,
desc: track.desc,
type: track.type,
number: track.number,
featureId,
segmentId,
coordinateProperties: {
times
}
}
};
}
}

View File

@@ -42,4 +42,16 @@ export default class Track {
this.trkseg = object.trkseg.map(trkseg => new TrackSegment(trkseg));;
}
}
toGeoJSON(): GeoJSON.Feature[] {
const features: GeoJSON.Feature[] = [];
if (!this.trkseg) return features;
this.trkseg.forEach((segment, index) => {
const feature = segment.toGeoJSON(this, index, features.length);
features.push(feature);
});
return features;
}
}

View File

@@ -78,4 +78,22 @@ export default class Waypoint {
this.link = object.link.map(l => new Link(l));
}
}
toGeoJSON(): GeoJSON.Feature {
return {
type: "Feature",
geometry: {
type: "Point",
coordinates: [this.$.lon ?? 0, this.$.lat ?? 0, this.ele ?? 0]
},
properties: {
name: this.name,
desc: this.desc,
time: this.time?.toISOString(),
type: this.type,
sym: this.sym
}
};
}
}

View File

@@ -1,6 +1,7 @@
import type { Actor } from "./activitypub/actor";
import type { Category } from "./category";
import type { Comment } from "./comment";
import type GPX from "./gpx/gpx";
import type { SummitLog } from "./summit_log";
import type { Tag } from "./tag";
import type { TrailLike } from "./trail_like";
@@ -40,6 +41,7 @@ class Trail {
author?: Actor
comments_via_trail?: Comment[]
gpx_data?: string
gpx?: GPX
trail_share_via_trail?: TrailShare[]
trail_like_via_trail?: TrailLike[]

View File

@@ -1,5 +1,5 @@
import GPX from "$lib/models/gpx/gpx";
import type Track from "$lib/models/gpx/track";
import Track from "$lib/models/gpx/track";
import TrackSegment from "$lib/models/gpx/track-segment";
import { haversineDistance } from "$lib/models/gpx/utils";
import Waypoint from "$lib/models/gpx/waypoint";
@@ -11,7 +11,7 @@ import type { LngLat } from "maplibre-gl";
import { _ } from "svelte-i18n";
import { get } from "svelte/store";
const emtpyTrack: Track = { trkseg: [] }
const emtpyTrack = new Track({ trkseg: [] })
class ValhallaStore {
route: GPX = $state(new GPX({ trk: [emtpyTrack] }));
@@ -193,8 +193,8 @@ export function reverseRoute() {
}
export function resetRoute() {
const delta = diff(valhallaStore.route, new GPX({ trk: [{ ...emtpyTrack }] }));
const reverseDelta = diff(new GPX({ trk: [{ ...emtpyTrack }] }), valhallaStore.route);
const delta = diff(valhallaStore.route, new GPX({ trk: [new Track({ ...emtpyTrack })] }));
const reverseDelta = diff(new GPX({ trk: [new Track({ ...emtpyTrack })] }), valhallaStore.route);
valhallaStore.route = applyChangeset(valhallaStore.route, delta);
pushToUndoStack(delta, reverseDelta)
@@ -214,8 +214,6 @@ export async function recalculateHeight() {
}
export async function splitSegment(index: number, pos: LngLat) {
console.log(valhallaStore.route.features.duration);
let seg = valhallaStore.route.trk?.at(0)?.trkseg?.at(index);
if (!seg || !seg.trkpt) {
return;

View File

@@ -18,9 +18,9 @@ import { handleFromRecordWithIRI } from "./activitypub_util";
import { Waypoint } from "$lib/models/waypoint";
export async function gpx2trail(gpxString: string, fallbackName?: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
export function gpx2trail(gpxString: string, fallbackName?: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const gpx = await GPX.parse(gpxString);
const gpx = GPX.parse(gpxString);
if (gpx instanceof Error) {
throw gpx;
@@ -87,7 +87,7 @@ export async function trail2gpx(trail: Trail, user?: AuthRecord) {
}
}
const gpx = await GPX.parse(gpxTrail.expand!.gpx_data!) as GPX;
const gpx = GPX.parse(gpxTrail.expand!.gpx_data!);
if (gpx instanceof Error) {
throw gpx;
@@ -108,12 +108,12 @@ export async function trail2gpx(trail: Trail, user?: AuthRecord) {
for (const wp of gpxTrail.expand!.waypoints ?? []) {
const gpxWpt = gpx.wpt.find((w) => w.$.lat == wp.lat && w.$.lon == wp.lon)
if (!gpxWpt) {
gpx.wpt.push({
gpx.wpt.push(new GPXWaypoint({
$: {
lat: wp.lat,
lon: wp.lon
}
})
}))
}
}
@@ -351,16 +351,6 @@ function isKMZFile(buffer: ArrayBuffer) {
return blob[0] === 0x50 && blob[1] === 0x4B && blob[2] === 0x03 && blob[3] === 0x04;
}
export function toGeoJson(gpxData: string) {
const parser = browser ? new DOMParser() : new xmldom.DOMParser();
let geojson = gpx(
parser.parseFromString(gpxData, "text/xml"),
) as GeoJSON;
geojson = splitMultiLineStringToLineStrings(geojson);
geojson.bbox = bbox(geojson)
return geojson
}
export function cropGPX(start: GPXWaypoint, end: GPXWaypoint, gpx: GPX): GPX {
let foundStart = false;
let done = false;
@@ -390,7 +380,7 @@ export function cropGPX(start: GPXWaypoint, end: GPXWaypoint, gpx: GPX): GPX {
}
if (newPoints.length > 0) {
croppedSegments.push({ trkpt: newPoints });
croppedSegments.push(new TrackSegment({ trkpt: newPoints }));
}
});
@@ -400,5 +390,5 @@ export function cropGPX(start: GPXWaypoint, end: GPXWaypoint, gpx: GPX): GPX {
};
}).filter(track => track.trkseg.length > 0);
return new GPX({ ...gpx, trk: croppedTrk ?? [], })
return new GPX({ ...gpx, trk: croppedTrk?.map(t => new Track({ ...t })) ?? [], })
}

View File

@@ -21,7 +21,7 @@ export async function PUT(event: RequestEvent) {
}
let parseResult: { trail: Trail, gpx: GPX };
try {
parseResult = (await gpx2trail(gpxData, data.get("name") as string | undefined, event.fetch));
parseResult = gpx2trail(gpxData, data.get("name") as string | undefined, event.fetch);
} catch (e: any) {
console.error(e)
throw new ClientResponseError({ status: 400, response: { message: "Invalid file" } })

View File

@@ -99,6 +99,8 @@
import { backInOut } from "svelte/easing";
import { fly, slide } from "svelte/transition";
import { z } from "zod";
import Track from "$lib/models/gpx/track.js";
import TrackSegment from "$lib/models/gpx/track-segment.js";
let { data } = $props();
@@ -202,6 +204,7 @@
photoFiles = [new File([blob], "route")];
}
form.expand!.gpx_data = valhallaStore.route.toString();
if (form.expand!.gpx_data && overwriteGPX) {
gpxFile = new Blob([form.expand!.gpx_data], {
type: "text/xml",
@@ -267,23 +270,25 @@
clearUndoRedoStack();
if ($formData.expand!.gpx_data) {
const gpx = await GPX.parse($formData.expand!.gpx_data);
const gpx = GPX.parse($formData.expand!.gpx_data);
if (!(gpx instanceof Error)) {
if (gpx.rte && !gpx.trk) {
gpx.trk = [
{
new Track({
trkseg: [
{
new TrackSegment({
trkpt: gpx.rte?.at(0)?.rtept,
},
}),
],
},
}),
];
gpx.rte = undefined;
}
setRoute(gpx);
initRouteAnchors(gpx);
updateTrailOnMap();
}
}
});
@@ -305,6 +310,7 @@
clearAnchors();
clearUndoRedoStack();
clearRoute();
mapTrail = [];
drawingActive = false;
overwriteGPX = false;
@@ -313,10 +319,11 @@
try {
const prevId = $formData.id;
const parseResult = await gpx2trail(gpxData, selectedFile.name);
const parseResult = gpx2trail(gpxData, selectedFile.name);
setFields(parseResult.trail);
$formData.id = prevId ?? cryptoRandomString({ length: 15 });
$formData.expand!.gpx_data = gpxData;
setFields(
"category",
page.data.settings.category || $categories[0].id,
@@ -345,18 +352,20 @@
if (parseResult.gpx.rte?.length && !parseResult.gpx.trk) {
parseResult.gpx.trk = [
{
new Track({
trkseg: [
{
new TrackSegment({
trkpt: parseResult.gpx.rte?.at(0)?.rtept,
},
}),
],
},
}),
];
parseResult.gpx.rte = undefined;
}
setRoute(parseResult.gpx);
initRouteAnchors(parseResult.gpx);
updateTrailOnMap();
} catch (e) {
console.error(e);
@@ -919,7 +928,6 @@
}
function toggleCropMarkers(active: boolean) {
console.log("toggleCropMarkers", active);
if (active) {
cropStartMarker?.setOpacity("1");
cropEndMarker?.setOpacity("1");
@@ -1035,8 +1043,8 @@
function updateTrailWithRouteData() {
overwriteGPX = true;
updateTotals(valhallaStore.route);
$formData.expand!.gpx_data = valhallaStore.route.toString();
updateTrailOnMap();
if (!$formData.id) {
$formData.id = cryptoRandomString({ length: 15 });
}
@@ -1044,14 +1052,19 @@
function updateTotals(gpx: GPX) {
const totals = gpx.features;
$formData.distance = totals.distance;
$formData.duration = totals.duration / 1000;
$formData.elevation_gain = totals.elevationGain;
$formData.elevation_loss = totals.elevationLoss;
formData.set({
...$formData,
distance: totals.distance,
duration: totals.duration / 1000,
elevation_gain: totals.elevationGain,
elevation_loss: totals.elevationLoss,
});
}
function updateTrailOnMap() {
mapTrail = [$formData as Trail];
const t = { ...$formData } as Trail;
t.expand!.gpx = valhallaStore.route;
mapTrail = [t];
}
function handleSearchClick(item: SearchItem) {
@@ -1071,12 +1084,6 @@
icon: getIconForLocation(h),
}));
}
let gpxData = $derived($formData.expand?.gpx_data);
$effect(() => {
if (gpxData) {
untrack(() => updateTrailOnMap());
}
});
function getTrailTags() {
return (