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();
|
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
|
/// 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.
|
/// meaning of the times, which are the ones of the track as a whole.
|
||||||
pub fn reverse_segments(
|
pub fn reverse_segments(
|
||||||
@@ -149,6 +164,29 @@ mod tests {
|
|||||||
assert_eq!(times(&s), [None, None, None]);
|
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]
|
#[test]
|
||||||
fn test_segments_keep_the_gaps_between_them() {
|
fn test_segments_keep_the_gaps_between_them() {
|
||||||
// 0..20, then 100..120: a gap of 80
|
// 0..20, then 100..120: a gap of 80
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::{Apply, CommandError, State};
|
|||||||
use crate::{
|
use crate::{
|
||||||
ChangeLoopStart, Clean, Crop, Delete, DeleteAll, DeleteWaypoint, Duplicate, EditWaypoint,
|
ChangeLoopStart, Clean, Crop, Delete, DeleteAll, DeleteWaypoint, Duplicate, EditWaypoint,
|
||||||
Elevation, Extract, InsertAnchor, Load, LoadFiles, Merge, Metadata, Move, MoveWaypoint, New,
|
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
|
/// 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),
|
NewTrack(NewTrack),
|
||||||
NewTrackSegment(NewTrackSegment),
|
NewTrackSegment(NewTrackSegment),
|
||||||
Reverse(Reverse),
|
Reverse(Reverse),
|
||||||
|
RoundTrip(RoundTrip),
|
||||||
Route(Route<'a>),
|
Route(Route<'a>),
|
||||||
NewWaypoint(NewWaypoint<'a>),
|
NewWaypoint(NewWaypoint<'a>),
|
||||||
EditWaypoint(EditWaypoint<'a>),
|
EditWaypoint(EditWaypoint<'a>),
|
||||||
@@ -58,6 +59,7 @@ impl Apply for Command<'_> {
|
|||||||
Command::NewTrack(c) => c.apply(state),
|
Command::NewTrack(c) => c.apply(state),
|
||||||
Command::NewTrackSegment(c) => c.apply(state),
|
Command::NewTrackSegment(c) => c.apply(state),
|
||||||
Command::Reverse(c) => c.apply(state),
|
Command::Reverse(c) => c.apply(state),
|
||||||
|
Command::RoundTrip(c) => c.apply(state),
|
||||||
Command::Route(c) => c.apply(state),
|
Command::Route(c) => c.apply(state),
|
||||||
Command::NewWaypoint(c) => c.apply(state),
|
Command::NewWaypoint(c) => c.apply(state),
|
||||||
Command::EditWaypoint(c) => c.apply(state),
|
Command::EditWaypoint(c) => c.apply(state),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ mod merge;
|
|||||||
mod move_waypoint;
|
mod move_waypoint;
|
||||||
mod new_waypoint;
|
mod new_waypoint;
|
||||||
mod reverse;
|
mod reverse;
|
||||||
|
mod round_trip;
|
||||||
mod route;
|
mod route;
|
||||||
mod split;
|
mod split;
|
||||||
mod time;
|
mod time;
|
||||||
@@ -24,6 +25,7 @@ pub use merge::*;
|
|||||||
pub use move_waypoint::*;
|
pub use move_waypoint::*;
|
||||||
pub use new_waypoint::*;
|
pub use new_waypoint::*;
|
||||||
pub use reverse::*;
|
pub use reverse::*;
|
||||||
|
pub use round_trip::*;
|
||||||
pub use route::*;
|
pub use route::*;
|
||||||
pub use split::*;
|
pub use split::*;
|
||||||
pub use time::*;
|
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))
|
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
|
// Routing
|
||||||
//
|
//
|
||||||
// The indices of the trackpoints and of the anchors are the ones of the routing buffers: they only
|
// The indices of the trackpoints and of the anchors are the ones of the routing buffers: they only
|
||||||
|
|||||||
@@ -180,7 +180,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.createRoundTripForSelection}
|
onclick={() => engine.roundTrip()}
|
||||||
>
|
>
|
||||||
<Repeat class="size-3" />{i18n._('toolbar.routing.round_trip.button')}
|
<Repeat class="size-3" />{i18n._('toolbar.routing.round_trip.button')}
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
|
|||||||
@@ -645,6 +645,15 @@ class Engine {
|
|||||||
return this.run((w) => w.reverse());
|
return this.run((w) => w.reverse());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes each selected segment (or the segments of the selected files and tracks) come back to
|
||||||
|
* where it started: a reversed copy of it is added after its last trackpoint (which is not
|
||||||
|
* repeated), and goes on in time. Resolves to `false` when no segment has the two trackpoints that a way back needs.
|
||||||
|
*/
|
||||||
|
roundTrip() {
|
||||||
|
return this.run((w) => w.round_trip());
|
||||||
|
}
|
||||||
|
|
||||||
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,32 +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 = {
|
||||||
createRoundTripForSelection() {
|
|
||||||
if (!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fileActionManager.applyGlobal((draft) => {
|
|
||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
|
||||||
let file = draft.get(fileId);
|
|
||||||
if (file) {
|
|
||||||
if (level === ListLevel.FILE) {
|
|
||||||
file.roundTrip();
|
|
||||||
} else if (level === ListLevel.TRACK) {
|
|
||||||
for (let item of items) {
|
|
||||||
let trackIndex = (item as ListTrackItem).getTrackIndex();
|
|
||||||
file.roundTripTrack(trackIndex);
|
|
||||||
}
|
|
||||||
} else if (level === ListLevel.SEGMENT) {
|
|
||||||
for (let item of items) {
|
|
||||||
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
|
|
||||||
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
|
|
||||||
file.roundTripTrackSegment(trackIndex, segmentIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
mergeSelection: (mergeTraces: boolean, removeGaps: boolean) => {
|
mergeSelection: (mergeTraces: boolean, removeGaps: boolean) => {
|
||||||
fileActionManager.applyGlobal((draft) => {
|
fileActionManager.applyGlobal((draft) => {
|
||||||
let first = true;
|
let first = true;
|
||||||
|
|||||||
Reference in new issue
Block a user