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
df07892e76
commit
a48608840f
6 files changed
+493
-265
No files matched your search
@@ -1,8 +1,10 @@
|
|||||||
mod anchors;
|
mod anchors;
|
||||||
|
mod reverse;
|
||||||
mod simplify;
|
mod simplify;
|
||||||
mod smooth;
|
mod smooth;
|
||||||
mod timestamps;
|
mod timestamps;
|
||||||
|
|
||||||
pub use anchors::*;
|
pub use anchors::*;
|
||||||
|
pub use reverse::*;
|
||||||
pub use simplify::*;
|
pub use simplify::*;
|
||||||
pub use timestamps::*;
|
pub use timestamps::*;
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
//! Reversing a path: the order of its trackpoints, and of the segments and tracks that hold them.
|
||||||
|
//!
|
||||||
|
//! The timestamps are mirrored, so that the reversed path goes through the same period of time:
|
||||||
|
//! a trackpoint at time `t` of a part that spans `start..end` ends up at `start + end - t`. When
|
||||||
|
//! the part is made of several ones (the segments of a track, the tracks of a file), the gaps
|
||||||
|
//! between them are kept: every part comes after the previous one in the new order, as late as the
|
||||||
|
//! original one came after the part that followed it.
|
||||||
|
|
||||||
|
use crate::{Track, TrackSegment, Trackpoint};
|
||||||
|
|
||||||
|
/// The time of the first trackpoint of a segment.
|
||||||
|
pub fn start_time(segment: &TrackSegment) -> Option<i64> {
|
||||||
|
segment.iter().next().and_then(|trkpt| trkpt.time)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The time of the last trackpoint of a segment.
|
||||||
|
pub fn end_time(segment: &TrackSegment) -> Option<i64> {
|
||||||
|
segment.last_index().and_then(|idx| segment[idx].time)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The time of the first trackpoint of a track.
|
||||||
|
pub fn track_start_time(track: &Track) -> Option<i64> {
|
||||||
|
track.trkseg.first().and_then(start_time)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The time of the last trackpoint of a track.
|
||||||
|
pub fn track_end_time(track: &Track) -> Option<i64> {
|
||||||
|
track.trkseg.last().and_then(end_time)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reverses the trackpoints of a segment.
|
||||||
|
///
|
||||||
|
/// `original_next` is the time at which what came after the segment started, and `new_previous`
|
||||||
|
/// the time at which what comes before it now ends: the segment is mirrored to start there. Both
|
||||||
|
/// are those of the segment itself if the segment is reversed on its own, which are the ones
|
||||||
|
/// used when they are missing (`new_previous` is the start of the segment, and `original_next`
|
||||||
|
/// follows from the duration of the segment). Without timestamps, the trackpoints are only put
|
||||||
|
/// in the opposite order.
|
||||||
|
pub fn reverse_segment(
|
||||||
|
segment: &mut TrackSegment,
|
||||||
|
original_next: Option<i64>,
|
||||||
|
new_previous: Option<i64>,
|
||||||
|
) {
|
||||||
|
let (original_start, original_end) = (start_time(segment), end_time(segment));
|
||||||
|
let new_previous = new_previous.or(original_start);
|
||||||
|
let original_next = original_next.or_else(|| {
|
||||||
|
// the segment lasts as long as before
|
||||||
|
Some(new_previous? + original_end? - original_start?)
|
||||||
|
});
|
||||||
|
|
||||||
|
let len = segment.len();
|
||||||
|
let mut points: Vec<Trackpoint> = segment.iter().cloned().collect();
|
||||||
|
if let (Some(next), Some(previous), Some(end)) = (original_next, new_previous, original_end) {
|
||||||
|
let new_start = previous + next - end;
|
||||||
|
for point in points.iter_mut() {
|
||||||
|
point.time = point.time.map(|time| new_start + (end - time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
points.reverse();
|
||||||
|
segment.splice(0, len, points);
|
||||||
|
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(
|
||||||
|
segments: &mut [TrackSegment],
|
||||||
|
original_next: Option<i64>,
|
||||||
|
new_previous: Option<i64>,
|
||||||
|
) {
|
||||||
|
segments.reverse();
|
||||||
|
let (mut next, mut previous) = (original_next, new_previous);
|
||||||
|
for segment in segments.iter_mut() {
|
||||||
|
// the next one is the one that was before, which started here
|
||||||
|
let original_start = start_time(segment);
|
||||||
|
reverse_segment(segment, next, previous);
|
||||||
|
next = original_start;
|
||||||
|
previous = end_time(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reverses the tracks of a file, and their segments and trackpoints.
|
||||||
|
pub fn reverse_tracks(tracks: &mut [Track], original_next: Option<i64>, new_previous: Option<i64>) {
|
||||||
|
tracks.reverse();
|
||||||
|
let (mut next, mut previous) = (original_next, new_previous);
|
||||||
|
for track in tracks.iter_mut() {
|
||||||
|
let original_start = track_start_time(track);
|
||||||
|
reverse_segments(&mut track.trkseg, next, previous);
|
||||||
|
next = original_start;
|
||||||
|
previous = track_end_time(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::LngLat;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn segment(points: &[(f64, Option<i64>)]) -> TrackSegment {
|
||||||
|
let mut segment = TrackSegment::default();
|
||||||
|
let points = points
|
||||||
|
.iter()
|
||||||
|
.map(|(lng, time)| Trackpoint {
|
||||||
|
coordinates: LngLat {
|
||||||
|
lng: *lng,
|
||||||
|
lat: 0.0,
|
||||||
|
},
|
||||||
|
time: *time,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
segment.splice(0, 0, points);
|
||||||
|
segment
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lngs(segment: &TrackSegment) -> Vec<f64> {
|
||||||
|
segment.iter().map(|p| p.coordinates.lng).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn times(segment: &TrackSegment) -> Vec<Option<i64>> {
|
||||||
|
segment.iter().map(|p| p.time).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_a_segment_is_mirrored_in_its_own_time_span() {
|
||||||
|
let mut s = segment(&[(0.0, Some(0)), (1.0, Some(10)), (2.0, Some(40))]);
|
||||||
|
let (start, end) = (start_time(&s), end_time(&s));
|
||||||
|
reverse_segment(&mut s, end, start);
|
||||||
|
assert_eq!(lngs(&s), [2.0, 1.0, 0.0]);
|
||||||
|
// same span, the durations between points follow the points
|
||||||
|
assert_eq!(times(&s), [Some(0), Some(30), Some(40)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reverse_segment_defaults() {
|
||||||
|
// without the times around, the segment is mirrored where it is
|
||||||
|
let mut s = segment(&[(0.0, Some(100)), (1.0, Some(130))]);
|
||||||
|
reverse_segment(&mut s, None, None);
|
||||||
|
assert_eq!(times(&s), [Some(100), Some(130)]);
|
||||||
|
assert_eq!(lngs(&s), [1.0, 0.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_a_segment_without_times_is_only_reversed() {
|
||||||
|
let mut s = segment(&[(0.0, None), (1.0, None), (2.0, None)]);
|
||||||
|
reverse_segment(&mut s, None, None);
|
||||||
|
assert_eq!(lngs(&s), [2.0, 1.0, 0.0]);
|
||||||
|
assert_eq!(times(&s), [None, None, None]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_segments_keep_the_gaps_between_them() {
|
||||||
|
// 0..20, then 100..120: a gap of 80
|
||||||
|
let mut segments = vec![
|
||||||
|
segment(&[(0.0, Some(0)), (1.0, Some(10)), (2.0, Some(20))]),
|
||||||
|
segment(&[(3.0, Some(100)), (4.0, Some(110)), (5.0, Some(120))]),
|
||||||
|
];
|
||||||
|
let (end, start) = (end_time(&segments[1]), start_time(&segments[0]));
|
||||||
|
reverse_segments(&mut segments, end, start);
|
||||||
|
assert_eq!(lngs(&segments[0]), [5.0, 4.0, 3.0]);
|
||||||
|
assert_eq!(lngs(&segments[1]), [2.0, 1.0, 0.0]);
|
||||||
|
assert_eq!(times(&segments[0]), [Some(0), Some(10), Some(20)]);
|
||||||
|
// the gap is still 80
|
||||||
|
assert_eq!(times(&segments[1]), [Some(100), Some(110), Some(120)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tracks_are_reversed_like_segments() {
|
||||||
|
let track = |segments: Vec<TrackSegment>| Track {
|
||||||
|
trkseg: segments,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut tracks = vec![
|
||||||
|
track(vec![
|
||||||
|
segment(&[(0.0, Some(0)), (1.0, Some(10))]),
|
||||||
|
segment(&[(2.0, Some(50)), (3.0, Some(60))]),
|
||||||
|
]),
|
||||||
|
track(vec![segment(&[(4.0, Some(200)), (5.0, Some(230))])]),
|
||||||
|
];
|
||||||
|
let (end, start) = (track_end_time(&tracks[1]), track_start_time(&tracks[0]));
|
||||||
|
reverse_tracks(&mut tracks, end, start);
|
||||||
|
let all: Vec<f64> = tracks
|
||||||
|
.iter()
|
||||||
|
.flat_map(|t| t.trkseg.iter().flat_map(lngs))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(all, [5.0, 4.0, 3.0, 2.0, 1.0, 0.0]);
|
||||||
|
// 3 segments: durations 10, 10, 30 and gaps 40 and 140 are reversed to 30, 10, 10 and
|
||||||
|
// gaps 140 and 40 (the first part of the time is the longest)
|
||||||
|
assert_eq!(times(&tracks[0].trkseg[0]), [Some(0), Some(30)]);
|
||||||
|
assert_eq!(times(&tracks[1].trkseg[0]), [Some(170), Some(180)]);
|
||||||
|
assert_eq!(times(&tracks[1].trkseg[1]), [Some(220), Some(230)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,294 @@
|
|||||||
use crate::{Apply, CommandError, State};
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Apply, CommandError, File, FileId, Selection, State, TrackId, TrackSegmentId, end_time,
|
||||||
|
reverse_segment, reverse_segments, reverse_tracks, start_time, track_end_time,
|
||||||
|
track_start_time,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Reverses the direction of the selected elements: the whole of each selected file (its tracks,
|
||||||
|
/// the segments of its tracks and their trackpoints are in the opposite order), of each selected
|
||||||
|
/// track (its segments and their trackpoints), or each selected segment on its own (its
|
||||||
|
/// trackpoints). The timestamps are mirrored, see [`reverse_segment`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Reverse;
|
pub struct Reverse;
|
||||||
|
|
||||||
impl Apply for Reverse {
|
impl Apply for Reverse {
|
||||||
fn apply(self, _state: &mut State) -> Result<(), CommandError> {
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||||
// TODO
|
let locations = state
|
||||||
Err(CommandError::NotImplemented("reverse"))
|
.selection
|
||||||
|
.segment_locations(state.files, &state.order.0);
|
||||||
|
let points: usize = locations
|
||||||
|
.iter()
|
||||||
|
.map(|location| {
|
||||||
|
state.files[&location.file_id].trk[location.trk].trkseg[location.seg].len()
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
|
if points <= 1 {
|
||||||
|
return Err(CommandError::NothingToDo);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut files: Vec<(FileId, File)> = vec![];
|
||||||
|
let mut edit = |file_id: &FileId, f: &dyn Fn(&mut File)| {
|
||||||
|
if let Some(file) = state.files.get(file_id) {
|
||||||
|
let mut file = (**file).clone();
|
||||||
|
f(&mut file);
|
||||||
|
files.push((*file_id, file));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match &*state.selection {
|
||||||
|
Selection::File { file_ids } => {
|
||||||
|
for file_id in file_ids {
|
||||||
|
edit(file_id, &|file| {
|
||||||
|
let end = file.trk.last().and_then(track_end_time);
|
||||||
|
let start = file.trk.first().and_then(track_start_time);
|
||||||
|
reverse_tracks(&mut file.trk, end, start);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selection::Track { file_id, trk_ids } => edit(file_id, &|file| {
|
||||||
|
reverse_selected_tracks(file, trk_ids);
|
||||||
|
}),
|
||||||
|
Selection::TrackSegment {
|
||||||
|
file_id,
|
||||||
|
trk_id,
|
||||||
|
trkseg_ids,
|
||||||
|
} => edit(file_id, &|file| {
|
||||||
|
reverse_selected_segments(file, *trk_id, trkseg_ids);
|
||||||
|
}),
|
||||||
|
Selection::Empty | Selection::Waypoints { .. } | Selection::Waypoint { .. } => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (file_id, file) in files {
|
||||||
|
state.files.insert(file_id, Rc::new(file));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reverse_selected_tracks(file: &mut File, ids: &std::collections::HashSet<TrackId>) {
|
||||||
|
for track in file.trk.iter_mut().filter(|track| ids.contains(&track.id)) {
|
||||||
|
let end = track.trkseg.last().and_then(end_time);
|
||||||
|
let start = track.trkseg.first().and_then(start_time);
|
||||||
|
reverse_segments(&mut track.trkseg, end, start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reverse_selected_segments(
|
||||||
|
file: &mut File,
|
||||||
|
track_id: TrackId,
|
||||||
|
ids: &std::collections::HashSet<TrackSegmentId>,
|
||||||
|
) {
|
||||||
|
for track in file.trk.iter_mut().filter(|track| track.id == track_id) {
|
||||||
|
for segment in track
|
||||||
|
.trkseg
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|segment| ids.contains(&segment.id))
|
||||||
|
{
|
||||||
|
let (end, start) = (end_time(segment), start_time(segment));
|
||||||
|
reverse_segment(segment, end, start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::{Load, TrackSegment, 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 all_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_reversing_a_file_reverses_tracks_segments_and_points() {
|
||||||
|
let (mut fx, id) = loaded("data/with_tracks_and_segments.gpx");
|
||||||
|
let before = all_segments(&fx, id);
|
||||||
|
assert!(before.len() >= 3);
|
||||||
|
let (tracks_before, ids_before): (Vec<usize>, Vec<_>) = (
|
||||||
|
fx.files[&id].trk.iter().map(|t| t.trkseg.len()).collect(),
|
||||||
|
fx.files[&id].trk.iter().map(|t| t.id).collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Reverse.apply(&mut fx.state()).unwrap();
|
||||||
|
|
||||||
|
let after = all_segments(&fx, id);
|
||||||
|
assert_eq!(after.len(), before.len());
|
||||||
|
// the whole path is the opposite one
|
||||||
|
let path =
|
||||||
|
|segments: &[TrackSegment]| -> Vec<f64> { segments.iter().flat_map(lngs).collect() };
|
||||||
|
let mut expected = path(&before);
|
||||||
|
expected.reverse();
|
||||||
|
assert_eq!(path(&after), expected);
|
||||||
|
// the tracks are in the opposite order, each with the same number of segments
|
||||||
|
let tracks_after: Vec<usize> = fx.files[&id].trk.iter().map(|t| t.trkseg.len()).collect();
|
||||||
|
let mut reversed = tracks_before.clone();
|
||||||
|
reversed.reverse();
|
||||||
|
assert_eq!(tracks_after, reversed);
|
||||||
|
let ids_after: Vec<_> = fx.files[&id].trk.iter().map(|t| t.id).collect();
|
||||||
|
let mut reversed_ids = ids_before;
|
||||||
|
reversed_ids.reverse();
|
||||||
|
assert_eq!(ids_after, reversed_ids);
|
||||||
|
// the segments changed, and they still end where they should
|
||||||
|
assert!(after.iter().all(|s| s.rev_id != before[0].rev_id));
|
||||||
|
for segment in &after {
|
||||||
|
assert_eq!(segment[0].anchor, Some(0));
|
||||||
|
assert_eq!(segment[segment.len() - 1].anchor, Some(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reversing_a_file_twice_gives_the_same_path_and_times() {
|
||||||
|
let (mut fx, id) = loaded("data/with_tracks_and_segments.gpx");
|
||||||
|
let before = all_segments(&fx, id);
|
||||||
|
let times = |segments: &[TrackSegment]| -> Vec<Option<i64>> {
|
||||||
|
segments
|
||||||
|
.iter()
|
||||||
|
.flat_map(|s| s.iter().map(|p| p.time))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
Reverse.apply(&mut fx.state()).unwrap();
|
||||||
|
Reverse.apply(&mut fx.state()).unwrap();
|
||||||
|
let after = all_segments(&fx, id);
|
||||||
|
for (a, b) in before.iter().zip(&after) {
|
||||||
|
assert_eq!(lngs(a), lngs(b));
|
||||||
|
assert_eq!(a.id, b.id);
|
||||||
|
}
|
||||||
|
assert_eq!(times(&before), times(&after));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reversing_segments_keeps_the_others() {
|
||||||
|
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);
|
||||||
|
let target = &trk.trkseg[1];
|
||||||
|
fx.selection = Selection::TrackSegment {
|
||||||
|
file_id: id,
|
||||||
|
trk_id: trk.id,
|
||||||
|
trkseg_ids: HashSet::from([target.id]),
|
||||||
|
};
|
||||||
|
Reverse.apply(&mut fx.state()).unwrap();
|
||||||
|
|
||||||
|
let after = &fx.files[&id].trk[0];
|
||||||
|
// the segment is reversed in place
|
||||||
|
let mut expected = lngs(target);
|
||||||
|
expected.reverse();
|
||||||
|
assert_eq!(lngs(&after.trkseg[1]), expected);
|
||||||
|
assert_eq!(after.trkseg[1].id, target.id);
|
||||||
|
assert_ne!(after.trkseg[1].rev_id, target.rev_id);
|
||||||
|
// the other ones are the same
|
||||||
|
for (i, segment) in trk.trkseg.iter().enumerate().filter(|(i, _)| *i != 1) {
|
||||||
|
assert_eq!(after.trkseg[i].rev_id, segment.rev_id, "{i}");
|
||||||
|
}
|
||||||
|
assert_eq!(fx.files[&id].trk.len(), file.trk.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reversing_tracks_reverses_their_segments_in_place() {
|
||||||
|
let (mut fx, id) = loaded("data/with_tracks_and_segments.gpx");
|
||||||
|
let file = fx.files[&id].clone();
|
||||||
|
let (index, trk) = file
|
||||||
|
.trk
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, t)| t.trkseg.len() >= 2)
|
||||||
|
.unwrap();
|
||||||
|
fx.selection = Selection::Track {
|
||||||
|
file_id: id,
|
||||||
|
trk_ids: HashSet::from([trk.id]),
|
||||||
|
};
|
||||||
|
Reverse.apply(&mut fx.state()).unwrap();
|
||||||
|
|
||||||
|
let after = &fx.files[&id];
|
||||||
|
// the track stays where it is, its segments are the opposite way
|
||||||
|
assert_eq!(after.trk[index].id, trk.id);
|
||||||
|
let path = |t: &crate::Track| -> Vec<f64> { t.trkseg.iter().flat_map(lngs).collect() };
|
||||||
|
let mut expected = path(trk);
|
||||||
|
expected.reverse();
|
||||||
|
assert_eq!(path(&after.trk[index]), expected);
|
||||||
|
// and the other tracks do not change
|
||||||
|
for (i, other) in file.trk.iter().enumerate().filter(|(i, _)| *i != index) {
|
||||||
|
assert_eq!(path(&after.trk[i]), path(other));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_times_are_mirrored() {
|
||||||
|
let (mut fx, id) = loaded("data/with_time.gpx");
|
||||||
|
let before = all_segments(&fx, id).remove(0);
|
||||||
|
Reverse.apply(&mut fx.state()).unwrap();
|
||||||
|
let after = all_segments(&fx, id).remove(0);
|
||||||
|
let (start, end) = (start_time(&before).unwrap(), end_time(&before).unwrap());
|
||||||
|
// the same period of time, the first point of the new path is the last one
|
||||||
|
assert_eq!(start_time(&after), Some(start));
|
||||||
|
assert_eq!(end_time(&after), Some(end));
|
||||||
|
let n = before.len();
|
||||||
|
for i in 0..n {
|
||||||
|
assert_eq!(
|
||||||
|
after[i].time.unwrap(),
|
||||||
|
start + end - before[n - 1 - i].time.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(after[i].coordinates.lng, before[n - 1 - i].coordinates.lng);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_nothing_to_reverse() {
|
||||||
|
let (mut fx, id) = loaded("data/simple.gpx");
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
fx.selection = Selection::Empty;
|
||||||
|
assert_eq!(
|
||||||
|
Reverse.apply(&mut fx.state()),
|
||||||
|
Err(CommandError::NothingToDo)
|
||||||
|
);
|
||||||
|
fx.selection = Selection::Waypoints { file_id: id };
|
||||||
|
assert_eq!(
|
||||||
|
Reverse.apply(&mut fx.state()),
|
||||||
|
Err(CommandError::NothingToDo)
|
||||||
|
);
|
||||||
|
assert!(Rc::ptr_eq(&fx.files[&id], &before));
|
||||||
|
|
||||||
|
// a single trackpoint has no direction
|
||||||
|
let mut file = (*fx.files[&id]).clone();
|
||||||
|
let segment = &mut file.trk[0].trkseg[0];
|
||||||
|
let len = segment.len();
|
||||||
|
segment.splice(1, len, vec![]);
|
||||||
|
fx.files.insert(id, Rc::new(file));
|
||||||
|
fx.selection = Selection::File {
|
||||||
|
file_ids: HashSet::from([id]),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
Reverse.apply(&mut fx.state()),
|
||||||
|
Err(CommandError::NothingToDo)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
class="gap-1 text-xs px-1.5 py-1.5 h-fit"
|
class="gap-1 text-xs px-1.5 py-1.5 h-fit"
|
||||||
disabled={!validSelection}
|
disabled={!validSelection}
|
||||||
onclick={fileActions.reverseSelection}
|
onclick={() => engine.reverse()}
|
||||||
>
|
>
|
||||||
<ArrowRightLeft class="size-3" />{i18n._('toolbar.routing.reverse.button')}
|
<ArrowRightLeft class="size-3" />{i18n._('toolbar.routing.reverse.button')}
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
|
|||||||
@@ -636,6 +636,15 @@ class Engine {
|
|||||||
return this.run((w) => w.new_track_segment());
|
return this.run((w) => w.new_track_segment());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverses the direction of the selection: the whole of each selected file or track, or each
|
||||||
|
* selected segment on its own. The timestamps are mirrored. Resolves to `false` when there is
|
||||||
|
* nothing to reverse (fewer than two trackpoints).
|
||||||
|
*/
|
||||||
|
reverse() {
|
||||||
|
return this.run((w) => w.reverse());
|
||||||
|
}
|
||||||
|
|
||||||
metadata(name: string, desc: string) {
|
metadata(name: string, desc: string) {
|
||||||
return this.run((w) => w.metadata(name, desc));
|
return this.run((w) => w.metadata(name, desc));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,121 +101,6 @@ export async function loadFiles(list: FileList | File[]): Promise<string[]> {
|
|||||||
|
|
||||||
// Helper functions for file operations
|
// Helper functions for file operations
|
||||||
export const fileActions = {
|
export const fileActions = {
|
||||||
add: (file: GPXFile) => {
|
|
||||||
if (file._data.id === undefined) {
|
|
||||||
file._data.id = getFileIds(1)[0];
|
|
||||||
}
|
|
||||||
return fileActionManager.applyGlobal((draft) => {
|
|
||||||
draft.set(file._data.id, freeze(file));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
addMultiple: (files: GPXFile[]) => {
|
|
||||||
let ids = getFileIds(files.length);
|
|
||||||
fileActionManager.applyGlobal((draft) => {
|
|
||||||
files.forEach((file, index) => {
|
|
||||||
file._data.id = ids[index];
|
|
||||||
draft.set(file._data.id, freeze(file));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return ids;
|
|
||||||
},
|
|
||||||
duplicateSelection: () => {
|
|
||||||
if (get(selection).size === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fileActionManager.applyGlobal((draft) => {
|
|
||||||
let ids = getFileIds(get(settings.fileOrder).length);
|
|
||||||
let index = 0;
|
|
||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
|
||||||
if (level === ListLevel.FILE) {
|
|
||||||
let file = fileStateCollection.getFile(fileId);
|
|
||||||
if (file) {
|
|
||||||
let newFile = file.clone();
|
|
||||||
newFile._data.id = ids[index++];
|
|
||||||
draft.set(newFile._data.id, freeze(newFile));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let file = draft.get(fileId);
|
|
||||||
if (file) {
|
|
||||||
if (level === ListLevel.TRACK) {
|
|
||||||
for (let item of items) {
|
|
||||||
let trackIndex = (item as ListTrackItem).getTrackIndex();
|
|
||||||
file.replaceTracks(trackIndex + 1, trackIndex, [
|
|
||||||
file.trk[trackIndex].clone(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} else if (level === ListLevel.SEGMENT) {
|
|
||||||
for (let item of items) {
|
|
||||||
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
|
|
||||||
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
|
|
||||||
file.replaceTrackSegments(
|
|
||||||
trackIndex,
|
|
||||||
segmentIndex + 1,
|
|
||||||
segmentIndex,
|
|
||||||
[file.trk[trackIndex].trkseg[segmentIndex].clone()]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (level === ListLevel.WAYPOINTS) {
|
|
||||||
file.replaceWaypoints(
|
|
||||||
file.wpt.length,
|
|
||||||
file.wpt.length - 1,
|
|
||||||
file.wpt.map((wpt) => wpt.clone())
|
|
||||||
);
|
|
||||||
} else if (level === ListLevel.WAYPOINT) {
|
|
||||||
for (let item of items) {
|
|
||||||
let waypointIndex = (item as ListWaypointItem).getWaypointIndex();
|
|
||||||
file.replaceWaypoints(waypointIndex + 1, waypointIndex, [
|
|
||||||
file.wpt[waypointIndex].clone(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
addNewTrack: (fileId: string) => {
|
|
||||||
fileActionManager.applyToFile(fileId, (file) =>
|
|
||||||
file.replaceTracks(file.trk.length, file.trk.length, [new Track()])
|
|
||||||
);
|
|
||||||
},
|
|
||||||
addNewSegment: (fileId: string, trackIndex: number) => {
|
|
||||||
fileActionManager.applyToFile(fileId, (file) => {
|
|
||||||
let track = file.trk[trackIndex];
|
|
||||||
track.replaceTrackSegments(track.trkseg.length, track.trkseg.length, [
|
|
||||||
new TrackSegment(),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
reverseSelection: () => {
|
|
||||||
if (
|
|
||||||
!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) ||
|
|
||||||
get(gpxStatistics).global.length <= 1
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fileActionManager.applyGlobal((draft) => {
|
|
||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
|
||||||
let file = draft.get(fileId);
|
|
||||||
if (file) {
|
|
||||||
if (level === ListLevel.FILE) {
|
|
||||||
file.reverse();
|
|
||||||
} else if (level === ListLevel.TRACK) {
|
|
||||||
for (let item of items) {
|
|
||||||
let trackIndex = (item as ListTrackItem).getTrackIndex();
|
|
||||||
file.reverseTrack(trackIndex);
|
|
||||||
}
|
|
||||||
} else if (level === ListLevel.SEGMENT) {
|
|
||||||
for (let item of items) {
|
|
||||||
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
|
|
||||||
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
|
|
||||||
file.reverseTrackSegment(trackIndex, segmentIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
createRoundTripForSelection() {
|
createRoundTripForSelection() {
|
||||||
if (!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) {
|
if (!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) {
|
||||||
return;
|
return;
|
||||||
@@ -601,136 +486,6 @@ export const fileActions = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addOrUpdateWaypoint: (waypoint: WaypointType, item?: ListWaypointItem) => {
|
|
||||||
getElevation([waypoint.attributes]).then((elevation) => {
|
|
||||||
if (item) {
|
|
||||||
fileActionManager.applyToFile(item.getFileId(), (file) => {
|
|
||||||
let wpt = file.wpt[item.getWaypointIndex()];
|
|
||||||
wpt.name = waypoint.name;
|
|
||||||
wpt.desc = waypoint.desc;
|
|
||||||
wpt.cmt = waypoint.cmt;
|
|
||||||
wpt.sym = waypoint.sym;
|
|
||||||
wpt.link = waypoint.link;
|
|
||||||
wpt.setCoordinates(waypoint.attributes);
|
|
||||||
wpt.ele = elevation[0];
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
let fileIds = new Set<string>();
|
|
||||||
get(selection)
|
|
||||||
.getSelected()
|
|
||||||
.forEach((item) => {
|
|
||||||
fileIds.add(item.getFileId());
|
|
||||||
});
|
|
||||||
let wpt = new Waypoint(waypoint);
|
|
||||||
wpt.ele = elevation[0];
|
|
||||||
fileActionManager.applyToFiles(Array.from(fileIds), (file) =>
|
|
||||||
file.replaceWaypoints(file.wpt.length, file.wpt.length, [wpt])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
deleteWaypoint: (fileId: string, waypointIndex: number) => {
|
|
||||||
fileActionManager.applyToFile(fileId, (file) =>
|
|
||||||
file.replaceWaypoints(waypointIndex, waypointIndex, [])
|
|
||||||
);
|
|
||||||
},
|
|
||||||
setStyleToSelection: (style: LineStyleExtension) => {
|
|
||||||
if (get(selection).size === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fileActionManager.applyGlobal((draft) => {
|
|
||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
|
||||||
let file = draft.get(fileId);
|
|
||||||
if (file && (level === ListLevel.FILE || level === ListLevel.TRACK)) {
|
|
||||||
if (level === ListLevel.FILE) {
|
|
||||||
file.setStyle(style);
|
|
||||||
} else if (level === ListLevel.TRACK) {
|
|
||||||
if (items.length === file.trk.length) {
|
|
||||||
file.setStyle(style);
|
|
||||||
} else {
|
|
||||||
for (let item of items) {
|
|
||||||
let trackIndex = (item as ListTrackItem).getTrackIndex();
|
|
||||||
file.trk[trackIndex].setStyle(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
setHiddenToSelection: (hidden: boolean) => {
|
|
||||||
if (get(selection).size === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fileActionManager.applyGlobal((draft) => {
|
|
||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
|
||||||
let file = draft.get(fileId);
|
|
||||||
if (file) {
|
|
||||||
if (level === ListLevel.FILE) {
|
|
||||||
file.setHidden(hidden);
|
|
||||||
} else if (level === ListLevel.TRACK) {
|
|
||||||
let trackIndices = items.map((item) =>
|
|
||||||
(item as ListTrackItem).getTrackIndex()
|
|
||||||
);
|
|
||||||
file.setHidden(hidden, trackIndices);
|
|
||||||
} else if (level === ListLevel.SEGMENT) {
|
|
||||||
let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
|
|
||||||
let segmentIndices = items.map((item) =>
|
|
||||||
(item as ListTrackSegmentItem).getSegmentIndex()
|
|
||||||
);
|
|
||||||
file.setHidden(hidden, trackIndices, segmentIndices);
|
|
||||||
} else if (level === ListLevel.WAYPOINTS) {
|
|
||||||
file.setHiddenWaypoints(hidden);
|
|
||||||
} else if (level === ListLevel.WAYPOINT) {
|
|
||||||
let waypointIndices = items.map((item) =>
|
|
||||||
(item as ListWaypointItem).getWaypointIndex()
|
|
||||||
);
|
|
||||||
file.setHiddenWaypoints(hidden, waypointIndices);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
deleteSelection: () => {
|
|
||||||
if (get(selection).size === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fileActionManager.applyGlobal((draft) => {
|
|
||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
|
||||||
if (level === ListLevel.FILE) {
|
|
||||||
draft.delete(fileId);
|
|
||||||
} else {
|
|
||||||
let file = draft.get(fileId);
|
|
||||||
if (file) {
|
|
||||||
if (level === ListLevel.TRACK) {
|
|
||||||
for (let item of items) {
|
|
||||||
let trackIndex = (item as ListTrackItem).getTrackIndex();
|
|
||||||
file.replaceTracks(trackIndex, trackIndex, []);
|
|
||||||
}
|
|
||||||
} else if (level === ListLevel.SEGMENT) {
|
|
||||||
for (let item of items) {
|
|
||||||
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
|
|
||||||
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
|
|
||||||
file.replaceTrackSegments(
|
|
||||||
trackIndex,
|
|
||||||
segmentIndex,
|
|
||||||
segmentIndex,
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (level === ListLevel.WAYPOINTS) {
|
|
||||||
file.replaceWaypoints(0, file.wpt.length - 1, []);
|
|
||||||
} else if (level === ListLevel.WAYPOINT) {
|
|
||||||
for (let item of items) {
|
|
||||||
let waypointIndex = (item as ListWaypointItem).getWaypointIndex();
|
|
||||||
file.replaceWaypoints(waypointIndex, waypointIndex, []);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
addElevationToSelection: async () => {
|
addElevationToSelection: async () => {
|
||||||
if (get(selection).size === 0) {
|
if (get(selection).size === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -799,19 +554,4 @@ export const fileActions = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteSelectedFiles: () => {
|
|
||||||
if (get(selection).size === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fileActionManager.applyGlobal((draft) => {
|
|
||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
|
||||||
draft.delete(fileId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
deleteAllFiles: () => {
|
|
||||||
fileActionManager.applyGlobal((draft) => {
|
|
||||||
draft.clear();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user