From 2e171dfbee3fc95dca6bbf192152053d5ef1f59d Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 24 Dec 2025 12:43:24 +0100 Subject: [PATCH] speed up wpt to segment matching --- gpx/src/simplify.ts | 8 +-- website/src/lib/logic/file-actions.ts | 77 ++++++++------------------- website/src/lib/utils.ts | 52 +++++++++++++++++- 3 files changed, 77 insertions(+), 60 deletions(-) diff --git a/gpx/src/simplify.ts b/gpx/src/simplify.ts index d55e5c231..7669b3cd3 100644 --- a/gpx/src/simplify.ts +++ b/gpx/src/simplify.ts @@ -59,13 +59,13 @@ function ramerDouglasPeuckerRecursive( } export function crossarcDistance( - point1: TrackPoint, - point2: TrackPoint, + point1: TrackPoint | Coordinates, + point2: TrackPoint | Coordinates, point3: TrackPoint | Coordinates ): number { return crossarc( - point1.getCoordinates(), - point2.getCoordinates(), + point1 instanceof TrackPoint ? point1.getCoordinates() : point1, + point2 instanceof TrackPoint ? point2.getCoordinates() : point2, point3 instanceof TrackPoint ? point3.getCoordinates() : point3 ); } diff --git a/website/src/lib/logic/file-actions.ts b/website/src/lib/logic/file-actions.ts index f8771e727..2fb9c8a0c 100644 --- a/website/src/lib/logic/file-actions.ts +++ b/website/src/lib/logic/file-actions.ts @@ -17,7 +17,6 @@ import { import { i18n } from '$lib/i18n.svelte'; import { freeze, type WritableDraft } from 'immer'; import { - distance, GPXFile, parseGPX, Track, @@ -30,7 +29,7 @@ import { } from 'gpx'; import { get } from 'svelte/store'; import { settings } from '$lib/logic/settings'; -import { getClosestLinePoint, getElevation } from '$lib/utils'; +import { getClosestLinePoint, getClosestTrackSegments, getElevation } from '$lib/utils'; import { gpxStatistics } from '$lib/logic/statistics'; import { boundsManager } from './bounds'; @@ -453,34 +452,13 @@ export const fileActions = { selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { if (level === ListLevel.FILE) { let file = fileStateCollection.getFile(fileId); - if (file) { + let statistics = fileStateCollection.getStatistics(fileId); + if (file && statistics) { if (file.trk.length > 1) { let fileIds = getFileIds(file.trk.length); - let closest = file.wpt.map((wpt, wptIndex) => { - return { - wptIndex: wptIndex, - index: [0], - distance: Number.MAX_VALUE, - }; - }); - file.trk.forEach((track, index) => { - track.getSegments().forEach((segment) => { - segment.trkpt.forEach((point) => { - file.wpt.forEach((wpt, wptIndex) => { - let dist = distance( - point.getCoordinates(), - wpt.getCoordinates() - ); - if (dist < closest[wptIndex].distance) { - closest[wptIndex].distance = dist; - closest[wptIndex].index = [index]; - } else if (dist === closest[wptIndex].distance) { - closest[wptIndex].index.push(index); - } - }); - }); - }); - }); + let closest = file.wpt.map((wpt) => + getClosestTrackSegments(file, statistics, wpt.getCoordinates()) + ); file.trk.forEach((track, index) => { let newFile = file.clone(); let tracks = track.trkseg.map((segment, segmentIndex) => { @@ -496,8 +474,12 @@ export const fileActions = { 0, file.wpt.length - 1, closest - .filter((c) => c.index.includes(index)) - .map((c) => file.wpt[c.wptIndex]) + .filter((c) => + c.some( + ([trackIndex, segmentIndex]) => trackIndex === index + ) + ) + .map((c, wptIndex) => file.wpt[wptIndex]) ); newFile._data.id = fileIds[index]; newFile.metadata.name = @@ -506,29 +488,9 @@ export const fileActions = { }); } else if (file.trk.length === 1) { let fileIds = getFileIds(file.trk[0].trkseg.length); - let closest = file.wpt.map((wpt, wptIndex) => { - return { - wptIndex: wptIndex, - index: [0], - distance: Number.MAX_VALUE, - }; - }); - file.trk[0].trkseg.forEach((segment, index) => { - segment.trkpt.forEach((point) => { - file.wpt.forEach((wpt, wptIndex) => { - let dist = distance( - point.getCoordinates(), - wpt.getCoordinates() - ); - if (dist < closest[wptIndex].distance) { - closest[wptIndex].distance = dist; - closest[wptIndex].index = [index]; - } else if (dist === closest[wptIndex].distance) { - closest[wptIndex].index.push(index); - } - }); - }); - }); + let closest = file.wpt.map((wpt) => + getClosestTrackSegments(file, statistics, wpt.getCoordinates()) + ); file.trk[0].trkseg.forEach((segment, index) => { let newFile = file.clone(); newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [ @@ -538,8 +500,13 @@ export const fileActions = { 0, file.wpt.length - 1, closest - .filter((c) => c.index.includes(index)) - .map((c) => file.wpt[c.wptIndex]) + .filter((c) => + c.some( + ([trackIndex, segmentIndex]) => + segmentIndex === index + ) + ) + .map((c, wptIndex) => file.wpt[wptIndex]) ); newFile._data.id = fileIds[index]; newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`; diff --git a/website/src/lib/utils.ts b/website/src/lib/utils.ts index 098aab0b4..7725075cd 100644 --- a/website/src/lib/utils.ts +++ b/website/src/lib/utils.ts @@ -2,11 +2,13 @@ import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; import { base } from '$app/paths'; import { languages } from '$lib/languages'; -import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance } from 'gpx'; +import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance, GPXFile } from 'gpx'; import mapboxgl from 'mapbox-gl'; import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt'; import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public'; import PNGReader from 'png.js'; +import type { GPXStatisticsTree } from '$lib/logic/statistics-tree'; +import { ListTrackSegmentItem } from '$lib/components/file-list/file-list'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -47,6 +49,54 @@ export function getClosestLinePoint( return closest; } +export function getClosestTrackSegments( + file: GPXFile, + statistics: GPXStatisticsTree, + point: Coordinates +): [number, number][] { + let segmentBoundsDistances: [number, number, number][] = []; + file.forEachSegment((segment, trackIndex, segmentIndex) => { + let segmentStatistics = statistics.getStatisticsFor( + new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex) + ); + let segmentBounds = segmentStatistics.global.bounds; + let northEast = segmentBounds.northEast; + let southWest = segmentBounds.southWest; + let northWest: Coordinates = { lat: northEast.lat, lon: southWest.lon }; + let southEast: Coordinates = { lat: southWest.lat, lon: northEast.lon }; + let distanceToSegment = Math.min( + crossarcDistance(northWest, northEast, point), + crossarcDistance(northEast, southEast, point), + crossarcDistance(southEast, southWest, point), + crossarcDistance(southWest, northWest, point) + ); + segmentBoundsDistances.push([distanceToSegment, trackIndex, segmentIndex]); + }); + segmentBoundsDistances.sort((a, b) => a[0] - b[0]); + + let closest: { distance: number; indices: [number, number][] } = { + distance: Number.MAX_VALUE, + indices: [], + }; + for (let s = 0; s < segmentBoundsDistances.length; s++) { + if (segmentBoundsDistances[s][0] > closest.distance) { + break; + } + const segment = file.getSegment(segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]); + segment.trkpt.forEach((pt) => { + let dist = distance(pt.getCoordinates(), point); + if (dist < closest.distance) { + closest.distance = dist; + closest.indices = [[segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]]; + } else if (dist === closest.distance) { + closest.indices.push([segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]); + } + }); + } + + return closest.indices; +} + export function getElevation( points: (TrackPoint | Waypoint | Coordinates)[], ELEVATION_ZOOM: number = 13,