mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-17 08:42:28 +00:00
progress
This commit is contained in:
1 parent
41b9116c6a
commit
97d597e139
181 files changed
+1970
-2643
No files matched your search
@@ -0,0 +1,96 @@
|
||||
/// Maps the values of a category of trackpoint data (the surface, the highway...) to small codes,
|
||||
/// so that the trackpoints only hold a `u8` instead of a string.
|
||||
///
|
||||
/// The codes are the positions of the names in the order of first appearance. The table only
|
||||
/// grows: a code never changes meaning, so it stays valid in every state of the history.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Categories(Vec<String>);
|
||||
|
||||
impl Categories {
|
||||
/// Maximum number of distinct values.
|
||||
pub const CAPACITY: usize = u8::MAX as usize;
|
||||
|
||||
/// The code of `name`, which is added to the table if it is new. `None` if the table is full.
|
||||
pub fn code(&mut self, name: &str) -> Option<u8> {
|
||||
let index = match self.0.iter().position(|known| known == name) {
|
||||
Some(index) => index,
|
||||
None if self.0.len() < Self::CAPACITY => {
|
||||
self.0.push(name.to_owned());
|
||||
self.0.len() - 1
|
||||
}
|
||||
None => return None,
|
||||
};
|
||||
u8::try_from(index).ok()
|
||||
}
|
||||
|
||||
/// The name of a code, `None` if it is unknown.
|
||||
pub fn name(&self, code: u8) -> Option<&str> {
|
||||
self.0.get(usize::from(code)).map(String::as_str)
|
||||
}
|
||||
|
||||
/// All the names, in the order of their codes.
|
||||
pub fn names(&self) -> &[String] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The categories of the data of the trackpoints. There is one table per engine, shared by all
|
||||
/// its files.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct TrackpointCategories {
|
||||
pub surface: Categories,
|
||||
pub highway: Categories,
|
||||
pub sac_scale: Categories,
|
||||
pub mtb_scale: Categories,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_codes_follow_the_order_of_appearance() {
|
||||
let mut categories = Categories::default();
|
||||
assert_eq!(categories.code("asphalt"), Some(0));
|
||||
assert_eq!(categories.code("gravel"), Some(1));
|
||||
// a known name keeps its code
|
||||
assert_eq!(categories.code("asphalt"), Some(0));
|
||||
assert_eq!(categories.names(), ["asphalt", "gravel"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_names_of_codes() {
|
||||
let mut categories = Categories::default();
|
||||
let code = categories.code("asphalt").unwrap();
|
||||
assert_eq!(categories.name(code), Some("asphalt"));
|
||||
assert_eq!(categories.name(code + 1), None);
|
||||
assert_eq!(Categories::default().name(0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_a_full_table_gives_no_code_to_new_names() {
|
||||
let mut categories = Categories::default();
|
||||
for i in 0..Categories::CAPACITY {
|
||||
assert_eq!(categories.code(&i.to_string()), Some(i as u8));
|
||||
}
|
||||
assert_eq!(categories.code("one too many"), None);
|
||||
assert_eq!(categories.names().len(), Categories::CAPACITY);
|
||||
// the known ones still have their code, the last one included
|
||||
assert_eq!(categories.code("0"), Some(0));
|
||||
assert_eq!(categories.code("254"), Some(254));
|
||||
assert_eq!(categories.name(254), Some("254"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_categories_are_independent() {
|
||||
let mut categories = TrackpointCategories::default();
|
||||
categories.surface.code("asphalt");
|
||||
assert_eq!(categories.highway.code("path"), Some(0));
|
||||
assert_eq!(categories.sac_scale.code("hiking"), Some(0));
|
||||
assert_eq!(categories.mtb_scale.code("1"), Some(0));
|
||||
assert_eq!(categories.surface.names(), ["asphalt"]);
|
||||
assert_eq!(categories.highway.names(), ["path"]);
|
||||
assert_eq!(categories.sac_scale.names(), ["hiking"]);
|
||||
assert_eq!(categories.mtb_scale.names(), ["1"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
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.
|
||||
///
|
||||
/// A chunk has an identity, which changes whenever the content does (a modified chunk is a new
|
||||
/// one): what is derived from a chunk, or kept of it, is up to date as long as the identity is.
|
||||
pub trait Chunk {
|
||||
type Item: Clone;
|
||||
|
||||
/// Maximum number of items of a chunk.
|
||||
const MAX_SIZE: usize;
|
||||
|
||||
/// 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>;
|
||||
|
||||
fn is_full(&self) -> bool {
|
||||
self.items().len() >= Self::MAX_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_TRKPT_CHUNK_SIZE: usize = 4096;
|
||||
|
||||
uuid_id!(TrackpointChunkId);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TrackpointChunk {
|
||||
pub id: TrackpointChunkId,
|
||||
pub trkpt: Vec<Trackpoint>,
|
||||
}
|
||||
|
||||
impl Chunk for TrackpointChunk {
|
||||
type Item = Trackpoint;
|
||||
const MAX_SIZE: usize = MAX_TRKPT_CHUNK_SIZE;
|
||||
|
||||
fn new(trkpt: Vec<Trackpoint>) -> Self {
|
||||
Self {
|
||||
trkpt,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn renew(&mut self) {
|
||||
self.id = Default::default();
|
||||
}
|
||||
|
||||
fn items(&self) -> &Vec<Trackpoint> {
|
||||
&self.trkpt
|
||||
}
|
||||
|
||||
fn items_mut(&mut self) -> &mut Vec<Trackpoint> {
|
||||
&mut self.trkpt
|
||||
}
|
||||
}
|
||||
|
||||
uuid_id!(WaypointChunkId);
|
||||
|
||||
const MAX_WPT_CHUNK_SIZE: usize = 128;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WaypointChunk {
|
||||
pub id: WaypointChunkId,
|
||||
pub wpt: Vec<Waypoint>,
|
||||
}
|
||||
|
||||
impl Chunk for WaypointChunk {
|
||||
type Item = Waypoint;
|
||||
const MAX_SIZE: usize = MAX_WPT_CHUNK_SIZE;
|
||||
|
||||
fn new(wpt: Vec<Waypoint>) -> Self {
|
||||
Self {
|
||||
wpt,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn renew(&mut self) {
|
||||
self.id = Default::default();
|
||||
}
|
||||
|
||||
fn items(&self) -> &Vec<Waypoint> {
|
||||
&self.wpt
|
||||
}
|
||||
|
||||
fn items_mut(&mut self) -> &mut Vec<Waypoint> {
|
||||
&mut self.wpt
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_trackpoint_chunk_full() {
|
||||
let mut chunk = TrackpointChunk::default();
|
||||
assert!(!chunk.is_full());
|
||||
for _ in 0..MAX_TRKPT_CHUNK_SIZE - 1 {
|
||||
chunk.trkpt.push(Trackpoint::default());
|
||||
}
|
||||
assert!(!chunk.is_full());
|
||||
chunk.trkpt.push(Trackpoint::default());
|
||||
assert!(chunk.is_full());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_waypoint_chunk_full() {
|
||||
let mut chunk = WaypointChunk::default();
|
||||
assert!(!chunk.is_full());
|
||||
for _ in 0..MAX_WPT_CHUNK_SIZE {
|
||||
chunk.wpt.push(Waypoint::default());
|
||||
}
|
||||
assert!(chunk.is_full());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_chunks_have_their_own_identity() {
|
||||
let a = TrackpointChunk::new(vec![Trackpoint::default()]);
|
||||
let b = TrackpointChunk::new(vec![Trackpoint::default()]);
|
||||
assert_ne!(a.id, b.id);
|
||||
assert_eq!(a.items().len(), 1);
|
||||
assert_ne!(WaypointChunk::new(vec![]).id, WaypointChunk::new(vec![]).id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
use std::{ops::Index, rc::Rc};
|
||||
|
||||
use crate::Chunk;
|
||||
|
||||
/// A list of items, stored in [`Chunk`]s that are shared between its versions: editing it only
|
||||
/// copies the chunks around the change, so that a version costs little more than the change.
|
||||
///
|
||||
/// There are no empty chunks.
|
||||
#[derive(Debug)]
|
||||
pub struct Chunked<C: Chunk> {
|
||||
chunks: Vec<Rc<C>>,
|
||||
cumul_length: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<C: Chunk> Clone for Chunked<C> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
chunks: self.chunks.clone(),
|
||||
cumul_length: self.cumul_length.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chunk> Default for Chunked<C> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
chunks: vec![],
|
||||
cumul_length: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The position of an item: in which chunk, where in it, and among all the items.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, PartialOrd, Ord)]
|
||||
pub struct ChunkIndex {
|
||||
pub chunk: usize,
|
||||
pub pos: usize,
|
||||
pub flat: usize,
|
||||
}
|
||||
|
||||
impl<C: Chunk> Chunked<C> {
|
||||
/// Adds a chunk at the end, which is dropped if it is empty.
|
||||
pub fn push(&mut self, chunk: C) {
|
||||
self.push_shared(Rc::new(chunk));
|
||||
}
|
||||
|
||||
pub fn push_shared(&mut self, chunk: Rc<C>) {
|
||||
if chunk.items().is_empty() {
|
||||
return;
|
||||
}
|
||||
self.cumul_length
|
||||
.push(self.cumul_length.last().copied().unwrap_or_default() + chunk.items().len());
|
||||
self.chunks.push(chunk);
|
||||
}
|
||||
|
||||
pub fn chunks(&self) -> &[Rc<C>] {
|
||||
&self.chunks
|
||||
}
|
||||
|
||||
/// Replaces the items in `start..end` by `items`. Panics if the range is out of bounds.
|
||||
///
|
||||
/// Chunks that are not concerned are kept as they are (shared), only the chunks around the
|
||||
/// range are copied and refilled.
|
||||
pub fn splice(&mut self, start: usize, end: usize, items: Vec<C::Item>) {
|
||||
assert!(
|
||||
start <= end && end <= self.len(),
|
||||
"splice range out of bounds"
|
||||
);
|
||||
let old = std::mem::take(&mut self.chunks);
|
||||
self.cumul_length.clear();
|
||||
|
||||
let mut items = Some(items);
|
||||
let mut pending = vec![];
|
||||
let mut offset = 0;
|
||||
for chunk in old {
|
||||
let (lo, hi) = (offset, offset + chunk.items().len());
|
||||
offset = hi;
|
||||
// a chunk ending at `start` is extended unless it is full, to avoid tiny chunks
|
||||
if hi < start || (hi == start && chunk.is_full()) {
|
||||
self.push_shared(chunk);
|
||||
continue;
|
||||
}
|
||||
if let Some(items) = items.take() {
|
||||
self.fill(&mut pending, chunk.items()[..start - lo].iter().cloned());
|
||||
self.fill(&mut pending, items);
|
||||
}
|
||||
if hi <= end {
|
||||
continue;
|
||||
}
|
||||
if lo >= end {
|
||||
self.flush(&mut pending);
|
||||
self.push_shared(chunk);
|
||||
} else {
|
||||
self.fill(&mut pending, chunk.items()[end - lo..].iter().cloned());
|
||||
}
|
||||
}
|
||||
if let Some(items) = items {
|
||||
self.fill(&mut pending, items);
|
||||
}
|
||||
self.flush(&mut pending);
|
||||
}
|
||||
|
||||
fn fill(&mut self, pending: &mut Vec<C::Item>, items: impl IntoIterator<Item = C::Item>) {
|
||||
for item in items {
|
||||
pending.push(item);
|
||||
if pending.len() >= C::MAX_SIZE {
|
||||
self.flush(pending);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self, pending: &mut Vec<C::Item>) {
|
||||
self.push(C::new(std::mem::take(pending)));
|
||||
}
|
||||
|
||||
/// Edits the items of the chunks that contain an item accepted by `filter`.
|
||||
///
|
||||
/// `f` receives a copy of the items of such a chunk and returns whether it changed them. A
|
||||
/// changed chunk is replaced by a new one (so that what is derived from it is computed
|
||||
/// again) and the chunks left empty are dropped. The other chunks are kept. Returns whether
|
||||
/// anything changed.
|
||||
pub fn edit(
|
||||
&mut self,
|
||||
filter: impl Fn(&C::Item) -> bool,
|
||||
mut f: impl FnMut(&mut Vec<C::Item>) -> bool,
|
||||
) -> bool {
|
||||
let mut changed = false;
|
||||
let old = std::mem::take(&mut self.chunks);
|
||||
self.cumul_length.clear();
|
||||
for chunk in old {
|
||||
if !chunk.items().iter().any(&filter) {
|
||||
self.push_shared(chunk);
|
||||
continue;
|
||||
}
|
||||
let mut items = chunk.items().clone();
|
||||
if !f(&mut items) {
|
||||
self.push_shared(chunk);
|
||||
continue;
|
||||
}
|
||||
changed = true;
|
||||
self.push(C::new(items));
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
/// Changes what is in the chunk `chunk`, but not how many items there are. A chunk that is
|
||||
/// shared is copied first, so that the other versions do not change.
|
||||
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());
|
||||
unique.renew();
|
||||
}
|
||||
None => {
|
||||
let mut items = self.chunks[chunk].items().clone();
|
||||
f(&mut items);
|
||||
self.chunks[chunk] = Rc::new(C::new(items));
|
||||
}
|
||||
}
|
||||
debug_assert_eq!(self.chunks[chunk].items().len(), len);
|
||||
}
|
||||
|
||||
/// Applies `f` to the item at `index`, which is copied in a new chunk. Panics if there is no
|
||||
/// such item.
|
||||
pub fn update(&mut self, index: usize, f: impl FnOnce(&mut C::Item)) {
|
||||
let ChunkIndex { chunk, pos, .. } = self.locate(index).unwrap();
|
||||
self.replace_chunk(chunk, |items| f(&mut items[pos]));
|
||||
}
|
||||
|
||||
/// Applies `f` to every item, with its index, in new chunks.
|
||||
pub fn update_all(&mut self, mut f: impl FnMut(usize, &mut C::Item)) {
|
||||
let mut offset = 0;
|
||||
for chunk in 0..self.chunks.len() {
|
||||
let len = self.chunks[chunk].items().len();
|
||||
self.replace_chunk(chunk, |items| {
|
||||
for (i, item) in items.iter_mut().enumerate() {
|
||||
f(offset + i, item);
|
||||
}
|
||||
});
|
||||
offset += len;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.cumul_length.last().copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.chunks.is_empty()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> ChunkedIter<'_, C> {
|
||||
ChunkedIter::new(self)
|
||||
}
|
||||
|
||||
pub fn first_index(&self) -> Option<ChunkIndex> {
|
||||
self.next_index(None)
|
||||
}
|
||||
|
||||
pub fn last_index(&self) -> Option<ChunkIndex> {
|
||||
self.prev_index(None)
|
||||
}
|
||||
|
||||
pub fn next_index(&self, cur: Option<ChunkIndex>) -> Option<ChunkIndex> {
|
||||
let mut next = cur.map_or_default(|idx| ChunkIndex {
|
||||
chunk: idx.chunk,
|
||||
pos: idx.pos + 1,
|
||||
flat: idx.flat + 1,
|
||||
});
|
||||
loop {
|
||||
if next.chunk >= self.chunks.len() {
|
||||
return None;
|
||||
}
|
||||
if next.pos == self.chunks[next.chunk].items().len() {
|
||||
next.chunk += 1;
|
||||
next.pos = 0;
|
||||
} else {
|
||||
return Some(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev_index(&self, cur: Option<ChunkIndex>) -> Option<ChunkIndex> {
|
||||
let mut prev = cur.unwrap_or(ChunkIndex {
|
||||
chunk: self.chunks.len(),
|
||||
pos: 0,
|
||||
flat: self.cumul_length.last().copied().unwrap_or_default(),
|
||||
});
|
||||
if prev.pos == 0 {
|
||||
while prev.chunk > 0 {
|
||||
prev.chunk -= 1;
|
||||
if !self.chunks[prev.chunk].items().is_empty() {
|
||||
prev.pos = self.chunks[prev.chunk].items().len() - 1;
|
||||
prev.flat -= 1;
|
||||
return Some(prev);
|
||||
}
|
||||
}
|
||||
None
|
||||
} else {
|
||||
prev.pos -= 1;
|
||||
prev.flat -= 1;
|
||||
Some(prev)
|
||||
}
|
||||
}
|
||||
|
||||
/// The position of the item `idx` among all the items.
|
||||
pub fn locate(&self, idx: usize) -> Option<ChunkIndex> {
|
||||
let chunk = self.cumul_length.partition_point(|l| idx >= *l);
|
||||
if chunk >= self.chunks.len() {
|
||||
return None;
|
||||
}
|
||||
let pos = if chunk > 0 {
|
||||
idx - self.cumul_length[chunk - 1]
|
||||
} else {
|
||||
idx
|
||||
};
|
||||
if pos >= self.chunks[chunk].items().len() {
|
||||
None
|
||||
} else {
|
||||
Some(ChunkIndex {
|
||||
chunk,
|
||||
pos,
|
||||
flat: idx,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chunk> Index<ChunkIndex> for Chunked<C> {
|
||||
type Output = C::Item;
|
||||
|
||||
fn index(&self, idx: ChunkIndex) -> &Self::Output {
|
||||
&self.chunks[idx.chunk].items()[idx.pos]
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chunk> Index<usize> for Chunked<C> {
|
||||
type Output = C::Item;
|
||||
|
||||
fn index(&self, idx: usize) -> &Self::Output {
|
||||
&self[self.locate(idx).unwrap()]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, C: Chunk> IntoIterator for &'a Chunked<C> {
|
||||
type Item = &'a C::Item;
|
||||
type IntoIter = ChunkedIter<'a, C>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
chunked: self.chunked,
|
||||
idx: self.idx,
|
||||
exhausted: self.exhausted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, C: Chunk> ChunkedIter<'a, C> {
|
||||
pub fn new(chunked: &'a Chunked<C>) -> Self {
|
||||
Self {
|
||||
chunked,
|
||||
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> {
|
||||
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> {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/// 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)]
|
||||
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)]
|
||||
pub struct Link {
|
||||
pub href: String,
|
||||
pub text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct LngLat {
|
||||
pub lng: f64,
|
||||
pub lat: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LngLatBounds {
|
||||
pub sw: LngLat,
|
||||
pub ne: LngLat,
|
||||
}
|
||||
|
||||
impl Default for LngLatBounds {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sw: LngLat {
|
||||
lng: 180.0,
|
||||
lat: 90.0,
|
||||
},
|
||||
ne: LngLat {
|
||||
lng: -180.0,
|
||||
lat: -90.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LngLatBounds {
|
||||
pub fn extend(&mut self, coordinates: LngLat) {
|
||||
self.sw.lng = self.sw.lng.min(coordinates.lng);
|
||||
self.sw.lat = self.sw.lat.min(coordinates.lat);
|
||||
self.ne.lng = self.ne.lng.max(coordinates.lng);
|
||||
self.ne.lat = self.ne.lat.max(coordinates.lat);
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: &LngLatBounds) {
|
||||
self.sw.lng = self.sw.lng.min(other.sw.lng);
|
||||
self.sw.lat = self.sw.lat.min(other.sw.lat);
|
||||
self.ne.lng = self.ne.lng.max(other.ne.lng);
|
||||
self.ne.lat = self.ne.lat.max(other.ne.lat);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bounds_extend() {
|
||||
let mut bounds = LngLatBounds::default();
|
||||
bounds.extend(LngLat {
|
||||
lng: 4.0,
|
||||
lat: 50.0,
|
||||
});
|
||||
assert_eq!((bounds.sw.lng, bounds.sw.lat), (4.0, 50.0));
|
||||
assert_eq!((bounds.ne.lng, bounds.ne.lat), (4.0, 50.0));
|
||||
|
||||
bounds.extend(LngLat {
|
||||
lng: 6.0,
|
||||
lat: 45.0,
|
||||
});
|
||||
bounds.extend(LngLat {
|
||||
lng: 5.0,
|
||||
lat: 48.0,
|
||||
});
|
||||
assert_eq!((bounds.sw.lng, bounds.sw.lat), (4.0, 45.0));
|
||||
assert_eq!((bounds.ne.lng, bounds.ne.lat), (6.0, 50.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bounds_merge() {
|
||||
let mut a = LngLatBounds::default();
|
||||
a.extend(LngLat { lng: 0.0, lat: 0.0 });
|
||||
a.extend(LngLat { lng: 1.0, lat: 1.0 });
|
||||
let mut b = LngLatBounds::default();
|
||||
b.extend(LngLat {
|
||||
lng: -2.0,
|
||||
lat: 0.5,
|
||||
});
|
||||
b.extend(LngLat { lng: 0.5, lat: 3.0 });
|
||||
|
||||
a.merge(&b);
|
||||
assert_eq!((a.sw.lng, a.sw.lat), (-2.0, 0.0));
|
||||
assert_eq!((a.ne.lng, a.ne.lat), (1.0, 3.0));
|
||||
|
||||
// merging an empty bounds changes nothing
|
||||
a.merge(&LngLatBounds::default());
|
||||
assert_eq!((a.sw.lng, a.sw.lat), (-2.0, 0.0));
|
||||
assert_eq!((a.ne.lng, a.ne.lat), (1.0, 3.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use crate::{Link, Track, Waypoints};
|
||||
|
||||
use super::common::uuid_id;
|
||||
|
||||
uuid_id!(FileId);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct File {
|
||||
pub id: FileId,
|
||||
pub info: FileInfo,
|
||||
pub trk: Vec<Track>,
|
||||
pub wpt: Waypoints,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct FileInfo {
|
||||
pub name: String,
|
||||
pub desc: Option<String>,
|
||||
pub author: Option<Author>,
|
||||
pub links: Vec<Link>,
|
||||
pub time: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Author {
|
||||
pub name: Option<String>,
|
||||
/// `id@domain`, which the file gives as the two attributes of an `email` element.
|
||||
pub email: Option<String>,
|
||||
/// The author has a single link, unlike the other elements.
|
||||
pub link: Option<Link>,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
mod categories;
|
||||
mod chunk;
|
||||
mod chunked;
|
||||
mod common;
|
||||
mod file;
|
||||
mod segment;
|
||||
mod track;
|
||||
mod trackpoint;
|
||||
mod waypoint;
|
||||
mod waypoints;
|
||||
|
||||
pub use categories::*;
|
||||
pub use chunk::*;
|
||||
pub use chunked::*;
|
||||
pub use common::*;
|
||||
pub use file::*;
|
||||
pub use segment::*;
|
||||
pub use track::*;
|
||||
pub use trackpoint::*;
|
||||
pub use waypoint::*;
|
||||
pub use waypoints::*;
|
||||
@@ -0,0 +1,411 @@
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{ChunkIndex, Chunked, ChunkedIter, Trackpoint, TrackpointChunk, compute_anchors};
|
||||
|
||||
use super::common::uuid_id;
|
||||
|
||||
uuid_id!(TrackSegmentId);
|
||||
|
||||
uuid_id!(TrackSegmentRevisionId);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TrackSegment {
|
||||
pub id: TrackSegmentId,
|
||||
pub rev_id: TrackSegmentRevisionId,
|
||||
points: Chunked<TrackpointChunk>,
|
||||
}
|
||||
|
||||
/// The position of a trackpoint in a segment.
|
||||
pub type TrackSegmentIndex = ChunkIndex;
|
||||
|
||||
pub type TrackSegmentIterator<'a> = ChunkedIter<'a, TrackpointChunk>;
|
||||
|
||||
impl Deref for TrackSegment {
|
||||
type Target = Chunked<TrackpointChunk>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&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();
|
||||
}
|
||||
|
||||
/// The first and the last trackpoints are always anchors, shown at every zoom level.
|
||||
///
|
||||
/// It is done by [`TrackSegment::splice`]; commands that build segments in another way need to
|
||||
/// call it.
|
||||
pub fn ensure_end_anchors(&mut self) {
|
||||
let Some(last) = self.len().checked_sub(1) else {
|
||||
return;
|
||||
};
|
||||
for index in [0, last] {
|
||||
if self[index].anchor != Some(0) {
|
||||
self.set_anchor(index, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.update(index, |trkpt| trkpt.anchor = Some(zoom));
|
||||
}
|
||||
|
||||
/// Sets the anchors of the trackpoints from the details of the path of the segment (see
|
||||
/// [`compute_anchors`]), forgetting the previous ones.
|
||||
pub fn compute_anchors(&mut self) {
|
||||
let anchors = compute_anchors(self);
|
||||
let mut anchors = anchors.into_iter().peekable();
|
||||
self.update_all(|index, trkpt| {
|
||||
trkpt.anchor = match anchors.peek() {
|
||||
Some(&(anchor, zoom)) if anchor == index => {
|
||||
anchors.next();
|
||||
Some(zoom)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::Chunk;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn create_track_segment(nb_chunks: usize) -> TrackSegment {
|
||||
let mut trkseg = TrackSegment::default();
|
||||
let mut count = 0;
|
||||
for n in 1..=nb_chunks {
|
||||
let mut chunk = TrackpointChunk::default();
|
||||
for _ in 0..n {
|
||||
chunk.trkpt.push(Trackpoint {
|
||||
ele: count as f64,
|
||||
..Default::default()
|
||||
});
|
||||
count += 1;
|
||||
}
|
||||
trkseg.push(chunk);
|
||||
}
|
||||
trkseg
|
||||
}
|
||||
|
||||
fn eles(trkseg: &TrackSegment) -> Vec<f64> {
|
||||
trkseg.iter().map(|p| p.ele).collect()
|
||||
}
|
||||
|
||||
fn points(eles: &[f64]) -> Vec<Trackpoint> {
|
||||
eles.iter()
|
||||
.map(|&ele| Trackpoint {
|
||||
ele,
|
||||
..Default::default()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_replace_insert_delete_append() {
|
||||
// chunks of 1..=5 points: [0] [1 2] [3 4 5] [6 7 8 9] [10..=14]
|
||||
let mut trkseg = create_track_segment(5);
|
||||
trkseg.splice(4, 8, points(&[-1.0, -2.0]));
|
||||
let mut expected: Vec<f64> = (0..4).map(f64::from).collect();
|
||||
expected.extend([-1.0, -2.0]);
|
||||
expected.extend((8..15).map(f64::from));
|
||||
assert_eq!(eles(&trkseg), expected);
|
||||
assert_eq!(trkseg.len(), expected.len());
|
||||
for i in 0..trkseg.len() {
|
||||
assert_eq!(trkseg[i].ele, expected[i]);
|
||||
}
|
||||
|
||||
trkseg.splice(2, 2, points(&[100.0]));
|
||||
expected.insert(2, 100.0);
|
||||
assert_eq!(eles(&trkseg), expected);
|
||||
|
||||
trkseg.splice(0, 3, vec![]);
|
||||
expected.drain(..3);
|
||||
assert_eq!(eles(&trkseg), expected);
|
||||
|
||||
let len = trkseg.len();
|
||||
trkseg.splice(len, len, points(&[7.0, 8.0]));
|
||||
expected.extend([7.0, 8.0]);
|
||||
assert_eq!(eles(&trkseg), expected);
|
||||
|
||||
let len = trkseg.len();
|
||||
trkseg.splice(0, len, vec![]);
|
||||
assert_eq!(trkseg.len(), 0);
|
||||
assert!(trkseg.first_index().is_none());
|
||||
}
|
||||
|
||||
fn anchors(trkseg: &TrackSegment) -> Vec<Option<u8>> {
|
||||
trkseg.iter().map(|p| p.anchor).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_keeps_the_ends_anchors() {
|
||||
let mut trkseg = TrackSegment::default();
|
||||
assert_eq!(anchors(&trkseg), vec![]);
|
||||
trkseg.splice(0, 0, points(&[0.0, 1.0, 2.0]));
|
||||
assert_eq!(anchors(&trkseg), [Some(0), None, Some(0)]);
|
||||
|
||||
// new ends: the previous ones stay anchors
|
||||
trkseg.splice(3, 3, points(&[3.0]));
|
||||
trkseg.splice(0, 0, points(&[-1.0]));
|
||||
assert_eq!(anchors(&trkseg), [Some(0), Some(0), None, Some(0), Some(0)]);
|
||||
|
||||
// removing the ends: the new ones become anchors
|
||||
trkseg.splice(4, 5, vec![]);
|
||||
trkseg.splice(0, 2, vec![]);
|
||||
assert_eq!(anchors(&trkseg), [Some(0), Some(0)]);
|
||||
|
||||
// a single point is both ends
|
||||
trkseg.splice(1, 2, vec![]);
|
||||
assert_eq!(anchors(&trkseg), [Some(0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_anchors_replaces_the_previous_ones() {
|
||||
let mut trkseg = TrackSegment::default();
|
||||
let line: Vec<_> = (0..10)
|
||||
.map(|i| Trackpoint {
|
||||
coordinates: crate::LngLat {
|
||||
lng: i as f64 * 0.001,
|
||||
lat: 0.0,
|
||||
},
|
||||
anchor: Some(5),
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
trkseg.splice(0, 0, line);
|
||||
trkseg.compute_anchors();
|
||||
let mut expected = vec![None; 10];
|
||||
expected[0] = Some(0);
|
||||
expected[9] = Some(0);
|
||||
assert_eq!(anchors(&trkseg), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_keeps_untouched_chunks_shared() {
|
||||
let mut trkseg = create_track_segment(5);
|
||||
// the ends are anchors already, or their chunks would be copied
|
||||
trkseg.ensure_end_anchors();
|
||||
let before = trkseg.chunks().to_vec();
|
||||
// inside the third chunk only
|
||||
trkseg.splice(4, 5, points(&[-1.0]));
|
||||
assert!(Rc::ptr_eq(&trkseg.chunks()[0], &before[0]));
|
||||
assert!(Rc::ptr_eq(&trkseg.chunks()[1], &before[1]));
|
||||
assert!(Rc::ptr_eq(trkseg.chunks().last().unwrap(), &before[4]));
|
||||
assert!(Rc::ptr_eq(
|
||||
&trkseg.chunks()[trkseg.chunks().len() - 2],
|
||||
&before[3]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_append_extends_last_chunk_and_respects_max_size() {
|
||||
let mut trkseg = TrackSegment::default();
|
||||
for i in 0..10_000 {
|
||||
let len = trkseg.len();
|
||||
trkseg.splice(len, len, points(&[i as f64]));
|
||||
}
|
||||
assert_eq!(trkseg.len(), 10_000);
|
||||
assert!(trkseg.chunks().len() <= 3);
|
||||
assert!(trkseg.chunks().iter().all(|c| c.trkpt.len() <= 4096));
|
||||
assert_eq!(trkseg[9_999].ele, 9_999.0);
|
||||
assert_eq!(trkseg.len(), 10_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_len() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
assert_eq!(trkseg.len(), nb_chunks * (nb_chunks + 1) / 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_locate() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
for i in 0..trkseg.len() {
|
||||
let idx = trkseg.locate(i);
|
||||
assert!(idx.is_some());
|
||||
assert_eq!(trkseg[idx.unwrap()].ele, i as f64);
|
||||
}
|
||||
|
||||
assert!(trkseg.locate(trkseg.len()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_index() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
for i in 0..trkseg.len() {
|
||||
assert_eq!(trkseg[i].ele, i as f64);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_index_out_of_bounds_1() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
let _ = trkseg[trkseg.len()];
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_index_out_of_bounds_2() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
let _ = trkseg[TrackSegmentIndex {
|
||||
chunk: trkseg.chunks().len(),
|
||||
pos: 0,
|
||||
flat: 0,
|
||||
}];
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
for (i, trkpt) in trkseg.iter().enumerate() {
|
||||
assert!(std::ptr::eq(&trkseg[i], trkpt));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_nth() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
let tenth = trkseg.iter().nth(10);
|
||||
assert!(tenth.is_some());
|
||||
assert!(std::ptr::eq(&trkseg[10], tenth.unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_skip() {
|
||||
let nb_chunks = 10;
|
||||
let trkseg = create_track_segment(nb_chunks);
|
||||
for (i, trkpt) in trkseg.iter().enumerate().skip(5) {
|
||||
assert!(std::ptr::eq(&trkseg[i], trkpt));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_last_index() {
|
||||
assert!(TrackSegment::default().first_index().is_none());
|
||||
assert!(TrackSegment::default().last_index().is_none());
|
||||
|
||||
let trkseg = create_track_segment(5); // 15 points in chunks of 1..=5
|
||||
let first = trkseg.first_index().unwrap();
|
||||
let last = trkseg.last_index().unwrap();
|
||||
assert_eq!((first.chunk, first.pos, first.flat), (0, 0, 0));
|
||||
assert_eq!((last.chunk, last.pos, last.flat), (4, 4, 14));
|
||||
assert_eq!(trkseg[last].ele, 14.0);
|
||||
assert_eq!(trkseg.locate(14), Some(last));
|
||||
assert!(trkseg.next_index(Some(last)).is_none());
|
||||
assert!(trkseg.prev_index(Some(first)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prev_next_are_inverse() {
|
||||
let trkseg = create_track_segment(6);
|
||||
let mut idx = trkseg.last_index();
|
||||
let mut visited = vec![];
|
||||
while let Some(i) = idx {
|
||||
assert_eq!(trkseg.locate(i.flat), Some(i));
|
||||
visited.push(i.flat);
|
||||
idx = trkseg.prev_index(Some(i));
|
||||
}
|
||||
assert_eq!(visited, (0..trkseg.len()).rev().collect::<Vec<_>>());
|
||||
|
||||
let mut idx = trkseg.first_index();
|
||||
while let Some(i) = idx {
|
||||
let next = trkseg.next_index(Some(i));
|
||||
if let Some(n) = next {
|
||||
assert_eq!(trkseg.prev_index(Some(n)), Some(i));
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use crate::{Link, TrackSegment};
|
||||
|
||||
use super::common::uuid_id;
|
||||
|
||||
uuid_id!(TrackId);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Track {
|
||||
pub id: TrackId,
|
||||
pub info: TrackInfo,
|
||||
pub trkseg: Vec<TrackSegment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct TrackInfo {
|
||||
pub name: Option<String>,
|
||||
pub cmt: Option<String>,
|
||||
pub desc: Option<String>,
|
||||
pub src: Option<String>,
|
||||
pub links: Vec<Link>,
|
||||
pub type_: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub opacity: Option<f64>,
|
||||
pub width: Option<f64>,
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use crate::LngLat;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Trackpoint {
|
||||
pub coordinates: LngLat,
|
||||
pub ele: f64,
|
||||
pub time: Option<i64>,
|
||||
pub atemp: Option<i16>,
|
||||
pub hr: Option<u16>,
|
||||
pub cad: Option<u16>,
|
||||
pub power: Option<u16>,
|
||||
/// Code of the surface, see [`crate::TrackpointCategories`].
|
||||
pub surface: Option<u8>,
|
||||
/// Code of the highway, see [`crate::TrackpointCategories`].
|
||||
pub highway: Option<u8>,
|
||||
/// Code of the SAC hiking scale, see [`crate::TrackpointCategories`].
|
||||
pub sac_scale: Option<u8>,
|
||||
/// Code of the mountain biking scale, see [`crate::TrackpointCategories`].
|
||||
pub mtb_scale: Option<u8>,
|
||||
/// Whether the trackpoint is an anchor of the routing tool (a point the user can drag to
|
||||
/// reroute the segment): the lowest map zoom level at which it is shown. The first and last
|
||||
/// trackpoints of a segment are always anchors that are always shown (zoom 0).
|
||||
pub anchor: Option<u8>,
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use crate::{Link, LngLat};
|
||||
|
||||
use super::common::uuid_id;
|
||||
|
||||
uuid_id!(WaypointId);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Waypoint {
|
||||
pub id: WaypointId,
|
||||
pub coordinates: LngLat,
|
||||
pub ele: f64,
|
||||
pub time: Option<i64>,
|
||||
pub name: Option<String>,
|
||||
pub cmt: Option<String>,
|
||||
pub desc: Option<String>,
|
||||
pub links: Vec<Link>,
|
||||
pub sym: Option<String>,
|
||||
pub type_: Option<String>,
|
||||
}
|
||||
|
||||
impl Waypoint {
|
||||
/// Whether the waypoints are the same, whatever their ids.
|
||||
pub fn same_content(&self, other: &Waypoint) -> bool {
|
||||
self.coordinates.lng == other.coordinates.lng
|
||||
&& self.coordinates.lat == other.coordinates.lat
|
||||
&& self.ele == other.ele
|
||||
&& self.time == other.time
|
||||
&& self.name == other.name
|
||||
&& self.cmt == other.cmt
|
||||
&& self.desc == other.desc
|
||||
&& self.links == other.links
|
||||
&& self.sym == other.sym
|
||||
&& self.type_ == other.type_
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::{Chunked, Waypoint, WaypointChunk, WaypointId};
|
||||
|
||||
use super::common::uuid_id;
|
||||
|
||||
uuid_id!(FileWaypointsRevisionId);
|
||||
|
||||
/// The waypoints of a file, in chunks that are shared between the versions of the file (see
|
||||
/// [`Chunked`]).
|
||||
///
|
||||
/// Everything that changes the waypoints changes the revision too, which is why there is no
|
||||
/// mutable access to the list itself: the revision tells what is derived from the waypoints
|
||||
/// (coordinates for the map...) that it is out of date.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Waypoints {
|
||||
chunks: Chunked<WaypointChunk>,
|
||||
/// Changes when the waypoints do.
|
||||
pub rev_id: FileWaypointsRevisionId,
|
||||
}
|
||||
|
||||
impl Deref for Waypoints {
|
||||
type Target = Chunked<WaypointChunk>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.chunks
|
||||
}
|
||||
}
|
||||
|
||||
impl Waypoints {
|
||||
/// Waypoints made of the given chunks, which are used as they are.
|
||||
pub fn new(chunks: impl IntoIterator<Item = WaypointChunk>) -> Self {
|
||||
let mut waypoints = Self::default();
|
||||
for chunk in chunks {
|
||||
waypoints.push(chunk);
|
||||
}
|
||||
waypoints
|
||||
}
|
||||
|
||||
/// Adds a chunk after the last waypoint.
|
||||
pub fn push(&mut self, chunk: WaypointChunk) {
|
||||
self.chunks.push(chunk);
|
||||
self.rev_id = Default::default();
|
||||
}
|
||||
|
||||
/// Adds a chunk that is shared with something else after the last waypoint.
|
||||
pub fn push_shared(&mut self, chunk: std::rc::Rc<WaypointChunk>) {
|
||||
self.chunks.push_shared(chunk);
|
||||
self.rev_id = Default::default();
|
||||
}
|
||||
|
||||
/// Replaces the waypoints in `start..end` by `waypoints`, see [`Chunked::splice`].
|
||||
pub fn splice(&mut self, start: usize, end: usize, waypoints: Vec<Waypoint>) {
|
||||
self.chunks.splice(start, end, waypoints);
|
||||
self.rev_id = Default::default();
|
||||
}
|
||||
|
||||
/// Inserts waypoints, so that the first one is at `index` among the waypoints (at the end if
|
||||
/// the index is past it). Only the chunks around the insertion are copied.
|
||||
pub fn insert_at(&mut self, index: usize, waypoints: Vec<Waypoint>) {
|
||||
if waypoints.is_empty() {
|
||||
return;
|
||||
}
|
||||
let index = index.min(self.len());
|
||||
self.splice(index, index, waypoints);
|
||||
}
|
||||
|
||||
/// Inserts waypoints right after the waypoint `after`, or at the end if there is none (or if
|
||||
/// it is not there). See [`Waypoints::insert_at`].
|
||||
pub fn insert_after(&mut self, after: Option<WaypointId>, waypoints: Vec<Waypoint>) {
|
||||
let index = after
|
||||
.and_then(|after| self.position(after))
|
||||
.map_or(usize::MAX, |i| i + 1);
|
||||
self.insert_at(index, waypoints);
|
||||
}
|
||||
|
||||
/// The position of the waypoint among the waypoints.
|
||||
pub fn position(&self, id: WaypointId) -> Option<usize> {
|
||||
self.iter().position(|wpt| wpt.id == id)
|
||||
}
|
||||
|
||||
/// Edits the waypoints of the chunks that contain one accepted by `filter`, see
|
||||
/// [`Chunked::edit`]. Returns whether anything changed.
|
||||
pub fn edit(
|
||||
&mut self,
|
||||
filter: impl Fn(&Waypoint) -> bool,
|
||||
f: impl FnMut(&mut Vec<Waypoint>) -> bool,
|
||||
) -> bool {
|
||||
let changed = self.chunks.edit(filter, f);
|
||||
if changed {
|
||||
self.rev_id = Default::default();
|
||||
}
|
||||
changed
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn waypoint() -> Waypoint {
|
||||
Waypoint::default()
|
||||
}
|
||||
|
||||
fn waypoints_with(chunks: &[usize]) -> (Waypoints, Vec<WaypointId>) {
|
||||
let mut waypoints = Waypoints::default();
|
||||
let mut ids = vec![];
|
||||
for n in chunks {
|
||||
let wpt: Vec<_> = (0..*n).map(|_| waypoint()).collect();
|
||||
ids.extend(wpt.iter().map(|w| w.id));
|
||||
waypoints.push(WaypointChunk {
|
||||
wpt,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
(waypoints, ids)
|
||||
}
|
||||
|
||||
fn ids(waypoints: &Waypoints) -> Vec<WaypointId> {
|
||||
waypoints.iter().map(|w| w.id).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_and_push() {
|
||||
let (waypoints, ids_) = waypoints_with(&[2, 1]);
|
||||
assert_eq!(waypoints.len(), 3);
|
||||
assert_eq!(waypoints.chunks().len(), 2);
|
||||
assert_eq!(ids(&waypoints), ids_);
|
||||
assert_eq!(waypoints.position(ids_[2]), Some(2));
|
||||
assert_eq!(waypoints.position(WaypointId::default()), None);
|
||||
// empty chunks are dropped
|
||||
let waypoints = Waypoints::new([WaypointChunk::default()]);
|
||||
assert!(waypoints.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_at_the_end() {
|
||||
let (mut waypoints, before) = waypoints_with(&[2, 1]);
|
||||
let first = waypoints.chunks()[0].clone();
|
||||
let rev = waypoints.rev_id;
|
||||
let new = vec![waypoint(), waypoint()];
|
||||
let new_ids: Vec<_> = new.iter().map(|w| w.id).collect();
|
||||
waypoints.insert_after(None, new);
|
||||
assert_eq!(ids(&waypoints), [before.clone(), new_ids].concat());
|
||||
// the chunk that is not touched is kept, the last one is extended
|
||||
assert_eq!(waypoints.chunks().len(), 2);
|
||||
assert!(Rc::ptr_eq(&waypoints.chunks()[0], &first));
|
||||
assert_ne!(waypoints.rev_id, rev);
|
||||
// an unknown waypoint is an insertion at the end too
|
||||
let (mut waypoints, before) = waypoints_with(&[2]);
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
waypoints.insert_after(Some(WaypointId::default()), vec![one]);
|
||||
assert_eq!(ids(&waypoints), [before, vec![one_id]].concat());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_after_the_last_waypoint_of_a_chunk_keeps_the_next_ones() {
|
||||
let (mut waypoints, before) = waypoints_with(&[2, 2]);
|
||||
let last = waypoints.chunks()[1].clone();
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
waypoints.insert_after(Some(before[1]), vec![one]);
|
||||
assert_eq!(
|
||||
ids(&waypoints),
|
||||
[&before[..2], &[one_id], &before[2..]].concat()
|
||||
);
|
||||
assert_eq!(waypoints.chunks().len(), 2);
|
||||
assert!(Rc::ptr_eq(&waypoints.chunks()[1], &last));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_in_the_middle_of_a_chunk_cuts_it() {
|
||||
let (mut waypoints, before) = waypoints_with(&[3, 1]);
|
||||
let last = waypoints.chunks()[1].clone();
|
||||
let new = vec![waypoint(), waypoint()];
|
||||
let new_ids: Vec<_> = new.iter().map(|w| w.id).collect();
|
||||
waypoints.insert_after(Some(before[0]), new);
|
||||
assert_eq!(
|
||||
ids(&waypoints),
|
||||
[&before[..1], &new_ids[..], &before[1..]].concat()
|
||||
);
|
||||
// the cut chunk is refilled, the other one is kept
|
||||
assert_eq!(waypoints.chunks().len(), 2);
|
||||
assert!(Rc::ptr_eq(&waypoints.chunks()[1], &last));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_nothing() {
|
||||
let (mut waypoints, before) = waypoints_with(&[2]);
|
||||
let rev = waypoints.rev_id;
|
||||
waypoints.insert_after(Some(before[0]), vec![]);
|
||||
assert_eq!(ids(&waypoints), before);
|
||||
assert_eq!(waypoints.rev_id, rev);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_many_fills_chunks() {
|
||||
let mut waypoints = Waypoints::default();
|
||||
let new: Vec<_> = (0..300).map(|_| waypoint()).collect();
|
||||
let new_ids: Vec<_> = new.iter().map(|w| w.id).collect();
|
||||
waypoints.insert_after(None, new);
|
||||
assert_eq!(ids(&waypoints), new_ids);
|
||||
assert_eq!(waypoints.chunks().len(), 3);
|
||||
assert!(waypoints.chunks()[..2].iter().all(|chunk| {
|
||||
use crate::Chunk;
|
||||
chunk.is_full()
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_at_an_index() {
|
||||
// at the start
|
||||
let (mut waypoints, before) = waypoints_with(&[2, 1]);
|
||||
let first = waypoints.chunks()[0].clone();
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
waypoints.insert_at(0, vec![one]);
|
||||
assert_eq!(ids(&waypoints), [vec![one_id], before.clone()].concat());
|
||||
// the existing chunks are kept
|
||||
assert_eq!(waypoints.chunks().len(), 3);
|
||||
assert!(Rc::ptr_eq(&waypoints.chunks()[1], &first));
|
||||
|
||||
// between two chunks
|
||||
let (mut waypoints, before) = waypoints_with(&[2, 1]);
|
||||
let last = waypoints.chunks()[1].clone();
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
waypoints.insert_at(2, vec![one]);
|
||||
assert_eq!(
|
||||
ids(&waypoints),
|
||||
[&before[..2], &[one_id], &before[2..]].concat()
|
||||
);
|
||||
assert!(Rc::ptr_eq(&waypoints.chunks()[1], &last));
|
||||
|
||||
// inside a chunk
|
||||
let (mut waypoints, before) = waypoints_with(&[3]);
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
waypoints.insert_at(1, vec![one]);
|
||||
assert_eq!(
|
||||
ids(&waypoints),
|
||||
[&before[..1], &[one_id], &before[1..]].concat()
|
||||
);
|
||||
|
||||
// at the end, or past it
|
||||
for index in [3, 100, usize::MAX] {
|
||||
let (mut waypoints, before) = waypoints_with(&[3]);
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
waypoints.insert_at(index, vec![one]);
|
||||
assert_eq!(ids(&waypoints), [before, vec![one_id]].concat());
|
||||
}
|
||||
|
||||
// in a file without waypoints
|
||||
let mut waypoints = Waypoints::default();
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
waypoints.insert_at(0, vec![one]);
|
||||
assert_eq!(ids(&waypoints), vec![one_id]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_replaces_waypoints() {
|
||||
let (mut waypoints, before) = waypoints_with(&[2, 2]);
|
||||
let rev = waypoints.rev_id;
|
||||
waypoints.splice(1, 3, vec![]);
|
||||
assert_eq!(ids(&waypoints), [before[0], before[3]]);
|
||||
assert_ne!(waypoints.rev_id, rev);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edit_changes_the_chunks_with_a_match_only() {
|
||||
let (mut waypoints, before) = waypoints_with(&[2, 2, 1]);
|
||||
let (first, last) = (waypoints.chunks()[0].clone(), waypoints.chunks()[2].clone());
|
||||
let rev = waypoints.rev_id;
|
||||
let target = before[2];
|
||||
let changed = waypoints.edit(
|
||||
|wpt| wpt.id == target,
|
||||
|wpts| {
|
||||
wpts.retain(|wpt| wpt.id != target);
|
||||
true
|
||||
},
|
||||
);
|
||||
assert!(changed);
|
||||
assert_ne!(waypoints.rev_id, rev);
|
||||
assert_eq!(ids(&waypoints), [&before[..2], &before[3..]].concat());
|
||||
// the chunks without a match are the same ones, the changed one is new
|
||||
assert!(Rc::ptr_eq(&waypoints.chunks()[0], &first));
|
||||
assert!(Rc::ptr_eq(&waypoints.chunks()[2], &last));
|
||||
assert_eq!(waypoints.chunks().len(), 3);
|
||||
|
||||
// a chunk left empty is dropped
|
||||
let (mut waypoints, before) = waypoints_with(&[1, 2]);
|
||||
waypoints.edit(
|
||||
|wpt| wpt.id == before[0],
|
||||
|wpts| {
|
||||
wpts.clear();
|
||||
true
|
||||
},
|
||||
);
|
||||
assert_eq!(waypoints.chunks().len(), 1);
|
||||
assert_eq!(ids(&waypoints), before[1..]);
|
||||
|
||||
// a chunk that is not changed is kept, and so is the revision
|
||||
let (mut waypoints, before) = waypoints_with(&[2]);
|
||||
let (chunk, rev) = (waypoints.chunks()[0].clone(), waypoints.rev_id);
|
||||
assert!(!waypoints.edit(|wpt| wpt.id == before[0], |_| false));
|
||||
assert!(Rc::ptr_eq(&waypoints.chunks()[0], &chunk));
|
||||
assert_eq!(waypoints.rev_id, rev);
|
||||
assert!(!waypoints.edit(|_| false, |_| true));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user