mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-09 23:26:31 +00:00
reverse
This commit is contained in:
1 parent
a48608840f
commit
290f4d8c44
8 files changed
+213
-28
No files matched your search
@@ -61,6 +61,21 @@ pub fn reverse_segment(
|
||||
segment.rev_id = Default::default();
|
||||
}
|
||||
|
||||
/// Makes a segment go back where it started: a reversed copy of it is added after its last
|
||||
/// trackpoint, without the first trackpoint of the copy, which is the last one of the segment.
|
||||
///
|
||||
/// The timestamps of the copy go on from the end of the segment: it would start at the time of the
|
||||
/// last trackpoint, and it lasts as long as the segment. Only the end of the segment changes, so
|
||||
/// its other chunks stay shared.
|
||||
pub fn round_trip(segment: &mut TrackSegment) {
|
||||
let len = segment.len();
|
||||
let end = end_time(segment);
|
||||
let mut back = segment.clone();
|
||||
reverse_segment(&mut back, end, end);
|
||||
segment.splice(len, len, back.iter().skip(1).cloned().collect());
|
||||
segment.rev_id = Default::default();
|
||||
}
|
||||
|
||||
/// Reverses the segments of a track, and their trackpoints: see [`reverse_segment`] for the
|
||||
/// meaning of the times, which are the ones of the track as a whole.
|
||||
pub fn reverse_segments(
|
||||
@@ -149,6 +164,29 @@ mod tests {
|
||||
assert_eq!(times(&s), [None, None, None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_goes_back_in_time_from_the_end() {
|
||||
let mut s = segment(&[(0.0, Some(0)), (1.0, Some(10)), (2.0, Some(40))]);
|
||||
let first = s.rev_id;
|
||||
round_trip(&mut s);
|
||||
assert_ne!(s.rev_id, first);
|
||||
// the last trackpoint is the turning point, it is not repeated
|
||||
assert_eq!(lngs(&s), [0.0, 1.0, 2.0, 1.0, 0.0]);
|
||||
// the way back lasts as long as the way there, and starts when it ended
|
||||
assert_eq!(times(&s), [Some(0), Some(10), Some(40), Some(70), Some(80)]);
|
||||
// and it is a segment whose ends are anchors
|
||||
assert_eq!(s[0].anchor, Some(0));
|
||||
assert_eq!(s[4].anchor, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_without_times() {
|
||||
let mut s = segment(&[(0.0, None), (1.0, None)]);
|
||||
round_trip(&mut s);
|
||||
assert_eq!(lngs(&s), [0.0, 1.0, 0.0]);
|
||||
assert_eq!(times(&s), [None; 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_segments_keep_the_gaps_between_them() {
|
||||
// 0..20, then 100..120: a gap of 80
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{Apply, CommandError, State};
|
||||
use crate::{
|
||||
ChangeLoopStart, Clean, Crop, Delete, DeleteAll, DeleteWaypoint, Duplicate, EditWaypoint,
|
||||
Elevation, Extract, InsertAnchor, Load, LoadFiles, Merge, Metadata, Move, MoveWaypoint, New,
|
||||
NewTrack, NewTrackSegment, NewWaypoint, Paste, Reverse, Route, Split, Style, Time,
|
||||
NewTrack, NewTrackSegment, NewWaypoint, Paste, Reverse, RoundTrip, Route, Split, Style, Time,
|
||||
};
|
||||
|
||||
/// A user action that edits the files. Each variant wraps the command's own struct, whose
|
||||
@@ -26,6 +26,7 @@ pub enum Command<'a> {
|
||||
NewTrack(NewTrack),
|
||||
NewTrackSegment(NewTrackSegment),
|
||||
Reverse(Reverse),
|
||||
RoundTrip(RoundTrip),
|
||||
Route(Route<'a>),
|
||||
NewWaypoint(NewWaypoint<'a>),
|
||||
EditWaypoint(EditWaypoint<'a>),
|
||||
@@ -58,6 +59,7 @@ impl Apply for Command<'_> {
|
||||
Command::NewTrack(c) => c.apply(state),
|
||||
Command::NewTrackSegment(c) => c.apply(state),
|
||||
Command::Reverse(c) => c.apply(state),
|
||||
Command::RoundTrip(c) => c.apply(state),
|
||||
Command::Route(c) => c.apply(state),
|
||||
Command::NewWaypoint(c) => c.apply(state),
|
||||
Command::EditWaypoint(c) => c.apply(state),
|
||||
|
||||
@@ -9,6 +9,7 @@ mod merge;
|
||||
mod move_waypoint;
|
||||
mod new_waypoint;
|
||||
mod reverse;
|
||||
mod round_trip;
|
||||
mod route;
|
||||
mod split;
|
||||
mod time;
|
||||
@@ -24,6 +25,7 @@ pub use merge::*;
|
||||
pub use move_waypoint::*;
|
||||
pub use new_waypoint::*;
|
||||
pub use reverse::*;
|
||||
pub use round_trip::*;
|
||||
pub use route::*;
|
||||
pub use split::*;
|
||||
pub use time::*;
|
||||
@@ -0,0 +1,153 @@
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use crate::{Apply, CommandError, File, FileId, State, round_trip};
|
||||
|
||||
/// Makes each selected segment come back to where it started: the segments of the selected files
|
||||
/// and tracks, and the selected segments, get a reversed copy of themselves after their last
|
||||
/// trackpoint (see [`round_trip`]).
|
||||
///
|
||||
/// There is nothing to do for segments with fewer than two trackpoints, which have no way back.
|
||||
#[derive(Debug)]
|
||||
pub struct RoundTrip;
|
||||
|
||||
impl Apply for RoundTrip {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let mut files: HashMap<FileId, File> = HashMap::new();
|
||||
for location in state
|
||||
.selection
|
||||
.segment_locations(state.files, &state.order.0)
|
||||
{
|
||||
let (file_id, trk, seg) = (location.file_id, location.trk, location.seg);
|
||||
if state.files[&file_id].trk[trk].trkseg[seg].len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let file = files
|
||||
.entry(file_id)
|
||||
.or_insert_with(|| (*state.files[&file_id]).clone());
|
||||
round_trip(&mut file.trk[trk].trkseg[seg]);
|
||||
}
|
||||
if files.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
for (file_id, file) in files {
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{Load, Selection, TrackSegment, end_time, engine::command::fixture::Fixture};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn loaded(path: &str) -> (Fixture, FileId) {
|
||||
let mut fx = Fixture::default();
|
||||
let data = std::fs::read(path).unwrap();
|
||||
Load {
|
||||
data: &data,
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let id = fx.order.0[0];
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([id]),
|
||||
};
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
fn segments(fx: &Fixture, id: FileId) -> Vec<TrackSegment> {
|
||||
fx.files[&id]
|
||||
.trk
|
||||
.iter()
|
||||
.flat_map(|track| track.trkseg.iter().cloned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn lngs(segment: &TrackSegment) -> Vec<f64> {
|
||||
segment.iter().map(|p| p.coordinates.lng).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_every_segment_of_a_file_comes_back() {
|
||||
let (mut fx, id) = loaded("data/with_tracks_and_segments.gpx");
|
||||
let before = segments(&fx, id);
|
||||
RoundTrip.apply(&mut fx.state()).unwrap();
|
||||
let after = segments(&fx, id);
|
||||
assert_eq!(after.len(), before.len());
|
||||
for (a, b) in after.iter().zip(&before) {
|
||||
if b.len() < 2 {
|
||||
assert_eq!(a.len(), b.len());
|
||||
assert_eq!(a.rev_id, b.rev_id);
|
||||
continue;
|
||||
}
|
||||
assert_eq!(a.id, b.id);
|
||||
// the last trackpoint is not repeated
|
||||
assert_eq!(a.len(), 2 * b.len() - 1);
|
||||
let mut expected = lngs(b);
|
||||
expected.extend(lngs(b).into_iter().rev().skip(1));
|
||||
assert_eq!(lngs(a), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_only_the_selected_segment_comes_back() {
|
||||
let (mut fx, id) = loaded("data/with_tracks_and_segments.gpx");
|
||||
let file = fx.files[&id].clone();
|
||||
let trk = &file.trk[0];
|
||||
assert!(trk.trkseg.len() >= 2);
|
||||
fx.selection = Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: trk.id,
|
||||
trkseg_ids: HashSet::from([trk.trkseg[1].id]),
|
||||
};
|
||||
RoundTrip.apply(&mut fx.state()).unwrap();
|
||||
let after = &fx.files[&id].trk[0];
|
||||
assert_eq!(after.trkseg[1].len(), 2 * trk.trkseg[1].len() - 1);
|
||||
assert_eq!(after.trkseg[0].rev_id, trk.trkseg[0].rev_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_times_go_on_from_the_end() {
|
||||
let (mut fx, id) = loaded("data/with_time.gpx");
|
||||
let before = segments(&fx, id).remove(0);
|
||||
RoundTrip.apply(&mut fx.state()).unwrap();
|
||||
let after = segments(&fx, id).remove(0);
|
||||
let n = before.len();
|
||||
let end = end_time(&before).unwrap();
|
||||
assert_eq!(after.len(), 2 * n - 1);
|
||||
// the original part is untouched
|
||||
for i in 0..n {
|
||||
assert_eq!(after[i].time, before[i].time);
|
||||
}
|
||||
// the way back goes on from the end, and lasts as long as the way there
|
||||
for i in 1..n {
|
||||
assert_eq!(
|
||||
after[n - 1 + i].time.unwrap(),
|
||||
2 * end - before[n - 1 - i].time.unwrap()
|
||||
);
|
||||
}
|
||||
assert_eq!(end_time(&after), Some(2 * end - before[0].time.unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nothing_to_do_without_a_way_back() {
|
||||
let (mut fx, id) = loaded("data/simple.gpx");
|
||||
let before = fx.files[&id].clone();
|
||||
fx.selection = Selection::Empty;
|
||||
assert_eq!(
|
||||
RoundTrip.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
fx.selection = Selection::Waypoints { file_id: id };
|
||||
assert_eq!(
|
||||
RoundTrip.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
assert!(Rc::ptr_eq(&fx.files[&id], &before));
|
||||
}
|
||||
}
|
||||
@@ -375,6 +375,13 @@ pub fn reverse() -> bool {
|
||||
edit(Command::Reverse(engine::Reverse))
|
||||
}
|
||||
|
||||
/// Adds to each selected segment a reversed copy of itself (without repeating its last point), so that it goes back to where it
|
||||
/// started.
|
||||
#[wasm_bindgen]
|
||||
pub fn round_trip() -> bool {
|
||||
edit(Command::RoundTrip(engine::RoundTrip))
|
||||
}
|
||||
|
||||
// Routing
|
||||
//
|
||||
// The indices of the trackpoints and of the anchors are the ones of the routing buffers: they only
|
||||
|
||||
Reference in new issue
Block a user