2026-09-23 19:53:39 +02:00
|
|
|
use std::{ops::Index, rc::Rc};
|
2026-09-18 09:03:51 +02:00
|
|
|
|
2026-10-02 18:06:45 +02:00
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
2026-10-07 17:58:17 +02:00
|
|
|
use crate::{Trackpoint, TrackpointChunk, compute_anchors};
|
2026-09-18 09:03:51 +02:00
|
|
|
|
2026-10-03 15:33:44 +02:00
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
2026-10-03 19:08:56 +02:00
|
|
|
pub struct TrackSegmentId(pub Uuid);
|
2026-10-02 18:06:45 +02:00
|
|
|
|
|
|
|
|
impl Default for TrackSegmentId {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self(Uuid::new_v4())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
2026-10-03 19:08:56 +02:00
|
|
|
pub struct TrackSegmentRevisionId(pub Uuid);
|
2026-10-02 18:06:45 +02:00
|
|
|
|
|
|
|
|
impl Default for TrackSegmentRevisionId {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self(Uuid::new_v4())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 15:33:44 +02:00
|
|
|
#[derive(Debug, Default, Clone, PartialEq)]
|
2026-09-18 09:03:51 +02:00
|
|
|
pub struct TrackSegment {
|
2026-10-02 18:06:45 +02:00
|
|
|
pub id: TrackSegmentId,
|
|
|
|
|
pub rev_id: TrackSegmentRevisionId,
|
2026-09-23 19:53:39 +02:00
|
|
|
chunks: Vec<Rc<TrackpointChunk>>,
|
|
|
|
|
cumul_length: Vec<usize>,
|
2026-09-18 09:03:51 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-21 23:49:57 +02:00
|
|
|
impl TrackSegment {
|
2026-09-23 19:53:39 +02:00
|
|
|
pub fn push(&mut self, chunk: TrackpointChunk) {
|
2026-10-03 18:03:13 +02:00
|
|
|
self.push_shared(Rc::new(chunk));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn push_shared(&mut self, chunk: Rc<TrackpointChunk>) {
|
2026-09-23 19:53:39 +02:00
|
|
|
if chunk.trkpt.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
self.cumul_length
|
|
|
|
|
.push(self.cumul_length.last().copied().unwrap_or_default() + chunk.trkpt.len());
|
2026-10-03 18:03:13 +02:00
|
|
|
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>) {
|
|
|
|
|
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 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);
|
2026-10-07 17:58:17 +02:00
|
|
|
self.ensure_end_anchors();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The first and the last trackpoints are always anchors, shown at every zoom level.
|
|
|
|
|
///
|
|
|
|
|
/// It is done by [`TrackSegment::splice`]; commands that build segments in another way need to
|
|
|
|
|
/// call it.
|
|
|
|
|
pub fn ensure_end_anchors(&mut self) {
|
|
|
|
|
let Some(last) = self.len().checked_sub(1) else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
for index in [0, last] {
|
|
|
|
|
if self[index].anchor != Some(0) {
|
|
|
|
|
self.point_mut(index).anchor = Some(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Makes the trackpoint at `index` an anchor shown from the map zoom level `zoom`. Panics if
|
|
|
|
|
/// there is no such trackpoint.
|
|
|
|
|
pub fn set_anchor(&mut self, index: usize, zoom: u8) {
|
|
|
|
|
self.point_mut(index).anchor = Some(zoom);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Sets the anchors of the trackpoints from the details of the path of the segment (see
|
|
|
|
|
/// [`compute_anchors`]), forgetting the previous ones.
|
|
|
|
|
pub fn compute_anchors(&mut self) {
|
|
|
|
|
let anchors = compute_anchors(self);
|
|
|
|
|
let mut anchors = anchors.into_iter().peekable();
|
|
|
|
|
self.map_points(|index, trkpt| {
|
|
|
|
|
trkpt.anchor = match anchors.peek() {
|
|
|
|
|
Some(&(anchor, zoom)) if anchor == index => {
|
|
|
|
|
anchors.next();
|
|
|
|
|
Some(zoom)
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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();
|
|
|
|
|
}
|
2026-10-03 18:03:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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));
|
2026-09-23 19:53:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn len(&self) -> usize {
|
|
|
|
|
self.cumul_length.last().copied().unwrap_or_default()
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 20:54:05 +02:00
|
|
|
pub fn is_empty(&self) -> bool {
|
|
|
|
|
self.chunks.is_empty()
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-23 19:53:39 +02:00
|
|
|
pub fn iter(&self) -> TrackSegmentIterator<'_> {
|
2026-09-21 23:49:57 +02:00
|
|
|
TrackSegmentIterator::new(self)
|
|
|
|
|
}
|
2026-09-23 19:53:39 +02:00
|
|
|
|
|
|
|
|
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,
|
2026-10-02 18:50:10 +02:00
|
|
|
flat: idx.flat + 1,
|
2026-09-23 19:53:39 +02:00
|
|
|
});
|
|
|
|
|
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,
|
2026-10-02 18:50:10 +02:00
|
|
|
flat: self.cumul_length.last().copied().unwrap_or_default(),
|
2026-09-23 19:53:39 +02:00
|
|
|
});
|
2026-10-03 16:16:47 +02:00
|
|
|
if prev.pos == 0 {
|
|
|
|
|
while prev.chunk > 0 {
|
2026-09-23 19:53:39 +02:00
|
|
|
prev.chunk -= 1;
|
2026-10-03 16:16:47 +02:00
|
|
|
if !self.chunks[prev.chunk].trkpt.is_empty() {
|
|
|
|
|
prev.pos = self.chunks[prev.chunk].trkpt.len() - 1;
|
|
|
|
|
prev.flat -= 1;
|
|
|
|
|
return Some(prev);
|
|
|
|
|
}
|
2026-09-23 19:53:39 +02:00
|
|
|
}
|
2026-10-03 16:16:47 +02:00
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
prev.pos -= 1;
|
|
|
|
|
prev.flat -= 1;
|
|
|
|
|
Some(prev)
|
2026-09-23 19:53:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-10-02 18:50:10 +02:00
|
|
|
Some(TrackSegmentIndex {
|
|
|
|
|
chunk,
|
|
|
|
|
pos,
|
|
|
|
|
flat: idx,
|
|
|
|
|
})
|
2026-09-23 19:53:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-09-21 23:49:57 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-23 19:53:39 +02:00
|
|
|
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,
|
2026-10-02 18:50:10 +02:00
|
|
|
pub flat: usize,
|
2026-09-23 19:53:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
2026-09-21 23:49:57 +02:00
|
|
|
pub struct TrackSegmentIterator<'a> {
|
|
|
|
|
trkseg: &'a TrackSegment,
|
2026-09-23 19:53:39 +02:00
|
|
|
idx: Option<TrackSegmentIndex>,
|
2026-09-21 23:49:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a> TrackSegmentIterator<'a> {
|
|
|
|
|
pub fn new(trkseg: &'a TrackSegment) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
trkseg,
|
2026-09-23 19:53:39 +02:00
|
|
|
idx: Default::default(),
|
2026-09-21 23:49:57 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a> Iterator for TrackSegmentIterator<'a> {
|
2026-09-23 19:53:39 +02:00
|
|
|
type Item = &'a Trackpoint;
|
2026-09-21 23:49:57 +02:00
|
|
|
|
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
2026-09-23 19:53:39 +02:00
|
|
|
self.idx = self.trkseg.next_index(self.idx);
|
|
|
|
|
self.idx.map(|idx| &self.trkseg[idx])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
2026-10-02 18:50:10 +02:00
|
|
|
let idx = self.idx.map_or_default(|idx| idx.flat) + n;
|
2026-09-23 19:53:39 +02:00
|
|
|
self.idx = self.trkseg.locate(idx);
|
|
|
|
|
self.idx.map(|idx| &self.trkseg[idx])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
fn create_track_segment(nb_chunks: usize) -> TrackSegment {
|
|
|
|
|
let mut trkseg = TrackSegment::default();
|
|
|
|
|
let mut count = 0;
|
|
|
|
|
for n in 1..=nb_chunks {
|
|
|
|
|
let mut chunk = TrackpointChunk::default();
|
|
|
|
|
for _ in 0..n {
|
2026-10-03 20:54:05 +02:00
|
|
|
chunk.trkpt.push(Trackpoint {
|
|
|
|
|
ele: count as f64,
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
2026-09-23 19:53:39 +02:00
|
|
|
count += 1;
|
|
|
|
|
}
|
|
|
|
|
trkseg.push(chunk);
|
|
|
|
|
}
|
|
|
|
|
trkseg
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 18:03:13 +02:00
|
|
|
fn eles(trkseg: &TrackSegment) -> Vec<f64> {
|
|
|
|
|
trkseg.iter().map(|p| p.ele).collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn points(eles: &[f64]) -> Vec<Trackpoint> {
|
|
|
|
|
eles.iter()
|
|
|
|
|
.map(|&ele| Trackpoint {
|
|
|
|
|
ele,
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_splice_replace_insert_delete_append() {
|
|
|
|
|
// chunks of 1..=5 points: [0] [1 2] [3 4 5] [6 7 8 9] [10..=14]
|
|
|
|
|
let mut trkseg = create_track_segment(5);
|
|
|
|
|
trkseg.splice(4, 8, points(&[-1.0, -2.0]));
|
|
|
|
|
let mut expected: Vec<f64> = (0..4).map(f64::from).collect();
|
|
|
|
|
expected.extend([-1.0, -2.0]);
|
|
|
|
|
expected.extend((8..15).map(f64::from));
|
|
|
|
|
assert_eq!(eles(&trkseg), expected);
|
|
|
|
|
assert_eq!(trkseg.len(), expected.len());
|
|
|
|
|
for i in 0..trkseg.len() {
|
|
|
|
|
assert_eq!(trkseg[i].ele, expected[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trkseg.splice(2, 2, points(&[100.0]));
|
|
|
|
|
expected.insert(2, 100.0);
|
|
|
|
|
assert_eq!(eles(&trkseg), expected);
|
|
|
|
|
|
|
|
|
|
trkseg.splice(0, 3, vec![]);
|
|
|
|
|
expected.drain(..3);
|
|
|
|
|
assert_eq!(eles(&trkseg), expected);
|
|
|
|
|
|
|
|
|
|
let len = trkseg.len();
|
|
|
|
|
trkseg.splice(len, len, points(&[7.0, 8.0]));
|
|
|
|
|
expected.extend([7.0, 8.0]);
|
|
|
|
|
assert_eq!(eles(&trkseg), expected);
|
|
|
|
|
|
|
|
|
|
let len = trkseg.len();
|
|
|
|
|
trkseg.splice(0, len, vec![]);
|
|
|
|
|
assert_eq!(trkseg.len(), 0);
|
|
|
|
|
assert!(trkseg.first_index().is_none());
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 17:58:17 +02:00
|
|
|
fn anchors(trkseg: &TrackSegment) -> Vec<Option<u8>> {
|
|
|
|
|
trkseg.iter().map(|p| p.anchor).collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_splice_keeps_the_ends_anchors() {
|
|
|
|
|
let mut trkseg = TrackSegment::default();
|
|
|
|
|
assert_eq!(anchors(&trkseg), vec![]);
|
|
|
|
|
trkseg.splice(0, 0, points(&[0.0, 1.0, 2.0]));
|
|
|
|
|
assert_eq!(anchors(&trkseg), [Some(0), None, Some(0)]);
|
|
|
|
|
|
|
|
|
|
// new ends: the previous ones stay anchors
|
|
|
|
|
trkseg.splice(3, 3, points(&[3.0]));
|
|
|
|
|
trkseg.splice(0, 0, points(&[-1.0]));
|
|
|
|
|
assert_eq!(anchors(&trkseg), [Some(0), Some(0), None, Some(0), Some(0)]);
|
|
|
|
|
|
|
|
|
|
// removing the ends: the new ones become anchors
|
|
|
|
|
trkseg.splice(4, 5, vec![]);
|
|
|
|
|
trkseg.splice(0, 2, vec![]);
|
|
|
|
|
assert_eq!(anchors(&trkseg), [Some(0), Some(0)]);
|
|
|
|
|
|
|
|
|
|
// a single point is both ends
|
|
|
|
|
trkseg.splice(1, 2, vec![]);
|
|
|
|
|
assert_eq!(anchors(&trkseg), [Some(0)]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_compute_anchors_replaces_the_previous_ones() {
|
|
|
|
|
let mut trkseg = TrackSegment::default();
|
|
|
|
|
let line: Vec<_> = (0..10)
|
|
|
|
|
.map(|i| Trackpoint {
|
|
|
|
|
coordinates: crate::LngLat {
|
|
|
|
|
lng: i as f64 * 0.001,
|
|
|
|
|
lat: 0.0,
|
|
|
|
|
},
|
|
|
|
|
anchor: Some(5),
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
trkseg.splice(0, 0, line);
|
|
|
|
|
trkseg.compute_anchors();
|
|
|
|
|
let mut expected = vec![None; 10];
|
|
|
|
|
expected[0] = Some(0);
|
|
|
|
|
expected[9] = Some(0);
|
|
|
|
|
assert_eq!(anchors(&trkseg), expected);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 18:03:13 +02:00
|
|
|
#[test]
|
|
|
|
|
fn test_splice_keeps_untouched_chunks_shared() {
|
|
|
|
|
let mut trkseg = create_track_segment(5);
|
2026-10-07 17:58:17 +02:00
|
|
|
// the ends are anchors already, or their chunks would be copied
|
|
|
|
|
trkseg.ensure_end_anchors();
|
2026-10-03 18:03:13 +02:00
|
|
|
let before = trkseg.chunks.clone();
|
|
|
|
|
// inside the third chunk only
|
|
|
|
|
trkseg.splice(4, 5, points(&[-1.0]));
|
|
|
|
|
assert!(Rc::ptr_eq(&trkseg.chunks[0], &before[0]));
|
|
|
|
|
assert!(Rc::ptr_eq(&trkseg.chunks[1], &before[1]));
|
|
|
|
|
assert!(Rc::ptr_eq(trkseg.chunks.last().unwrap(), &before[4]));
|
|
|
|
|
assert!(Rc::ptr_eq(
|
|
|
|
|
&trkseg.chunks[trkseg.chunks.len() - 2],
|
|
|
|
|
&before[3]
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_splice_append_extends_last_chunk_and_respects_max_size() {
|
|
|
|
|
let mut trkseg = TrackSegment::default();
|
|
|
|
|
for i in 0..10_000 {
|
|
|
|
|
let len = trkseg.len();
|
|
|
|
|
trkseg.splice(len, len, points(&[i as f64]));
|
|
|
|
|
}
|
|
|
|
|
assert_eq!(trkseg.len(), 10_000);
|
|
|
|
|
assert!(trkseg.chunks.len() <= 3);
|
|
|
|
|
assert!(trkseg.chunks.iter().all(|c| c.trkpt.len() <= 4096));
|
|
|
|
|
assert_eq!(trkseg[9_999].ele, 9_999.0);
|
|
|
|
|
assert_eq!(*trkseg.cumul_length.last().unwrap(), 10_000);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-23 19:53:39 +02:00
|
|
|
#[test]
|
|
|
|
|
fn test_len() {
|
|
|
|
|
let nb_chunks = 10;
|
|
|
|
|
let trkseg = create_track_segment(nb_chunks);
|
|
|
|
|
assert_eq!(trkseg.len(), nb_chunks * (nb_chunks + 1) / 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_locate() {
|
|
|
|
|
let nb_chunks = 10;
|
|
|
|
|
let trkseg = create_track_segment(nb_chunks);
|
|
|
|
|
for i in 0..trkseg.len() {
|
|
|
|
|
let idx = trkseg.locate(i);
|
|
|
|
|
assert!(idx.is_some());
|
|
|
|
|
assert_eq!(trkseg[idx.unwrap()].ele, i as f64);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert!(trkseg.locate(trkseg.len()).is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_index() {
|
|
|
|
|
let nb_chunks = 10;
|
|
|
|
|
let trkseg = create_track_segment(nb_chunks);
|
|
|
|
|
for i in 0..trkseg.len() {
|
|
|
|
|
assert_eq!(trkseg[i].ele, i as f64);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic]
|
|
|
|
|
fn test_index_out_of_bounds_1() {
|
|
|
|
|
let nb_chunks = 10;
|
|
|
|
|
let trkseg = create_track_segment(nb_chunks);
|
2026-10-03 20:54:05 +02:00
|
|
|
let _ = trkseg[trkseg.len()];
|
2026-09-23 19:53:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[should_panic]
|
|
|
|
|
fn test_index_out_of_bounds_2() {
|
|
|
|
|
let nb_chunks = 10;
|
|
|
|
|
let trkseg = create_track_segment(nb_chunks);
|
2026-10-03 20:54:05 +02:00
|
|
|
let _ = trkseg[TrackSegmentIndex {
|
2026-09-23 19:53:39 +02:00
|
|
|
chunk: trkseg.chunks.len(),
|
|
|
|
|
pos: 0,
|
2026-10-02 18:50:10 +02:00
|
|
|
flat: 0,
|
2026-10-03 20:54:05 +02:00
|
|
|
}];
|
2026-09-23 19:53:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_iter() {
|
|
|
|
|
let nb_chunks = 10;
|
|
|
|
|
let trkseg = create_track_segment(nb_chunks);
|
|
|
|
|
for (i, trkpt) in trkseg.iter().enumerate() {
|
|
|
|
|
assert!(std::ptr::eq(&trkseg[i], trkpt));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_iter_nth() {
|
|
|
|
|
let nb_chunks = 10;
|
|
|
|
|
let trkseg = create_track_segment(nb_chunks);
|
|
|
|
|
let tenth = trkseg.iter().nth(10);
|
|
|
|
|
assert!(tenth.is_some());
|
|
|
|
|
assert!(std::ptr::eq(&trkseg[10], tenth.unwrap()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_iter_skip() {
|
|
|
|
|
let nb_chunks = 10;
|
|
|
|
|
let trkseg = create_track_segment(nb_chunks);
|
|
|
|
|
for (i, trkpt) in trkseg.iter().enumerate().skip(5) {
|
|
|
|
|
assert!(std::ptr::eq(&trkseg[i], trkpt));
|
2026-09-21 23:49:57 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-03 16:16:47 +02:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_first_last_index() {
|
|
|
|
|
assert!(TrackSegment::default().first_index().is_none());
|
|
|
|
|
assert!(TrackSegment::default().last_index().is_none());
|
|
|
|
|
|
|
|
|
|
let trkseg = create_track_segment(5); // 15 points in chunks of 1..=5
|
|
|
|
|
let first = trkseg.first_index().unwrap();
|
|
|
|
|
let last = trkseg.last_index().unwrap();
|
|
|
|
|
assert_eq!((first.chunk, first.pos, first.flat), (0, 0, 0));
|
|
|
|
|
assert_eq!((last.chunk, last.pos, last.flat), (4, 4, 14));
|
|
|
|
|
assert_eq!(trkseg[last].ele, 14.0);
|
|
|
|
|
assert_eq!(trkseg.locate(14), Some(last));
|
|
|
|
|
assert!(trkseg.next_index(Some(last)).is_none());
|
|
|
|
|
assert!(trkseg.prev_index(Some(first)).is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_prev_next_are_inverse() {
|
|
|
|
|
let trkseg = create_track_segment(6);
|
|
|
|
|
let mut idx = trkseg.last_index();
|
|
|
|
|
let mut visited = vec![];
|
|
|
|
|
while let Some(i) = idx {
|
|
|
|
|
assert_eq!(trkseg.locate(i.flat), Some(i));
|
|
|
|
|
visited.push(i.flat);
|
|
|
|
|
idx = trkseg.prev_index(Some(i));
|
|
|
|
|
}
|
|
|
|
|
assert_eq!(visited, (0..trkseg.len()).rev().collect::<Vec<_>>());
|
|
|
|
|
|
|
|
|
|
let mut idx = trkseg.first_index();
|
|
|
|
|
while let Some(i) = idx {
|
|
|
|
|
let next = trkseg.next_index(Some(i));
|
|
|
|
|
if let Some(n) = next {
|
|
|
|
|
assert_eq!(trkseg.prev_index(Some(n)), Some(i));
|
|
|
|
|
}
|
|
|
|
|
idx = next;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-21 23:49:57 +02:00
|
|
|
}
|