Files
gpx.studio/gpx-rs/src/engine/command/file/delete.rs
T

305 lines
10 KiB
Rust
Raw Normal View History

2026-10-03 16:52:21 +02:00
use std::rc::Rc;
2026-10-14 08:17:51 +02:00
use crate::{Apply, CommandError, FileId, Files, Selection, State, Waypoint};
2026-10-03 16:40:17 +02:00
2026-10-04 14:49:53 +02:00
/// Deletes the selected elements. With `whole_files`, the files holding the selected elements
2026-10-26 18:51:56 +01:00
/// are deleted instead, even if only a route, a track or a waypoint is selected.
2026-10-03 16:40:17 +02:00
#[derive(Debug)]
2026-10-04 14:49:53 +02:00
pub struct Delete {
pub whole_files: bool,
}
2026-10-03 16:40:17 +02:00
impl Apply for Delete {
2026-10-03 16:52:21 +02:00
fn apply(self, state: &mut State) -> Result<(), CommandError> {
2026-10-04 14:49:53 +02:00
let files = state.selection.to_files();
let selection = if self.whole_files {
&files
} else {
&*state.selection
};
let next = match selection {
2026-10-03 16:52:21 +02:00
Selection::File { file_ids } => {
state.files.retain(|id, _| !file_ids.contains(id));
state.order.0.retain(|id| !file_ids.contains(id));
Selection::Empty
}
2026-10-26 18:51:56 +01:00
Selection::Route { file_id, rte_ids } => {
let file = state
.files
.get_mut(file_id)
.ok_or(CommandError::NothingToDo)?;
let file = Rc::make_mut(file);
let len = file.rte.len();
file.rte.retain(|rte| !rte_ids.contains(&rte.id));
if file.rte.len() == len {
return Err(CommandError::NothingToDo);
}
Selection::File {
file_ids: [*file_id].into(),
}
}
2026-10-03 16:52:21 +02:00
Selection::Track { file_id, trk_ids } => {
let file = state
.files
.get_mut(file_id)
.ok_or(CommandError::NothingToDo)?;
let file = Rc::make_mut(file);
let len = file.trk.len();
file.trk.retain(|trk| !trk_ids.contains(&trk.id));
if file.trk.len() == len {
return Err(CommandError::NothingToDo);
}
Selection::File {
file_ids: [*file_id].into(),
}
}
Selection::TrackSegment {
file_id,
trk_id,
trkseg_ids,
} => {
let file = state
.files
.get_mut(file_id)
.ok_or(CommandError::NothingToDo)?;
let file = Rc::make_mut(file);
let trk = file
.trk
.iter_mut()
.find(|trk| trk.id == *trk_id)
.ok_or(CommandError::NothingToDo)?;
let len = trk.trkseg.len();
trk.trkseg.retain(|seg| !trkseg_ids.contains(&seg.id));
if trk.trkseg.len() == len {
return Err(CommandError::NothingToDo);
}
Selection::Track {
file_id: *file_id,
trk_ids: [*trk_id].into(),
}
}
2026-10-03 17:21:16 +02:00
Selection::Waypoints { file_id } => delete_waypoints(state.files, *file_id, |_| true)?,
Selection::Waypoint { file_id, wpt_ids } => {
delete_waypoints(state.files, *file_id, |wpt| wpt_ids.contains(&wpt.id))?
2026-10-03 16:52:21 +02:00
}
2026-10-03 17:21:16 +02:00
Selection::Empty => return Err(CommandError::NothingToDo),
2026-10-03 16:52:21 +02:00
};
*state.selection = next;
Ok(())
}
}
2026-10-04 17:09:41 +02:00
pub(crate) fn delete_waypoints(
2026-10-14 08:17:51 +02:00
files: &mut Files,
2026-10-03 17:21:16 +02:00
file_id: FileId,
filter: impl Fn(&Waypoint) -> bool,
) -> Result<Selection, CommandError> {
let file = files.get(&file_id).ok_or(CommandError::NothingToDo)?;
let mut file = (**file).clone();
2026-10-07 18:22:30 +02:00
let changed = file.wpt.edit(&filter, |wpts| {
2026-10-03 17:21:16 +02:00
wpts.retain(|wpt| !filter(wpt));
true
});
if !changed {
return Err(CommandError::NothingToDo);
}
files.insert(file_id, Rc::new(file));
Ok(Selection::File {
file_ids: [file_id].into(),
})
}
2026-10-03 16:52:21 +02:00
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::engine::command::fixture::Fixture;
use crate::{FileId, Load};
use super::*;
fn loaded() -> (Fixture, FileId) {
let mut fx = Fixture::default();
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
2026-10-04 11:07:12 +02:00
Load {
data: &data,
name: "file",
}
.apply(&mut fx.state())
.unwrap();
2026-10-03 16:52:21 +02:00
let id = fx.order.0[0];
(fx, id)
}
#[test]
fn test_delete_nothing_selected() {
let mut fx = Fixture::default();
assert_eq!(
2026-10-04 14:49:53 +02:00
Delete { whole_files: false }.apply(&mut fx.state()),
2026-10-03 16:52:21 +02:00
Err(CommandError::NothingToDo)
);
}
#[test]
fn test_delete_selected_files() {
let (mut fx, id) = loaded();
2026-10-07 17:58:17 +02:00
crate::New {
name: "keep",
trackpoint: None,
2026-10-26 19:22:14 +01:00
route: false,
2026-10-07 17:58:17 +02:00
}
.apply(&mut fx.state())
.unwrap();
2026-10-03 16:52:21 +02:00
let keep = fx.order.0[1];
fx.selection = Selection::File {
file_ids: HashSet::from([id]),
};
2026-10-04 14:49:53 +02:00
Delete { whole_files: false }
.apply(&mut fx.state())
.unwrap();
2026-10-03 16:52:21 +02:00
assert_eq!(fx.order.0, vec![keep]);
assert!(fx.files.contains_key(&keep) && !fx.files.contains_key(&id));
assert!(matches!(fx.selection, Selection::Empty));
}
#[test]
fn test_delete_selected_tracks() {
let (mut fx, id) = loaded();
let before = fx.files[&id].trk.len();
let trk_id = fx.files[&id].trk[0].id;
fx.selection = Selection::Track {
file_id: id,
trk_ids: HashSet::from([trk_id]),
};
2026-10-04 14:49:53 +02:00
Delete { whole_files: false }
.apply(&mut fx.state())
.unwrap();
2026-10-03 16:52:21 +02:00
let file = &fx.files[&id];
assert_eq!(file.trk.len(), before - 1);
assert!(file.trk.iter().all(|t| t.id != trk_id));
assert_eq!(fx.selected_files(), HashSet::from([id]));
}
#[test]
fn test_delete_selected_segments() {
let (mut fx, id) = loaded();
let trk = &fx.files[&id].trk[0];
let (trk_id, seg_id, before) = (trk.id, trk.trkseg[0].id, trk.trkseg.len());
fx.selection = Selection::TrackSegment {
file_id: id,
trk_id,
trkseg_ids: HashSet::from([seg_id]),
};
2026-10-04 14:49:53 +02:00
Delete { whole_files: false }
.apply(&mut fx.state())
.unwrap();
2026-10-03 16:52:21 +02:00
let trk = &fx.files[&id].trk[0];
assert_eq!(trk.trkseg.len(), before - 1);
assert!(trk.trkseg.iter().all(|s| s.id != seg_id));
assert!(
matches!(&fx.selection, Selection::Track { trk_ids, .. } if trk_ids.contains(&trk_id))
);
}
2026-10-03 17:21:16 +02:00
fn with_waypoints(fx: &mut Fixture, id: FileId, n: usize) -> Vec<crate::WaypointId> {
let wpts: Vec<_> = (0..n).map(|_| Waypoint::default()).collect();
let ids = wpts.iter().map(|w| w.id).collect();
let mut file = (*fx.files[&id]).clone();
2026-10-07 18:22:30 +02:00
file.wpt = crate::Waypoints::new([crate::WaypointChunk {
2026-10-03 17:21:16 +02:00
wpt: wpts,
..Default::default()
2026-10-07 18:22:30 +02:00
}]);
2026-10-03 17:21:16 +02:00
fx.files.insert(id, Rc::new(file));
ids
}
#[test]
fn test_delete_selected_waypoints() {
let (mut fx, id) = loaded();
let ids = with_waypoints(&mut fx, id, 3);
fx.selection = Selection::Waypoint {
file_id: id,
wpt_ids: HashSet::from([ids[1]]),
};
2026-10-07 18:22:30 +02:00
let rev = fx.files[&id].wpt.rev_id;
2026-10-04 14:49:53 +02:00
Delete { whole_files: false }
.apply(&mut fx.state())
.unwrap();
2026-10-07 18:22:30 +02:00
assert_ne!(fx.files[&id].wpt.rev_id, rev);
let left: Vec<_> = fx.files[&id].wpt.iter().map(|w| w.id).collect();
2026-10-03 17:21:16 +02:00
assert_eq!(left, vec![ids[0], ids[2]]);
assert_eq!(fx.selected_files(), HashSet::from([id]));
}
#[test]
fn test_delete_all_waypoints_of_file() {
let (mut fx, id) = loaded();
with_waypoints(&mut fx, id, 3);
fx.selection = Selection::Waypoints { file_id: id };
2026-10-04 14:49:53 +02:00
Delete { whole_files: false }
.apply(&mut fx.state())
.unwrap();
2026-10-03 17:21:16 +02:00
assert!(fx.files[&id].wpt.is_empty());
fx.selection = Selection::Waypoints { file_id: id };
assert_eq!(
2026-10-04 14:49:53 +02:00
Delete { whole_files: false }.apply(&mut fx.state()),
2026-10-03 17:21:16 +02:00
Err(CommandError::NothingToDo)
);
}
2026-10-03 16:52:21 +02:00
#[test]
fn test_delete_unknown_ids_is_nothing_to_do() {
let (mut fx, id) = loaded();
fx.selection = Selection::Track {
file_id: id,
trk_ids: HashSet::from([Default::default()]),
};
assert_eq!(
2026-10-04 14:49:53 +02:00
Delete { whole_files: false }.apply(&mut fx.state()),
Err(CommandError::NothingToDo)
);
}
#[test]
fn test_delete_whole_files_of_the_selected_elements() {
for selection in [
|id: FileId, fx: &Fixture| Selection::Track {
file_id: id,
trk_ids: HashSet::from([fx.files[&id].trk[0].id]),
},
|id: FileId, fx: &Fixture| Selection::TrackSegment {
file_id: id,
trk_id: fx.files[&id].trk[0].id,
trkseg_ids: HashSet::from([fx.files[&id].trk[0].trkseg[0].id]),
},
|id: FileId, _: &Fixture| Selection::Waypoints { file_id: id },
|id: FileId, _: &Fixture| Selection::File {
file_ids: HashSet::from([id]),
},
] {
let (mut fx, id) = loaded();
2026-10-07 17:58:17 +02:00
crate::New {
name: "keep",
trackpoint: None,
2026-10-26 19:22:14 +01:00
route: false,
2026-10-07 17:58:17 +02:00
}
.apply(&mut fx.state())
.unwrap();
2026-10-04 14:49:53 +02:00
let keep = fx.order.0[1];
fx.selection = selection(id, &fx);
Delete { whole_files: true }.apply(&mut fx.state()).unwrap();
assert_eq!(fx.order.0, vec![keep]);
assert!(!fx.files.contains_key(&id));
assert!(matches!(fx.selection, Selection::Empty));
}
}
#[test]
fn test_delete_whole_files_nothing_selected() {
let mut fx = Fixture::default();
assert_eq!(
Delete { whole_files: true }.apply(&mut fx.state()),
2026-10-03 16:52:21 +02:00
Err(CommandError::NothingToDo)
);
2026-10-03 16:40:17 +02:00
}
}