diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index 7adf1f60..3eb15ef7 100644 --- a/gpx/src/gpx.ts +++ b/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(obj: T): T { if (obj === null || typeof obj !== 'object') { @@ -68,33 +68,31 @@ abstract class GPXTreeNode> extends GPXTreeElement // Producers _reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date) { - return produce(this, (draft: Draft>) => { - 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{ attributes: GPXFileAttributes; metadata: Metadata; - readonly wpt: ReadonlyArray>; - readonly trk: ReadonlyArray; + 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{ 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 { @@ -200,51 +209,23 @@ export class GPXFile extends GPXTreeNode{ } // 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{ } 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{ export class Track extends GPXTreeNode { [immerable] = true; - readonly name?: string; - readonly cmt?: string; - readonly desc?: string; - readonly src?: string; - readonly link?: Link; - readonly type?: string; - readonly trkseg: ReadonlyArray; - 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 { } // 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>; + 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>, 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>, di return result; } -function distanceWindowSmoothingWithDistanceAccumulator(points: ReadonlyArray>, 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())); } diff --git a/gpx/src/simplify.ts b/gpx/src/simplify.ts index dfa880c2..16d6d8fe 100644 --- a/gpx/src/simplify.ts +++ b/gpx/src/simplify.ts @@ -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 diff --git a/gpx/src/types.ts b/gpx/src/types.ts index a6ab6fc0..05033174 100644 --- a/gpx/src/types.ts +++ b/gpx/src/types.ts @@ -1,8 +1,8 @@ export type GPXFileType = { attributes: GPXFileAttributes; metadata: Metadata; - wpt: ReadonlyArray; - trk: ReadonlyArray; + wpt: WaypointType[]; + trk: TrackType[]; }; export type GPXFileAttributes = { @@ -52,7 +52,7 @@ export type TrackType = { src?: string; link?: Link; type?: string; - trkseg: ReadonlyArray; + trkseg: TrackSegmentType[]; extensions?: TrackExtensions; }; @@ -67,7 +67,7 @@ export type LineStyleExtension = { }; export type TrackSegmentType = { - trkpt: ReadonlyArray; + trkpt: TrackPointType[]; }; export type TrackPointType = { diff --git a/website/src/lib/components/Menu.svelte b/website/src/lib/components/Menu.svelte index b2c70028..ae00b296 100644 --- a/website/src/lib/components/Menu.svelte +++ b/website/src/lib/components/Menu.svelte @@ -48,11 +48,8 @@ triggerFileInput, createFile, loadFiles, - toggleSelectionVisibility, updateSelectionFromKey, - showSelection, - hideSelection, - anyHidden, + allHidden, editMetadata, editStyle, exportState, @@ -217,6 +214,7 @@ > {$_('menu.metadata.button')} + { - if ($anyHidden) { - showSelection(); + if ($allHidden) { + dbUtils.setHiddenToSelection(false); } else { - hideSelection(); + dbUtils.setHiddenToSelection(true); } }} disabled={$selection.size == 0} > - {#if $anyHidden} + {#if $allHidden} {$_('menu.unhide')} {:else} @@ -248,7 +246,7 @@ - + {$_('menu.select_all')} @@ -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(); diff --git a/website/src/lib/components/file-list/FileList.svelte b/website/src/lib/components/file-list/FileList.svelte index 46549f95..5f5e33eb 100644 --- a/website/src/lib/components/file-list/FileList.svelte +++ b/website/src/lib/components/file-list/FileList.svelte @@ -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 @@ + + + {$_('menu.select_all')} + + + { - 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)); } } diff --git a/website/src/lib/components/file-list/FileListNode.svelte b/website/src/lib/components/file-list/FileListNode.svelte index 972b2a47..2e054b9e 100644 --- a/website/src/lib/components/file-list/FileListNode.svelte +++ b/website/src/lib/components/file-list/FileListNode.svelte @@ -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> | GPXTreeElement - | ReadonlyArray> - | Readonly; + | Waypoint[] + | Waypoint; export let item: ListItem; let recursive = getContext('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') : ''; diff --git a/website/src/lib/components/file-list/FileListNodeContent.svelte b/website/src/lib/components/file-list/FileListNodeContent.svelte index 2d110594..3be75eab 100644 --- a/website/src/lib/components/file-list/FileListNodeContent.svelte +++ b/website/src/lib/components/file-list/FileListNodeContent.svelte @@ -6,21 +6,28 @@ @@ -153,7 +143,9 @@ /> {/if} { if (e.ctrlKey) { // Add to selection instead of opening context menu @@ -189,9 +181,18 @@ {:else if item.level === ListLevel.WAYPOINT} {/if} - + {label} + {#if hidden} + + {/if} @@ -200,41 +201,39 @@ ($editMetadata = true)}> {$_('menu.metadata.button')} + ($editStyle = true)}> {$_('menu.style.button')} - {#if item instanceof ListFileItem} - { - if ($anyHidden) { - showSelection(); - } else { - hideSelection(); - } - }} - > - {#if $anyHidden} - - {$_('menu.unhide')} - {:else} - - {$_('menu.hide')} - {/if} - - - {/if} - {/if} + { + if ($allHidden) { + dbUtils.setHiddenToSelection(false); + } else { + dbUtils.setHiddenToSelection(true); + } + }} + > + {#if $allHidden} + + {$_('menu.unhide')} + {:else} + + {$_('menu.hide')} + {/if} + + + {#if $verticalFileView} {#if item instanceof ListFileItem} - 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()]) )} > @@ -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()] + ) ); }} > diff --git a/website/src/lib/components/file-list/MetadataDialog.svelte b/website/src/lib/components/file-list/MetadataDialog.svelte index 317b2f33..3d2ce706 100644 --- a/website/src/lib/components/file-list/MetadataDialog.svelte +++ b/website/src/lib/components/file-list/MetadataDialog.svelte @@ -11,10 +11,7 @@ import { _ } from 'svelte-i18n'; import { editMetadata } from '$lib/stores'; - export let node: - | GPXTreeElement - | ReadonlyArray> - | Readonly; + export let node: GPXTreeElement | 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; }} diff --git a/website/src/lib/components/gpx-layer/GPXLayer.ts b/website/src/lib/components/gpx-layer/GPXLayer.ts index 282826ab..9649410d 100644 --- a/website/src/lib/components/gpx-layer/GPXLayer.ts +++ b/website/src/lib/components/gpx-layer/GPXLayer.ts @@ -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; 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) { diff --git a/website/src/lib/components/gpx-layer/WaypointPopup.ts b/website/src/lib/components/gpx-layer/WaypointPopup.ts index db8696f0..0f8820de 100644 --- a/website/src/lib/components/gpx-layer/WaypointPopup.ts +++ b/website/src/lib/components/gpx-layer/WaypointPopup.ts @@ -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, [])); } \ No newline at end of file diff --git a/website/src/lib/components/toolbar/tools/Time.svelte b/website/src/lib/components/toolbar/tools/Time.svelte index 32d81c70..6524b633 100644 --- a/website/src/lib/components/toolbar/tools/Time.svelte +++ b/website/src/lib/components/toolbar/tools/Time.svelte @@ -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, diff --git a/website/src/lib/components/toolbar/tools/Waypoint.svelte b/website/src/lib/components/toolbar/tools/Waypoint.svelte index b6e4b08c..76d98355 100644 --- a/website/src/lib/components/toolbar/tools/Waypoint.svelte +++ b/website/src/lib/components/toolbar/tools/Waypoint.svelte @@ -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(); @@ -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); diff --git a/website/src/lib/components/toolbar/tools/routing/Routing.svelte b/website/src/lib/components/toolbar/tools/routing/Routing.svelte index 09bfbc4f..4348f264 100644 --- a/website/src/lib/components/toolbar/tools/routing/Routing.svelte +++ b/website/src/lib/components/toolbar/tools/routing/Routing.svelte @@ -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); } diff --git a/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts b/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts index be816b70..52551f07 100644 --- a/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts +++ b/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts @@ -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)); }); } diff --git a/website/src/lib/components/toolbar/tools/routing/Simplify.ts b/website/src/lib/components/toolbar/tools/routing/Simplify.ts index 19b9ed16..a3d7b012 100644 --- a/website/src/lib/components/toolbar/tools/routing/Simplify.ts +++ b/website/src/lib/components/toolbar/tools/routing/Simplify.ts @@ -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; } } } diff --git a/website/src/lib/db.ts b/website/src/lib/db.ts index 18b5832e..d4746e6c 100644 --- a/website/src/lib/db.ts +++ b/website/src/lib/db.ts @@ -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; @@ -192,6 +191,10 @@ function dexieGPXFileStore(id: string): Readable & { dest file: gpx, statistics }); + + if (get(selection).hasAnyChildren(new ListFileItem(id))) { + updateAllHidden(); + } } }); return { @@ -203,10 +206,61 @@ function dexieGPXFileStore(id: string): Readable & { 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, 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, 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) => void) { } // Helper function to apply a callback to multiple files -function applyToFiles(fileIds: string[], callback: (file: WritableDraft) => GPXFile) { +function applyToFiles(fileIds: string[], callback: (file: WritableDraft) => 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 } // Helper function to apply different callbacks to multiple files -function applyEachToFilesAndGlobal(fileIds: string[], callbacks: ((file: WritableDraft, context?: any) => GPXFile)[], globalCallback: (files: Map, context?: any) => void, context?: any) { +function applyEachToFilesAndGlobal(fileIds: string[], callbacks: ((file: WritableDraft, context?: any) => void)[], globalCallback: (files: Map, 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) => { + applyToFile: (id: string, callback: (file: WritableDraft) => void) => { applyToFiles([id], callback); }, - applyToFiles: (ids: string[], callback: (file: WritableDraft) => GPXFile) => { + applyToFiles: (ids: string[], callback: (file: WritableDraft) => void) => { applyToFiles(ids, callback); }, - applyEachToFilesAndGlobal: (ids: string[], callbacks: ((file: WritableDraft, context?: any) => GPXFile)[], globalCallback: (files: Map, context?: any) => void, context?: any) => { + applyEachToFilesAndGlobal: (ids: string[], callbacks: ((file: WritableDraft, context?: any) => void)[], globalCallback: (files: Map, 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)); } } }); diff --git a/website/src/lib/stores.ts b/website/src/lib/stores.ts index e69e32d2..e8d3da8a 100644 --- a/website/src/lib/stores.ts +++ b/website/src/lib/stores.ts @@ -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(); - 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(); - 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(); - 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); diff --git a/website/src/locales/en.json b/website/src/locales/en.json index e2afbf18..c894f163 100644 --- a/website/src/locales/en.json +++ b/website/src/locales/en.json @@ -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"