mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-09 23:26:31 +00:00
cleaning
This commit is contained in:
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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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`]).
|
||||||
|
|||||||
@@ -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]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = ×[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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user