mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-04 12:58:23 +00:00
splice
This commit is contained in:
@@ -32,12 +32,77 @@ pub struct TrackSegment {
|
||||
|
||||
impl TrackSegment {
|
||||
pub fn push(&mut self, chunk: TrackpointChunk) {
|
||||
self.push_shared(Rc::new(chunk));
|
||||
}
|
||||
|
||||
fn push_shared(&mut self, chunk: Rc<TrackpointChunk>) {
|
||||
if chunk.trkpt.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.cumul_length
|
||||
.push(self.cumul_length.last().copied().unwrap_or_default() + chunk.trkpt.len());
|
||||
self.chunks.push(Rc::new(chunk));
|
||||
self.chunks.push(chunk);
|
||||
}
|
||||
|
||||
/// Replaces the points in `start..end` by `points`. Panics if the range is out of bounds.
|
||||
///
|
||||
/// Chunks that are not concerned are kept as they are (shared), only the chunks around the
|
||||
/// range are copied and refilled.
|
||||
pub fn splice(&mut self, start: usize, end: usize, points: Vec<Trackpoint>) {
|
||||
assert!(
|
||||
start <= end && end <= self.len(),
|
||||
"splice range out of bounds"
|
||||
);
|
||||
let old = std::mem::take(&mut self.chunks);
|
||||
self.cumul_length.clear();
|
||||
|
||||
let mut pending = TrackpointChunk::default();
|
||||
let mut inserted = false;
|
||||
let mut offset = 0;
|
||||
for chunk in old {
|
||||
let (lo, hi) = (offset, offset + chunk.trkpt.len());
|
||||
offset = hi;
|
||||
// a chunk ending at `start` is extended unless it is full, to avoid tiny chunks
|
||||
if hi < start || (hi == start && chunk.is_full()) {
|
||||
self.push_shared(chunk);
|
||||
continue;
|
||||
}
|
||||
if !inserted {
|
||||
self.fill(&mut pending, chunk.trkpt[..start - lo].iter().cloned());
|
||||
self.fill(&mut pending, points.iter().cloned());
|
||||
inserted = true;
|
||||
}
|
||||
if hi <= end {
|
||||
continue;
|
||||
}
|
||||
if lo >= end {
|
||||
self.flush(&mut pending);
|
||||
self.push_shared(chunk);
|
||||
} else {
|
||||
self.fill(&mut pending, chunk.trkpt[end - lo..].iter().cloned());
|
||||
}
|
||||
}
|
||||
if !inserted {
|
||||
self.fill(&mut pending, points);
|
||||
}
|
||||
self.flush(&mut pending);
|
||||
}
|
||||
|
||||
fn fill(
|
||||
&mut self,
|
||||
pending: &mut TrackpointChunk,
|
||||
points: impl IntoIterator<Item = Trackpoint>,
|
||||
) {
|
||||
for trkpt in points {
|
||||
pending.trkpt.push(trkpt);
|
||||
if pending.is_full() {
|
||||
self.flush(pending);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self, pending: &mut TrackpointChunk) {
|
||||
self.push(std::mem::take(pending));
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
@@ -193,6 +258,81 @@ mod tests {
|
||||
trkseg
|
||||
}
|
||||
|
||||
fn eles(trkseg: &TrackSegment) -> Vec<f64> {
|
||||
trkseg.iter().map(|p| p.ele).collect()
|
||||
}
|
||||
|
||||
fn points(eles: &[f64]) -> Vec<Trackpoint> {
|
||||
eles.iter()
|
||||
.map(|&ele| Trackpoint {
|
||||
ele,
|
||||
..Default::default()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_replace_insert_delete_append() {
|
||||
// chunks of 1..=5 points: [0] [1 2] [3 4 5] [6 7 8 9] [10..=14]
|
||||
let mut trkseg = create_track_segment(5);
|
||||
trkseg.splice(4, 8, points(&[-1.0, -2.0]));
|
||||
let mut expected: Vec<f64> = (0..4).map(f64::from).collect();
|
||||
expected.extend([-1.0, -2.0]);
|
||||
expected.extend((8..15).map(f64::from));
|
||||
assert_eq!(eles(&trkseg), expected);
|
||||
assert_eq!(trkseg.len(), expected.len());
|
||||
for i in 0..trkseg.len() {
|
||||
assert_eq!(trkseg[i].ele, expected[i]);
|
||||
}
|
||||
|
||||
trkseg.splice(2, 2, points(&[100.0]));
|
||||
expected.insert(2, 100.0);
|
||||
assert_eq!(eles(&trkseg), expected);
|
||||
|
||||
trkseg.splice(0, 3, vec![]);
|
||||
expected.drain(..3);
|
||||
assert_eq!(eles(&trkseg), expected);
|
||||
|
||||
let len = trkseg.len();
|
||||
trkseg.splice(len, len, points(&[7.0, 8.0]));
|
||||
expected.extend([7.0, 8.0]);
|
||||
assert_eq!(eles(&trkseg), expected);
|
||||
|
||||
let len = trkseg.len();
|
||||
trkseg.splice(0, len, vec![]);
|
||||
assert_eq!(trkseg.len(), 0);
|
||||
assert!(trkseg.first_index().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_keeps_untouched_chunks_shared() {
|
||||
let mut trkseg = create_track_segment(5);
|
||||
let before = trkseg.chunks.clone();
|
||||
// inside the third chunk only
|
||||
trkseg.splice(4, 5, points(&[-1.0]));
|
||||
assert!(Rc::ptr_eq(&trkseg.chunks[0], &before[0]));
|
||||
assert!(Rc::ptr_eq(&trkseg.chunks[1], &before[1]));
|
||||
assert!(Rc::ptr_eq(trkseg.chunks.last().unwrap(), &before[4]));
|
||||
assert!(Rc::ptr_eq(
|
||||
&trkseg.chunks[trkseg.chunks.len() - 2],
|
||||
&before[3]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_append_extends_last_chunk_and_respects_max_size() {
|
||||
let mut trkseg = TrackSegment::default();
|
||||
for i in 0..10_000 {
|
||||
let len = trkseg.len();
|
||||
trkseg.splice(len, len, points(&[i as f64]));
|
||||
}
|
||||
assert_eq!(trkseg.len(), 10_000);
|
||||
assert!(trkseg.chunks.len() <= 3);
|
||||
assert!(trkseg.chunks.iter().all(|c| c.trkpt.len() <= 4096));
|
||||
assert_eq!(trkseg[9_999].ele, 9_999.0);
|
||||
assert_eq!(*trkseg.cumul_length.last().unwrap(), 10_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_len() {
|
||||
let nb_chunks = 10;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::LngLat;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Trackpoint {
|
||||
pub coordinates: LngLat,
|
||||
pub ele: f64,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
use crate::{Apply, CommandError, State};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Apply, CommandError, File, FileId, LngLat, Selection, State, Trackpoint};
|
||||
|
||||
/// Replaces the trackpoints `start..end` of the last track segment of the selection by the
|
||||
/// given ones (a pure insertion when `start == end`, a pure removal when there are no new
|
||||
/// points).
|
||||
///
|
||||
/// The selection can be made of several files, tracks or track segments: the indices are
|
||||
/// meant for its very last segment, in file order, then track order, then segment order.
|
||||
#[derive(Debug)]
|
||||
pub struct SpliceTrackpoints<'a> {
|
||||
pub start: u32,
|
||||
@@ -10,8 +18,288 @@ pub struct SpliceTrackpoints<'a> {
|
||||
}
|
||||
|
||||
impl Apply for SpliceTrackpoints<'_> {
|
||||
fn apply(self, _state: &mut State) -> Result<(), CommandError> {
|
||||
// TODO
|
||||
Err(CommandError::NotImplemented("splice_trackpoints"))
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let (start, end) = (self.start as usize, self.end as usize);
|
||||
if start > end || self.lng.len() != self.lat.len() || self.lat.len() != self.ele.len() {
|
||||
return Err(CommandError::InvalidData(
|
||||
"invalid trackpoints range".into(),
|
||||
));
|
||||
}
|
||||
let (file_id, trk, seg) = last_segment(state).ok_or(CommandError::NothingToDo)?;
|
||||
let mut file = (*state.files[&file_id]).clone();
|
||||
let segment = &mut file.trk[trk].trkseg[seg];
|
||||
if end > segment.len() {
|
||||
return Err(CommandError::InvalidData(
|
||||
"trackpoints range out of bounds".into(),
|
||||
));
|
||||
}
|
||||
if start == end && self.lng.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
let points = (0..self.lng.len())
|
||||
.map(|i| Trackpoint {
|
||||
coordinates: LngLat {
|
||||
lng: self.lng[i],
|
||||
lat: self.lat[i],
|
||||
},
|
||||
ele: self.ele[i],
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
segment.splice(start, end, points);
|
||||
segment.rev_id = Default::default();
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Position (file, track index, segment index) of the last segment of the selection.
|
||||
fn last_segment(state: &State) -> Option<(FileId, usize, usize)> {
|
||||
let last_of_track = |file: &File, trk: usize| file.trk[trk].trkseg.len().checked_sub(1);
|
||||
let last_of_file = |file: &File, filter: &dyn Fn(usize) -> bool| {
|
||||
(0..file.trk.len())
|
||||
.rev()
|
||||
.filter(|&trk| filter(trk))
|
||||
.find_map(|trk| last_of_track(file, trk).map(|seg| (trk, seg)))
|
||||
};
|
||||
match &*state.selection {
|
||||
Selection::File { file_ids } => state
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|id| file_ids.contains(id))
|
||||
.find_map(|id| {
|
||||
let (trk, seg) = last_of_file(state.files.get(id)?, &|_| true)?;
|
||||
Some((*id, trk, seg))
|
||||
}),
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = state.files.get(file_id)?;
|
||||
let (trk, seg) = last_of_file(file, &|trk| trk_ids.contains(&file.trk[trk].id))?;
|
||||
Some((*file_id, trk, seg))
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => {
|
||||
let file = state.files.get(file_id)?;
|
||||
let trk = file.trk.iter().position(|trk| trk.id == *trk_id)?;
|
||||
let seg = file.trk[trk]
|
||||
.trkseg
|
||||
.iter()
|
||||
.rposition(|seg| trkseg_ids.contains(&seg.id))?;
|
||||
Some((*file_id, trk, seg))
|
||||
}
|
||||
Selection::Empty | Selection::Waypoints { .. } | Selection::Waypoint { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{FileId, Load, engine::command::fixture::Fixture};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn loaded() -> (Fixture, FileId) {
|
||||
let mut fx = Fixture::default();
|
||||
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||
Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||
let id = fx.order.0[0];
|
||||
let trk = &fx.files[&id].trk[0];
|
||||
fx.selection = Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: trk.id,
|
||||
trkseg_ids: HashSet::from([trk.trkseg[0].id]),
|
||||
};
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
fn splice<'a>(
|
||||
start: u32,
|
||||
end: u32,
|
||||
lng: &'a [f64],
|
||||
lat: &'a [f64],
|
||||
ele: &'a [f64],
|
||||
) -> SpliceTrackpoints<'a> {
|
||||
SpliceTrackpoints {
|
||||
start,
|
||||
end,
|
||||
lng,
|
||||
lat,
|
||||
ele,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_replaces_points_of_selected_segment_only() {
|
||||
let (mut fx, id) = loaded();
|
||||
let before = fx.files[&id].clone();
|
||||
let len = before.trk[0].trkseg[0].len();
|
||||
assert!(len >= 3);
|
||||
|
||||
splice(1, 3, &[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0], &[7.0, 8.0, 9.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let after = &fx.files[&id];
|
||||
let (old, new) = (&before.trk[0].trkseg[0], &after.trk[0].trkseg[0]);
|
||||
assert_eq!(new.len(), len + 1);
|
||||
assert_eq!(new[0].coordinates.lng, old[0].coordinates.lng);
|
||||
assert_eq!(new[1].coordinates.lng, 1.0);
|
||||
assert_eq!(new[3].coordinates.lat, 6.0);
|
||||
assert_eq!(new[3].ele, 9.0);
|
||||
assert_eq!(new[4].coordinates.lng, old[3].coordinates.lng);
|
||||
assert_ne!(new.rev_id, old.rev_id);
|
||||
assert_eq!(new.id, old.id);
|
||||
// other segments and tracks keep their revision
|
||||
for (b, a) in before.trk.iter().zip(&after.trk) {
|
||||
for (b, a) in b.trkseg.iter().zip(&a.trkseg) {
|
||||
if a.id != new.id {
|
||||
assert_eq!(a.rev_id, b.rev_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_append_and_remove() {
|
||||
let (mut fx, id) = loaded();
|
||||
let len = fx.files[&id].trk[0].trkseg[0].len() as u32;
|
||||
splice(len, len, &[1.0], &[2.0], &[3.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let seg = &fx.files[&id].trk[0].trkseg[0];
|
||||
assert_eq!(seg.len(), len as usize + 1);
|
||||
assert_eq!(seg[len as usize].ele, 3.0);
|
||||
|
||||
splice(0, len + 1, &[], &[], &[])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(fx.files[&id].trk[0].trkseg[0].len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_invalid_arguments_change_nothing() {
|
||||
let (mut fx, id) = loaded();
|
||||
let before = fx.files[&id].clone();
|
||||
let len = before.trk[0].trkseg[0].len() as u32;
|
||||
let results = [
|
||||
splice(2, 1, &[], &[], &[]).apply(&mut fx.state()),
|
||||
splice(0, 0, &[1.0], &[], &[]).apply(&mut fx.state()),
|
||||
splice(0, len + 1, &[], &[], &[]).apply(&mut fx.state()),
|
||||
];
|
||||
assert!(
|
||||
results
|
||||
.iter()
|
||||
.all(|r| matches!(r, Err(CommandError::InvalidData(_))))
|
||||
);
|
||||
assert_eq!(
|
||||
splice(0, 0, &[], &[], &[]).apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
assert!(std::rc::Rc::ptr_eq(&fx.files[&id], &before));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_needs_a_segment_selection() {
|
||||
let (mut fx, _) = loaded();
|
||||
fx.selection = Selection::Empty;
|
||||
assert_eq!(
|
||||
splice(0, 0, &[1.0], &[1.0], &[1.0]).apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_targets_last_segment_of_the_selection() {
|
||||
let (mut fx, id) = loaded();
|
||||
let file = fx.files[&id].clone();
|
||||
assert!(file.trk.len() >= 2);
|
||||
let last_trk = file.trk.len() - 1;
|
||||
let last_seg = file.trk[last_trk].trkseg.len() - 1;
|
||||
|
||||
// whole file selected: last segment of its last track
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([id]),
|
||||
};
|
||||
splice(0, 0, &[1.0], &[2.0], &[3.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let after = &fx.files[&id];
|
||||
for (t, (b, a)) in file.trk.iter().zip(&after.trk).enumerate() {
|
||||
for (s, (b, a)) in b.trkseg.iter().zip(&a.trkseg).enumerate() {
|
||||
let target = (t, s) == (last_trk, last_seg);
|
||||
assert_eq!(a.len(), b.len() + usize::from(target));
|
||||
assert_eq!(a.rev_id != b.rev_id, target);
|
||||
}
|
||||
}
|
||||
|
||||
// several tracks selected: last selected one (in file order, whatever the set order)
|
||||
let ids: Vec<_> = file.trk.iter().map(|t| t.id).collect();
|
||||
fx.selection = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: HashSet::from([ids[1], ids[0]]),
|
||||
};
|
||||
let len = |fx: &Fixture, t: usize| {
|
||||
let trk = &fx.files[&id].trk[t];
|
||||
trk.trkseg.last().unwrap().len()
|
||||
};
|
||||
let (l0, l1) = (len(&fx, 0), len(&fx, 1));
|
||||
splice(0, 0, &[1.0], &[2.0], &[3.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(len(&fx, 0), l0);
|
||||
assert_eq!(len(&fx, 1), l1 + 1);
|
||||
|
||||
// several segments selected
|
||||
let trk = &fx.files[&id].trk[0];
|
||||
let seg_ids: Vec<_> = trk.trkseg.iter().map(|s| s.id).collect();
|
||||
if seg_ids.len() >= 2 {
|
||||
fx.selection = Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: trk.id,
|
||||
trkseg_ids: seg_ids.iter().copied().collect(),
|
||||
};
|
||||
let lens: Vec<_> = trk.trkseg.iter().map(|s| s.len()).collect();
|
||||
splice(0, 0, &[1.0], &[2.0], &[3.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let after: Vec<_> = fx.files[&id].trk[0]
|
||||
.trkseg
|
||||
.iter()
|
||||
.map(|s| s.len())
|
||||
.collect();
|
||||
let n = lens.len();
|
||||
assert_eq!(after[..n - 1], lens[..n - 1]);
|
||||
assert_eq!(after[n - 1], lens[n - 1] + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_on_multiple_files_uses_last_in_file_order() {
|
||||
let (mut fx, first) = loaded();
|
||||
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||
Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||
let second = fx.order.0[1];
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([second, first]),
|
||||
};
|
||||
let total = |fx: &Fixture, id: FileId| -> usize {
|
||||
fx.files[&id]
|
||||
.trk
|
||||
.iter()
|
||||
.flat_map(|t| &t.trkseg)
|
||||
.map(|s| s.len())
|
||||
.sum()
|
||||
};
|
||||
let (a, b) = (total(&fx, first), total(&fx, second));
|
||||
splice(0, 0, &[1.0], &[2.0], &[3.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(total(&fx, first), a);
|
||||
assert_eq!(total(&fx, second), b + 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user