From a48608840f372d28ddeb0611c2ce18a6776e22d2 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 7 Oct 2026 18:36:00 +0200 Subject: [PATCH] reverse --- gpx-rs/engine/src/core/algorithm/mod.rs | 2 + gpx-rs/engine/src/core/algorithm/reverse.rs | 194 ++++++++++++ .../src/engine/command/tools/reverse.rs | 291 +++++++++++++++++- .../toolbar/tools/routing/Routing.svelte | 2 +- website/src/lib/engine.ts | 9 + website/src/lib/logic/file-actions.ts | 260 ---------------- 6 files changed, 493 insertions(+), 265 deletions(-) create mode 100644 gpx-rs/engine/src/core/algorithm/reverse.rs diff --git a/gpx-rs/engine/src/core/algorithm/mod.rs b/gpx-rs/engine/src/core/algorithm/mod.rs index 5fa253462..4e5d539dc 100644 --- a/gpx-rs/engine/src/core/algorithm/mod.rs +++ b/gpx-rs/engine/src/core/algorithm/mod.rs @@ -1,8 +1,10 @@ mod anchors; +mod reverse; mod simplify; mod smooth; mod timestamps; pub use anchors::*; +pub use reverse::*; pub use simplify::*; pub use timestamps::*; diff --git a/gpx-rs/engine/src/core/algorithm/reverse.rs b/gpx-rs/engine/src/core/algorithm/reverse.rs new file mode 100644 index 000000000..cc8412cd6 --- /dev/null +++ b/gpx-rs/engine/src/core/algorithm/reverse.rs @@ -0,0 +1,194 @@ +//! Reversing a path: the order of its trackpoints, and of the segments and tracks that hold them. +//! +//! The timestamps are mirrored, so that the reversed path goes through the same period of time: +//! a trackpoint at time `t` of a part that spans `start..end` ends up at `start + end - t`. When +//! the part is made of several ones (the segments of a track, the tracks of a file), the gaps +//! between them are kept: every part comes after the previous one in the new order, as late as the +//! original one came after the part that followed it. + +use crate::{Track, TrackSegment, Trackpoint}; + +/// The time of the first trackpoint of a segment. +pub fn start_time(segment: &TrackSegment) -> Option { + segment.iter().next().and_then(|trkpt| trkpt.time) +} + +/// The time of the last trackpoint of a segment. +pub fn end_time(segment: &TrackSegment) -> Option { + segment.last_index().and_then(|idx| segment[idx].time) +} + +/// The time of the first trackpoint of a track. +pub fn track_start_time(track: &Track) -> Option { + track.trkseg.first().and_then(start_time) +} + +/// The time of the last trackpoint of a track. +pub fn track_end_time(track: &Track) -> Option { + track.trkseg.last().and_then(end_time) +} + +/// Reverses the trackpoints of a segment. +/// +/// `original_next` is the time at which what came after the segment started, and `new_previous` +/// the time at which what comes before it now ends: the segment is mirrored to start there. Both +/// are those of the segment itself if the segment is reversed on its own, which are the ones +/// used when they are missing (`new_previous` is the start of the segment, and `original_next` +/// follows from the duration of the segment). Without timestamps, the trackpoints are only put +/// in the opposite order. +pub fn reverse_segment( + segment: &mut TrackSegment, + original_next: Option, + new_previous: Option, +) { + let (original_start, original_end) = (start_time(segment), end_time(segment)); + let new_previous = new_previous.or(original_start); + let original_next = original_next.or_else(|| { + // the segment lasts as long as before + Some(new_previous? + original_end? - original_start?) + }); + + let len = segment.len(); + let mut points: Vec = segment.iter().cloned().collect(); + if let (Some(next), Some(previous), Some(end)) = (original_next, new_previous, original_end) { + let new_start = previous + next - end; + for point in points.iter_mut() { + point.time = point.time.map(|time| new_start + (end - time)); + } + } + points.reverse(); + segment.splice(0, len, points); + segment.rev_id = Default::default(); +} + +/// Reverses the segments of a track, and their trackpoints: see [`reverse_segment`] for the +/// meaning of the times, which are the ones of the track as a whole. +pub fn reverse_segments( + segments: &mut [TrackSegment], + original_next: Option, + new_previous: Option, +) { + segments.reverse(); + let (mut next, mut previous) = (original_next, new_previous); + for segment in segments.iter_mut() { + // the next one is the one that was before, which started here + let original_start = start_time(segment); + reverse_segment(segment, next, previous); + next = original_start; + previous = end_time(segment); + } +} + +/// Reverses the tracks of a file, and their segments and trackpoints. +pub fn reverse_tracks(tracks: &mut [Track], original_next: Option, new_previous: Option) { + tracks.reverse(); + let (mut next, mut previous) = (original_next, new_previous); + for track in tracks.iter_mut() { + let original_start = track_start_time(track); + reverse_segments(&mut track.trkseg, next, previous); + next = original_start; + previous = track_end_time(track); + } +} + +#[cfg(test)] +mod tests { + use crate::LngLat; + + use super::*; + + fn segment(points: &[(f64, Option)]) -> TrackSegment { + let mut segment = TrackSegment::default(); + let points = points + .iter() + .map(|(lng, time)| Trackpoint { + coordinates: LngLat { + lng: *lng, + lat: 0.0, + }, + time: *time, + ..Default::default() + }) + .collect(); + segment.splice(0, 0, points); + segment + } + + fn lngs(segment: &TrackSegment) -> Vec { + segment.iter().map(|p| p.coordinates.lng).collect() + } + + fn times(segment: &TrackSegment) -> Vec> { + segment.iter().map(|p| p.time).collect() + } + + #[test] + fn test_a_segment_is_mirrored_in_its_own_time_span() { + let mut s = segment(&[(0.0, Some(0)), (1.0, Some(10)), (2.0, Some(40))]); + let (start, end) = (start_time(&s), end_time(&s)); + reverse_segment(&mut s, end, start); + assert_eq!(lngs(&s), [2.0, 1.0, 0.0]); + // same span, the durations between points follow the points + assert_eq!(times(&s), [Some(0), Some(30), Some(40)]); + } + + #[test] + fn test_reverse_segment_defaults() { + // without the times around, the segment is mirrored where it is + let mut s = segment(&[(0.0, Some(100)), (1.0, Some(130))]); + reverse_segment(&mut s, None, None); + assert_eq!(times(&s), [Some(100), Some(130)]); + assert_eq!(lngs(&s), [1.0, 0.0]); + } + + #[test] + fn test_a_segment_without_times_is_only_reversed() { + let mut s = segment(&[(0.0, None), (1.0, None), (2.0, None)]); + reverse_segment(&mut s, None, None); + assert_eq!(lngs(&s), [2.0, 1.0, 0.0]); + assert_eq!(times(&s), [None, None, None]); + } + + #[test] + fn test_segments_keep_the_gaps_between_them() { + // 0..20, then 100..120: a gap of 80 + let mut segments = vec![ + segment(&[(0.0, Some(0)), (1.0, Some(10)), (2.0, Some(20))]), + segment(&[(3.0, Some(100)), (4.0, Some(110)), (5.0, Some(120))]), + ]; + let (end, start) = (end_time(&segments[1]), start_time(&segments[0])); + reverse_segments(&mut segments, end, start); + assert_eq!(lngs(&segments[0]), [5.0, 4.0, 3.0]); + assert_eq!(lngs(&segments[1]), [2.0, 1.0, 0.0]); + assert_eq!(times(&segments[0]), [Some(0), Some(10), Some(20)]); + // the gap is still 80 + assert_eq!(times(&segments[1]), [Some(100), Some(110), Some(120)]); + } + + #[test] + fn test_tracks_are_reversed_like_segments() { + let track = |segments: Vec| Track { + trkseg: segments, + ..Default::default() + }; + let mut tracks = vec![ + track(vec![ + segment(&[(0.0, Some(0)), (1.0, Some(10))]), + segment(&[(2.0, Some(50)), (3.0, Some(60))]), + ]), + track(vec![segment(&[(4.0, Some(200)), (5.0, Some(230))])]), + ]; + let (end, start) = (track_end_time(&tracks[1]), track_start_time(&tracks[0])); + reverse_tracks(&mut tracks, end, start); + let all: Vec = tracks + .iter() + .flat_map(|t| t.trkseg.iter().flat_map(lngs)) + .collect(); + assert_eq!(all, [5.0, 4.0, 3.0, 2.0, 1.0, 0.0]); + // 3 segments: durations 10, 10, 30 and gaps 40 and 140 are reversed to 30, 10, 10 and + // gaps 140 and 40 (the first part of the time is the longest) + assert_eq!(times(&tracks[0].trkseg[0]), [Some(0), Some(30)]); + assert_eq!(times(&tracks[1].trkseg[0]), [Some(170), Some(180)]); + assert_eq!(times(&tracks[1].trkseg[1]), [Some(220), Some(230)]); + } +} diff --git a/gpx-rs/engine/src/engine/command/tools/reverse.rs b/gpx-rs/engine/src/engine/command/tools/reverse.rs index ff06eb5a5..2689a3304 100644 --- a/gpx-rs/engine/src/engine/command/tools/reverse.rs +++ b/gpx-rs/engine/src/engine/command/tools/reverse.rs @@ -1,11 +1,294 @@ -use crate::{Apply, CommandError, State}; +use std::rc::Rc; +use crate::{ + Apply, CommandError, File, FileId, Selection, State, TrackId, TrackSegmentId, end_time, + reverse_segment, reverse_segments, reverse_tracks, start_time, track_end_time, + track_start_time, +}; + +/// Reverses the direction of the selected elements: the whole of each selected file (its tracks, +/// the segments of its tracks and their trackpoints are in the opposite order), of each selected +/// track (its segments and their trackpoints), or each selected segment on its own (its +/// trackpoints). The timestamps are mirrored, see [`reverse_segment`]. #[derive(Debug)] pub struct Reverse; impl Apply for Reverse { - fn apply(self, _state: &mut State) -> Result<(), CommandError> { - // TODO - Err(CommandError::NotImplemented("reverse")) + fn apply(self, state: &mut State) -> Result<(), CommandError> { + let locations = state + .selection + .segment_locations(state.files, &state.order.0); + let points: usize = locations + .iter() + .map(|location| { + state.files[&location.file_id].trk[location.trk].trkseg[location.seg].len() + }) + .sum(); + if points <= 1 { + return Err(CommandError::NothingToDo); + } + + let mut files: Vec<(FileId, File)> = vec![]; + let mut edit = |file_id: &FileId, f: &dyn Fn(&mut File)| { + if let Some(file) = state.files.get(file_id) { + let mut file = (**file).clone(); + f(&mut file); + files.push((*file_id, file)); + } + }; + match &*state.selection { + Selection::File { file_ids } => { + for file_id in file_ids { + edit(file_id, &|file| { + let end = file.trk.last().and_then(track_end_time); + let start = file.trk.first().and_then(track_start_time); + reverse_tracks(&mut file.trk, end, start); + }); + } + } + Selection::Track { file_id, trk_ids } => edit(file_id, &|file| { + reverse_selected_tracks(file, trk_ids); + }), + Selection::TrackSegment { + file_id, + trk_id, + trkseg_ids, + } => edit(file_id, &|file| { + reverse_selected_segments(file, *trk_id, trkseg_ids); + }), + Selection::Empty | Selection::Waypoints { .. } | Selection::Waypoint { .. } => {} + } + + for (file_id, file) in files { + state.files.insert(file_id, Rc::new(file)); + } + Ok(()) + } +} + +fn reverse_selected_tracks(file: &mut File, ids: &std::collections::HashSet) { + for track in file.trk.iter_mut().filter(|track| ids.contains(&track.id)) { + let end = track.trkseg.last().and_then(end_time); + let start = track.trkseg.first().and_then(start_time); + reverse_segments(&mut track.trkseg, end, start); + } +} + +fn reverse_selected_segments( + file: &mut File, + track_id: TrackId, + ids: &std::collections::HashSet, +) { + for track in file.trk.iter_mut().filter(|track| track.id == track_id) { + for segment in track + .trkseg + .iter_mut() + .filter(|segment| ids.contains(&segment.id)) + { + let (end, start) = (end_time(segment), start_time(segment)); + reverse_segment(segment, end, start); + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use crate::{Load, TrackSegment, engine::command::fixture::Fixture}; + + use super::*; + + fn loaded(path: &str) -> (Fixture, FileId) { + let mut fx = Fixture::default(); + let data = std::fs::read(path).unwrap(); + Load { + data: &data, + name: "file", + } + .apply(&mut fx.state()) + .unwrap(); + let id = fx.order.0[0]; + fx.selection = Selection::File { + file_ids: HashSet::from([id]), + }; + (fx, id) + } + + fn all_segments(fx: &Fixture, id: FileId) -> Vec { + fx.files[&id] + .trk + .iter() + .flat_map(|track| track.trkseg.iter().cloned()) + .collect() + } + + fn lngs(segment: &TrackSegment) -> Vec { + segment.iter().map(|p| p.coordinates.lng).collect() + } + + #[test] + fn test_reversing_a_file_reverses_tracks_segments_and_points() { + let (mut fx, id) = loaded("data/with_tracks_and_segments.gpx"); + let before = all_segments(&fx, id); + assert!(before.len() >= 3); + let (tracks_before, ids_before): (Vec, Vec<_>) = ( + fx.files[&id].trk.iter().map(|t| t.trkseg.len()).collect(), + fx.files[&id].trk.iter().map(|t| t.id).collect(), + ); + + Reverse.apply(&mut fx.state()).unwrap(); + + let after = all_segments(&fx, id); + assert_eq!(after.len(), before.len()); + // the whole path is the opposite one + let path = + |segments: &[TrackSegment]| -> Vec { segments.iter().flat_map(lngs).collect() }; + let mut expected = path(&before); + expected.reverse(); + assert_eq!(path(&after), expected); + // the tracks are in the opposite order, each with the same number of segments + let tracks_after: Vec = fx.files[&id].trk.iter().map(|t| t.trkseg.len()).collect(); + let mut reversed = tracks_before.clone(); + reversed.reverse(); + assert_eq!(tracks_after, reversed); + let ids_after: Vec<_> = fx.files[&id].trk.iter().map(|t| t.id).collect(); + let mut reversed_ids = ids_before; + reversed_ids.reverse(); + assert_eq!(ids_after, reversed_ids); + // the segments changed, and they still end where they should + assert!(after.iter().all(|s| s.rev_id != before[0].rev_id)); + for segment in &after { + assert_eq!(segment[0].anchor, Some(0)); + assert_eq!(segment[segment.len() - 1].anchor, Some(0)); + } + } + + #[test] + fn test_reversing_a_file_twice_gives_the_same_path_and_times() { + let (mut fx, id) = loaded("data/with_tracks_and_segments.gpx"); + let before = all_segments(&fx, id); + let times = |segments: &[TrackSegment]| -> Vec> { + segments + .iter() + .flat_map(|s| s.iter().map(|p| p.time)) + .collect() + }; + Reverse.apply(&mut fx.state()).unwrap(); + Reverse.apply(&mut fx.state()).unwrap(); + let after = all_segments(&fx, id); + for (a, b) in before.iter().zip(&after) { + assert_eq!(lngs(a), lngs(b)); + assert_eq!(a.id, b.id); + } + assert_eq!(times(&before), times(&after)); + } + + #[test] + fn test_reversing_segments_keeps_the_others() { + let (mut fx, id) = loaded("data/with_tracks_and_segments.gpx"); + let file = fx.files[&id].clone(); + let trk = &file.trk[0]; + assert!(trk.trkseg.len() >= 2); + let target = &trk.trkseg[1]; + fx.selection = Selection::TrackSegment { + file_id: id, + trk_id: trk.id, + trkseg_ids: HashSet::from([target.id]), + }; + Reverse.apply(&mut fx.state()).unwrap(); + + let after = &fx.files[&id].trk[0]; + // the segment is reversed in place + let mut expected = lngs(target); + expected.reverse(); + assert_eq!(lngs(&after.trkseg[1]), expected); + assert_eq!(after.trkseg[1].id, target.id); + assert_ne!(after.trkseg[1].rev_id, target.rev_id); + // the other ones are the same + for (i, segment) in trk.trkseg.iter().enumerate().filter(|(i, _)| *i != 1) { + assert_eq!(after.trkseg[i].rev_id, segment.rev_id, "{i}"); + } + assert_eq!(fx.files[&id].trk.len(), file.trk.len()); + } + + #[test] + fn test_reversing_tracks_reverses_their_segments_in_place() { + let (mut fx, id) = loaded("data/with_tracks_and_segments.gpx"); + let file = fx.files[&id].clone(); + let (index, trk) = file + .trk + .iter() + .enumerate() + .find(|(_, t)| t.trkseg.len() >= 2) + .unwrap(); + fx.selection = Selection::Track { + file_id: id, + trk_ids: HashSet::from([trk.id]), + }; + Reverse.apply(&mut fx.state()).unwrap(); + + let after = &fx.files[&id]; + // the track stays where it is, its segments are the opposite way + assert_eq!(after.trk[index].id, trk.id); + let path = |t: &crate::Track| -> Vec { t.trkseg.iter().flat_map(lngs).collect() }; + let mut expected = path(trk); + expected.reverse(); + assert_eq!(path(&after.trk[index]), expected); + // and the other tracks do not change + for (i, other) in file.trk.iter().enumerate().filter(|(i, _)| *i != index) { + assert_eq!(path(&after.trk[i]), path(other)); + } + } + + #[test] + fn test_times_are_mirrored() { + let (mut fx, id) = loaded("data/with_time.gpx"); + let before = all_segments(&fx, id).remove(0); + Reverse.apply(&mut fx.state()).unwrap(); + let after = all_segments(&fx, id).remove(0); + let (start, end) = (start_time(&before).unwrap(), end_time(&before).unwrap()); + // the same period of time, the first point of the new path is the last one + assert_eq!(start_time(&after), Some(start)); + assert_eq!(end_time(&after), Some(end)); + let n = before.len(); + for i in 0..n { + assert_eq!( + after[i].time.unwrap(), + start + end - before[n - 1 - i].time.unwrap() + ); + assert_eq!(after[i].coordinates.lng, before[n - 1 - i].coordinates.lng); + } + } + + #[test] + fn test_nothing_to_reverse() { + let (mut fx, id) = loaded("data/simple.gpx"); + let before = fx.files[&id].clone(); + fx.selection = Selection::Empty; + assert_eq!( + Reverse.apply(&mut fx.state()), + Err(CommandError::NothingToDo) + ); + fx.selection = Selection::Waypoints { file_id: id }; + assert_eq!( + Reverse.apply(&mut fx.state()), + Err(CommandError::NothingToDo) + ); + assert!(Rc::ptr_eq(&fx.files[&id], &before)); + + // a single trackpoint has no direction + let mut file = (*fx.files[&id]).clone(); + let segment = &mut file.trk[0].trkseg[0]; + let len = segment.len(); + segment.splice(1, len, vec![]); + fx.files.insert(id, Rc::new(file)); + fx.selection = Selection::File { + file_ids: HashSet::from([id]), + }; + assert_eq!( + Reverse.apply(&mut fx.state()), + Err(CommandError::NothingToDo) + ); } } diff --git a/website/src/lib/components/toolbar/tools/routing/Routing.svelte b/website/src/lib/components/toolbar/tools/routing/Routing.svelte index f0cec8696..61be3b0b8 100644 --- a/website/src/lib/components/toolbar/tools/routing/Routing.svelte +++ b/website/src/lib/components/toolbar/tools/routing/Routing.svelte @@ -156,7 +156,7 @@ variant="outline" class="gap-1 text-xs px-1.5 py-1.5 h-fit" disabled={!validSelection} - onclick={fileActions.reverseSelection} + onclick={() => engine.reverse()} > {i18n._('toolbar.routing.reverse.button')} diff --git a/website/src/lib/engine.ts b/website/src/lib/engine.ts index 80097c195..7bf501a1b 100644 --- a/website/src/lib/engine.ts +++ b/website/src/lib/engine.ts @@ -636,6 +636,15 @@ class Engine { return this.run((w) => w.new_track_segment()); } + /** + * Reverses the direction of the selection: the whole of each selected file or track, or each + * selected segment on its own. The timestamps are mirrored. Resolves to `false` when there is + * nothing to reverse (fewer than two trackpoints). + */ + reverse() { + return this.run((w) => w.reverse()); + } + metadata(name: string, desc: string) { return this.run((w) => w.metadata(name, desc)); } diff --git a/website/src/lib/logic/file-actions.ts b/website/src/lib/logic/file-actions.ts index d2516369a..2e9ebe398 100644 --- a/website/src/lib/logic/file-actions.ts +++ b/website/src/lib/logic/file-actions.ts @@ -101,121 +101,6 @@ export async function loadFiles(list: FileList | File[]): Promise { // Helper functions for file operations export const fileActions = { - add: (file: GPXFile) => { - if (file._data.id === undefined) { - file._data.id = getFileIds(1)[0]; - } - return fileActionManager.applyGlobal((draft) => { - draft.set(file._data.id, freeze(file)); - }); - }, - addMultiple: (files: GPXFile[]) => { - let ids = getFileIds(files.length); - fileActionManager.applyGlobal((draft) => { - files.forEach((file, index) => { - file._data.id = ids[index]; - draft.set(file._data.id, freeze(file)); - }); - }); - return ids; - }, - duplicateSelection: () => { - if (get(selection).size === 0) { - return; - } - fileActionManager.applyGlobal((draft) => { - let ids = getFileIds(get(settings.fileOrder).length); - let index = 0; - selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - if (level === ListLevel.FILE) { - let file = fileStateCollection.getFile(fileId); - if (file) { - let newFile = file.clone(); - newFile._data.id = ids[index++]; - 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(), - ]); - } - } - } - } - }); - }); - }, - addNewTrack: (fileId: string) => { - fileActionManager.applyToFile(fileId, (file) => - file.replaceTracks(file.trk.length, file.trk.length, [new Track()]) - ); - }, - addNewSegment: (fileId: string, trackIndex: number) => { - fileActionManager.applyToFile(fileId, (file) => { - let track = file.trk[trackIndex]; - track.replaceTrackSegments(track.trkseg.length, track.trkseg.length, [ - new TrackSegment(), - ]); - }); - }, - reverseSelection: () => { - if ( - !get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) || - get(gpxStatistics).global.length <= 1 - ) { - return; - } - fileActionManager.applyGlobal((draft) => { - selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - let file = draft.get(fileId); - if (file) { - if (level === ListLevel.FILE) { - file.reverse(); - } else if (level === ListLevel.TRACK) { - for (let item of items) { - let trackIndex = (item as ListTrackItem).getTrackIndex(); - 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(); - file.reverseTrackSegment(trackIndex, segmentIndex); - } - } - } - }); - }); - }, createRoundTripForSelection() { if (!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) { return; @@ -601,136 +486,6 @@ export const fileActions = { }); }); }, - addOrUpdateWaypoint: (waypoint: WaypointType, item?: ListWaypointItem) => { - getElevation([waypoint.attributes]).then((elevation) => { - if (item) { - fileActionManager.applyToFile(item.getFileId(), (file) => { - let wpt = file.wpt[item.getWaypointIndex()]; - wpt.name = waypoint.name; - wpt.desc = waypoint.desc; - wpt.cmt = waypoint.cmt; - wpt.sym = waypoint.sym; - wpt.link = waypoint.link; - wpt.setCoordinates(waypoint.attributes); - wpt.ele = elevation[0]; - }); - } else { - let fileIds = new Set(); - get(selection) - .getSelected() - .forEach((item) => { - fileIds.add(item.getFileId()); - }); - let wpt = new Waypoint(waypoint); - wpt.ele = elevation[0]; - fileActionManager.applyToFiles(Array.from(fileIds), (file) => - file.replaceWaypoints(file.wpt.length, file.wpt.length, [wpt]) - ); - } - }); - }, - deleteWaypoint: (fileId: string, waypointIndex: number) => { - fileActionManager.applyToFile(fileId, (file) => - file.replaceWaypoints(waypointIndex, waypointIndex, []) - ); - }, - setStyleToSelection: (style: LineStyleExtension) => { - if (get(selection).size === 0) { - return; - } - fileActionManager.applyGlobal((draft) => { - selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - let file = draft.get(fileId); - if (file && (level === ListLevel.FILE || level === ListLevel.TRACK)) { - if (level === ListLevel.FILE) { - file.setStyle(style); - } else if (level === ListLevel.TRACK) { - if (items.length === file.trk.length) { - file.setStyle(style); - } else { - for (let item of items) { - let trackIndex = (item as ListTrackItem).getTrackIndex(); - file.trk[trackIndex].setStyle(style); - } - } - } - } - }); - }); - }, - setHiddenToSelection: (hidden: boolean) => { - if (get(selection).size === 0) { - return; - } - fileActionManager.applyGlobal((draft) => { - selection.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); - } - } - }); - }); - }, - deleteSelection: () => { - if (get(selection).size === 0) { - return; - } - fileActionManager.applyGlobal((draft) => { - selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - if (level === ListLevel.FILE) { - draft.delete(fileId); - } 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, trackIndex, []); - } - } 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, - segmentIndex, - [] - ); - } - } else if (level === ListLevel.WAYPOINTS) { - file.replaceWaypoints(0, file.wpt.length - 1, []); - } else if (level === ListLevel.WAYPOINT) { - for (let item of items) { - let waypointIndex = (item as ListWaypointItem).getWaypointIndex(); - file.replaceWaypoints(waypointIndex, waypointIndex, []); - } - } - } - } - }); - }); - }, addElevationToSelection: async () => { if (get(selection).size === 0) { return; @@ -799,19 +554,4 @@ export const fileActions = { }); }); }, - deleteSelectedFiles: () => { - if (get(selection).size === 0) { - return; - } - fileActionManager.applyGlobal((draft) => { - selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - draft.delete(fileId); - }); - }); - }, - deleteAllFiles: () => { - fileActionManager.applyGlobal((draft) => { - draft.clear(); - }); - }, };