mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-17 08:42:28 +00:00
keep original flag
This commit is contained in:
1 parent
55a8b020e0
commit
187296a277
8 files changed
+350
-35
No files matched your search
@@ -436,13 +436,27 @@ pub fn create_artificial_timestamps(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Merges the selection into its first element. `remove_gaps` brings the parts that are connected
|
/// Merges the selection into its first element. `remove_gaps` brings the parts that are connected
|
||||||
/// closer in time, if they have timestamps.
|
/// closer in time, if they have timestamps. With `keep_originals`, merged files are left as they
|
||||||
pub fn merge(engine: &mut Engine, type_: engine::MergeType, remove_gaps: bool) -> Outcome {
|
/// are and the result is a new file.
|
||||||
edit(engine, Command::Merge(engine::Merge { type_, remove_gaps }))
|
pub fn merge(
|
||||||
|
engine: &mut Engine,
|
||||||
|
type_: engine::MergeType,
|
||||||
|
remove_gaps: bool,
|
||||||
|
keep_originals: bool,
|
||||||
|
) -> Outcome {
|
||||||
|
edit(
|
||||||
|
engine,
|
||||||
|
Command::Merge(engine::Merge {
|
||||||
|
type_,
|
||||||
|
remove_gaps,
|
||||||
|
keep_originals,
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract(engine: &mut Engine) -> Outcome {
|
/// With `keep_originals`, extracted files are left as they are and the result is new files.
|
||||||
edit(engine, Command::Extract(engine::Extract))
|
pub fn extract(engine: &mut Engine, keep_originals: bool) -> Outcome {
|
||||||
|
edit(engine, Command::Extract(engine::Extract { keep_originals }))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the elevation of the trackpoints of the selection, one elevation per trackpoint.
|
/// Sets the elevation of the trackpoints of the selection, one elevation per trackpoint.
|
||||||
@@ -562,14 +576,23 @@ pub fn change_loop_start(engine: &mut Engine, revision: u32, index: u32) -> Outc
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Cuts the file, the track or the segment (see `SplitType`) of the trackpoint `at` of the
|
/// 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.
|
/// selection in two, there: the trackpoint ends the first part and starts the second one. With
|
||||||
|
/// `keep_originals`, a file that is cut is left as it is and its two parts are new files.
|
||||||
pub fn split(
|
pub fn split(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
revision: u32,
|
revision: u32,
|
||||||
at: u32,
|
at: u32,
|
||||||
split_type: engine::SplitType,
|
split_type: engine::SplitType,
|
||||||
|
keep_originals: bool,
|
||||||
) -> Outcome {
|
) -> Outcome {
|
||||||
at_revision(engine, revision, |engine| {
|
at_revision(engine, revision, |engine| {
|
||||||
edit(engine, Command::Split(engine::Split { at, split_type }))
|
edit(
|
||||||
|
engine,
|
||||||
|
Command::Split(engine::Split {
|
||||||
|
at,
|
||||||
|
split_type,
|
||||||
|
keep_originals,
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ use std::{collections::HashSet, rc::Rc};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, CommandError, File, FileId, LngLat, LngLatBounds, Selection, State, Track, TrackId,
|
Apply, CommandError, File, FileId, LngLat, LngLatBounds, Selection, State, Track, TrackId,
|
||||||
TrackSegment, Waypoint, Waypoints, copy_waypoint, crossarc_lnglat, distance,
|
TrackSegment, Waypoint, Waypoints, copy_file, copy_waypoint, crossarc_lnglat, distance,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Splits the selected elements that are made of several segments into elements that have one
|
/// Splits the selected elements that are made of several segments into elements that have one
|
||||||
@@ -17,28 +17,43 @@ use crate::{
|
|||||||
/// are named after the track and numbered.
|
/// are named after the track and numbered.
|
||||||
///
|
///
|
||||||
/// What has a single segment is left as it is. There is nothing to do if nothing was changed.
|
/// What has a single segment is left as it is. There is nothing to do if nothing was changed.
|
||||||
|
///
|
||||||
|
/// With `keep_originals`, the files that are extracted stay where they are, and the new files come
|
||||||
|
/// right after each of them, and only the new files are selected. Extracting
|
||||||
|
/// tracks changes the file in place anyway, so the flag has no effect on it.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Extract;
|
pub struct Extract {
|
||||||
|
pub keep_originals: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl Apply for Extract {
|
impl Apply for Extract {
|
||||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||||
let selection = state.selection.clone();
|
let selection = state.selection.clone();
|
||||||
match &selection {
|
match &selection {
|
||||||
Selection::File { file_ids } => extract_files(state, file_ids),
|
Selection::File { file_ids } => extract_files(state, file_ids, self.keep_originals),
|
||||||
Selection::Track { file_id, trk_ids } => extract_tracks(state, *file_id, trk_ids),
|
Selection::Track { file_id, trk_ids } => extract_tracks(state, *file_id, trk_ids),
|
||||||
_ => Err(CommandError::NothingToDo),
|
_ => Err(CommandError::NothingToDo),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_files(state: &mut State, file_ids: &HashSet<FileId>) -> Result<(), CommandError> {
|
fn extract_files(
|
||||||
|
state: &mut State,
|
||||||
|
file_ids: &HashSet<FileId>,
|
||||||
|
keep_originals: bool,
|
||||||
|
) -> Result<(), CommandError> {
|
||||||
// the files that have something to extract, with the files that come out of them
|
// the files that have something to extract, with the files that come out of them
|
||||||
let mut extracted: Vec<(FileId, Vec<File>)> = vec![];
|
let mut extracted: Vec<(FileId, Vec<File>)> = vec![];
|
||||||
for id in state.order.0.iter().filter(|id| file_ids.contains(id)) {
|
for id in state.order.0.iter().filter(|id| file_ids.contains(id)) {
|
||||||
let file = &state.files[id];
|
let file = &state.files[id];
|
||||||
let segments: usize = file.trk.iter().map(|track| track.trkseg.len()).sum();
|
let segments: usize = file.trk.iter().map(|track| track.trkseg.len()).sum();
|
||||||
if segments > 1 {
|
if segments > 1 {
|
||||||
extracted.push((*id, extract_file(file)));
|
let mut files = extract_file(file);
|
||||||
|
if keep_originals {
|
||||||
|
// the tracks and segments are the ones of the file, which stays: they need new ids
|
||||||
|
files = files.iter().map(copy_file).collect();
|
||||||
|
}
|
||||||
|
extracted.push((*id, files));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if extracted.is_empty() {
|
if extracted.is_empty() {
|
||||||
@@ -48,13 +63,25 @@ fn extract_files(state: &mut State, file_ids: &HashSet<FileId>) -> Result<(), Co
|
|||||||
let mut order = vec![];
|
let mut order = vec![];
|
||||||
for id in state.order.0.iter() {
|
for id in state.order.0.iter() {
|
||||||
match extracted.iter().find(|(original, _)| original == id) {
|
match extracted.iter().find(|(original, _)| original == id) {
|
||||||
Some((_, files)) => order.extend(files.iter().map(|file| file.id)),
|
Some((_, files)) => {
|
||||||
|
if keep_originals {
|
||||||
|
order.push(*id);
|
||||||
|
}
|
||||||
|
order.extend(files.iter().map(|file| file.id));
|
||||||
|
}
|
||||||
None => order.push(*id),
|
None => order.push(*id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut selected: HashSet<FileId> = file_ids.clone();
|
// the files that are kept stay selected, unless the originals are kept: then only what is new is
|
||||||
|
let mut selected: HashSet<FileId> = if keep_originals {
|
||||||
|
HashSet::new()
|
||||||
|
} else {
|
||||||
|
file_ids.clone()
|
||||||
|
};
|
||||||
for (original, files) in extracted {
|
for (original, files) in extracted {
|
||||||
state.files.remove(&original);
|
if !keep_originals {
|
||||||
|
state.files.remove(&original);
|
||||||
|
}
|
||||||
selected.remove(&original);
|
selected.remove(&original);
|
||||||
for file in files {
|
for file in files {
|
||||||
selected.insert(file.id);
|
selected.insert(file.id);
|
||||||
@@ -372,7 +399,11 @@ mod tests {
|
|||||||
fx.files.insert(other_id, Rc::new(other));
|
fx.files.insert(other_id, Rc::new(other));
|
||||||
fx.order.0.push(other_id);
|
fx.order.0.push(other_id);
|
||||||
|
|
||||||
Extract.apply(&mut fx.state()).unwrap();
|
Extract {
|
||||||
|
keep_originals: false,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// in the place of the file, in order, named after the track
|
// in the place of the file, in order, named after the track
|
||||||
assert!(!fx.files.contains_key(&id));
|
assert!(!fx.files.contains_key(&id));
|
||||||
@@ -402,6 +433,48 @@ mod tests {
|
|||||||
if file_ids.len() == 3 && !file_ids.contains(&other_id)));
|
if file_ids.len() == 3 && !file_ids.contains(&other_id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extracting_files_keeping_the_originals() {
|
||||||
|
let original = file(
|
||||||
|
"route",
|
||||||
|
vec![track(Some("climb"), &[0.0, 10.0, 20.0])],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
let segment_ids: Vec<_> = original.trk[0].trkseg.iter().map(|s| s.id).collect();
|
||||||
|
let (mut fx, id) = fixture(original);
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
let single = file("single", vec![track(None, &[50.0])], vec![]);
|
||||||
|
let single_id = single.id;
|
||||||
|
fx.files.insert(single_id, Rc::new(single));
|
||||||
|
fx.order.0.push(single_id);
|
||||||
|
fx.selection = Selection::File {
|
||||||
|
file_ids: HashSet::from([id, single_id]),
|
||||||
|
};
|
||||||
|
|
||||||
|
Extract {
|
||||||
|
keep_originals: true,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// the file stays, the new ones come right after it
|
||||||
|
assert!(Rc::ptr_eq(&fx.files[&id], &before));
|
||||||
|
assert_eq!(
|
||||||
|
names(&fx),
|
||||||
|
["route", "climb (1)", "climb (2)", "climb (3)", "single"]
|
||||||
|
);
|
||||||
|
assert_eq!(fx.order.0[0], id);
|
||||||
|
assert_eq!(fx.order.0[4], single_id);
|
||||||
|
// the segments are copies, not the ones of the file
|
||||||
|
for new_id in &fx.order.0[1..4] {
|
||||||
|
let segment = fx.files[new_id].trk[0].trkseg[0].id;
|
||||||
|
assert!(!segment_ids.contains(&segment));
|
||||||
|
}
|
||||||
|
// only the new files are selected
|
||||||
|
assert!(matches!(&fx.selection, Selection::File { file_ids }
|
||||||
|
if *file_ids == fx.order.0[1..4].iter().copied().collect::<HashSet<_>>()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_a_file_with_several_tracks_becomes_one_file_per_track() {
|
fn test_a_file_with_several_tracks_becomes_one_file_per_track() {
|
||||||
let original = file(
|
let original = file(
|
||||||
@@ -415,7 +488,11 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let ids: Vec<_> = original.trk.iter().map(|t| t.id).collect();
|
let ids: Vec<_> = original.trk.iter().map(|t| t.id).collect();
|
||||||
let (mut fx, _) = fixture(original);
|
let (mut fx, _) = fixture(original);
|
||||||
Extract.apply(&mut fx.state()).unwrap();
|
Extract {
|
||||||
|
keep_originals: false,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// named after the track, or the file
|
// named after the track, or the file
|
||||||
assert_eq!(names(&fx), ["first", "route (2)", "third"]);
|
assert_eq!(names(&fx), ["first", "route (2)", "third"]);
|
||||||
@@ -451,7 +528,11 @@ mod tests {
|
|||||||
vec![shared, waypoint("lost", 100.0)],
|
vec![shared, waypoint("lost", 100.0)],
|
||||||
);
|
);
|
||||||
let (mut fx, _) = fixture(original);
|
let (mut fx, _) = fixture(original);
|
||||||
Extract.apply(&mut fx.state()).unwrap();
|
Extract {
|
||||||
|
keep_originals: false,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
let first = &fx.files[&fx.order.0[0]];
|
let first = &fx.files[&fx.order.0[0]];
|
||||||
let second = &fx.files[&fx.order.0[1]];
|
let second = &fx.files[&fx.order.0[1]];
|
||||||
// the tie goes to both, with different ids
|
// the tie goes to both, with different ids
|
||||||
@@ -480,7 +561,11 @@ mod tests {
|
|||||||
vec![waypoint("w", 1.0)],
|
vec![waypoint("w", 1.0)],
|
||||||
);
|
);
|
||||||
let (mut fx, _) = fixture(original);
|
let (mut fx, _) = fixture(original);
|
||||||
Extract.apply(&mut fx.state()).unwrap();
|
Extract {
|
||||||
|
keep_originals: false,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
assert_eq!(fx.files[&fx.order.0[0]].wpt.len(), 1);
|
assert_eq!(fx.files[&fx.order.0[0]].wpt.len(), 1);
|
||||||
assert_eq!(fx.files[&fx.order.0[1]].wpt.len(), 0);
|
assert_eq!(fx.files[&fx.order.0[1]].wpt.len(), 0);
|
||||||
}
|
}
|
||||||
@@ -503,7 +588,11 @@ mod tests {
|
|||||||
trk_ids: [ids[1], ids[2]].into(),
|
trk_ids: [ids[1], ids[2]].into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Extract.apply(&mut fx.state()).unwrap();
|
Extract {
|
||||||
|
keep_originals: false,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let file = &fx.files[&id];
|
let file = &fx.files[&id];
|
||||||
let names: Vec<_> = file.trk.iter().map(|t| t.info.name.clone()).collect();
|
let names: Vec<_> = file.trk.iter().map(|t| t.info.name.clone()).collect();
|
||||||
@@ -539,7 +628,10 @@ mod tests {
|
|||||||
let (mut fx, id) = fixture(original);
|
let (mut fx, id) = fixture(original);
|
||||||
let before = fx.files[&id].clone();
|
let before = fx.files[&id].clone();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Extract.apply(&mut fx.state()),
|
Extract {
|
||||||
|
keep_originals: false,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state()),
|
||||||
Err(CommandError::NothingToDo)
|
Err(CommandError::NothingToDo)
|
||||||
);
|
);
|
||||||
fx.selection = Selection::Track {
|
fx.selection = Selection::Track {
|
||||||
@@ -547,7 +639,10 @@ mod tests {
|
|||||||
trk_ids: [track_id].into(),
|
trk_ids: [track_id].into(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Extract.apply(&mut fx.state()),
|
Extract {
|
||||||
|
keep_originals: false,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state()),
|
||||||
Err(CommandError::NothingToDo)
|
Err(CommandError::NothingToDo)
|
||||||
);
|
);
|
||||||
for selection in [
|
for selection in [
|
||||||
@@ -561,7 +656,10 @@ mod tests {
|
|||||||
] {
|
] {
|
||||||
fx.selection = selection;
|
fx.selection = selection;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Extract.apply(&mut fx.state()),
|
Extract {
|
||||||
|
keep_originals: false,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state()),
|
||||||
Err(CommandError::NothingToDo)
|
Err(CommandError::NothingToDo)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::{collections::HashSet, rc::Rc};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, CommandError, FileId, GlobalStatistics, Selection, State, Statistics, Track, TrackId,
|
Apply, CommandError, FileId, GlobalStatistics, Selection, State, Statistics, Track, TrackId,
|
||||||
TrackSegment, TrackSegmentId, Waypoint, Waypoints, replace_trackpoints,
|
TrackSegment, TrackSegmentId, Waypoint, Waypoints, copy_track, replace_trackpoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
|
||||||
@@ -18,7 +18,9 @@ pub enum MergeType {
|
|||||||
///
|
///
|
||||||
/// - files: the other files are deleted. The tracks of all of them are in the first file, as
|
/// - 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
|
/// 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.
|
/// all of them are in the first file too, except the ones that are the same. With
|
||||||
|
/// `keep_originals`, no file is deleted nor changed: the result is a new file (with the info of
|
||||||
|
/// the first one and new ids), which comes right after the last of the files and is selected.
|
||||||
/// - tracks of a file: the first track gets their segments, as they are with `Group`, or all
|
/// - 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.
|
/// 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
|
/// - segments of a track: they are all connected in a segment at the position of the first one
|
||||||
@@ -33,6 +35,8 @@ pub enum MergeType {
|
|||||||
pub struct Merge {
|
pub struct Merge {
|
||||||
pub type_: MergeType,
|
pub type_: MergeType,
|
||||||
pub remove_gaps: bool,
|
pub remove_gaps: bool,
|
||||||
|
/// Only for files: tracks and segments are merged in place anyway.
|
||||||
|
pub keep_originals: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Apply for Merge {
|
impl Apply for Merge {
|
||||||
@@ -110,6 +114,23 @@ impl Merge {
|
|||||||
target.trk = tracks;
|
target.trk = tracks;
|
||||||
target.wpt = Waypoints::default();
|
target.wpt = Waypoints::default();
|
||||||
target.wpt.insert_at(0, waypoints);
|
target.wpt.insert_at(0, waypoints);
|
||||||
|
if self.keep_originals {
|
||||||
|
// a new file, whose tracks and segments do not share their ids with the originals
|
||||||
|
target.id = Default::default();
|
||||||
|
target.trk = target.trk.iter().map(copy_track).collect();
|
||||||
|
let at = state
|
||||||
|
.order
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.rposition(|id| file_ids.contains(id))
|
||||||
|
.map_or(state.order.0.len(), |i| i + 1);
|
||||||
|
state.order.0.insert(at, target.id);
|
||||||
|
*state.selection = Selection::File {
|
||||||
|
file_ids: [target.id].into(),
|
||||||
|
};
|
||||||
|
state.files.insert(target.id, Rc::new(target));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
for id in &ordered[1..] {
|
for id in &ordered[1..] {
|
||||||
state.files.remove(id);
|
state.files.remove(id);
|
||||||
}
|
}
|
||||||
@@ -274,6 +295,7 @@ mod tests {
|
|||||||
Merge {
|
Merge {
|
||||||
type_,
|
type_,
|
||||||
remove_gaps: false,
|
remove_gaps: false,
|
||||||
|
keep_originals: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,6 +363,73 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_files_keeping_the_originals() {
|
||||||
|
for type_ in [MergeType::Group, MergeType::Connect] {
|
||||||
|
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 c = 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 {
|
||||||
|
type_,
|
||||||
|
remove_gaps: false,
|
||||||
|
keep_originals: true,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// the files are untouched, the new one comes right after the last of them
|
||||||
|
assert!(Rc::ptr_eq(&fx.files[&a], &before_a));
|
||||||
|
assert!(Rc::ptr_eq(&fx.files[&b], &before_b));
|
||||||
|
assert_eq!(fx.files.len(), 4);
|
||||||
|
assert_eq!(&fx.order.0[..2], [a, b]);
|
||||||
|
assert_eq!(fx.order.0[3], c);
|
||||||
|
let merged_id = fx.order.0[2];
|
||||||
|
let merged = &fx.files[&merged_id];
|
||||||
|
assert_eq!(points(merged), points(&before_a) + points(&before_b));
|
||||||
|
assert_eq!(merged.info, before_a.info);
|
||||||
|
// new ids for the tracks and segments
|
||||||
|
let ids = |f: &File| -> Vec<_> {
|
||||||
|
f.trk
|
||||||
|
.iter()
|
||||||
|
.flat_map(|t| std::iter::once(t.id.0).chain(t.trkseg.iter().map(|s| s.id.0)))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let originals: Vec<_> = ids(&before_a).into_iter().chain(ids(&before_b)).collect();
|
||||||
|
assert!(ids(merged).iter().all(|id| !originals.contains(id)));
|
||||||
|
// and it is the selection
|
||||||
|
assert!(matches!(&fx.selection, Selection::File { file_ids }
|
||||||
|
if *file_ids == HashSet::from([merged_id])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keeping_the_originals_does_not_matter_when_merging_tracks() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let a = load(&mut fx, "data/with_tracks_and_segments.gpx");
|
||||||
|
let tracks: HashSet<_> = fx.files[&a].trk.iter().map(|t| t.id).collect();
|
||||||
|
let before = fx.files[&a].trk.len();
|
||||||
|
assert!(before > 1);
|
||||||
|
fx.selection = Selection::Track {
|
||||||
|
file_id: a,
|
||||||
|
trk_ids: tracks,
|
||||||
|
};
|
||||||
|
|
||||||
|
Merge {
|
||||||
|
type_: MergeType::Group,
|
||||||
|
remove_gaps: false,
|
||||||
|
keep_originals: true,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(fx.files.len(), 1);
|
||||||
|
assert_eq!(fx.files[&a].trk.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_connect_files() {
|
fn test_connect_files() {
|
||||||
let mut fx = Fixture::default();
|
let mut fx = Fixture::default();
|
||||||
@@ -430,6 +519,7 @@ mod tests {
|
|||||||
Merge {
|
Merge {
|
||||||
type_: MergeType::Connect,
|
type_: MergeType::Connect,
|
||||||
remove_gaps,
|
remove_gaps,
|
||||||
|
keep_originals: false,
|
||||||
}
|
}
|
||||||
.apply(&mut fx2.state())
|
.apply(&mut fx2.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, CommandError, File, State, Track, TrackSegment, Waypoints, copy_segment, copy_track,
|
Apply, CommandError, File, Selection, State, Track, TrackSegment, Waypoints, copy_file,
|
||||||
copy_waypoint,
|
copy_segment, copy_track, copy_waypoint,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::route::find_target;
|
use super::route::find_target;
|
||||||
@@ -27,10 +27,15 @@ pub enum SplitType {
|
|||||||
/// come after the trackpoint (the following segments of the track, the following tracks of the
|
/// come after the trackpoint (the following segments of the track, the following tracks of the
|
||||||
/// file), which are moved to it. When the file is cut, the new file comes right after it. The
|
/// file), which are moved to it. When the file is cut, the new file comes right after it. The
|
||||||
/// selection does not change.
|
/// selection does not change.
|
||||||
|
///
|
||||||
|
/// With `keep_originals`, a file that is cut (`SplitType::Files`) is left as it is: its two parts
|
||||||
|
/// are two new files, with new ids, which come right after it and are selected. Cutting a track
|
||||||
|
/// or a segment changes the file in place anyway, so the flag has no effect on them.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Split {
|
pub struct Split {
|
||||||
pub at: u32,
|
pub at: u32,
|
||||||
pub split_type: SplitType,
|
pub split_type: SplitType,
|
||||||
|
pub keep_originals: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Apply for Split {
|
impl Apply for Split {
|
||||||
@@ -75,6 +80,41 @@ impl Apply for Split {
|
|||||||
let (first_track, second_track) = split_track();
|
let (first_track, second_track) = split_track();
|
||||||
file.trk.splice(trk..=trk, [first_track, second_track]);
|
file.trk.splice(trk..=trk, [first_track, second_track]);
|
||||||
}
|
}
|
||||||
|
SplitType::Files if self.keep_originals => {
|
||||||
|
let (first_track, second_track) = split_track();
|
||||||
|
// the parts are new files: nothing is shared by id with the file, nor between them
|
||||||
|
let first_file = copy_file(&File {
|
||||||
|
trk: file.trk[..trk]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.chain([first_track])
|
||||||
|
.collect(),
|
||||||
|
..file.clone()
|
||||||
|
});
|
||||||
|
let mut second_file = File {
|
||||||
|
id: Default::default(),
|
||||||
|
trk: vec![second_track],
|
||||||
|
wpt: Waypoints::default(),
|
||||||
|
..file.clone()
|
||||||
|
};
|
||||||
|
second_file
|
||||||
|
.trk
|
||||||
|
.extend(file.trk[trk + 1..].iter().map(copy_track));
|
||||||
|
second_file
|
||||||
|
.wpt
|
||||||
|
.insert_at(0, file.wpt.iter().map(copy_waypoint).collect());
|
||||||
|
|
||||||
|
let ids = [first_file.id, second_file.id];
|
||||||
|
let position = state.order.0.iter().position(|id| *id == file_id);
|
||||||
|
let at = position.map_or(state.order.0.len(), |p| p + 1);
|
||||||
|
state.order.0.splice(at..at, ids);
|
||||||
|
state.files.insert(first_file.id, Rc::new(first_file));
|
||||||
|
state.files.insert(second_file.id, Rc::new(second_file));
|
||||||
|
*state.selection = Selection::File {
|
||||||
|
file_ids: ids.into(),
|
||||||
|
};
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
SplitType::Files => {
|
SplitType::Files => {
|
||||||
let (first_track, second_track) = split_track();
|
let (first_track, second_track) = split_track();
|
||||||
let mut second_file = File {
|
let mut second_file = File {
|
||||||
@@ -147,7 +187,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn split(at: u32, split_type: SplitType) -> Split {
|
fn split(at: u32, split_type: SplitType) -> Split {
|
||||||
Split { at, split_type }
|
Split {
|
||||||
|
at,
|
||||||
|
split_type,
|
||||||
|
keep_originals: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lngs(segment: &TrackSegment) -> Vec<f64> {
|
fn lngs(segment: &TrackSegment) -> Vec<f64> {
|
||||||
@@ -316,6 +360,66 @@ mod tests {
|
|||||||
assert!(matches!(&fx.selection, Selection::File { file_ids } if file_ids.contains(&id)));
|
assert!(matches!(&fx.selection, Selection::File { file_ids } if file_ids.contains(&id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_split_files_keeping_the_originals() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
let (trk, seg) = long_segment(&before);
|
||||||
|
let at = index(&before, trk, seg, 1);
|
||||||
|
|
||||||
|
Split {
|
||||||
|
at,
|
||||||
|
split_type: SplitType::Files,
|
||||||
|
keep_originals: true,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// the file is untouched, the two parts come right after it
|
||||||
|
assert!(Rc::ptr_eq(&fx.files[&id], &before));
|
||||||
|
assert_eq!(fx.files.len(), 3);
|
||||||
|
assert_eq!(fx.order.0[0], id);
|
||||||
|
let (first_id, second_id) = (fx.order.0[1], fx.order.0[2]);
|
||||||
|
let (a, b) = (&fx.files[&first_id], &fx.files[&second_id]);
|
||||||
|
let all = path(&before);
|
||||||
|
assert_eq!(path(a), all[..=at as usize]);
|
||||||
|
assert_eq!(path(b), all[at as usize..]);
|
||||||
|
// nothing is shared by id with the file, nor between the parts
|
||||||
|
let ids = |f: &File| -> HashSet<_> {
|
||||||
|
f.trk
|
||||||
|
.iter()
|
||||||
|
.flat_map(|t| std::iter::once(t.id.0).chain(t.trkseg.iter().map(|s| s.id.0)))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
assert!(ids(a).is_disjoint(&ids(&before)));
|
||||||
|
assert!(ids(b).is_disjoint(&ids(&before)));
|
||||||
|
assert!(ids(a).is_disjoint(&ids(b)));
|
||||||
|
// the new files are selected
|
||||||
|
assert!(matches!(&fx.selection, Selection::File { file_ids }
|
||||||
|
if *file_ids == HashSet::from([first_id, second_id])));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keeping_the_originals_does_not_matter_when_cutting_a_segment() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
let (trk, seg) = long_segment(&before);
|
||||||
|
|
||||||
|
Split {
|
||||||
|
at: index(&before, trk, seg, 2),
|
||||||
|
split_type: SplitType::Segments,
|
||||||
|
keep_originals: true,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(fx.files.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
fx.files[&id].trk[trk].trkseg.len(),
|
||||||
|
before.trk[trk].trkseg.len() + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_or_nothing() {
|
fn test_invalid_or_nothing() {
|
||||||
let (mut fx, id) = loaded();
|
let (mut fx, id) = loaded();
|
||||||
|
|||||||
@@ -139,12 +139,12 @@ pub fn create_artificial_timestamps(
|
|||||||
|
|
||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
pub fn merge(state: State<AppState>, merge_type: MergeType, remove_gaps: bool) -> api::Outcome {
|
pub fn merge(state: State<AppState>, merge_type: MergeType, remove_gaps: bool) -> api::Outcome {
|
||||||
state.run(move |e| api::merge(e, merge_type, remove_gaps))
|
state.run(move |e| api::merge(e, merge_type, remove_gaps, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
pub fn extract(state: State<AppState>) -> api::Outcome {
|
pub fn extract(state: State<AppState>) -> api::Outcome {
|
||||||
state.run(api::extract)
|
state.run(|e| api::extract(e, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
@@ -214,5 +214,5 @@ pub fn split(
|
|||||||
at: u32,
|
at: u32,
|
||||||
split_type: SplitType,
|
split_type: SplitType,
|
||||||
) -> api::Outcome {
|
) -> api::Outcome {
|
||||||
state.run(move |e| api::split(e, revision, at, split_type))
|
state.run(move |e| api::split(e, revision, at, split_type, false))
|
||||||
}
|
}
|
||||||
+1
-1
@@ -255,7 +255,7 @@ mod tests {
|
|||||||
// another session
|
// another session
|
||||||
let storage = FileStorage::open(&root).unwrap();
|
let storage = FileStorage::open(&root).unwrap();
|
||||||
let restored = pollster::block_on(Persister::default().restore(&storage)).unwrap();
|
let restored = pollster::block_on(Persister::default().restore(&storage)).unwrap();
|
||||||
let mut other = Engine::default();
|
let mut other: Engine = Engine::default();
|
||||||
other.restore(restored);
|
other.restore(restored);
|
||||||
assert_eq!(other.order(), order);
|
assert_eq!(other.order(), order);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -62,5 +62,5 @@ pub fn change_loop_start(revision: u32, index: u32) -> Outcome {
|
|||||||
/// nothing, and returns false, if the revision of the routing buffers is not the given one.
|
/// nothing, and returns false, if the revision of the routing buffers is not the given one.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn split(revision: u32, at: u32, split_type: SplitType) -> Outcome {
|
pub fn split(revision: u32, at: u32, split_type: SplitType) -> Outcome {
|
||||||
run(|e| api::split(e, revision, at, split_type.into()))
|
run(|e| api::split(e, revision, at, split_type.into(), false))
|
||||||
}
|
}
|
||||||
@@ -55,12 +55,12 @@ pub fn create_artificial_timestamps(start_time: f64, total_time: f64) -> Outcome
|
|||||||
/// that are connected closer in time, if they have timestamps.
|
/// that are connected closer in time, if they have timestamps.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn merge(type_: MergeType, remove_gaps: bool) -> Outcome {
|
pub fn merge(type_: MergeType, remove_gaps: bool) -> Outcome {
|
||||||
run(|e| api::merge(e, type_.into(), remove_gaps))
|
run(|e| api::merge(e, type_.into(), remove_gaps, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn extract() -> Outcome {
|
pub fn extract() -> Outcome {
|
||||||
run(api::extract)
|
run(|e| api::extract(e, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the elevation of the trackpoints of the selection, one elevation per trackpoint.
|
/// Sets the elevation of the trackpoints of the selection, one elevation per trackpoint.
|
||||||
|
|||||||
Reference in new issue
Block a user