8 Commits

Author SHA1 Message Date
vcoppe
d3e733aa3e fix wpt colors 2025-12-24 16:34:40 +01:00
vcoppe
a011768d2d computeStatistics compatible with read-only segment 2025-12-24 16:12:44 +01:00
vcoppe
4b45b5d716 update bits-ui 2025-12-24 15:27:44 +01:00
vcoppe
ebe9681c12 avoid creating useless data 2025-12-24 13:31:04 +01:00
vcoppe
51c85e4cd5 fix wpt to segment matching 2025-12-24 13:07:22 +01:00
vcoppe
2e171dfbee speed up wpt to segment matching 2025-12-24 12:43:24 +01:00
vcoppe
a6a3917986 improve statistics tree performance 2025-12-24 12:21:27 +01:00
vcoppe
21f2448213 improve cloning performance 2025-12-24 12:03:36 +01:00
9 changed files with 194 additions and 150 deletions

View File

@@ -16,16 +16,6 @@ import {
} from './types'; } from './types';
import { immerable, isDraft, original, freeze } from 'immer'; import { immerable, isDraft, original, freeze } from 'immer';
function cloneJSON<T>(obj: T): T {
if (obj === undefined) {
return undefined;
}
if (obj === null || typeof obj !== 'object') {
return null;
}
return JSON.parse(JSON.stringify(obj));
}
// An abstract class that groups functions that need to be computed recursively in the GPX file hierarchy // An abstract class that groups functions that need to be computed recursively in the GPX file hierarchy
export abstract class GPXTreeElement<T extends GPXTreeElement<any>> { export abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
_data: { [key: string]: any } = {}; _data: { [key: string]: any } = {};
@@ -148,7 +138,9 @@ export class GPXFile extends GPXTreeNode<Track> {
}, },
}, },
}; };
this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : []; this.wpt = gpx.wpt
? gpx.wpt.map((waypoint, index) => new Waypoint(waypoint, index))
: [];
this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : []; this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : [];
if (gpx.rte && gpx.rte.length > 0) { if (gpx.rte && gpx.rte.length > 0) {
this.trk = this.trk.concat(gpx.rte.map((route) => convertRouteToTrack(route))); this.trk = this.trk.concat(gpx.rte.map((route) => convertRouteToTrack(route)));
@@ -186,9 +178,6 @@ export class GPXFile extends GPXTreeNode<Track> {
segment._data['segmentIndex'] = segmentIndex; segment._data['segmentIndex'] = segmentIndex;
}); });
}); });
this.wpt.forEach((waypoint, waypointIndex) => {
waypoint._data['index'] = waypointIndex;
});
} }
get children(): Array<Track> { get children(): Array<Track> {
@@ -249,12 +238,12 @@ export class GPXFile extends GPXTreeNode<Track> {
clone(): GPXFile { clone(): GPXFile {
return new GPXFile({ return new GPXFile({
attributes: cloneJSON(this.attributes), attributes: structuredClone(this.attributes),
metadata: cloneJSON(this.metadata), metadata: structuredClone(this.metadata),
wpt: this.wpt.map((waypoint) => waypoint.clone()), wpt: this.wpt.map((waypoint) => waypoint.clone()),
trk: this.trk.map((track) => track.clone()), trk: this.trk.map((track) => track.clone()),
rte: [], rte: [],
_data: cloneJSON(this._data), _data: structuredClone(this._data),
}); });
} }
@@ -267,7 +256,7 @@ export class GPXFile extends GPXTreeNode<Track> {
toGPXFileType(exclude: string[] = []): GPXFileType { toGPXFileType(exclude: string[] = []): GPXFileType {
let file: GPXFileType = { let file: GPXFileType = {
attributes: cloneJSON(this.attributes), attributes: structuredClone(this.attributes),
metadata: {}, metadata: {},
wpt: this.wpt.map((wpt) => wpt.toWaypointType(exclude)), wpt: this.wpt.map((wpt) => wpt.toWaypointType(exclude)),
trk: this.trk.map((track) => track.toTrackType(exclude)), trk: this.trk.map((track) => track.toTrackType(exclude)),
@@ -281,10 +270,10 @@ export class GPXFile extends GPXTreeNode<Track> {
file.metadata.desc = this.metadata.desc; file.metadata.desc = this.metadata.desc;
} }
if (this.metadata.author) { if (this.metadata.author) {
file.metadata.author = cloneJSON(this.metadata.author); file.metadata.author = structuredClone(this.metadata.author);
} }
if (this.metadata.link) { if (this.metadata.link) {
file.metadata.link = cloneJSON(this.metadata.link); file.metadata.link = structuredClone(this.metadata.link);
} }
if (this.metadata.time && !exclude.includes('time')) { if (this.metadata.time && !exclude.includes('time')) {
file.metadata.time = this.metadata.time; file.metadata.time = this.metadata.time;
@@ -577,11 +566,11 @@ export class Track extends GPXTreeNode<TrackSegment> {
cmt: this.cmt, cmt: this.cmt,
desc: this.desc, desc: this.desc,
src: this.src, src: this.src,
link: cloneJSON(this.link), link: structuredClone(this.link),
type: this.type, type: this.type,
extensions: cloneJSON(this.extensions), extensions: structuredClone(this.extensions),
trkseg: this.trkseg.map((seg) => seg.clone()), trkseg: this.trkseg.map((seg) => seg.clone()),
_data: cloneJSON(this._data), _data: structuredClone(this._data),
}); });
} }
@@ -807,7 +796,7 @@ export class TrackSegment extends GPXTreeLeaf {
constructor(segment?: (TrackSegmentType & { _data?: any }) | TrackSegment) { constructor(segment?: (TrackSegmentType & { _data?: any }) | TrackSegment) {
super(); super();
if (segment) { if (segment) {
this.trkpt = segment.trkpt.map((point) => new TrackPoint(point)); this.trkpt = segment.trkpt.map((point, index) => new TrackPoint(point, index));
if (segment.hasOwnProperty('_data')) { if (segment.hasOwnProperty('_data')) {
this._data = segment._data; this._data = segment._data;
} }
@@ -819,12 +808,10 @@ export class TrackSegment extends GPXTreeLeaf {
_computeStatistics(): GPXStatistics { _computeStatistics(): GPXStatistics {
let statistics = new GPXStatistics(); let statistics = new GPXStatistics();
statistics.local.points = this.trkpt.map((point) => point); statistics.local.points = this.trkpt.slice(0);
const points = this.trkpt; const points = this.trkpt;
for (let i = 0; i < points.length; i++) { for (let i = 0; i < points.length; i++) {
points[i]._data['index'] = i;
// distance // distance
let dist = 0; let dist = 0;
if (i > 0) { if (i > 0) {
@@ -1100,7 +1087,7 @@ export class TrackSegment extends GPXTreeLeaf {
clone(): TrackSegment { clone(): TrackSegment {
return new TrackSegment({ return new TrackSegment({
trkpt: this.trkpt.map((point) => point.clone()), trkpt: this.trkpt.map((point) => point.clone()),
_data: cloneJSON(this._data), _data: structuredClone(this._data),
}); });
} }
@@ -1226,14 +1213,14 @@ export class TrackSegment extends GPXTreeLeaf {
let trkpt = og.trkpt.map( let trkpt = og.trkpt.map(
(point, i) => (point, i) =>
new TrackPoint({ new TrackPoint({
attributes: cloneJSON(point.attributes), attributes: structuredClone(point.attributes),
ele: point.ele, ele: point.ele,
time: new Date( time: new Date(
newStartTimestamp.getTime() + newStartTimestamp.getTime() +
(originalEndTimestamp.getTime() - og.trkpt[i].time.getTime()) (originalEndTimestamp.getTime() - og.trkpt[i].time.getTime())
), ),
extensions: cloneJSON(point.extensions), extensions: structuredClone(point.extensions),
_data: cloneJSON(point._data), _data: structuredClone(point._data),
}) })
); );
@@ -1317,7 +1304,7 @@ export class TrackPoint {
_data: { [key: string]: any } = {}; _data: { [key: string]: any } = {};
constructor(point: (TrackPointType & { _data?: any }) | TrackPoint) { constructor(point: (TrackPointType & { _data?: any }) | TrackPoint, index?: number) {
this.attributes = point.attributes; this.attributes = point.attributes;
this.ele = point.ele; this.ele = point.ele;
this.time = point.time; this.time = point.time;
@@ -1325,6 +1312,9 @@ export class TrackPoint {
if (point.hasOwnProperty('_data')) { if (point.hasOwnProperty('_data')) {
this._data = point._data; this._data = point._data;
} }
if (index !== undefined) {
this._data.index = index;
}
} }
getCoordinates(): Coordinates { getCoordinates(): Coordinates {
@@ -1468,11 +1458,18 @@ export class TrackPoint {
clone(): TrackPoint { clone(): TrackPoint {
return new TrackPoint({ return new TrackPoint({
attributes: cloneJSON(this.attributes), attributes: {
lat: this.attributes.lat,
lon: this.attributes.lon,
},
ele: this.ele, ele: this.ele,
time: this.time ? new Date(this.time.getTime()) : undefined, time: this.time ? new Date(this.time.getTime()) : undefined,
extensions: cloneJSON(this.extensions), extensions: this.extensions ? structuredClone(this.extensions) : undefined,
_data: cloneJSON(this._data), _data: {
index: this._data?.index,
anchor: this._data?.anchor,
zoom: this._data?.zoom,
},
}); });
} }
} }
@@ -1491,7 +1488,7 @@ export class Waypoint {
type?: string; type?: string;
_data: { [key: string]: any } = {}; _data: { [key: string]: any } = {};
constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint) { constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint, index?: number) {
this.attributes = waypoint.attributes; this.attributes = waypoint.attributes;
this.ele = waypoint.ele; this.ele = waypoint.ele;
this.time = waypoint.time; this.time = waypoint.time;
@@ -1510,6 +1507,9 @@ export class Waypoint {
if (waypoint.hasOwnProperty('_data')) { if (waypoint.hasOwnProperty('_data')) {
this._data = waypoint._data; this._data = waypoint._data;
} }
if (index !== undefined) {
this._data.index = index;
}
} }
getCoordinates(): Coordinates { getCoordinates(): Coordinates {
@@ -1557,13 +1557,16 @@ export class Waypoint {
clone(): Waypoint { clone(): Waypoint {
return new Waypoint({ return new Waypoint({
attributes: cloneJSON(this.attributes), attributes: {
lat: this.attributes.lat,
lon: this.attributes.lon,
},
ele: this.ele, ele: this.ele,
time: this.time ? new Date(this.time.getTime()) : undefined, time: this.time ? new Date(this.time.getTime()) : undefined,
name: this.name, name: this.name,
cmt: this.cmt, cmt: this.cmt,
desc: this.desc, desc: this.desc,
link: cloneJSON(this.link), link: structuredClone(this.link),
sym: this.sym, sym: this.sym,
type: this.type, type: this.type,
}); });

View File

@@ -59,13 +59,13 @@ function ramerDouglasPeuckerRecursive(
} }
export function crossarcDistance( export function crossarcDistance(
point1: TrackPoint, point1: TrackPoint | Coordinates,
point2: TrackPoint, point2: TrackPoint | Coordinates,
point3: TrackPoint | Coordinates point3: TrackPoint | Coordinates
): number { ): number {
return crossarc( return crossarc(
point1.getCoordinates(), point1 instanceof TrackPoint ? point1.getCoordinates() : point1,
point2.getCoordinates(), point2 instanceof TrackPoint ? point2.getCoordinates() : point2,
point3 instanceof TrackPoint ? point3.getCoordinates() : point3 point3 instanceof TrackPoint ? point3.getCoordinates() : point3
); );
} }

View File

@@ -47,7 +47,7 @@
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^8.33.1", "@typescript-eslint/eslint-plugin": "^8.33.1",
"@typescript-eslint/parser": "^8.33.1", "@typescript-eslint/parser": "^8.33.1",
"bits-ui": "^2.12.0", "bits-ui": "^2.14.4",
"eslint": "^9.28.0", "eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.9.1", "eslint-plugin-svelte": "^3.9.1",
@@ -3241,9 +3241,9 @@
] ]
}, },
"node_modules/bits-ui": { "node_modules/bits-ui": {
"version": "2.12.0", "version": "2.14.4",
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.12.0.tgz", "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.14.4.tgz",
"integrity": "sha512-8NF4ILNyAJlIxDXpl/akGXGBV5QmZAe+8gTfPttM5P6/+LrijumcSfFXY5cr4QkXwTmLA7H5stYpbgJf2XFJvg==", "integrity": "sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -31,7 +31,7 @@
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^8.33.1", "@typescript-eslint/eslint-plugin": "^8.33.1",
"@typescript-eslint/parser": "^8.33.1", "@typescript-eslint/parser": "^8.33.1",
"bits-ui": "^2.12.0", "bits-ui": "^2.14.4",
"eslint": "^9.28.0", "eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.9.1", "eslint-plugin-svelte": "^3.9.1",

View File

@@ -428,57 +428,72 @@ export class ElevationProfile {
segment: {}, segment: {},
}; };
this._chart.data.datasets[1] = { this._chart.data.datasets[1] = {
data: data.local.points.map((point, index) => { data:
return { data.global.time.total > 0
x: getConvertedDistance(data.local.distance.total[index]), ? data.local.points.map((point, index) => {
y: getConvertedVelocity(data.local.speed[index]), return {
index: index, x: getConvertedDistance(data.local.distance.total[index]),
}; y: getConvertedVelocity(data.local.speed[index]),
}), index: index,
};
})
: [],
normalized: true, normalized: true,
yAxisID: 'yspeed', yAxisID: 'yspeed',
}; };
this._chart.data.datasets[2] = { this._chart.data.datasets[2] = {
data: data.local.points.map((point, index) => { data:
return { data.global.hr.count > 0
x: getConvertedDistance(data.local.distance.total[index]), ? data.local.points.map((point, index) => {
y: point.getHeartRate(), return {
index: index, x: getConvertedDistance(data.local.distance.total[index]),
}; y: point.getHeartRate(),
}), index: index,
};
})
: [],
normalized: true, normalized: true,
yAxisID: 'yhr', yAxisID: 'yhr',
}; };
this._chart.data.datasets[3] = { this._chart.data.datasets[3] = {
data: data.local.points.map((point, index) => { data:
return { data.global.cad.count > 0
x: getConvertedDistance(data.local.distance.total[index]), ? data.local.points.map((point, index) => {
y: point.getCadence(), return {
index: index, x: getConvertedDistance(data.local.distance.total[index]),
}; y: point.getCadence(),
}), index: index,
};
})
: [],
normalized: true, normalized: true,
yAxisID: 'ycad', yAxisID: 'ycad',
}; };
this._chart.data.datasets[4] = { this._chart.data.datasets[4] = {
data: data.local.points.map((point, index) => { data:
return { data.global.atemp.count > 0
x: getConvertedDistance(data.local.distance.total[index]), ? data.local.points.map((point, index) => {
y: getConvertedTemperature(point.getTemperature()), return {
index: index, x: getConvertedDistance(data.local.distance.total[index]),
}; y: getConvertedTemperature(point.getTemperature()),
}), index: index,
};
})
: [],
normalized: true, normalized: true,
yAxisID: 'yatemp', yAxisID: 'yatemp',
}; };
this._chart.data.datasets[5] = { this._chart.data.datasets[5] = {
data: data.local.points.map((point, index) => { data:
return { data.global.power.count > 0
x: getConvertedDistance(data.local.distance.total[index]), ? data.local.points.map((point, index) => {
y: point.getPower(), return {
index: index, x: getConvertedDistance(data.local.distance.total[index]),
}; y: point.getPower(),
}), index: index,
};
})
: [],
normalized: true, normalized: true,
yAxisID: 'ypower', yAxisID: 'ypower',
}; };

View File

@@ -153,8 +153,6 @@ export class GPXLayer {
return; return;
} }
this.loadIcons();
if ( if (
file._data.style && file._data.style &&
file._data.style.color && file._data.style.color &&
@@ -164,6 +162,8 @@ export class GPXLayer {
this.layerColor = `#${file._data.style.color}`; this.layerColor = `#${file._data.style.color}`;
} }
this.loadIcons();
try { try {
let source = _map.getSource(this.fileId) as mapboxgl.GeoJSONSource | undefined; let source = _map.getSource(this.fileId) as mapboxgl.GeoJSONSource | undefined;
if (source) { if (source) {
@@ -702,7 +702,7 @@ export class GPXLayer {
properties: { properties: {
fileId: this.fileId, fileId: this.fileId,
waypointIndex: index, waypointIndex: index,
icon: `${this.fileId}-waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}`, icon: `waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}-${this.layerColor}`,
}, },
}); });
}); });
@@ -723,7 +723,7 @@ export class GPXLayer {
}); });
symbols.forEach((symbol) => { symbols.forEach((symbol) => {
const iconId = `${this.fileId}-waypoint-${symbol ?? 'default'}`; const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`;
if (!_map.hasImage(iconId)) { if (!_map.hasImage(iconId)) {
let icon = new Image(100, 100); let icon = new Image(100, 100);
icon.onload = () => { icon.onload = () => {

View File

@@ -17,7 +17,6 @@ import {
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { freeze, type WritableDraft } from 'immer'; import { freeze, type WritableDraft } from 'immer';
import { import {
distance,
GPXFile, GPXFile,
parseGPX, parseGPX,
Track, Track,
@@ -30,7 +29,7 @@ import {
} from 'gpx'; } from 'gpx';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { settings } from '$lib/logic/settings'; 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 { gpxStatistics } from '$lib/logic/statistics';
import { boundsManager } from './bounds'; import { boundsManager } from './bounds';
@@ -453,34 +452,13 @@ export const fileActions = {
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
if (level === ListLevel.FILE) { if (level === ListLevel.FILE) {
let file = fileStateCollection.getFile(fileId); let file = fileStateCollection.getFile(fileId);
if (file) { let statistics = fileStateCollection.getStatistics(fileId);
if (file && statistics) {
if (file.trk.length > 1) { if (file.trk.length > 1) {
let fileIds = getFileIds(file.trk.length); let fileIds = getFileIds(file.trk.length);
let closest = file.wpt.map((wpt, wptIndex) => { let closest = file.wpt.map((wpt) =>
return { getClosestTrackSegments(file, statistics, wpt.getCoordinates())
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);
}
});
});
});
});
file.trk.forEach((track, index) => { file.trk.forEach((track, index) => {
let newFile = file.clone(); let newFile = file.clone();
let tracks = track.trkseg.map((segment, segmentIndex) => { let tracks = track.trkseg.map((segment, segmentIndex) => {
@@ -495,9 +473,11 @@ export const fileActions = {
newFile.replaceWaypoints( newFile.replaceWaypoints(
0, 0,
file.wpt.length - 1, file.wpt.length - 1,
closest file.wpt.filter((wpt, wptIndex) =>
.filter((c) => c.index.includes(index)) closest[wptIndex].some(
.map((c) => file.wpt[c.wptIndex]) ([trackIndex, segmentIndex]) => trackIndex === index
)
)
); );
newFile._data.id = fileIds[index]; newFile._data.id = fileIds[index];
newFile.metadata.name = newFile.metadata.name =
@@ -506,29 +486,9 @@ export const fileActions = {
}); });
} else if (file.trk.length === 1) { } else if (file.trk.length === 1) {
let fileIds = getFileIds(file.trk[0].trkseg.length); let fileIds = getFileIds(file.trk[0].trkseg.length);
let closest = file.wpt.map((wpt, wptIndex) => { let closest = file.wpt.map((wpt) =>
return { getClosestTrackSegments(file, statistics, wpt.getCoordinates())
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);
}
});
});
});
file.trk[0].trkseg.forEach((segment, index) => { file.trk[0].trkseg.forEach((segment, index) => {
let newFile = file.clone(); let newFile = file.clone();
newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [ newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [
@@ -537,9 +497,11 @@ export const fileActions = {
newFile.replaceWaypoints( newFile.replaceWaypoints(
0, 0,
file.wpt.length - 1, file.wpt.length - 1,
closest file.wpt.filter((wpt, wptIndex) =>
.filter((c) => c.index.includes(index)) closest[wptIndex].some(
.map((c) => file.wpt[c.wptIndex]) ([trackIndex, segmentIndex]) => segmentIndex === index
)
)
); );
newFile._data.id = fileIds[index]; newFile._data.id = fileIds[index];
newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`; newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`;

View File

@@ -22,25 +22,34 @@ export class GPXStatisticsTree {
} }
getStatisticsFor(item: ListItem): GPXStatistics { getStatisticsFor(item: ListItem): GPXStatistics {
let statistics = new GPXStatistics(); let statistics = [];
let id = item.getIdAtLevel(this.level); let id = item.getIdAtLevel(this.level);
if (id === undefined || id === 'waypoints') { if (id === undefined || id === 'waypoints') {
Object.keys(this.statistics).forEach((key) => { Object.keys(this.statistics).forEach((key) => {
if (this.statistics[key] instanceof GPXStatistics) { if (this.statistics[key] instanceof GPXStatistics) {
statistics.mergeWith(this.statistics[key]); statistics.push(this.statistics[key]);
} else { } else {
statistics.mergeWith(this.statistics[key].getStatisticsFor(item)); statistics.push(this.statistics[key].getStatisticsFor(item));
} }
}); });
} else { } else {
let child = this.statistics[id]; let child = this.statistics[id];
if (child instanceof GPXStatistics) { if (child instanceof GPXStatistics) {
statistics.mergeWith(child); statistics.push(child);
} else if (child !== undefined) { } else if (child !== undefined) {
statistics.mergeWith(child.getStatisticsFor(item)); statistics.push(child.getStatisticsFor(item));
} }
} }
return statistics; if (statistics.length === 0) {
return new GPXStatistics();
} else if (statistics.length === 1) {
return statistics[0];
} else {
return statistics.reduce((acc, curr) => {
acc.mergeWith(curr);
return acc;
}, new GPXStatistics());
}
} }
} }
export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree }; export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree };

View File

@@ -2,11 +2,13 @@ import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { base } from '$app/paths'; import { base } from '$app/paths';
import { languages } from '$lib/languages'; 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 mapboxgl from 'mapbox-gl';
import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt'; import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt';
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public'; import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import PNGReader from 'png.js'; 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[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@@ -47,6 +49,59 @@ export function getClosestLinePoint(
return closest; 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 bounds = new mapboxgl.LngLatBounds(southWest, northEast);
if (bounds.contains(point)) {
segmentBoundsDistances.push([0, trackIndex, segmentIndex]);
} else {
let northWest: Coordinates = { lat: northEast.lat, lon: southWest.lon };
let southEast: Coordinates = { lat: southWest.lat, lon: northEast.lon };
let distanceToBounds = Math.min(
crossarcDistance(northWest, northEast, point),
crossarcDistance(northEast, southEast, point),
crossarcDistance(southEast, southWest, point),
crossarcDistance(southWest, northWest, point)
);
segmentBoundsDistances.push([distanceToBounds, 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( export function getElevation(
points: (TrackPoint | Waypoint | Coordinates)[], points: (TrackPoint | Waypoint | Coordinates)[],
ELEVATION_ZOOM: number = 13, ELEVATION_ZOOM: number = 13,