mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-10 07:36:29 +00:00
routing
This commit is contained in:
1 parent
29850c1c8f
commit
594dd8979a
35 files changed
+3135
-1171
No files matched your search
@@ -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)]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
mod anchors;
|
||||
mod simplify;
|
||||
mod smooth;
|
||||
mod timestamps;
|
||||
|
||||
pub use anchors::*;
|
||||
pub use simplify::*;
|
||||
pub use timestamps::*;
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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]));
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user