mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-11-15 07:07:07 +00:00
Migration to new Rust-based engine (#401)
* wasm first steps * parsing start * parsing continued * progress * progress * progress * progress * progress * progress * progress * progress * fixes * reorganize * change type * fix * progress * refactoring * progress * progress * progress * keep style info * progress * progress * remove (copy) in name * progress * merge methods into 1 * splice * progress * progress * progress * cleaning * progress * progress * progress * visibility * progress * progress * progress * progress * progress * fixes * progress * fix markers * fix * progress * progress * progress * progress * progress * progress * small fix * use engine info * fix popups * small fix * progress * progress * progress * clean tool * handle self closing elements * reduce data exchanges across boundary * copy on wasm side to avoid any data corruption * speed fix * routing * refactoring * reverse * reverse * crop * split * merge * progress * time tool * fix bounds for waypoints * reduce * fix reactivity * handle corrupt time data * export * progress * remove gpx js package * remove todo * update readme * cleaning * progress * fix link fields * remove unused fields * refactoring and tests * import and export of routes, ref #192 * per-chunk categories storage * cross-tab sync * outdated state * refactoring * reorder methods * refactoring * fix * improve flow * renaming * progress * progress * fix * progress * per file history * keep original flag * use history variant * fix * progress * file explorer * progress * fix * sticky folders * improved sticky folders * fix sortable * fix sortable * fix * fix tab size * no file delete on desktop * fix routing on other file * fix sorting * remember language * open links in browser * commit missing changes * avoid selecting on resize * clarify filename * explorer actions * fix folder styling * drop anywhere in folder * cleaning * fixes * remove focus-ring * full screen * small fix * consistent highlight color * improved preview * performance improvements * sparser data transfers * sparser data transfers * show popup once data is loaded * fix routing control size * fix * sparse desktop build * remove custom ign styles * use new endpoint for openhikingmap * strip native menu * reorder context menu items * route full support * route - track conversion * create route file action * route export, filter out anchors at high zoom level * clarify name handling * move hide & center to view * routes continued * update docs * fix power format, closes #378 * lint fixes * fix display of additional data with gaps, closes #361 * fix linting issues * fix import modules * fix multidrag * change base to resolve * fix type errors * fix layer typings * fix last type error * each keys * improve typings * deploy to cloudflare --------- Co-authored-by: vcoppe <vcoppe@hexaly.com>
This commit is contained in:
1 parent
6a0d434371
commit
8fe7d8d64d
484 files changed
+53949
-41152
No files matched your search
@@ -0,0 +1,323 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, FileId, Files, Selection, State, Waypoint, copy_file, copy_route,
|
||||
copy_segment, copy_track, copy_waypoint,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Duplicate;
|
||||
|
||||
impl Apply for Duplicate {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let next = match &*state.selection {
|
||||
Selection::File { file_ids } => {
|
||||
let mut copies = HashSet::new();
|
||||
let mut order = Vec::new();
|
||||
for id in state.order.0.iter().filter(|id| file_ids.contains(id)) {
|
||||
let Some(file) = state.files.get(id) else {
|
||||
continue;
|
||||
};
|
||||
let copy = copy_file(file);
|
||||
order.push(copy.id);
|
||||
copies.insert(copy.id);
|
||||
state.files.insert(copy.id, Rc::new(copy));
|
||||
}
|
||||
if copies.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
let at = state
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.rposition(|id| file_ids.contains(id))
|
||||
.map_or(state.order.0.len(), |i| i + 1);
|
||||
state.order.0.splice(at..at, order);
|
||||
Selection::File { file_ids: copies }
|
||||
}
|
||||
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 mut copies = HashSet::new();
|
||||
append_copies(
|
||||
&mut file.rte,
|
||||
|rte| rte_ids.contains(&rte.id),
|
||||
|rte| {
|
||||
let copy = copy_route(rte);
|
||||
copies.insert(copy.id);
|
||||
copy
|
||||
},
|
||||
);
|
||||
if copies.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
Selection::Route {
|
||||
file_id: *file_id,
|
||||
rte_ids: copies,
|
||||
}
|
||||
}
|
||||
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 mut copies = HashSet::new();
|
||||
append_copies(
|
||||
&mut file.trk,
|
||||
|trk| trk_ids.contains(&trk.id),
|
||||
|trk| {
|
||||
let copy = copy_track(trk);
|
||||
copies.insert(copy.id);
|
||||
copy
|
||||
},
|
||||
);
|
||||
if copies.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
Selection::Track {
|
||||
file_id: *file_id,
|
||||
trk_ids: copies,
|
||||
}
|
||||
}
|
||||
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 mut copies = HashSet::new();
|
||||
append_copies(
|
||||
&mut trk.trkseg,
|
||||
|seg| trkseg_ids.contains(&seg.id),
|
||||
|seg| {
|
||||
let copy = copy_segment(seg);
|
||||
copies.insert(copy.id);
|
||||
copy
|
||||
},
|
||||
);
|
||||
if copies.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id: *file_id,
|
||||
trk_id: *trk_id,
|
||||
trkseg_ids: copies,
|
||||
}
|
||||
}
|
||||
Selection::Waypoints { file_id } => {
|
||||
duplicate_waypoints(state.files, *file_id, |_| true)?
|
||||
}
|
||||
Selection::Waypoint { file_id, wpt_ids } => {
|
||||
duplicate_waypoints(state.files, *file_id, |wpt| wpt_ids.contains(&wpt.id))?
|
||||
}
|
||||
Selection::Empty => return Err(CommandError::NothingToDo),
|
||||
};
|
||||
*state.selection = next;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn duplicate_waypoints(
|
||||
files: &mut Files,
|
||||
file_id: FileId,
|
||||
filter: impl Fn(&Waypoint) -> bool,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let file = files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let selected: Vec<&Waypoint> = file.wpt.iter().filter(|wpt| filter(wpt)).collect();
|
||||
let last = selected.last().ok_or(CommandError::NothingToDo)?.id;
|
||||
let copies: Vec<Waypoint> = selected.into_iter().map(copy_waypoint).collect();
|
||||
let copy_ids = copies.iter().map(|wpt| wpt.id).collect();
|
||||
|
||||
// The copies go right after the last selected waypoint.
|
||||
let mut file = (**file).clone();
|
||||
file.wpt.insert_after(Some(last), copies);
|
||||
files.insert(file_id, Rc::new(file));
|
||||
Ok(Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: copy_ids,
|
||||
})
|
||||
}
|
||||
|
||||
/// Inserts a copy of each item matching `filter`, as a block after the last matching item.
|
||||
fn append_copies<T>(items: &mut Vec<T>, filter: impl Fn(&T) -> bool, copy: impl FnMut(&T) -> T) {
|
||||
let Some(last) = items.iter().rposition(&filter) else {
|
||||
return;
|
||||
};
|
||||
let copies: Vec<T> = items.iter().filter(|item| filter(item)).map(copy).collect();
|
||||
items.splice(last + 1..last + 1, copies);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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();
|
||||
Load {
|
||||
data: &data,
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let id = fx.order.0[0];
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_nothing_selected() {
|
||||
let mut fx = Fixture::default();
|
||||
assert_eq!(
|
||||
Duplicate.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_file() {
|
||||
let (mut fx, id) = loaded();
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
assert_eq!(fx.files.len(), 2);
|
||||
let copy_id = fx.order.0[1];
|
||||
assert_eq!(fx.selected_files(), [copy_id].into());
|
||||
let (orig, copy) = (&fx.files[&id], &fx.files[©_id]);
|
||||
assert_ne!(copy_id, id);
|
||||
assert_eq!(copy.info.name, orig.info.name);
|
||||
assert_eq!(copy.trk.len(), orig.trk.len());
|
||||
for (a, b) in orig.trk.iter().zip(©.trk) {
|
||||
assert_ne!(a.id, b.id);
|
||||
assert_eq!(a.info, b.info);
|
||||
for (a, b) in a.trkseg.iter().zip(&b.trkseg) {
|
||||
assert_ne!(a.id, b.id);
|
||||
assert_eq!(a.len(), b.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_files_go_after_last_selected() {
|
||||
let mut fx = Fixture::default();
|
||||
for name in ["a", "b", "c"] {
|
||||
crate::New {
|
||||
name,
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
}
|
||||
let [a, b, c] = [fx.order.0[0], fx.order.0[1], fx.order.0[2]];
|
||||
fx.selection = Selection::File {
|
||||
file_ids: [a, b].into(),
|
||||
};
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
assert_eq!(fx.order.0.len(), 5);
|
||||
assert_eq!(fx.order.0[..2], [a, b]);
|
||||
assert_eq!(fx.order.0[4], c);
|
||||
assert_eq!(
|
||||
fx.selected_files(),
|
||||
fx.order.0[2..4].iter().copied().collect()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_track_appends_copy() {
|
||||
let (mut fx, id) = loaded();
|
||||
let before = fx.files[&id].trk.len();
|
||||
let (trk_id, second_id) = (fx.files[&id].trk[0].id, fx.files[&id].trk[1].id);
|
||||
fx.selection = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [trk_id].into(),
|
||||
};
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
let file = &fx.files[&id];
|
||||
assert_eq!(file.trk.len(), before + 1);
|
||||
assert_eq!(file.trk[0].id, trk_id);
|
||||
assert_ne!(file.trk[1].id, trk_id);
|
||||
// copies go right after the selected track, not at the end
|
||||
assert_eq!(file.trk[2].id, second_id);
|
||||
assert!(
|
||||
matches!(&fx.selection, Selection::Track { trk_ids, .. } if trk_ids.contains(&file.trk[1].id))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_selected_waypoints() {
|
||||
let (mut fx, id) = loaded();
|
||||
let wpts: Vec<_> = (0..3).map(|_| Waypoint::default()).collect();
|
||||
let ids: Vec<_> = wpts.iter().map(|w| w.id).collect();
|
||||
let mut file = (*fx.files[&id]).clone();
|
||||
file.wpt = crate::Waypoints::new([crate::WaypointChunk {
|
||||
wpt: wpts,
|
||||
..Default::default()
|
||||
}]);
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
let original_chunk = fx.files[&id].wpt.chunks()[0].clone();
|
||||
|
||||
fx.selection = Selection::Waypoint {
|
||||
file_id: id,
|
||||
wpt_ids: HashSet::from([ids[0]]),
|
||||
};
|
||||
let rev = fx.files[&id].wpt.rev_id;
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
assert_ne!(fx.files[&id].wpt.rev_id, rev);
|
||||
let all: Vec<_> = fx.files[&id].wpt.iter().map(|w| w.id).collect();
|
||||
assert_eq!(all.len(), 4);
|
||||
assert!(
|
||||
matches!(&fx.selection, Selection::Waypoint { wpt_ids, .. } if wpt_ids == &HashSet::from([all[1]]))
|
||||
);
|
||||
// the copy comes right after the selected waypoint, in a new chunk
|
||||
assert_eq!(all[..], [ids[0], all[1], ids[1], ids[2]]);
|
||||
assert_eq!(fx.files[&id].wpt.chunks().len(), 1);
|
||||
assert_ne!(fx.files[&id].wpt.chunks()[0].id, original_chunk.id);
|
||||
|
||||
// selecting the last waypoint: the copy goes at the end
|
||||
let last = fx.files[&id].wpt.iter().last().unwrap().id;
|
||||
fx.selection = Selection::Waypoint {
|
||||
file_id: id,
|
||||
wpt_ids: HashSet::from([last]),
|
||||
};
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
let all: Vec<_> = fx.files[&id].wpt.iter().map(|w| w.id).collect();
|
||||
assert_eq!(all.len(), 5);
|
||||
assert_eq!(all[..4], [ids[0], all[1], ids[1], ids[2]]);
|
||||
|
||||
fx.selection = Selection::Waypoints { file_id: id };
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
assert_eq!(fx.files[&id].wpt.len(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_segment_appends_copy() {
|
||||
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: [seg_id].into(),
|
||||
};
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
let trk = &fx.files[&id].trk[0];
|
||||
assert_eq!(trk.trkseg.len(), before + 1);
|
||||
assert_eq!(trk.trkseg[0].id, seg_id);
|
||||
assert_ne!(trk.trkseg[1].id, seg_id);
|
||||
assert_eq!(trk.trkseg[0].len(), trk.trkseg[1].len());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user