mirror of
				https://github.com/gpxstudio/gpx.studio.git
				synced 2025-11-04 05:21:09 +00:00 
			
		
		
		
	Merge branch 'main' into docs
syncing from main
This commit is contained in:
		
							
								
								
									
										618
									
								
								gpx/src/gpx.ts
									
									
									
									
									
								
							
							
						
						
									
										618
									
								
								gpx/src/gpx.ts
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
import { ramerDouglasPeucker } from "./simplify";
 | 
			
		||||
import { Coordinates, GPXFileAttributes, GPXFileType, LineStyleExtension, Link, Metadata, TrackExtensions, TrackPointExtensions, TrackPointType, TrackSegmentType, TrackType, WaypointType } from "./types";
 | 
			
		||||
import { Draft, immerable, isDraft, original, produce, freeze } from "immer";
 | 
			
		||||
import { immerable, isDraft, original, freeze } from "immer";
 | 
			
		||||
 | 
			
		||||
function cloneJSON<T>(obj: T): T {
 | 
			
		||||
    if (obj === null || typeof obj !== 'object') {
 | 
			
		||||
@@ -68,33 +68,31 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
 | 
			
		||||
 | 
			
		||||
    // Producers
 | 
			
		||||
    _reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date) {
 | 
			
		||||
        return produce(this, (draft: Draft<GPXTreeNode<T>>) => {
 | 
			
		||||
            let og = getOriginal(draft);
 | 
			
		||||
            if (!originalNextTimestamp && !newPreviousTimestamp) {
 | 
			
		||||
                originalNextTimestamp = og.getEndTimestamp();
 | 
			
		||||
                newPreviousTimestamp = og.getStartTimestamp();
 | 
			
		||||
            }
 | 
			
		||||
        let og = getOriginal(this);
 | 
			
		||||
        if (!originalNextTimestamp && !newPreviousTimestamp) {
 | 
			
		||||
            originalNextTimestamp = og.getEndTimestamp();
 | 
			
		||||
            newPreviousTimestamp = og.getStartTimestamp();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            let children = og.children.slice();
 | 
			
		||||
            children.reverse();
 | 
			
		||||
        let children = og.children.slice();
 | 
			
		||||
        children.reverse();
 | 
			
		||||
 | 
			
		||||
            for (let i = 0; i < og.children.length; i++) {
 | 
			
		||||
                let originalStartTimestamp = og.children[og.children.length - i - 1].getStartTimestamp();
 | 
			
		||||
        for (let i = 0; i < og.children.length; i++) {
 | 
			
		||||
            let originalStartTimestamp = og.children[og.children.length - i - 1].getStartTimestamp();
 | 
			
		||||
 | 
			
		||||
                children[i] = children[i]._reverse(originalNextTimestamp, newPreviousTimestamp);
 | 
			
		||||
            children[i]._reverse(originalNextTimestamp, newPreviousTimestamp);
 | 
			
		||||
 | 
			
		||||
                originalNextTimestamp = originalStartTimestamp;
 | 
			
		||||
                newPreviousTimestamp = children[i].getEndTimestamp();
 | 
			
		||||
            }
 | 
			
		||||
            originalNextTimestamp = originalStartTimestamp;
 | 
			
		||||
            newPreviousTimestamp = children[i].getEndTimestamp();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            if (this instanceof GPXFile) {
 | 
			
		||||
                // @ts-ignore
 | 
			
		||||
                draft.trk = freeze(children);
 | 
			
		||||
            } else if (this instanceof Track) {
 | 
			
		||||
                // @ts-ignore
 | 
			
		||||
                draft.trkseg = freeze(children);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        if (this instanceof GPXFile) {
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            this.trk = freeze(children);
 | 
			
		||||
        } else if (this instanceof Track) {
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            this.trkseg = freeze(children);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -115,15 +113,15 @@ export class GPXFile extends GPXTreeNode<Track>{
 | 
			
		||||
 | 
			
		||||
    attributes: GPXFileAttributes;
 | 
			
		||||
    metadata: Metadata;
 | 
			
		||||
    readonly wpt: ReadonlyArray<Readonly<Waypoint>>;
 | 
			
		||||
    readonly trk: ReadonlyArray<Track>;
 | 
			
		||||
    wpt: Waypoint[];
 | 
			
		||||
    trk: Track[];
 | 
			
		||||
 | 
			
		||||
    constructor(gpx?: GPXFileType & { _data?: any } | GPXFile) {
 | 
			
		||||
        super();
 | 
			
		||||
        if (gpx) {
 | 
			
		||||
            this.attributes = gpx.attributes
 | 
			
		||||
            this.metadata = gpx.metadata;
 | 
			
		||||
            this.wpt = gpx.wpt ? gpx.wpt.map((waypoint, index) => new Waypoint(waypoint, index)) : [];
 | 
			
		||||
            this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : [];
 | 
			
		||||
            this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : [];
 | 
			
		||||
            if (gpx.hasOwnProperty('_data')) {
 | 
			
		||||
                this._data = gpx._data;
 | 
			
		||||
@@ -134,6 +132,17 @@ export class GPXFile extends GPXTreeNode<Track>{
 | 
			
		||||
            this.wpt = [];
 | 
			
		||||
            this.trk = [new Track()];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.trk.forEach((track, trackIndex) => {
 | 
			
		||||
            track._data['trackIndex'] = trackIndex;
 | 
			
		||||
            track.trkseg.forEach((segment, segmentIndex) => {
 | 
			
		||||
                segment._data['trackIndex'] = trackIndex;
 | 
			
		||||
                segment._data['segmentIndex'] = segmentIndex;
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        this.wpt.forEach((waypoint, waypointIndex) => {
 | 
			
		||||
            waypoint._data['index'] = waypointIndex;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get children(): ReadonlyArray<Track> {
 | 
			
		||||
@@ -200,51 +209,23 @@ export class GPXFile extends GPXTreeNode<Track>{
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Producers
 | 
			
		||||
    replaceTracks(start: number, end: number, tracks: Track[]): [GPXFile, Track[]] {
 | 
			
		||||
        let removed = [];
 | 
			
		||||
        let result = produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            if (og._data.style) {
 | 
			
		||||
                tracks = tracks.map((track) => track.setStyle(og._data.style, false));
 | 
			
		||||
            }
 | 
			
		||||
            let trk = og.trk.slice();
 | 
			
		||||
            removed = trk.splice(start, end - start + 1, ...tracks);
 | 
			
		||||
            draft.trk = freeze(trk); // Pre-freeze the array, faster as well
 | 
			
		||||
        });
 | 
			
		||||
        return [result, removed];
 | 
			
		||||
    replaceTracks(start: number, end: number, tracks: Track[]) {
 | 
			
		||||
        if (this._data.style) {
 | 
			
		||||
            tracks.forEach((track) => track.setStyle(this._data.style, false));
 | 
			
		||||
        }
 | 
			
		||||
        this.trk.splice(start, end - start + 1, ...tracks);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    replaceTrackSegments(trackIndex: number, start: number, end: number, segments: TrackSegment[]): [GPXFile, TrackSegment[]] {
 | 
			
		||||
        let removed = [];
 | 
			
		||||
        let result = produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trk = og.trk.slice();
 | 
			
		||||
            let [result, rmv] = trk[trackIndex].replaceTrackSegments(start, end, segments);
 | 
			
		||||
            trk[trackIndex] = result;
 | 
			
		||||
            removed = rmv;
 | 
			
		||||
            draft.trk = freeze(trk); // Pre-freeze the array, faster as well
 | 
			
		||||
        });
 | 
			
		||||
        return [result, removed];
 | 
			
		||||
    replaceTrackSegments(trackIndex: number, start: number, end: number, segments: TrackSegment[]) {
 | 
			
		||||
        this.trk[trackIndex].replaceTrackSegments(start, end, segments);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    replaceTrackPoints(trackIndex: number, segmentIndex: number, start: number, end: number, points: TrackPoint[], speed?: number, startTime?: Date) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trk = og.trk.slice();
 | 
			
		||||
            trk[trackIndex] = trk[trackIndex].replaceTrackPoints(segmentIndex, start, end, points, speed, startTime);
 | 
			
		||||
            draft.trk = freeze(trk); // Pre-freeze the array, faster as well
 | 
			
		||||
        });
 | 
			
		||||
        this.trk[trackIndex].replaceTrackPoints(segmentIndex, start, end, points, speed, startTime);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    replaceWaypoints(start: number, end: number, waypoints: Waypoint[]): [GPXFile, Waypoint[]] {
 | 
			
		||||
        let removed = [];
 | 
			
		||||
        let result = produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let wpt = og.wpt.slice();
 | 
			
		||||
            removed = wpt.splice(start, end - start + 1, ...waypoints);
 | 
			
		||||
            draft.wpt = freeze(wpt); // Pre-freeze the array, faster as well
 | 
			
		||||
        });
 | 
			
		||||
        return [result, removed];
 | 
			
		||||
    replaceWaypoints(start: number, end: number, waypoints: Waypoint[]) {
 | 
			
		||||
        this.wpt.splice(start, end - start + 1, ...waypoints);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    reverse() {
 | 
			
		||||
@@ -252,120 +233,136 @@ export class GPXFile extends GPXTreeNode<Track>{
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    reverseTrack(trackIndex: number) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trk = og.trk.slice();
 | 
			
		||||
            trk[trackIndex] = trk[trackIndex]._reverse();
 | 
			
		||||
            draft.trk = freeze(trk); // Pre-freeze the array, faster as well
 | 
			
		||||
        });
 | 
			
		||||
        this.trk[trackIndex]._reverse();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    reverseTrackSegment(trackIndex: number, segmentIndex: number) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trk = og.trk.slice();
 | 
			
		||||
            trk[trackIndex] = trk[trackIndex].reverseTrackSegment(segmentIndex);
 | 
			
		||||
            draft.trk = freeze(trk); // Pre-freeze the array, faster as well
 | 
			
		||||
        });
 | 
			
		||||
        this.trk[trackIndex].reverseTrackSegment(segmentIndex);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    crop(start: number, end: number, trackIndices?: number[], segmentIndices?: number[]) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trk = og.trk.slice();
 | 
			
		||||
        let i = 0;
 | 
			
		||||
        let trackIndex = 0;
 | 
			
		||||
        while (i < this.trk.length) {
 | 
			
		||||
            let length = this.trk[i].getNumberOfTrackPoints();
 | 
			
		||||
            if (trackIndices === undefined || trackIndices.includes(trackIndex)) {
 | 
			
		||||
                if (start >= length || end < 0) {
 | 
			
		||||
                    this.trk.splice(i, 1);
 | 
			
		||||
                } else {
 | 
			
		||||
                    if (start > 0 || end < length - 1) {
 | 
			
		||||
                        this.trk[i].crop(Math.max(0, start), Math.min(length - 1, end), segmentIndices);
 | 
			
		||||
                    }
 | 
			
		||||
                    i++;
 | 
			
		||||
                }
 | 
			
		||||
                start -= length;
 | 
			
		||||
                end -= length;
 | 
			
		||||
            } else {
 | 
			
		||||
                i++;
 | 
			
		||||
            }
 | 
			
		||||
            trackIndex++;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    clean(bounds: [Coordinates, Coordinates], inside: boolean, deleteTrackPoints: boolean, deleteWaypoints: boolean, trackIndices?: number[], segmentIndices?: number[], waypointIndices?: number[]) {
 | 
			
		||||
        if (deleteTrackPoints) {
 | 
			
		||||
            let i = 0;
 | 
			
		||||
            let trackIndex = 0;
 | 
			
		||||
            while (i < trk.length) {
 | 
			
		||||
                let length = trk[i].getNumberOfTrackPoints();
 | 
			
		||||
            while (i < this.trk.length) {
 | 
			
		||||
                if (trackIndices === undefined || trackIndices.includes(trackIndex)) {
 | 
			
		||||
                    if (start >= length || end < 0) {
 | 
			
		||||
                        trk.splice(i, 1);
 | 
			
		||||
                    this.trk[i].clean(bounds, inside, segmentIndices);
 | 
			
		||||
                    if (this.trk[i].getNumberOfTrackPoints() === 0) {
 | 
			
		||||
                        this.trk.splice(i, 1);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        if (start > 0 || end < length - 1) {
 | 
			
		||||
                            trk[i] = trk[i].crop(Math.max(0, start), Math.min(length - 1, end), segmentIndices);
 | 
			
		||||
                        }
 | 
			
		||||
                        i++;
 | 
			
		||||
                    }
 | 
			
		||||
                    start -= length;
 | 
			
		||||
                    end -= length;
 | 
			
		||||
                } else {
 | 
			
		||||
                    i++;
 | 
			
		||||
                }
 | 
			
		||||
                trackIndex++;
 | 
			
		||||
            }
 | 
			
		||||
            draft.trk = freeze(trk); // Pre-freeze the array, faster as well
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    clean(bounds: [Coordinates, Coordinates], inside: boolean, deleteTrackPoints: boolean, deleteWaypoints: boolean, trackIndices?: number[], segmentIndices?: number[], waypointIndices?: number[]) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            if (deleteTrackPoints) {
 | 
			
		||||
                let trk = og.trk.slice();
 | 
			
		||||
                let i = 0;
 | 
			
		||||
                let trackIndex = 0;
 | 
			
		||||
                while (i < trk.length) {
 | 
			
		||||
                    if (trackIndices === undefined || trackIndices.includes(trackIndex)) {
 | 
			
		||||
                        trk[i] = trk[i].clean(bounds, inside, segmentIndices);
 | 
			
		||||
                        if (trk[i].getNumberOfTrackPoints() === 0) {
 | 
			
		||||
                            trk.splice(i, 1);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            i++;
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        i++;
 | 
			
		||||
                    }
 | 
			
		||||
                    trackIndex++;
 | 
			
		||||
        }
 | 
			
		||||
        if (deleteWaypoints) {
 | 
			
		||||
            let og = getOriginal(this); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let wpt = og.wpt.filter((point, waypointIndex) => {
 | 
			
		||||
                if (waypointIndices === undefined || waypointIndices.includes(waypointIndex)) {
 | 
			
		||||
                    let inBounds = point.attributes.lat >= bounds[0].lat && point.attributes.lat <= bounds[1].lat && point.attributes.lon >= bounds[0].lon && point.attributes.lon <= bounds[1].lon;
 | 
			
		||||
                    return inBounds !== inside;
 | 
			
		||||
                } else {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                draft.trk = freeze(trk); // Pre-freeze the array, faster as well
 | 
			
		||||
            }
 | 
			
		||||
            if (deleteWaypoints) {
 | 
			
		||||
                let wpt = og.wpt.filter((point, waypointIndex) => {
 | 
			
		||||
                    if (waypointIndices === undefined || waypointIndices.includes(waypointIndex)) {
 | 
			
		||||
                        let inBounds = point.attributes.lat >= bounds[0].lat && point.attributes.lat <= bounds[1].lat && point.attributes.lon >= bounds[0].lon && point.attributes.lon <= bounds[1].lon;
 | 
			
		||||
                        return inBounds !== inside;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        return true;
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
                draft.wpt = freeze(wpt); // Pre-freeze the array, faster as well
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
            });
 | 
			
		||||
            this.wpt = freeze(wpt); // Pre-freeze the array, faster as well
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    changeTimestamps(startTime: Date, speed: number, ratio: number, trackIndex?: number, segmentIndex?: number) {
 | 
			
		||||
        let lastPoint = undefined;
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trk = og.trk.map((track, index) => {
 | 
			
		||||
                if (trackIndex === undefined || trackIndex === index) {
 | 
			
		||||
                    return track.changeTimestamps(startTime, speed, ratio, lastPoint, segmentIndex);
 | 
			
		||||
                } else {
 | 
			
		||||
                    return track;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            draft.trk = freeze(trk); // Pre-freeze the array, faster as well
 | 
			
		||||
        this.trk.forEach((track, index) => {
 | 
			
		||||
            if (trackIndex === undefined || trackIndex === index) {
 | 
			
		||||
                track.changeTimestamps(startTime, speed, ratio, lastPoint, segmentIndex);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setStyle(style: LineStyleExtension) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trk = og.trk.map((track) => track.setStyle(style));
 | 
			
		||||
            draft.trk = freeze(trk); // Pre-freeze the array, faster as well
 | 
			
		||||
            if (!draft._data.style) {
 | 
			
		||||
                draft._data.style = {};
 | 
			
		||||
            }
 | 
			
		||||
            if (style.color) {
 | 
			
		||||
                draft._data.style.color = style.color;
 | 
			
		||||
            }
 | 
			
		||||
            if (style.opacity) {
 | 
			
		||||
                draft._data.style.opacity = style.opacity;
 | 
			
		||||
            }
 | 
			
		||||
            if (style.weight) {
 | 
			
		||||
                draft._data.style.weight = style.weight;
 | 
			
		||||
        this.trk.forEach((track) => {
 | 
			
		||||
            track.setStyle(style);
 | 
			
		||||
        });
 | 
			
		||||
        if (!this._data.style) {
 | 
			
		||||
            this._data.style = {};
 | 
			
		||||
        }
 | 
			
		||||
        if (style.color) {
 | 
			
		||||
            this._data.style.color = style.color;
 | 
			
		||||
        }
 | 
			
		||||
        if (style.opacity) {
 | 
			
		||||
            this._data.style.opacity = style.opacity;
 | 
			
		||||
        }
 | 
			
		||||
        if (style.weight) {
 | 
			
		||||
            this._data.style.weight = style.weight;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setHidden(hidden: boolean, trackIndices?: number[], segmentIndices?: number[]) {
 | 
			
		||||
        let allHidden = hidden;
 | 
			
		||||
        this.trk.forEach((track, index) => {
 | 
			
		||||
            if (trackIndices === undefined || trackIndices.includes(index)) {
 | 
			
		||||
                track.setHidden(hidden, segmentIndices);
 | 
			
		||||
            } else {
 | 
			
		||||
                allHidden = allHidden && (track._data.hidden === true);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        this.wpt.forEach((waypoint) => {
 | 
			
		||||
            if (trackIndices === undefined && segmentIndices === undefined) {
 | 
			
		||||
                waypoint.setHidden(hidden);
 | 
			
		||||
            } else {
 | 
			
		||||
                allHidden = allHidden && (waypoint._data.hidden === true);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (trackIndices === undefined && segmentIndices === undefined) {
 | 
			
		||||
            this._data.hiddenWpt = hidden;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this._data.hidden = allHidden;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setHiddenWaypoints(hidden: boolean, waypointIndices?: number[]) {
 | 
			
		||||
        let allHiddenWpt = hidden;
 | 
			
		||||
        this.wpt.forEach((waypoint, index) => {
 | 
			
		||||
            if (waypointIndices === undefined || waypointIndices.includes(index)) {
 | 
			
		||||
                waypoint.setHidden(hidden);
 | 
			
		||||
            } else {
 | 
			
		||||
                allHiddenWpt = allHiddenWpt && (waypoint._data.hidden === true);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let allHiddenTrk = true;
 | 
			
		||||
        this.trk.forEach((track) => {
 | 
			
		||||
            allHiddenTrk = allHiddenTrk && (track._data.hidden === true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this._data.hiddenWpt = allHiddenWpt;
 | 
			
		||||
        this._data.hidden = allHiddenTrk && allHiddenWpt;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -373,14 +370,14 @@ export class GPXFile extends GPXTreeNode<Track>{
 | 
			
		||||
export class Track extends GPXTreeNode<TrackSegment> {
 | 
			
		||||
    [immerable] = true;
 | 
			
		||||
 | 
			
		||||
    readonly name?: string;
 | 
			
		||||
    readonly cmt?: string;
 | 
			
		||||
    readonly desc?: string;
 | 
			
		||||
    readonly src?: string;
 | 
			
		||||
    readonly link?: Link;
 | 
			
		||||
    readonly type?: string;
 | 
			
		||||
    readonly trkseg: ReadonlyArray<TrackSegment>;
 | 
			
		||||
    readonly extensions?: TrackExtensions;
 | 
			
		||||
    name?: string;
 | 
			
		||||
    cmt?: string;
 | 
			
		||||
    desc?: string;
 | 
			
		||||
    src?: string;
 | 
			
		||||
    link?: Link;
 | 
			
		||||
    type?: string;
 | 
			
		||||
    trkseg: TrackSegment[];
 | 
			
		||||
    extensions?: TrackExtensions;
 | 
			
		||||
 | 
			
		||||
    constructor(track?: TrackType & { _data?: any } | Track) {
 | 
			
		||||
        super();
 | 
			
		||||
@@ -455,131 +452,106 @@ export class Track extends GPXTreeNode<TrackSegment> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Producers
 | 
			
		||||
    replaceTrackSegments(start: number, end: number, segments: TrackSegment[]): [Track, TrackSegment[]] {
 | 
			
		||||
        let removed = [];
 | 
			
		||||
        let result = produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trkseg = og.trkseg.slice();
 | 
			
		||||
            removed = trkseg.splice(start, end - start + 1, ...segments);
 | 
			
		||||
            draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
 | 
			
		||||
        });
 | 
			
		||||
        return [result, removed];
 | 
			
		||||
    replaceTrackSegments(start: number, end: number, segments: TrackSegment[]) {
 | 
			
		||||
        this.trkseg.splice(start, end - start + 1, ...segments);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    replaceTrackPoints(segmentIndex: number, start: number, end: number, points: TrackPoint[], speed?: number, startTime?: Date) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trkseg = og.trkseg.slice();
 | 
			
		||||
            trkseg[segmentIndex] = trkseg[segmentIndex].replaceTrackPoints(start, end, points, speed, startTime);
 | 
			
		||||
            draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
 | 
			
		||||
        });
 | 
			
		||||
        this.trkseg[segmentIndex].replaceTrackPoints(start, end, points, speed, startTime);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    reverseTrackSegment(segmentIndex: number) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trkseg = og.trkseg.slice();
 | 
			
		||||
            trkseg[segmentIndex] = trkseg[segmentIndex]._reverse(trkseg[segmentIndex].getEndTimestamp(), trkseg[segmentIndex].getStartTimestamp());
 | 
			
		||||
            draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
 | 
			
		||||
        });
 | 
			
		||||
        this.trkseg[segmentIndex]._reverse(this.trkseg[segmentIndex].getEndTimestamp(), this.trkseg[segmentIndex].getStartTimestamp());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    crop(start: number, end: number, segmentIndices?: number[]) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trkseg = og.trkseg.slice();
 | 
			
		||||
            let i = 0;
 | 
			
		||||
            let segmentIndex = 0;
 | 
			
		||||
            while (i < trkseg.length) {
 | 
			
		||||
                let length = trkseg[i].getNumberOfTrackPoints();
 | 
			
		||||
                if (segmentIndices === undefined || segmentIndices.includes(segmentIndex)) {
 | 
			
		||||
                    if (start >= length || end < 0) {
 | 
			
		||||
                        trkseg.splice(i, 1);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        if (start > 0 || end < length - 1) {
 | 
			
		||||
                            trkseg[i] = trkseg[i].crop(Math.max(0, start), Math.min(length - 1, end));
 | 
			
		||||
                        }
 | 
			
		||||
                        i++;
 | 
			
		||||
                    }
 | 
			
		||||
                    start -= length;
 | 
			
		||||
                    end -= length;
 | 
			
		||||
        let i = 0;
 | 
			
		||||
        let segmentIndex = 0;
 | 
			
		||||
        while (i < this.trkseg.length) {
 | 
			
		||||
            let length = this.trkseg[i].getNumberOfTrackPoints();
 | 
			
		||||
            if (segmentIndices === undefined || segmentIndices.includes(segmentIndex)) {
 | 
			
		||||
                if (start >= length || end < 0) {
 | 
			
		||||
                    this.trkseg.splice(i, 1);
 | 
			
		||||
                } else {
 | 
			
		||||
                    if (start > 0 || end < length - 1) {
 | 
			
		||||
                        this.trkseg[i].crop(Math.max(0, start), Math.min(length - 1, end));
 | 
			
		||||
                    }
 | 
			
		||||
                    i++;
 | 
			
		||||
                }
 | 
			
		||||
                segmentIndex++;
 | 
			
		||||
                start -= length;
 | 
			
		||||
                end -= length;
 | 
			
		||||
            } else {
 | 
			
		||||
                i++;
 | 
			
		||||
            }
 | 
			
		||||
            draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
 | 
			
		||||
        });
 | 
			
		||||
            segmentIndex++;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    clean(bounds: [Coordinates, Coordinates], inside: boolean, segmentIndices?: number[]) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trkseg = og.trkseg.slice();
 | 
			
		||||
            let i = 0;
 | 
			
		||||
            let segmentIndex = 0;
 | 
			
		||||
            while (i < trkseg.length) {
 | 
			
		||||
                if (segmentIndices === undefined || segmentIndices.includes(segmentIndex)) {
 | 
			
		||||
                    trkseg[i] = trkseg[i].clean(bounds, inside);
 | 
			
		||||
                    if (trkseg[i].getNumberOfTrackPoints() === 0) {
 | 
			
		||||
                        trkseg.splice(i, 1);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        i++;
 | 
			
		||||
                    }
 | 
			
		||||
        let i = 0;
 | 
			
		||||
        let segmentIndex = 0;
 | 
			
		||||
        while (i < this.trkseg.length) {
 | 
			
		||||
            if (segmentIndices === undefined || segmentIndices.includes(segmentIndex)) {
 | 
			
		||||
                this.trkseg[i].clean(bounds, inside);
 | 
			
		||||
                if (this.trkseg[i].getNumberOfTrackPoints() === 0) {
 | 
			
		||||
                    this.trkseg.splice(i, 1);
 | 
			
		||||
                } else {
 | 
			
		||||
                    i++;
 | 
			
		||||
                }
 | 
			
		||||
                segmentIndex++;
 | 
			
		||||
            } else {
 | 
			
		||||
                i++;
 | 
			
		||||
            }
 | 
			
		||||
            draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
 | 
			
		||||
        });
 | 
			
		||||
            segmentIndex++;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    changeTimestamps(startTime: Date, speed: number, ratio: number, lastPoint?: TrackPoint, segmentIndex?: number) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trkseg = og.trkseg.slice();
 | 
			
		||||
            trkseg = trkseg.map((segment, index) => {
 | 
			
		||||
                if (segmentIndex === undefined || segmentIndex === index) {
 | 
			
		||||
                    let seg = segment.changeTimestamps(startTime, speed, ratio, lastPoint);
 | 
			
		||||
                    if (seg.trkpt.length > 0) {
 | 
			
		||||
                        lastPoint = seg.trkpt[seg.trkpt.length - 1];
 | 
			
		||||
                    }
 | 
			
		||||
                    return seg;
 | 
			
		||||
                } else {
 | 
			
		||||
                    return segment;
 | 
			
		||||
        this.trkseg.forEach((segment, index) => {
 | 
			
		||||
            if (segmentIndex === undefined || segmentIndex === index) {
 | 
			
		||||
                segment.changeTimestamps(startTime, speed, ratio, lastPoint);
 | 
			
		||||
                if (segment.trkpt.length > 0) {
 | 
			
		||||
                    lastPoint = segment.trkpt[segment.trkpt.length - 1];
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setStyle(style: LineStyleExtension, force: boolean = true) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            if (!draft.extensions) {
 | 
			
		||||
                draft.extensions = {};
 | 
			
		||||
            }
 | 
			
		||||
            if (!draft.extensions['gpx_style:line']) {
 | 
			
		||||
                draft.extensions['gpx_style:line'] = {};
 | 
			
		||||
            }
 | 
			
		||||
            if (style.color !== undefined && (force || draft.extensions['gpx_style:line'].color === undefined)) {
 | 
			
		||||
                draft.extensions['gpx_style:line'].color = style.color;
 | 
			
		||||
            }
 | 
			
		||||
            if (style.opacity !== undefined && (force || draft.extensions['gpx_style:line'].opacity === undefined)) {
 | 
			
		||||
                draft.extensions['gpx_style:line'].opacity = style.opacity;
 | 
			
		||||
            }
 | 
			
		||||
            if (style.weight !== undefined && (force || draft.extensions['gpx_style:line'].weight === undefined)) {
 | 
			
		||||
                draft.extensions['gpx_style:line'].weight = style.weight;
 | 
			
		||||
        if (!this.extensions) {
 | 
			
		||||
            this.extensions = {};
 | 
			
		||||
        }
 | 
			
		||||
        if (!this.extensions['gpx_style:line']) {
 | 
			
		||||
            this.extensions['gpx_style:line'] = {};
 | 
			
		||||
        }
 | 
			
		||||
        if (style.color !== undefined && (force || this.extensions['gpx_style:line'].color === undefined)) {
 | 
			
		||||
            this.extensions['gpx_style:line'].color = style.color;
 | 
			
		||||
        }
 | 
			
		||||
        if (style.opacity !== undefined && (force || this.extensions['gpx_style:line'].opacity === undefined)) {
 | 
			
		||||
            this.extensions['gpx_style:line'].opacity = style.opacity;
 | 
			
		||||
        }
 | 
			
		||||
        if (style.weight !== undefined && (force || this.extensions['gpx_style:line'].weight === undefined)) {
 | 
			
		||||
            this.extensions['gpx_style:line'].weight = style.weight;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setHidden(hidden: boolean, segmentIndices?: number[]) {
 | 
			
		||||
        let allHidden = hidden;
 | 
			
		||||
        this.trkseg.forEach((segment, index) => {
 | 
			
		||||
            if (segmentIndices === undefined || segmentIndices.includes(index)) {
 | 
			
		||||
                segment.setHidden(hidden);
 | 
			
		||||
            } else {
 | 
			
		||||
                allHidden = allHidden && (segment._data.hidden === true);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        this._data.hidden = allHidden;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// A class that represents a TrackSegment in a GPX file
 | 
			
		||||
export class TrackSegment extends GPXTreeLeaf {
 | 
			
		||||
    [immerable] = true;
 | 
			
		||||
 | 
			
		||||
    readonly trkpt: ReadonlyArray<Readonly<TrackPoint>>;
 | 
			
		||||
    trkpt: TrackPoint[];
 | 
			
		||||
 | 
			
		||||
    constructor(segment?: TrackSegmentType & { _data?: any } | TrackSegment) {
 | 
			
		||||
        super();
 | 
			
		||||
@@ -786,83 +758,79 @@ export class TrackSegment extends GPXTreeLeaf {
 | 
			
		||||
 | 
			
		||||
    // Producers
 | 
			
		||||
    replaceTrackPoints(start: number, end: number, points: TrackPoint[], speed?: number, startTime?: Date) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trkpt = og.trkpt.slice();
 | 
			
		||||
        let og = getOriginal(this); // Read as much as possible from the original object because it is faster
 | 
			
		||||
        let trkpt = og.trkpt.slice();
 | 
			
		||||
 | 
			
		||||
            if (speed !== undefined || (trkpt.length > 0 && trkpt[0].time !== undefined)) {
 | 
			
		||||
                if (start > 0 && trkpt[0].time === undefined) {
 | 
			
		||||
                    trkpt.splice(0, 0, withTimestamps(trkpt.splice(0, start), speed, undefined, startTime));
 | 
			
		||||
                }
 | 
			
		||||
                if (points.length > 0) {
 | 
			
		||||
                    let last = start > 0 ? trkpt[start - 1] : undefined;
 | 
			
		||||
                    if (points[0].time === undefined || (points.length > 1 && points[1].time === undefined)) {
 | 
			
		||||
                        points = withTimestamps(points, speed, last, startTime);
 | 
			
		||||
                    } else if (last !== undefined && points[0].time < last.time) {
 | 
			
		||||
                        points = withShiftedAndCompressedTimestamps(points, speed, 1, last);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (end < trkpt.length - 1) {
 | 
			
		||||
                    let last = points.length > 0 ? points[points.length - 1] : start > 0 ? trkpt[start - 1] : undefined;
 | 
			
		||||
                    if (trkpt[end + 1].time === undefined) {
 | 
			
		||||
                        trkpt.splice(end + 1, 0, withTimestamps(trkpt.splice(end + 1), speed, last, startTime));
 | 
			
		||||
                    } else if (last !== undefined && trkpt[end + 1].time < last.time) {
 | 
			
		||||
                        points = withShiftedAndCompressedTimestamps(points, speed, 1, last);
 | 
			
		||||
                    }
 | 
			
		||||
        if (speed !== undefined || (trkpt.length > 0 && trkpt[0].time !== undefined)) {
 | 
			
		||||
            if (start > 0 && trkpt[0].time === undefined) {
 | 
			
		||||
                trkpt.splice(0, 0, ...withTimestamps(trkpt.splice(0, start), speed, undefined, startTime));
 | 
			
		||||
            }
 | 
			
		||||
            if (points.length > 0) {
 | 
			
		||||
                let last = start > 0 ? trkpt[start - 1] : undefined;
 | 
			
		||||
                if (points[0].time === undefined || (points.length > 1 && points[1].time === undefined)) {
 | 
			
		||||
                    points = withTimestamps(points, speed, last, startTime);
 | 
			
		||||
                } else if (last !== undefined && points[0].time < last.time) {
 | 
			
		||||
                    points = withShiftedAndCompressedTimestamps(points, speed, 1, last);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (end < trkpt.length - 1) {
 | 
			
		||||
                let last = points.length > 0 ? points[points.length - 1] : start > 0 ? trkpt[start - 1] : undefined;
 | 
			
		||||
                if (trkpt[end + 1].time === undefined) {
 | 
			
		||||
                    trkpt.splice(end + 1, 0, ...withTimestamps(trkpt.splice(end + 1), speed, last, startTime));
 | 
			
		||||
                } else if (last !== undefined && trkpt[end + 1].time < last.time) {
 | 
			
		||||
                    points = withShiftedAndCompressedTimestamps(points, speed, 1, last);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            trkpt.splice(start, end - start + 1, ...points);
 | 
			
		||||
            draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
 | 
			
		||||
        });
 | 
			
		||||
        trkpt.splice(start, end - start + 1, ...points);
 | 
			
		||||
        this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            if (originalNextTimestamp !== undefined && newPreviousTimestamp !== undefined) {
 | 
			
		||||
                let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
        let og = getOriginal(this); // Read as much as possible from the original object because it is faster
 | 
			
		||||
        let originalStartTimestamp = og.getStartTimestamp();
 | 
			
		||||
        let originalEndTimestamp = og.getEndTimestamp();
 | 
			
		||||
        if (!newPreviousTimestamp) {
 | 
			
		||||
            newPreviousTimestamp = originalStartTimestamp;
 | 
			
		||||
        }
 | 
			
		||||
        if (newPreviousTimestamp && originalEndTimestamp && !originalNextTimestamp) {
 | 
			
		||||
            originalNextTimestamp = new Date(newPreviousTimestamp.getTime() + originalEndTimestamp.getTime() - originalStartTimestamp.getTime());
 | 
			
		||||
        }
 | 
			
		||||
        if (originalNextTimestamp !== undefined && newPreviousTimestamp !== undefined && originalEndTimestamp !== undefined) {
 | 
			
		||||
            let newStartTimestamp = new Date(
 | 
			
		||||
                newPreviousTimestamp.getTime() + originalNextTimestamp.getTime() - originalEndTimestamp.getTime()
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
                let originalEndTimestamp = og.getEndTimestamp();
 | 
			
		||||
                let newStartTimestamp = new Date(
 | 
			
		||||
                    newPreviousTimestamp.getTime() + originalNextTimestamp.getTime() - originalEndTimestamp.getTime()
 | 
			
		||||
                );
 | 
			
		||||
            let trkpt = og.trkpt.map((point, i) => new TrackPoint({
 | 
			
		||||
                attributes: cloneJSON(point.attributes),
 | 
			
		||||
                ele: point.ele,
 | 
			
		||||
                time: new Date(
 | 
			
		||||
                    newStartTimestamp.getTime() + (originalEndTimestamp.getTime() - og.trkpt[i].time.getTime())
 | 
			
		||||
                ),
 | 
			
		||||
                extensions: cloneJSON(point.extensions),
 | 
			
		||||
                _data: cloneJSON(point._data),
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
                let trkpt = og.trkpt.map((point, i) => new TrackPoint({
 | 
			
		||||
                    attributes: cloneJSON(point.attributes),
 | 
			
		||||
                    ele: point.ele,
 | 
			
		||||
                    time: new Date(
 | 
			
		||||
                        newStartTimestamp.getTime() + (originalEndTimestamp.getTime() - og.trkpt[i].time.getTime())
 | 
			
		||||
                    ),
 | 
			
		||||
                    extensions: cloneJSON(point.extensions),
 | 
			
		||||
                    _data: cloneJSON(point._data),
 | 
			
		||||
                }));
 | 
			
		||||
            trkpt.reverse();
 | 
			
		||||
 | 
			
		||||
                trkpt.reverse();
 | 
			
		||||
 | 
			
		||||
                draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
 | 
			
		||||
            } else {
 | 
			
		||||
                draft.trkpt.reverse();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
            this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
 | 
			
		||||
        } else {
 | 
			
		||||
            this.trkpt.reverse();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    crop(start: number, end: number) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trkpt = og.trkpt.slice(start, end + 1);
 | 
			
		||||
            draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
 | 
			
		||||
        });
 | 
			
		||||
        this.trkpt = this.trkpt.slice(start, end + 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    clean(bounds: [Coordinates, Coordinates], inside: boolean) {
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            let trkpt = og.trkpt.filter((point) => {
 | 
			
		||||
                let inBounds = point.attributes.lat >= bounds[0].lat && point.attributes.lat <= bounds[1].lat && point.attributes.lon >= bounds[0].lon && point.attributes.lon <= bounds[1].lon;
 | 
			
		||||
                return inBounds !== inside;
 | 
			
		||||
            });
 | 
			
		||||
            draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
 | 
			
		||||
        let og = getOriginal(this); // Read as much as possible from the original object because it is faster
 | 
			
		||||
        let trkpt = og.trkpt.filter((point) => {
 | 
			
		||||
            let inBounds = point.attributes.lat >= bounds[0].lat && point.attributes.lat <= bounds[1].lat && point.attributes.lon >= bounds[0].lon && point.attributes.lon <= bounds[1].lon;
 | 
			
		||||
            return inBounds !== inside;
 | 
			
		||||
        });
 | 
			
		||||
        this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    changeTimestamps(startTime: Date, speed: number, ratio: number, lastPoint?: TrackPoint) {
 | 
			
		||||
@@ -870,16 +838,18 @@ export class TrackSegment extends GPXTreeLeaf {
 | 
			
		||||
            lastPoint = this.trkpt[0].clone();
 | 
			
		||||
            lastPoint.time = startTime;
 | 
			
		||||
        }
 | 
			
		||||
        return produce(this, (draft) => {
 | 
			
		||||
            let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
 | 
			
		||||
            if (og.trkpt.length > 0 && og.trkpt[0].time === undefined) {
 | 
			
		||||
                let trkpt = withTimestamps(og.trkpt, speed, lastPoint, startTime);
 | 
			
		||||
                draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
 | 
			
		||||
            } else {
 | 
			
		||||
                let trkpt = withShiftedAndCompressedTimestamps(og.trkpt, speed, ratio, lastPoint);
 | 
			
		||||
                draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let og = getOriginal(this); // Read as much as possible from the original object because it is faster
 | 
			
		||||
        if (og.trkpt.length > 0 && og.trkpt[0].time === undefined) {
 | 
			
		||||
            let trkpt = withTimestamps(og.trkpt, speed, lastPoint, startTime);
 | 
			
		||||
            this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
 | 
			
		||||
        } else {
 | 
			
		||||
            let trkpt = withShiftedAndCompressedTimestamps(og.trkpt, speed, ratio, lastPoint);
 | 
			
		||||
            this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    setHidden(hidden: boolean) {
 | 
			
		||||
        this._data.hidden = hidden;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -985,7 +955,7 @@ export class Waypoint {
 | 
			
		||||
    type?: string;
 | 
			
		||||
    _data: { [key: string]: any } = {};
 | 
			
		||||
 | 
			
		||||
    constructor(waypoint: WaypointType & { _data?: any } | Waypoint, index?: number) {
 | 
			
		||||
    constructor(waypoint: WaypointType & { _data?: any } | Waypoint) {
 | 
			
		||||
        this.attributes = waypoint.attributes;
 | 
			
		||||
        this.ele = waypoint.ele;
 | 
			
		||||
        this.time = waypoint.time;
 | 
			
		||||
@@ -998,9 +968,6 @@ export class Waypoint {
 | 
			
		||||
        if (waypoint.hasOwnProperty('_data')) {
 | 
			
		||||
            this._data = waypoint._data;
 | 
			
		||||
        }
 | 
			
		||||
        if (index !== undefined) {
 | 
			
		||||
            this._data['index'] = index;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getCoordinates(): Coordinates {
 | 
			
		||||
@@ -1032,6 +999,11 @@ export class Waypoint {
 | 
			
		||||
            type: this.type,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Producers
 | 
			
		||||
    setHidden(hidden: boolean) {
 | 
			
		||||
        this._data.hidden = hidden;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class GPXStatistics {
 | 
			
		||||
@@ -1213,7 +1185,7 @@ export function distance(coord1: Coordinates, coord2: Coordinates): number {
 | 
			
		||||
    return maxMeters;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function distanceWindowSmoothing(points: ReadonlyArray<Readonly<TrackPoint>>, distanceWindow: number, accumulate: (index: number) => number, compute: (accumulated: number, start: number, end: number) => number, remove?: (index: number) => number): number[] {
 | 
			
		||||
function distanceWindowSmoothing(points: TrackPoint[], distanceWindow: number, accumulate: (index: number) => number, compute: (accumulated: number, start: number, end: number) => number, remove?: (index: number) => number): number[] {
 | 
			
		||||
    let result = [];
 | 
			
		||||
 | 
			
		||||
    let start = 0, end = 0, accumulated = 0;
 | 
			
		||||
@@ -1236,7 +1208,7 @@ function distanceWindowSmoothing(points: ReadonlyArray<Readonly<TrackPoint>>, di
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function distanceWindowSmoothingWithDistanceAccumulator(points: ReadonlyArray<Readonly<TrackPoint>>, distanceWindow: number, compute: (accumulated: number, start: number, end: number) => number): number[] {
 | 
			
		||||
function distanceWindowSmoothingWithDistanceAccumulator(points: TrackPoint[], distanceWindow: number, compute: (accumulated: number, start: number, end: number) => number): number[] {
 | 
			
		||||
    return distanceWindowSmoothing(points, distanceWindow, (index) => index > 0 ? distance(points[index - 1].getCoordinates(), points[index].getCoordinates()) : 0, compute, (index) => distance(points[index].getCoordinates(), points[index + 1].getCoordinates()));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ export type SimplifiedTrackPoint = { point: TrackPoint, distance?: number };
 | 
			
		||||
 | 
			
		||||
const earthRadius = 6371008.8;
 | 
			
		||||
 | 
			
		||||
export function ramerDouglasPeucker(points: readonly TrackPoint[], epsilon: number = 50, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = computeCrossarc): SimplifiedTrackPoint[] {
 | 
			
		||||
export function ramerDouglasPeucker(points: TrackPoint[], epsilon: number = 50, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = computeCrossarc): SimplifiedTrackPoint[] {
 | 
			
		||||
    if (points.length == 0) {
 | 
			
		||||
        return [];
 | 
			
		||||
    } else if (points.length == 1) {
 | 
			
		||||
@@ -24,7 +24,7 @@ export function ramerDouglasPeucker(points: readonly TrackPoint[], epsilon: numb
 | 
			
		||||
    return simplified;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ramerDouglasPeuckerRecursive(points: readonly TrackPoint[], epsilon: number, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number, start: number, end: number, simplified: SimplifiedTrackPoint[]) {
 | 
			
		||||
function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number, start: number, end: number, simplified: SimplifiedTrackPoint[]) {
 | 
			
		||||
    let largest = {
 | 
			
		||||
        index: 0,
 | 
			
		||||
        distance: 0
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
export type GPXFileType = {
 | 
			
		||||
    attributes: GPXFileAttributes;
 | 
			
		||||
    metadata: Metadata;
 | 
			
		||||
    wpt: ReadonlyArray<WaypointType>;
 | 
			
		||||
    trk: ReadonlyArray<TrackType>;
 | 
			
		||||
    wpt: WaypointType[];
 | 
			
		||||
    trk: TrackType[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type GPXFileAttributes = {
 | 
			
		||||
@@ -52,7 +52,7 @@ export type TrackType = {
 | 
			
		||||
    src?: string;
 | 
			
		||||
    link?: Link;
 | 
			
		||||
    type?: string;
 | 
			
		||||
    trkseg: ReadonlyArray<TrackSegmentType>;
 | 
			
		||||
    trkseg: TrackSegmentType[];
 | 
			
		||||
    extensions?: TrackExtensions;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -67,7 +67,7 @@ export type LineStyleExtension = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type TrackSegmentType = {
 | 
			
		||||
    trkpt: ReadonlyArray<TrackPointType>;
 | 
			
		||||
    trkpt: TrackPointType[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type TrackPointType = {
 | 
			
		||||
 
 | 
			
		||||
@@ -48,11 +48,8 @@
 | 
			
		||||
		triggerFileInput,
 | 
			
		||||
		createFile,
 | 
			
		||||
		loadFiles,
 | 
			
		||||
		toggleSelectionVisibility,
 | 
			
		||||
		updateSelectionFromKey,
 | 
			
		||||
		showSelection,
 | 
			
		||||
		hideSelection,
 | 
			
		||||
		anyHidden,
 | 
			
		||||
		allHidden,
 | 
			
		||||
		editMetadata,
 | 
			
		||||
		editStyle,
 | 
			
		||||
		exportState,
 | 
			
		||||
@@ -217,6 +214,7 @@
 | 
			
		||||
					>
 | 
			
		||||
						<Info size="16" class="mr-1" />
 | 
			
		||||
						{$_('menu.metadata.button')}
 | 
			
		||||
						<Shortcut key="I" ctrl={true} />
 | 
			
		||||
					</Menubar.Item>
 | 
			
		||||
					<Menubar.Item
 | 
			
		||||
						disabled={$selection.size === 0 ||
 | 
			
		||||
@@ -230,15 +228,15 @@
 | 
			
		||||
					</Menubar.Item>
 | 
			
		||||
					<Menubar.Item
 | 
			
		||||
						on:click={() => {
 | 
			
		||||
							if ($anyHidden) {
 | 
			
		||||
								showSelection();
 | 
			
		||||
							if ($allHidden) {
 | 
			
		||||
								dbUtils.setHiddenToSelection(false);
 | 
			
		||||
							} else {
 | 
			
		||||
								hideSelection();
 | 
			
		||||
								dbUtils.setHiddenToSelection(true);
 | 
			
		||||
							}
 | 
			
		||||
						}}
 | 
			
		||||
						disabled={$selection.size == 0}
 | 
			
		||||
					>
 | 
			
		||||
						{#if $anyHidden}
 | 
			
		||||
						{#if $allHidden}
 | 
			
		||||
							<Eye size="16" class="mr-1" />
 | 
			
		||||
							{$_('menu.unhide')}
 | 
			
		||||
						{:else}
 | 
			
		||||
@@ -248,7 +246,7 @@
 | 
			
		||||
						<Shortcut key="H" ctrl={true} />
 | 
			
		||||
					</Menubar.Item>
 | 
			
		||||
					<Menubar.Separator />
 | 
			
		||||
					<Menubar.Item on:click={selectAll}>
 | 
			
		||||
					<Menubar.Item on:click={selectAll} disabled={$fileObservers.size == 0}>
 | 
			
		||||
						<FileStack size="16" class="mr-1" />
 | 
			
		||||
						{$_('menu.select_all')}
 | 
			
		||||
						<Shortcut key="A" ctrl={true} />
 | 
			
		||||
@@ -525,6 +523,16 @@
 | 
			
		||||
				selectAll();
 | 
			
		||||
				e.preventDefault();
 | 
			
		||||
			}
 | 
			
		||||
		} else if (e.key === 'i' && (e.metaKey || e.ctrlKey)) {
 | 
			
		||||
			if (
 | 
			
		||||
				$selection.size === 1 &&
 | 
			
		||||
				$selection
 | 
			
		||||
					.getSelected()
 | 
			
		||||
					.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
 | 
			
		||||
			) {
 | 
			
		||||
				$editMetadata = true;
 | 
			
		||||
			}
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
		} else if (e.key === 'p' && (e.metaKey || e.ctrlKey)) {
 | 
			
		||||
			$elevationProfile = !$elevationProfile;
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
@@ -532,7 +540,11 @@
 | 
			
		||||
			$verticalFileView = !$verticalFileView;
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
		} else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) {
 | 
			
		||||
			toggleSelectionVisibility();
 | 
			
		||||
			if ($allHidden) {
 | 
			
		||||
				dbUtils.setHiddenToSelection(false);
 | 
			
		||||
			} else {
 | 
			
		||||
				dbUtils.setHiddenToSelection(true);
 | 
			
		||||
			}
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
		} else if (e.key === 'F1') {
 | 
			
		||||
			switchBasemaps();
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,8 @@
 | 
			
		||||
	import { fileObservers, settings } from '$lib/db';
 | 
			
		||||
	import { setContext } from 'svelte';
 | 
			
		||||
	import { ListFileItem, ListLevel, ListRootItem, allowedPastes } from './FileList';
 | 
			
		||||
	import { copied, pasteSelection, selection } from './Selection';
 | 
			
		||||
	import { ClipboardPaste, Plus } from 'lucide-svelte';
 | 
			
		||||
	import { copied, pasteSelection, selectAll, selection } from './Selection';
 | 
			
		||||
	import { ClipboardPaste, FileStack, Plus } from 'lucide-svelte';
 | 
			
		||||
	import Shortcut from '$lib/components/Shortcut.svelte';
 | 
			
		||||
	import { _ } from 'svelte-i18n';
 | 
			
		||||
	import { createFile } from '$lib/stores';
 | 
			
		||||
@@ -66,6 +66,12 @@
 | 
			
		||||
						<Shortcut key="+" ctrl={true} />
 | 
			
		||||
					</ContextMenu.Item>
 | 
			
		||||
					<ContextMenu.Separator />
 | 
			
		||||
					<ContextMenu.Item on:click={selectAll} disabled={$fileObservers.size === 0}>
 | 
			
		||||
						<FileStack size="16" class="mr-1" />
 | 
			
		||||
						{$_('menu.select_all')}
 | 
			
		||||
						<Shortcut key="A" ctrl={true} />
 | 
			
		||||
					</ContextMenu.Item>
 | 
			
		||||
					<ContextMenu.Separator />
 | 
			
		||||
					<ContextMenu.Item
 | 
			
		||||
						disabled={$copied === undefined ||
 | 
			
		||||
							$copied.length === 0 ||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { dbUtils, getFile, getFileIds } from "$lib/db";
 | 
			
		||||
import { dbUtils, getFile } from "$lib/db";
 | 
			
		||||
import { castDraft, freeze } from "immer";
 | 
			
		||||
import { GPXFile, Track, TrackSegment, Waypoint } from "gpx";
 | 
			
		||||
import { selection } from "./Selection";
 | 
			
		||||
@@ -327,85 +327,68 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sortItems(fromItems, remove && !(fromParent instanceof ListRootItem));
 | 
			
		||||
    sortItems(fromItems, false);
 | 
			
		||||
    sortItems(toItems, false);
 | 
			
		||||
 | 
			
		||||
    let context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[] = [];
 | 
			
		||||
    if (!remove || fromParent instanceof ListRootItem) {
 | 
			
		||||
        fromItems.forEach((item) => {
 | 
			
		||||
            let file = getFile(item.getFileId());
 | 
			
		||||
            if (file) {
 | 
			
		||||
                if (item instanceof ListFileItem) {
 | 
			
		||||
                    context.push(file.clone());
 | 
			
		||||
                } else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
 | 
			
		||||
                    context.push(file.trk[item.getTrackIndex()].clone());
 | 
			
		||||
                } else if (item instanceof ListTrackSegmentItem && item.getTrackIndex() < file.trk.length && item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length) {
 | 
			
		||||
                    context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone());
 | 
			
		||||
                } else if (item instanceof ListWaypointsItem) {
 | 
			
		||||
                    context.push(file.wpt.map((wpt) => wpt.clone()));
 | 
			
		||||
                } else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) {
 | 
			
		||||
                    context.push(file.wpt[item.getWaypointIndex()].clone());
 | 
			
		||||
                }
 | 
			
		||||
    fromItems.forEach((item) => {
 | 
			
		||||
        let file = getFile(item.getFileId());
 | 
			
		||||
        if (file) {
 | 
			
		||||
            if (item instanceof ListFileItem) {
 | 
			
		||||
                context.push(file.clone());
 | 
			
		||||
            } else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
 | 
			
		||||
                context.push(file.trk[item.getTrackIndex()].clone());
 | 
			
		||||
            } else if (item instanceof ListTrackSegmentItem && item.getTrackIndex() < file.trk.length && item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length) {
 | 
			
		||||
                context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone());
 | 
			
		||||
            } else if (item instanceof ListWaypointsItem) {
 | 
			
		||||
                context.push(file.wpt.map((wpt) => wpt.clone()));
 | 
			
		||||
            } else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) {
 | 
			
		||||
                context.push(file.wpt[item.getWaypointIndex()].clone());
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (remove && !(fromParent instanceof ListRootItem)) {
 | 
			
		||||
        sortItems(fromItems, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let files = [fromParent.getFileId(), toParent.getFileId()];
 | 
			
		||||
    let callbacks = [
 | 
			
		||||
        (file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
 | 
			
		||||
            let newFile = file;
 | 
			
		||||
            fromItems.forEach((item) => {
 | 
			
		||||
                if (item instanceof ListTrackItem) {
 | 
			
		||||
                    let [result, removed] = newFile.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
 | 
			
		||||
                    newFile = castDraft(result);
 | 
			
		||||
                    context.push(...removed);
 | 
			
		||||
                    file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
 | 
			
		||||
                } else if (item instanceof ListTrackSegmentItem) {
 | 
			
		||||
                    let [result, removed] = newFile.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex(), []);
 | 
			
		||||
                    newFile = castDraft(result);
 | 
			
		||||
                    context.push(...removed);
 | 
			
		||||
                    file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex(), []);
 | 
			
		||||
                } else if (item instanceof ListWaypointsItem) {
 | 
			
		||||
                    let [result, removed] = newFile.replaceWaypoints(0, newFile.wpt.length - 1, []);
 | 
			
		||||
                    newFile = castDraft(result);
 | 
			
		||||
                    context.push(removed);
 | 
			
		||||
                    file.replaceWaypoints(0, file.wpt.length - 1, []);
 | 
			
		||||
                } else if (item instanceof ListWaypointItem) {
 | 
			
		||||
                    let [result, removed] = newFile.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex(), []);
 | 
			
		||||
                    newFile = castDraft(result);
 | 
			
		||||
                    context.push(...removed);
 | 
			
		||||
                    file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex(), []);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            context.reverse();
 | 
			
		||||
            return newFile;
 | 
			
		||||
        },
 | 
			
		||||
        (file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
 | 
			
		||||
            let newFile = file;
 | 
			
		||||
            toItems.forEach((item, i) => {
 | 
			
		||||
                if (item instanceof ListTrackItem) {
 | 
			
		||||
                    if (context[i] instanceof Track) {
 | 
			
		||||
                        let [result, _removed] = newFile.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [context[i]]);
 | 
			
		||||
                        newFile = castDraft(result);
 | 
			
		||||
                        file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [context[i]]);
 | 
			
		||||
                    } else if (context[i] instanceof TrackSegment) {
 | 
			
		||||
                        let [result, _removed] = newFile.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [new Track({
 | 
			
		||||
                        file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [new Track({
 | 
			
		||||
                            trkseg: [context[i]]
 | 
			
		||||
                        })]);
 | 
			
		||||
                        newFile = castDraft(result);
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (item instanceof ListTrackSegmentItem && context[i] instanceof TrackSegment) {
 | 
			
		||||
                    let [result, _removed] = newFile.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex() - 1, [context[i]]);
 | 
			
		||||
                    newFile = castDraft(result);
 | 
			
		||||
                    file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex() - 1, [context[i]]);
 | 
			
		||||
                } else if (item instanceof ListWaypointsItem) {
 | 
			
		||||
                    if (Array.isArray(context[i]) && context[i].length > 0 && context[i][0] instanceof Waypoint) {
 | 
			
		||||
                        let [result, _removed] = newFile.replaceWaypoints(newFile.wpt.length, newFile.wpt.length - 1, context[i]);
 | 
			
		||||
                        newFile = castDraft(result);
 | 
			
		||||
                        file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, context[i]);
 | 
			
		||||
                    } else if (context[i] instanceof Waypoint) {
 | 
			
		||||
                        let [result, _removed] = newFile.replaceWaypoints(newFile.wpt.length, newFile.wpt.length - 1, [context[i]]);
 | 
			
		||||
                        newFile = castDraft(result);
 | 
			
		||||
                        file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]);
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (item instanceof ListWaypointItem && context[i] instanceof Waypoint) {
 | 
			
		||||
                    let [result, _removed] = newFile.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [context[i]]);
 | 
			
		||||
                    newFile = castDraft(result);
 | 
			
		||||
                    file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [context[i]]);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            return newFile;
 | 
			
		||||
        }
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
@@ -433,14 +416,14 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
 | 
			
		||||
                    if (context[i].name) {
 | 
			
		||||
                        newFile.metadata.name = context[i].name;
 | 
			
		||||
                    }
 | 
			
		||||
                    newFile = newFile.replaceTracks(0, 0, [context[i]])[0];
 | 
			
		||||
                    newFile.replaceTracks(0, 0, [context[i]]);
 | 
			
		||||
                    files.set(item.getFileId(), freeze(newFile));
 | 
			
		||||
                } else if (context[i] instanceof TrackSegment) {
 | 
			
		||||
                    let newFile = newGPXFile();
 | 
			
		||||
                    newFile._data.id = item.getFileId();
 | 
			
		||||
                    newFile = newFile.replaceTracks(0, 0, [new Track({
 | 
			
		||||
                    newFile.replaceTracks(0, 0, [new Track({
 | 
			
		||||
                        trkseg: [context[i]]
 | 
			
		||||
                    })])[0];
 | 
			
		||||
                    })]);
 | 
			
		||||
                    files.set(item.getFileId(), freeze(newFile));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,10 @@
 | 
			
		||||
	import FileListNodeLabel from './FileListNodeLabel.svelte';
 | 
			
		||||
	import { afterUpdate, getContext } from 'svelte';
 | 
			
		||||
	import {
 | 
			
		||||
		ListFileItem,
 | 
			
		||||
		ListTrackSegmentItem,
 | 
			
		||||
		ListWaypointItem,
 | 
			
		||||
		ListWaypointsItem,
 | 
			
		||||
		type ListItem,
 | 
			
		||||
		type ListTrackItem
 | 
			
		||||
	} from './FileList';
 | 
			
		||||
@@ -25,8 +27,8 @@
 | 
			
		||||
	export let node:
 | 
			
		||||
		| Map<string, Readable<GPXFileWithStatistics | undefined>>
 | 
			
		||||
		| GPXTreeElement<AnyGPXTreeElement>
 | 
			
		||||
		| ReadonlyArray<Readonly<Waypoint>>
 | 
			
		||||
		| Readonly<Waypoint>;
 | 
			
		||||
		| Waypoint[]
 | 
			
		||||
		| Waypoint;
 | 
			
		||||
	export let item: ListItem;
 | 
			
		||||
 | 
			
		||||
	let recursive = getContext<boolean>('recursive');
 | 
			
		||||
@@ -34,7 +36,7 @@
 | 
			
		||||
	let collapsible: CollapsibleTreeNode;
 | 
			
		||||
 | 
			
		||||
	$: label =
 | 
			
		||||
		node instanceof GPXFile
 | 
			
		||||
		node instanceof GPXFile && item instanceof ListFileItem
 | 
			
		||||
			? node.metadata.name
 | 
			
		||||
			: node instanceof Track
 | 
			
		||||
				? node.name ?? `${$_('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}`
 | 
			
		||||
@@ -42,7 +44,7 @@
 | 
			
		||||
					? `${$_('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
 | 
			
		||||
					: node instanceof Waypoint
 | 
			
		||||
						? node.name ?? `${$_('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`
 | 
			
		||||
						: Array.isArray(node) && node.length > 0 && node[0] instanceof Waypoint
 | 
			
		||||
						: node instanceof GPXFile && item instanceof ListWaypointsItem
 | 
			
		||||
							? $_('gpx.waypoints')
 | 
			
		||||
							: '';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,21 +6,28 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
 | 
			
		||||
	import { afterUpdate, getContext, onMount } from 'svelte';
 | 
			
		||||
	import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
 | 
			
		||||
	import Sortable from 'sortablejs/Sortable';
 | 
			
		||||
	import { getFileIds, settings, type GPXFileWithStatistics } from '$lib/db';
 | 
			
		||||
	import { get, writable, type Readable, type Writable } from 'svelte/store';
 | 
			
		||||
	import FileListNodeStore from './FileListNodeStore.svelte';
 | 
			
		||||
	import FileListNode from './FileListNode.svelte';
 | 
			
		||||
	import { ListLevel, ListRootItem, allowedMoves, moveItems, type ListItem } from './FileList';
 | 
			
		||||
	import {
 | 
			
		||||
		ListFileItem,
 | 
			
		||||
		ListLevel,
 | 
			
		||||
		ListRootItem,
 | 
			
		||||
		ListWaypointsItem,
 | 
			
		||||
		allowedMoves,
 | 
			
		||||
		moveItems,
 | 
			
		||||
		type ListItem
 | 
			
		||||
	} from './FileList';
 | 
			
		||||
	import { selection } from './Selection';
 | 
			
		||||
	import { _ } from 'svelte-i18n';
 | 
			
		||||
 | 
			
		||||
	export let node:
 | 
			
		||||
		| Map<string, Readable<GPXFileWithStatistics | undefined>>
 | 
			
		||||
		| GPXTreeElement<AnyGPXTreeElement>
 | 
			
		||||
		| ReadonlyArray<Readonly<Waypoint>>
 | 
			
		||||
		| Readonly<Waypoint>;
 | 
			
		||||
		| Waypoint;
 | 
			
		||||
	export let item: ListItem;
 | 
			
		||||
	export let waypointRoot: boolean = false;
 | 
			
		||||
 | 
			
		||||
@@ -32,46 +39,64 @@
 | 
			
		||||
			: node instanceof GPXFile
 | 
			
		||||
				? waypointRoot
 | 
			
		||||
					? ListLevel.WAYPOINTS
 | 
			
		||||
					: ListLevel.TRACK
 | 
			
		||||
					: item instanceof ListWaypointsItem
 | 
			
		||||
						? ListLevel.WAYPOINT
 | 
			
		||||
						: ListLevel.TRACK
 | 
			
		||||
				: node instanceof Track
 | 
			
		||||
					? ListLevel.SEGMENT
 | 
			
		||||
					: ListLevel.WAYPOINT;
 | 
			
		||||
	let sortable: Sortable;
 | 
			
		||||
	let orientation = getContext<'vertical' | 'horizontal'>('orientation');
 | 
			
		||||
 | 
			
		||||
	let destroyed = false;
 | 
			
		||||
	let lastUpdateStart = 0;
 | 
			
		||||
	function updateToSelection(e) {
 | 
			
		||||
		if (updating) return;
 | 
			
		||||
		updating = true;
 | 
			
		||||
		// Sortable updates selection
 | 
			
		||||
		let changed = getChangedIds();
 | 
			
		||||
		if (changed.length > 0) {
 | 
			
		||||
			selection.update(($selection) => {
 | 
			
		||||
				$selection.clear();
 | 
			
		||||
				Object.entries(elements).forEach(([id, element]) => {
 | 
			
		||||
					$selection.set(
 | 
			
		||||
						item.extend(getRealId(id)),
 | 
			
		||||
						element.classList.contains('sortable-selected')
 | 
			
		||||
					);
 | 
			
		||||
				});
 | 
			
		||||
		if (destroyed) {
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
				if (
 | 
			
		||||
					e.originalEvent &&
 | 
			
		||||
					$selection.size > 1 &&
 | 
			
		||||
					!(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey)
 | 
			
		||||
				) {
 | 
			
		||||
					// Fix bug that sometimes causes a single select to be treated as a multi-select
 | 
			
		||||
					$selection.clear();
 | 
			
		||||
					$selection.set(item.extend(getRealId(changed[0])), true);
 | 
			
		||||
		lastUpdateStart = Date.now();
 | 
			
		||||
		setTimeout(() => {
 | 
			
		||||
			if (Date.now() - lastUpdateStart >= 40) {
 | 
			
		||||
				if (updating) {
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return $selection;
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		updating = false;
 | 
			
		||||
				updating = true;
 | 
			
		||||
				// Sortable updates selection
 | 
			
		||||
				let changed = getChangedIds();
 | 
			
		||||
				if (changed.length > 0) {
 | 
			
		||||
					selection.update(($selection) => {
 | 
			
		||||
						$selection.clear();
 | 
			
		||||
						Object.entries(elements).forEach(([id, element]) => {
 | 
			
		||||
							$selection.set(
 | 
			
		||||
								item.extend(getRealId(id)),
 | 
			
		||||
								element.classList.contains('sortable-selected')
 | 
			
		||||
							);
 | 
			
		||||
						});
 | 
			
		||||
 | 
			
		||||
						if (
 | 
			
		||||
							e.originalEvent &&
 | 
			
		||||
							$selection.size > 1 &&
 | 
			
		||||
							!(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey)
 | 
			
		||||
						) {
 | 
			
		||||
							// Fix bug that sometimes causes a single select to be treated as a multi-select
 | 
			
		||||
							$selection.clear();
 | 
			
		||||
							$selection.set(item.extend(getRealId(changed[0])), true);
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						return $selection;
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
				updating = false;
 | 
			
		||||
			}
 | 
			
		||||
		}, 50);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function updateFromSelection() {
 | 
			
		||||
		if (updating) return;
 | 
			
		||||
		if (destroyed || updating) {
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		updating = true;
 | 
			
		||||
		// Selection updates sortable
 | 
			
		||||
		let changed = getChangedIds();
 | 
			
		||||
@@ -165,8 +190,9 @@
 | 
			
		||||
					if (Sortable.get(e.from)._waypointRoot) {
 | 
			
		||||
						fromItems = [fromItem.extend('waypoints')];
 | 
			
		||||
					} else {
 | 
			
		||||
						let oldIndices =
 | 
			
		||||
						let oldIndices: number[] =
 | 
			
		||||
							e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex];
 | 
			
		||||
						oldIndices = oldIndices.filter((i) => i >= 0);
 | 
			
		||||
						oldIndices.sort((a, b) => a - b);
 | 
			
		||||
 | 
			
		||||
						fromItems = oldIndices.map((i) => fromItem.extend(i));
 | 
			
		||||
@@ -179,8 +205,9 @@
 | 
			
		||||
							toItem = toItem.extend('waypoints');
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						let newIndices =
 | 
			
		||||
						let newIndices: number[] =
 | 
			
		||||
							e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex];
 | 
			
		||||
						newIndices = newIndices.filter((i) => i >= 0);
 | 
			
		||||
						newIndices.sort((a, b) => a - b);
 | 
			
		||||
 | 
			
		||||
						if (toItem instanceof ListRootItem) {
 | 
			
		||||
@@ -211,6 +238,7 @@
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		createSortable();
 | 
			
		||||
		destroyed = false;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	afterUpdate(() => {
 | 
			
		||||
@@ -232,6 +260,10 @@
 | 
			
		||||
		updateFromSelection();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	onDestroy(() => {
 | 
			
		||||
		destroyed = true;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	function getChangedIds() {
 | 
			
		||||
		let changed: (string | number)[] = [];
 | 
			
		||||
		Object.entries(elements).forEach(([id, element]) => {
 | 
			
		||||
@@ -268,10 +300,16 @@
 | 
			
		||||
			</div>
 | 
			
		||||
		{/each}
 | 
			
		||||
	{:else if node instanceof GPXFile}
 | 
			
		||||
		{#if waypointRoot}
 | 
			
		||||
		{#if item instanceof ListWaypointsItem}
 | 
			
		||||
			{#each node.wpt as wpt, i (wpt)}
 | 
			
		||||
				<div data-id={i} class="ml-1">
 | 
			
		||||
					<FileListNode node={wpt} item={item.extend(i)} />
 | 
			
		||||
				</div>
 | 
			
		||||
			{/each}
 | 
			
		||||
		{:else if waypointRoot}
 | 
			
		||||
			{#if node.wpt.length > 0}
 | 
			
		||||
				<div data-id="waypoints">
 | 
			
		||||
					<FileListNode node={node.wpt} item={item.extend('waypoints')} />
 | 
			
		||||
					<FileListNode {node} item={item.extend('waypoints')} />
 | 
			
		||||
				</div>
 | 
			
		||||
			{/if}
 | 
			
		||||
		{:else}
 | 
			
		||||
@@ -287,16 +325,10 @@
 | 
			
		||||
				<FileListNode node={child} item={item.extend(i)} />
 | 
			
		||||
			</div>
 | 
			
		||||
		{/each}
 | 
			
		||||
	{:else if Array.isArray(node) && node.length > 0 && node[0] instanceof Waypoint}
 | 
			
		||||
		{#each node as wpt, i (wpt)}
 | 
			
		||||
			<div data-id={i} class="ml-1">
 | 
			
		||||
				<FileListNode node={wpt} item={item.extend(i)} />
 | 
			
		||||
			</div>
 | 
			
		||||
		{/each}
 | 
			
		||||
	{/if}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{#if node instanceof GPXFile}
 | 
			
		||||
{#if node instanceof GPXFile && item instanceof ListFileItem}
 | 
			
		||||
	{#if !waypointRoot}
 | 
			
		||||
		<svelte:self {node} {item} waypointRoot={true} />
 | 
			
		||||
	{/if}
 | 
			
		||||
 
 | 
			
		||||
@@ -38,15 +38,7 @@
 | 
			
		||||
	} from './Selection';
 | 
			
		||||
	import { getContext } from 'svelte';
 | 
			
		||||
	import { get } from 'svelte/store';
 | 
			
		||||
	import {
 | 
			
		||||
		anyHidden,
 | 
			
		||||
		editMetadata,
 | 
			
		||||
		editStyle,
 | 
			
		||||
		gpxLayers,
 | 
			
		||||
		hideSelection,
 | 
			
		||||
		map,
 | 
			
		||||
		showSelection
 | 
			
		||||
	} from '$lib/stores';
 | 
			
		||||
	import { allHidden, editMetadata, editStyle, gpxLayers, map } from '$lib/stores';
 | 
			
		||||
	import {
 | 
			
		||||
		GPXTreeElement,
 | 
			
		||||
		Track,
 | 
			
		||||
@@ -59,10 +51,7 @@
 | 
			
		||||
	import MetadataDialog from './MetadataDialog.svelte';
 | 
			
		||||
	import StyleDialog from './StyleDialog.svelte';
 | 
			
		||||
 | 
			
		||||
	export let node:
 | 
			
		||||
		| GPXTreeElement<AnyGPXTreeElement>
 | 
			
		||||
		| ReadonlyArray<Readonly<Waypoint>>
 | 
			
		||||
		| Readonly<Waypoint>;
 | 
			
		||||
	export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
 | 
			
		||||
	export let item: ListItem;
 | 
			
		||||
	export let label: string | undefined;
 | 
			
		||||
 | 
			
		||||
@@ -114,6 +103,7 @@
 | 
			
		||||
		$editStyle &&
 | 
			
		||||
		$selection.has(item) &&
 | 
			
		||||
		$selection.getSelected().findIndex((i) => i.getFullId() === item.getFullId()) === 0;
 | 
			
		||||
	$: hidden = item.level === ListLevel.WAYPOINTS ? node._data.hiddenWpt : node._data.hidden;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
 | 
			
		||||
@@ -153,7 +143,9 @@
 | 
			
		||||
				/>
 | 
			
		||||
			{/if}
 | 
			
		||||
			<span
 | 
			
		||||
				class="w-full text-left truncate py-1 flex flex-row items-center"
 | 
			
		||||
				class="w-full text-left truncate py-1 flex flex-row items-center {hidden
 | 
			
		||||
					? 'text-muted-foreground'
 | 
			
		||||
					: ''}"
 | 
			
		||||
				on:contextmenu={(e) => {
 | 
			
		||||
					if (e.ctrlKey) {
 | 
			
		||||
						// Add to selection instead of opening context menu
 | 
			
		||||
@@ -189,9 +181,18 @@
 | 
			
		||||
				{:else if item.level === ListLevel.WAYPOINT}
 | 
			
		||||
					<MapPin size="16" class="mr-1 shrink-0" />
 | 
			
		||||
				{/if}
 | 
			
		||||
				<span class="grow select-none truncate {$verticalFileView ? 'mr-2' : ''}">
 | 
			
		||||
				<span class="grow select-none truncate {$verticalFileView ? 'last:mr-2' : ''}">
 | 
			
		||||
					{label}
 | 
			
		||||
				</span>
 | 
			
		||||
				{#if hidden}
 | 
			
		||||
					<EyeOff
 | 
			
		||||
						size="12"
 | 
			
		||||
						class="shrink-0 mt-1 ml-1 {$verticalFileView ? 'mr-2' : ''} {item.level ===
 | 
			
		||||
							ListLevel.SEGMENT || item.level === ListLevel.WAYPOINT
 | 
			
		||||
							? 'mr-3'
 | 
			
		||||
							: ''}"
 | 
			
		||||
					/>
 | 
			
		||||
				{/if}
 | 
			
		||||
			</span>
 | 
			
		||||
		</Button>
 | 
			
		||||
	</ContextMenu.Trigger>
 | 
			
		||||
@@ -200,41 +201,39 @@
 | 
			
		||||
			<ContextMenu.Item disabled={!singleSelection} on:click={() => ($editMetadata = true)}>
 | 
			
		||||
				<Info size="16" class="mr-1" />
 | 
			
		||||
				{$_('menu.metadata.button')}
 | 
			
		||||
				<Shortcut key="I" ctrl={true} />
 | 
			
		||||
			</ContextMenu.Item>
 | 
			
		||||
			<ContextMenu.Item on:click={() => ($editStyle = true)}>
 | 
			
		||||
				<PaintBucket size="16" class="mr-1" />
 | 
			
		||||
				{$_('menu.style.button')}
 | 
			
		||||
			</ContextMenu.Item>
 | 
			
		||||
			{#if item instanceof ListFileItem}
 | 
			
		||||
				<ContextMenu.Item
 | 
			
		||||
					on:click={() => {
 | 
			
		||||
						if ($anyHidden) {
 | 
			
		||||
							showSelection();
 | 
			
		||||
						} else {
 | 
			
		||||
							hideSelection();
 | 
			
		||||
						}
 | 
			
		||||
					}}
 | 
			
		||||
				>
 | 
			
		||||
					{#if $anyHidden}
 | 
			
		||||
						<Eye size="16" class="mr-1" />
 | 
			
		||||
						{$_('menu.unhide')}
 | 
			
		||||
					{:else}
 | 
			
		||||
						<EyeOff size="16" class="mr-1" />
 | 
			
		||||
						{$_('menu.hide')}
 | 
			
		||||
					{/if}
 | 
			
		||||
					<Shortcut key="H" ctrl={true} />
 | 
			
		||||
				</ContextMenu.Item>
 | 
			
		||||
			{/if}
 | 
			
		||||
			<ContextMenu.Separator />
 | 
			
		||||
		{/if}
 | 
			
		||||
		<ContextMenu.Item
 | 
			
		||||
			on:click={() => {
 | 
			
		||||
				if ($allHidden) {
 | 
			
		||||
					dbUtils.setHiddenToSelection(false);
 | 
			
		||||
				} else {
 | 
			
		||||
					dbUtils.setHiddenToSelection(true);
 | 
			
		||||
				}
 | 
			
		||||
			}}
 | 
			
		||||
		>
 | 
			
		||||
			{#if $allHidden}
 | 
			
		||||
				<Eye size="16" class="mr-1" />
 | 
			
		||||
				{$_('menu.unhide')}
 | 
			
		||||
			{:else}
 | 
			
		||||
				<EyeOff size="16" class="mr-1" />
 | 
			
		||||
				{$_('menu.hide')}
 | 
			
		||||
			{/if}
 | 
			
		||||
			<Shortcut key="H" ctrl={true} />
 | 
			
		||||
		</ContextMenu.Item>
 | 
			
		||||
		<ContextMenu.Separator />
 | 
			
		||||
		{#if $verticalFileView}
 | 
			
		||||
			{#if item instanceof ListFileItem}
 | 
			
		||||
				<ContextMenu.Item
 | 
			
		||||
					disabled={!singleSelection}
 | 
			
		||||
					on:click={() =>
 | 
			
		||||
						dbUtils.applyToFile(
 | 
			
		||||
							item.getFileId(),
 | 
			
		||||
							(file) => file.replaceTracks(file.trk.length, file.trk.length, [new Track()])[0]
 | 
			
		||||
						dbUtils.applyToFile(item.getFileId(), (file) =>
 | 
			
		||||
							file.replaceTracks(file.trk.length, file.trk.length, [new Track()])
 | 
			
		||||
						)}
 | 
			
		||||
				>
 | 
			
		||||
					<Plus size="16" class="mr-1" />
 | 
			
		||||
@@ -246,15 +245,13 @@
 | 
			
		||||
					disabled={!singleSelection}
 | 
			
		||||
					on:click={() => {
 | 
			
		||||
						let trackIndex = item.getTrackIndex();
 | 
			
		||||
						dbUtils.applyToFile(
 | 
			
		||||
							item.getFileId(),
 | 
			
		||||
							(file) =>
 | 
			
		||||
								file.replaceTrackSegments(
 | 
			
		||||
									trackIndex,
 | 
			
		||||
									file.trk[trackIndex].trkseg.length,
 | 
			
		||||
									file.trk[trackIndex].trkseg.length,
 | 
			
		||||
									[new TrackSegment()]
 | 
			
		||||
								)[0]
 | 
			
		||||
						dbUtils.applyToFile(item.getFileId(), (file) =>
 | 
			
		||||
							file.replaceTrackSegments(
 | 
			
		||||
								trackIndex,
 | 
			
		||||
								file.trk[trackIndex].trkseg.length,
 | 
			
		||||
								file.trk[trackIndex].trkseg.length,
 | 
			
		||||
								[new TrackSegment()]
 | 
			
		||||
							)
 | 
			
		||||
						);
 | 
			
		||||
					}}
 | 
			
		||||
				>
 | 
			
		||||
 
 | 
			
		||||
@@ -11,10 +11,7 @@
 | 
			
		||||
	import { _ } from 'svelte-i18n';
 | 
			
		||||
	import { editMetadata } from '$lib/stores';
 | 
			
		||||
 | 
			
		||||
	export let node:
 | 
			
		||||
		| GPXTreeElement<AnyGPXTreeElement>
 | 
			
		||||
		| ReadonlyArray<Readonly<Waypoint>>
 | 
			
		||||
		| Readonly<Waypoint>;
 | 
			
		||||
	export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
 | 
			
		||||
	export let item: ListItem;
 | 
			
		||||
	export let open = false;
 | 
			
		||||
 | 
			
		||||
@@ -54,7 +51,6 @@
 | 
			
		||||
						file.trk[item.getTrackIndex()].name = name;
 | 
			
		||||
						file.trk[item.getTrackIndex()].desc = description;
 | 
			
		||||
					}
 | 
			
		||||
					return file;
 | 
			
		||||
				});
 | 
			
		||||
				open = false;
 | 
			
		||||
			}}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ import { currentPopupWaypoint, deleteWaypoint, waypointPopup } from "./WaypointP
 | 
			
		||||
import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection";
 | 
			
		||||
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
 | 
			
		||||
import type { Waypoint } from "gpx";
 | 
			
		||||
import { produce } from "immer";
 | 
			
		||||
import { resetCursor, setCursor, setGrabbingCursor, setPointerCursor } from "$lib/utils";
 | 
			
		||||
import { font } from "$lib/assets/layers";
 | 
			
		||||
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
 | 
			
		||||
@@ -50,7 +49,6 @@ export class GPXLayer {
 | 
			
		||||
    fileId: string;
 | 
			
		||||
    file: Readable<GPXFileWithStatistics | undefined>;
 | 
			
		||||
    layerColor: string;
 | 
			
		||||
    hidden: boolean = false;
 | 
			
		||||
    markers: mapboxgl.Marker[] = [];
 | 
			
		||||
    selected: boolean = false;
 | 
			
		||||
    draggable: boolean;
 | 
			
		||||
@@ -165,6 +163,15 @@ export class GPXLayer {
 | 
			
		||||
                    this.map.removeLayer(this.fileId + '-direction');
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let visibleItems: [number, number][] = [];
 | 
			
		||||
            file.forEachSegment((segment, trackIndex, segmentIndex) => {
 | 
			
		||||
                if (!segment._data.hidden) {
 | 
			
		||||
                    visibleItems.push([trackIndex, segmentIndex]);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.map.setFilter(this.fileId, ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false });
 | 
			
		||||
        } catch (e) { // No reliable way to check if the map is ready to add sources and layers
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -223,13 +230,13 @@ export class GPXLayer {
 | 
			
		||||
                        resetCursor();
 | 
			
		||||
                        marker.getElement().style.cursor = '';
 | 
			
		||||
                        dbUtils.applyToFile(this.fileId, (file) => {
 | 
			
		||||
                            return produce(file, (draft) => {
 | 
			
		||||
                                let latLng = marker.getLngLat();
 | 
			
		||||
                                draft.wpt[marker._waypoint._data.index].setCoordinates({
 | 
			
		||||
                                    lat: latLng.lat,
 | 
			
		||||
                                    lon: latLng.lng
 | 
			
		||||
                                });
 | 
			
		||||
                            let latLng = marker.getLngLat();
 | 
			
		||||
                            let wpt = file.wpt[marker._waypoint._data.index];
 | 
			
		||||
                            wpt.setCoordinates({
 | 
			
		||||
                                lat: latLng.lat,
 | 
			
		||||
                                lon: latLng.lng
 | 
			
		||||
                            });
 | 
			
		||||
                            wpt.ele = this.map.queryTerrainElevation([latLng.lng, latLng.lat], { exaggerated: false }) ?? 0;
 | 
			
		||||
                        });
 | 
			
		||||
                        dragEndTimestamp = Date.now()
 | 
			
		||||
                    });
 | 
			
		||||
@@ -244,7 +251,11 @@ export class GPXLayer {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.markers.forEach((marker) => {
 | 
			
		||||
            marker.addTo(this.map);
 | 
			
		||||
            if (!marker._waypoint._data.hidden) {
 | 
			
		||||
                marker.addTo(this.map);
 | 
			
		||||
            } else {
 | 
			
		||||
                marker.remove();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -366,17 +377,6 @@ export class GPXLayer {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toggleVisibility() {
 | 
			
		||||
        this.hidden = !this.hidden;
 | 
			
		||||
        if (this.hidden) {
 | 
			
		||||
            this.map.setLayoutProperty(this.fileId, 'visibility', 'none');
 | 
			
		||||
            this.markers.forEach(marker => marker.remove());
 | 
			
		||||
        } else {
 | 
			
		||||
            this.map.setLayoutProperty(this.fileId, 'visibility', 'visible');
 | 
			
		||||
            this.markers.forEach(marker => marker.addTo(this.map));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getGeoJSON(): GeoJSON.FeatureCollection {
 | 
			
		||||
        let file = get(this.file)?.file;
 | 
			
		||||
        if (!file) {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,5 +21,5 @@ export const waypointPopup = new mapboxgl.Popup({
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export function deleteWaypoint(fileId: string, waypointIndex: number) {
 | 
			
		||||
    dbUtils.applyToFile(fileId, (file) => file.replaceWaypoints(waypointIndex, waypointIndex, [])[0]);
 | 
			
		||||
    dbUtils.applyToFile(fileId, (file) => file.replaceWaypoints(waypointIndex, waypointIndex, []));
 | 
			
		||||
}
 | 
			
		||||
@@ -303,16 +303,16 @@
 | 
			
		||||
				let fileId = item.getFileId();
 | 
			
		||||
				dbUtils.applyToFile(fileId, (file) => {
 | 
			
		||||
					if (item instanceof ListFileItem) {
 | 
			
		||||
						return file.changeTimestamps(getDate(startDate, startTime), effectiveSpeed, ratio);
 | 
			
		||||
						file.changeTimestamps(getDate(startDate, startTime), effectiveSpeed, ratio);
 | 
			
		||||
					} else if (item instanceof ListTrackItem) {
 | 
			
		||||
						return file.changeTimestamps(
 | 
			
		||||
						file.changeTimestamps(
 | 
			
		||||
							getDate(startDate, startTime),
 | 
			
		||||
							effectiveSpeed,
 | 
			
		||||
							ratio,
 | 
			
		||||
							item.getTrackIndex()
 | 
			
		||||
						);
 | 
			
		||||
					} else if (item instanceof ListTrackSegmentItem) {
 | 
			
		||||
						return file.changeTimestamps(
 | 
			
		||||
						file.changeTimestamps(
 | 
			
		||||
							getDate(startDate, startTime),
 | 
			
		||||
							effectiveSpeed,
 | 
			
		||||
							ratio,
 | 
			
		||||
 
 | 
			
		||||
@@ -104,19 +104,16 @@
 | 
			
		||||
		longitude = parseFloat(longitude.toFixed(6));
 | 
			
		||||
		if ($selectedWaypoint) {
 | 
			
		||||
			dbUtils.applyToFile($selectedWaypoint[1], (file) => {
 | 
			
		||||
				let waypoint = $selectedWaypoint[0].clone();
 | 
			
		||||
				waypoint.name = name;
 | 
			
		||||
				waypoint.desc = description;
 | 
			
		||||
				waypoint.cmt = description;
 | 
			
		||||
				waypoint.setCoordinates({
 | 
			
		||||
				let wpt = file.wpt[$selectedWaypoint[0]._data.index];
 | 
			
		||||
				wpt.name = name;
 | 
			
		||||
				wpt.desc = description;
 | 
			
		||||
				wpt.cmt = description;
 | 
			
		||||
				wpt.setCoordinates({
 | 
			
		||||
					lat: latitude,
 | 
			
		||||
					lon: longitude
 | 
			
		||||
				});
 | 
			
		||||
				return file.replaceWaypoints(
 | 
			
		||||
					$selectedWaypoint[0]._data.index,
 | 
			
		||||
					$selectedWaypoint[0]._data.index,
 | 
			
		||||
					[waypoint]
 | 
			
		||||
				)[0];
 | 
			
		||||
				wpt.ele =
 | 
			
		||||
					get(map)?.queryTerrainElevation([longitude, latitude], { exaggerated: false }) ?? 0;
 | 
			
		||||
			});
 | 
			
		||||
		} else {
 | 
			
		||||
			let fileIds = new Set<string>();
 | 
			
		||||
@@ -134,9 +131,8 @@
 | 
			
		||||
			});
 | 
			
		||||
			waypoint.ele =
 | 
			
		||||
				get(map)?.queryTerrainElevation([longitude, latitude], { exaggerated: false }) ?? 0;
 | 
			
		||||
			dbUtils.applyToFiles(
 | 
			
		||||
				Array.from(fileIds),
 | 
			
		||||
				(file) => file.replaceWaypoints(file.wpt.length, file.wpt.length, [waypoint])[0]
 | 
			
		||||
			dbUtils.applyToFiles(Array.from(fileIds), (file) =>
 | 
			
		||||
				file.replaceWaypoints(file.wpt.length, file.wpt.length, [waypoint])
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
		selectedWaypoint.set(undefined);
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,6 @@
 | 
			
		||||
	import { flyAndScale } from '$lib/utils';
 | 
			
		||||
	import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
	import { TrackPoint } from 'gpx';
 | 
			
		||||
	import { produce } from 'immer';
 | 
			
		||||
 | 
			
		||||
	export let popup: mapboxgl.Popup;
 | 
			
		||||
	export let popupElement: HTMLElement;
 | 
			
		||||
@@ -71,7 +70,7 @@
 | 
			
		||||
	function createFileWithPoint(e: any) {
 | 
			
		||||
		if ($selection.size === 0) {
 | 
			
		||||
			let file = newGPXFile();
 | 
			
		||||
			file = file.replaceTrackPoints(0, 0, 0, 0, [
 | 
			
		||||
			file.replaceTrackPoints(0, 0, 0, 0, [
 | 
			
		||||
				new TrackPoint({
 | 
			
		||||
					attributes: {
 | 
			
		||||
						lat: e.lngLat.lat,
 | 
			
		||||
@@ -79,9 +78,7 @@
 | 
			
		||||
					}
 | 
			
		||||
				})
 | 
			
		||||
			]);
 | 
			
		||||
			file = produce(file, (draft) => {
 | 
			
		||||
				draft._data.id = getFileIds(1)[0];
 | 
			
		||||
			});
 | 
			
		||||
			file._data.id = getFileIds(1)[0];
 | 
			
		||||
			dbUtils.add(file);
 | 
			
		||||
			selectFileWhenLoaded(file._data.id);
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -351,7 +351,7 @@ export class RoutingControls {
 | 
			
		||||
        } else if (nextAnchor === null) { // Last point, remove trackpoints from previousAnchor
 | 
			
		||||
            dbUtils.applyToFile(this.fileId, (file) => {
 | 
			
		||||
                let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex);
 | 
			
		||||
                return file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, previousAnchor.point._data.index + 1, segment.trkpt.length - 1, []);
 | 
			
		||||
                file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, previousAnchor.point._data.index + 1, segment.trkpt.length - 1, []);
 | 
			
		||||
            });
 | 
			
		||||
        } else { // Route between previousAnchor and nextAnchor
 | 
			
		||||
            this.routeBetweenAnchors([previousAnchor, nextAnchor], [previousAnchor.point.getCoordinates(), nextAnchor.point.getCoordinates()]);
 | 
			
		||||
@@ -374,8 +374,8 @@ export class RoutingControls {
 | 
			
		||||
 | 
			
		||||
        let segment = anchor.segment;
 | 
			
		||||
        dbUtils.applyToFile(this.fileId, (file) => {
 | 
			
		||||
            let newFile = file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, segment.trkpt.length, segment.trkpt.length - 1, segment.trkpt.slice(0, anchor.point._data.index), speed > 0 ? speed : undefined);
 | 
			
		||||
            return newFile.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, anchor.point._data.index - 1, []);
 | 
			
		||||
            file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, segment.trkpt.length, segment.trkpt.length - 1, segment.trkpt.slice(0, anchor.point._data.index), speed > 0 ? speed : undefined);
 | 
			
		||||
            file.crop(anchor.point._data.index, anchor.point._data.index + segment.trkpt.length - 1, [anchor.trackIndex], [anchor.segmentIndex]);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -412,14 +412,14 @@ export class RoutingControls {
 | 
			
		||||
                }
 | 
			
		||||
                if (file.trk.length === 0) {
 | 
			
		||||
                    let track = new Track();
 | 
			
		||||
                    track = track.replaceTrackPoints(0, 0, 0, [newPoint]);
 | 
			
		||||
                    return file.replaceTracks(0, 0, [track])[0];
 | 
			
		||||
                    track.replaceTrackPoints(0, 0, 0, [newPoint]);
 | 
			
		||||
                    file.replaceTracks(0, 0, [track]);
 | 
			
		||||
                } else if (file.trk[trackIndex].trkseg.length === 0) {
 | 
			
		||||
                    let segment = new TrackSegment();
 | 
			
		||||
                    segment = segment.replaceTrackPoints(0, 0, [newPoint]);
 | 
			
		||||
                    return file.replaceTrackSegments(trackIndex, 0, 0, [segment])[0];
 | 
			
		||||
                    segment.replaceTrackPoints(0, 0, [newPoint]);
 | 
			
		||||
                    file.replaceTrackSegments(trackIndex, 0, 0, [segment]);
 | 
			
		||||
                } else {
 | 
			
		||||
                    return file.replaceTrackPoints(trackIndex, segmentIndex, 0, 0, [newPoint]);
 | 
			
		||||
                    file.replaceTrackPoints(trackIndex, segmentIndex, 0, 0, [newPoint]);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            return;
 | 
			
		||||
@@ -458,11 +458,11 @@ export class RoutingControls {
 | 
			
		||||
 | 
			
		||||
        let lastAnchor = this.anchors[this.anchors.length - 1];
 | 
			
		||||
 | 
			
		||||
        let segment = lastAnchor.segment;
 | 
			
		||||
        dbUtils.applyToFile(this.fileId, (file) => {
 | 
			
		||||
            let segment = original(file).getSegment(lastAnchor.trackIndex, lastAnchor.segmentIndex);
 | 
			
		||||
            let newSegment = segment.clone();
 | 
			
		||||
            newSegment = newSegment._reverse(segment.getEndTimestamp(), segment.getEndTimestamp());
 | 
			
		||||
            return file.replaceTrackPoints(lastAnchor.trackIndex, lastAnchor.segmentIndex, segment.trkpt.length, segment.trkpt.length, newSegment.trkpt.map((point) => point));
 | 
			
		||||
            newSegment._reverse(segment.getEndTimestamp(), segment.getEndTimestamp());
 | 
			
		||||
            file.replaceTrackPoints(lastAnchor.trackIndex, lastAnchor.segmentIndex, segment.trkpt.length, segment.trkpt.length, newSegment.trkpt.map((point) => point));
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,15 +23,11 @@ export function updateAnchorPoints(file: GPXFile) {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (segment.trkpt.length > 0) {
 | 
			
		||||
            if (!segment.trkpt[0]._data.anchor) { // First point is not an anchor, make it one
 | 
			
		||||
                segment.trkpt[0]._data.anchor = true;
 | 
			
		||||
                segment.trkpt[0]._data.zoom = 0;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!segment.trkpt[segment.trkpt.length - 1]._data.anchor) { // Last point is not an anchor, make it one
 | 
			
		||||
                segment.trkpt[segment.trkpt.length - 1]._data.anchor = true;
 | 
			
		||||
                segment.trkpt[segment.trkpt.length - 1]._data.zoom = 0;
 | 
			
		||||
            }
 | 
			
		||||
            // Ensure first and last points are anchors and always visible
 | 
			
		||||
            segment.trkpt[0]._data.anchor = true;
 | 
			
		||||
            segment.trkpt[0]._data.zoom = 0;
 | 
			
		||||
            segment.trkpt[segment.trkpt.length - 1]._data.anchor = true;
 | 
			
		||||
            segment.trkpt[segment.trkpt.length - 1]._data.zoom = 0;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import Dexie, { liveQuery } from 'dexie';
 | 
			
		||||
import { GPXFile, GPXStatistics, Track, TrackSegment, Waypoint, TrackPoint, type Coordinates, distance, type LineStyleExtension } from 'gpx';
 | 
			
		||||
import { enableMapSet, enablePatches, applyPatches, type Patch, type WritableDraft, castDraft, freeze, produceWithPatches, original, produce } from 'immer';
 | 
			
		||||
import { enableMapSet, enablePatches, applyPatches, type Patch, type WritableDraft, freeze, produceWithPatches } from 'immer';
 | 
			
		||||
import { writable, get, derived, type Readable, type Writable } from 'svelte/store';
 | 
			
		||||
import { gpxStatistics, initTargetMapBounds, splitAs, updateTargetMapBounds } from './stores';
 | 
			
		||||
import { gpxStatistics, initTargetMapBounds, splitAs, updateAllHidden, updateTargetMapBounds } from './stores';
 | 
			
		||||
import { mode } from 'mode-watcher';
 | 
			
		||||
import { defaultBasemap, defaultBasemapTree, defaultOverlayTree, defaultOverlays, type CustomLayer, defaultOpacities } from './assets/layers';
 | 
			
		||||
import { applyToOrderedItemsFromFile, applyToOrderedSelectedItemsFromFile, selection } from '$lib/components/file-list/Selection';
 | 
			
		||||
@@ -13,7 +13,6 @@ import { SplitType } from '$lib/components/toolbar/tools/Scissors.svelte';
 | 
			
		||||
enableMapSet();
 | 
			
		||||
enablePatches();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Database extends Dexie {
 | 
			
		||||
 | 
			
		||||
    fileids!: Dexie.Table<string, string>;
 | 
			
		||||
@@ -192,6 +191,10 @@ function dexieGPXFileStore(id: string): Readable<GPXFileWithStatistics> & { dest
 | 
			
		||||
                file: gpx,
 | 
			
		||||
                statistics
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (get(selection).hasAnyChildren(new ListFileItem(id))) {
 | 
			
		||||
                updateAllHidden();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    return {
 | 
			
		||||
@@ -203,10 +206,61 @@ function dexieGPXFileStore(id: string): Readable<GPXFileWithStatistics> & { dest
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updateSelection(updatedFiles: GPXFile[], deletedFileIds: string[]) {
 | 
			
		||||
    let removedItems: ListItem[] = [];
 | 
			
		||||
 | 
			
		||||
    applyToOrderedItemsFromFile(get(selection).getSelected(), (fileId, level, items) => {
 | 
			
		||||
        let file = updatedFiles.find((file) => file._data.id === fileId);
 | 
			
		||||
        if (file) {
 | 
			
		||||
            items.forEach((item) => {
 | 
			
		||||
                if (item instanceof ListTrackItem) {
 | 
			
		||||
                    let newTrackIndex = file.trk.findIndex((track) => track._data.trackIndex === item.getTrackIndex());
 | 
			
		||||
                    if (newTrackIndex === -1) {
 | 
			
		||||
                        removedItems.push(item);
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (item instanceof ListTrackSegmentItem) {
 | 
			
		||||
                    let newTrackIndex = file.trk.findIndex((track) => track._data.trackIndex === item.getTrackIndex());
 | 
			
		||||
                    if (newTrackIndex === -1) {
 | 
			
		||||
                        removedItems.push(item);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        let newSegmentIndex = file.trk[newTrackIndex].trkseg.findIndex((segment) => segment._data.segmentIndex === item.getSegmentIndex());
 | 
			
		||||
                        if (newSegmentIndex === -1) {
 | 
			
		||||
                            removedItems.push(item);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (item instanceof ListWaypointItem) {
 | 
			
		||||
                    let newWaypointIndex = file.wpt.findIndex((wpt) => wpt._data.index === item.getWaypointIndex());
 | 
			
		||||
                    if (newWaypointIndex === -1) {
 | 
			
		||||
                        removedItems.push(item);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        } else if (deletedFileIds.includes(fileId)) {
 | 
			
		||||
            items.forEach((item) => {
 | 
			
		||||
                removedItems.push(item);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (removedItems.length > 0) {
 | 
			
		||||
        selection.update(($selection) => {
 | 
			
		||||
            removedItems.forEach((item) => {
 | 
			
		||||
                if (item instanceof ListFileItem) {
 | 
			
		||||
                    $selection.deleteChild(item.getFileId());
 | 
			
		||||
                } else {
 | 
			
		||||
                    $selection.set(item, false);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            return $selection;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Commit the changes to the file state to the database
 | 
			
		||||
function commitFileStateChange(newFileState: ReadonlyMap<string, GPXFile>, patch: Patch[]) {
 | 
			
		||||
    let changedFileIds = getChangedFileIds(patch);
 | 
			
		||||
    let updatedFileIds: string[] = [], deletedFileIds: string[] = [];
 | 
			
		||||
 | 
			
		||||
    changedFileIds.forEach(id => {
 | 
			
		||||
        if (newFileState.has(id)) {
 | 
			
		||||
            updatedFileIds.push(id);
 | 
			
		||||
@@ -218,6 +272,8 @@ function commitFileStateChange(newFileState: ReadonlyMap<string, GPXFile>, patch
 | 
			
		||||
    let updatedFiles = updatedFileIds.map(id => newFileState.get(id)).filter(file => file !== undefined) as GPXFile[];
 | 
			
		||||
    updatedFileIds = updatedFiles.map(file => file._data.id);
 | 
			
		||||
 | 
			
		||||
    updateSelection(updatedFiles, deletedFileIds);
 | 
			
		||||
 | 
			
		||||
    return db.transaction('rw', db.fileids, db.files, async () => {
 | 
			
		||||
        if (updatedFileIds.length > 0) {
 | 
			
		||||
            await db.fileids.bulkPut(updatedFileIds, updatedFileIds);
 | 
			
		||||
@@ -255,14 +311,6 @@ liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => {
 | 
			
		||||
            });
 | 
			
		||||
            return $files;
 | 
			
		||||
        });
 | 
			
		||||
        if (deletedFiles.length > 0) {
 | 
			
		||||
            selection.update(($selection) => {
 | 
			
		||||
                deletedFiles.forEach((fileId) => {
 | 
			
		||||
                    $selection.deleteChild(fileId);
 | 
			
		||||
                });
 | 
			
		||||
                return $selection;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        settings.fileOrder.update((order) => {
 | 
			
		||||
            newFiles.forEach((fileId) => {
 | 
			
		||||
                if (!order.includes(fileId)) {
 | 
			
		||||
@@ -311,12 +359,12 @@ function applyGlobal(callback: (files: Map<string, GPXFile>) => void) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to apply a callback to multiple files
 | 
			
		||||
function applyToFiles(fileIds: string[], callback: (file: WritableDraft<GPXFile>) => GPXFile) {
 | 
			
		||||
function applyToFiles(fileIds: string[], callback: (file: WritableDraft<GPXFile>) => void) {
 | 
			
		||||
    const [newFileState, patch, inversePatch] = produceWithPatches(fileState, (draft) => {
 | 
			
		||||
        fileIds.forEach((fileId) => {
 | 
			
		||||
            let file = draft.get(fileId);
 | 
			
		||||
            if (file) {
 | 
			
		||||
                draft.set(fileId, castDraft(callback(file)));
 | 
			
		||||
                callback(file);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
@@ -327,12 +375,12 @@ function applyToFiles(fileIds: string[], callback: (file: WritableDraft<GPXFile>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to apply different callbacks to multiple files
 | 
			
		||||
function applyEachToFilesAndGlobal(fileIds: string[], callbacks: ((file: WritableDraft<GPXFile>, context?: any) => GPXFile)[], globalCallback: (files: Map<string, GPXFile>, context?: any) => void, context?: any) {
 | 
			
		||||
function applyEachToFilesAndGlobal(fileIds: string[], callbacks: ((file: WritableDraft<GPXFile>, context?: any) => void)[], globalCallback: (files: Map<string, GPXFile>, context?: any) => void, context?: any) {
 | 
			
		||||
    const [newFileState, patch, inversePatch] = produceWithPatches(fileState, (draft) => {
 | 
			
		||||
        fileIds.forEach((fileId, index) => {
 | 
			
		||||
            let file = draft.get(fileId);
 | 
			
		||||
            if (file) {
 | 
			
		||||
                draft.set(fileId, castDraft(callbacks[index](file, context)));
 | 
			
		||||
                callbacks[index](file, context);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        globalCallback(draft, context);
 | 
			
		||||
@@ -410,13 +458,13 @@ export const dbUtils = {
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    applyToFile: (id: string, callback: (file: WritableDraft<GPXFile>) => GPXFile) => {
 | 
			
		||||
    applyToFile: (id: string, callback: (file: WritableDraft<GPXFile>) => void) => {
 | 
			
		||||
        applyToFiles([id], callback);
 | 
			
		||||
    },
 | 
			
		||||
    applyToFiles: (ids: string[], callback: (file: WritableDraft<GPXFile>) => GPXFile) => {
 | 
			
		||||
    applyToFiles: (ids: string[], callback: (file: WritableDraft<GPXFile>) => void) => {
 | 
			
		||||
        applyToFiles(ids, callback);
 | 
			
		||||
    },
 | 
			
		||||
    applyEachToFilesAndGlobal: (ids: string[], callbacks: ((file: WritableDraft<GPXFile>, context?: any) => GPXFile)[], globalCallback: (files: Map<string, GPXFile>, context?: any) => void, context?: any) => {
 | 
			
		||||
    applyEachToFilesAndGlobal: (ids: string[], callbacks: ((file: WritableDraft<GPXFile>, context?: any) => void)[], globalCallback: (files: Map<string, GPXFile>, context?: any) => void, context?: any) => {
 | 
			
		||||
        applyEachToFilesAndGlobal(ids, callbacks, globalCallback, context);
 | 
			
		||||
    },
 | 
			
		||||
    duplicateSelection: () => {
 | 
			
		||||
@@ -427,36 +475,36 @@ export const dbUtils = {
 | 
			
		||||
            let ids = getFileIds(get(settings.fileOrder).length);
 | 
			
		||||
            let index = 0;
 | 
			
		||||
            applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
 | 
			
		||||
                let file = original(draft)?.get(fileId);
 | 
			
		||||
                if (file) {
 | 
			
		||||
                    let newFile = file;
 | 
			
		||||
                    if (level === ListLevel.FILE) {
 | 
			
		||||
                        newFile = file.clone();
 | 
			
		||||
                if (level === ListLevel.FILE) {
 | 
			
		||||
                    let file = getFile(fileId);
 | 
			
		||||
                    if (file) {
 | 
			
		||||
                        let newFile = file.clone();
 | 
			
		||||
                        newFile._data.id = ids[index++];
 | 
			
		||||
                    } else if (level === ListLevel.TRACK) {
 | 
			
		||||
                        for (let item of items) {
 | 
			
		||||
                            let trackIndex = (item as ListTrackItem).getTrackIndex();
 | 
			
		||||
                            let [result, _removed] = newFile.replaceTracks(trackIndex + 1, trackIndex, [file.trk[trackIndex].clone()]);
 | 
			
		||||
                            newFile = result;
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (level === ListLevel.SEGMENT) {
 | 
			
		||||
                        for (let item of items) {
 | 
			
		||||
                            let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
 | 
			
		||||
                            let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
 | 
			
		||||
                            let [result, _removed] = newFile.replaceTrackSegments(trackIndex, segmentIndex + 1, segmentIndex, [file.trk[trackIndex].trkseg[segmentIndex].clone()]);
 | 
			
		||||
                            newFile = result;
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (level === ListLevel.WAYPOINTS) {
 | 
			
		||||
                        let [result, _removed] = newFile.replaceWaypoints(file.wpt.length, file.wpt.length - 1, file.wpt.map((wpt) => wpt.clone()));
 | 
			
		||||
                        newFile = result;
 | 
			
		||||
                    } else if (level === ListLevel.WAYPOINT) {
 | 
			
		||||
                        for (let item of items) {
 | 
			
		||||
                            let waypointIndex = (item as ListWaypointItem).getWaypointIndex();
 | 
			
		||||
                            let [result, _removed] = newFile.replaceWaypoints(waypointIndex + 1, waypointIndex, [file.wpt[waypointIndex].clone()]);
 | 
			
		||||
                            newFile = result;
 | 
			
		||||
                        draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    let file = draft.get(fileId);
 | 
			
		||||
                    if (file) {
 | 
			
		||||
                        if (level === ListLevel.TRACK) {
 | 
			
		||||
                            for (let item of items) {
 | 
			
		||||
                                let trackIndex = (item as ListTrackItem).getTrackIndex();
 | 
			
		||||
                                file.replaceTracks(trackIndex + 1, trackIndex, [file.trk[trackIndex].clone()]);
 | 
			
		||||
                            }
 | 
			
		||||
                        } else if (level === ListLevel.SEGMENT) {
 | 
			
		||||
                            for (let item of items) {
 | 
			
		||||
                                let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
 | 
			
		||||
                                let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
 | 
			
		||||
                                file.replaceTrackSegments(trackIndex, segmentIndex + 1, segmentIndex, [file.trk[trackIndex].trkseg[segmentIndex].clone()]);
 | 
			
		||||
                            }
 | 
			
		||||
                        } else if (level === ListLevel.WAYPOINTS) {
 | 
			
		||||
                            file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, file.wpt.map((wpt) => wpt.clone()));
 | 
			
		||||
                        } else if (level === ListLevel.WAYPOINT) {
 | 
			
		||||
                            for (let item of items) {
 | 
			
		||||
                                let waypointIndex = (item as ListWaypointItem).getWaypointIndex();
 | 
			
		||||
                                file.replaceWaypoints(waypointIndex + 1, waypointIndex, [file.wpt[waypointIndex].clone()]);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
@@ -467,24 +515,22 @@ export const dbUtils = {
 | 
			
		||||
        }
 | 
			
		||||
        applyGlobal((draft) => {
 | 
			
		||||
            applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
 | 
			
		||||
                let file = original(draft)?.get(fileId);
 | 
			
		||||
                let file = draft.get(fileId);
 | 
			
		||||
                if (file) {
 | 
			
		||||
                    let newFile = file;
 | 
			
		||||
                    if (level === ListLevel.FILE) {
 | 
			
		||||
                        newFile = file.reverse();
 | 
			
		||||
                        file.reverse();
 | 
			
		||||
                    } else if (level === ListLevel.TRACK) {
 | 
			
		||||
                        for (let item of items) {
 | 
			
		||||
                            let trackIndex = (item as ListTrackItem).getTrackIndex();
 | 
			
		||||
                            newFile = newFile.reverseTrack(trackIndex);
 | 
			
		||||
                            file.reverseTrack(trackIndex);
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (level === ListLevel.SEGMENT) {
 | 
			
		||||
                        for (let item of items) {
 | 
			
		||||
                            let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
 | 
			
		||||
                            let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
 | 
			
		||||
                            newFile = newFile.reverseTrackSegment(trackIndex, segmentIndex);
 | 
			
		||||
                            file.reverseTrackSegment(trackIndex, segmentIndex);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
@@ -504,23 +550,15 @@ export const dbUtils = {
 | 
			
		||||
                wpt: []
 | 
			
		||||
            };
 | 
			
		||||
            applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
 | 
			
		||||
                let file = original(draft)?.get(fileId);
 | 
			
		||||
                if (file) {
 | 
			
		||||
                    let newFile = file;
 | 
			
		||||
                let file = draft.get(fileId);
 | 
			
		||||
                let originalFile = getFile(fileId);
 | 
			
		||||
                if (file && originalFile) {
 | 
			
		||||
                    if (level === ListLevel.FILE) {
 | 
			
		||||
                        {
 | 
			
		||||
                            let [result, removed] = newFile.replaceTracks(0, newFile.trk.length - 1, []);
 | 
			
		||||
                            toMerge.trk.push(...removed);
 | 
			
		||||
                            newFile = result;
 | 
			
		||||
                        }
 | 
			
		||||
                        {
 | 
			
		||||
                            let [result, removed] = newFile.replaceWaypoints(0, newFile.wpt.length - 1, []);
 | 
			
		||||
                            toMerge.wpt.push(...removed);
 | 
			
		||||
                            newFile = result;
 | 
			
		||||
                        }
 | 
			
		||||
                        toMerge.trk.push(...originalFile.trk.map((track) => track.clone()));
 | 
			
		||||
                        toMerge.wpt.push(...originalFile.wpt.map((wpt) => wpt.clone()));
 | 
			
		||||
                        if (first) {
 | 
			
		||||
                            target = items[0];
 | 
			
		||||
                            targetFile = newFile;
 | 
			
		||||
                            targetFile = file;
 | 
			
		||||
                        } else {
 | 
			
		||||
                            draft.delete(fileId);
 | 
			
		||||
                        }
 | 
			
		||||
@@ -528,15 +566,12 @@ export const dbUtils = {
 | 
			
		||||
                        if (level === ListLevel.TRACK) {
 | 
			
		||||
                            items.forEach((item, index) => {
 | 
			
		||||
                                let trackIndex = (item as ListTrackItem).getTrackIndex();
 | 
			
		||||
                                toMerge.trkseg.splice(0, 0, ...originalFile.trk[trackIndex].trkseg.map((segment) => segment.clone()));
 | 
			
		||||
                                if (index === items.length - 1) { // Order is reversed, so the last track is the first one and the one to keep
 | 
			
		||||
                                    let [result, removed] = newFile.replaceTrackSegments(trackIndex, 0, newFile.trk[trackIndex].trkseg.length - 1, []);
 | 
			
		||||
                                    toMerge.trkseg.splice(0, 0, ...removed);
 | 
			
		||||
                                    newFile = result;
 | 
			
		||||
                                    target = item;
 | 
			
		||||
                                    file.trk[trackIndex].trkseg = [];
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    let [result, removed] = newFile.replaceTracks(trackIndex, trackIndex, []);
 | 
			
		||||
                                    toMerge.trkseg.push(...removed[0].trkseg);
 | 
			
		||||
                                    newFile = result;
 | 
			
		||||
                                    file.trk.splice(trackIndex, 1);
 | 
			
		||||
                                }
 | 
			
		||||
                            });
 | 
			
		||||
                        } else if (level === ListLevel.SEGMENT) {
 | 
			
		||||
@@ -546,16 +581,11 @@ export const dbUtils = {
 | 
			
		||||
                                if (index === items.length - 1) { // Order is reversed, so the last segment is the first one and the one to keep
 | 
			
		||||
                                    target = item;
 | 
			
		||||
                                }
 | 
			
		||||
                                let [result, removed] = newFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, []);
 | 
			
		||||
                                toMerge.trkseg.splice(0, 0, ...removed);
 | 
			
		||||
                                newFile = result;
 | 
			
		||||
                                toMerge.trkseg.splice(0, 0, originalFile.trk[trackIndex].trkseg[segmentIndex].clone());
 | 
			
		||||
                                file.trk[trackIndex].trkseg.splice(segmentIndex, 1);
 | 
			
		||||
                            });
 | 
			
		||||
                        }
 | 
			
		||||
                        if (first) {
 | 
			
		||||
                            targetFile = newFile;
 | 
			
		||||
                        } else {
 | 
			
		||||
                            draft.set(fileId, freeze(newFile));
 | 
			
		||||
                        }
 | 
			
		||||
                        targetFile = file;
 | 
			
		||||
                    }
 | 
			
		||||
                    first = false;
 | 
			
		||||
                }
 | 
			
		||||
@@ -576,19 +606,20 @@ export const dbUtils = {
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (toMerge.trk.length > 0) {
 | 
			
		||||
                if (toMerge.trk.length > 0 && toMerge.trk[0].trkseg.length > 0) {
 | 
			
		||||
                    let s = new TrackSegment();
 | 
			
		||||
                    toMerge.trk.map((track) => {
 | 
			
		||||
                        track.trkseg.forEach((segment) => {
 | 
			
		||||
                            s = s.replaceTrackPoints(s.trkpt.length, s.trkpt.length, segment.trkpt.slice(), speed, startTime);
 | 
			
		||||
                            s.replaceTrackPoints(s.trkpt.length, s.trkpt.length, segment.trkpt.slice(), speed, startTime);
 | 
			
		||||
                        });
 | 
			
		||||
                    });
 | 
			
		||||
                    toMerge.trk = [toMerge.trk[0].replaceTrackSegments(0, toMerge.trk[0].trkseg.length - 1, [s])[0]];
 | 
			
		||||
                    toMerge.trk = [toMerge.trk[0]];
 | 
			
		||||
                    toMerge.trk[0].trkseg = [s];
 | 
			
		||||
                }
 | 
			
		||||
                if (toMerge.trkseg.length > 0) {
 | 
			
		||||
                    let s = new TrackSegment();
 | 
			
		||||
                    toMerge.trkseg.forEach((segment) => {
 | 
			
		||||
                        s = s.replaceTrackPoints(s.trkpt.length, s.trkpt.length, segment.trkpt.slice(), speed, startTime);
 | 
			
		||||
                        s.replaceTrackPoints(s.trkpt.length, s.trkpt.length, segment.trkpt.slice(), speed, startTime);
 | 
			
		||||
                    });
 | 
			
		||||
                    toMerge.trkseg = [s];
 | 
			
		||||
                }
 | 
			
		||||
@@ -596,17 +627,16 @@ export const dbUtils = {
 | 
			
		||||
 | 
			
		||||
            if (targetFile) {
 | 
			
		||||
                if (target instanceof ListFileItem) {
 | 
			
		||||
                    targetFile = targetFile.replaceTracks(0, targetFile.trk.length - 1, toMerge.trk)[0];
 | 
			
		||||
                    targetFile = targetFile.replaceWaypoints(0, targetFile.wpt.length - 1, toMerge.wpt)[0];
 | 
			
		||||
                    targetFile.replaceTracks(0, targetFile.trk.length - 1, toMerge.trk);
 | 
			
		||||
                    targetFile.replaceWaypoints(0, targetFile.wpt.length - 1, toMerge.wpt);
 | 
			
		||||
                } else if (target instanceof ListTrackItem) {
 | 
			
		||||
                    let trackIndex = target.getTrackIndex();
 | 
			
		||||
                    targetFile = targetFile.replaceTrackSegments(trackIndex, 0, -1, toMerge.trkseg)[0];
 | 
			
		||||
                    targetFile.replaceTrackSegments(trackIndex, 0, -1, toMerge.trkseg);
 | 
			
		||||
                } else if (target instanceof ListTrackSegmentItem) {
 | 
			
		||||
                    let trackIndex = target.getTrackIndex();
 | 
			
		||||
                    let segmentIndex = target.getSegmentIndex();
 | 
			
		||||
                    targetFile = targetFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex - 1, toMerge.trkseg)[0];
 | 
			
		||||
                    targetFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex - 1, toMerge.trkseg);
 | 
			
		||||
                }
 | 
			
		||||
                draft.set(targetFile._data.id, freeze(targetFile));
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
@@ -616,27 +646,24 @@ export const dbUtils = {
 | 
			
		||||
        }
 | 
			
		||||
        applyGlobal((draft) => {
 | 
			
		||||
            applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
 | 
			
		||||
                let file = original(draft)?.get(fileId);
 | 
			
		||||
                let file = draft.get(fileId);
 | 
			
		||||
                if (file) {
 | 
			
		||||
                    if (level === ListLevel.FILE) {
 | 
			
		||||
                        let length = file.getNumberOfTrackPoints();
 | 
			
		||||
                        if (start >= length || end < 0) {
 | 
			
		||||
                            draft.delete(fileId);
 | 
			
		||||
                        } else if (start > 0 || end < length - 1) {
 | 
			
		||||
                            let newFile = file.crop(Math.max(0, start), Math.min(length - 1, end));
 | 
			
		||||
                            draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                            file.crop(Math.max(0, start), Math.min(length - 1, end));
 | 
			
		||||
                        }
 | 
			
		||||
                        start -= length;
 | 
			
		||||
                        end -= length;
 | 
			
		||||
                    } else if (level === ListLevel.TRACK) {
 | 
			
		||||
                        let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex());
 | 
			
		||||
                        let newFile = file.crop(start, end, trackIndices);
 | 
			
		||||
                        draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                        file.crop(start, end, trackIndices);
 | 
			
		||||
                    } else if (level === ListLevel.SEGMENT) {
 | 
			
		||||
                        let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
 | 
			
		||||
                        let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex());
 | 
			
		||||
                        let newFile = file.crop(start, end, trackIndices, segmentIndices);
 | 
			
		||||
                        draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                        file.crop(start, end, trackIndices, segmentIndices);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }, false);
 | 
			
		||||
@@ -645,9 +672,9 @@ export const dbUtils = {
 | 
			
		||||
    extractSelection: () => {
 | 
			
		||||
        return applyGlobal((draft) => {
 | 
			
		||||
            applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
 | 
			
		||||
                let file = original(draft)?.get(fileId);
 | 
			
		||||
                if (file) {
 | 
			
		||||
                    if (level === ListLevel.FILE) {
 | 
			
		||||
                if (level === ListLevel.FILE) {
 | 
			
		||||
                    let file = getFile(fileId);
 | 
			
		||||
                    if (file) {
 | 
			
		||||
                        if (file.trk.length > 1) {
 | 
			
		||||
                            let fileIds = getFileIds(file.trk.length);
 | 
			
		||||
 | 
			
		||||
@@ -675,25 +702,24 @@ export const dbUtils = {
 | 
			
		||||
                            });
 | 
			
		||||
 | 
			
		||||
                            file.trk.forEach((track, index) => {
 | 
			
		||||
                                let newFile = file.clone();
 | 
			
		||||
                                let tracks = track.trkseg.map((segment, segmentIndex) => {
 | 
			
		||||
                                    let t = track.replaceTrackSegments(0, track.trkseg.length - 1, [segment])[0];
 | 
			
		||||
                                    let t = track.clone();
 | 
			
		||||
                                    t.replaceTrackSegments(0, track.trkseg.length - 1, [segment]);
 | 
			
		||||
                                    if (track.name) {
 | 
			
		||||
                                        t.name = `${track.name} (${segmentIndex + 1})`;
 | 
			
		||||
                                    }
 | 
			
		||||
                                    return t;
 | 
			
		||||
                                });
 | 
			
		||||
                                let newFile = file.replaceTracks(0, file.trk.length - 1, tracks)[0];
 | 
			
		||||
                                newFile = newFile.replaceWaypoints(0, file.wpt.length - 1, closest.filter((c) => c.index.includes(index)).map((c) => file.wpt[c.wptIndex]))[0];
 | 
			
		||||
                                newFile = produce(newFile, (f) => {
 | 
			
		||||
                                    f._data.id = fileIds[index];
 | 
			
		||||
                                    f.metadata.name = track.name ?? `${file.metadata.name} (${index + 1})`;
 | 
			
		||||
                                });
 | 
			
		||||
                                newFile.replaceTracks(0, file.trk.length - 1, tracks);
 | 
			
		||||
                                newFile.replaceWaypoints(0, file.wpt.length - 1, closest.filter((c) => c.index.includes(index)).map((c) => file.wpt[c.wptIndex]));
 | 
			
		||||
                                newFile._data.id = fileIds[index];
 | 
			
		||||
                                newFile.metadata.name = track.name ?? `${file.metadata.name} (${index + 1})`;
 | 
			
		||||
                                draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                            });
 | 
			
		||||
                        } else if (file.trk.length === 1) {
 | 
			
		||||
                            let fileIds = getFileIds(file.trk[0].trkseg.length);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                            let closest = file.wpt.map((wpt, wptIndex) => {
 | 
			
		||||
                                return {
 | 
			
		||||
                                    wptIndex: wptIndex,
 | 
			
		||||
@@ -716,31 +742,32 @@ export const dbUtils = {
 | 
			
		||||
                            });
 | 
			
		||||
 | 
			
		||||
                            file.trk[0].trkseg.forEach((segment, index) => {
 | 
			
		||||
                                let newFile = file.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [segment])[0];
 | 
			
		||||
                                newFile = newFile.replaceWaypoints(0, file.wpt.length - 1, closest.filter((c) => c.index.includes(index)).map((c) => file.wpt[c.wptIndex]))[0];
 | 
			
		||||
                                newFile = produce(newFile, (f) => {
 | 
			
		||||
                                    f._data.id = fileIds[index];
 | 
			
		||||
                                    f.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`;
 | 
			
		||||
                                });
 | 
			
		||||
                                let newFile = file.clone();
 | 
			
		||||
                                newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [segment]);
 | 
			
		||||
                                newFile.replaceWaypoints(0, file.wpt.length - 1, closest.filter((c) => c.index.includes(index)).map((c) => file.wpt[c.wptIndex]));
 | 
			
		||||
                                newFile._data.id = fileIds[index];
 | 
			
		||||
                                newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`;
 | 
			
		||||
                                draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                            });
 | 
			
		||||
                        }
 | 
			
		||||
                        draft.delete(fileId);
 | 
			
		||||
                    } else if (level === ListLevel.TRACK) {
 | 
			
		||||
                        let newFile = file;
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (level === ListLevel.TRACK) {
 | 
			
		||||
                    let file = draft.get(fileId);
 | 
			
		||||
                    if (file) {
 | 
			
		||||
                        for (let item of items) {
 | 
			
		||||
                            let trackIndex = (item as ListTrackItem).getTrackIndex();
 | 
			
		||||
                            let track = file.trk[trackIndex];
 | 
			
		||||
                            let tracks = track.trkseg.map((segment, segmentIndex) => {
 | 
			
		||||
                                let t = track.clone().replaceTrackSegments(0, track.trkseg.length - 1, [segment])[0];
 | 
			
		||||
                                let t = track.clone();
 | 
			
		||||
                                t.replaceTrackSegments(0, track.trkseg.length - 1, [segment]);
 | 
			
		||||
                                if (track.name) {
 | 
			
		||||
                                    t.name = `${track.name} (${segmentIndex + 1})`;
 | 
			
		||||
                                }
 | 
			
		||||
                                return t;
 | 
			
		||||
                            });
 | 
			
		||||
                            newFile = newFile.replaceTracks(trackIndex, trackIndex, tracks)[0];
 | 
			
		||||
                            file.replaceTracks(trackIndex, trackIndex, tracks);
 | 
			
		||||
                        }
 | 
			
		||||
                        draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
@@ -749,7 +776,7 @@ export const dbUtils = {
 | 
			
		||||
    split(fileId: string, trackIndex: number, segmentIndex: number, coordinates: Coordinates) {
 | 
			
		||||
        let splitType = get(splitAs);
 | 
			
		||||
        return applyGlobal((draft) => {
 | 
			
		||||
            let file = original(draft)?.get(fileId);
 | 
			
		||||
            let file = getFile(fileId);
 | 
			
		||||
            if (file) {
 | 
			
		||||
                let segment = file.trk[trackIndex].trkseg[segmentIndex];
 | 
			
		||||
 | 
			
		||||
@@ -772,18 +799,32 @@ export const dbUtils = {
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                if (splitType === SplitType.FILES) {
 | 
			
		||||
                    let newFile = file.crop(0, absoluteIndex);
 | 
			
		||||
                    draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                    let newFile2 = file.clone();
 | 
			
		||||
                    newFile2._data.id = getFileIds(1)[0];
 | 
			
		||||
                    newFile2 = newFile2.crop(absoluteIndex, file.getNumberOfTrackPoints() - 1);
 | 
			
		||||
                    draft.set(newFile2._data.id, freeze(newFile2));
 | 
			
		||||
                    let newFile = draft.get(fileId);
 | 
			
		||||
                    if (newFile) {
 | 
			
		||||
                        newFile.crop(0, absoluteIndex);
 | 
			
		||||
                        let newFile2 = file.clone();
 | 
			
		||||
                        newFile2._data.id = getFileIds(1)[0];
 | 
			
		||||
                        newFile2.crop(absoluteIndex, file.getNumberOfTrackPoints() - 1);
 | 
			
		||||
                        draft.set(newFile2._data.id, freeze(newFile2));
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (splitType === SplitType.TRACKS) {
 | 
			
		||||
                    let newFile = file.replaceTracks(trackIndex, trackIndex, [file.trk[trackIndex].crop(0, absoluteIndex), file.trk[trackIndex].crop(absoluteIndex, file.trk[trackIndex].getNumberOfTrackPoints() - 1)])[0];
 | 
			
		||||
                    draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                    let newFile = draft.get(fileId);
 | 
			
		||||
                    if (newFile) {
 | 
			
		||||
                        let start = file.trk[trackIndex].clone();
 | 
			
		||||
                        start.crop(0, absoluteIndex);
 | 
			
		||||
                        let end = file.trk[trackIndex].clone();
 | 
			
		||||
                        end.crop(absoluteIndex, file.trk[trackIndex].getNumberOfTrackPoints() - 1);
 | 
			
		||||
                        newFile.replaceTracks(trackIndex, trackIndex, [start, end]);
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (splitType === SplitType.SEGMENTS) {
 | 
			
		||||
                    let newFile = file.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, [segment.crop(0, minIndex), segment.crop(minIndex, segment.trkpt.length - 1)])[0];
 | 
			
		||||
                    draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                    let newFile = draft.get(fileId);
 | 
			
		||||
                    if (newFile) {
 | 
			
		||||
                        let start = segment.clone();
 | 
			
		||||
                        start.crop(0, minIndex);
 | 
			
		||||
                        let end = segment.clone();
 | 
			
		||||
                        end.crop(minIndex, segment.trkpt.length - 1);
 | 
			
		||||
                        newFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, [start, end]);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
@@ -794,25 +835,23 @@ export const dbUtils = {
 | 
			
		||||
        }
 | 
			
		||||
        applyGlobal((draft) => {
 | 
			
		||||
            applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
 | 
			
		||||
                let file = original(draft)?.get(fileId);
 | 
			
		||||
                let file = draft.get(fileId);
 | 
			
		||||
                if (file) {
 | 
			
		||||
                    let newFile = file;
 | 
			
		||||
                    if (level === ListLevel.FILE) {
 | 
			
		||||
                        newFile = file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints);
 | 
			
		||||
                        file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints);
 | 
			
		||||
                    } else if (level === ListLevel.TRACK) {
 | 
			
		||||
                        let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex());
 | 
			
		||||
                        newFile = newFile.clean(bounds, inside, deleteTrackPoints, deleteWaypoints, trackIndices);
 | 
			
		||||
                        file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints, trackIndices);
 | 
			
		||||
                    } else if (level === ListLevel.SEGMENT) {
 | 
			
		||||
                        let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
 | 
			
		||||
                        let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex());
 | 
			
		||||
                        newFile = newFile.clean(bounds, inside, deleteTrackPoints, deleteWaypoints, trackIndices, segmentIndices);
 | 
			
		||||
                        file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints, trackIndices, segmentIndices);
 | 
			
		||||
                    } else if (level === ListLevel.WAYPOINTS) {
 | 
			
		||||
                        newFile = newFile.clean(bounds, inside, false, deleteWaypoints);
 | 
			
		||||
                        file.clean(bounds, inside, false, deleteWaypoints);
 | 
			
		||||
                    } else if (level === ListLevel.WAYPOINT) {
 | 
			
		||||
                        let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex());
 | 
			
		||||
                        newFile = newFile.clean(bounds, inside, false, deleteWaypoints, [], [], waypointIndices);
 | 
			
		||||
                        file.clean(bounds, inside, false, deleteWaypoints, [], [], waypointIndices);
 | 
			
		||||
                    }
 | 
			
		||||
                    draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
@@ -824,20 +863,18 @@ export const dbUtils = {
 | 
			
		||||
        applyGlobal((draft) => {
 | 
			
		||||
            let allItems = Array.from(itemsAndPoints.keys());
 | 
			
		||||
            applyToOrderedItemsFromFile(allItems, (fileId, level, items) => {
 | 
			
		||||
                let file = original(draft)?.get(fileId);
 | 
			
		||||
                let file = draft.get(fileId);
 | 
			
		||||
                if (file) {
 | 
			
		||||
                    let newFile = file;
 | 
			
		||||
                    for (let item of items) {
 | 
			
		||||
                        if (item instanceof ListTrackSegmentItem) {
 | 
			
		||||
                            let trackIndex = item.getTrackIndex();
 | 
			
		||||
                            let segmentIndex = item.getSegmentIndex();
 | 
			
		||||
                            let points = itemsAndPoints.get(item);
 | 
			
		||||
                            if (points) {
 | 
			
		||||
                                newFile = newFile.replaceTrackPoints(trackIndex, segmentIndex, 0, file.trk[trackIndex].trkseg[segmentIndex].getNumberOfTrackPoints() - 1, points);
 | 
			
		||||
                                file.replaceTrackPoints(trackIndex, segmentIndex, 0, file.trk[trackIndex].trkseg[segmentIndex].getNumberOfTrackPoints() - 1, points);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
@@ -848,21 +885,47 @@ export const dbUtils = {
 | 
			
		||||
        }
 | 
			
		||||
        applyGlobal((draft) => {
 | 
			
		||||
            applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
 | 
			
		||||
                let file = original(draft)?.get(fileId);
 | 
			
		||||
                let file = draft.get(fileId);
 | 
			
		||||
                if (file && (level === ListLevel.FILE || level === ListLevel.TRACK)) {
 | 
			
		||||
                    let newFile = file;
 | 
			
		||||
                    if (level === ListLevel.FILE) {
 | 
			
		||||
                        newFile = file.setStyle(style);
 | 
			
		||||
                        file.setStyle(style);
 | 
			
		||||
                    } else if (level === ListLevel.TRACK) {
 | 
			
		||||
                        for (let item of items) {
 | 
			
		||||
                            let trackIndex = (item as ListTrackItem).getTrackIndex();
 | 
			
		||||
                            newFile = newFile.replaceTracks(trackIndex, trackIndex, [file.trk[trackIndex].setStyle(style)])[0];
 | 
			
		||||
                        }
 | 
			
		||||
                        if (items.length === file.trk.length) {
 | 
			
		||||
                            newFile = newFile.setStyle(style);
 | 
			
		||||
                            file.setStyle(style);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            for (let item of items) {
 | 
			
		||||
                                let trackIndex = (item as ListTrackItem).getTrackIndex();
 | 
			
		||||
                                file.trk[trackIndex].setStyle(style);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    setHiddenToSelection: (hidden: boolean) => {
 | 
			
		||||
        if (get(selection).size === 0) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        applyGlobal((draft) => {
 | 
			
		||||
            applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
 | 
			
		||||
                let file = draft.get(fileId);
 | 
			
		||||
                if (file) {
 | 
			
		||||
                    if (level === ListLevel.FILE) {
 | 
			
		||||
                        file.setHidden(hidden);
 | 
			
		||||
                    } else if (level === ListLevel.TRACK) {
 | 
			
		||||
                        let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex());
 | 
			
		||||
                        file.setHidden(hidden, trackIndices);
 | 
			
		||||
                    } else if (level === ListLevel.SEGMENT) {
 | 
			
		||||
                        let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
 | 
			
		||||
                        let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex());
 | 
			
		||||
                        file.setHidden(hidden, trackIndices, segmentIndices);
 | 
			
		||||
                    } else if (level === ListLevel.WAYPOINTS) {
 | 
			
		||||
                        file.setHiddenWaypoints(hidden);
 | 
			
		||||
                    } else if (level === ListLevel.WAYPOINT) {
 | 
			
		||||
                        let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex());
 | 
			
		||||
                        file.setHiddenWaypoints(hidden, waypointIndices);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
@@ -876,33 +939,27 @@ export const dbUtils = {
 | 
			
		||||
                if (level === ListLevel.FILE) {
 | 
			
		||||
                    draft.delete(fileId);
 | 
			
		||||
                } else {
 | 
			
		||||
                    let file = original(draft)?.get(fileId);
 | 
			
		||||
                    let file = draft.get(fileId);
 | 
			
		||||
                    if (file) {
 | 
			
		||||
                        let newFile = file;
 | 
			
		||||
                        if (level === ListLevel.TRACK) {
 | 
			
		||||
                            for (let item of items) {
 | 
			
		||||
                                let trackIndex = (item as ListTrackItem).getTrackIndex();
 | 
			
		||||
                                let [result, _removed] = newFile.replaceTracks(trackIndex, trackIndex, []);
 | 
			
		||||
                                newFile = result;
 | 
			
		||||
                                file.replaceTracks(trackIndex, trackIndex, []);
 | 
			
		||||
                            }
 | 
			
		||||
                        } else if (level === ListLevel.SEGMENT) {
 | 
			
		||||
                            for (let item of items) {
 | 
			
		||||
                                let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
 | 
			
		||||
                                let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
 | 
			
		||||
                                let [result, _removed] = newFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, []);
 | 
			
		||||
                                newFile = result;
 | 
			
		||||
                                file.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, []);
 | 
			
		||||
                            }
 | 
			
		||||
                        } else if (level === ListLevel.WAYPOINTS) {
 | 
			
		||||
                            let [result, _removed] = newFile.replaceWaypoints(0, newFile.wpt.length - 1, []);
 | 
			
		||||
                            newFile = result;
 | 
			
		||||
                            file.replaceWaypoints(0, file.wpt.length - 1, []);
 | 
			
		||||
                        } else if (level === ListLevel.WAYPOINT) {
 | 
			
		||||
                            for (let item of items) {
 | 
			
		||||
                                let waypointIndex = (item as ListWaypointItem).getWaypointIndex();
 | 
			
		||||
                                let [result, _removed] = newFile.replaceWaypoints(waypointIndex, waypointIndex, []);
 | 
			
		||||
                                newFile = result;
 | 
			
		||||
                                file.replaceWaypoints(waypointIndex, waypointIndex, []);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        draft.set(newFile._data.id, freeze(newFile));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 
 | 
			
		||||
@@ -344,56 +344,35 @@ export function exportFile(file: GPXFile) {
 | 
			
		||||
    URL.revokeObjectURL(url);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const anyHidden = writable(false);
 | 
			
		||||
function updateAnyHidden() {
 | 
			
		||||
    anyHidden.set(get(selection).getSelected().some((item) => {
 | 
			
		||||
        let layer = gpxLayers.get(item.getFileId());
 | 
			
		||||
        return layer && layer.hidden;
 | 
			
		||||
    }));
 | 
			
		||||
}
 | 
			
		||||
selection.subscribe(updateAnyHidden);
 | 
			
		||||
export const allHidden = writable(false);
 | 
			
		||||
 | 
			
		||||
export function toggleSelectionVisibility() {
 | 
			
		||||
    let files = new Set<string>();
 | 
			
		||||
    get(selection).forEach((item) => {
 | 
			
		||||
        files.add(item.getFileId());
 | 
			
		||||
    });
 | 
			
		||||
    files.forEach((fileId) => {
 | 
			
		||||
        let layer = gpxLayers.get(fileId);
 | 
			
		||||
        if (layer) {
 | 
			
		||||
            layer.toggleVisibility();
 | 
			
		||||
export function updateAllHidden() {
 | 
			
		||||
    let hidden = true;
 | 
			
		||||
    applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
 | 
			
		||||
        let file = getFile(fileId);
 | 
			
		||||
        if (file) {
 | 
			
		||||
            for (let item of items) {
 | 
			
		||||
                if (!hidden) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (item instanceof ListFileItem) {
 | 
			
		||||
                    hidden = hidden && (file._data.hidden === true);
 | 
			
		||||
                } else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
 | 
			
		||||
                    hidden = hidden && (file.trk[item.getTrackIndex()]._data.hidden === true);
 | 
			
		||||
                } else if (item instanceof ListTrackSegmentItem && item.getTrackIndex() < file.trk.length && item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length) {
 | 
			
		||||
                    hidden = hidden && (file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()]._data.hidden === true);
 | 
			
		||||
                } else if (item instanceof ListWaypointsItem) {
 | 
			
		||||
                    hidden = hidden && (file._data.hiddenWpt === true);
 | 
			
		||||
                } else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) {
 | 
			
		||||
                    hidden = hidden && (file.wpt[item.getWaypointIndex()]._data.hidden === true);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    updateAnyHidden();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function hideSelection() {
 | 
			
		||||
    let files = new Set<string>();
 | 
			
		||||
    get(selection).forEach((item) => {
 | 
			
		||||
        files.add(item.getFileId());
 | 
			
		||||
    });
 | 
			
		||||
    files.forEach((fileId) => {
 | 
			
		||||
        let layer = gpxLayers.get(fileId);
 | 
			
		||||
        if (layer && !layer.hidden) {
 | 
			
		||||
            layer.toggleVisibility();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    anyHidden.set(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function showSelection() {
 | 
			
		||||
    let files = new Set<string>();
 | 
			
		||||
    get(selection).forEach((item) => {
 | 
			
		||||
        files.add(item.getFileId());
 | 
			
		||||
    });
 | 
			
		||||
    files.forEach((fileId) => {
 | 
			
		||||
        let layer = gpxLayers.get(fileId);
 | 
			
		||||
        if (layer && layer.hidden) {
 | 
			
		||||
            layer.toggleVisibility();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    anyHidden.set(false);
 | 
			
		||||
    allHidden.set(hidden);
 | 
			
		||||
}
 | 
			
		||||
selection.subscribe(updateAllHidden);
 | 
			
		||||
 | 
			
		||||
export const editMetadata = writable(false);
 | 
			
		||||
export const editStyle = writable(false);
 | 
			
		||||
 
 | 
			
		||||
@@ -60,13 +60,13 @@
 | 
			
		||||
        "click": "Click",
 | 
			
		||||
        "drag": "Drag",
 | 
			
		||||
        "metadata": {
 | 
			
		||||
            "button": "Edit info",
 | 
			
		||||
            "button": "Info...",
 | 
			
		||||
            "name": "Name",
 | 
			
		||||
            "description": "Description",
 | 
			
		||||
            "save": "Save"
 | 
			
		||||
        },
 | 
			
		||||
        "style": {
 | 
			
		||||
            "button": "Change style",
 | 
			
		||||
            "button": "Appearance...",
 | 
			
		||||
            "color": "Color",
 | 
			
		||||
            "opacity": "Opacity",
 | 
			
		||||
            "weight": "Weight"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user