mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-07 06:14:07 +00:00
progress
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
use crate::{Apply, CommandError, State};
|
||||
use crate::{
|
||||
Clean, Crop, Delete, DeleteAll, Duplicate, Elevation, Extract, Load, Merge, Metadata, Move,
|
||||
MoveWaypoint, New, NewTrack, NewTrackSegment, NewWaypoint, Paste, Reverse, SpliceTrackpoints,
|
||||
Split, Style, Time,
|
||||
Clean, Crop, Delete, DeleteAll, Duplicate, Elevation, Extract, Load, LoadFiles, Merge,
|
||||
Metadata, Move, MoveWaypoint, New, NewTrack, NewTrackSegment, NewWaypoint, Paste, Reverse,
|
||||
SpliceTrackpoints, Split, Style, Time,
|
||||
};
|
||||
|
||||
/// A user action that edits the files. Each variant wraps the command's own struct, whose
|
||||
@@ -14,6 +14,7 @@ use crate::{
|
||||
pub enum Command<'a> {
|
||||
New(New<'a>),
|
||||
Load(Load<'a>),
|
||||
LoadFiles(LoadFiles<'a>),
|
||||
Duplicate(Duplicate),
|
||||
Delete(Delete),
|
||||
Paste(Paste),
|
||||
@@ -41,6 +42,7 @@ impl Apply for Command<'_> {
|
||||
match self {
|
||||
Command::New(c) => c.apply(state),
|
||||
Command::Load(c) => c.apply(state),
|
||||
Command::LoadFiles(c) => c.apply(state),
|
||||
Command::Duplicate(c) => c.apply(state),
|
||||
Command::Delete(c) => c.apply(state),
|
||||
Command::Paste(c) => c.apply(state),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Apply, CommandError, State, parse, produce};
|
||||
use crate::{Apply, CommandError, File, State, parse, produce};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Load<'a> {
|
||||
@@ -8,14 +8,49 @@ pub struct Load<'a> {
|
||||
pub name: &'a str,
|
||||
}
|
||||
|
||||
impl Apply for Load<'_> {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
impl Load<'_> {
|
||||
fn parse(self) -> Result<File, CommandError> {
|
||||
let mut file =
|
||||
parse(self.data).map_err(|err| CommandError::InvalidData(err.to_string()))?;
|
||||
if file.info.name.trim().is_empty() {
|
||||
file.info.name = self.name.to_owned();
|
||||
}
|
||||
produce(state, |_| vec![file]);
|
||||
Ok(file)
|
||||
}
|
||||
}
|
||||
|
||||
impl Apply for Load<'_> {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
LoadFiles { files: vec![self] }.apply(state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads several files at once, as a single command (one undo step): the files are added at the
|
||||
/// end of the list, in the given order, and the first one is selected.
|
||||
///
|
||||
/// The files that cannot be read are skipped, unless none of them can: then nothing is loaded
|
||||
/// and the error is the one of the first file.
|
||||
#[derive(Debug)]
|
||||
pub struct LoadFiles<'a> {
|
||||
pub files: Vec<Load<'a>>,
|
||||
}
|
||||
|
||||
impl Apply for LoadFiles<'_> {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let mut files = Vec::with_capacity(self.files.len());
|
||||
let mut first_error = None;
|
||||
for load in self.files {
|
||||
match load.parse() {
|
||||
Ok(file) => files.push(file),
|
||||
Err(error) => {
|
||||
first_error.get_or_insert(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
if files.is_empty() {
|
||||
return Err(first_error.unwrap_or(CommandError::NothingToDo));
|
||||
}
|
||||
produce(state, |_| files);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -85,4 +120,101 @@ mod tests {
|
||||
let empty = br#"<gpx version="1.1"><metadata><name> </name></metadata></gpx>"#;
|
||||
assert_eq!(load_with_name(empty), "from disk");
|
||||
}
|
||||
|
||||
fn gpx(name: &str) -> Vec<u8> {
|
||||
format!(r#"<gpx version="1.1"><metadata><name>{name}</name></metadata></gpx>"#).into_bytes()
|
||||
}
|
||||
|
||||
fn load<'a>(data: &'a [u8], name: &'a str) -> Load<'a> {
|
||||
Load { data, name }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_several_files() {
|
||||
let mut fx = Fixture::default();
|
||||
crate::New { name: "before" }
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let before = fx.order.0[0];
|
||||
let (a, b, c) = (gpx("a"), gpx("b"), gpx("c"));
|
||||
|
||||
LoadFiles {
|
||||
files: vec![load(&a, "x"), load(&b, "x"), load(&c, "x")],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
// added after the others, in the order they were given
|
||||
let names: Vec<_> = fx
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.map(|id| fx.files[id].info.name.clone())
|
||||
.collect();
|
||||
assert_eq!(names, ["before", "a", "b", "c"]);
|
||||
assert_eq!(fx.order.0[0], before);
|
||||
// the first one is selected
|
||||
assert_eq!(fx.selected_files(), [fx.order.0[1]].into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_several_files_skips_the_invalid_ones() {
|
||||
let mut fx = Fixture::default();
|
||||
let (a, c) = (gpx("a"), gpx("c"));
|
||||
LoadFiles {
|
||||
files: vec![load(&a, "x"), load(b"<gpx><trk></gpx>", "x"), load(&c, "x")],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let names: Vec<_> = fx
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.map(|id| fx.files[id].info.name.clone())
|
||||
.collect();
|
||||
assert_eq!(names, ["a", "c"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_several_invalid_files_changes_nothing() {
|
||||
let mut fx = Fixture::default();
|
||||
crate::New { name: "keep" }.apply(&mut fx.state()).unwrap();
|
||||
let selected = fx.selected_files();
|
||||
|
||||
let result = LoadFiles {
|
||||
files: vec![
|
||||
load(b"<gpx><trk></gpx>", "x"),
|
||||
load(b"<gpx><wpt></gpx>", "y"),
|
||||
],
|
||||
}
|
||||
.apply(&mut fx.state());
|
||||
assert!(matches!(result, Err(CommandError::InvalidData(_))));
|
||||
assert_eq!(fx.files.len(), 1);
|
||||
assert_eq!(fx.order.0.len(), 1);
|
||||
assert_eq!(fx.selected_files(), selected);
|
||||
|
||||
// nothing to load
|
||||
assert_eq!(
|
||||
LoadFiles { files: vec![] }.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_each_file_without_name_gets_its_own_name() {
|
||||
let mut fx = Fixture::default();
|
||||
let unnamed = br#"<gpx version="1.1"><trk><trkseg></trkseg></trk></gpx>"#;
|
||||
LoadFiles {
|
||||
files: vec![load(unnamed, "first"), load(unnamed, "second")],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let names: Vec<_> = fx
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.map(|id| fx.files[id].info.name.clone())
|
||||
.collect();
|
||||
assert_eq!(names, ["first", "second"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1071,4 +1071,43 @@ mod tests {
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert_eq!(track_ids(&engine, a), tracks);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_several_files_is_one_edit() {
|
||||
use crate::LoadFiles;
|
||||
|
||||
let mut engine = Engine::default();
|
||||
new(&mut engine, "before");
|
||||
let gpx = |name: &str| {
|
||||
format!(r#"<gpx version="1.1"><metadata><name>{name}</name></metadata></gpx>"#)
|
||||
.into_bytes()
|
||||
};
|
||||
let (a, b) = (gpx("a"), gpx("b"));
|
||||
assert!(edit(
|
||||
&mut engine,
|
||||
Command::LoadFiles(LoadFiles {
|
||||
files: vec![
|
||||
Load {
|
||||
data: &a,
|
||||
name: "x"
|
||||
},
|
||||
Load {
|
||||
data: &b,
|
||||
name: "x"
|
||||
},
|
||||
],
|
||||
})
|
||||
));
|
||||
assert_eq!(engine.order().len(), 3);
|
||||
let diff = engine.last_diff().unwrap();
|
||||
assert_eq!(diff.added.len(), 2);
|
||||
// the first one that was loaded is selected
|
||||
assert_eq!(engine.selection(), &files(&[engine.order()[1]]));
|
||||
|
||||
// a single undo removes both
|
||||
assert!(engine.execute(Action::Undo));
|
||||
assert_eq!(engine.order().len(), 1);
|
||||
assert!(engine.execute(Action::Redo));
|
||||
assert_eq!(engine.order().len(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user