This commit is contained in:
vcoppe committed 2026-10-07 17:58:17 +02:00
1 parent 29850c1c8f
commit 594dd8979a
35 files changed
+3135 -1171

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
use std::f64::consts::PI;
use crate::{
TrackSegment, TrackSegmentIndex, core::utils::crossarc_lnglat, ramer_douglas_peucker_by,
};
/// Highest zoom level of the map at which an anchor can appear.
pub const MAX_ANCHOR_ZOOM: u8 = 22;
/// Details smaller than this (in meters) do not get an anchor.
const ANCHOR_EPSILON: f64 = 1.0;
const EARTH_RADIUS_METERS: f64 = 6371008.8;
/// Lowest map zoom level at which an anchor selected at `distance` meters from the line of its
/// neighbours is worth showing: the smaller the detail, the higher the zoom. The ends of a
/// segment (no distance) are always shown.
pub fn anchor_zoom(lat: f64, distance: Option<f64>) -> u8 {
let Some(distance) = distance else {
return 0;
};
let width = EARTH_RADIUS_METERS * (lat * PI / 180.0).cos() / distance;
width.log2().round().clamp(0.0, MAX_ANCHOR_ZOOM as f64) as u8
}
/// The anchors of a segment, as `(index, zoom)` sorted by index: the trackpoints kept by the
/// Ramer-Douglas-Peucker simplification of its path, the ones for smaller details being shown at
/// higher zoom levels only.
pub fn compute_anchors(trkseg: &TrackSegment) -> Vec<(usize, u8)> {
let measure = |start: TrackSegmentIndex, end: TrackSegmentIndex, idx: TrackSegmentIndex| {
crossarc_lnglat(
trkseg[start].coordinates,
trkseg[end].coordinates,
trkseg[idx].coordinates,
)
};
ramer_douglas_peucker_by(trkseg, &measure, ANCHOR_EPSILON)
.into_iter()
.map(|(idx, distance)| (idx.flat, anchor_zoom(trkseg[idx].coordinates.lat, distance)))
.collect()
}
#[cfg(test)]
mod tests {
use crate::{LngLat, Trackpoint, TrackpointChunk};
use super::*;
fn segment(points: &[(f64, f64)]) -> TrackSegment {
let mut trkseg = TrackSegment::default();
let mut chunk = TrackpointChunk::default();
for (lng, lat) in points {
chunk.trkpt.push(Trackpoint {
coordinates: LngLat {
lng: *lng,
lat: *lat,
},
..Default::default()
});
}
trkseg.push(chunk);
trkseg
}
#[test]
fn test_zoom_depends_on_the_size_of_the_detail() {
assert_eq!(anchor_zoom(45.0, None), 0);
// 2^13 meters is close to the radius of the earth divided by 1 km
assert_eq!(anchor_zoom(0.0, Some(1_000.0)), 13);
assert!(anchor_zoom(0.0, Some(10.0)) > anchor_zoom(0.0, Some(1_000.0)));
assert_eq!(anchor_zoom(0.0, Some(1e-9)), MAX_ANCHOR_ZOOM);
assert_eq!(anchor_zoom(0.0, Some(1e9)), 0);
// the poles do not give NaN or a panic
assert_eq!(anchor_zoom(90.0, Some(10.0)), 0);
}
#[test]
fn test_anchors_of_a_straight_line_are_its_ends() {
let line: Vec<_> = (0..10).map(|i| (i as f64 * 0.001, 0.0)).collect();
assert_eq!(compute_anchors(&segment(&line)), vec![(0, 0), (9, 0)]);
}
#[test]
fn test_anchors_keep_the_corners() {
// a big corner and a small bump
let points = [
(0.0, 0.0),
(0.01, 0.0),
(0.02, 0.0),
(0.02, 0.01),
(0.02, 0.02),
(0.02, 0.020005),
(0.02, 0.03),
];
let anchors = compute_anchors(&segment(&points));
let indices: Vec<_> = anchors.iter().map(|(i, _)| *i).collect();
assert_eq!(indices, vec![0, 2, 6]);
assert_eq!(anchors[0].1, 0);
assert_eq!(anchors[2].1, 0);
assert!(anchors[1].1 > 0);
}
#[test]
fn test_anchors_of_short_segments() {
assert!(compute_anchors(&segment(&[])).is_empty());
assert_eq!(compute_anchors(&segment(&[(1.0, 2.0)])), vec![(0, 0)]);
assert_eq!(
compute_anchors(&segment(&[(1.0, 2.0), (1.0, 2.1)])),
vec![(0, 0), (1, 0)]
);
}
}
+4
View File
@@ -1,4 +1,8 @@
mod anchors;
mod simplify;
mod smooth;
mod timestamps;
pub use anchors::*;
pub use simplify::*;
pub use timestamps::*;
+38 -18
View File
@@ -10,47 +10,67 @@ pub fn ramer_douglas_peucker<F>(
) -> Vec<TrackSegmentIndex>
where
F: Fn(TrackSegmentIndex) -> (f64, f64),
{
let measure = |start, end, idx| {
let (start, end, pt) = (mapping(start), mapping(end), mapping(idx));
crossarc(start.0, start.1, end.0, end.1, pt.0, pt.1)
};
ramer_douglas_peucker_by(trkseg, &measure, epsilon)
.into_iter()
.map(|(idx, _)| idx)
.collect()
}
/// Ramer-Douglas-Peucker with any distance from a point (the last argument) to the line made of
/// two others (the first ones). Gives the kept points, with the distance at which each of them
/// was selected (`None` for the ends of the segment, which are always kept).
pub fn ramer_douglas_peucker_by<M>(
trkseg: &TrackSegment,
measure: &M,
epsilon: f64,
) -> Vec<(TrackSegmentIndex, Option<f64>)>
where
M: Fn(TrackSegmentIndex, TrackSegmentIndex, TrackSegmentIndex) -> f64,
{
match trkseg.len() {
0 => vec![],
1 => vec![trkseg.first_index().unwrap()],
2 => vec![trkseg.first_index().unwrap(), trkseg.last_index().unwrap()],
1 => vec![(trkseg.first_index().unwrap(), None)],
2 => vec![
(trkseg.first_index().unwrap(), None),
(trkseg.last_index().unwrap(), None),
],
_ => {
let first = trkseg.first_index().unwrap();
let last = trkseg.last_index().unwrap();
let mut indices = vec![first];
ramer_douglas_peucker_helper(trkseg, first, last, mapping, epsilon, &mut indices);
indices.push(last);
indices
let mut kept = vec![(first, None)];
ramer_douglas_peucker_helper(trkseg, first, last, measure, epsilon, &mut kept);
kept.push((last, None));
kept
}
}
}
fn ramer_douglas_peucker_helper<F>(
fn ramer_douglas_peucker_helper<M>(
trkseg: &TrackSegment,
start: TrackSegmentIndex,
end: TrackSegmentIndex,
mapping: &F,
measure: &M,
epsilon: f64,
indices: &mut Vec<TrackSegmentIndex>,
kept: &mut Vec<(TrackSegmentIndex, Option<f64>)>,
) where
F: Fn(TrackSegmentIndex) -> (f64, f64),
M: Fn(TrackSegmentIndex, TrackSegmentIndex, TrackSegmentIndex) -> f64,
{
let mut max_idx = None;
let mut max_dist = 0.0;
let start_pt = mapping(start);
let end_pt = mapping(end);
let mut cur = trkseg.next_index(Some(start));
while let Some(idx) = cur {
if idx == end {
break;
}
let pt = mapping(idx);
let dist = crossarc(start_pt.0, start_pt.1, end_pt.0, end_pt.1, pt.0, pt.1);
let dist = measure(start, end, idx);
if dist > max_dist {
max_idx = Some(idx);
max_dist = dist;
@@ -60,9 +80,9 @@ fn ramer_douglas_peucker_helper<F>(
}
if let Some(idx) = max_idx.filter(|_| max_dist > epsilon) {
ramer_douglas_peucker_helper(trkseg, start, idx, mapping, epsilon, indices);
indices.push(idx);
ramer_douglas_peucker_helper(trkseg, idx, end, mapping, epsilon, indices);
ramer_douglas_peucker_helper(trkseg, start, idx, measure, epsilon, kept);
kept.push((idx, Some(max_dist)));
ramer_douglas_peucker_helper(trkseg, idx, end, measure, epsilon, kept);
}
}
@@ -0,0 +1,346 @@
use crate::{Trackpoint, distance};
/// Replaces the trackpoints `start..end` of `trkpt` by `points`, keeping the timestamps of the
/// whole path consistent. It is the work of the old `replaceTrackPoints` of the TS library.
///
/// The timestamps only matter if the path has some, or if `speed` (in km/h) is given: then the
/// new points that have none get some (a trackpoint after the previous one at `speed`, or one
/// second after it without a speed), and so do the points before or after the range if they
/// had none. `start_time` is the time of the first trackpoint when nothing else gives it.
/// New points that have timestamps are shifted if they are too early to follow the previous
/// trackpoint, and so are the points after the range. With `remove_gaps`, the new points are
/// also brought closer to the previous trackpoint if they come long after it.
///
/// Panics if `start..end` is not inside `trkpt`.
pub fn replace_trackpoints(
trkpt: &mut Vec<Trackpoint>,
start: usize,
end: usize,
mut points: Vec<Trackpoint>,
speed: Option<f64>,
start_time: Option<i64>,
remove_gaps: bool,
) {
assert!(start <= end && end <= trkpt.len());
let has_times = trkpt.first().is_some_and(|first| first.time.is_some());
if speed.is_some() || has_times {
if start > 0 && trkpt[0].time.is_none() {
// the points before the range have no timestamps
let before: Vec<_> = trkpt.drain(..start).collect();
trkpt.splice(0..0, with_timestamps(before, speed, None, start_time));
}
if !points.is_empty() {
let last = start.checked_sub(1).map(|i| &trkpt[i]);
let missing =
points[0].time.is_none() || points.get(1).is_some_and(|p| p.time.is_none());
if missing {
points = with_timestamps(points, speed, last, start_time);
} else if let Some(last) = last {
if is_before(points[0].time, last.time) {
// too early
points = shifted_and_compressed(points, speed, 1.0, last);
} else if remove_gaps {
if same_place(&points[0], last) {
// the same place: the new points start at the previous trackpoint, which
// is the new first one
if is_before(last.time, points[0].time) {
points = shifted_and_compressed(points, speed, 1.0, last);
points.remove(0);
}
} else if matches!((points[0].time, last.time), (Some(new), Some(old)) if new - old > 1000)
{
// another place: the new points start one second after the previous one
let mut artificial_last = points[0].clone();
artificial_last.time = last.time.map(|time| time + 1000);
points = shifted_and_compressed(points, speed, 1.0, &artificial_last);
}
}
}
}
if end < trkpt.len() {
// the points after the range
let last = points
.last()
.or_else(|| start.checked_sub(1).map(|i| &trkpt[i]))
.cloned();
if trkpt[end].time.is_none() {
let after: Vec<_> = trkpt.drain(end..).collect();
let after = with_timestamps(after, speed, last.as_ref(), start_time);
trkpt.extend(after);
} else if let Some(last) = last
&& is_before(trkpt[end].time, last.time)
{
let after: Vec<_> = trkpt.drain(end..).collect();
trkpt.extend(shifted_and_compressed(after, speed, 1.0, &last));
}
}
}
trkpt.splice(start..end, points);
}
fn same_place(a: &Trackpoint, b: &Trackpoint) -> bool {
a.coordinates.lng == b.coordinates.lng && a.coordinates.lat == b.coordinates.lat
}
/// Whether both times are known and `a` is before `b`.
fn is_before(a: Option<i64>, b: Option<i64>) -> bool {
matches!((a, b), (Some(a), Some(b)) if a < b)
}
/// The time at which `b` is reached after `a`, at `speed` km/h: one second later without a
/// speed, unknown if `a` has no time.
///
/// The old code gave an invalid date for a null or negative speed: it is as if there was none.
fn timestamp_after(a: &Trackpoint, b: &Trackpoint, speed: Option<f64>) -> Option<i64> {
let time = a.time?;
match speed.filter(|speed| speed.is_finite() && *speed > 0.0) {
None => Some(time + 1000),
Some(speed) => {
let km = distance(a.coordinates, b.coordinates);
Some(time + (3_600_000.0 * km / speed) as i64)
}
}
}
/// `points` with the timestamps that follow `last` (the trackpoint before them), one after the
/// other. Without `last`, the first one is at `start_time`.
///
/// Unlike the old code, a `last` without a time does not get `start_time` itself, only the copy
/// used here.
fn with_timestamps(
points: Vec<Trackpoint>,
speed: Option<f64>,
last: Option<&Trackpoint>,
start_time: Option<i64>,
) -> Vec<Trackpoint> {
let Some(first) = points.first() else {
return points;
};
let mut last = match last {
Some(last) if last.time.is_some() => last.clone(),
other => {
let mut last = other.unwrap_or(first).clone();
last.time = start_time;
last
}
};
points
.into_iter()
.map(|mut point| {
point.time = timestamp_after(&last, &point, speed);
last = point.clone();
point
})
.collect()
}
/// `points` with their timestamps moved so that the first one follows `last`, and their durations
/// multiplied by `ratio`. The ones that have no timestamp follow the previous one, as in
/// [`with_timestamps`]. Without a time for `last`, or for the first point, none is known.
fn shifted_and_compressed(
points: Vec<Trackpoint>,
speed: Option<f64>,
ratio: f64,
last: &Trackpoint,
) -> Vec<Trackpoint> {
let Some(first) = points.first() else {
return points;
};
let start = timestamp_after(last, first, speed);
let first_time = first.time;
let mut previous = first.clone();
points
.into_iter()
.map(|point| {
let mut shifted = point.clone();
shifted.time = match point.time {
None => timestamp_after(&previous, &point, speed),
Some(time) => start
.zip(first_time)
.map(|(start, first)| start + (ratio * (time - first) as f64) as i64),
};
previous = shifted.clone();
shifted
})
.collect()
}
#[cfg(test)]
mod tests {
use crate::LngLat;
use super::*;
/// A point every 1 km along the equator... about, at 0.009 degrees of longitude.
fn point(i: usize, time: Option<i64>) -> Trackpoint {
Trackpoint {
coordinates: LngLat {
lng: i as f64 * 0.009,
lat: 0.0,
},
time,
..Default::default()
}
}
fn timed(range: std::ops::Range<usize>, start: i64, step: i64) -> Vec<Trackpoint> {
range
.map(|i| point(i, Some(start + step * i as i64)))
.collect()
}
fn untimed(range: std::ops::Range<usize>) -> Vec<Trackpoint> {
range.map(|i| point(i, None)).collect()
}
fn times(trkpt: &[Trackpoint]) -> Vec<Option<i64>> {
trkpt.iter().map(|p| p.time).collect()
}
#[test]
fn test_without_times_nor_speed_it_is_a_splice() {
let mut trkpt = untimed(0..5);
replace_trackpoints(&mut trkpt, 1, 3, untimed(10..13), None, None, false);
assert_eq!(trkpt.len(), 6);
assert!(trkpt.iter().all(|p| p.time.is_none()));
assert_eq!(trkpt[1].coordinates.lng, point(10, None).coordinates.lng);
assert_eq!(trkpt[4].coordinates.lng, point(3, None).coordinates.lng);
}
#[test]
fn test_new_points_follow_the_previous_one_at_the_speed() {
// 1 km in 10 s is 360 km/h
let mut trkpt = timed(0..4, 0, 10_000);
let km = distance(point(0, None).coordinates, point(1, None).coordinates);
let speed = Some(36.0 * km / 0.01); // the time of 1 point to the next is 100 s
replace_trackpoints(&mut trkpt, 2, 3, untimed(2..4), speed, None, false);
let expected = 10_000 + (3_600_000.0 * km / speed.unwrap()) as i64;
assert_eq!(trkpt[2].time, Some(expected));
assert!(trkpt[2].time > trkpt[1].time);
// the points after follow the new ones, they were not too early
assert_eq!(trkpt.len(), 5);
assert_eq!(trkpt[4].time, Some(30_000));
}
#[test]
fn test_without_a_speed_new_points_are_one_second_apart() {
let mut trkpt = timed(0..3, 5_000, 60_000);
replace_trackpoints(&mut trkpt, 1, 2, untimed(10..13), None, None, false);
assert_eq!(
times(&trkpt[..4]),
[Some(5_000), Some(6_000), Some(7_000), Some(8_000)]
);
// what comes after was after, it is kept
assert_eq!(trkpt[4].time, Some(125_000));
}
#[test]
fn test_the_points_after_are_shifted_if_they_are_too_early() {
let mut trkpt = timed(0..4, 0, 10_000);
// 30 new points, one second apart: the last one is at 30 s, but the next original
// trackpoint is at 20 s
replace_trackpoints(&mut trkpt, 1, 2, untimed(10..40), None, None, false);
assert_eq!(trkpt.len(), 33);
assert_eq!(trkpt[1].time, Some(1_000));
assert_eq!(trkpt[30].time, Some(30_000));
// the following points keep the duration between them, after the new ones
assert_eq!(trkpt[31].time, Some(31_000));
assert_eq!(trkpt[32].time, Some(41_000));
}
#[test]
fn test_a_path_without_times_gets_them_from_the_speed_and_start_time() {
let mut trkpt = untimed(0..4);
replace_trackpoints(
&mut trkpt,
3,
4,
untimed(3..6),
Some(10.0),
Some(1_000),
false,
);
let times: Vec<_> = times(&trkpt);
assert_eq!(times[0], Some(1_000));
assert!(times.iter().all(Option::is_some));
assert!(times.windows(2).all(|w| w[0].unwrap() < w[1].unwrap()));
}
#[test]
fn test_an_unusable_speed_is_as_if_there_was_none() {
for speed in [0.0, -5.0, f64::NAN, f64::INFINITY] {
let mut trkpt = timed(0..2, 0, 10_000);
replace_trackpoints(&mut trkpt, 2, 2, untimed(2..4), Some(speed), None, false);
assert_eq!(
times(&trkpt),
[Some(0), Some(10_000), Some(11_000), Some(12_000)],
"{speed}"
);
}
}
#[test]
fn test_new_points_with_times_that_are_too_early_are_shifted() {
let mut trkpt = timed(0..3, 100_000, 10_000);
// they are at 0, 5 s: they are moved to follow the trackpoint at 110 s
let new = vec![point(5, Some(0)), point(6, Some(5_000))];
replace_trackpoints(&mut trkpt, 2, 3, new, None, None, false);
// they follow the previous trackpoint after one second, keeping their own durations
assert_eq!(trkpt[2].time, Some(111_000));
assert_eq!(trkpt[3].time, Some(116_000));
}
#[test]
fn test_remove_gaps_at_the_same_place_and_elsewhere() {
// same place: the first new point is the previous trackpoint
let mut trkpt = timed(0..3, 0, 10_000);
let new = vec![point(2, Some(500_000)), point(3, Some(510_000))];
replace_trackpoints(&mut trkpt, 3, 3, new.clone(), None, None, true);
assert_eq!(trkpt.len(), 4);
// without a speed, it is one second after the previous trackpoint, not at its time
assert_eq!(trkpt[3].time, Some(31_000));
// with a speed, the distance is zero
let mut trkpt = timed(0..3, 0, 10_000);
replace_trackpoints(&mut trkpt, 3, 3, new, Some(10.0), None, true);
assert_eq!(trkpt[3].time, Some(30_000));
// another place: one second after the previous one
let mut trkpt = timed(0..3, 0, 10_000);
let new = vec![point(5, Some(500_000)), point(6, Some(510_000))];
replace_trackpoints(&mut trkpt, 3, 3, new, None, None, true);
assert_eq!(trkpt.len(), 5);
// the old code adds the second twice with no speed, see `timestamp_after`
assert_eq!(trkpt[3].time, Some(22_000));
assert_eq!(trkpt[4].time, Some(32_000));
// without removing the gaps they stay
let mut trkpt = timed(0..3, 0, 10_000);
let new = vec![point(5, Some(500_000)), point(6, Some(510_000))];
replace_trackpoints(&mut trkpt, 3, 3, new, None, None, false);
assert_eq!(trkpt[3].time, Some(500_000));
}
#[test]
fn test_removing_points_adapts_nothing_if_the_next_one_is_after() {
let mut trkpt = timed(0..5, 0, 10_000);
replace_trackpoints(&mut trkpt, 1, 3, vec![], None, None, false);
assert_eq!(times(&trkpt), [Some(0), Some(30_000), Some(40_000)]);
// from the beginning
replace_trackpoints(&mut trkpt, 0, 2, vec![], None, None, false);
assert_eq!(times(&trkpt), [Some(40_000)]);
replace_trackpoints(&mut trkpt, 0, 1, vec![], None, None, false);
assert!(trkpt.is_empty());
}
#[test]
fn test_points_without_a_known_previous_time_get_none() {
// nothing gives a time to start with
let mut trkpt = untimed(0..2);
replace_trackpoints(&mut trkpt, 2, 2, untimed(2..4), Some(10.0), None, false);
assert!(trkpt.iter().all(|p| p.time.is_none()));
}
}
+116 -1
View File
@@ -2,7 +2,7 @@ use std::{ops::Index, rc::Rc};
use uuid::Uuid;
use crate::{Trackpoint, TrackpointChunk};
use crate::{Trackpoint, TrackpointChunk, compute_anchors};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TrackSegmentId(pub Uuid);
@@ -86,6 +86,72 @@ impl TrackSegment {
self.fill(&mut pending, points);
}
self.flush(&mut pending);
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();
}
}
fn fill(
@@ -309,9 +375,58 @@ mod tests {
assert!(trkseg.first_index().is_none());
}
fn anchors(trkseg: &TrackSegment) -> Vec<Option<u8>> {
trkseg.iter().map(|p| p.anchor).collect()
}
#[test]
fn test_splice_keeps_the_ends_anchors() {
let mut trkseg = TrackSegment::default();
assert_eq!(anchors(&trkseg), vec![]);
trkseg.splice(0, 0, points(&[0.0, 1.0, 2.0]));
assert_eq!(anchors(&trkseg), [Some(0), None, Some(0)]);
// new ends: the previous ones stay anchors
trkseg.splice(3, 3, points(&[3.0]));
trkseg.splice(0, 0, points(&[-1.0]));
assert_eq!(anchors(&trkseg), [Some(0), Some(0), None, Some(0), Some(0)]);
// removing the ends: the new ones become anchors
trkseg.splice(4, 5, vec![]);
trkseg.splice(0, 2, vec![]);
assert_eq!(anchors(&trkseg), [Some(0), Some(0)]);
// a single point is both ends
trkseg.splice(1, 2, vec![]);
assert_eq!(anchors(&trkseg), [Some(0)]);
}
#[test]
fn test_compute_anchors_replaces_the_previous_ones() {
let mut trkseg = TrackSegment::default();
let line: Vec<_> = (0..10)
.map(|i| Trackpoint {
coordinates: crate::LngLat {
lng: i as f64 * 0.001,
lat: 0.0,
},
anchor: Some(5),
..Default::default()
})
.collect();
trkseg.splice(0, 0, line);
trkseg.compute_anchors();
let mut expected = vec![None; 10];
expected[0] = Some(0);
expected[9] = Some(0);
assert_eq!(anchors(&trkseg), expected);
}
#[test]
fn test_splice_keeps_untouched_chunks_shared() {
let mut trkseg = create_track_segment(5);
// the ends are anchors already, or their chunks would be copied
trkseg.ensure_end_anchors();
let before = trkseg.chunks.clone();
// inside the third chunk only
trkseg.splice(4, 5, points(&[-1.0]));
+4
View File
@@ -17,4 +17,8 @@ pub struct Trackpoint {
pub sac_scale: Option<u8>,
/// Code of the mountain biking scale, see [`crate::TrackpointCategories`].
pub mtb_scale: Option<u8>,
/// Whether the trackpoint is an anchor of the routing tool (a point the user can drag to
/// reroute the segment): the lowest map zoom level at which it is shown. The first and last
/// trackpoints of a segment are always anchors that are always shown (zoom 0).
pub anchor: Option<u8>,
}
+1
View File
@@ -179,6 +179,7 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
{
// `push` ignores the chunk if it is empty
trkseg.push(std::mem::take(&mut trkpt_chunk));
trkseg.compute_anchors();
trk.trkseg.push(trkseg);
}
}