diff --git a/gpx-rs/engine/src/core/gpx/waypoint.rs b/gpx-rs/engine/src/core/gpx/waypoint.rs index e6c4c93cb..bd35617ed 100644 --- a/gpx-rs/engine/src/core/gpx/waypoint.rs +++ b/gpx-rs/engine/src/core/gpx/waypoint.rs @@ -24,3 +24,19 @@ pub struct Waypoint { pub sym: Option, pub type_: Option, } + +impl Waypoint { + /// Whether the waypoints are the same, whatever their ids. + pub fn same_content(&self, other: &Waypoint) -> bool { + self.coordinates.lng == other.coordinates.lng + && self.coordinates.lat == other.coordinates.lat + && self.ele == other.ele + && self.time == other.time + && self.name == other.name + && self.cmt == other.cmt + && self.desc == other.desc + && self.link == other.link + && self.sym == other.sym + && self.type_ == other.type_ + } +} diff --git a/gpx-rs/engine/src/engine/command/tools/merge.rs b/gpx-rs/engine/src/engine/command/tools/merge.rs index 8c659df65..68907bc4e 100644 --- a/gpx-rs/engine/src/engine/command/tools/merge.rs +++ b/gpx-rs/engine/src/engine/command/tools/merge.rs @@ -1,19 +1,584 @@ -use crate::{Apply, CommandError, State}; +use std::{collections::HashSet, rc::Rc}; + +use crate::{ + Apply, CommandError, FileId, GlobalStatistics, Selection, State, Statistics, Track, TrackId, + TrackSegment, TrackSegmentId, Waypoint, Waypoints, replace_trackpoints, +}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MergeType { + /// The trackpoints of the elements are connected in a single segment. Connect, + /// The elements are put together, in the first one, which keeps them as they are. Group, } +/// Merges the selected elements into the first of them: +/// +/// - files: the other files are deleted. The tracks of all of them are in the first file, as +/// they are with `Group`, or all connected in the first track with `Connect`. The waypoints of +/// all of them are in the first file too, except the ones that are the same. +/// - tracks of a file: the first track gets their segments, as they are with `Group`, or all +/// connected in one segment with `Connect`. The other tracks are removed. +/// - segments of a track: they are all connected in a segment at the position of the first one +/// with `Connect`. With `Group` they are put one after the other, at that position. +/// +/// The segment made by `Connect` goes on in time, if there are timestamps, at the moving speed +/// of the selection, see [`replace_trackpoints`], which `remove_gaps` is given. +/// +/// There is nothing to do when there is nothing to merge: a single file to group, a single track +/// that has no more than one segment, a single segment... #[derive(Debug)] pub struct Merge { pub type_: MergeType, + pub remove_gaps: bool, } impl Apply for Merge { - fn apply(self, _state: &mut State) -> Result<(), CommandError> { - // TODO - Err(CommandError::NotImplemented("merge")) + fn apply(self, state: &mut State) -> Result<(), CommandError> { + let selection = state.selection.clone(); + match &selection { + Selection::File { file_ids } => self.merge_files(state, file_ids), + Selection::Track { file_id, trk_ids } => self.merge_tracks(state, *file_id, trk_ids), + Selection::TrackSegment { + file_id, + trk_id, + trkseg_ids, + } => self.merge_segments(state, *file_id, *trk_id, trkseg_ids), + Selection::Empty | Selection::Waypoints { .. } | Selection::Waypoint { .. } => { + Err(CommandError::NothingToDo) + } + } + } +} + +impl Merge { + fn merge_files( + &self, + state: &mut State, + file_ids: &HashSet, + ) -> Result<(), CommandError> { + // in the order of the files + let ordered: Vec = state + .order + .0 + .iter() + .filter(|id| file_ids.contains(id)) + .chain(file_ids.iter().filter(|id| !state.order.0.contains(id))) + .filter(|id| state.files.contains_key(id)) + .copied() + .collect(); + let Some(&target_id) = ordered.first() else { + return Err(CommandError::NothingToDo); + }; + + let mut tracks: Vec = vec![]; + let mut waypoints: Vec = vec![]; + for id in &ordered { + let file = &state.files[id]; + tracks.extend(file.trk.iter().cloned()); + for waypoint in file.wpt.iter() { + if !waypoints.iter().any(|kept| kept.same_content(waypoint)) { + waypoints.push(waypoint.clone()); + } + } + } + let segments: Vec = tracks + .iter() + .flat_map(|track| track.trkseg.iter().cloned()) + .collect(); + + match self.type_ { + MergeType::Group if ordered.len() == 1 => return Err(CommandError::NothingToDo), + MergeType::Connect if ordered.len() == 1 && segments.len() <= 1 => { + return Err(CommandError::NothingToDo); + } + MergeType::Connect if !segments.is_empty() => { + // a single track, which is the first one, with the connected segment + let merged = connect(&segments, segments[0].id, self.remove_gaps); + let first = Track { + trkseg: vec![merged], + ..tracks[0].clone() + }; + tracks = vec![first]; + } + _ => {} + } + + let mut target = (*state.files[&target_id]).clone(); + target.trk = tracks; + target.wpt = Waypoints::default(); + target.wpt.insert_at(0, waypoints); + for id in &ordered[1..] { + state.files.remove(id); + } + state.order.0.retain(|id| !ordered[1..].contains(id)); + state.files.insert(target_id, Rc::new(target)); + Ok(()) + } + + fn merge_tracks( + &self, + state: &mut State, + file_id: FileId, + trk_ids: &HashSet, + ) -> Result<(), CommandError> { + let file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?; + let selected: Vec = (0..file.trk.len()) + .filter(|i| trk_ids.contains(&file.trk[*i].id)) + .collect(); + let Some(&first) = selected.first() else { + return Err(CommandError::NothingToDo); + }; + let segments: Vec = selected + .iter() + .flat_map(|i| file.trk[*i].trkseg.iter().cloned()) + .collect(); + if selected.len() == 1 && (self.type_ == MergeType::Group || segments.len() <= 1) { + return Err(CommandError::NothingToDo); + } + + let merged = match self.type_ { + MergeType::Group => segments, + MergeType::Connect if segments.is_empty() => vec![], + MergeType::Connect => { + vec![connect(&segments, segments[0].id, self.remove_gaps)] + } + }; + let mut file = (**file).clone(); + file.trk[first].trkseg = merged; + let mut index = 0; + file.trk.retain(|_| { + index += 1; + !selected[1..].contains(&(index - 1)) + }); + state.files.insert(file_id, Rc::new(file)); + Ok(()) + } + + fn merge_segments( + &self, + state: &mut State, + file_id: FileId, + trk_id: TrackId, + ids: &HashSet, + ) -> Result<(), CommandError> { + let file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?; + let trk = file + .trk + .iter() + .position(|trk| trk.id == trk_id) + .ok_or(CommandError::NothingToDo)?; + let segments = &file.trk[trk].trkseg; + let selected: Vec = (0..segments.len()) + .filter(|i| ids.contains(&segments[*i].id)) + .collect(); + if selected.len() < 2 { + return Err(CommandError::NothingToDo); + } + let first = selected[0]; + let chosen: Vec = selected.iter().map(|i| segments[*i].clone()).collect(); + let merged = match self.type_ { + MergeType::Connect => vec![connect(&chosen, chosen[0].id, self.remove_gaps)], + MergeType::Group => { + if selected.iter().enumerate().all(|(i, s)| *s == first + i) { + // already one after the other + return Err(CommandError::NothingToDo); + } + chosen + } + }; + + let mut file = (**file).clone(); + let mut remaining: Vec = file.trk[trk] + .trkseg + .iter() + .filter(|segment| !ids.contains(&segment.id)) + .cloned() + .collect(); + // the ones before the first selected one are all kept + remaining.splice(first..first, merged); + file.trk[trk].trkseg = remaining; + state.files.insert(file_id, Rc::new(file)); + Ok(()) + } +} + +/// The segments connected in a single one, which has the id `id`: the trackpoints of the second +/// go after the ones of the first and so on. +/// +/// If there are timestamps, the new segment goes on in time, see [`replace_trackpoints`]: the +/// trackpoints that have none get some, at the moving speed of the segments, and the ones that +/// are too early follow the previous ones. The time of the first trackpoint is the one it has, or +/// the one it would have if it was as far in time from the first timed one as its distance +/// says. +fn connect(segments: &[TrackSegment], id: TrackSegmentId, remove_gaps: bool) -> TrackSegment { + let mut global = GlobalStatistics::default(); + let mut distance = 0.0; + // the first time, and the distance from the start at which it is + let mut first_timed: Option<(i64, f64)> = None; + for segment in segments { + let stats = Statistics::compute(segment); + if first_timed.is_none() { + first_timed = segment.iter().zip(&stats.local).find_map(|(point, local)| { + point + .time + .map(|time| (time, distance + local.total_distance)) + }); + } + distance += stats.global.total_distance; + global.merge(&stats.global); + } + let speed = global.moving_speed().filter(|speed| *speed > 0.0); + let start_time = speed + .and_then(|speed| first_timed.map(|(time, km)| time - (3_600_000.0 * km / speed) as i64)); + + let mut points = vec![]; + for segment in segments { + let end = points.len(); + replace_trackpoints( + &mut points, + end, + end, + segment.iter().cloned().collect(), + speed, + start_time, + remove_gaps, + ); + } + let mut merged = TrackSegment::default(); + merged.splice(0, 0, points); + merged.id = id; + merged +} + +#[cfg(test)] +mod tests { + use crate::{File, Load, engine::command::fixture::Fixture}; + + use super::*; + + fn load(fx: &mut Fixture, path: &str) -> FileId { + let data = std::fs::read(path).unwrap(); + Load { + data: &data, + name: "file", + } + .apply(&mut fx.state()) + .unwrap(); + *fx.order.0.last().unwrap() + } + + fn merge(type_: MergeType) -> Merge { + Merge { + type_, + remove_gaps: false, + } + } + + fn select_files(fx: &mut Fixture, ids: &[FileId]) { + fx.selection = Selection::File { + file_ids: ids.iter().copied().collect(), + }; + } + + fn points(file: &File) -> usize { + file.trk + .iter() + .flat_map(|t| &t.trkseg) + .map(|s| s.len()) + .sum() + } + + fn waypoint(name: &str) -> Waypoint { + Waypoint { + name: Some(name.into()), + ..Default::default() + } + } + + #[test] + fn test_group_files() { + let mut fx = Fixture::default(); + let a = load(&mut fx, "data/with_tracks_and_segments.gpx"); + let b = load(&mut fx, "data/simple.gpx"); + let mut file_a = (*fx.files[&a]).clone(); + file_a + .wpt + .insert_at(0, vec![waypoint("x"), waypoint("same")]); + fx.files.insert(a, Rc::new(file_a)); + let mut file_b = (*fx.files[&b]).clone(); + file_b + .wpt + .insert_at(0, vec![waypoint("same"), waypoint("y")]); + fx.files.insert(b, Rc::new(file_b)); + let (before_a, before_b) = (fx.files[&a].clone(), fx.files[&b].clone()); + select_files(&mut fx, &[b, a]); + + merge(MergeType::Group).apply(&mut fx.state()).unwrap(); + + // the first file (in the order of the files) gets everything, the other one goes + assert_eq!(fx.order.0, [a]); + assert!(!fx.files.contains_key(&b)); + let merged = &fx.files[&a]; + assert_eq!(merged.trk.len(), before_a.trk.len() + before_b.trk.len()); + assert_eq!(merged.trk[0].id, before_a.trk[0].id); + assert_eq!( + merged.trk.last().unwrap().id, + before_b.trk.last().unwrap().id + ); + assert_eq!(points(merged), points(&before_a) + points(&before_b)); + // the waypoints that are the same are only once + let names: Vec<_> = merged.wpt.iter().map(|w| w.name.clone().unwrap()).collect(); + assert_eq!(names, ["x", "same", "y"]); + assert_eq!(merged.info, before_a.info); + // a single file has nothing to group + select_files(&mut fx, &[a]); + assert_eq!( + merge(MergeType::Group).apply(&mut fx.state()), + Err(CommandError::NothingToDo) + ); + } + + #[test] + fn test_connect_files() { + let mut fx = Fixture::default(); + let a = load(&mut fx, "data/with_tracks_and_segments.gpx"); + let b = load(&mut fx, "data/simple.gpx"); + let (before_a, before_b) = (fx.files[&a].clone(), fx.files[&b].clone()); + select_files(&mut fx, &[a, b]); + + merge(MergeType::Connect).apply(&mut fx.state()).unwrap(); + + let merged = &fx.files[&a]; + assert_eq!(fx.order.0, [a]); + // a single track, with a single segment that has all the trackpoints in order + assert_eq!(merged.trk.len(), 1); + assert_eq!(merged.trk[0].id, before_a.trk[0].id); + assert_eq!(merged.trk[0].trkseg.len(), 1); + let segment = &merged.trk[0].trkseg[0]; + assert_eq!(segment.len(), points(&before_a) + points(&before_b)); + let expected: Vec = before_a + .trk + .iter() + .chain(&before_b.trk) + .flat_map(|t| &t.trkseg) + .flat_map(|s| s.iter().map(|p| p.coordinates.lng)) + .collect(); + let lngs: Vec = segment.iter().map(|p| p.coordinates.lng).collect(); + assert_eq!(lngs, expected); + assert_eq!(segment.id, before_a.trk[0].trkseg[0].id); + assert_eq!(segment[0].anchor, Some(0)); + } + + #[test] + fn test_connect_goes_on_in_time() { + let mut fx = Fixture::default(); + // the same times twice: the second part would be too early + let a = load(&mut fx, "data/with_time.gpx"); + let b = load(&mut fx, "data/with_time.gpx"); + select_files(&mut fx, &[a, b]); + merge(MergeType::Connect).apply(&mut fx.state()).unwrap(); + let segment = &fx.files[&a].trk[0].trkseg[0]; + let times: Vec = segment.iter().map(|p| p.time.unwrap()).collect(); + assert_eq!(times.len(), 160); + assert!(times.windows(2).all(|w| w[0] <= w[1]), "{times:?}"); + assert!(times[80] > times[79]); + } + + #[test] + fn test_connect_without_gaps_brings_the_parts_closer() { + let mut fx = Fixture::default(); + let a = load(&mut fx, "data/with_time.gpx"); + // the second file comes a day later + let b = { + let data = std::fs::read("data/with_time.gpx").unwrap(); + Load { + data: &data, + name: "late", + } + .apply(&mut fx.state()) + .unwrap(); + *fx.order.0.last().unwrap() + }; + let mut file = (*fx.files[&b]).clone(); + for track in file.trk.iter_mut() { + for segment in track.trkseg.iter_mut() { + let points: Vec<_> = segment + .iter() + .map(|p| { + let mut p = p.clone(); + p.time = p.time.map(|t| t + 86_400_000); + p + }) + .collect(); + let len = segment.len(); + segment.splice(0, len, points); + } + } + fx.files.insert(b, Rc::new(file)); + let gap = |fx: &Fixture, remove_gaps: bool| { + let mut fx2 = Fixture::default(); + fx2.files = fx.files.clone(); + fx2.order = crate::FileOrder(fx.order.0.clone()); + fx2.selection = Selection::File { + file_ids: [a, b].into(), + }; + Merge { + type_: MergeType::Connect, + remove_gaps, + } + .apply(&mut fx2.state()) + .unwrap(); + let segment = &fx2.files[&a].trk[0].trkseg[0]; + segment[80].time.unwrap() - segment[79].time.unwrap() + }; + let with_gap = gap(&fx, false); + let without = gap(&fx, true); + assert!(with_gap > 80_000_000, "{with_gap}"); + assert!(without < 60_000, "{without}"); + } + + #[test] + fn test_merge_tracks() { + let mut fx = Fixture::default(); + let id = load(&mut fx, "data/with_tracks_and_segments.gpx"); + let file = fx.files[&id].clone(); + assert!(file.trk.len() >= 2); + let ids: HashSet = file.trk.iter().map(|t| t.id).collect(); + fx.selection = Selection::Track { + file_id: id, + trk_ids: ids, + }; + let segments: usize = file.trk.iter().map(|t| t.trkseg.len()).sum(); + + merge(MergeType::Group).apply(&mut fx.state()).unwrap(); + let merged = &fx.files[&id]; + assert_eq!(merged.trk.len(), 1); + assert_eq!(merged.trk[0].id, file.trk[0].id); + assert_eq!(merged.trk[0].trkseg.len(), segments); + assert_eq!(points(merged), points(&file)); + + // connected: a single segment + fx.files.insert(id, file.clone()); + merge(MergeType::Connect).apply(&mut fx.state()).unwrap(); + let merged = &fx.files[&id]; + assert_eq!(merged.trk.len(), 1); + assert_eq!(merged.trk[0].trkseg.len(), 1); + assert_eq!(merged.trk[0].trkseg[0].len(), points(&file)); + + // one track with a single segment: nothing to do + fx.selection = Selection::Track { + file_id: id, + trk_ids: [file.trk[0].id].into(), + }; + for type_ in [MergeType::Group, MergeType::Connect] { + assert_eq!( + merge(type_).apply(&mut fx.state()), + Err(CommandError::NothingToDo) + ); + } + } + + #[test] + fn test_connect_the_segments_of_a_track() { + let mut fx = Fixture::default(); + let id = load(&mut fx, "data/with_tracks_and_segments.gpx"); + let file = fx.files[&id].clone(); + let (index, track) = file + .trk + .iter() + .enumerate() + .find(|(_, t)| t.trkseg.len() >= 2) + .unwrap(); + fx.selection = Selection::Track { + file_id: id, + trk_ids: [track.id].into(), + }; + merge(MergeType::Connect).apply(&mut fx.state()).unwrap(); + let merged = &fx.files[&id].trk[index]; + assert_eq!(merged.id, track.id); + assert_eq!(merged.trkseg.len(), 1); + assert_eq!( + merged.trkseg[0].len(), + track.trkseg.iter().map(|s| s.len()).sum::() + ); + // the other tracks are untouched + assert_eq!(fx.files[&id].trk.len(), file.trk.len()); + } + + #[test] + fn test_merge_segments() { + let mut fx = Fixture::default(); + let id = load(&mut fx, "data/with_tracks_and_segments.gpx"); + // a track with three segments + let mut three = (*fx.files[&id]).clone(); + let base = three.trk[0].trkseg[0].clone(); + three.trk[0].trkseg = (0..3).map(|_| crate::copy_segment(&base)).collect(); + fx.files.insert(id, Rc::new(three)); + let file = fx.files[&id].clone(); + let (index, track) = (0, &file.trk[0]); + let segments = &track.trkseg; + // the first and the last ones + let (first, last) = (&segments[0], &segments[2]); + fx.selection = Selection::TrackSegment { + file_id: id, + trk_id: track.id, + trkseg_ids: [first.id, last.id].into(), + }; + + // grouped: next to each other, at the place of the first one + merge(MergeType::Group).apply(&mut fx.state()).unwrap(); + let ids: Vec<_> = fx.files[&id].trk[index] + .trkseg + .iter() + .map(|s| s.id) + .collect(); + assert_eq!(ids[..3], [first.id, last.id, segments[1].id]); + + // connected: one segment, with the id of the first one, at its place + fx.files.insert(id, file.clone()); + merge(MergeType::Connect).apply(&mut fx.state()).unwrap(); + let merged = &fx.files[&id].trk[index].trkseg; + assert_eq!(merged.len(), segments.len() - 1); + assert_eq!(merged[0].id, first.id); + assert_eq!(merged[0].len(), first.len() + last.len()); + assert_eq!(merged[1].id, segments[1].id); + + // a single segment, or segments that are already together: nothing to do + fx.selection = Selection::TrackSegment { + file_id: id, + trk_id: track.id, + trkseg_ids: [first.id].into(), + }; + assert_eq!( + merge(MergeType::Connect).apply(&mut fx.state()), + Err(CommandError::NothingToDo) + ); + fx.files.insert(id, file.clone()); + fx.selection = Selection::TrackSegment { + file_id: id, + trk_id: track.id, + trkseg_ids: [first.id, segments[1].id].into(), + }; + assert_eq!( + merge(MergeType::Group).apply(&mut fx.state()), + Err(CommandError::NothingToDo) + ); + } + + #[test] + fn test_nothing_to_merge() { + let mut fx = Fixture::default(); + let id = load(&mut fx, "data/simple.gpx"); + let before = fx.files[&id].clone(); + for selection in [Selection::Empty, Selection::Waypoints { file_id: id }] { + fx.selection = selection; + assert_eq!( + merge(MergeType::Connect).apply(&mut fx.state()), + Err(CommandError::NothingToDo) + ); + } + assert!(Rc::ptr_eq(&fx.files[&id], &before)); } } diff --git a/gpx-rs/wasm/src/lib.rs b/gpx-rs/wasm/src/lib.rs index 5f93beb82..e74c02bf8 100644 --- a/gpx-rs/wasm/src/lib.rs +++ b/gpx-rs/wasm/src/lib.rs @@ -618,10 +618,13 @@ pub fn time() -> bool { edit(Command::Time(engine::Time)) } +/// Merges the selection into its first element (see `MergeType`). `remove_gaps` brings the parts +/// that are connected closer in time, if they have timestamps. #[wasm_bindgen] -pub fn merge(type_: MergeType) -> bool { +pub fn merge(type_: MergeType, remove_gaps: bool) -> bool { edit(Command::Merge(engine::Merge { type_: type_.into(), + remove_gaps, })) } diff --git a/website/src/lib/components/toolbar/tools/Merge.svelte b/website/src/lib/components/toolbar/tools/Merge.svelte index bd4cdc519..02530b108 100644 --- a/website/src/lib/components/toolbar/tools/Merge.svelte +++ b/website/src/lib/components/toolbar/tools/Merge.svelte @@ -6,7 +6,6 @@ @@ -68,7 +67,7 @@ {i18n._('toolbar.merge.merge_contents')} - {#if mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0} + {#if mergeType === MergeType.TRACES && hasTimes}
@@ -80,9 +79,9 @@ disabled={(mergeType === MergeType.TRACES && !canMergeTraces) || (mergeType === MergeType.CONTENTS && !canMergeContents)} onclick={() => { - fileActions.mergeSelection( - mergeType === MergeType.TRACES, - mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0 && removeGaps + engine.merge( + mergeType === MergeType.TRACES ? 'connect' : 'group', + mergeType === MergeType.TRACES && hasTimes && removeGaps ); }} > diff --git a/website/src/lib/engine.ts b/website/src/lib/engine.ts index 5020ba732..b8a012717 100644 --- a/website/src/lib/engine.ts +++ b/website/src/lib/engine.ts @@ -32,6 +32,12 @@ export type { type Wasm = typeof import('gpx-rs'); +/** + * How a merge puts the selected elements together: `connect` makes a single segment of their + * trackpoints, `group` keeps them as they are, in the first one. + */ +export type MergeType = 'connect' | 'group'; + /** What a split cuts in two: the file, the track or the segment of the trackpoint. */ export type SplitType = 'files' | 'tracks' | 'segments'; @@ -648,6 +654,17 @@ class Engine { return this.run((w) => w.reverse()); } + /** + * Merges the selected files, tracks or segments into the first of them (see `MergeType`). With + * `removeGaps`, the parts that are connected get closer in time if they have timestamps. + * Resolves to `false` when there is nothing to merge. + */ + merge(type: MergeType, removeGaps = false) { + return this.run((w) => + w.merge(type === 'connect' ? w.MergeType.Connect : w.MergeType.Group, removeGaps) + ); + } + /** * Cuts the file, the track or the segment (see `SplitType`) of the trackpoint `at` of the * selection in two, there: the trackpoint ends the first part and starts the second one. The diff --git a/website/src/lib/logic/file-actions.ts b/website/src/lib/logic/file-actions.ts index a995c9c0c..2a167f887 100644 --- a/website/src/lib/logic/file-actions.ts +++ b/website/src/lib/logic/file-actions.ts @@ -99,154 +99,6 @@ export async function loadFiles(list: FileList | File[]): Promise { // Helper functions for file operations export const fileActions = { - mergeSelection: (mergeTraces: boolean, removeGaps: boolean) => { - fileActionManager.applyGlobal((draft) => { - let first = true; - let target: ListItem = new ListRootItem(); - let targetFile: GPXFile | undefined = undefined; - let toMerge: { - trk: Track[]; - trkseg: TrackSegment[]; - wpt: Waypoint[]; - } = { - trk: [], - trkseg: [], - wpt: [], - }; - selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - let file = draft.get(fileId); - let originalFile = fileStateCollection.getFile(fileId); - if (file && originalFile) { - if (level === ListLevel.FILE) { - toMerge.trk.push(...originalFile.trk.map((track) => track.clone())); - for (const wpt of originalFile.wpt) { - if (!toMerge.wpt.some((w) => w.equals(wpt))) { - toMerge.wpt.push(wpt.clone()); - } - } - if (first) { - target = items[0]; - targetFile = file; - } else { - draft.delete(fileId); - } - } else { - 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 - target = item; - file.trk[trackIndex].trkseg = []; - } else { - file.trk.splice(trackIndex, 1); - } - }); - } else if (level === ListLevel.SEGMENT) { - items.forEach((item, index) => { - let trackIndex = (item as ListTrackSegmentItem).getTrackIndex(); - let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex(); - if (index === items.length - 1) { - // Order is reversed, so the last segment is the first one and the one to keep - target = item; - } - toMerge.trkseg.splice( - 0, - 0, - originalFile.trk[trackIndex].trkseg[segmentIndex].clone() - ); - file.trk[trackIndex].trkseg.splice(segmentIndex, 1); - }); - } - targetFile = file; - } - first = false; - } - }); - if (mergeTraces) { - let statistics = get(gpxStatistics); - let speed = - statistics.global.speed.moving > 0 ? statistics.global.speed.moving : undefined; - let startTime: Date | undefined = undefined; - if (speed !== undefined) { - if ( - statistics.global.length > 0 && - statistics.getTrackPoint(0)!.trkpt.time !== undefined - ) { - startTime = statistics.getTrackPoint(0)!.trkpt.time; - } else { - for (let i = 0; i < statistics.global.length; i++) { - const point = statistics.getTrackPoint(i)!; - if (point.trkpt.time !== undefined) { - startTime = new Date( - point.trkpt.time.getTime() - - (1000 * 3600 * point.distance.total) / speed - ); - break; - } - } - } - } - if (toMerge.trk.length > 0 && toMerge.trk[0].trkseg.length > 0) { - let s = new TrackSegment(); - toMerge.trk.map((track) => { - track.trkseg.forEach((segment) => { - s.replaceTrackPoints( - s.trkpt.length, - s.trkpt.length, - segment.trkpt.slice(), - speed, - startTime, - removeGaps - ); - }); - }); - toMerge.trk = [toMerge.trk[0]]; - toMerge.trk[0].trkseg = [s]; - } - if (toMerge.trkseg.length > 0) { - let s = new TrackSegment(); - toMerge.trkseg.forEach((segment) => { - s.replaceTrackPoints( - s.trkpt.length, - s.trkpt.length, - segment.trkpt.slice(), - speed, - startTime, - removeGaps - ); - }); - toMerge.trkseg = [s]; - } - } - if (targetFile) { - targetFile = targetFile as GPXFile; - if (target instanceof ListFileItem) { - 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.replaceTrackSegments(trackIndex, 0, -1, toMerge.trkseg); - } else if (target instanceof ListTrackSegmentItem) { - let trackIndex = target.getTrackIndex(); - let segmentIndex = target.getSegmentIndex(); - targetFile.replaceTrackSegments( - trackIndex, - segmentIndex, - segmentIndex - 1, - toMerge.trkseg - ); - } - } - }); - }, extractSelection: () => { return fileActionManager.applyGlobal((draft) => { selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {