mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-09 23:26:31 +00:00
refactoring
This commit is contained in:
1 parent
594dd8979a
commit
df07892e76
29 files changed
+876
-747
No files matched your search
@@ -2,6 +2,29 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::{Trackpoint, Waypoint};
|
use crate::{Trackpoint, Waypoint};
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// Chunks are equal when they have the same identity, which is cheap to compare and changes
|
||||||
|
/// whenever the content does (a modified chunk is a new one).
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
const MAX_TRKPT_CHUNK_SIZE: usize = 4096;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
@@ -25,9 +48,23 @@ impl PartialEq for TrackpointChunk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TrackpointChunk {
|
impl Chunk for TrackpointChunk {
|
||||||
pub fn is_full(&self) -> bool {
|
type Item = Trackpoint;
|
||||||
self.trkpt.len() == MAX_TRKPT_CHUNK_SIZE
|
const MAX_SIZE: usize = MAX_TRKPT_CHUNK_SIZE;
|
||||||
|
|
||||||
|
fn new(trkpt: Vec<Trackpoint>) -> Self {
|
||||||
|
Self {
|
||||||
|
trkpt,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn items(&self) -> &Vec<Trackpoint> {
|
||||||
|
&self.trkpt
|
||||||
|
}
|
||||||
|
|
||||||
|
fn items_mut(&mut self) -> &mut Vec<Trackpoint> {
|
||||||
|
&mut self.trkpt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,9 +91,23 @@ impl PartialEq for WaypointChunk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WaypointChunk {
|
impl Chunk for WaypointChunk {
|
||||||
pub fn is_full(&self) -> bool {
|
type Item = Waypoint;
|
||||||
self.wpt.len() == MAX_WPT_CHUNK_SIZE
|
const MAX_SIZE: usize = MAX_WPT_CHUNK_SIZE;
|
||||||
|
|
||||||
|
fn new(wpt: Vec<Waypoint>) -> Self {
|
||||||
|
Self {
|
||||||
|
wpt,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn items(&self) -> &Vec<Waypoint> {
|
||||||
|
&self.wpt
|
||||||
|
}
|
||||||
|
|
||||||
|
fn items_mut(&mut self) -> &mut Vec<Waypoint> {
|
||||||
|
&mut self.wpt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +137,15 @@ mod tests {
|
|||||||
assert!(chunk.is_full());
|
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, b);
|
||||||
|
assert_eq!(a.items().len(), 1);
|
||||||
|
assert_ne!(WaypointChunk::new(vec![]), WaypointChunk::new(vec![]));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_chunk_equality_is_by_id() {
|
fn test_chunk_equality_is_by_id() {
|
||||||
let a = TrackpointChunk::default();
|
let a = TrackpointChunk::default();
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
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, PartialEq)]
|
||||||
|
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()),
|
||||||
|
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>,
|
||||||
|
idx: Option<ChunkIndex>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Chunk> Clone for ChunkedIter<'_, C> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
chunked: self.chunked,
|
||||||
|
idx: self.idx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, C: Chunk> ChunkedIter<'a, C> {
|
||||||
|
pub fn new(chunked: &'a Chunked<C>) -> Self {
|
||||||
|
Self {
|
||||||
|
chunked,
|
||||||
|
idx: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{Link, Track, WaypointChunk};
|
use crate::{Link, Track, Waypoints};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct FileId(pub Uuid);
|
pub struct FileId(pub Uuid);
|
||||||
@@ -13,22 +11,12 @@ impl Default for FileId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct FileWaypointsRevisionId(pub Uuid);
|
|
||||||
|
|
||||||
impl Default for FileWaypointsRevisionId {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self(Uuid::new_v4())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq)]
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
pub struct File {
|
pub struct File {
|
||||||
pub id: FileId,
|
pub id: FileId,
|
||||||
pub info: FileInfo,
|
pub info: FileInfo,
|
||||||
pub trk: Vec<Track>,
|
pub trk: Vec<Track>,
|
||||||
pub wpt: Vec<Rc<WaypointChunk>>,
|
pub wpt: Waypoints,
|
||||||
pub wpt_rev_id: FileWaypointsRevisionId,
|
|
||||||
// TODO routes
|
// TODO routes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
mod categories;
|
mod categories;
|
||||||
mod chunk;
|
mod chunk;
|
||||||
|
mod chunked;
|
||||||
mod common;
|
mod common;
|
||||||
mod file;
|
mod file;
|
||||||
mod segment;
|
mod segment;
|
||||||
mod track;
|
mod track;
|
||||||
mod trackpoint;
|
mod trackpoint;
|
||||||
mod waypoint;
|
mod waypoint;
|
||||||
|
mod waypoints;
|
||||||
|
|
||||||
pub use categories::*;
|
pub use categories::*;
|
||||||
pub use chunk::*;
|
pub use chunk::*;
|
||||||
|
pub use chunked::*;
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
pub use file::*;
|
pub use file::*;
|
||||||
pub use segment::*;
|
pub use segment::*;
|
||||||
pub use track::*;
|
pub use track::*;
|
||||||
pub use trackpoint::*;
|
pub use trackpoint::*;
|
||||||
pub use waypoint::*;
|
pub use waypoint::*;
|
||||||
|
pub use waypoints::*;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::{ops::Index, rc::Rc};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{Trackpoint, TrackpointChunk, compute_anchors};
|
use crate::{ChunkIndex, Chunked, ChunkedIter, Trackpoint, TrackpointChunk, compute_anchors};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct TrackSegmentId(pub Uuid);
|
pub struct TrackSegmentId(pub Uuid);
|
||||||
@@ -26,66 +26,33 @@ impl Default for TrackSegmentRevisionId {
|
|||||||
pub struct TrackSegment {
|
pub struct TrackSegment {
|
||||||
pub id: TrackSegmentId,
|
pub id: TrackSegmentId,
|
||||||
pub rev_id: TrackSegmentRevisionId,
|
pub rev_id: TrackSegmentRevisionId,
|
||||||
chunks: Vec<Rc<TrackpointChunk>>,
|
points: Chunked<TrackpointChunk>,
|
||||||
cumul_length: Vec<usize>,
|
}
|
||||||
|
|
||||||
|
/// 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 DerefMut for TrackSegment {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.points
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TrackSegment {
|
impl TrackSegment {
|
||||||
pub fn push(&mut self, chunk: TrackpointChunk) {
|
/// Replaces the points in `start..end` by `points`, see [`Chunked::splice`]. The first and
|
||||||
self.push_shared(Rc::new(chunk));
|
/// last trackpoints are anchors afterwards.
|
||||||
}
|
|
||||||
|
|
||||||
fn push_shared(&mut self, chunk: Rc<TrackpointChunk>) {
|
|
||||||
if chunk.trkpt.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.cumul_length
|
|
||||||
.push(self.cumul_length.last().copied().unwrap_or_default() + chunk.trkpt.len());
|
|
||||||
self.chunks.push(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replaces the points in `start..end` by `points`. Panics if the range is out of bounds.
|
|
||||||
///
|
|
||||||
/// Chunks that are not concerned are kept as they are (shared), only the chunks around the
|
|
||||||
/// range are copied and refilled.
|
|
||||||
pub fn splice(&mut self, start: usize, end: usize, points: Vec<Trackpoint>) {
|
pub fn splice(&mut self, start: usize, end: usize, points: Vec<Trackpoint>) {
|
||||||
assert!(
|
self.points.splice(start, end, points);
|
||||||
start <= end && end <= self.len(),
|
|
||||||
"splice range out of bounds"
|
|
||||||
);
|
|
||||||
let old = std::mem::take(&mut self.chunks);
|
|
||||||
self.cumul_length.clear();
|
|
||||||
|
|
||||||
let mut pending = TrackpointChunk::default();
|
|
||||||
let mut inserted = false;
|
|
||||||
let mut offset = 0;
|
|
||||||
for chunk in old {
|
|
||||||
let (lo, hi) = (offset, offset + chunk.trkpt.len());
|
|
||||||
offset = hi;
|
|
||||||
// a chunk ending at `start` is extended unless it is full, to avoid tiny chunks
|
|
||||||
if hi < start || (hi == start && chunk.is_full()) {
|
|
||||||
self.push_shared(chunk);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !inserted {
|
|
||||||
self.fill(&mut pending, chunk.trkpt[..start - lo].iter().cloned());
|
|
||||||
self.fill(&mut pending, points.iter().cloned());
|
|
||||||
inserted = true;
|
|
||||||
}
|
|
||||||
if hi <= end {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if lo >= end {
|
|
||||||
self.flush(&mut pending);
|
|
||||||
self.push_shared(chunk);
|
|
||||||
} else {
|
|
||||||
self.fill(&mut pending, chunk.trkpt[end - lo..].iter().cloned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !inserted {
|
|
||||||
self.fill(&mut pending, points);
|
|
||||||
}
|
|
||||||
self.flush(&mut pending);
|
|
||||||
self.ensure_end_anchors();
|
self.ensure_end_anchors();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +66,7 @@ impl TrackSegment {
|
|||||||
};
|
};
|
||||||
for index in [0, last] {
|
for index in [0, last] {
|
||||||
if self[index].anchor != Some(0) {
|
if self[index].anchor != Some(0) {
|
||||||
self.point_mut(index).anchor = Some(0);
|
self.set_anchor(index, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +74,7 @@ impl TrackSegment {
|
|||||||
/// Makes the trackpoint at `index` an anchor shown from the map zoom level `zoom`. Panics if
|
/// Makes the trackpoint at `index` an anchor shown from the map zoom level `zoom`. Panics if
|
||||||
/// there is no such trackpoint.
|
/// there is no such trackpoint.
|
||||||
pub fn set_anchor(&mut self, index: usize, zoom: u8) {
|
pub fn set_anchor(&mut self, index: usize, zoom: u8) {
|
||||||
self.point_mut(index).anchor = Some(zoom);
|
self.points.update(index, |trkpt| trkpt.anchor = Some(zoom));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the anchors of the trackpoints from the details of the path of the segment (see
|
/// Sets the anchors of the trackpoints from the details of the path of the segment (see
|
||||||
@@ -115,7 +82,7 @@ impl TrackSegment {
|
|||||||
pub fn compute_anchors(&mut self) {
|
pub fn compute_anchors(&mut self) {
|
||||||
let anchors = compute_anchors(self);
|
let anchors = compute_anchors(self);
|
||||||
let mut anchors = anchors.into_iter().peekable();
|
let mut anchors = anchors.into_iter().peekable();
|
||||||
self.map_points(|index, trkpt| {
|
self.points.update_all(|index, trkpt| {
|
||||||
trkpt.anchor = match anchors.peek() {
|
trkpt.anchor = match anchors.peek() {
|
||||||
Some(&(anchor, zoom)) if anchor == index => {
|
Some(&(anchor, zoom)) if anchor == index => {
|
||||||
anchors.next();
|
anchors.next();
|
||||||
@@ -125,191 +92,12 @@ impl TrackSegment {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A chunk that can be modified: the shared chunks are copied first.
|
|
||||||
fn chunk_mut(&mut self, chunk: usize) -> &mut TrackpointChunk {
|
|
||||||
let shared = &mut self.chunks[chunk];
|
|
||||||
if Rc::get_mut(shared).is_none() {
|
|
||||||
*shared = Rc::new(TrackpointChunk {
|
|
||||||
trkpt: shared.trkpt.clone(),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Rc::get_mut(shared).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn point_mut(&mut self, index: usize) -> &mut Trackpoint {
|
|
||||||
let TrackSegmentIndex { chunk, pos, .. } = self.locate(index).unwrap();
|
|
||||||
&mut self.chunk_mut(chunk).trkpt[pos]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Applies `f` to every trackpoint, with its index in the segment.
|
|
||||||
fn map_points(&mut self, mut f: impl FnMut(usize, &mut Trackpoint)) {
|
|
||||||
let mut offset = 0;
|
|
||||||
for chunk in 0..self.chunks.len() {
|
|
||||||
for (i, trkpt) in self.chunk_mut(chunk).trkpt.iter_mut().enumerate() {
|
|
||||||
f(offset + i, trkpt);
|
|
||||||
}
|
|
||||||
offset += self.chunks[chunk].trkpt.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fill(
|
|
||||||
&mut self,
|
|
||||||
pending: &mut TrackpointChunk,
|
|
||||||
points: impl IntoIterator<Item = Trackpoint>,
|
|
||||||
) {
|
|
||||||
for trkpt in points {
|
|
||||||
pending.trkpt.push(trkpt);
|
|
||||||
if pending.is_full() {
|
|
||||||
self.flush(pending);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self, pending: &mut TrackpointChunk) {
|
|
||||||
self.push(std::mem::take(pending));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.cumul_length.last().copied().unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.chunks.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iter(&self) -> TrackSegmentIterator<'_> {
|
|
||||||
TrackSegmentIterator::new(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn first_index(&self) -> Option<TrackSegmentIndex> {
|
|
||||||
self.next_index(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn last_index(&self) -> Option<TrackSegmentIndex> {
|
|
||||||
self.prev_index(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_index(&self, cur: Option<TrackSegmentIndex>) -> Option<TrackSegmentIndex> {
|
|
||||||
let mut next = cur.map_or_default(|idx| TrackSegmentIndex {
|
|
||||||
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].trkpt.len() {
|
|
||||||
next.chunk += 1;
|
|
||||||
next.pos = 0;
|
|
||||||
} else {
|
|
||||||
return Some(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prev_index(&self, cur: Option<TrackSegmentIndex>) -> Option<TrackSegmentIndex> {
|
|
||||||
let mut prev = cur.unwrap_or(TrackSegmentIndex {
|
|
||||||
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].trkpt.is_empty() {
|
|
||||||
prev.pos = self.chunks[prev.chunk].trkpt.len() - 1;
|
|
||||||
prev.flat -= 1;
|
|
||||||
return Some(prev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
prev.pos -= 1;
|
|
||||||
prev.flat -= 1;
|
|
||||||
Some(prev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn locate(&self, idx: usize) -> Option<TrackSegmentIndex> {
|
|
||||||
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].trkpt.len() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(TrackSegmentIndex {
|
|
||||||
chunk,
|
|
||||||
pos,
|
|
||||||
flat: idx,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Index<TrackSegmentIndex> for TrackSegment {
|
|
||||||
type Output = Trackpoint;
|
|
||||||
|
|
||||||
fn index(&self, idx: TrackSegmentIndex) -> &Self::Output {
|
|
||||||
&self.chunks[idx.chunk].trkpt[idx.pos]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Index<usize> for TrackSegment {
|
|
||||||
type Output = Trackpoint;
|
|
||||||
|
|
||||||
fn index(&self, idx: usize) -> &Self::Output {
|
|
||||||
&self[self.locate(idx).unwrap()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, PartialOrd, Ord)]
|
|
||||||
pub struct TrackSegmentIndex {
|
|
||||||
pub chunk: usize,
|
|
||||||
pub pos: usize,
|
|
||||||
pub flat: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TrackSegmentIterator<'a> {
|
|
||||||
trkseg: &'a TrackSegment,
|
|
||||||
idx: Option<TrackSegmentIndex>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> TrackSegmentIterator<'a> {
|
|
||||||
pub fn new(trkseg: &'a TrackSegment) -> Self {
|
|
||||||
Self {
|
|
||||||
trkseg,
|
|
||||||
idx: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Iterator for TrackSegmentIterator<'a> {
|
|
||||||
type Item = &'a Trackpoint;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
self.idx = self.trkseg.next_index(self.idx);
|
|
||||||
self.idx.map(|idx| &self.trkseg[idx])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
|
||||||
let idx = self.idx.map_or_default(|idx| idx.flat) + n;
|
|
||||||
self.idx = self.trkseg.locate(idx);
|
|
||||||
self.idx.map(|idx| &self.trkseg[idx])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn create_track_segment(nb_chunks: usize) -> TrackSegment {
|
fn create_track_segment(nb_chunks: usize) -> TrackSegment {
|
||||||
@@ -427,14 +215,14 @@ mod tests {
|
|||||||
let mut trkseg = create_track_segment(5);
|
let mut trkseg = create_track_segment(5);
|
||||||
// the ends are anchors already, or their chunks would be copied
|
// the ends are anchors already, or their chunks would be copied
|
||||||
trkseg.ensure_end_anchors();
|
trkseg.ensure_end_anchors();
|
||||||
let before = trkseg.chunks.clone();
|
let before = trkseg.chunks().to_vec();
|
||||||
// inside the third chunk only
|
// inside the third chunk only
|
||||||
trkseg.splice(4, 5, points(&[-1.0]));
|
trkseg.splice(4, 5, points(&[-1.0]));
|
||||||
assert!(Rc::ptr_eq(&trkseg.chunks[0], &before[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()[1], &before[1]));
|
||||||
assert!(Rc::ptr_eq(trkseg.chunks.last().unwrap(), &before[4]));
|
assert!(Rc::ptr_eq(trkseg.chunks().last().unwrap(), &before[4]));
|
||||||
assert!(Rc::ptr_eq(
|
assert!(Rc::ptr_eq(
|
||||||
&trkseg.chunks[trkseg.chunks.len() - 2],
|
&trkseg.chunks()[trkseg.chunks().len() - 2],
|
||||||
&before[3]
|
&before[3]
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -447,10 +235,10 @@ mod tests {
|
|||||||
trkseg.splice(len, len, points(&[i as f64]));
|
trkseg.splice(len, len, points(&[i as f64]));
|
||||||
}
|
}
|
||||||
assert_eq!(trkseg.len(), 10_000);
|
assert_eq!(trkseg.len(), 10_000);
|
||||||
assert!(trkseg.chunks.len() <= 3);
|
assert!(trkseg.chunks().len() <= 3);
|
||||||
assert!(trkseg.chunks.iter().all(|c| c.trkpt.len() <= 4096));
|
assert!(trkseg.chunks().iter().all(|c| c.trkpt.len() <= 4096));
|
||||||
assert_eq!(trkseg[9_999].ele, 9_999.0);
|
assert_eq!(trkseg[9_999].ele, 9_999.0);
|
||||||
assert_eq!(*trkseg.cumul_length.last().unwrap(), 10_000);
|
assert_eq!(trkseg.len(), 10_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -496,7 +284,7 @@ mod tests {
|
|||||||
let nb_chunks = 10;
|
let nb_chunks = 10;
|
||||||
let trkseg = create_track_segment(nb_chunks);
|
let trkseg = create_track_segment(nb_chunks);
|
||||||
let _ = trkseg[TrackSegmentIndex {
|
let _ = trkseg[TrackSegmentIndex {
|
||||||
chunk: trkseg.chunks.len(),
|
chunk: trkseg.chunks().len(),
|
||||||
pos: 0,
|
pos: 0,
|
||||||
flat: 0,
|
flat: 0,
|
||||||
}];
|
}];
|
||||||
|
|||||||
@@ -0,0 +1,316 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
impl Default for FileWaypointsRevisionId {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Uuid::new_v4())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The waypoints of a file, in chunks that are shared between the versions of the file (see
|
||||||
|
/// [`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, PartialEq)]
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Author, File, Link, LngLat, Track, TrackSegment, Trackpoint, TrackpointCategories,
|
Author, Chunk, File, Link, LngLat, Track, TrackSegment, Trackpoint, TrackpointCategories,
|
||||||
TrackpointChunk, Waypoint, WaypointChunk,
|
TrackpointChunk, Waypoint, WaypointChunk,
|
||||||
};
|
};
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
@@ -133,7 +131,7 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
if wpt_chunk.is_full() {
|
if wpt_chunk.is_full() {
|
||||||
gpx.wpt.push(Rc::new(std::mem::take(&mut wpt_chunk)));
|
gpx.wpt.push(std::mem::take(&mut wpt_chunk));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
@@ -141,7 +139,7 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
|
|||||||
Ok(Event::End(e)) => match e.name().as_ref() {
|
Ok(Event::End(e)) => match e.name().as_ref() {
|
||||||
"gpx" => {
|
"gpx" => {
|
||||||
if !wpt_chunk.wpt.is_empty() {
|
if !wpt_chunk.wpt.is_empty() {
|
||||||
gpx.wpt.push(Rc::new(std::mem::take(&mut wpt_chunk)));
|
gpx.wpt.push(std::mem::take(&mut wpt_chunk));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"metadata" => {
|
"metadata" => {
|
||||||
@@ -197,7 +195,7 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
|
|||||||
if let Some(GPXElement::Waypoint(wpt)) = stack.pop() {
|
if let Some(GPXElement::Waypoint(wpt)) = stack.pop() {
|
||||||
wpt_chunk.wpt.push(wpt);
|
wpt_chunk.wpt.push(wpt);
|
||||||
if wpt_chunk.is_full() {
|
if wpt_chunk.is_full() {
|
||||||
gpx.wpt.push(Rc::new(std::mem::take(&mut wpt_chunk)));
|
gpx.wpt.push(std::mem::take(&mut wpt_chunk));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -393,7 +391,7 @@ mod tests {
|
|||||||
let gpx = parse_data("self_closing_points");
|
let gpx = parse_data("self_closing_points");
|
||||||
|
|
||||||
assert_eq!(gpx.info.name, "self closing points");
|
assert_eq!(gpx.info.name, "self closing points");
|
||||||
let wpt: Vec<_> = gpx.wpt.iter().flat_map(|chunk| &chunk.wpt).collect();
|
let wpt: Vec<_> = gpx.wpt.iter().collect();
|
||||||
assert_eq!(wpt.len(), 3);
|
assert_eq!(wpt.len(), 3);
|
||||||
assert_eq!(wpt[0].coordinates.lat, 50.0);
|
assert_eq!(wpt[0].coordinates.lat, 50.0);
|
||||||
assert_eq!(wpt[0].name, None);
|
assert_eq!(wpt[0].name, None);
|
||||||
@@ -550,9 +548,7 @@ mod tests {
|
|||||||
let gpx = parse_data("with_waypoint");
|
let gpx = parse_data("with_waypoint");
|
||||||
|
|
||||||
assert_eq!(gpx.wpt.len(), 1);
|
assert_eq!(gpx.wpt.len(), 1);
|
||||||
let chunk = &gpx.wpt[0];
|
let wpt = &gpx.wpt[0];
|
||||||
assert_eq!(chunk.wpt.len(), 1);
|
|
||||||
let wpt = &chunk.wpt[0];
|
|
||||||
assert_eq!(wpt.coordinates.lat, 50.7836710064975);
|
assert_eq!(wpt.coordinates.lat, 50.7836710064975);
|
||||||
assert_eq!(wpt.coordinates.lng, 4.410764082658738);
|
assert_eq!(wpt.coordinates.lng, 4.410764082658738);
|
||||||
assert!(wpt.name.as_ref().is_some_and(|n| n == "waypoint name"));
|
assert!(wpt.name.as_ref().is_some_and(|n| n == "waypoint name"));
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{Apply, CommandError, FileId, Selection, StackEntry, State, Waypoint};
|
||||||
Apply, CommandError, FileId, Selection, StackEntry, State, Waypoint, edit_waypoint_chunks,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Deletes the selected elements. With `whole_files`, the files holding the selected elements
|
/// Deletes the selected elements. With `whole_files`, the files holding the selected elements
|
||||||
/// are deleted instead, even if only a track or a waypoint is selected.
|
/// are deleted instead, even if only a track or a waypoint is selected.
|
||||||
@@ -83,7 +81,7 @@ pub(crate) fn delete_waypoints(
|
|||||||
) -> Result<Selection, CommandError> {
|
) -> Result<Selection, CommandError> {
|
||||||
let file = files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
let file = files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||||
let mut file = (**file).clone();
|
let mut file = (**file).clone();
|
||||||
let changed = edit_waypoint_chunks(&mut file, &filter, |wpts| {
|
let changed = file.wpt.edit(&filter, |wpts| {
|
||||||
wpts.retain(|wpt| !filter(wpt));
|
wpts.retain(|wpt| !filter(wpt));
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
@@ -191,10 +189,10 @@ mod tests {
|
|||||||
let wpts: Vec<_> = (0..n).map(|_| Waypoint::default()).collect();
|
let wpts: Vec<_> = (0..n).map(|_| Waypoint::default()).collect();
|
||||||
let ids = wpts.iter().map(|w| w.id).collect();
|
let ids = wpts.iter().map(|w| w.id).collect();
|
||||||
let mut file = (*fx.files[&id]).clone();
|
let mut file = (*fx.files[&id]).clone();
|
||||||
file.wpt = vec![Rc::new(crate::WaypointChunk {
|
file.wpt = crate::Waypoints::new([crate::WaypointChunk {
|
||||||
wpt: wpts,
|
wpt: wpts,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})];
|
}]);
|
||||||
fx.files.insert(id, Rc::new(file));
|
fx.files.insert(id, Rc::new(file));
|
||||||
ids
|
ids
|
||||||
}
|
}
|
||||||
@@ -207,16 +205,12 @@ mod tests {
|
|||||||
file_id: id,
|
file_id: id,
|
||||||
wpt_ids: HashSet::from([ids[1]]),
|
wpt_ids: HashSet::from([ids[1]]),
|
||||||
};
|
};
|
||||||
let rev = fx.files[&id].wpt_rev_id;
|
let rev = fx.files[&id].wpt.rev_id;
|
||||||
Delete { whole_files: false }
|
Delete { whole_files: false }
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_ne!(fx.files[&id].wpt_rev_id, rev);
|
assert_ne!(fx.files[&id].wpt.rev_id, rev);
|
||||||
let left: Vec<_> = fx.files[&id]
|
let left: Vec<_> = fx.files[&id].wpt.iter().map(|w| w.id).collect();
|
||||||
.wpt
|
|
||||||
.iter()
|
|
||||||
.flat_map(|c| c.wpt.iter().map(|w| w.id))
|
|
||||||
.collect();
|
|
||||||
assert_eq!(left, vec![ids[0], ids[2]]);
|
assert_eq!(left, vec![ids[0], ids[2]]);
|
||||||
assert_eq!(fx.selected_files(), HashSet::from([id]));
|
assert_eq!(fx.selected_files(), HashSet::from([id]));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ mod tests {
|
|||||||
fn test_deletes_one_waypoint_without_touching_the_selection() {
|
fn test_deletes_one_waypoint_without_touching_the_selection() {
|
||||||
let mut fx = Fixture::default();
|
let mut fx = Fixture::default();
|
||||||
let mut file = File::default();
|
let mut file = File::default();
|
||||||
file.wpt.push(Rc::new(WaypointChunk {
|
file.wpt.push(WaypointChunk {
|
||||||
wpt: (0..3)
|
wpt: (0..3)
|
||||||
.map(|i| Waypoint {
|
.map(|i| Waypoint {
|
||||||
name: Some(format!("w{i}")),
|
name: Some(format!("w{i}")),
|
||||||
@@ -45,7 +45,7 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
});
|
||||||
let id = file.id;
|
let id = file.id;
|
||||||
let ids: Vec<_> = waypoint_ids(&file).collect();
|
let ids: Vec<_> = waypoint_ids(&file).collect();
|
||||||
fx.files.insert(id, Rc::new(file));
|
fx.files.insert(id, Rc::new(file));
|
||||||
@@ -64,7 +64,6 @@ mod tests {
|
|||||||
let names: Vec<_> = fx.files[&id]
|
let names: Vec<_> = fx.files[&id]
|
||||||
.wpt
|
.wpt
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|chunk| &chunk.wpt)
|
|
||||||
.map(|wpt| wpt.name.clone().unwrap())
|
.map(|wpt| wpt.name.clone().unwrap())
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(names, ["w0", "w2"]);
|
assert_eq!(names, ["w0", "w2"]);
|
||||||
@@ -80,10 +79,10 @@ mod tests {
|
|||||||
fn test_deleting_the_selected_waypoint_selects_its_file() {
|
fn test_deleting_the_selected_waypoint_selects_its_file() {
|
||||||
let mut fx = Fixture::default();
|
let mut fx = Fixture::default();
|
||||||
let mut file = File::default();
|
let mut file = File::default();
|
||||||
file.wpt.push(Rc::new(WaypointChunk {
|
file.wpt.push(WaypointChunk {
|
||||||
wpt: vec![Waypoint::default(), Waypoint::default()],
|
wpt: vec![Waypoint::default(), Waypoint::default()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
});
|
||||||
let id = file.id;
|
let id = file.id;
|
||||||
let ids: Vec<_> = waypoint_ids(&file).collect();
|
let ids: Vec<_> = waypoint_ids(&file).collect();
|
||||||
fx.files.insert(id, Rc::new(file));
|
fx.files.insert(id, Rc::new(file));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::{collections::HashSet, rc::Rc};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, CommandError, FileId, Selection, StackEntry, State, Waypoint, copy_file, copy_segment,
|
Apply, CommandError, FileId, Selection, StackEntry, State, Waypoint, copy_file, copy_segment,
|
||||||
copy_track, copy_waypoint, insert_waypoints,
|
copy_track, copy_waypoint,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -112,19 +112,14 @@ fn duplicate_waypoints(
|
|||||||
filter: impl Fn(&Waypoint) -> bool,
|
filter: impl Fn(&Waypoint) -> bool,
|
||||||
) -> Result<Selection, CommandError> {
|
) -> Result<Selection, CommandError> {
|
||||||
let file = files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
let file = files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||||
let selected: Vec<&Waypoint> = file
|
let selected: Vec<&Waypoint> = file.wpt.iter().filter(|wpt| filter(wpt)).collect();
|
||||||
.wpt
|
|
||||||
.iter()
|
|
||||||
.flat_map(|chunk| &chunk.wpt)
|
|
||||||
.filter(|wpt| filter(wpt))
|
|
||||||
.collect();
|
|
||||||
let last = selected.last().ok_or(CommandError::NothingToDo)?.id;
|
let last = selected.last().ok_or(CommandError::NothingToDo)?.id;
|
||||||
let copies: Vec<Waypoint> = selected.into_iter().map(copy_waypoint).collect();
|
let copies: Vec<Waypoint> = selected.into_iter().map(copy_waypoint).collect();
|
||||||
let copy_ids = copies.iter().map(|wpt| wpt.id).collect();
|
let copy_ids = copies.iter().map(|wpt| wpt.id).collect();
|
||||||
|
|
||||||
// The copies go right after the last selected waypoint.
|
// The copies go right after the last selected waypoint.
|
||||||
let mut file = (**file).clone();
|
let mut file = (**file).clone();
|
||||||
insert_waypoints(&mut file, Some(last), copies);
|
file.wpt.insert_after(Some(last), copies);
|
||||||
files.insert(file_id, Rc::new(file));
|
files.insert(file_id, Rc::new(file));
|
||||||
Ok(Selection::Waypoint {
|
Ok(Selection::Waypoint {
|
||||||
file_id,
|
file_id,
|
||||||
@@ -243,57 +238,44 @@ mod tests {
|
|||||||
let wpts: Vec<_> = (0..3).map(|_| Waypoint::default()).collect();
|
let wpts: Vec<_> = (0..3).map(|_| Waypoint::default()).collect();
|
||||||
let ids: Vec<_> = wpts.iter().map(|w| w.id).collect();
|
let ids: Vec<_> = wpts.iter().map(|w| w.id).collect();
|
||||||
let mut file = (*fx.files[&id]).clone();
|
let mut file = (*fx.files[&id]).clone();
|
||||||
file.wpt = vec![Rc::new(crate::WaypointChunk {
|
file.wpt = crate::Waypoints::new([crate::WaypointChunk {
|
||||||
wpt: wpts,
|
wpt: wpts,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})];
|
}]);
|
||||||
fx.files.insert(id, Rc::new(file));
|
fx.files.insert(id, Rc::new(file));
|
||||||
let original_chunk = fx.files[&id].wpt[0].clone();
|
let original_chunk = fx.files[&id].wpt.chunks()[0].clone();
|
||||||
|
|
||||||
fx.selection = Selection::Waypoint {
|
fx.selection = Selection::Waypoint {
|
||||||
file_id: id,
|
file_id: id,
|
||||||
wpt_ids: HashSet::from([ids[0]]),
|
wpt_ids: HashSet::from([ids[0]]),
|
||||||
};
|
};
|
||||||
let rev = fx.files[&id].wpt_rev_id;
|
let rev = fx.files[&id].wpt.rev_id;
|
||||||
Duplicate.apply(&mut fx.state()).unwrap();
|
Duplicate.apply(&mut fx.state()).unwrap();
|
||||||
assert_ne!(fx.files[&id].wpt_rev_id, rev);
|
assert_ne!(fx.files[&id].wpt.rev_id, rev);
|
||||||
let all: Vec<_> = fx.files[&id]
|
let all: Vec<_> = fx.files[&id].wpt.iter().map(|w| w.id).collect();
|
||||||
.wpt
|
|
||||||
.iter()
|
|
||||||
.flat_map(|c| c.wpt.iter().map(|w| w.id))
|
|
||||||
.collect();
|
|
||||||
assert_eq!(all.len(), 4);
|
assert_eq!(all.len(), 4);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&fx.selection, Selection::Waypoint { wpt_ids, .. } if wpt_ids == &HashSet::from([all[1]]))
|
matches!(&fx.selection, Selection::Waypoint { wpt_ids, .. } if wpt_ids == &HashSet::from([all[1]]))
|
||||||
);
|
);
|
||||||
// the chunk is cut after the selected waypoint: [w0] [copy] [w1 w2]
|
// the copy comes right after the selected waypoint, in a new chunk
|
||||||
assert_eq!(all[..], [ids[0], all[1], ids[1], ids[2]]);
|
assert_eq!(all[..], [ids[0], all[1], ids[1], ids[2]]);
|
||||||
assert_eq!(fx.files[&id].wpt.len(), 3);
|
assert_eq!(fx.files[&id].wpt.chunks().len(), 1);
|
||||||
assert_ne!(fx.files[&id].wpt[0].id, original_chunk.id);
|
assert_ne!(fx.files[&id].wpt.chunks()[0].id, original_chunk.id);
|
||||||
|
|
||||||
// selecting the last waypoint of a chunk does not cut it
|
// selecting the last waypoint: the copy goes at the end
|
||||||
let last = fx.files[&id].wpt.last().unwrap().wpt.last().unwrap().id;
|
let last = fx.files[&id].wpt.iter().last().unwrap().id;
|
||||||
let chunks = fx.files[&id].wpt.clone();
|
|
||||||
fx.selection = Selection::Waypoint {
|
fx.selection = Selection::Waypoint {
|
||||||
file_id: id,
|
file_id: id,
|
||||||
wpt_ids: HashSet::from([last]),
|
wpt_ids: HashSet::from([last]),
|
||||||
};
|
};
|
||||||
Duplicate.apply(&mut fx.state()).unwrap();
|
Duplicate.apply(&mut fx.state()).unwrap();
|
||||||
let file = &fx.files[&id];
|
let all: Vec<_> = fx.files[&id].wpt.iter().map(|w| w.id).collect();
|
||||||
assert_eq!(file.wpt.len(), 4);
|
assert_eq!(all.len(), 5);
|
||||||
assert!(
|
assert_eq!(all[..4], [ids[0], all[1], ids[1], ids[2]]);
|
||||||
file.wpt[..3]
|
|
||||||
.iter()
|
|
||||||
.zip(&chunks)
|
|
||||||
.all(|(a, b)| Rc::ptr_eq(a, b))
|
|
||||||
);
|
|
||||||
|
|
||||||
fx.selection = Selection::Waypoints { file_id: id };
|
fx.selection = Selection::Waypoints { file_id: id };
|
||||||
Duplicate.apply(&mut fx.state()).unwrap();
|
Duplicate.apply(&mut fx.state()).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(fx.files[&id].wpt.len(), 10);
|
||||||
fx.files[&id].wpt.iter().map(|c| c.wpt.len()).sum::<usize>(),
|
|
||||||
10
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ mod tests {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
File, Track, TrackInfo, TrackSegment, TrackSegmentId, Waypoint, WaypointChunk, WaypointId,
|
File, Track, TrackInfo, TrackSegment, TrackSegmentId, Waypoint, WaypointChunk, WaypointId,
|
||||||
engine::command::fixture::Fixture, new_file, waypoint_ids,
|
Waypoints, engine::command::fixture::Fixture, new_file, waypoint_ids,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -93,10 +93,10 @@ mod tests {
|
|||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
if waypoints > 0 {
|
if waypoints > 0 {
|
||||||
file.wpt = vec![Rc::new(WaypointChunk {
|
file.wpt = Waypoints::new([WaypointChunk {
|
||||||
wpt: (0..waypoints).map(|_| Waypoint::default()).collect(),
|
wpt: (0..waypoints).map(|_| Waypoint::default()).collect(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})];
|
}]);
|
||||||
}
|
}
|
||||||
file
|
file
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ mod tests {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Clipboard, ClipboardIds, File, FileId, Track, TrackId, TrackInfo, TrackSegment,
|
Clipboard, ClipboardIds, File, FileId, Track, TrackId, TrackInfo, TrackSegment,
|
||||||
TrackSegmentId, Waypoint, WaypointId, engine::command::fixture::Fixture, new_file,
|
TrackSegmentId, Waypoint, WaypointId, Waypoints, engine::command::fixture::Fixture,
|
||||||
|
new_file,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -162,10 +163,10 @@ mod tests {
|
|||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
if waypoints > 0 {
|
if waypoints > 0 {
|
||||||
file.wpt = vec![Rc::new(crate::WaypointChunk {
|
file.wpt = Waypoints::new([crate::WaypointChunk {
|
||||||
wpt: (0..waypoints).map(|_| Waypoint::default()).collect(),
|
wpt: (0..waypoints).map(|_| Waypoint::default()).collect(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})];
|
}]);
|
||||||
}
|
}
|
||||||
file
|
file
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::{collections::HashSet, rc::Rc};
|
|||||||
use crate::{
|
use crate::{
|
||||||
ClipboardContent, ClipboardSegment, ClipboardTrack, CommandError, File, FileId, Selection,
|
ClipboardContent, ClipboardSegment, ClipboardTrack, CommandError, File, FileId, Selection,
|
||||||
State, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint, WaypointId, copy_file,
|
State, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint, WaypointId, copy_file,
|
||||||
copy_segment, copy_track, copy_waypoint, edit_waypoint_chunks, insert_waypoints_at,
|
copy_segment, copy_track, copy_waypoint,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Where elements are put, among the others of the same list.
|
/// Where elements are put, among the others of the same list.
|
||||||
@@ -52,10 +52,7 @@ pub enum Destination {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn waypoint_ids(file: &File) -> impl Iterator<Item = WaypointId> + '_ {
|
pub fn waypoint_ids(file: &File) -> impl Iterator<Item = WaypointId> + '_ {
|
||||||
file.wpt
|
file.wpt.iter().map(|wpt| wpt.id)
|
||||||
.iter()
|
|
||||||
.flat_map(|chunk| &chunk.wpt)
|
|
||||||
.map(|wpt| wpt.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_mut<'a>(state: &'a mut State, id: FileId) -> Result<&'a mut File, CommandError> {
|
fn file_mut<'a>(state: &'a mut State, id: FileId) -> Result<&'a mut File, CommandError> {
|
||||||
@@ -119,8 +116,7 @@ pub fn remove_elements(state: &mut State, content: &ClipboardContent) {
|
|||||||
let ids: HashSet<WaypointId> = waypoints.iter().map(|wpt| wpt.id).collect();
|
let ids: HashSet<WaypointId> = waypoints.iter().map(|wpt| wpt.id).collect();
|
||||||
for file in state.files.values_mut() {
|
for file in state.files.values_mut() {
|
||||||
if waypoint_ids(file).any(|id| ids.contains(&id)) {
|
if waypoint_ids(file).any(|id| ids.contains(&id)) {
|
||||||
edit_waypoint_chunks(
|
Rc::make_mut(file).wpt.edit(
|
||||||
Rc::make_mut(file),
|
|
||||||
|wpt| ids.contains(&wpt.id),
|
|wpt| ids.contains(&wpt.id),
|
||||||
|wpts| {
|
|wpts| {
|
||||||
wpts.retain(|wpt| !ids.contains(&wpt.id));
|
wpts.retain(|wpt| !ids.contains(&wpt.id));
|
||||||
@@ -334,7 +330,7 @@ fn transfer_waypoints(
|
|||||||
let ids = waypoints.iter().map(|wpt| wpt.id).collect();
|
let ids = waypoints.iter().map(|wpt| wpt.id).collect();
|
||||||
let file = file_mut(state, file_id)?;
|
let file = file_mut(state, file_id)?;
|
||||||
let existing: Vec<WaypointId> = waypoint_ids(file).collect();
|
let existing: Vec<WaypointId> = waypoint_ids(file).collect();
|
||||||
insert_waypoints_at(file, place.position(&existing), waypoints);
|
file.wpt.insert_at(place.position(&existing), waypoints);
|
||||||
Ok(Selection::Waypoint {
|
Ok(Selection::Waypoint {
|
||||||
file_id,
|
file_id,
|
||||||
wpt_ids: ids,
|
wpt_ids: ids,
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use crate::{File, Waypoint, WaypointChunk};
|
|
||||||
|
|
||||||
/// Edits the waypoints of the chunks containing a waypoint accepted by `filter`.
|
|
||||||
///
|
|
||||||
/// `f` receives a copy of the chunk's waypoints and returns whether it changed them. A
|
|
||||||
/// changed chunk is replaced by a new one (with a new id, so that derived data is recomputed)
|
|
||||||
/// and chunks left empty are dropped. Returns whether anything changed.
|
|
||||||
pub fn edit_waypoint_chunks(
|
|
||||||
file: &mut File,
|
|
||||||
filter: impl Fn(&Waypoint) -> bool,
|
|
||||||
mut f: impl FnMut(&mut Vec<Waypoint>) -> bool,
|
|
||||||
) -> bool {
|
|
||||||
let mut changed = false;
|
|
||||||
let mut chunks = Vec::with_capacity(file.wpt.len());
|
|
||||||
for chunk in file.wpt.drain(..) {
|
|
||||||
if !chunk.wpt.iter().any(&filter) {
|
|
||||||
chunks.push(chunk);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut wpt = chunk.wpt.clone();
|
|
||||||
if !f(&mut wpt) {
|
|
||||||
chunks.push(chunk);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
changed = true;
|
|
||||||
if !wpt.is_empty() {
|
|
||||||
chunks.push(Rc::new(WaypointChunk {
|
|
||||||
wpt,
|
|
||||||
..Default::default()
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file.wpt = chunks;
|
|
||||||
if changed {
|
|
||||||
file.wpt_rev_id = Default::default();
|
|
||||||
}
|
|
||||||
changed
|
|
||||||
}
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use crate::{File, Waypoint, WaypointChunk, WaypointId};
|
|
||||||
|
|
||||||
/// Inserts waypoints in the file, right after the waypoint `after`, or at the end if there is
|
|
||||||
/// none (or if it is not in the file).
|
|
||||||
///
|
|
||||||
/// See [`insert_waypoints_at`].
|
|
||||||
pub fn insert_waypoints(file: &mut File, after: Option<WaypointId>, waypoints: Vec<Waypoint>) {
|
|
||||||
let index = after
|
|
||||||
.and_then(|after| {
|
|
||||||
file.wpt
|
|
||||||
.iter()
|
|
||||||
.flat_map(|chunk| &chunk.wpt)
|
|
||||||
.position(|wpt| wpt.id == after)
|
|
||||||
})
|
|
||||||
.map_or(usize::MAX, |i| i + 1);
|
|
||||||
insert_waypoints_at(file, index, waypoints);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Inserts waypoints in the file, so that the first one is at `index` among the waypoints of
|
|
||||||
/// the file (at the end if the index is past it).
|
|
||||||
///
|
|
||||||
/// Only the chunk holding the waypoint that was at `index` is cut (when the waypoints are not
|
|
||||||
/// inserted between two chunks), all the other chunks are kept as they are.
|
|
||||||
pub fn insert_waypoints_at(file: &mut File, index: usize, waypoints: Vec<Waypoint>) {
|
|
||||||
if waypoints.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut inserted = Vec::new();
|
|
||||||
let mut chunk = WaypointChunk::default();
|
|
||||||
for wpt in waypoints {
|
|
||||||
chunk.wpt.push(wpt);
|
|
||||||
if chunk.is_full() {
|
|
||||||
inserted.push(Rc::new(std::mem::take(&mut chunk)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !chunk.wpt.is_empty() {
|
|
||||||
inserted.push(Rc::new(chunk));
|
|
||||||
}
|
|
||||||
|
|
||||||
// the chunk holding the waypoint at `index`, and the position of that waypoint in it
|
|
||||||
let mut start = 0;
|
|
||||||
let position = file.wpt.iter().enumerate().find_map(|(i, chunk)| {
|
|
||||||
let offset = index
|
|
||||||
.checked_sub(start)
|
|
||||||
.filter(|offset| *offset < chunk.wpt.len());
|
|
||||||
start += chunk.wpt.len();
|
|
||||||
offset.map(|offset| (i, offset))
|
|
||||||
});
|
|
||||||
match position {
|
|
||||||
None => file.wpt.extend(inserted),
|
|
||||||
Some((i, 0)) => {
|
|
||||||
file.wpt.splice(i..i, inserted);
|
|
||||||
}
|
|
||||||
Some((i, offset)) => {
|
|
||||||
let cut = &file.wpt[i];
|
|
||||||
let part = |wpt: &[Waypoint]| {
|
|
||||||
Rc::new(WaypointChunk {
|
|
||||||
wpt: wpt.to_vec(),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let mut chunks = vec![part(&cut.wpt[..offset])];
|
|
||||||
chunks.extend(inserted);
|
|
||||||
chunks.push(part(&cut.wpt[offset..]));
|
|
||||||
file.wpt.splice(i..=i, chunks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file.wpt_rev_id = Default::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn waypoint() -> Waypoint {
|
|
||||||
Waypoint::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn file_with(chunks: &[usize]) -> (File, Vec<WaypointId>) {
|
|
||||||
let mut file = File::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));
|
|
||||||
file.wpt.push(Rc::new(WaypointChunk {
|
|
||||||
wpt,
|
|
||||||
..Default::default()
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
(file, ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ids(file: &File) -> Vec<WaypointId> {
|
|
||||||
file.wpt
|
|
||||||
.iter()
|
|
||||||
.flat_map(|chunk| chunk.wpt.iter().map(|w| w.id))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_insert_at_the_end() {
|
|
||||||
let (mut file, before) = file_with(&[2, 1]);
|
|
||||||
let rev = file.wpt_rev_id;
|
|
||||||
let new = vec![waypoint(), waypoint()];
|
|
||||||
let new_ids: Vec<_> = new.iter().map(|w| w.id).collect();
|
|
||||||
insert_waypoints(&mut file, None, new);
|
|
||||||
assert_eq!(ids(&file), [before.clone(), new_ids].concat());
|
|
||||||
// the chunks that were there are kept
|
|
||||||
assert_eq!(file.wpt.len(), 3);
|
|
||||||
assert_ne!(file.wpt_rev_id, rev);
|
|
||||||
// an unknown waypoint is an insertion at the end too
|
|
||||||
let (mut file, before) = file_with(&[2]);
|
|
||||||
let one = waypoint();
|
|
||||||
let one_id = one.id;
|
|
||||||
insert_waypoints(&mut file, Some(WaypointId::default()), vec![one]);
|
|
||||||
assert_eq!(ids(&file), [before, vec![one_id]].concat());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_insert_after_the_last_waypoint_of_a_chunk_keeps_the_chunks() {
|
|
||||||
let (mut file, before) = file_with(&[2, 2]);
|
|
||||||
let first = file.wpt[0].clone();
|
|
||||||
let one = waypoint();
|
|
||||||
let one_id = one.id;
|
|
||||||
insert_waypoints(&mut file, Some(before[1]), vec![one]);
|
|
||||||
assert_eq!(ids(&file), [&before[..2], &[one_id], &before[2..]].concat());
|
|
||||||
// 2 chunks + the inserted one, nothing was cut
|
|
||||||
assert_eq!(file.wpt.len(), 3);
|
|
||||||
assert!(Rc::ptr_eq(&file.wpt[0], &first));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_insert_in_the_middle_of_a_chunk_cuts_it() {
|
|
||||||
let (mut file, before) = file_with(&[3, 1]);
|
|
||||||
let last = file.wpt[1].clone();
|
|
||||||
let new = vec![waypoint(), waypoint()];
|
|
||||||
let new_ids: Vec<_> = new.iter().map(|w| w.id).collect();
|
|
||||||
insert_waypoints(&mut file, Some(before[0]), new);
|
|
||||||
assert_eq!(
|
|
||||||
ids(&file),
|
|
||||||
[&before[..1], &new_ids[..], &before[1..]].concat()
|
|
||||||
);
|
|
||||||
// the cut chunk, the inserted one and the end of the cut one, then the untouched chunk
|
|
||||||
assert_eq!(file.wpt.len(), 4);
|
|
||||||
assert!(Rc::ptr_eq(&file.wpt[3], &last));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_insert_nothing() {
|
|
||||||
let (mut file, before) = file_with(&[2]);
|
|
||||||
let rev = file.wpt_rev_id;
|
|
||||||
insert_waypoints(&mut file, Some(before[0]), vec![]);
|
|
||||||
assert_eq!(ids(&file), before);
|
|
||||||
assert_eq!(file.wpt_rev_id, rev);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_insert_many_fills_chunks() {
|
|
||||||
let mut file = File::default();
|
|
||||||
let new: Vec<_> = (0..300).map(|_| waypoint()).collect();
|
|
||||||
let new_ids: Vec<_> = new.iter().map(|w| w.id).collect();
|
|
||||||
insert_waypoints(&mut file, None, new);
|
|
||||||
assert_eq!(ids(&file), new_ids);
|
|
||||||
assert_eq!(file.wpt.len(), 3);
|
|
||||||
assert!(file.wpt[..2].iter().all(|chunk| chunk.is_full()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_insert_at_an_index() {
|
|
||||||
// at the start
|
|
||||||
let (mut file, before) = file_with(&[2, 1]);
|
|
||||||
let first = file.wpt[0].clone();
|
|
||||||
let one = waypoint();
|
|
||||||
let one_id = one.id;
|
|
||||||
insert_waypoints_at(&mut file, 0, vec![one]);
|
|
||||||
assert_eq!(ids(&file), [vec![one_id], before.clone()].concat());
|
|
||||||
// the existing chunks are kept
|
|
||||||
assert_eq!(file.wpt.len(), 3);
|
|
||||||
assert!(Rc::ptr_eq(&file.wpt[1], &first));
|
|
||||||
|
|
||||||
// between two chunks
|
|
||||||
let (mut file, before) = file_with(&[2, 1]);
|
|
||||||
let (first, last) = (file.wpt[0].clone(), file.wpt[1].clone());
|
|
||||||
let one = waypoint();
|
|
||||||
let one_id = one.id;
|
|
||||||
insert_waypoints_at(&mut file, 2, vec![one]);
|
|
||||||
assert_eq!(ids(&file), [&before[..2], &[one_id], &before[2..]].concat());
|
|
||||||
assert_eq!(file.wpt.len(), 3);
|
|
||||||
assert!(Rc::ptr_eq(&file.wpt[0], &first) && Rc::ptr_eq(&file.wpt[2], &last));
|
|
||||||
|
|
||||||
// inside a chunk
|
|
||||||
let (mut file, before) = file_with(&[3]);
|
|
||||||
let one = waypoint();
|
|
||||||
let one_id = one.id;
|
|
||||||
insert_waypoints_at(&mut file, 1, vec![one]);
|
|
||||||
assert_eq!(ids(&file), [&before[..1], &[one_id], &before[1..]].concat());
|
|
||||||
assert_eq!(file.wpt.len(), 3);
|
|
||||||
|
|
||||||
// at the end, or past it
|
|
||||||
for index in [3, 100] {
|
|
||||||
let (mut file, before) = file_with(&[3]);
|
|
||||||
let one = waypoint();
|
|
||||||
let one_id = one.id;
|
|
||||||
insert_waypoints_at(&mut file, index, vec![one]);
|
|
||||||
assert_eq!(ids(&file), [before, vec![one_id]].concat());
|
|
||||||
}
|
|
||||||
|
|
||||||
// in a file without waypoints
|
|
||||||
let mut file = File::default();
|
|
||||||
let one = waypoint();
|
|
||||||
let one_id = one.id;
|
|
||||||
insert_waypoints_at(&mut file, 0, vec![one]);
|
|
||||||
assert_eq!(ids(&file), vec![one_id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
mod copy;
|
mod copy;
|
||||||
mod edit_waypoint_chunks;
|
|
||||||
mod insert_waypoints;
|
|
||||||
mod produce;
|
mod produce;
|
||||||
mod update_selected;
|
mod update_selected;
|
||||||
mod update_waypoint;
|
mod update_waypoint;
|
||||||
|
|
||||||
pub use copy::*;
|
pub use copy::*;
|
||||||
pub use edit_waypoint_chunks::*;
|
|
||||||
pub use insert_waypoints::*;
|
|
||||||
pub use produce::*;
|
pub use produce::*;
|
||||||
pub use update_selected::*;
|
pub use update_selected::*;
|
||||||
pub use update_waypoint::*;
|
pub use update_waypoint::*;
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{File, FileId, Selection, StackEntry, State, Track, TrackSegment, Waypoint};
|
||||||
File, FileId, Selection, StackEntry, State, Track, TrackSegment, Waypoint, edit_waypoint_chunks,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// What an [`Editor`] hook did to the element it was given.
|
/// What an [`Editor`] hook did to the element it was given.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -87,7 +85,7 @@ fn edit_waypoints<E: Editor + ?Sized>(
|
|||||||
filter: impl Fn(&Waypoint) -> bool,
|
filter: impl Fn(&Waypoint) -> bool,
|
||||||
editor: &mut E,
|
editor: &mut E,
|
||||||
) -> Edit {
|
) -> Edit {
|
||||||
let changed = edit_waypoint_chunks(file, &filter, |wpts| {
|
let changed = file.wpt.edit(&filter, |wpts| {
|
||||||
edit_where(wpts, &filter, |wpt| editor.waypoint(wpt)) == Edit::Changed
|
edit_where(wpts, &filter, |wpt| editor.waypoint(wpt)) == Edit::Changed
|
||||||
});
|
});
|
||||||
if changed {
|
if changed {
|
||||||
@@ -287,35 +285,34 @@ mod tests {
|
|||||||
let wpts: Vec<_> = (0..3).map(|_| Waypoint::default()).collect();
|
let wpts: Vec<_> = (0..3).map(|_| Waypoint::default()).collect();
|
||||||
let ids: Vec<_> = wpts.iter().map(|w| w.id).collect();
|
let ids: Vec<_> = wpts.iter().map(|w| w.id).collect();
|
||||||
let mut file = (*fx.files[&id]).clone();
|
let mut file = (*fx.files[&id]).clone();
|
||||||
file.wpt = vec![Rc::new(crate::WaypointChunk {
|
file.wpt = crate::Waypoints::new([crate::WaypointChunk {
|
||||||
wpt: wpts,
|
wpt: wpts,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})];
|
}]);
|
||||||
fx.files.insert(id, Rc::new(file));
|
fx.files.insert(id, Rc::new(file));
|
||||||
let chunk_id = fx.files[&id].wpt[0].id;
|
let chunk_id = fx.files[&id].wpt.chunks()[0].id;
|
||||||
|
|
||||||
// A file selection never touches waypoints.
|
// A file selection never touches waypoints.
|
||||||
fx.selection = Selection::File {
|
fx.selection = Selection::File {
|
||||||
file_ids: HashSet::from([id]),
|
file_ids: HashSet::from([id]),
|
||||||
};
|
};
|
||||||
update_selected(&mut fx.state(), &mut Name);
|
update_selected(&mut fx.state(), &mut Name);
|
||||||
assert!(fx.files[&id].wpt[0].wpt.iter().all(|w| w.name.is_none()));
|
assert!(fx.files[&id].wpt.iter().all(|w| w.name.is_none()));
|
||||||
|
|
||||||
fx.selection = Selection::Waypoint {
|
fx.selection = Selection::Waypoint {
|
||||||
file_id: id,
|
file_id: id,
|
||||||
wpt_ids: HashSet::from([ids[1]]),
|
wpt_ids: HashSet::from([ids[1]]),
|
||||||
};
|
};
|
||||||
let rev = fx.files[&id].wpt_rev_id;
|
let rev = fx.files[&id].wpt.rev_id;
|
||||||
update_selected(&mut fx.state(), &mut Name);
|
update_selected(&mut fx.state(), &mut Name);
|
||||||
assert_ne!(fx.files[&id].wpt_rev_id, rev);
|
assert_ne!(fx.files[&id].wpt.rev_id, rev);
|
||||||
let chunk = &fx.files[&id].wpt[0];
|
assert_ne!(fx.files[&id].wpt.chunks()[0].id, chunk_id);
|
||||||
assert_ne!(chunk.id, chunk_id);
|
let named: Vec<_> = fx.files[&id].wpt.iter().map(|w| w.name.is_some()).collect();
|
||||||
let named: Vec<_> = chunk.wpt.iter().map(|w| w.name.is_some()).collect();
|
|
||||||
assert_eq!(named, [false, true, false]);
|
assert_eq!(named, [false, true, false]);
|
||||||
|
|
||||||
fx.selection = Selection::Waypoints { file_id: id };
|
fx.selection = Selection::Waypoints { file_id: id };
|
||||||
update_selected(&mut fx.state(), &mut Name);
|
update_selected(&mut fx.state(), &mut Name);
|
||||||
assert!(fx.files[&id].wpt[0].wpt.iter().all(|w| w.name.is_some()));
|
assert!(fx.files[&id].wpt.iter().all(|w| w.name.is_some()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{CommandError, FileId, State, Waypoint, WaypointId, edit_waypoint_chunks};
|
use crate::{CommandError, FileId, State, Waypoint, WaypointId};
|
||||||
|
|
||||||
/// Changes one waypoint of a file with `f`, whatever is selected. Nothing to do if the file or
|
/// Changes one waypoint of a file with `f`, whatever is selected. Nothing to do if the file or
|
||||||
/// the waypoint does not exist.
|
/// the waypoint does not exist.
|
||||||
@@ -12,8 +12,7 @@ pub fn update_waypoint(
|
|||||||
) -> Result<(), CommandError> {
|
) -> Result<(), CommandError> {
|
||||||
let file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
let file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||||
let mut file = (**file).clone();
|
let mut file = (**file).clone();
|
||||||
let changed = edit_waypoint_chunks(
|
let changed = file.wpt.edit(
|
||||||
&mut file,
|
|
||||||
|wpt| wpt.id == waypoint_id,
|
|wpt| wpt.id == waypoint_id,
|
||||||
|wpts| {
|
|wpts| {
|
||||||
wpts.iter_mut()
|
wpts.iter_mut()
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ use std::{collections::HashSet, rc::Rc};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, CommandError, Edit, Editor, File, FileId, LngLat, LngLatBounds, Selection, State,
|
Apply, CommandError, Edit, Editor, File, FileId, LngLat, LngLatBounds, Selection, State,
|
||||||
TrackSegment, TrackSegmentId, Trackpoint, Waypoint, delete_waypoints, edit_waypoint_chunks,
|
TrackSegment, TrackSegmentId, Trackpoint, Waypoint, delete_waypoints, update_selected,
|
||||||
update_selected,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -91,8 +90,7 @@ impl Editor for Cleaner<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.clean.wpt
|
if self.clean.wpt
|
||||||
&& edit_waypoint_chunks(
|
&& file.wpt.edit(
|
||||||
file,
|
|
||||||
|wpt| self.clean.removes(wpt.coordinates),
|
|wpt| self.clean.removes(wpt.coordinates),
|
||||||
|wpts| {
|
|wpts| {
|
||||||
wpts.retain(|wpt| !self.clean.removes(wpt.coordinates));
|
wpts.retain(|wpt| !self.clean.removes(wpt.coordinates));
|
||||||
@@ -244,7 +242,7 @@ fn fixed_selection(state: &State) -> Selection {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::{FileId, Load, engine::command::fixture::Fixture};
|
use crate::{FileId, Load, WaypointChunk, Waypoints, engine::command::fixture::Fixture};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -363,18 +361,14 @@ mod tests {
|
|||||||
fn test_waypoints_flag_and_selection() {
|
fn test_waypoints_flag_and_selection() {
|
||||||
let (mut fx, id) = loaded();
|
let (mut fx, id) = loaded();
|
||||||
let mut file = (*fx.files[&id]).clone();
|
let mut file = (*fx.files[&id]).clone();
|
||||||
file.wpt = vec![std::rc::Rc::new(crate::WaypointChunk {
|
file.wpt = Waypoints::new([WaypointChunk {
|
||||||
wpt: vec![Waypoint::default(), Waypoint::default()],
|
wpt: vec![Waypoint::default(), Waypoint::default()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})];
|
}]);
|
||||||
let ids: Vec<_> = file.wpt[0].wpt.iter().map(|w| w.id).collect();
|
let ids: Vec<_> = file.wpt.iter().map(|w| w.id).collect();
|
||||||
fx.files.insert(id, std::rc::Rc::new(file));
|
fx.files.insert(id, std::rc::Rc::new(file));
|
||||||
let everywhere = bounds(-180.0, -90.0, 180.0, 90.0);
|
let everywhere = bounds(-180.0, -90.0, 180.0, 90.0);
|
||||||
let wpt_count = |fx: &Fixture| {
|
let wpt_count = |fx: &Fixture| fx.files.get(&id).map_or(0, |f| f.wpt.len());
|
||||||
fx.files
|
|
||||||
.get(&id)
|
|
||||||
.map_or(0, |f| f.wpt.iter().map(|c| c.wpt.len()).sum::<usize>())
|
|
||||||
};
|
|
||||||
|
|
||||||
// trackpoints only: waypoints are kept
|
// trackpoints only: waypoints are kept
|
||||||
Clean {
|
Clean {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ mod tests {
|
|||||||
fn test_edit_the_given_waypoint() {
|
fn test_edit_the_given_waypoint() {
|
||||||
let mut fx = Fixture::default();
|
let mut fx = Fixture::default();
|
||||||
let mut file = File::default();
|
let mut file = File::default();
|
||||||
file.wpt.push(Rc::new(WaypointChunk {
|
file.wpt.push(WaypointChunk {
|
||||||
wpt: vec![
|
wpt: vec![
|
||||||
Waypoint {
|
Waypoint {
|
||||||
name: Some("old".into()),
|
name: Some("old".into()),
|
||||||
@@ -64,7 +64,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
});
|
||||||
let id = file.id;
|
let id = file.id;
|
||||||
let ids: Vec<_> = waypoint_ids(&file).collect();
|
let ids: Vec<_> = waypoint_ids(&file).collect();
|
||||||
fx.files.insert(id, Rc::new(file));
|
fx.files.insert(id, Rc::new(file));
|
||||||
@@ -83,11 +83,7 @@ mod tests {
|
|||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let wpts: Vec<_> = fx.files[&id]
|
let wpts: Vec<_> = fx.files[&id].wpt.iter().collect();
|
||||||
.wpt
|
|
||||||
.iter()
|
|
||||||
.flat_map(|c| c.wpt.iter())
|
|
||||||
.collect();
|
|
||||||
let edited = wpts[0];
|
let edited = wpts[0];
|
||||||
assert_eq!(edited.id, ids[0]);
|
assert_eq!(edited.id, ids[0]);
|
||||||
assert_eq!(edited.name.as_deref(), Some("new"));
|
assert_eq!(edited.name.as_deref(), Some("new"));
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ mod tests {
|
|||||||
fn fixture() -> (Fixture, crate::FileId, Vec<crate::WaypointId>) {
|
fn fixture() -> (Fixture, crate::FileId, Vec<crate::WaypointId>) {
|
||||||
let mut fx = Fixture::default();
|
let mut fx = Fixture::default();
|
||||||
let mut file = File::default();
|
let mut file = File::default();
|
||||||
file.wpt.push(Rc::new(WaypointChunk {
|
file.wpt.push(WaypointChunk {
|
||||||
wpt: (0..3)
|
wpt: (0..3)
|
||||||
.map(|i| Waypoint {
|
.map(|i| Waypoint {
|
||||||
name: Some(format!("w{i}")),
|
name: Some(format!("w{i}")),
|
||||||
@@ -44,7 +44,7 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
});
|
||||||
let id = file.id;
|
let id = file.id;
|
||||||
let ids = waypoint_ids(&file).collect();
|
let ids = waypoint_ids(&file).collect();
|
||||||
fx.files.insert(id, Rc::new(file));
|
fx.files.insert(id, Rc::new(file));
|
||||||
@@ -55,7 +55,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_only_the_given_waypoint_moves_whatever_is_selected() {
|
fn test_only_the_given_waypoint_moves_whatever_is_selected() {
|
||||||
let (mut fx, id, ids) = fixture();
|
let (mut fx, id, ids) = fixture();
|
||||||
let rev = fx.files[&id].wpt_rev_id;
|
let rev = fx.files[&id].wpt.rev_id;
|
||||||
|
|
||||||
MoveWaypoint {
|
MoveWaypoint {
|
||||||
file_id: id,
|
file_id: id,
|
||||||
@@ -67,11 +67,7 @@ mod tests {
|
|||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let wpts: Vec<_> = fx.files[&id]
|
let wpts: Vec<_> = fx.files[&id].wpt.iter().collect();
|
||||||
.wpt
|
|
||||||
.iter()
|
|
||||||
.flat_map(|c| c.wpt.iter())
|
|
||||||
.collect();
|
|
||||||
assert_eq!(wpts.len(), 3);
|
assert_eq!(wpts.len(), 3);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(
|
(
|
||||||
@@ -87,7 +83,7 @@ mod tests {
|
|||||||
assert_eq!((wpts[0].coordinates.lng, wpts[0].ele), (0.0, 0.0));
|
assert_eq!((wpts[0].coordinates.lng, wpts[0].ele), (0.0, 0.0));
|
||||||
assert_eq!((wpts[2].coordinates.lat, wpts[2].ele), (0.0, 0.0));
|
assert_eq!((wpts[2].coordinates.lat, wpts[2].ele), (0.0, 0.0));
|
||||||
// the coordinates changed, which is noticed by what is derived from them
|
// the coordinates changed, which is noticed by what is derived from them
|
||||||
assert_ne!(fx.files[&id].wpt_rev_id, rev);
|
assert_ne!(fx.files[&id].wpt.rev_id, rev);
|
||||||
assert_eq!(fx.selection, Selection::Empty);
|
assert_eq!(fx.selection, Selection::Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{Apply, CommandError, File, FileId, Link, LngLat, Selection, State, Waypoint};
|
||||||
Apply, CommandError, File, FileId, Link, LngLat, Selection, State, Waypoint,
|
|
||||||
insert_waypoints_at,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Adds a waypoint at the end of the waypoints of each selected file (or of the file of the
|
/// Adds a waypoint at the end of the waypoints of each selected file (or of the file of the
|
||||||
/// selected elements). The strings are empty when the waypoint does not have the field.
|
/// selected elements). The strings are empty when the waypoint does not have the field.
|
||||||
@@ -77,7 +74,7 @@ impl Apply for NewWaypoint<'_> {
|
|||||||
self.link,
|
self.link,
|
||||||
);
|
);
|
||||||
let file: &mut File = Rc::make_mut(state.files.get_mut(&id).unwrap());
|
let file: &mut File = Rc::make_mut(state.files.get_mut(&id).unwrap());
|
||||||
insert_waypoints_at(file, usize::MAX, vec![waypoint]);
|
file.wpt.insert_at(usize::MAX, vec![waypoint]);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -104,11 +101,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn waypoints(fx: &Fixture, id: FileId) -> Vec<Waypoint> {
|
fn waypoints(fx: &Fixture, id: FileId) -> Vec<Waypoint> {
|
||||||
fx.files[&id]
|
fx.files[&id].wpt.iter().cloned().collect()
|
||||||
.wpt
|
|
||||||
.iter()
|
|
||||||
.flat_map(|chunk| chunk.wpt.iter().cloned())
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -127,7 +120,7 @@ mod tests {
|
|||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let (a, b) = (fx.order.0[0], fx.order.0[1]);
|
let (a, b) = (fx.order.0[0], fx.order.0[1]);
|
||||||
let rev = fx.files[&b].wpt_rev_id;
|
let rev = fx.files[&b].wpt.rev_id;
|
||||||
|
|
||||||
// b is selected
|
// b is selected
|
||||||
new_waypoint("peak").apply(&mut fx.state()).unwrap();
|
new_waypoint("peak").apply(&mut fx.state()).unwrap();
|
||||||
@@ -147,7 +140,7 @@ mod tests {
|
|||||||
wpt.link.as_ref().map(|l| l.href.as_str()),
|
wpt.link.as_ref().map(|l| l.href.as_str()),
|
||||||
Some("https://example.com")
|
Some("https://example.com")
|
||||||
);
|
);
|
||||||
assert_ne!(fx.files[&b].wpt_rev_id, rev);
|
assert_ne!(fx.files[&b].wpt.rev_id, rev);
|
||||||
|
|
||||||
// added after the others
|
// added after the others
|
||||||
let mut second = new_waypoint("second");
|
let mut second = new_waypoint("second");
|
||||||
|
|||||||
@@ -496,7 +496,7 @@ mod tests {
|
|||||||
let lens: Vec<_> = file
|
let lens: Vec<_> = file
|
||||||
.trk
|
.trk
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|trk| trk.trkseg.iter().map(TrackSegment::len))
|
.flat_map(|trk| trk.trkseg.iter().map(|segment| segment.len()))
|
||||||
.collect();
|
.collect();
|
||||||
assert!(lens.len() >= 2);
|
assert!(lens.len() >= 2);
|
||||||
|
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ impl CoordinatesCache {
|
|||||||
if self
|
if self
|
||||||
.waypoints
|
.waypoints
|
||||||
.get(&file.id)
|
.get(&file.id)
|
||||||
.is_none_or(|(r, _)| *r != file.wpt_rev_id)
|
.is_none_or(|(r, _)| *r != file.wpt.rev_id)
|
||||||
{
|
{
|
||||||
let mut coordinates = Vec::new();
|
let mut coordinates = Vec::new();
|
||||||
for wpt in file.wpt.iter().flat_map(|chunk| &chunk.wpt) {
|
for wpt in file.wpt.iter() {
|
||||||
coordinates.extend([wpt.coordinates.lng, wpt.coordinates.lat]);
|
coordinates.extend([wpt.coordinates.lng, wpt.coordinates.lat]);
|
||||||
}
|
}
|
||||||
self.waypoints
|
self.waypoints
|
||||||
.insert(file.id, (file.wpt_rev_id, coordinates));
|
.insert(file.id, (file.wpt.rev_id, coordinates));
|
||||||
}
|
}
|
||||||
for seg in file.trk.iter().flat_map(|trk| &trk.trkseg) {
|
for seg in file.trk.iter().flat_map(|trk| &trk.trkseg) {
|
||||||
segments.insert(seg.id);
|
segments.insert(seg.id);
|
||||||
@@ -138,17 +138,16 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let mut file = (*fx.files[&id]).clone();
|
let mut file = (*fx.files[&id]).clone();
|
||||||
file.wpt_rev_id = Default::default();
|
file.wpt = crate::Waypoints::new([
|
||||||
file.wpt = vec![
|
WaypointChunk {
|
||||||
Rc::new(WaypointChunk {
|
|
||||||
wpt: vec![wpt(1.0, 2.0)],
|
wpt: vec![wpt(1.0, 2.0)],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
},
|
||||||
Rc::new(WaypointChunk {
|
WaypointChunk {
|
||||||
wpt: vec![wpt(3.0, 4.0)],
|
wpt: vec![wpt(3.0, 4.0)],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
},
|
||||||
];
|
]);
|
||||||
fx.files.insert(id, Rc::new(file));
|
fx.files.insert(id, Rc::new(file));
|
||||||
let mut cache = CoordinatesCache::default();
|
let mut cache = CoordinatesCache::default();
|
||||||
cache.update(Some(&fx.files));
|
cache.update(Some(&fx.files));
|
||||||
|
|||||||
@@ -76,21 +76,19 @@ impl FileStructure {
|
|||||||
waypoints: file
|
waypoints: file
|
||||||
.wpt
|
.wpt
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|chunk| &chunk.wpt)
|
|
||||||
.map(|wpt| WaypointNode {
|
.map(|wpt| WaypointNode {
|
||||||
id: wpt.id,
|
id: wpt.id,
|
||||||
name: wpt.name.clone(),
|
name: wpt.name.clone(),
|
||||||
sym: wpt.sym.clone(),
|
sym: wpt.sym.clone(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
wpt_rev_id: file.wpt_rev_id,
|
wpt_rev_id: file.wpt.rev_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use crate::{Apply, Load, Waypoint, WaypointChunk, engine::command::fixture::Fixture};
|
use crate::{Apply, Load, Waypoint, WaypointChunk, engine::command::fixture::Fixture};
|
||||||
|
|
||||||
@@ -133,18 +131,17 @@ mod tests {
|
|||||||
name: Some(n.to_string()),
|
name: Some(n.to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let before = file.wpt_rev_id;
|
let before = file.wpt.rev_id;
|
||||||
file.wpt_rev_id = Default::default();
|
file.wpt = crate::Waypoints::new([
|
||||||
file.wpt = vec![
|
WaypointChunk {
|
||||||
Rc::new(WaypointChunk {
|
|
||||||
wpt: vec![wpt("a"), wpt("b")],
|
wpt: vec![wpt("a"), wpt("b")],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
},
|
||||||
Rc::new(WaypointChunk {
|
WaypointChunk {
|
||||||
wpt: vec![wpt("c")],
|
wpt: vec![wpt("c")],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
},
|
||||||
];
|
]);
|
||||||
let node = FileStructure::new(&file);
|
let node = FileStructure::new(&file);
|
||||||
let names: Vec<_> = node
|
let names: Vec<_> = node
|
||||||
.waypoints
|
.waypoints
|
||||||
@@ -153,6 +150,6 @@ mod tests {
|
|||||||
.collect();
|
.collect();
|
||||||
assert_eq!(names, ["a", "b", "c"]);
|
assert_eq!(names, ["a", "b", "c"]);
|
||||||
assert_ne!(node.wpt_rev_id, before);
|
assert_ne!(node.wpt_rev_id, before);
|
||||||
assert_eq!(node.wpt_rev_id, file.wpt_rev_id);
|
assert_eq!(node.wpt_rev_id, file.wpt.rev_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ impl RoutingBuffer {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::{collections::HashSet, rc::Rc};
|
use std::{collections::HashSet, rc::Rc};
|
||||||
|
|
||||||
use crate::{TrackSegment, parse};
|
use crate::parse;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ mod tests {
|
|||||||
let lengths: Vec<usize> = files[&id]
|
let lengths: Vec<usize> = files[&id]
|
||||||
.trk
|
.trk
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|trk| trk.trkseg.iter().map(TrackSegment::len))
|
.flat_map(|trk| trk.trkseg.iter().map(|segment| segment.len()))
|
||||||
.collect();
|
.collect();
|
||||||
let mut buffer = RoutingBuffer::default();
|
let mut buffer = RoutingBuffer::default();
|
||||||
buffer.update(Some(&files), &file_selection(id), &[id]);
|
buffer.update(Some(&files), &file_selection(id), &[id]);
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ impl Engine {
|
|||||||
.get(file_id)?
|
.get(file_id)?
|
||||||
.wpt
|
.wpt
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|chunk| &chunk.wpt)
|
|
||||||
.find(|wpt| wpt.id == *id)
|
.find(|wpt| wpt.id == *id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,20 +120,14 @@ impl Clipboard {
|
|||||||
.collect(),
|
.collect(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Selection::Waypoints { file_id } => ClipboardContent::Waypoints(
|
Selection::Waypoints { file_id } => {
|
||||||
files
|
ClipboardContent::Waypoints(files.get(file_id)?.wpt.iter().cloned().collect())
|
||||||
.get(file_id)?
|
}
|
||||||
.wpt
|
|
||||||
.iter()
|
|
||||||
.flat_map(|chunk| chunk.wpt.iter().cloned())
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
Selection::Waypoint { file_id, wpt_ids } => ClipboardContent::Waypoints(
|
Selection::Waypoint { file_id, wpt_ids } => ClipboardContent::Waypoints(
|
||||||
files
|
files
|
||||||
.get(file_id)?
|
.get(file_id)?
|
||||||
.wpt
|
.wpt
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|chunk| &chunk.wpt)
|
|
||||||
.filter(|wpt| wpt_ids.contains(&wpt.id))
|
.filter(|wpt| wpt_ids.contains(&wpt.id))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect(),
|
.collect(),
|
||||||
@@ -233,14 +227,14 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
file.wpt.push(Rc::new(WaypointChunk {
|
file.wpt.push(WaypointChunk {
|
||||||
wpt: vec![Waypoint::default(), Waypoint::default()],
|
wpt: vec![Waypoint::default(), Waypoint::default()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
});
|
||||||
file.wpt.push(Rc::new(WaypointChunk {
|
file.wpt.push(WaypointChunk {
|
||||||
wpt: vec![Waypoint::default()],
|
wpt: vec![Waypoint::default()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
});
|
||||||
file
|
file
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,11 +261,7 @@ mod tests {
|
|||||||
let file = file("source");
|
let file = file("source");
|
||||||
let (t0, t2) = (file.trk[0].id, file.trk[2].id);
|
let (t0, t2) = (file.trk[0].id, file.trk[2].id);
|
||||||
let (s0, s1) = (file.trk[1].trkseg[0].id, file.trk[1].trkseg[1].id);
|
let (s0, s1) = (file.trk[1].trkseg[0].id, file.trk[1].trkseg[1].id);
|
||||||
let wpt: Vec<_> = file
|
let wpt: Vec<_> = file.wpt.iter().map(|w| w.id).collect();
|
||||||
.wpt
|
|
||||||
.iter()
|
|
||||||
.flat_map(|c| c.wpt.iter().map(|w| w.id))
|
|
||||||
.collect();
|
|
||||||
let files = files_of(vec![file.clone()]);
|
let files = files_of(vec![file.clone()]);
|
||||||
|
|
||||||
// tracks, in the order of the file, with the name of their file
|
// tracks, in the order of the file, with the name of their file
|
||||||
@@ -447,7 +437,7 @@ mod tests {
|
|||||||
let waypoints = Selection::Waypoints { file_id: file.id };
|
let waypoints = Selection::Waypoints { file_id: file.id };
|
||||||
let waypoint = Selection::Waypoint {
|
let waypoint = Selection::Waypoint {
|
||||||
file_id: file.id,
|
file_id: file.id,
|
||||||
wpt_ids: [file.wpt[0].wpt[0].id].into(),
|
wpt_ids: [file.wpt[0].id].into(),
|
||||||
};
|
};
|
||||||
let targets = [
|
let targets = [
|
||||||
Selection::Empty,
|
Selection::Empty,
|
||||||
|
|||||||
@@ -126,12 +126,7 @@ impl Selection {
|
|||||||
Selection::Waypoints { file_id } => !files.contains_key(file_id),
|
Selection::Waypoints { file_id } => !files.contains_key(file_id),
|
||||||
Selection::Waypoint { file_id, wpt_ids } => match files.get(file_id) {
|
Selection::Waypoint { file_id, wpt_ids } => match files.get(file_id) {
|
||||||
Some(file) => {
|
Some(file) => {
|
||||||
wpt_ids.retain(|id| {
|
wpt_ids.retain(|id| file.wpt.iter().any(|wpt| wpt.id == *id));
|
||||||
file.wpt
|
|
||||||
.iter()
|
|
||||||
.flat_map(|chunk| &chunk.wpt)
|
|
||||||
.any(|wpt| wpt.id == *id)
|
|
||||||
});
|
|
||||||
wpt_ids.is_empty()
|
wpt_ids.is_empty()
|
||||||
}
|
}
|
||||||
None => true,
|
None => true,
|
||||||
@@ -324,13 +319,7 @@ impl Selection {
|
|||||||
Selection::Waypoints { .. } => None,
|
Selection::Waypoints { .. } => None,
|
||||||
Selection::Waypoint { file_id, .. } => Some(Selection::Waypoint {
|
Selection::Waypoint { file_id, .. } => Some(Selection::Waypoint {
|
||||||
file_id: *file_id,
|
file_id: *file_id,
|
||||||
wpt_ids: files
|
wpt_ids: files.get(file_id)?.wpt.iter().map(|wpt| wpt.id).collect(),
|
||||||
.get(file_id)?
|
|
||||||
.wpt
|
|
||||||
.iter()
|
|
||||||
.flat_map(|chunk| &chunk.wpt)
|
|
||||||
.map(|wpt| wpt.id)
|
|
||||||
.collect(),
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,13 +373,8 @@ impl Selection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Selection::Waypoint { file_id, wpt_ids } => {
|
Selection::Waypoint { file_id, wpt_ids } => {
|
||||||
let ids: Vec<WaypointId> = files
|
let ids: Vec<WaypointId> =
|
||||||
.get(file_id)?
|
files.get(file_id)?.wpt.iter().map(|wpt| wpt.id).collect();
|
||||||
.wpt
|
|
||||||
.iter()
|
|
||||||
.flat_map(|chunk| &chunk.wpt)
|
|
||||||
.map(|wpt| wpt.id)
|
|
||||||
.collect();
|
|
||||||
Some(Selection::Waypoint {
|
Some(Selection::Waypoint {
|
||||||
file_id: *file_id,
|
file_id: *file_id,
|
||||||
wpt_ids: [neighbour(&ids, wpt_ids, down)?].into(),
|
wpt_ids: [neighbour(&ids, wpt_ids, down)?].into(),
|
||||||
@@ -444,12 +428,12 @@ mod tests {
|
|||||||
trkseg: vec![TrackSegment::default()],
|
trkseg: vec![TrackSegment::default()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
file.wpt.push(Rc::new(WaypointChunk {
|
file.wpt.push(WaypointChunk {
|
||||||
wpt: vec![Waypoint::default()],
|
wpt: vec![Waypoint::default()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
});
|
||||||
let (file_id, trk_id) = (file.id, file.trk[0].id);
|
let (file_id, trk_id) = (file.id, file.trk[0].id);
|
||||||
let (seg_id, wpt_id) = (file.trk[0].trkseg[0].id, file.wpt[0].wpt[0].id);
|
let (seg_id, wpt_id) = (file.trk[0].trkseg[0].id, file.wpt[0].id);
|
||||||
let mut files = StackEntry::default();
|
let mut files = StackEntry::default();
|
||||||
files.insert(file_id, Rc::new(file));
|
files.insert(file_id, Rc::new(file));
|
||||||
|
|
||||||
@@ -595,14 +579,14 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
file.wpt.push(Rc::new(WaypointChunk {
|
file.wpt.push(WaypointChunk {
|
||||||
wpt: vec![Waypoint::default(), Waypoint::default()],
|
wpt: vec![Waypoint::default(), Waypoint::default()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
});
|
||||||
file.wpt.push(Rc::new(WaypointChunk {
|
file.wpt.push(WaypointChunk {
|
||||||
wpt: vec![Waypoint::default()],
|
wpt: vec![Waypoint::default()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
});
|
||||||
let tree = Tree {
|
let tree = Tree {
|
||||||
file: file.id,
|
file: file.id,
|
||||||
tracks: file.trk.iter().map(|t| t.id).collect(),
|
tracks: file.trk.iter().map(|t| t.id).collect(),
|
||||||
@@ -611,12 +595,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|t| t.trkseg.iter().map(|s| s.id).collect())
|
.map(|t| t.trkseg.iter().map(|s| s.id).collect())
|
||||||
.collect(),
|
.collect(),
|
||||||
waypoints: file
|
waypoints: file.wpt.iter().map(|w| w.id).collect(),
|
||||||
.wpt
|
|
||||||
.iter()
|
|
||||||
.flat_map(|chunk| &chunk.wpt)
|
|
||||||
.map(|w| w.id)
|
|
||||||
.collect(),
|
|
||||||
order: vec![file.id],
|
order: vec![file.id],
|
||||||
files: StackEntry::from([(file.id, Rc::new(file))]),
|
files: StackEntry::from([(file.id, Rc::new(file))]),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user