mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-11-01 04:07:18 +00:00
* 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>
726 lines
27 KiB
Rust
726 lines
27 KiB
Rust
use std::{collections::HashSet, rc::Rc};
|
|
|
|
use crate::{
|
|
Apply, CommandError, FileId, GlobalStatistics, Route, RouteId, Selection, State, Statistics,
|
|
Track, TrackId, TrackSegment, TrackSegmentId, Waypoint, Waypoints, copy_route, copy_track,
|
|
replace_trackpoints,
|
|
};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum MergeType {
|
|
/// The trackpoints of the elements are connected in a single segment.
|
|
Connect,
|
|
/// The elements are put together, in the first one, which keeps them as they are.
|
|
Group,
|
|
}
|
|
|
|
/// Merges the selected elements into the first of them:
|
|
///
|
|
/// - files: the other files are deleted. The routes and tracks of all of them are in the first
|
|
/// file, as they are with `Group`, or all connected in the first of them (a route or a track,
|
|
/// the routes of a file coming before its tracks) with `Connect`. The waypoints of
|
|
/// 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
|
|
/// connected in one segment with `Connect`. The other tracks are removed.
|
|
/// - routes of a file: the first route gets their trackpoints, all connected with `Connect`, and
|
|
/// the other routes are removed. A route has a single segment, so there is nothing to `Group`.
|
|
/// - segments of a track: they are all connected in a segment at the position of the first one
|
|
/// with `Connect`. With `Group` they are put one after the other, at that position.
|
|
///
|
|
/// The segment made by `Connect` goes on in time, if there are timestamps, at the moving speed
|
|
/// of the selection, see [`replace_trackpoints`], which `remove_gaps` is given.
|
|
///
|
|
/// There is nothing to do when there is nothing to merge: a single file to group, a single track
|
|
/// that has no more than one segment, a single segment...
|
|
#[derive(Debug)]
|
|
pub struct Merge {
|
|
pub type_: MergeType,
|
|
pub remove_gaps: bool,
|
|
/// Only for files: tracks and segments are merged in place anyway.
|
|
pub keep_originals: bool,
|
|
}
|
|
|
|
impl Apply for Merge {
|
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
|
let selection = state.selection.clone();
|
|
match &selection {
|
|
Selection::File { file_ids } => self.merge_files(state, file_ids),
|
|
Selection::Route { file_id, rte_ids } => self.merge_routes(state, *file_id, rte_ids),
|
|
Selection::Track { file_id, trk_ids } => self.merge_tracks(state, *file_id, trk_ids),
|
|
Selection::TrackSegment {
|
|
file_id,
|
|
trk_id,
|
|
trkseg_ids,
|
|
} => self.merge_segments(state, *file_id, *trk_id, trkseg_ids),
|
|
Selection::Empty | Selection::Waypoints { .. } | Selection::Waypoint { .. } => {
|
|
Err(CommandError::NothingToDo)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Merge {
|
|
fn merge_files(
|
|
&self,
|
|
state: &mut State,
|
|
file_ids: &HashSet<FileId>,
|
|
) -> Result<(), CommandError> {
|
|
// in the order of the files
|
|
let ordered: Vec<FileId> = state
|
|
.order
|
|
.0
|
|
.iter()
|
|
.filter(|id| file_ids.contains(id))
|
|
.chain(file_ids.iter().filter(|id| !state.order.0.contains(id)))
|
|
.filter(|id| state.files.contains_key(id))
|
|
.copied()
|
|
.collect();
|
|
let Some(&target_id) = ordered.first() else {
|
|
return Err(CommandError::NothingToDo);
|
|
};
|
|
|
|
let mut routes: Vec<Route> = vec![];
|
|
let mut tracks: Vec<Track> = vec![];
|
|
let mut segments: Vec<TrackSegment> = vec![];
|
|
let mut waypoints: Vec<Waypoint> = vec![];
|
|
for id in &ordered {
|
|
let file = &state.files[id];
|
|
routes.extend(file.rte.iter().cloned());
|
|
tracks.extend(file.trk.iter().cloned());
|
|
segments.extend(file.segments().cloned());
|
|
for waypoint in file.wpt.iter() {
|
|
if !waypoints.iter().any(|kept| kept.same_content(waypoint)) {
|
|
waypoints.push(waypoint.clone());
|
|
}
|
|
}
|
|
}
|
|
// the first route or track, which holds the connected segment
|
|
let first_is_route = ordered.iter().find_map(|id| {
|
|
let file = &state.files[id];
|
|
(!file.has_no_path()).then(|| !file.rte.is_empty())
|
|
});
|
|
|
|
match self.type_ {
|
|
MergeType::Group if ordered.len() == 1 => return Err(CommandError::NothingToDo),
|
|
MergeType::Connect if ordered.len() == 1 && segments.len() <= 1 => {
|
|
return Err(CommandError::NothingToDo);
|
|
}
|
|
MergeType::Connect if !segments.is_empty() => {
|
|
// a single route or track, which is the first one, with the connected segment
|
|
let merged = connect(&segments, segments[0].id, self.remove_gaps);
|
|
if first_is_route == Some(true) {
|
|
let first = Route {
|
|
seg: merged,
|
|
..routes[0].clone()
|
|
};
|
|
(routes, tracks) = (vec![first], vec![]);
|
|
} else {
|
|
let first = Track {
|
|
trkseg: vec![merged],
|
|
..tracks[0].clone()
|
|
};
|
|
(routes, tracks) = (vec![], vec![first]);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
let mut target = (*state.files[&target_id]).clone();
|
|
target.rte = routes;
|
|
target.trk = tracks;
|
|
target.wpt = Waypoints::default();
|
|
target.wpt.insert_at(0, waypoints);
|
|
if self.keep_originals {
|
|
// a new file, whose routes, tracks and segments do not share their ids with the
|
|
// originals
|
|
target.id = Default::default();
|
|
target.rte = target.rte.iter().map(copy_route).collect();
|
|
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..] {
|
|
state.files.remove(id);
|
|
}
|
|
state.order.0.retain(|id| !ordered[1..].contains(id));
|
|
state.files.insert(target_id, Rc::new(target));
|
|
Ok(())
|
|
}
|
|
|
|
fn merge_routes(
|
|
&self,
|
|
state: &mut State,
|
|
file_id: FileId,
|
|
rte_ids: &HashSet<RouteId>,
|
|
) -> Result<(), CommandError> {
|
|
let file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
|
let selected: Vec<usize> = (0..file.rte.len())
|
|
.filter(|i| rte_ids.contains(&file.rte[*i].id))
|
|
.collect();
|
|
if selected.len() < 2 || self.type_ == MergeType::Group {
|
|
return Err(CommandError::NothingToDo);
|
|
}
|
|
let first = selected[0];
|
|
let segments: Vec<TrackSegment> =
|
|
selected.iter().map(|i| file.rte[*i].seg.clone()).collect();
|
|
let mut file = (**file).clone();
|
|
file.rte[first].seg = connect(&segments, segments[0].id, self.remove_gaps);
|
|
let mut index = 0;
|
|
file.rte.retain(|_| {
|
|
index += 1;
|
|
!selected[1..].contains(&(index - 1))
|
|
});
|
|
state.files.insert(file_id, Rc::new(file));
|
|
Ok(())
|
|
}
|
|
|
|
fn merge_tracks(
|
|
&self,
|
|
state: &mut State,
|
|
file_id: FileId,
|
|
trk_ids: &HashSet<TrackId>,
|
|
) -> Result<(), CommandError> {
|
|
let file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
|
let selected: Vec<usize> = (0..file.trk.len())
|
|
.filter(|i| trk_ids.contains(&file.trk[*i].id))
|
|
.collect();
|
|
let Some(&first) = selected.first() else {
|
|
return Err(CommandError::NothingToDo);
|
|
};
|
|
let segments: Vec<TrackSegment> = selected
|
|
.iter()
|
|
.flat_map(|i| file.trk[*i].trkseg.iter().cloned())
|
|
.collect();
|
|
if selected.len() == 1 && (self.type_ == MergeType::Group || segments.len() <= 1) {
|
|
return Err(CommandError::NothingToDo);
|
|
}
|
|
|
|
let merged = match self.type_ {
|
|
MergeType::Group => segments,
|
|
MergeType::Connect if segments.is_empty() => vec![],
|
|
MergeType::Connect => {
|
|
vec![connect(&segments, segments[0].id, self.remove_gaps)]
|
|
}
|
|
};
|
|
let mut file = (**file).clone();
|
|
file.trk[first].trkseg = merged;
|
|
let mut index = 0;
|
|
file.trk.retain(|_| {
|
|
index += 1;
|
|
!selected[1..].contains(&(index - 1))
|
|
});
|
|
state.files.insert(file_id, Rc::new(file));
|
|
Ok(())
|
|
}
|
|
|
|
fn merge_segments(
|
|
&self,
|
|
state: &mut State,
|
|
file_id: FileId,
|
|
trk_id: TrackId,
|
|
ids: &HashSet<TrackSegmentId>,
|
|
) -> Result<(), CommandError> {
|
|
let file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
|
let trk = file
|
|
.trk
|
|
.iter()
|
|
.position(|trk| trk.id == trk_id)
|
|
.ok_or(CommandError::NothingToDo)?;
|
|
let segments = &file.trk[trk].trkseg;
|
|
let selected: Vec<usize> = (0..segments.len())
|
|
.filter(|i| ids.contains(&segments[*i].id))
|
|
.collect();
|
|
if selected.len() < 2 {
|
|
return Err(CommandError::NothingToDo);
|
|
}
|
|
let first = selected[0];
|
|
let chosen: Vec<TrackSegment> = selected.iter().map(|i| segments[*i].clone()).collect();
|
|
let merged = match self.type_ {
|
|
MergeType::Connect => vec![connect(&chosen, chosen[0].id, self.remove_gaps)],
|
|
MergeType::Group => {
|
|
if selected.iter().enumerate().all(|(i, s)| *s == first + i) {
|
|
// already one after the other
|
|
return Err(CommandError::NothingToDo);
|
|
}
|
|
chosen
|
|
}
|
|
};
|
|
|
|
let mut file = (**file).clone();
|
|
let mut remaining: Vec<TrackSegment> = file.trk[trk]
|
|
.trkseg
|
|
.iter()
|
|
.filter(|segment| !ids.contains(&segment.id))
|
|
.cloned()
|
|
.collect();
|
|
// the ones before the first selected one are all kept
|
|
remaining.splice(first..first, merged);
|
|
file.trk[trk].trkseg = remaining;
|
|
state.files.insert(file_id, Rc::new(file));
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// The segments connected in a single one, which has the id `id`: the trackpoints of the second
|
|
/// go after the ones of the first and so on.
|
|
///
|
|
/// If there are timestamps, the new segment goes on in time, see [`replace_trackpoints`]: the
|
|
/// trackpoints that have none get some, at the moving speed of the segments, and the ones that
|
|
/// are too early follow the previous ones. The time of the first trackpoint is the one it has, or
|
|
/// the one it would have if it was as far in time from the first timed one as its distance
|
|
/// says.
|
|
fn connect(segments: &[TrackSegment], id: TrackSegmentId, remove_gaps: bool) -> TrackSegment {
|
|
let mut global = GlobalStatistics::default();
|
|
let mut distance = 0.0;
|
|
// the first time, and the distance from the start at which it is
|
|
let mut first_timed: Option<(i64, f64)> = None;
|
|
for segment in segments {
|
|
let stats = Statistics::compute(segment);
|
|
if first_timed.is_none() {
|
|
first_timed = segment.iter().zip(&stats.local).find_map(|(point, local)| {
|
|
point
|
|
.time
|
|
.map(|time| (time, distance + local.total_distance))
|
|
});
|
|
}
|
|
distance += stats.global.total_distance;
|
|
global.merge(&stats.global);
|
|
}
|
|
let speed = global.moving_speed().filter(|speed| *speed > 0.0);
|
|
let start_time = speed
|
|
.and_then(|speed| first_timed.map(|(time, km)| time - (3_600_000.0 * km / speed) as i64));
|
|
|
|
let mut points = vec![];
|
|
for segment in segments {
|
|
let end = points.len();
|
|
replace_trackpoints(
|
|
&mut points,
|
|
end,
|
|
end,
|
|
segment.iter().cloned().collect(),
|
|
speed,
|
|
start_time,
|
|
remove_gaps,
|
|
);
|
|
}
|
|
let mut merged = TrackSegment::default();
|
|
merged.splice(0, 0, points);
|
|
merged.id = id;
|
|
merged
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::{File, Load, engine::command::fixture::Fixture};
|
|
|
|
use super::*;
|
|
|
|
fn load(fx: &mut Fixture, path: &str) -> FileId {
|
|
let data = std::fs::read(path).unwrap();
|
|
Load {
|
|
data: &data,
|
|
name: "file",
|
|
}
|
|
.apply(&mut fx.state())
|
|
.unwrap();
|
|
*fx.order.0.last().unwrap()
|
|
}
|
|
|
|
fn merge(type_: MergeType) -> Merge {
|
|
Merge {
|
|
type_,
|
|
remove_gaps: false,
|
|
keep_originals: false,
|
|
}
|
|
}
|
|
|
|
fn select_files(fx: &mut Fixture, ids: &[FileId]) {
|
|
fx.selection = Selection::File {
|
|
file_ids: ids.iter().copied().collect(),
|
|
};
|
|
}
|
|
|
|
fn points(file: &File) -> usize {
|
|
file.trk
|
|
.iter()
|
|
.flat_map(|t| &t.trkseg)
|
|
.map(|s| s.len())
|
|
.sum()
|
|
}
|
|
|
|
fn waypoint(name: &str) -> Waypoint {
|
|
Waypoint {
|
|
name: Some(name.into()),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_group_files() {
|
|
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 mut file_a = (*fx.files[&a]).clone();
|
|
file_a
|
|
.wpt
|
|
.insert_at(0, vec![waypoint("x"), waypoint("same")]);
|
|
fx.files.insert(a, Rc::new(file_a));
|
|
let mut file_b = (*fx.files[&b]).clone();
|
|
file_b
|
|
.wpt
|
|
.insert_at(0, vec![waypoint("same"), waypoint("y")]);
|
|
fx.files.insert(b, Rc::new(file_b));
|
|
let (before_a, before_b) = (fx.files[&a].clone(), fx.files[&b].clone());
|
|
select_files(&mut fx, &[b, a]);
|
|
|
|
merge(MergeType::Group).apply(&mut fx.state()).unwrap();
|
|
|
|
// the first file (in the order of the files) gets everything, the other one goes
|
|
assert_eq!(fx.order.0, [a]);
|
|
assert!(!fx.files.contains_key(&b));
|
|
let merged = &fx.files[&a];
|
|
assert_eq!(merged.trk.len(), before_a.trk.len() + before_b.trk.len());
|
|
assert_eq!(merged.trk[0].id, before_a.trk[0].id);
|
|
assert_eq!(
|
|
merged.trk.last().unwrap().id,
|
|
before_b.trk.last().unwrap().id
|
|
);
|
|
assert_eq!(points(merged), points(&before_a) + points(&before_b));
|
|
// the waypoints that are the same are only once
|
|
let names: Vec<_> = merged.wpt.iter().map(|w| w.name.clone().unwrap()).collect();
|
|
assert_eq!(names, ["x", "same", "y"]);
|
|
assert_eq!(merged.info, before_a.info);
|
|
// a single file has nothing to group
|
|
select_files(&mut fx, &[a]);
|
|
assert_eq!(
|
|
merge(MergeType::Group).apply(&mut fx.state()),
|
|
Err(CommandError::NothingToDo)
|
|
);
|
|
}
|
|
|
|
#[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]
|
|
fn test_connect_files() {
|
|
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 (before_a, before_b) = (fx.files[&a].clone(), fx.files[&b].clone());
|
|
select_files(&mut fx, &[a, b]);
|
|
|
|
merge(MergeType::Connect).apply(&mut fx.state()).unwrap();
|
|
|
|
let merged = &fx.files[&a];
|
|
assert_eq!(fx.order.0, [a]);
|
|
// a single track, with a single segment that has all the trackpoints in order
|
|
assert_eq!(merged.trk.len(), 1);
|
|
assert_eq!(merged.trk[0].id, before_a.trk[0].id);
|
|
assert_eq!(merged.trk[0].trkseg.len(), 1);
|
|
let segment = &merged.trk[0].trkseg[0];
|
|
assert_eq!(segment.len(), points(&before_a) + points(&before_b));
|
|
let expected: Vec<f64> = before_a
|
|
.trk
|
|
.iter()
|
|
.chain(&before_b.trk)
|
|
.flat_map(|t| &t.trkseg)
|
|
.flat_map(|s| s.iter().map(|p| p.coordinates.lng))
|
|
.collect();
|
|
let lngs: Vec<f64> = segment.iter().map(|p| p.coordinates.lng).collect();
|
|
assert_eq!(lngs, expected);
|
|
assert_eq!(segment.id, before_a.trk[0].trkseg[0].id);
|
|
assert_eq!(segment[0].anchor, Some(0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_connect_goes_on_in_time() {
|
|
let mut fx = Fixture::default();
|
|
// the same times twice: the second part would be too early
|
|
let a = load(&mut fx, "data/with_time.gpx");
|
|
let b = load(&mut fx, "data/with_time.gpx");
|
|
select_files(&mut fx, &[a, b]);
|
|
merge(MergeType::Connect).apply(&mut fx.state()).unwrap();
|
|
let segment = &fx.files[&a].trk[0].trkseg[0];
|
|
let times: Vec<i64> = segment.iter().map(|p| p.time.unwrap()).collect();
|
|
assert_eq!(times.len(), 160);
|
|
assert!(times.windows(2).all(|w| w[0] <= w[1]), "{times:?}");
|
|
assert!(times[80] > times[79]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_connect_without_gaps_brings_the_parts_closer() {
|
|
let mut fx = Fixture::default();
|
|
let a = load(&mut fx, "data/with_time.gpx");
|
|
// the second file comes a day later
|
|
let b = {
|
|
let data = std::fs::read("data/with_time.gpx").unwrap();
|
|
Load {
|
|
data: &data,
|
|
name: "late",
|
|
}
|
|
.apply(&mut fx.state())
|
|
.unwrap();
|
|
*fx.order.0.last().unwrap()
|
|
};
|
|
let mut file = (*fx.files[&b]).clone();
|
|
for track in file.trk.iter_mut() {
|
|
for segment in track.trkseg.iter_mut() {
|
|
let points: Vec<_> = segment
|
|
.iter()
|
|
.map(|p| {
|
|
let mut p = p.clone();
|
|
p.time = p.time.map(|t| t + 86_400_000);
|
|
p
|
|
})
|
|
.collect();
|
|
let len = segment.len();
|
|
segment.splice(0, len, points);
|
|
}
|
|
}
|
|
fx.files.insert(b, Rc::new(file));
|
|
let gap = |fx: &Fixture, remove_gaps: bool| {
|
|
let mut fx2 = Fixture {
|
|
files: fx.files.clone(),
|
|
order: crate::FileOrder(fx.order.0.clone()),
|
|
selection: Selection::File {
|
|
file_ids: [a, b].into(),
|
|
},
|
|
..Default::default()
|
|
};
|
|
Merge {
|
|
type_: MergeType::Connect,
|
|
remove_gaps,
|
|
keep_originals: false,
|
|
}
|
|
.apply(&mut fx2.state())
|
|
.unwrap();
|
|
let segment = &fx2.files[&a].trk[0].trkseg[0];
|
|
segment[80].time.unwrap() - segment[79].time.unwrap()
|
|
};
|
|
let with_gap = gap(&fx, false);
|
|
let without = gap(&fx, true);
|
|
assert!(with_gap > 80_000_000, "{with_gap}");
|
|
assert!(without < 60_000, "{without}");
|
|
}
|
|
|
|
#[test]
|
|
fn test_merge_tracks() {
|
|
let mut fx = Fixture::default();
|
|
let id = load(&mut fx, "data/with_tracks_and_segments.gpx");
|
|
let file = fx.files[&id].clone();
|
|
assert!(file.trk.len() >= 2);
|
|
let ids: HashSet<TrackId> = file.trk.iter().map(|t| t.id).collect();
|
|
fx.selection = Selection::Track {
|
|
file_id: id,
|
|
trk_ids: ids,
|
|
};
|
|
let segments: usize = file.trk.iter().map(|t| t.trkseg.len()).sum();
|
|
|
|
merge(MergeType::Group).apply(&mut fx.state()).unwrap();
|
|
let merged = &fx.files[&id];
|
|
assert_eq!(merged.trk.len(), 1);
|
|
assert_eq!(merged.trk[0].id, file.trk[0].id);
|
|
assert_eq!(merged.trk[0].trkseg.len(), segments);
|
|
assert_eq!(points(merged), points(&file));
|
|
|
|
// connected: a single segment
|
|
fx.files.insert(id, file.clone());
|
|
merge(MergeType::Connect).apply(&mut fx.state()).unwrap();
|
|
let merged = &fx.files[&id];
|
|
assert_eq!(merged.trk.len(), 1);
|
|
assert_eq!(merged.trk[0].trkseg.len(), 1);
|
|
assert_eq!(merged.trk[0].trkseg[0].len(), points(&file));
|
|
|
|
// one track with a single segment: nothing to do
|
|
fx.selection = Selection::Track {
|
|
file_id: id,
|
|
trk_ids: [file.trk[0].id].into(),
|
|
};
|
|
for type_ in [MergeType::Group, MergeType::Connect] {
|
|
assert_eq!(
|
|
merge(type_).apply(&mut fx.state()),
|
|
Err(CommandError::NothingToDo)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_connect_the_segments_of_a_track() {
|
|
let mut fx = Fixture::default();
|
|
let id = load(&mut fx, "data/with_tracks_and_segments.gpx");
|
|
let file = fx.files[&id].clone();
|
|
let (index, track) = file
|
|
.trk
|
|
.iter()
|
|
.enumerate()
|
|
.find(|(_, t)| t.trkseg.len() >= 2)
|
|
.unwrap();
|
|
fx.selection = Selection::Track {
|
|
file_id: id,
|
|
trk_ids: [track.id].into(),
|
|
};
|
|
merge(MergeType::Connect).apply(&mut fx.state()).unwrap();
|
|
let merged = &fx.files[&id].trk[index];
|
|
assert_eq!(merged.id, track.id);
|
|
assert_eq!(merged.trkseg.len(), 1);
|
|
assert_eq!(
|
|
merged.trkseg[0].len(),
|
|
track.trkseg.iter().map(|s| s.len()).sum::<usize>()
|
|
);
|
|
// the other tracks are untouched
|
|
assert_eq!(fx.files[&id].trk.len(), file.trk.len());
|
|
}
|
|
|
|
#[test]
|
|
fn test_merge_segments() {
|
|
let mut fx = Fixture::default();
|
|
let id = load(&mut fx, "data/with_tracks_and_segments.gpx");
|
|
// a track with three segments
|
|
let mut three = (*fx.files[&id]).clone();
|
|
let base = three.trk[0].trkseg[0].clone();
|
|
three.trk[0].trkseg = (0..3).map(|_| crate::copy_segment(&base)).collect();
|
|
fx.files.insert(id, Rc::new(three));
|
|
let file = fx.files[&id].clone();
|
|
let (index, track) = (0, &file.trk[0]);
|
|
let segments = &track.trkseg;
|
|
// the first and the last ones
|
|
let (first, last) = (&segments[0], &segments[2]);
|
|
fx.selection = Selection::TrackSegment {
|
|
file_id: id,
|
|
trk_id: track.id,
|
|
trkseg_ids: [first.id, last.id].into(),
|
|
};
|
|
|
|
// grouped: next to each other, at the place of the first one
|
|
merge(MergeType::Group).apply(&mut fx.state()).unwrap();
|
|
let ids: Vec<_> = fx.files[&id].trk[index]
|
|
.trkseg
|
|
.iter()
|
|
.map(|s| s.id)
|
|
.collect();
|
|
assert_eq!(ids[..3], [first.id, last.id, segments[1].id]);
|
|
|
|
// connected: one segment, with the id of the first one, at its place
|
|
fx.files.insert(id, file.clone());
|
|
merge(MergeType::Connect).apply(&mut fx.state()).unwrap();
|
|
let merged = &fx.files[&id].trk[index].trkseg;
|
|
assert_eq!(merged.len(), segments.len() - 1);
|
|
assert_eq!(merged[0].id, first.id);
|
|
assert_eq!(merged[0].len(), first.len() + last.len());
|
|
assert_eq!(merged[1].id, segments[1].id);
|
|
|
|
// a single segment, or segments that are already together: nothing to do
|
|
fx.selection = Selection::TrackSegment {
|
|
file_id: id,
|
|
trk_id: track.id,
|
|
trkseg_ids: [first.id].into(),
|
|
};
|
|
assert_eq!(
|
|
merge(MergeType::Connect).apply(&mut fx.state()),
|
|
Err(CommandError::NothingToDo)
|
|
);
|
|
fx.files.insert(id, file.clone());
|
|
fx.selection = Selection::TrackSegment {
|
|
file_id: id,
|
|
trk_id: track.id,
|
|
trkseg_ids: [first.id, segments[1].id].into(),
|
|
};
|
|
assert_eq!(
|
|
merge(MergeType::Group).apply(&mut fx.state()),
|
|
Err(CommandError::NothingToDo)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_nothing_to_merge() {
|
|
let mut fx = Fixture::default();
|
|
let id = load(&mut fx, "data/simple.gpx");
|
|
let before = fx.files[&id].clone();
|
|
for selection in [Selection::Empty, Selection::Waypoints { file_id: id }] {
|
|
fx.selection = selection;
|
|
assert_eq!(
|
|
merge(MergeType::Connect).apply(&mut fx.state()),
|
|
Err(CommandError::NothingToDo)
|
|
);
|
|
}
|
|
assert!(Rc::ptr_eq(&fx.files[&id], &before));
|
|
}
|
|
}
|