This commit is contained in:
vcoppe committed 2026-10-08 19:04:46 +02:00
1 parent 840e943626
commit b085a1d476
26 files changed
+1010 -196

No files matched your search

@@ -248,9 +248,9 @@ mod tests {
// 1 km in 10 s is 360 km/h // 1 km in 10 s is 360 km/h
let mut trkpt = timed(0..4, 0, 10_000); let mut trkpt = timed(0..4, 0, 10_000);
let km = distance(point(0, None).coordinates, point(1, None).coordinates); let km = distance(point(0, None).coordinates, point(1, None).coordinates);
let speed = Some(36.0 * km / 0.01); // the time of 1 point to the next is 100 s let speed = 36.0 * km / 0.01; // the time of 1 point to the next is 100 s
replace_trackpoints(&mut trkpt, 2, 3, untimed(2..4), speed, None, false); replace_trackpoints(&mut trkpt, 2, 3, untimed(2..4), Some(speed), None, false);
let expected = 10_000 + (3_600_000.0 * km / speed.unwrap()) as i64; let expected = 10_000 + (3_600_000.0 * km / speed) as i64;
assert_eq!(trkpt[2].time, Some(expected)); assert_eq!(trkpt[2].time, Some(expected));
assert!(trkpt[2].time > trkpt[1].time); assert!(trkpt[2].time > trkpt[1].time);
// the points after follow the new ones, they were not too early // the points after follow the new ones, they were not too early
+15 -18
View File
@@ -1,7 +1,7 @@
use uuid::Uuid;
use crate::{Trackpoint, Waypoint}; use crate::{Trackpoint, Waypoint};
use super::common::uuid_id;
/// A run of items, shared between the successive versions of a [`crate::Chunked`] list: a chunk /// A run of items, shared between the successive versions of a [`crate::Chunked`] list: a chunk
/// that is not touched by an edit is not copied. /// that is not touched by an edit is not copied.
/// ///
@@ -16,6 +16,9 @@ pub trait Chunk {
/// A chunk with a new identity, holding `items`. /// A chunk with a new identity, holding `items`.
fn new(items: Vec<Self::Item>) -> Self; fn new(items: Vec<Self::Item>) -> Self;
/// Gives the chunk a new identity, after its items were changed in place.
fn renew(&mut self);
fn items(&self) -> &Vec<Self::Item>; fn items(&self) -> &Vec<Self::Item>;
fn items_mut(&mut self) -> &mut Vec<Self::Item>; fn items_mut(&mut self) -> &mut Vec<Self::Item>;
@@ -27,14 +30,7 @@ pub trait Chunk {
const MAX_TRKPT_CHUNK_SIZE: usize = 4096; const MAX_TRKPT_CHUNK_SIZE: usize = 4096;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] uuid_id!(TrackpointChunkId);
pub struct TrackpointChunkId(pub Uuid);
impl Default for TrackpointChunkId {
fn default() -> Self {
Self(Uuid::new_v4())
}
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct TrackpointChunk { pub struct TrackpointChunk {
@@ -53,6 +49,10 @@ impl Chunk for TrackpointChunk {
} }
} }
fn renew(&mut self) {
self.id = Default::default();
}
fn items(&self) -> &Vec<Trackpoint> { fn items(&self) -> &Vec<Trackpoint> {
&self.trkpt &self.trkpt
} }
@@ -62,14 +62,7 @@ impl Chunk for TrackpointChunk {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] uuid_id!(WaypointChunkId);
pub struct WaypointChunkId(pub Uuid);
impl Default for WaypointChunkId {
fn default() -> Self {
Self(Uuid::new_v4())
}
}
const MAX_WPT_CHUNK_SIZE: usize = 128; const MAX_WPT_CHUNK_SIZE: usize = 128;
@@ -90,6 +83,10 @@ impl Chunk for WaypointChunk {
} }
} }
fn renew(&mut self) {
self.id = Default::default();
}
fn items(&self) -> &Vec<Waypoint> { fn items(&self) -> &Vec<Waypoint> {
&self.wpt &self.wpt
} }
+298 -7
View File
@@ -148,7 +148,10 @@ impl<C: Chunk> Chunked<C> {
fn replace_chunk(&mut self, chunk: usize, f: impl FnOnce(&mut Vec<C::Item>)) { fn replace_chunk(&mut self, chunk: usize, f: impl FnOnce(&mut Vec<C::Item>)) {
let len = self.chunks[chunk].items().len(); let len = self.chunks[chunk].items().len();
match Rc::get_mut(&mut self.chunks[chunk]) { match Rc::get_mut(&mut self.chunks[chunk]) {
Some(unique) => f(unique.items_mut()), Some(unique) => {
f(unique.items_mut());
unique.renew();
}
None => { None => {
let mut items = self.chunks[chunk].items().clone(); let mut items = self.chunks[chunk].items().clone();
f(&mut items); f(&mut items);
@@ -291,7 +294,9 @@ impl<'a, C: Chunk> IntoIterator for &'a Chunked<C> {
pub struct ChunkedIter<'a, C: Chunk> { pub struct ChunkedIter<'a, C: Chunk> {
chunked: &'a Chunked<C>, chunked: &'a Chunked<C>,
/// The item returned last.
idx: Option<ChunkIndex>, idx: Option<ChunkIndex>,
exhausted: bool,
} }
impl<C: Chunk> Clone for ChunkedIter<'_, C> { impl<C: Chunk> Clone for ChunkedIter<'_, C> {
@@ -299,6 +304,7 @@ impl<C: Chunk> Clone for ChunkedIter<'_, C> {
Self { Self {
chunked: self.chunked, chunked: self.chunked,
idx: self.idx, idx: self.idx,
exhausted: self.exhausted,
} }
} }
} }
@@ -307,22 +313,307 @@ impl<'a, C: Chunk> ChunkedIter<'a, C> {
pub fn new(chunked: &'a Chunked<C>) -> Self { pub fn new(chunked: &'a Chunked<C>) -> Self {
Self { Self {
chunked, chunked,
idx: Default::default(), idx: None,
exhausted: false,
} }
} }
fn move_to(&mut self, idx: Option<ChunkIndex>) -> Option<&'a C::Item> {
self.exhausted = idx.is_none();
self.idx = idx.or(self.idx);
idx.map(|idx| &self.chunked[idx])
}
} }
impl<'a, C: Chunk> Iterator for ChunkedIter<'a, C> { impl<'a, C: Chunk> Iterator for ChunkedIter<'a, C> {
type Item = &'a C::Item; type Item = &'a C::Item;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
self.idx = self.chunked.next_index(self.idx); if self.exhausted {
self.idx.map(|idx| &self.chunked[idx]) return None;
}
let next = self.chunked.next_index(self.idx);
self.move_to(next)
} }
fn nth(&mut self, n: usize) -> Option<Self::Item> { fn nth(&mut self, n: usize) -> Option<Self::Item> {
let idx = self.idx.map_or_default(|idx| idx.flat) + n; if self.exhausted {
self.idx = self.chunked.locate(idx); return None;
self.idx.map(|idx| &self.chunked[idx]) }
let target = self.idx.map_or(0, |idx| idx.flat + 1).saturating_add(n);
let next = self.chunked.locate(target);
self.move_to(next)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Chunks of at most 3 numbers, with an identity like the real ones.
#[derive(Debug)]
struct Numbers {
id: usize,
items: Vec<u32>,
}
thread_local! {
static NEXT_ID: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
fn next_id() -> usize {
NEXT_ID.with(|id| id.replace(id.get() + 1))
}
impl Chunk for Numbers {
type Item = u32;
const MAX_SIZE: usize = 3;
fn new(items: Vec<u32>) -> Self {
Self {
id: next_id(),
items,
}
}
fn renew(&mut self) {
self.id = next_id();
}
fn items(&self) -> &Vec<u32> {
&self.items
}
fn items_mut(&mut self) -> &mut Vec<u32> {
&mut self.items
}
}
/// The numbers `0..n` in chunks of 3.
fn numbers(n: u32) -> Chunked<Numbers> {
let mut chunked = Chunked::default();
for start in (0..n).step_by(3) {
chunked.push(Numbers::new((start..(start + 3).min(n)).collect()));
}
chunked
}
fn values(chunked: &Chunked<Numbers>) -> Vec<u32> {
chunked.iter().copied().collect()
}
fn ids(chunked: &Chunked<Numbers>) -> Vec<usize> {
chunked.chunks().iter().map(|chunk| chunk.id).collect()
}
fn assert_consistent(chunked: &Chunked<Numbers>) {
let sizes: Vec<usize> = chunked.chunks().iter().map(|c| c.items.len()).collect();
assert!(
sizes
.iter()
.all(|&size| 0 < size && size <= Numbers::MAX_SIZE)
);
assert_eq!(chunked.len(), sizes.iter().sum::<usize>());
for index in 0..chunked.len() {
assert_eq!(chunked.locate(index).unwrap().flat, index);
}
assert!(chunked.locate(chunked.len()).is_none());
}
#[test]
fn test_push_drops_empty_chunks() {
let mut chunked = numbers(4);
chunked.push(Numbers::new(vec![]));
assert_eq!(chunked.chunks().len(), 2);
assert_eq!(chunked.len(), 4);
assert!(Chunked::<Numbers>::default().is_empty());
assert!(!chunked.is_empty());
}
#[test]
fn test_locate_index_and_iterate() {
let chunked = numbers(7);
assert_eq!(
chunked.locate(4),
Some(ChunkIndex {
chunk: 1,
pos: 1,
flat: 4
})
);
assert_eq!(chunked[4], 4);
assert_eq!(chunked[chunked.locate(6).unwrap()], 6);
assert!(chunked.locate(7).is_none());
assert_eq!(values(&chunked), (0..7).collect::<Vec<_>>());
assert_eq!(chunked.iter().count(), 7);
assert_eq!((&chunked).into_iter().last(), Some(&6));
assert_eq!(values(&Chunked::default()), Vec::<u32>::new());
}
#[test]
fn test_first_last_next_and_previous_index() {
let chunked = numbers(7);
let first = chunked.first_index().unwrap();
let last = chunked.last_index().unwrap();
assert_eq!((first.flat, last.flat), (0, 6));
assert_eq!((last.chunk, last.pos), (2, 0));
let mut forward = vec![];
let mut cur = None;
while let Some(next) = chunked.next_index(cur) {
forward.push(chunked[next]);
cur = Some(next);
}
assert_eq!(forward, (0..7).collect::<Vec<_>>());
let mut backward = vec![];
let mut cur = None;
while let Some(prev) = chunked.prev_index(cur) {
backward.push(chunked[prev]);
cur = Some(prev);
}
assert_eq!(backward, (0..7).rev().collect::<Vec<_>>());
let empty = Chunked::<Numbers>::default();
assert!(empty.first_index().is_none() && empty.last_index().is_none());
}
#[test]
#[allow(clippy::iter_nth_zero)]
fn test_iterator_nth_goes_on_from_the_current_item() {
let chunked = numbers(10);
let mut iter = chunked.iter();
assert_eq!(iter.nth(1), Some(&1));
assert_eq!(iter.next(), Some(&2));
// skips 3 and 4
assert_eq!(iter.nth(2), Some(&5));
assert_eq!(iter.nth(0), Some(&6));
assert_eq!(iter.nth(2), Some(&9));
assert_eq!(iter.nth(0), None);
assert_eq!(iter.next(), None, "an exhausted iterator stays exhausted");
assert_eq!(values(&numbers(10)).iter().skip(4).count(), 6);
assert_eq!(
chunked.iter().skip(4).copied().collect::<Vec<_>>(),
(4..10).collect::<Vec<_>>()
);
assert_eq!(
chunked.iter().step_by(4).copied().collect::<Vec<_>>(),
vec![0, 4, 8]
);
}
#[test]
fn test_splice_replaces_inserts_deletes_and_appends() {
let mut chunked = numbers(10);
chunked.splice(2, 5, vec![100, 101]);
assert_eq!(values(&chunked), [0, 1, 100, 101, 5, 6, 7, 8, 9]);
assert_consistent(&chunked);
chunked.splice(0, 0, vec![50]);
assert_eq!(values(&chunked)[..3], [50, 0, 1]);
chunked.splice(chunked.len(), chunked.len(), vec![7, 7, 7, 7]);
assert_eq!(values(&chunked)[chunked.len() - 5..], [9, 7, 7, 7, 7]);
assert_consistent(&chunked);
let len = chunked.len();
chunked.splice(0, len, vec![]);
assert!(chunked.is_empty());
assert_eq!(chunked.len(), 0);
}
#[test]
#[should_panic(expected = "splice range out of bounds")]
fn test_splice_out_of_bounds_panics() {
numbers(4).splice(2, 5, vec![]);
}
#[test]
fn test_splice_keeps_the_chunks_it_does_not_touch() {
let mut chunked = numbers(12);
let before = ids(&chunked);
// inside the second chunk
chunked.splice(4, 5, vec![40]);
let after = ids(&chunked);
assert_eq!(after.len(), before.len());
assert_eq!(
(after[0], after[2], after[3]),
(before[0], before[2], before[3])
);
assert_ne!(after[1], before[1]);
}
#[test]
fn test_edit_replaces_only_the_changed_chunks() {
let mut chunked = numbers(9);
let before = ids(&chunked);
// nothing is changed: nothing is replaced
assert!(!chunked.edit(|n| *n == 4, |_| false));
assert_eq!(ids(&chunked), before);
// the filter picks the chunk, the hook changes it
assert!(chunked.edit(
|n| *n == 4,
|items| {
items[1] = 400;
true
}
));
let after = ids(&chunked);
assert_eq!((after[0], after[2]), (before[0], before[2]));
assert_ne!(after[1], before[1]);
assert_eq!(values(&chunked), [0, 1, 2, 3, 400, 5, 6, 7, 8]);
// a chunk left empty is dropped
assert!(chunked.edit(
|n| *n == 0,
|items| {
items.clear();
true
}
));
assert_eq!(chunked.chunks().len(), 2);
assert_consistent(&chunked);
}
#[test]
fn test_update_copies_the_chunk_so_that_other_versions_do_not_change() {
let mut chunked = numbers(6);
let version = chunked.clone();
let before = ids(&chunked);
chunked.update(4, |n| *n = 40);
assert_eq!(values(&chunked), [0, 1, 2, 3, 40, 5]);
assert_eq!(values(&version), (0..6).collect::<Vec<_>>());
assert_eq!(ids(&version), before);
assert_eq!(ids(&chunked)[0], before[0]);
assert_ne!(ids(&chunked)[1], before[1]);
}
#[test]
fn test_updated_chunks_get_a_new_identity_even_when_nothing_shares_them() {
// what is derived from or kept of a chunk is keyed by its identity
let mut chunked = numbers(6);
let before = ids(&chunked);
chunked.update(1, |n| *n += 1);
assert_ne!(ids(&chunked)[0], before[0]);
assert_eq!(ids(&chunked)[1], before[1]);
let before = ids(&chunked);
chunked.update_all(|_, n| *n += 1);
let after = ids(&chunked);
assert!(before.iter().zip(&after).all(|(a, b)| a != b));
}
#[test]
fn test_update_all_gives_the_index_of_each_item() {
let mut chunked = numbers(8);
let mut seen = vec![];
chunked.update_all(|index, n| {
seen.push(index);
*n = 10 * index as u32;
});
assert_eq!(seen, (0..8).collect::<Vec<_>>());
assert_eq!(values(&chunked), [0, 10, 20, 30, 40, 50, 60, 70]);
} }
} }
+17
View File
@@ -1,3 +1,20 @@
/// Defines an identifier made of a UUID, which is a new one by default.
macro_rules! uuid_id {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct $name(pub uuid::Uuid);
impl Default for $name {
fn default() -> Self {
Self(uuid::Uuid::new_v4())
}
}
};
}
pub(crate) use uuid_id;
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Link { pub struct Link {
pub href: String, pub href: String,
+2 -9
View File
@@ -1,15 +1,8 @@
use uuid::Uuid;
use crate::{Link, Track, Waypoints}; use crate::{Link, Track, Waypoints};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] use super::common::uuid_id;
pub struct FileId(pub Uuid);
impl Default for FileId { uuid_id!(FileId);
fn default() -> Self {
Self(Uuid::new_v4())
}
}
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct File { pub struct File {
+80 -26
View File
@@ -1,26 +1,13 @@
use std::ops::{Deref, DerefMut}; use std::ops::Deref;
use std::rc::Rc;
use uuid::Uuid;
use crate::{ChunkIndex, Chunked, ChunkedIter, Trackpoint, TrackpointChunk, compute_anchors}; use crate::{ChunkIndex, Chunked, ChunkedIter, Trackpoint, TrackpointChunk, compute_anchors};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] use super::common::uuid_id;
pub struct TrackSegmentId(pub Uuid);
impl Default for TrackSegmentId { uuid_id!(TrackSegmentId);
fn default() -> Self {
Self(Uuid::new_v4())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] uuid_id!(TrackSegmentRevisionId);
pub struct TrackSegmentRevisionId(pub Uuid);
impl Default for TrackSegmentRevisionId {
fn default() -> Self {
Self(Uuid::new_v4())
}
}
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct TrackSegment { pub struct TrackSegment {
@@ -42,17 +29,37 @@ impl Deref for TrackSegment {
} }
} }
impl DerefMut for TrackSegment {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.points
}
}
impl TrackSegment { impl TrackSegment {
/// Adds a chunk after the last trackpoint, which is dropped if it is empty.
pub fn push(&mut self, chunk: TrackpointChunk) {
self.points.push(chunk);
self.rev_id = Default::default();
}
/// Adds a chunk that is shared with something else after the last trackpoint.
pub fn push_shared(&mut self, chunk: Rc<TrackpointChunk>) {
self.points.push_shared(chunk);
self.rev_id = Default::default();
}
/// Applies `f` to the trackpoint at `index`, see [`Chunked::update`]. Panics if there is no
/// such trackpoint.
pub fn update(&mut self, index: usize, f: impl FnOnce(&mut Trackpoint)) {
self.points.update(index, f);
self.rev_id = Default::default();
}
/// Applies `f` to every trackpoint, with its index, see [`Chunked::update_all`].
pub fn update_all(&mut self, f: impl FnMut(usize, &mut Trackpoint)) {
self.points.update_all(f);
self.rev_id = Default::default();
}
/// Replaces the points in `start..end` by `points`, see [`Chunked::splice`]. The first and /// Replaces the points in `start..end` by `points`, see [`Chunked::splice`]. The first and
/// last trackpoints are anchors afterwards. /// last trackpoints are anchors afterwards.
pub fn splice(&mut self, start: usize, end: usize, points: Vec<Trackpoint>) { pub fn splice(&mut self, start: usize, end: usize, points: Vec<Trackpoint>) {
self.points.splice(start, end, points); self.points.splice(start, end, points);
self.rev_id = Default::default();
self.ensure_end_anchors(); self.ensure_end_anchors();
} }
@@ -74,7 +81,7 @@ impl TrackSegment {
/// Makes the trackpoint at `index` an anchor shown from the map zoom level `zoom`. Panics if /// Makes the trackpoint at `index` an anchor shown from the map zoom level `zoom`. Panics if
/// there is no such trackpoint. /// there is no such trackpoint.
pub fn set_anchor(&mut self, index: usize, zoom: u8) { pub fn set_anchor(&mut self, index: usize, zoom: u8) {
self.points.update(index, |trkpt| trkpt.anchor = Some(zoom)); self.update(index, |trkpt| trkpt.anchor = Some(zoom));
} }
/// Sets the anchors of the trackpoints from the details of the path of the segment (see /// Sets the anchors of the trackpoints from the details of the path of the segment (see
@@ -82,7 +89,7 @@ impl TrackSegment {
pub fn compute_anchors(&mut self) { pub fn compute_anchors(&mut self) {
let anchors = compute_anchors(self); let anchors = compute_anchors(self);
let mut anchors = anchors.into_iter().peekable(); let mut anchors = anchors.into_iter().peekable();
self.points.update_all(|index, trkpt| { self.update_all(|index, trkpt| {
trkpt.anchor = match anchors.peek() { trkpt.anchor = match anchors.peek() {
Some(&(anchor, zoom)) if anchor == index => { Some(&(anchor, zoom)) if anchor == index => {
anchors.next(); anchors.next();
@@ -98,6 +105,8 @@ impl TrackSegment {
mod tests { mod tests {
use std::rc::Rc; use std::rc::Rc;
use crate::Chunk;
use super::*; use super::*;
fn create_track_segment(nb_chunks: usize) -> TrackSegment { fn create_track_segment(nb_chunks: usize) -> TrackSegment {
@@ -354,4 +363,49 @@ mod tests {
idx = next; idx = next;
} }
} }
fn points_of(n: usize) -> Vec<Trackpoint> {
(0..n).map(|_| Trackpoint::default()).collect()
}
#[test]
fn test_every_change_gives_a_new_revision() {
type Change = (&'static str, fn(&mut TrackSegment));
let changes: [Change; 7] = [
("push", |s| s.push(TrackpointChunk::new(points_of(1)))),
("push_shared", |s| {
s.push_shared(Rc::new(TrackpointChunk::new(points_of(1))))
}),
("update", |s| s.update(0, |p| p.ele += 1.0)),
("update_all", |s| s.update_all(|_, p| p.ele += 1.0)),
("splice", |s| s.splice(1, 2, points_of(2))),
("set_anchor", |s| s.set_anchor(1, 5)),
("compute_anchors", |s| s.compute_anchors()),
];
for (name, change) in changes {
let mut trkseg = create_track_segment(3);
let before = trkseg.rev_id;
change(&mut trkseg);
assert_ne!(trkseg.rev_id, before, "{name}");
}
// reading does not
let trkseg = create_track_segment(3);
let before = trkseg.rev_id;
let _ = trkseg.iter().count();
assert_eq!(trkseg.rev_id, before);
}
#[test]
fn test_ends_are_anchors_after_a_splice() {
let mut trkseg = create_track_segment(3);
trkseg.splice(0, 1, points_of(2));
let last = trkseg.len() - 1;
assert_eq!(trkseg[0].anchor, Some(0));
assert_eq!(trkseg[last].anchor, Some(0));
// nothing to anchor in an empty segment
let mut empty = TrackSegment::default();
empty.ensure_end_anchors();
assert!(empty.is_empty());
}
} }
+2 -9
View File
@@ -1,15 +1,8 @@
use uuid::Uuid;
use crate::{Link, TrackSegment}; use crate::{Link, TrackSegment};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] use super::common::uuid_id;
pub struct TrackId(pub Uuid);
impl Default for TrackId { uuid_id!(TrackId);
fn default() -> Self {
Self(Uuid::new_v4())
}
}
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct Track { pub struct Track {
+2 -9
View File
@@ -1,15 +1,8 @@
use uuid::Uuid;
use crate::{Link, LngLat}; use crate::{Link, LngLat};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] use super::common::uuid_id;
pub struct WaypointId(pub Uuid);
impl Default for WaypointId { uuid_id!(WaypointId);
fn default() -> Self {
Self(Uuid::new_v4())
}
}
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct Waypoint { pub struct Waypoint {
+2 -9
View File
@@ -1,17 +1,10 @@
use std::ops::Deref; use std::ops::Deref;
use uuid::Uuid;
use crate::{Chunked, Waypoint, WaypointChunk, WaypointId}; use crate::{Chunked, Waypoint, WaypointChunk, WaypointId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] use super::common::uuid_id;
pub struct FileWaypointsRevisionId(pub Uuid);
impl Default for FileWaypointsRevisionId { uuid_id!(FileWaypointsRevisionId);
fn default() -> Self {
Self(Uuid::new_v4())
}
}
/// The waypoints of a file, in chunks that are shared between the versions of the file (see /// The waypoints of a file, in chunks that are shared between the versions of the file (see
/// [`Chunked`]). /// [`Chunked`]).
+4 -4
View File
@@ -198,25 +198,25 @@ fn apply_text(
Some(GPXElement::Surface) => { Some(GPXElement::Surface) => {
stack.pop(); stack.pop();
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() { if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
trkpt.surface = categories.surface.code(&e); trkpt.surface = categories.surface.code(e);
} }
} }
Some(GPXElement::Highway) => { Some(GPXElement::Highway) => {
stack.pop(); stack.pop();
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() { if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
trkpt.highway = categories.highway.code(&e); trkpt.highway = categories.highway.code(e);
} }
} }
Some(GPXElement::SacScale) => { Some(GPXElement::SacScale) => {
stack.pop(); stack.pop();
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() { if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
trkpt.sac_scale = categories.sac_scale.code(&e); trkpt.sac_scale = categories.sac_scale.code(e);
} }
} }
Some(GPXElement::MtbScale) => { Some(GPXElement::MtbScale) => {
stack.pop(); stack.pop();
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() { if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
trkpt.mtb_scale = categories.mtb_scale.code(&e); trkpt.mtb_scale = categories.mtb_scale.code(e);
} }
} }
Some(GPXElement::Symbol) => { Some(GPXElement::Symbol) => {
@@ -12,8 +12,6 @@ pub trait Apply {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommandError { pub enum CommandError {
/// The command is not implemented yet.
NotImplemented(&'static str),
/// The command would not change anything. /// The command would not change anything.
NothingToDo, NothingToDo,
/// The input data could not be parsed. /// The input data could not be parsed.
@@ -23,7 +21,6 @@ pub enum CommandError {
impl fmt::Display for CommandError { impl fmt::Display for CommandError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::NotImplemented(name) => write!(f, "{name} is not implemented yet"),
Self::NothingToDo => write!(f, "nothing to do"), Self::NothingToDo => write!(f, "nothing to do"),
Self::InvalidData(err) => write!(f, "invalid data: {err}"), Self::InvalidData(err) => write!(f, "invalid data: {err}"),
} }
@@ -719,8 +719,6 @@ mod tests {
let a = add(&mut fx, file("a", &[(Some("t"), 2)], 2)); let a = add(&mut fx, file("a", &[(Some("t"), 2)], 2));
let b = add(&mut fx, file("b", &[], 0)); let b = add(&mut fx, file("b", &[], 0));
let t = track_ids(&fx, a); let t = track_ids(&fx, a);
let s = segment_ids(&fx, a, 0);
let w = wpt_ids(&fx, a);
copy(&mut fx, tracks(a, &t), false); copy(&mut fx, tracks(a, &t), false);
fx.selection = files(&[b]); fx.selection = files(&[b]);
@@ -736,7 +734,7 @@ mod tests {
let mut fx = Fixture::default(); let mut fx = Fixture::default();
let a = add(&mut fx, file("a", &[(Some("t"), 2)], 2)); let a = add(&mut fx, file("a", &[(Some("t"), 2)], 2));
let b = add(&mut fx, file("b", &[], 0)); let b = add(&mut fx, file("b", &[], 0));
let (t, s, w) = (track_ids(&fx, a), segment_ids(&fx, a, 0), wpt_ids(&fx, a)); let (t, s) = (track_ids(&fx, a), segment_ids(&fx, a, 0));
copy(&mut fx, segments(a, t[0], &s), false); copy(&mut fx, segments(a, t[0], &s), false);
fx.selection = files(&[b]); fx.selection = files(&[b]);
delete_file(&mut fx, a); delete_file(&mut fx, a);
@@ -148,16 +148,19 @@ pub fn transfer(
.is_some_and(|file| file.trk.iter().any(|trk| trk.id == trk_id)), .is_some_and(|file| file.trk.iter().any(|trk| trk.id == trk_id)),
Destination::Waypoints(file_id, _) => state.files.contains_key(&file_id), Destination::Waypoints(file_id, _) => state.files.contains_key(&file_id),
}; };
let valid = match (content, to) { let valid = matches!(
(content, to),
(ClipboardContent::Files(_), Destination::Files(_)) (ClipboardContent::Files(_), Destination::Files(_))
| (ClipboardContent::Tracks(_), Destination::Files(_) | Destination::Tracks(..)) | (
| ( ClipboardContent::Tracks(_),
ClipboardContent::Segments(_), Destination::Files(_) | Destination::Tracks(..)
Destination::Files(_) | Destination::Tracks(..) | Destination::Segments(..), )
) | (
| (ClipboardContent::Waypoints(_), Destination::Waypoints(..)) => true, ClipboardContent::Segments(_),
_ => false, Destination::Files(_) | Destination::Tracks(..) | Destination::Segments(..),
}; )
| (ClipboardContent::Waypoints(_), Destination::Waypoints(..))
);
let empty = match content { let empty = match content {
ClipboardContent::Files(files) => files.is_empty(), ClipboardContent::Files(files) => files.is_empty(),
ClipboardContent::Tracks(tracks) => tracks.is_empty(), ClipboardContent::Tracks(tracks) => tracks.is_empty(),
@@ -1,9 +1,11 @@
mod copy; mod copy;
mod produce; mod produce;
mod update_segments;
mod update_selected; mod update_selected;
mod update_waypoint; mod update_waypoint;
pub use copy::*; pub use copy::*;
pub use produce::*; pub use produce::*;
pub use update_segments::*;
pub use update_selected::*; pub use update_selected::*;
pub use update_waypoint::*; pub use update_waypoint::*;
@@ -0,0 +1,91 @@
use std::collections::HashMap;
use std::rc::Rc;
use crate::{File, FileId, SegmentLocation, State, TrackSegment};
/// Edits the segments at `locations`, calling `f` with the position of each location in the slice
/// and its segment, in that order.
///
/// Each file concerned is copied once (cheap, trackpoints are shared chunks) and stored back as a
/// new snapshot. The segments take care of their own revision (see [`TrackSegment`]).
pub fn update_segments(
state: &mut State,
locations: &[SegmentLocation],
mut f: impl FnMut(usize, &mut TrackSegment),
) {
let mut files: HashMap<FileId, File> = HashMap::new();
for (i, location) in locations.iter().enumerate() {
let file = files
.entry(location.file_id)
.or_insert_with(|| (*state.files[&location.file_id]).clone());
f(i, &mut file.trk[location.trk].trkseg[location.seg]);
}
for (file_id, file) in files {
state.files.insert(file_id, Rc::new(file));
}
}
#[cfg(test)]
mod tests {
use std::rc::Rc;
use crate::engine::command::fixture::Fixture;
use super::*;
fn fixture() -> (Fixture, FileId) {
let mut fx = Fixture::default();
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
let file = Rc::new(crate::parse(&data, &mut Default::default()).unwrap());
let id = file.id;
fx.files.insert(id, file);
(fx, id)
}
#[test]
fn test_only_the_given_segments_are_edited_in_order() {
let (mut fx, id) = fixture();
let before = fx.files[&id].clone();
let locations = [
SegmentLocation {
file_id: id,
trk: 1,
seg: 0,
},
SegmentLocation {
file_id: id,
trk: 0,
seg: 0,
},
];
let mut calls = vec![];
update_segments(&mut fx.state(), &locations, |i, segment| {
calls.push(i);
segment.update(0, |trkpt| trkpt.ele = 1234.0);
});
assert_eq!(calls, vec![0, 1]);
let after = &fx.files[&id];
assert!(!Rc::ptr_eq(after, &before));
for (trk, seg) in [(1, 0), (0, 0)] {
assert_eq!(after.trk[trk].trkseg[seg][0].ele, 1234.0);
assert_ne!(
after.trk[trk].trkseg[seg].rev_id,
before.trk[trk].trkseg[seg].rev_id
);
}
// the other segments are the same ones
assert_eq!(
after.trk[0].trkseg[1].rev_id,
before.trk[0].trkseg[1].rev_id
);
}
#[test]
fn test_no_location_leaves_the_files_alone() {
let (mut fx, id) = fixture();
let before = fx.files[&id].clone();
update_segments(&mut fx.state(), &[], |_, _| panic!("nothing to edit"));
assert!(Rc::ptr_eq(&fx.files[&id], &before));
}
}
@@ -1,6 +1,4 @@
use std::rc::Rc; use crate::{Apply, CommandError, State, update_segments};
use crate::{Apply, CommandError, File, State};
/// Sets the elevation of the trackpoints of the selected segments (the ones of the statistics of /// Sets the elevation of the trackpoints of the selected segments (the ones of the statistics of
/// the selection, in the same order): `ele` has one elevation per trackpoint. /// the selection, in the same order): `ele` has one elevation per trackpoint.
@@ -31,17 +29,9 @@ impl Apply for Elevation<'_> {
} }
let mut ele = self.ele.iter().copied(); let mut ele = self.ele.iter().copied();
// the segments of a file are consecutive update_segments(state, &locations, |_, segment| {
for group in locations.chunk_by(|a, b| a.file_id == b.file_id) { segment.update_all(|_, pt| pt.ele = ele.next().unwrap_or(pt.ele));
let file_id = group[0].file_id; });
let mut file: File = (*state.files[&file_id]).clone();
for location in group {
let segment = &mut file.trk[location.trk].trkseg[location.seg];
segment.update_all(|_, pt| pt.ele = ele.next().unwrap_or(pt.ele));
segment.rev_id = Default::default();
}
state.files.insert(file_id, Rc::new(file));
}
Ok(()) Ok(())
} }
} }
@@ -418,11 +418,13 @@ mod tests {
} }
fx.files.insert(b, Rc::new(file)); fx.files.insert(b, Rc::new(file));
let gap = |fx: &Fixture, remove_gaps: bool| { let gap = |fx: &Fixture, remove_gaps: bool| {
let mut fx2 = Fixture::default(); let mut fx2 = Fixture {
fx2.files = fx.files.clone(); files: fx.files.clone(),
fx2.order = crate::FileOrder(fx.order.0.clone()); order: crate::FileOrder(fx.order.0.clone()),
fx2.selection = Selection::File { selection: Selection::File {
file_ids: [a, b].into(), file_ids: [a, b].into(),
},
..Default::default()
}; };
Merge { Merge {
type_: MergeType::Connect, type_: MergeType::Connect,
@@ -1,6 +1,4 @@
use std::rc::Rc; use crate::{Apply, CommandError, State, reduce_indices, update_segments};
use crate::{Apply, CommandError, File, State, Trackpoint, reduce_indices};
/// Removes the trackpoints of the selected segments that are less than `tolerance` meters away /// Removes the trackpoints of the selected segments that are less than `tolerance` meters away
/// from the line of their neighbours (Ramer-Douglas-Peucker, see [`reduce_indices`]). The ends of the segments are /// from the line of their neighbours (Ramer-Douglas-Peucker, see [`reduce_indices`]). The ends of the segments are
@@ -22,32 +20,25 @@ impl Apply for Reduce {
let locations = state let locations = state
.selection .selection
.segment_locations(state.files, &state.order.0); .segment_locations(state.files, &state.order.0);
let mut reduced = vec![]; let mut changed = vec![];
let mut kept_points = vec![];
for location in &locations { for location in &locations {
let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg]; let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg];
let kept = reduce_indices(segment, self.tolerance); let kept = reduce_indices(segment, self.tolerance);
if kept.len() == segment.len() { if kept.len() == segment.len() {
continue; continue;
} }
let points: Vec<Trackpoint> = kept.iter().map(|&i| segment[i].clone()).collect(); changed.push(*location);
reduced.push((*location, points)); kept_points.push(kept.iter().map(|&i| segment[i].clone()).collect::<Vec<_>>());
} }
if reduced.is_empty() { if changed.is_empty() {
return Err(CommandError::NothingToDo); return Err(CommandError::NothingToDo);
} }
// the segments of a file are consecutive update_segments(state, &changed, |i, segment| {
for group in reduced.chunk_by(|a, b| a.0.file_id == b.0.file_id) { let len = segment.len();
let file_id = group[0].0.file_id; segment.splice(0, len, std::mem::take(&mut kept_points[i]));
let mut file: File = (*state.files[&file_id]).clone(); });
for (location, points) in group {
let segment = &mut file.trk[location.trk].trkseg[location.seg];
let len = segment.len();
segment.splice(0, len, points.clone());
segment.rev_id = Default::default();
}
state.files.insert(file_id, Rc::new(file));
}
Ok(()) Ok(())
} }
} }
@@ -1,6 +1,4 @@
use std::{collections::HashMap, rc::Rc}; use crate::{Apply, CommandError, State, round_trip, update_segments};
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 /// 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 /// and tracks, and the selected segments, get a reversed copy of themselves after their last
@@ -12,26 +10,16 @@ pub struct RoundTrip;
impl Apply for RoundTrip { impl Apply for RoundTrip {
fn apply(self, state: &mut State) -> Result<(), CommandError> { fn apply(self, state: &mut State) -> Result<(), CommandError> {
let mut files: HashMap<FileId, File> = HashMap::new(); let locations: Vec<_> = state
for location in state
.selection .selection
.segment_locations(state.files, &state.order.0) .segment_locations(state.files, &state.order.0)
{ .into_iter()
let (file_id, trk, seg) = (location.file_id, location.trk, location.seg); .filter(|l| state.files[&l.file_id].trk[l.trk].trkseg[l.seg].len() >= 2)
if state.files[&file_id].trk[trk].trkseg[seg].len() < 2 { .collect();
continue; if locations.is_empty() {
}
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); return Err(CommandError::NothingToDo);
} }
for (file_id, file) in files { update_segments(state, &locations, |_, segment| round_trip(segment));
state.files.insert(file_id, Rc::new(file));
}
Ok(()) Ok(())
} }
} }
@@ -39,8 +27,11 @@ impl Apply for RoundTrip {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashSet; use std::collections::HashSet;
use std::rc::Rc;
use crate::{Load, Selection, TrackSegment, end_time, engine::command::fixture::Fixture}; use crate::{
FileId, Load, Selection, TrackSegment, end_time, engine::command::fixture::Fixture,
};
use super::*; use super::*;
@@ -397,7 +397,7 @@ mod tests {
} }
} }
fn segment<'a>(fx: &'a Fixture, id: FileId, trk: usize, seg: usize) -> &'a TrackSegment { fn segment(fx: &Fixture, id: FileId, trk: usize, seg: usize) -> &TrackSegment {
&fx.files[&id].trk[trk].trkseg[seg] &fx.files[&id].trk[trk].trkseg[seg]
} }
+8 -18
View File
@@ -1,8 +1,7 @@
use std::rc::Rc;
use crate::{ use crate::{
Apply, CommandError, File, SegmentLocation, State, Statistics, TrackSegment, Trackpoint, Apply, CommandError, SegmentLocation, State, Statistics, TrackSegment, Trackpoint,
artificial_weights, shifted_and_compressed, with_artificial_timestamps, with_timestamps, artificial_weights, shifted_and_compressed, update_segments, with_artificial_timestamps,
with_timestamps,
}; };
/// How the timestamps of the selection are set. /// How the timestamps of the selection are set.
@@ -94,7 +93,7 @@ impl Apply for Time {
}) })
.collect(); .collect();
let mut total: f64 = weights.iter().flatten().sum(); let mut total: f64 = weights.iter().flatten().sum();
if !(total > 0.0) { if total.is_nan() || total <= 0.0 {
// no distance to share the time by: every interval takes as long // no distance to share the time by: every interval takes as long
for weights in &mut weights { for weights in &mut weights {
weights.iter_mut().for_each(|w| *w = 1.0); weights.iter_mut().for_each(|w| *w = 1.0);
@@ -122,19 +121,10 @@ impl Apply for Time {
} }
} }
// the segments of a file are consecutive update_segments(state, &locations, |i, segment| {
let mut times = times.into_iter(); let times = &times[i];
for group in locations.chunk_by(|a, b| a.file_id == b.file_id) { segment.update_all(|j, pt| pt.time = times.get(j).copied().flatten());
let file_id = group[0].file_id; });
let mut file: File = (*state.files[&file_id]).clone();
for location in group {
let times = times.next().unwrap_or_default();
let segment = &mut file.trk[location.trk].trkseg[location.seg];
segment.update_all(|i, pt| pt.time = times.get(i).copied().flatten());
segment.rev_id = Default::default();
}
state.files.insert(file_id, Rc::new(file));
}
Ok(()) Ok(())
} }
} }
+127 -2
View File
@@ -446,7 +446,7 @@ mod tests {
} }
#[test] #[test]
fn test_not_implemented_command_is_rejected() { fn test_command_with_nothing_to_do_is_rejected() {
let mut engine = Engine::default(); let mut engine = Engine::default();
new(&mut engine, "file"); new(&mut engine, "file");
assert!(!edit(&mut engine, Command::Reverse(crate::Reverse))); assert!(!edit(&mut engine, Command::Reverse(crate::Reverse)));
@@ -754,7 +754,7 @@ mod tests {
)); ));
assert!(!select_elements(&mut engine, node.clone(), SelectMode::Add)); assert!(!select_elements(&mut engine, node.clone(), SelectMode::Add));
assert_eq!(engine.selection(), &node); assert_eq!(engine.selection(), &node);
assert!(engine.selection_changed() == false); assert!(!engine.selection_changed());
// unknown elements and files are ignored when adding // unknown elements and files are ignored when adding
let unknown = tracks(&[TrackId::default()]); let unknown = tracks(&[TrackId::default()]);
@@ -1429,4 +1429,129 @@ mod tests {
assert!(engine.trackpoint(&file, &seg_id, len).is_none()); assert!(engine.trackpoint(&file, &seg_id, len).is_none());
assert!(engine.trackpoint(&Default::default(), &seg_id, 0).is_none()); assert!(engine.trackpoint(&Default::default(), &seg_id, 0).is_none());
} }
#[test]
fn test_export_writes_the_file_as_it_is() {
let mut engine = Engine::default();
assert!(
engine
.export(&FileId::default(), ExportOptions::ALL)
.is_none()
);
load(&mut engine, "data/with_time.gpx");
let id = engine.order()[0];
let bytes = engine.export(&id, ExportOptions::ALL).unwrap();
let again = crate::parse(&bytes, &mut engine.categories().clone()).unwrap();
let file = engine.stack.current().unwrap()[&id].clone();
assert_eq!(again.trk.len(), file.trk.len());
assert_eq!(again.trk[0].trkseg[0].len(), file.trk[0].trkseg[0].len());
assert!(again.trk[0].trkseg[0][0].time.is_some());
// the options leave data out
let bytes = engine.export(&id, ExportOptions::NONE).unwrap();
let again = crate::parse(&bytes, &mut engine.categories().clone()).unwrap();
assert!(again.trk[0].trkseg[0].iter().all(|p| p.time.is_none()));
// a file that is gone cannot be exported
engine.execute(Action::Edit(Command::DeleteAll(DeleteAll)));
assert!(engine.export(&id, ExportOptions::ALL).is_none());
}
#[test]
fn test_exportable_data_is_the_union_over_the_files() {
let mut engine = Engine::default();
assert_eq!(engine.exportable_data(&[]), ExportOptions::NONE);
load(&mut engine, "data/simple.gpx");
load(&mut engine, "data/with_time.gpx");
let (plain, timed) = (engine.order()[0], engine.order()[1]);
assert!(!engine.exportable_data(&[plain]).time);
assert!(engine.exportable_data(&[timed]).time);
assert!(engine.exportable_data(&[plain, timed]).time);
// unknown files have nothing
assert_eq!(
engine.exportable_data(&[FileId::default()]),
ExportOptions::NONE
);
assert!(!engine.exportable_data(&[plain, FileId::default()]).time);
}
#[test]
fn test_file_statistics_are_the_ones_of_the_whole_file() {
let mut engine = Engine::default();
assert!(engine.file_statistics(&FileId::default()).is_none());
load(&mut engine, "data/simple.gpx");
let id = engine.order()[0];
let stats = engine.file_statistics(&id).unwrap();
assert!(stats.total_distance > 0.0);
let last = engine.statistics().total_distance.last().copied().unwrap();
assert!((stats.total_distance - last).abs() < 1e-9);
assert!(engine.file_statistics(&FileId::default()).is_none());
// statistics of a file do not depend on the selection
engine.execute(Action::Select {
selection: Selection::Empty,
mode: SelectMode::Replace,
});
assert_eq!(
engine.file_statistics(&id).unwrap().total_distance,
stats.total_distance
);
engine.execute(Action::Undo);
assert!(engine.file_statistics(&id).is_none());
}
#[test]
fn test_reduction_distances_follow_the_selection() {
let mut engine = Engine::default();
assert!(engine.reduction_distances().is_empty());
load(&mut engine, "data/simple.gpx");
let n = engine.statistics().total_distance.len();
let distances = engine.reduction_distances();
assert_eq!(distances.len(), n);
assert!(distances.iter().all(|d| !d.is_nan()));
engine.execute(Action::Select {
selection: Selection::Empty,
mode: SelectMode::Replace,
});
assert!(engine.reduction_distances().is_empty());
}
#[test]
fn test_routing_describes_the_anchors_of_the_selection() {
let mut engine = Engine::default();
assert!(engine.routing().anchor_indices.is_empty());
load(&mut engine, "data/simple.gpx");
let n = engine.statistics().total_distance.len() as u32;
let routing = engine.routing();
assert_eq!(routing.segment_starts, vec![0]);
assert_eq!(routing.segment_ids.len(), 1);
// the ends of a segment are always anchors
assert_eq!(routing.anchor_indices.first(), Some(&0));
assert_eq!(routing.anchor_indices.last(), Some(&(n - 1)));
assert_eq!(routing.anchor_indices.len(), routing.anchor_zooms.len());
assert_eq!(routing.anchor_zooms[0], 0);
// the revision follows what the indices refer to
let revision = routing.revision;
engine.execute(Action::Select {
selection: Selection::Empty,
mode: SelectMode::Replace,
});
assert!(engine.routing().anchor_indices.is_empty());
assert_ne!(engine.routing().revision, revision);
// nothing changed for the routing: same revision
let revision = engine.routing().revision;
engine.execute(Action::Select {
selection: Selection::Empty,
mode: SelectMode::Replace,
});
assert_eq!(engine.routing().revision, revision);
}
} }
+4 -8
View File
@@ -244,13 +244,10 @@ impl Selection {
} }
/// Position of the first and last of the `selected` ids among the `all` ids. /// Position of the first and last of the `selected` ids among the `all` ids.
fn selected_range<T: PartialEq>( fn selected_range<T: std::hash::Hash + Eq>(
all: impl Iterator<Item = T>, all: impl Iterator<Item = T>,
selected: &HashSet<T>, selected: &HashSet<T>,
) -> Option<(usize, usize)> ) -> Option<(usize, usize)> {
where
T: std::hash::Hash + Eq,
{
let mut range: Option<(usize, usize)> = None; let mut range: Option<(usize, usize)> = None;
for (i, id) in all.enumerate() { for (i, id) in all.enumerate() {
if selected.contains(&id) { if selected.contains(&id) {
@@ -587,7 +584,7 @@ mod tests {
wpt: vec![Waypoint::default()], wpt: vec![Waypoint::default()],
..Default::default() ..Default::default()
}); });
let tree = Tree { Tree {
file: file.id, file: file.id,
tracks: file.trk.iter().map(|t| t.id).collect(), tracks: file.trk.iter().map(|t| t.id).collect(),
segments: file segments: file
@@ -598,8 +595,7 @@ mod tests {
waypoints: file.wpt.iter().map(|w| w.id).collect(), waypoints: file.wpt.iter().map(|w| w.id).collect(),
order: vec![file.id], order: vec![file.id],
files: StackEntry::from([(file.id, Rc::new(file))]), files: StackEntry::from([(file.id, Rc::new(file))]),
}; }
tree
} }
#[test] #[test]
+156
View File
@@ -128,3 +128,159 @@ pub fn decode_waypoints(id: Uuid, bytes: &[u8]) -> Option<WaypointChunk> {
wpt: decode::<Vec<Waypoint>>(bytes)?, wpt: decode::<Vec<Waypoint>>(bytes)?,
}) })
} }
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::Chunk;
use super::*;
fn trackpoints(n: usize) -> TrackpointChunk {
TrackpointChunk::new(
(0..n)
.map(|i| Trackpoint {
ele: i as f64,
..Default::default()
})
.collect(),
)
}
#[test]
fn test_encode_decode_round_trip() {
let value = vec![(1_u32, "a".to_string()), (2, "b".to_string())];
let bytes = encode(&value);
assert_eq!(bytes[0], VERSION);
assert_eq!(decode::<Vec<(u32, String)>>(&bytes), Some(value));
}
#[test]
fn test_decode_rejects_other_versions_and_garbage() {
let mut bytes = encode(&42_u32);
assert_eq!(decode::<u32>(&bytes), Some(42));
bytes[0] = VERSION + 1;
assert_eq!(decode::<u32>(&bytes), None);
assert_eq!(decode::<u32>(&[]), None);
// the right version, but not what was asked for
assert_eq!(decode::<Vec<String>>(&[VERSION, 0xff, 0xff, 0xff]), None);
assert_eq!(decode::<u32>(&[VERSION]), None);
}
#[test]
fn test_chunks_keep_their_identity_and_items() {
let chunk = trackpoints(3);
let decoded = decode_trackpoints(chunk.id.0, &encode_trackpoints(&chunk)).unwrap();
assert_eq!(decoded.id, chunk.id);
let eles: Vec<f64> = decoded.trkpt.iter().map(|p| p.ele).collect();
assert_eq!(eles, [0.0, 1.0, 2.0]);
assert!(decode_trackpoints(chunk.id.0, &[VERSION, 9, 9]).is_none());
let chunk = WaypointChunk::new(vec![Waypoint {
name: Some("summit".into()),
..Default::default()
}]);
let decoded = decode_waypoints(chunk.id.0, &encode_waypoints(&chunk)).unwrap();
assert_eq!(decoded.id, chunk.id);
assert_eq!(decoded.wpt[0].name.as_deref(), Some("summit"));
assert!(decode_waypoints(chunk.id.0, &[]).is_none());
}
fn file() -> File {
let mut file = File::default();
file.info.name = "file".into();
for name in ["a", "b"] {
let mut track = Track::default();
track.info.name = Some(name.into());
let mut segment = TrackSegment::default();
segment.push(trackpoints(2));
segment.push(trackpoints(1));
track.trkseg.push(segment);
track.trkseg.push(TrackSegment::default());
file.trk.push(track);
}
file.wpt.push(WaypointChunk::new(vec![Waypoint::default()]));
file
}
/// The chunks of a file, by identity.
fn chunks_of(
file: &File,
) -> (
HashMap<TrackpointChunkId, Rc<TrackpointChunk>>,
HashMap<WaypointChunkId, Rc<WaypointChunk>>,
) {
let trackpoints = file
.trk
.iter()
.flat_map(|track| &track.trkseg)
.flat_map(|segment| segment.chunks().iter().map(|c| (c.id, c.clone())))
.collect();
let waypoints = file
.wpt
.chunks()
.iter()
.map(|c| (c.id, c.clone()))
.collect();
(trackpoints, waypoints)
}
#[test]
fn test_file_record_rebuilds_the_file_with_the_same_chunks() {
let file = file();
let (trackpoints, waypoints) = chunks_of(&file);
let record: FileRecord = decode(&encode(&FileRecord::new(&file))).unwrap();
let built = record
.build(file.id, &|id| trackpoints.get(id).cloned(), &|id| {
waypoints.get(id).cloned()
})
.unwrap();
assert_eq!(built.id, file.id);
assert_eq!(built.info, file.info);
assert_eq!(built.trk.len(), 2);
for (built, original) in built.trk.iter().zip(&file.trk) {
assert_eq!(built.id, original.id);
assert_eq!(built.info, original.info);
assert_eq!(built.trkseg.len(), 2);
for (built, original) in built.trkseg.iter().zip(&original.trkseg) {
assert_eq!(built.id, original.id);
assert_eq!(built.len(), original.len());
for (a, b) in built.chunks().iter().zip(original.chunks()) {
// the chunks are the ones that were given, not copies
assert!(Rc::ptr_eq(a, b));
}
}
}
assert_eq!(built.wpt.len(), 1);
assert!(Rc::ptr_eq(&built.wpt.chunks()[0], &file.wpt.chunks()[0]));
}
#[test]
fn test_file_record_of_a_missing_chunk_builds_nothing() {
let file = file();
let (trackpoints, waypoints) = chunks_of(&file);
let record = || FileRecord::new(&file);
assert!(
record()
.build(file.id, &|_| None, &|id| waypoints.get(id).cloned())
.is_none()
);
assert!(
record()
.build(file.id, &|id| trackpoints.get(id).cloned(), &|_| None)
.is_none()
);
}
#[test]
fn test_file_record_without_data_builds_an_empty_file() {
let file = File::default();
let built = FileRecord::new(&file)
.build(file.id, &|_| None, &|_| None)
.unwrap();
assert!(built.trk.is_empty() && built.wpt.is_empty());
}
}
+107
View File
@@ -103,3 +103,110 @@ impl Storage for MemoryStorage {
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use std::future::Future;
use std::task::{Context, Poll, Waker};
use super::*;
fn block_on<F: Future>(future: F) -> F::Output {
let mut future = std::pin::pin!(future);
let mut context = Context::from_waker(Waker::noop());
match future.as_mut().poll(&mut context) {
Poll::Ready(value) => value,
Poll::Pending => panic!("the memory storage does not wait"),
}
}
#[test]
fn test_empty_storage_has_nothing() {
let storage = MemoryStorage::default();
let data = block_on(storage.load()).unwrap();
assert_eq!(data, StoredData::default());
assert_eq!(storage.counts(), (0, 0, 0));
assert!(block_on(storage.load_settings()).unwrap().is_empty());
}
#[test]
fn test_commit_puts_and_deletes() {
let storage = MemoryStorage::default();
let (a, b) = (FileId::default(), FileId::default());
let (t, w) = (Uuid::new_v4(), Uuid::new_v4());
block_on(storage.commit(&Batch {
put_files: vec![(a, vec![1]), (b, vec![2])],
put_trackpoint_chunks: vec![(t, vec![3])],
put_waypoint_chunks: vec![(w, vec![4])],
order: Some(vec![5]),
categories: Some(vec![6]),
..Default::default()
}))
.unwrap();
assert_eq!(storage.counts(), (2, 1, 1));
let mut data = block_on(storage.load()).unwrap();
data.files.sort_by_key(|(_, bytes)| bytes.clone());
assert_eq!(data.files, vec![(a, vec![1]), (b, vec![2])]);
assert_eq!(data.trackpoint_chunks, vec![(t, vec![3])]);
assert_eq!(data.waypoint_chunks, vec![(w, vec![4])]);
assert_eq!(
(data.order, data.categories),
(Some(vec![5]), Some(vec![6]))
);
// a put replaces, a delete removes, what is not mentioned stays
block_on(storage.commit(&Batch {
put_files: vec![(a, vec![10])],
delete_files: vec![b],
delete_trackpoint_chunks: vec![t],
..Default::default()
}))
.unwrap();
assert_eq!(storage.counts(), (1, 0, 1));
let data = block_on(storage.load()).unwrap();
assert_eq!(data.files, vec![(a, vec![10])]);
assert_eq!(
(data.order, data.categories),
(Some(vec![5]), Some(vec![6]))
);
block_on(storage.commit(&Batch {
delete_waypoint_chunks: vec![w],
order: Some(vec![]),
..Default::default()
}))
.unwrap();
assert_eq!(storage.counts(), (1, 0, 0));
assert_eq!(block_on(storage.load()).unwrap().order, Some(vec![]));
}
#[test]
fn test_settings() {
let storage = MemoryStorage::default();
block_on(storage.put_setting("a", "1")).unwrap();
block_on(storage.put_setting("b", "2")).unwrap();
block_on(storage.put_setting("a", "3")).unwrap();
let mut settings = block_on(storage.load_settings()).unwrap();
settings.sort();
assert_eq!(
settings,
vec![("a".into(), "3".into()), ("b".into(), "2".into())]
);
block_on(storage.delete_setting("a")).unwrap();
// deleting what is not there is fine
block_on(storage.delete_setting("missing")).unwrap();
assert_eq!(
block_on(storage.load_settings()).unwrap(),
vec![("b".into(), "2".into())]
);
}
#[test]
fn test_settings_are_not_files() {
let storage = MemoryStorage::default();
block_on(storage.put_setting("a", "1")).unwrap();
assert_eq!(block_on(storage.load()).unwrap(), StoredData::default());
}
}
+44
View File
@@ -83,3 +83,47 @@ pub trait Storage {
async fn delete_setting(&self, key: &str) -> Result<(), StorageError>; async fn delete_setting(&self, key: &str) -> Result<(), StorageError>;
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_batch_is_empty_until_it_holds_something() {
assert!(Batch::default().is_empty());
let holds = [
Batch {
put_files: vec![(FileId::default(), vec![])],
..Default::default()
},
Batch {
delete_files: vec![FileId::default()],
..Default::default()
},
Batch {
delete_trackpoint_chunks: vec![Uuid::new_v4()],
..Default::default()
},
Batch {
delete_waypoint_chunks: vec![Uuid::new_v4()],
..Default::default()
},
Batch {
order: Some(vec![]),
..Default::default()
},
Batch {
categories: Some(vec![]),
..Default::default()
},
];
assert!(holds.iter().all(|batch| !batch.is_empty()));
}
#[test]
fn test_storage_error_is_readable() {
let error = StorageError("disk full".into());
assert_eq!(error.to_string(), "storage error: disk full");
let _: &dyn std::error::Error = &error;
}
}