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
+1003
-189
No files matched your search
@@ -248,9 +248,9 @@ mod tests {
|
||||
// 1 km in 10 s is 360 km/h
|
||||
let mut trkpt = timed(0..4, 0, 10_000);
|
||||
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
|
||||
replace_trackpoints(&mut trkpt, 2, 3, untimed(2..4), speed, None, false);
|
||||
let expected = 10_000 + (3_600_000.0 * km / speed.unwrap()) as i64;
|
||||
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), Some(speed), None, false);
|
||||
let expected = 10_000 + (3_600_000.0 * km / speed) as i64;
|
||||
assert_eq!(trkpt[2].time, Some(expected));
|
||||
assert!(trkpt[2].time > trkpt[1].time);
|
||||
// the points after follow the new ones, they were not too early
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
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
|
||||
/// 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`.
|
||||
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_mut(&mut self) -> &mut Vec<Self::Item>;
|
||||
@@ -27,14 +30,7 @@ pub trait Chunk {
|
||||
|
||||
const MAX_TRKPT_CHUNK_SIZE: usize = 4096;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TrackpointChunkId(pub Uuid);
|
||||
|
||||
impl Default for TrackpointChunkId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
uuid_id!(TrackpointChunkId);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TrackpointChunk {
|
||||
@@ -53,6 +49,10 @@ impl Chunk for TrackpointChunk {
|
||||
}
|
||||
}
|
||||
|
||||
fn renew(&mut self) {
|
||||
self.id = Default::default();
|
||||
}
|
||||
|
||||
fn items(&self) -> &Vec<Trackpoint> {
|
||||
&self.trkpt
|
||||
}
|
||||
@@ -62,14 +62,7 @@ impl Chunk for TrackpointChunk {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct WaypointChunkId(pub Uuid);
|
||||
|
||||
impl Default for WaypointChunkId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
uuid_id!(WaypointChunkId);
|
||||
|
||||
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> {
|
||||
&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>)) {
|
||||
let len = self.chunks[chunk].items().len();
|
||||
match Rc::get_mut(&mut self.chunks[chunk]) {
|
||||
Some(unique) => f(unique.items_mut()),
|
||||
Some(unique) => {
|
||||
f(unique.items_mut());
|
||||
unique.renew();
|
||||
}
|
||||
None => {
|
||||
let mut items = self.chunks[chunk].items().clone();
|
||||
f(&mut items);
|
||||
@@ -291,7 +294,9 @@ impl<'a, C: Chunk> IntoIterator for &'a Chunked<C> {
|
||||
|
||||
pub struct ChunkedIter<'a, C: Chunk> {
|
||||
chunked: &'a Chunked<C>,
|
||||
/// The item returned last.
|
||||
idx: Option<ChunkIndex>,
|
||||
exhausted: bool,
|
||||
}
|
||||
|
||||
impl<C: Chunk> Clone for ChunkedIter<'_, C> {
|
||||
@@ -299,6 +304,7 @@ impl<C: Chunk> Clone for ChunkedIter<'_, C> {
|
||||
Self {
|
||||
chunked: self.chunked,
|
||||
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 {
|
||||
Self {
|
||||
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> {
|
||||
type Item = &'a C::Item;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.idx = self.chunked.next_index(self.idx);
|
||||
self.idx.map(|idx| &self.chunked[idx])
|
||||
if self.exhausted {
|
||||
return None;
|
||||
}
|
||||
let next = self.chunked.next_index(self.idx);
|
||||
self.move_to(next)
|
||||
}
|
||||
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
let idx = self.idx.map_or_default(|idx| idx.flat) + n;
|
||||
self.idx = self.chunked.locate(idx);
|
||||
self.idx.map(|idx| &self.chunked[idx])
|
||||
if self.exhausted {
|
||||
return None;
|
||||
}
|
||||
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)]
|
||||
pub struct Link {
|
||||
pub href: String,
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{Link, Track, Waypoints};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FileId(pub Uuid);
|
||||
use super::common::uuid_id;
|
||||
|
||||
impl Default for FileId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
uuid_id!(FileId);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct File {
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use uuid::Uuid;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{ChunkIndex, Chunked, ChunkedIter, Trackpoint, TrackpointChunk, compute_anchors};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TrackSegmentId(pub Uuid);
|
||||
use super::common::uuid_id;
|
||||
|
||||
impl Default for TrackSegmentId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
uuid_id!(TrackSegmentId);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct TrackSegmentRevisionId(pub Uuid);
|
||||
|
||||
impl Default for TrackSegmentRevisionId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
uuid_id!(TrackSegmentRevisionId);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
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 {
|
||||
/// 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
|
||||
/// last trackpoints are anchors afterwards.
|
||||
pub fn splice(&mut self, start: usize, end: usize, points: Vec<Trackpoint>) {
|
||||
self.points.splice(start, end, points);
|
||||
self.rev_id = Default::default();
|
||||
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
|
||||
/// there is no such trackpoint.
|
||||
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
|
||||
@@ -82,7 +89,7 @@ impl TrackSegment {
|
||||
pub fn compute_anchors(&mut self) {
|
||||
let anchors = compute_anchors(self);
|
||||
let mut anchors = anchors.into_iter().peekable();
|
||||
self.points.update_all(|index, trkpt| {
|
||||
self.update_all(|index, trkpt| {
|
||||
trkpt.anchor = match anchors.peek() {
|
||||
Some(&(anchor, zoom)) if anchor == index => {
|
||||
anchors.next();
|
||||
@@ -98,6 +105,8 @@ impl TrackSegment {
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::Chunk;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn create_track_segment(nb_chunks: usize) -> TrackSegment {
|
||||
@@ -354,4 +363,49 @@ mod tests {
|
||||
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};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TrackId(pub Uuid);
|
||||
use super::common::uuid_id;
|
||||
|
||||
impl Default for TrackId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
uuid_id!(TrackId);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Track {
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{Link, LngLat};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct WaypointId(pub Uuid);
|
||||
use super::common::uuid_id;
|
||||
|
||||
impl Default for WaypointId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
uuid_id!(WaypointId);
|
||||
|
||||
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Waypoint {
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{Chunked, Waypoint, WaypointChunk, WaypointId};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct FileWaypointsRevisionId(pub Uuid);
|
||||
use super::common::uuid_id;
|
||||
|
||||
impl Default for FileWaypointsRevisionId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
uuid_id!(FileWaypointsRevisionId);
|
||||
|
||||
/// The waypoints of a file, in chunks that are shared between the versions of the file (see
|
||||
/// [`Chunked`]).
|
||||
|
||||
@@ -198,25 +198,25 @@ fn apply_text(
|
||||
Some(GPXElement::Surface) => {
|
||||
stack.pop();
|
||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||
trkpt.surface = categories.surface.code(&e);
|
||||
trkpt.surface = categories.surface.code(e);
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Highway) => {
|
||||
stack.pop();
|
||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||
trkpt.highway = categories.highway.code(&e);
|
||||
trkpt.highway = categories.highway.code(e);
|
||||
}
|
||||
}
|
||||
Some(GPXElement::SacScale) => {
|
||||
stack.pop();
|
||||
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) => {
|
||||
stack.pop();
|
||||
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) => {
|
||||
|
||||
@@ -12,8 +12,6 @@ pub trait Apply {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CommandError {
|
||||
/// The command is not implemented yet.
|
||||
NotImplemented(&'static str),
|
||||
/// The command would not change anything.
|
||||
NothingToDo,
|
||||
/// The input data could not be parsed.
|
||||
@@ -23,7 +21,6 @@ pub enum CommandError {
|
||||
impl fmt::Display for CommandError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::NotImplemented(name) => write!(f, "{name} is not implemented yet"),
|
||||
Self::NothingToDo => write!(f, "nothing to do"),
|
||||
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 b = add(&mut fx, file("b", &[], 0));
|
||||
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);
|
||||
fx.selection = files(&[b]);
|
||||
@@ -736,7 +734,7 @@ mod tests {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 2)], 2));
|
||||
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);
|
||||
fx.selection = files(&[b]);
|
||||
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)),
|
||||
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::Tracks(_), Destination::Files(_) | Destination::Tracks(..))
|
||||
| (
|
||||
ClipboardContent::Tracks(_),
|
||||
Destination::Files(_) | Destination::Tracks(..)
|
||||
)
|
||||
| (
|
||||
ClipboardContent::Segments(_),
|
||||
Destination::Files(_) | Destination::Tracks(..) | Destination::Segments(..),
|
||||
)
|
||||
| (ClipboardContent::Waypoints(_), Destination::Waypoints(..)) => true,
|
||||
_ => false,
|
||||
};
|
||||
| (ClipboardContent::Waypoints(_), Destination::Waypoints(..))
|
||||
);
|
||||
let empty = match content {
|
||||
ClipboardContent::Files(files) => files.is_empty(),
|
||||
ClipboardContent::Tracks(tracks) => tracks.is_empty(),
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
mod copy;
|
||||
mod produce;
|
||||
mod update_segments;
|
||||
mod update_selected;
|
||||
mod update_waypoint;
|
||||
|
||||
pub use copy::*;
|
||||
pub use produce::*;
|
||||
pub use update_segments::*;
|
||||
pub use update_selected::*;
|
||||
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, File, State};
|
||||
use crate::{Apply, CommandError, State, update_segments};
|
||||
|
||||
/// 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.
|
||||
@@ -31,17 +29,9 @@ impl Apply for Elevation<'_> {
|
||||
}
|
||||
|
||||
let mut ele = self.ele.iter().copied();
|
||||
// the segments of a file are consecutive
|
||||
for group in locations.chunk_by(|a, b| a.file_id == b.file_id) {
|
||||
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];
|
||||
update_segments(state, &locations, |_, segment| {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,11 +418,13 @@ mod tests {
|
||||
}
|
||||
fx.files.insert(b, Rc::new(file));
|
||||
let gap = |fx: &Fixture, remove_gaps: bool| {
|
||||
let mut fx2 = Fixture::default();
|
||||
fx2.files = fx.files.clone();
|
||||
fx2.order = crate::FileOrder(fx.order.0.clone());
|
||||
fx2.selection = Selection::File {
|
||||
let mut fx2 = Fixture {
|
||||
files: fx.files.clone(),
|
||||
order: crate::FileOrder(fx.order.0.clone()),
|
||||
selection: Selection::File {
|
||||
file_ids: [a, b].into(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
Merge {
|
||||
type_: MergeType::Connect,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Apply, CommandError, File, State, Trackpoint, reduce_indices};
|
||||
use crate::{Apply, CommandError, State, reduce_indices, update_segments};
|
||||
|
||||
/// 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
|
||||
@@ -22,32 +20,25 @@ impl Apply for Reduce {
|
||||
let locations = state
|
||||
.selection
|
||||
.segment_locations(state.files, &state.order.0);
|
||||
let mut reduced = vec![];
|
||||
let mut changed = vec![];
|
||||
let mut kept_points = vec![];
|
||||
for location in &locations {
|
||||
let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg];
|
||||
let kept = reduce_indices(segment, self.tolerance);
|
||||
if kept.len() == segment.len() {
|
||||
continue;
|
||||
}
|
||||
let points: Vec<Trackpoint> = kept.iter().map(|&i| segment[i].clone()).collect();
|
||||
reduced.push((*location, points));
|
||||
changed.push(*location);
|
||||
kept_points.push(kept.iter().map(|&i| segment[i].clone()).collect::<Vec<_>>());
|
||||
}
|
||||
if reduced.is_empty() {
|
||||
if changed.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
// the segments of a file are consecutive
|
||||
for group in reduced.chunk_by(|a, b| a.0.file_id == b.0.file_id) {
|
||||
let file_id = group[0].0.file_id;
|
||||
let mut file: File = (*state.files[&file_id]).clone();
|
||||
for (location, points) in group {
|
||||
let segment = &mut file.trk[location.trk].trkseg[location.seg];
|
||||
update_segments(state, &changed, |i, segment| {
|
||||
let len = segment.len();
|
||||
segment.splice(0, len, points.clone());
|
||||
segment.rev_id = Default::default();
|
||||
}
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
}
|
||||
segment.splice(0, len, std::mem::take(&mut kept_points[i]));
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use crate::{Apply, CommandError, File, FileId, State, round_trip};
|
||||
use crate::{Apply, CommandError, State, round_trip, update_segments};
|
||||
|
||||
/// 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
|
||||
@@ -12,26 +10,16 @@ pub struct RoundTrip;
|
||||
|
||||
impl Apply for RoundTrip {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let mut files: HashMap<FileId, File> = HashMap::new();
|
||||
for location in state
|
||||
let locations: Vec<_> = state
|
||||
.selection
|
||||
.segment_locations(state.files, &state.order.0)
|
||||
{
|
||||
let (file_id, trk, seg) = (location.file_id, location.trk, location.seg);
|
||||
if state.files[&file_id].trk[trk].trkseg[seg].len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let file = files
|
||||
.entry(file_id)
|
||||
.or_insert_with(|| (*state.files[&file_id]).clone());
|
||||
round_trip(&mut file.trk[trk].trkseg[seg]);
|
||||
}
|
||||
if files.is_empty() {
|
||||
.into_iter()
|
||||
.filter(|l| state.files[&l.file_id].trk[l.trk].trkseg[l.seg].len() >= 2)
|
||||
.collect();
|
||||
if locations.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
for (file_id, file) in files {
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
}
|
||||
update_segments(state, &locations, |_, segment| round_trip(segment));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -39,8 +27,11 @@ impl Apply for RoundTrip {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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::*;
|
||||
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, File, SegmentLocation, State, Statistics, TrackSegment, Trackpoint,
|
||||
artificial_weights, shifted_and_compressed, with_artificial_timestamps, with_timestamps,
|
||||
Apply, CommandError, SegmentLocation, State, Statistics, TrackSegment, Trackpoint,
|
||||
artificial_weights, shifted_and_compressed, update_segments, with_artificial_timestamps,
|
||||
with_timestamps,
|
||||
};
|
||||
|
||||
/// How the timestamps of the selection are set.
|
||||
@@ -94,7 +93,7 @@ impl Apply for Time {
|
||||
})
|
||||
.collect();
|
||||
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
|
||||
for weights in &mut weights {
|
||||
weights.iter_mut().for_each(|w| *w = 1.0);
|
||||
@@ -122,19 +121,10 @@ impl Apply for Time {
|
||||
}
|
||||
}
|
||||
|
||||
// the segments of a file are consecutive
|
||||
let mut times = times.into_iter();
|
||||
for group in locations.chunk_by(|a, b| a.file_id == b.file_id) {
|
||||
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));
|
||||
}
|
||||
update_segments(state, &locations, |i, segment| {
|
||||
let times = ×[i];
|
||||
segment.update_all(|j, pt| pt.time = times.get(j).copied().flatten());
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,7 +446,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_implemented_command_is_rejected() {
|
||||
fn test_command_with_nothing_to_do_is_rejected() {
|
||||
let mut engine = Engine::default();
|
||||
new(&mut engine, "file");
|
||||
assert!(!edit(&mut engine, Command::Reverse(crate::Reverse)));
|
||||
@@ -754,7 +754,7 @@ mod tests {
|
||||
));
|
||||
assert!(!select_elements(&mut engine, node.clone(), SelectMode::Add));
|
||||
assert_eq!(engine.selection(), &node);
|
||||
assert!(engine.selection_changed() == false);
|
||||
assert!(!engine.selection_changed());
|
||||
|
||||
// unknown elements and files are ignored when adding
|
||||
let unknown = tracks(&[TrackId::default()]);
|
||||
@@ -1429,4 +1429,129 @@ mod tests {
|
||||
assert!(engine.trackpoint(&file, &seg_id, len).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.
|
||||
fn selected_range<T: PartialEq>(
|
||||
fn selected_range<T: std::hash::Hash + Eq>(
|
||||
all: impl Iterator<Item = T>,
|
||||
selected: &HashSet<T>,
|
||||
) -> Option<(usize, usize)>
|
||||
where
|
||||
T: std::hash::Hash + Eq,
|
||||
{
|
||||
) -> Option<(usize, usize)> {
|
||||
let mut range: Option<(usize, usize)> = None;
|
||||
for (i, id) in all.enumerate() {
|
||||
if selected.contains(&id) {
|
||||
@@ -587,7 +584,7 @@ mod tests {
|
||||
wpt: vec![Waypoint::default()],
|
||||
..Default::default()
|
||||
});
|
||||
let tree = Tree {
|
||||
Tree {
|
||||
file: file.id,
|
||||
tracks: file.trk.iter().map(|t| t.id).collect(),
|
||||
segments: file
|
||||
@@ -598,8 +595,7 @@ mod tests {
|
||||
waypoints: file.wpt.iter().map(|w| w.id).collect(),
|
||||
order: vec![file.id],
|
||||
files: StackEntry::from([(file.id, Rc::new(file))]),
|
||||
};
|
||||
tree
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -128,3 +128,159 @@ pub fn decode_waypoints(id: Uuid, bytes: &[u8]) -> Option<WaypointChunk> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[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>;
|
||||
}
|
||||
|
||||
#[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