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,674 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, File, FileId, LngLat, LngLatBounds, Route, Selection, State, Track,
|
||||
TrackId, 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
|
||||
/// each:
|
||||
///
|
||||
/// - files: a file with several routes and tracks becomes one file per route and per track, in
|
||||
/// which the track is split into one track per segment. A file with a single track becomes one
|
||||
/// file per segment. The new files are in the place of the file, which goes, and are selected.
|
||||
/// They are named after the route or track, or the file, and numbered. The waypoints go to the files that have the segments
|
||||
/// closest to them (to the first file if the file has no trackpoint).
|
||||
/// - tracks: each selected track is replaced by one track per segment, which are selected. They
|
||||
/// are named after the track and numbered.
|
||||
///
|
||||
/// What has a single segment is left as it is, which routes always are. 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)]
|
||||
pub struct Extract {
|
||||
pub keep_originals: bool,
|
||||
}
|
||||
|
||||
impl Apply for Extract {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let selection = state.selection.clone();
|
||||
match &selection {
|
||||
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),
|
||||
_ => Err(CommandError::NothingToDo),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
let mut extracted: Vec<(FileId, Vec<File>)> = vec![];
|
||||
for id in state.order.0.iter().filter(|id| file_ids.contains(id)) {
|
||||
let file = &state.files[id];
|
||||
if file.segments().count() > 1 {
|
||||
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() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
let mut order = vec![];
|
||||
for id in state.order.0.iter() {
|
||||
match extracted.iter().find(|(original, _)| original == id) {
|
||||
Some((_, files)) => {
|
||||
if keep_originals {
|
||||
order.push(*id);
|
||||
}
|
||||
order.extend(files.iter().map(|file| file.id));
|
||||
}
|
||||
None => order.push(*id),
|
||||
}
|
||||
}
|
||||
// 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 {
|
||||
if !keep_originals {
|
||||
state.files.remove(&original);
|
||||
}
|
||||
selected.remove(&original);
|
||||
for file in files {
|
||||
selected.insert(file.id);
|
||||
state.files.insert(file.id, Rc::new(file));
|
||||
}
|
||||
}
|
||||
state.order.0 = order;
|
||||
*state.selection = Selection::File { file_ids: selected };
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The files that a file with several segments becomes.
|
||||
fn extract_file(file: &File) -> Vec<File> {
|
||||
// for each new file: its routes, its tracks and its name, and the segments that go with it
|
||||
struct Part {
|
||||
routes: Vec<Route>,
|
||||
tracks: Vec<Track>,
|
||||
name: String,
|
||||
}
|
||||
let name = |name: &Option<String>, i: usize| {
|
||||
name.clone()
|
||||
.unwrap_or_else(|| format!("{} ({})", file.info.name, i + 1))
|
||||
};
|
||||
let (parts, owner): (Vec<Part>, Vec<usize>) =
|
||||
if file.rte.len() + file.trk.len() > 1 {
|
||||
// one file per route and per track, the segments of a track follow it
|
||||
let routes = file.rte.iter().enumerate().map(|(i, route)| Part {
|
||||
routes: vec![route.clone()],
|
||||
tracks: vec![],
|
||||
name: name(&route.info.name, i),
|
||||
});
|
||||
let tracks = file.trk.iter().enumerate().map(|(i, track)| Part {
|
||||
routes: vec![],
|
||||
tracks: split_track(track),
|
||||
name: name(&track.info.name, file.rte.len() + i),
|
||||
});
|
||||
let parts = routes.chain(tracks).collect();
|
||||
let owner =
|
||||
(0..file.rte.len())
|
||||
.chain(file.trk.iter().enumerate().flat_map(|(i, track)| {
|
||||
track.trkseg.iter().map(move |_| file.rte.len() + i)
|
||||
}))
|
||||
.collect();
|
||||
(parts, owner)
|
||||
} else {
|
||||
// one file per segment
|
||||
let track = &file.trk[0];
|
||||
let base = track
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| file.info.name.clone());
|
||||
let parts = track
|
||||
.trkseg
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, segment)| Part {
|
||||
routes: vec![],
|
||||
tracks: vec![Track {
|
||||
id: if i == 0 { track.id } else { Default::default() },
|
||||
trkseg: vec![segment.clone()],
|
||||
..track.clone()
|
||||
}],
|
||||
name: format!("{base} ({})", i + 1),
|
||||
})
|
||||
.collect();
|
||||
let owner = (0..track.trkseg.len()).collect();
|
||||
(parts, owner)
|
||||
};
|
||||
|
||||
// the waypoints go with the segments that are the closest to them
|
||||
let segments: Vec<&TrackSegment> = file.segments().collect();
|
||||
let mut waypoints: Vec<Vec<Waypoint>> = vec![vec![]; parts.len()];
|
||||
for waypoint in file.wpt.iter() {
|
||||
let mut owners: Vec<usize> = closest_segments(&segments, waypoint.coordinates)
|
||||
.into_iter()
|
||||
.map(|segment| owner[segment])
|
||||
.collect();
|
||||
owners.sort_unstable();
|
||||
owners.dedup();
|
||||
if owners.is_empty() {
|
||||
owners.push(0);
|
||||
}
|
||||
for (n, part) in owners.into_iter().enumerate() {
|
||||
// a waypoint that goes to several files has a new id in all but the first
|
||||
waypoints[part].push(if n == 0 {
|
||||
waypoint.clone()
|
||||
} else {
|
||||
copy_waypoint(waypoint)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
parts
|
||||
.into_iter()
|
||||
.zip(waypoints)
|
||||
.map(|(part, waypoints)| {
|
||||
let mut extracted = File {
|
||||
id: Default::default(),
|
||||
rte: part.routes,
|
||||
trk: part.tracks,
|
||||
wpt: Waypoints::default(),
|
||||
..file.clone()
|
||||
};
|
||||
extracted.info.name = part.name;
|
||||
extracted.wpt.insert_at(0, waypoints);
|
||||
extracted
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn extract_tracks(
|
||||
state: &mut State,
|
||||
file_id: FileId,
|
||||
trk_ids: &HashSet<TrackId>,
|
||||
) -> Result<(), CommandError> {
|
||||
let file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let mut changed = false;
|
||||
let mut tracks = vec![];
|
||||
let mut selected: HashSet<TrackId> = trk_ids.clone();
|
||||
for track in &file.trk {
|
||||
if trk_ids.contains(&track.id) && track.trkseg.len() > 1 {
|
||||
changed = true;
|
||||
selected.remove(&track.id);
|
||||
let pieces = split_track(track);
|
||||
selected.extend(pieces.iter().map(|piece| piece.id));
|
||||
tracks.extend(pieces);
|
||||
} else {
|
||||
tracks.push(track.clone());
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
let mut file = (**file).clone();
|
||||
file.trk = tracks;
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
*state.selection = Selection::Track {
|
||||
file_id,
|
||||
trk_ids: selected,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The tracks that a track becomes: one per segment, the first one with the id of the track. The
|
||||
/// others have the same info, with the name of the track and the number of the segment, if it has
|
||||
/// a name. A track with at most one segment stays as it is.
|
||||
fn split_track(track: &Track) -> Vec<Track> {
|
||||
if track.trkseg.len() <= 1 {
|
||||
return vec![track.clone()];
|
||||
}
|
||||
track
|
||||
.trkseg
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, segment)| {
|
||||
let mut piece = Track {
|
||||
id: if i == 0 { track.id } else { Default::default() },
|
||||
trkseg: vec![segment.clone()],
|
||||
..track.clone()
|
||||
};
|
||||
piece.info.name = track
|
||||
.info
|
||||
.name
|
||||
.as_ref()
|
||||
.map(|name| format!("{name} ({})", i + 1));
|
||||
piece
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The indices of the segments that have the trackpoint that is the closest to `point`, which are
|
||||
/// several when the closest trackpoints are as close.
|
||||
///
|
||||
/// The segments are looked at from the closest to the farthest bounds, which is how they are
|
||||
/// left out: the ones whose bounds are farther than the closest trackpoint found.
|
||||
fn closest_segments(segments: &[&TrackSegment], point: LngLat) -> Vec<usize> {
|
||||
let mut by_bounds: Vec<(f64, usize)> = segments
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, segment)| !segment.is_empty())
|
||||
.map(|(i, segment)| {
|
||||
let mut bounds = LngLatBounds::default();
|
||||
segment
|
||||
.iter()
|
||||
.for_each(|trkpt| bounds.extend(trkpt.coordinates));
|
||||
(distance_to_bounds(&bounds, point), i)
|
||||
})
|
||||
.collect();
|
||||
by_bounds.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
|
||||
let mut closest = f64::MAX;
|
||||
let mut indices: Vec<usize> = vec![];
|
||||
for (bounds_distance, i) in by_bounds {
|
||||
if bounds_distance > closest {
|
||||
break;
|
||||
}
|
||||
for trkpt in segments[i].iter() {
|
||||
// in meters, like the distances to the bounds
|
||||
let d = distance(trkpt.coordinates, point) * 1000.0;
|
||||
if d < closest {
|
||||
closest = d;
|
||||
indices = vec![i];
|
||||
} else if d == closest {
|
||||
indices.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
indices.sort_unstable();
|
||||
indices.dedup();
|
||||
indices
|
||||
}
|
||||
|
||||
/// 0 inside the bounds, else the distance in meters to the closest side.
|
||||
fn distance_to_bounds(bounds: &LngLatBounds, point: LngLat) -> f64 {
|
||||
let (sw, ne) = (bounds.sw, bounds.ne);
|
||||
if (sw.lng..=ne.lng).contains(&point.lng) && (sw.lat..=ne.lat).contains(&point.lat) {
|
||||
return 0.0;
|
||||
}
|
||||
let nw = LngLat {
|
||||
lng: sw.lng,
|
||||
lat: ne.lat,
|
||||
};
|
||||
let se = LngLat {
|
||||
lng: ne.lng,
|
||||
lat: sw.lat,
|
||||
};
|
||||
[(nw, ne), (ne, se), (se, sw), (sw, nw)]
|
||||
.into_iter()
|
||||
.map(|(a, b)| crossarc_lnglat(a, b, point))
|
||||
.fold(f64::MAX, f64::min)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{TrackInfo, Trackpoint, engine::command::fixture::Fixture};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A segment of 5 trackpoints along the equator, from the longitude `from`.
|
||||
fn segment(from: f64) -> TrackSegment {
|
||||
let mut segment = TrackSegment::default();
|
||||
let points = (0..5)
|
||||
.map(|i| Trackpoint {
|
||||
coordinates: LngLat {
|
||||
lng: from + i as f64 * 0.001,
|
||||
lat: 0.0,
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
segment.splice(0, 0, points);
|
||||
segment
|
||||
}
|
||||
|
||||
fn track(name: Option<&str>, from: &[f64]) -> Track {
|
||||
Track {
|
||||
info: TrackInfo {
|
||||
name: name.map(str::to_string),
|
||||
..Default::default()
|
||||
},
|
||||
trkseg: from.iter().map(|from| segment(*from)).collect(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn waypoint(name: &str, lng: f64) -> Waypoint {
|
||||
Waypoint {
|
||||
name: Some(name.into()),
|
||||
coordinates: LngLat { lng, lat: 0.001 },
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn fixture(file: File) -> (Fixture, FileId) {
|
||||
let mut fx = Fixture::default();
|
||||
let id = file.id;
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.order.0.push(id);
|
||||
fx.selection = Selection::File {
|
||||
file_ids: [id].into(),
|
||||
};
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
fn file(name: &str, tracks: Vec<Track>, waypoints: Vec<Waypoint>) -> File {
|
||||
let mut file = File {
|
||||
trk: tracks,
|
||||
..Default::default()
|
||||
};
|
||||
file.info.name = name.into();
|
||||
file.wpt.insert_at(0, waypoints);
|
||||
file
|
||||
}
|
||||
|
||||
fn names(fx: &Fixture) -> Vec<String> {
|
||||
fx.order
|
||||
.0
|
||||
.iter()
|
||||
.map(|id| fx.files[id].info.name.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_a_file_with_one_track_becomes_one_file_per_segment() {
|
||||
let original = file(
|
||||
"route",
|
||||
vec![track(Some("climb"), &[0.0, 10.0, 20.0])],
|
||||
// near the first segment, near the last one, and in between, closer to the middle one
|
||||
vec![
|
||||
waypoint("a", 0.002),
|
||||
waypoint("c", 20.002),
|
||||
waypoint("b", 10.0),
|
||||
],
|
||||
);
|
||||
let track_id = original.trk[0].id;
|
||||
let segment_ids: Vec<_> = original.trk[0].trkseg.iter().map(|s| s.id).collect();
|
||||
let (mut fx, id) = fixture(original);
|
||||
let other = file("other", vec![track(None, &[50.0])], vec![]);
|
||||
let other_id = other.id;
|
||||
fx.files.insert(other_id, Rc::new(other));
|
||||
fx.order.0.push(other_id);
|
||||
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
// in the place of the file, in order, named after the track
|
||||
assert!(!fx.files.contains_key(&id));
|
||||
assert_eq!(names(&fx), ["climb (1)", "climb (2)", "climb (3)", "other"]);
|
||||
assert_eq!(fx.order.0[3], other_id);
|
||||
for (i, file_id) in fx.order.0[..3].iter().enumerate() {
|
||||
let file = &fx.files[file_id];
|
||||
assert_eq!(file.trk.len(), 1);
|
||||
assert_eq!(file.trk[0].trkseg.len(), 1);
|
||||
assert_eq!(file.trk[0].trkseg[0].id, segment_ids[i]);
|
||||
assert_eq!(file.trk[0].info.name.as_deref(), Some("climb"));
|
||||
}
|
||||
assert_eq!(fx.files[&fx.order.0[0]].trk[0].id, track_id);
|
||||
// the waypoints are with the closest segment
|
||||
let waypoints = |i: usize| -> Vec<String> {
|
||||
fx.files[&fx.order.0[i]]
|
||||
.wpt
|
||||
.iter()
|
||||
.map(|w| w.name.clone().unwrap())
|
||||
.collect()
|
||||
};
|
||||
assert_eq!(waypoints(0), ["a"]);
|
||||
assert_eq!(waypoints(1), ["b"]);
|
||||
assert_eq!(waypoints(2), ["c"]);
|
||||
// the new files are selected, the other one is not
|
||||
assert!(matches!(&fx.selection, Selection::File { file_ids }
|
||||
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]
|
||||
fn test_a_file_with_several_tracks_becomes_one_file_per_track() {
|
||||
let original = file(
|
||||
"route",
|
||||
vec![
|
||||
track(Some("first"), &[0.0, 10.0]),
|
||||
track(None, &[20.0]),
|
||||
track(Some("third"), &[30.0, 40.0]),
|
||||
],
|
||||
vec![],
|
||||
);
|
||||
let ids: Vec<_> = original.trk.iter().map(|t| t.id).collect();
|
||||
let (mut fx, _) = fixture(original);
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
// named after the track, or the file
|
||||
assert_eq!(names(&fx), ["first", "route (2)", "third"]);
|
||||
let tracks = |i: usize| &fx.files[&fx.order.0[i]].trk;
|
||||
// a track with several segments is split in as many tracks
|
||||
assert_eq!(tracks(0).len(), 2);
|
||||
assert_eq!(tracks(0)[0].info.name.as_deref(), Some("first (1)"));
|
||||
assert_eq!(tracks(0)[1].info.name.as_deref(), Some("first (2)"));
|
||||
assert_eq!(tracks(0)[0].id, ids[0]);
|
||||
// and one that has a single segment is not renamed
|
||||
assert_eq!(tracks(1).len(), 1);
|
||||
assert_eq!(tracks(1)[0].id, ids[1]);
|
||||
assert_eq!(tracks(1)[0].info.name, None);
|
||||
assert_eq!(tracks(2).len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_waypoints_with_closest_segments_that_tie_are_copied() {
|
||||
// the segments share a trackpoint, where the waypoint is: it is as close to both
|
||||
let a = segment(0.0);
|
||||
let b = segment(a[4].coordinates.lng);
|
||||
let shared = Waypoint {
|
||||
name: Some("mid".into()),
|
||||
coordinates: a[4].coordinates,
|
||||
..Default::default()
|
||||
};
|
||||
let original = file(
|
||||
"f",
|
||||
vec![Track {
|
||||
trkseg: vec![a, b],
|
||||
..Default::default()
|
||||
}],
|
||||
vec![shared, waypoint("lost", 100.0)],
|
||||
);
|
||||
let (mut fx, _) = fixture(original);
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let first = &fx.files[&fx.order.0[0]];
|
||||
let second = &fx.files[&fx.order.0[1]];
|
||||
// the tie goes to both, with different ids
|
||||
let mid_in = |f: &File| {
|
||||
f.wpt
|
||||
.iter()
|
||||
.find(|w| w.name.as_deref() == Some("mid"))
|
||||
.map(|w| w.id)
|
||||
};
|
||||
assert!(mid_in(first).is_some() && mid_in(second).is_some());
|
||||
assert_ne!(mid_in(first), mid_in(second));
|
||||
// a far waypoint goes to its closest segment, which is the last one
|
||||
assert!(second.wpt.iter().any(|w| w.name.as_deref() == Some("lost")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_a_waypoint_of_a_file_without_trackpoints_is_kept() {
|
||||
let mut empty = TrackSegment::default();
|
||||
empty.rev_id = Default::default();
|
||||
let original = file(
|
||||
"f",
|
||||
vec![Track {
|
||||
trkseg: vec![empty.clone(), TrackSegment::default()],
|
||||
..Default::default()
|
||||
}],
|
||||
vec![waypoint("w", 1.0)],
|
||||
);
|
||||
let (mut fx, _) = fixture(original);
|
||||
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[1]].wpt.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_tracks_in_place() {
|
||||
let original = file(
|
||||
"route",
|
||||
vec![
|
||||
track(Some("a"), &[0.0]),
|
||||
track(Some("b"), &[10.0, 20.0, 30.0]),
|
||||
track(None, &[40.0, 50.0]),
|
||||
],
|
||||
vec![],
|
||||
);
|
||||
let ids: Vec<_> = original.trk.iter().map(|t| t.id).collect();
|
||||
let (mut fx, id) = fixture(original);
|
||||
fx.selection = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [ids[1], ids[2]].into(),
|
||||
};
|
||||
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let file = &fx.files[&id];
|
||||
let names: Vec<_> = file.trk.iter().map(|t| t.info.name.clone()).collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
[
|
||||
Some("a".into()),
|
||||
Some("b (1)".into()),
|
||||
Some("b (2)".into()),
|
||||
Some("b (3)".into()),
|
||||
None,
|
||||
None
|
||||
]
|
||||
);
|
||||
assert_eq!(file.trk.len(), 6);
|
||||
assert!(file.trk.iter().all(|t| t.trkseg.len() == 1));
|
||||
assert_eq!(file.trk[0].id, ids[0]);
|
||||
assert_eq!(file.trk[1].id, ids[1]);
|
||||
// the new tracks are selected
|
||||
let Selection::Track { trk_ids, .. } = &fx.selection else {
|
||||
panic!("tracks are selected");
|
||||
};
|
||||
assert_eq!(trk_ids.len(), 5);
|
||||
assert!(trk_ids.contains(&ids[1]) && !trk_ids.contains(&ids[0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nothing_to_extract() {
|
||||
// a single segment
|
||||
let original = file("f", vec![track(Some("t"), &[0.0])], vec![]);
|
||||
let track_id = original.trk[0].id;
|
||||
let segment_id = original.trk[0].trkseg[0].id;
|
||||
let (mut fx, id) = fixture(original);
|
||||
let before = fx.files[&id].clone();
|
||||
assert_eq!(
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
fx.selection = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [track_id].into(),
|
||||
};
|
||||
assert_eq!(
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
for selection in [
|
||||
Selection::Empty,
|
||||
Selection::Waypoints { file_id: id },
|
||||
Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: track_id,
|
||||
trkseg_ids: [segment_id].into(),
|
||||
},
|
||||
] {
|
||||
fx.selection = selection;
|
||||
assert_eq!(
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
assert!(Rc::ptr_eq(&fx.files[&id], &before));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user