mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-09 23:26:31 +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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{Apply, CommandError, State};
|
||||
use crate::{
|
||||
Clean, Crop, Delete, DeleteAll, DeleteWaypoint, Duplicate, EditWaypoint, Elevation, Extract,
|
||||
Load, LoadFiles, Merge, Metadata, Move, MoveWaypoint, New, NewTrack, NewTrackSegment,
|
||||
NewWaypoint, Paste, Reverse, SpliceTrackpoints, Split, Style, Time,
|
||||
ChangeLoopStart, Clean, Crop, Delete, DeleteAll, DeleteWaypoint, Duplicate, EditWaypoint,
|
||||
Elevation, Extract, InsertAnchor, Load, LoadFiles, Merge, Metadata, Move, MoveWaypoint, New,
|
||||
NewTrack, NewTrackSegment, NewWaypoint, Paste, Reverse, Route, Split, Style, Time,
|
||||
};
|
||||
|
||||
/// A user action that edits the files. Each variant wraps the command's own struct, whose
|
||||
@@ -26,7 +26,7 @@ pub enum Command<'a> {
|
||||
NewTrack(NewTrack),
|
||||
NewTrackSegment(NewTrackSegment),
|
||||
Reverse(Reverse),
|
||||
SpliceTrackpoints(SpliceTrackpoints<'a>),
|
||||
Route(Route<'a>),
|
||||
NewWaypoint(NewWaypoint<'a>),
|
||||
EditWaypoint(EditWaypoint<'a>),
|
||||
MoveWaypoint(MoveWaypoint),
|
||||
@@ -35,8 +35,10 @@ pub enum Command<'a> {
|
||||
Time(Time),
|
||||
Merge(Merge),
|
||||
Extract(Extract),
|
||||
InsertAnchor(InsertAnchor),
|
||||
Elevation(Elevation<'a>),
|
||||
Clean(Clean),
|
||||
ChangeLoopStart(ChangeLoopStart),
|
||||
}
|
||||
|
||||
impl Apply for Command<'_> {
|
||||
@@ -56,7 +58,7 @@ impl Apply for Command<'_> {
|
||||
Command::NewTrack(c) => c.apply(state),
|
||||
Command::NewTrackSegment(c) => c.apply(state),
|
||||
Command::Reverse(c) => c.apply(state),
|
||||
Command::SpliceTrackpoints(c) => c.apply(state),
|
||||
Command::Route(c) => c.apply(state),
|
||||
Command::NewWaypoint(c) => c.apply(state),
|
||||
Command::EditWaypoint(c) => c.apply(state),
|
||||
Command::MoveWaypoint(c) => c.apply(state),
|
||||
@@ -65,8 +67,10 @@ impl Apply for Command<'_> {
|
||||
Command::Time(c) => c.apply(state),
|
||||
Command::Merge(c) => c.apply(state),
|
||||
Command::Extract(c) => c.apply(state),
|
||||
Command::InsertAnchor(c) => c.apply(state),
|
||||
Command::Elevation(c) => c.apply(state),
|
||||
Command::Clean(c) => c.apply(state),
|
||||
Command::ChangeLoopStart(c) => c.apply(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,18 @@ mod tests {
|
||||
#[test]
|
||||
fn test_metadata_applies_to_selected_file() {
|
||||
let mut fx = Fixture::default();
|
||||
New { name: "first" }.apply(&mut fx.state()).unwrap();
|
||||
New { name: "second" }.apply(&mut fx.state()).unwrap(); // now selected
|
||||
New {
|
||||
name: "first",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
New {
|
||||
name: "second",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap(); // now selected
|
||||
let selected = *fx.selected_files().iter().next().unwrap();
|
||||
|
||||
Metadata {
|
||||
@@ -95,7 +105,12 @@ mod tests {
|
||||
|
||||
/// A new file with `nb_tracks` tracks (without name), selected.
|
||||
fn file_with_tracks(fx: &mut Fixture, nb_tracks: usize) {
|
||||
New { name: "file" }.apply(&mut fx.state()).unwrap();
|
||||
New {
|
||||
name: "file",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
for _ in 0..nb_tracks {
|
||||
NewTrack.apply(&mut fx.state()).unwrap();
|
||||
}
|
||||
|
||||
@@ -37,7 +37,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_new_track() {
|
||||
let mut fx = Fixture::default();
|
||||
New { name: "file" }.apply(&mut fx.state()).unwrap();
|
||||
New {
|
||||
name: "file",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
NewTrack.apply(&mut fx.state()).unwrap();
|
||||
NewTrack.apply(&mut fx.state()).unwrap();
|
||||
let file = fx.files.values().next().unwrap();
|
||||
@@ -48,7 +53,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_new_track_inherits_agreeing_style_only() {
|
||||
let mut fx = Fixture::default();
|
||||
New { name: "file" }.apply(&mut fx.state()).unwrap();
|
||||
New {
|
||||
name: "file",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
NewTrack.apply(&mut fx.state()).unwrap();
|
||||
NewTrack.apply(&mut fx.state()).unwrap();
|
||||
let file = std::rc::Rc::make_mut(fx.files.values_mut().next().unwrap());
|
||||
|
||||
@@ -26,7 +26,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_new_track_segment_on_each_track_of_selected_file() {
|
||||
let mut fx = Fixture::default();
|
||||
New { name: "file" }.apply(&mut fx.state()).unwrap();
|
||||
New {
|
||||
name: "file",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
NewTrack.apply(&mut fx.state()).unwrap();
|
||||
NewTrack.apply(&mut fx.state()).unwrap();
|
||||
NewTrackSegment.apply(&mut fx.state()).unwrap();
|
||||
|
||||
@@ -130,7 +130,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_delete_selected_files() {
|
||||
let (mut fx, id) = loaded();
|
||||
crate::New { name: "keep" }.apply(&mut fx.state()).unwrap();
|
||||
crate::New {
|
||||
name: "keep",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let keep = fx.order.0[1];
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([id]),
|
||||
@@ -263,7 +268,12 @@ mod tests {
|
||||
},
|
||||
] {
|
||||
let (mut fx, id) = loaded();
|
||||
crate::New { name: "keep" }.apply(&mut fx.state()).unwrap();
|
||||
crate::New {
|
||||
name: "keep",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let keep = fx.order.0[1];
|
||||
fx.selection = selection(id, &fx);
|
||||
Delete { whole_files: true }.apply(&mut fx.state()).unwrap();
|
||||
|
||||
@@ -28,8 +28,18 @@ mod tests {
|
||||
DeleteAll.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
crate::New { name: "a" }.apply(&mut fx.state()).unwrap();
|
||||
crate::New { name: "b" }.apply(&mut fx.state()).unwrap();
|
||||
crate::New {
|
||||
name: "a",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
crate::New {
|
||||
name: "b",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert!(DeleteAll.apply(&mut fx.state()).is_ok());
|
||||
assert!(fx.files.is_empty());
|
||||
assert!(fx.order.0.is_empty());
|
||||
|
||||
@@ -195,7 +195,12 @@ mod tests {
|
||||
fn test_duplicate_files_go_after_last_selected() {
|
||||
let mut fx = Fixture::default();
|
||||
for name in ["a", "b", "c"] {
|
||||
crate::New { name }.apply(&mut fx.state()).unwrap();
|
||||
crate::New {
|
||||
name,
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
}
|
||||
let [a, b, c] = [fx.order.0[0], fx.order.0[1], fx.order.0[2]];
|
||||
fx.selection = Selection::File {
|
||||
|
||||
@@ -82,7 +82,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_load_invalid_file_changes_nothing() {
|
||||
let mut fx = Fixture::default();
|
||||
crate::New { name: "keep" }.apply(&mut fx.state()).unwrap();
|
||||
crate::New {
|
||||
name: "keep",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let selected = fx.selected_files();
|
||||
|
||||
let result = Load {
|
||||
@@ -132,9 +137,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_load_several_files() {
|
||||
let mut fx = Fixture::default();
|
||||
crate::New { name: "before" }
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
crate::New {
|
||||
name: "before",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let before = fx.order.0[0];
|
||||
let (a, b, c) = (gpx("a"), gpx("b"), gpx("c"));
|
||||
|
||||
@@ -178,7 +186,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_load_several_invalid_files_changes_nothing() {
|
||||
let mut fx = Fixture::default();
|
||||
crate::New { name: "keep" }.apply(&mut fx.state()).unwrap();
|
||||
crate::New {
|
||||
name: "keep",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let selected = fx.selected_files();
|
||||
|
||||
let result = LoadFiles {
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
use crate::{Apply, CommandError, File, State, produce};
|
||||
use crate::{Apply, CommandError, File, LngLat, State, Track, TrackSegment, Trackpoint, produce};
|
||||
|
||||
/// Creates an empty file and selects it. With a `trackpoint`, the file starts with a track and a
|
||||
/// segment that hold it: it is how the routing tool starts a route on an empty map, in a single
|
||||
/// undo step.
|
||||
#[derive(Debug)]
|
||||
pub struct New<'a> {
|
||||
pub name: &'a str,
|
||||
pub trackpoint: Option<NewTrackpoint>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct NewTrackpoint {
|
||||
pub lng: f64,
|
||||
pub lat: f64,
|
||||
pub ele: f64,
|
||||
}
|
||||
|
||||
impl Apply for New<'_> {
|
||||
@@ -10,6 +21,26 @@ impl Apply for New<'_> {
|
||||
produce(state, |_| {
|
||||
let mut file = File::default();
|
||||
file.info.name = self.name.to_owned();
|
||||
if let Some(point) = self.trackpoint {
|
||||
let mut segment = TrackSegment::default();
|
||||
// the ends of a segment are anchors of the routing tool
|
||||
segment.splice(
|
||||
0,
|
||||
0,
|
||||
vec![Trackpoint {
|
||||
coordinates: LngLat {
|
||||
lng: point.lng,
|
||||
lat: point.lat,
|
||||
},
|
||||
ele: point.ele,
|
||||
..Default::default()
|
||||
}],
|
||||
);
|
||||
file.trk.push(Track {
|
||||
trkseg: vec![segment],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
vec![file]
|
||||
});
|
||||
Ok(())
|
||||
@@ -25,7 +56,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_new() {
|
||||
let mut fx = Fixture::default();
|
||||
assert!(New { name: "new" }.apply(&mut fx.state()).is_ok());
|
||||
assert!(
|
||||
New {
|
||||
name: "new",
|
||||
trackpoint: None
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(fx.files.len(), 1);
|
||||
let file = fx.files.values().next().unwrap();
|
||||
assert_eq!(file.info.name, "new");
|
||||
@@ -33,4 +71,31 @@ mod tests {
|
||||
assert_eq!(fx.selected_files(), [file.id].into());
|
||||
assert_eq!(fx.order.0, vec![file.id]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_with_a_trackpoint() {
|
||||
let mut fx = Fixture::default();
|
||||
let point = NewTrackpoint {
|
||||
lng: 1.0,
|
||||
lat: 2.0,
|
||||
ele: 3.0,
|
||||
};
|
||||
New {
|
||||
name: "new",
|
||||
trackpoint: Some(point),
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = fx.files.values().next().unwrap();
|
||||
assert_eq!(file.trk.len(), 1);
|
||||
assert_eq!(file.trk[0].trkseg.len(), 1);
|
||||
let segment = &file.trk[0].trkseg[0];
|
||||
assert_eq!(segment.len(), 1);
|
||||
assert_eq!(segment[0].coordinates.lng, 1.0);
|
||||
assert_eq!(segment[0].coordinates.lat, 2.0);
|
||||
assert_eq!(segment[0].ele, 3.0);
|
||||
// the only trackpoint is an anchor of the routing tool
|
||||
assert_eq!(segment[0].anchor, Some(0));
|
||||
assert_eq!(fx.selected_files(), [file.id].into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Apply, CommandError, State, Trackpoint, replace_trackpoints};
|
||||
|
||||
use super::route::find_target;
|
||||
|
||||
/// Makes the trackpoint `index` of the selection (see `RoutingBuffer`) the start of its segment,
|
||||
/// which is a loop: the trackpoints before it, and itself, are copied after the last trackpoint
|
||||
/// of the segment, and the original ones are removed. The trackpoint is both the first and the
|
||||
/// last one, which closes the loop on it.
|
||||
///
|
||||
/// The copies keep their durations, and start one second after the previous last trackpoint if
|
||||
/// the segment has timestamps (see [`replace_trackpoints`]).
|
||||
///
|
||||
/// It is how the routing tool starts a loop somewhere else. The first and last trackpoints are
|
||||
/// anchors shown at every zoom level, as any end of a segment.
|
||||
#[derive(Debug)]
|
||||
pub struct ChangeLoopStart {
|
||||
pub index: u32,
|
||||
}
|
||||
|
||||
impl Apply for ChangeLoopStart {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let index = self.index as usize;
|
||||
let target = find_target(state, index, index)?.ok_or(CommandError::NothingToDo)?;
|
||||
let mut file = (*state.files[&target.location.file_id]).clone();
|
||||
let segment = &mut file.trk[target.location.trk].trkseg[target.location.seg];
|
||||
let (start, len) = (target.start, segment.len());
|
||||
if start == 0 || start >= len {
|
||||
// it already is the start, or it is not a trackpoint
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
// the trackpoint itself ends the loop as well
|
||||
let moved: Vec<Trackpoint> = segment.iter().take(start + 1).cloned().collect();
|
||||
// the copies are shifted after the last trackpoint, which is all that matters of the rest
|
||||
let mut tail = vec![segment[len - 1].clone()];
|
||||
replace_trackpoints(&mut tail, 1, 1, moved, None, None, false);
|
||||
tail.remove(0);
|
||||
|
||||
// only the chunks at the ends of the segment change: the others stay shared
|
||||
segment.splice(len, len, tail);
|
||||
segment.splice(0, start, vec![]);
|
||||
segment.rev_id = Default::default();
|
||||
state.files.insert(target.location.file_id, Rc::new(file));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{FileId, Load, Selection, TrackSegment, engine::command::fixture::Fixture};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn loaded(path: &str) -> (Fixture, FileId) {
|
||||
let mut fx = Fixture::default();
|
||||
let data = std::fs::read(path).unwrap();
|
||||
Load {
|
||||
data: &data,
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let id = fx.order.0[0];
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([id]),
|
||||
};
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
fn first_segment(fx: &Fixture, id: FileId) -> TrackSegment {
|
||||
fx.files[&id].trk[0].trkseg[0].clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_points_before_move_to_the_end_and_the_loop_closes_on_the_start() {
|
||||
let (mut fx, id) = loaded("data/simple.gpx");
|
||||
let before = first_segment(&fx, id);
|
||||
let n = before.len();
|
||||
assert!(n > 10);
|
||||
|
||||
ChangeLoopStart { index: 4 }.apply(&mut fx.state()).unwrap();
|
||||
let after = first_segment(&fx, id);
|
||||
// the trackpoint is repeated at the end
|
||||
assert_eq!(after.len(), n + 1);
|
||||
assert_ne!(after.rev_id, before.rev_id);
|
||||
let lng = |s: &TrackSegment, i: usize| s[i].coordinates.lng;
|
||||
for i in 0..=n {
|
||||
assert_eq!(lng(&after, i), lng(&before, (i + 4) % n), "{i}");
|
||||
}
|
||||
assert_eq!(lng(&after, n), lng(&after, 0));
|
||||
// the start and the end of the new segment are anchors, whatever they were
|
||||
assert_eq!(after[0].anchor, Some(0));
|
||||
assert_eq!(after[n].anchor, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_moved_part_is_shifted_after_the_last_point() {
|
||||
let (mut fx, id) = loaded("data/with_time.gpx");
|
||||
let before = first_segment(&fx, id);
|
||||
let n = before.len();
|
||||
ChangeLoopStart { index: 30 }
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let after = first_segment(&fx, id);
|
||||
assert_eq!(after.len(), n + 1);
|
||||
let times: Vec<_> = after.iter().map(|p| p.time.unwrap()).collect();
|
||||
assert!(times.windows(2).all(|w| w[0] < w[1]), "{times:?}");
|
||||
// the part that was after the index is untouched
|
||||
for i in 0..n - 30 {
|
||||
assert_eq!(after[i].time, before[30 + i].time);
|
||||
}
|
||||
// the copies follow the last point by a second, and keep their durations
|
||||
let last = before[n - 1].time.unwrap();
|
||||
assert_eq!(after[n - 30].time, Some(last + 1000));
|
||||
for i in n - 30..n {
|
||||
let original = i - (n - 30);
|
||||
assert_eq!(
|
||||
after[i + 1].time.unwrap() - after[i].time.unwrap(),
|
||||
before[original + 1].time.unwrap() - before[original].time.unwrap(),
|
||||
"{i}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nothing_to_do_on_the_start_or_out_of_the_trackpoints() {
|
||||
let (mut fx, id) = loaded("data/simple.gpx");
|
||||
let before = fx.files[&id].clone();
|
||||
assert_eq!(
|
||||
ChangeLoopStart { index: 0 }.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
let total = first_segment(&fx, id).len() as u32;
|
||||
assert!(matches!(
|
||||
ChangeLoopStart { index: total + 5 }.apply(&mut fx.state()),
|
||||
Err(CommandError::InvalidData(_))
|
||||
));
|
||||
assert!(Rc::ptr_eq(&fx.files[&id], &before));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Apply, CommandError, LngLat, SegmentLocation, State, Trackpoint, distance, projected};
|
||||
|
||||
/// Under this distance (in meters) to a trackpoint, that trackpoint becomes the anchor instead of
|
||||
/// a new one being inserted next to it.
|
||||
const SNAP_DISTANCE: f64 = 1.0;
|
||||
|
||||
/// Makes an anchor of the routing tool, shown at every zoom level, of the point of the selected
|
||||
/// segments that is the closest to the given coordinates. That is the trackpoint itself if the
|
||||
/// coordinates are on one, otherwise a new trackpoint is inserted on the path, at the position
|
||||
/// of the coordinates projected on it, with an elevation and a time in between the ones of its
|
||||
/// neighbours.
|
||||
#[derive(Debug)]
|
||||
pub struct InsertAnchor {
|
||||
pub lng: f64,
|
||||
pub lat: f64,
|
||||
}
|
||||
|
||||
/// Where the anchor goes: between the trackpoints `before` and `before + 1` of a segment, at
|
||||
/// `ratio` of the distance from the first one, which is `projection`. `distance` is the one from
|
||||
/// there to the coordinates, in meters.
|
||||
struct Position {
|
||||
location: SegmentLocation,
|
||||
before: usize,
|
||||
ratio: f64,
|
||||
projection: LngLat,
|
||||
distance: f64,
|
||||
}
|
||||
|
||||
impl Apply for InsertAnchor {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let target = LngLat {
|
||||
lng: self.lng,
|
||||
lat: self.lat,
|
||||
};
|
||||
let mut closest: Option<Position> = None;
|
||||
for location in state
|
||||
.selection
|
||||
.segment_locations(state.files, &state.order.0)
|
||||
{
|
||||
let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg];
|
||||
let mut points = segment.iter();
|
||||
let Some(mut previous) = points.next() else {
|
||||
continue;
|
||||
};
|
||||
if segment.len() == 1 {
|
||||
// a single point is its own path
|
||||
let d = distance(previous.coordinates, target) * 1000.0;
|
||||
if closest.as_ref().is_none_or(|c| d < c.distance) {
|
||||
closest = Some(Position {
|
||||
location,
|
||||
before: 0,
|
||||
ratio: 0.0,
|
||||
projection: previous.coordinates,
|
||||
distance: d,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (i, point) in points.enumerate() {
|
||||
let projection = projected(previous.coordinates, point.coordinates, target);
|
||||
let d = distance(projection, target) * 1000.0;
|
||||
if closest.as_ref().is_none_or(|c| d < c.distance) {
|
||||
let length = distance(previous.coordinates, point.coordinates);
|
||||
let ratio = if length > 0.0 {
|
||||
distance(previous.coordinates, projection) / length
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
closest = Some(Position {
|
||||
location,
|
||||
before: i,
|
||||
ratio,
|
||||
projection,
|
||||
distance: d,
|
||||
});
|
||||
}
|
||||
previous = point;
|
||||
}
|
||||
}
|
||||
let closest = closest.ok_or(CommandError::NothingToDo)?;
|
||||
|
||||
let mut file = (*state.files[&closest.location.file_id]).clone();
|
||||
let segment = &mut file.trk[closest.location.trk].trkseg[closest.location.seg];
|
||||
let before = closest.before;
|
||||
// on a trackpoint already, if it is closer than `SNAP_DISTANCE`
|
||||
let snapped = if segment.len() == 1 {
|
||||
Some(0)
|
||||
} else {
|
||||
let (a, b) = (&segment[before], &segment[before + 1]);
|
||||
let meters = 1000.0 * distance(a.coordinates, b.coordinates);
|
||||
if closest.ratio * meters < SNAP_DISTANCE {
|
||||
Some(before)
|
||||
} else if (1.0 - closest.ratio) * meters < SNAP_DISTANCE {
|
||||
Some(before + 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(index) = snapped {
|
||||
if segment[index].anchor == Some(0) {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
segment.set_anchor(index, 0);
|
||||
} else {
|
||||
let (a, b) = (&segment[before], &segment[before + 1]);
|
||||
let ratio = closest.ratio;
|
||||
let mut point: Trackpoint = a.clone();
|
||||
point.coordinates = closest.projection;
|
||||
point.ele = (1.0 - ratio) * a.ele + ratio * b.ele;
|
||||
point.time = a
|
||||
.time
|
||||
.zip(b.time)
|
||||
.map(|(a, b)| ((1.0 - ratio) * a as f64 + ratio * b as f64) as i64);
|
||||
point.anchor = Some(0);
|
||||
segment.splice(before + 1, before + 1, vec![point]);
|
||||
}
|
||||
segment.rev_id = Default::default();
|
||||
state.files.insert(closest.location.file_id, Rc::new(file));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{FileId, Load, Selection, engine::command::fixture::Fixture};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn loaded(path: &str) -> (Fixture, FileId) {
|
||||
let mut fx = Fixture::default();
|
||||
let data = std::fs::read(path).unwrap();
|
||||
Load {
|
||||
data: &data,
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let id = fx.order.0[0];
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([id]),
|
||||
};
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
fn first_segment(fx: &Fixture, id: FileId) -> crate::TrackSegment {
|
||||
fx.files[&id].trk[0].trkseg[0].clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_a_point_is_inserted_between_its_neighbours() {
|
||||
let (mut fx, id) = loaded("data/with_time.gpx");
|
||||
let before = first_segment(&fx, id);
|
||||
let (a, b) = (&before[10], &before[11]);
|
||||
// a bit off the line, closer to the middle of the two points
|
||||
let target = LngLat {
|
||||
lng: (a.coordinates.lng + b.coordinates.lng) / 2.0 + 1e-6,
|
||||
lat: (a.coordinates.lat + b.coordinates.lat) / 2.0 + 1e-6,
|
||||
};
|
||||
InsertAnchor {
|
||||
lng: target.lng,
|
||||
lat: target.lat,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let after = first_segment(&fx, id);
|
||||
assert_eq!(after.len(), before.len() + 1);
|
||||
assert_ne!(after.rev_id, before.rev_id);
|
||||
let point = &after[11];
|
||||
assert_eq!(point.anchor, Some(0));
|
||||
assert_eq!(after[10].coordinates.lng, a.coordinates.lng);
|
||||
assert_eq!(after[12].coordinates.lng, b.coordinates.lng);
|
||||
// between its neighbours, with the interpolated elevation and time
|
||||
let ratio = (point.time.unwrap() - a.time.unwrap()) as f64
|
||||
/ (b.time.unwrap() - a.time.unwrap()) as f64;
|
||||
assert!((0.3..0.7).contains(&ratio), "{ratio}");
|
||||
let expected = a.ele + ratio * (b.ele - a.ele);
|
||||
assert!(
|
||||
(point.ele - expected).abs() < 0.5,
|
||||
"{} {expected}",
|
||||
point.ele
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_a_trackpoint_that_is_hit_becomes_the_anchor() {
|
||||
let (mut fx, id) = loaded("data/with_time.gpx");
|
||||
let before = first_segment(&fx, id);
|
||||
assert_eq!(before[10].anchor.is_some(), before[10].anchor == Some(0));
|
||||
let point = &before[10];
|
||||
InsertAnchor {
|
||||
lng: point.coordinates.lng,
|
||||
lat: point.coordinates.lat,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let after = first_segment(&fx, id);
|
||||
assert_eq!(after.len(), before.len());
|
||||
let anchors = (0..after.len())
|
||||
.filter(|i| after[*i].anchor == Some(0))
|
||||
.count();
|
||||
assert!(anchors >= 3, "{anchors}");
|
||||
assert!(
|
||||
after
|
||||
.iter()
|
||||
.zip(before.iter())
|
||||
.all(|(a, b)| a.coordinates.lng == b.coordinates.lng)
|
||||
);
|
||||
// doing it again changes nothing
|
||||
let again = InsertAnchor {
|
||||
lng: point.coordinates.lng,
|
||||
lat: point.coordinates.lat,
|
||||
}
|
||||
.apply(&mut fx.state());
|
||||
assert!(again.is_ok() || again == Err(CommandError::NothingToDo));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nothing_to_do_without_a_segment() {
|
||||
let (mut fx, _) = loaded("data/with_time.gpx");
|
||||
fx.selection = Selection::Empty;
|
||||
assert_eq!(
|
||||
InsertAnchor { lng: 1.0, lat: 1.0 }.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,29 @@
|
||||
mod change_loop_start;
|
||||
mod clean;
|
||||
mod crop;
|
||||
mod edit_waypoint;
|
||||
mod elevation;
|
||||
mod extract;
|
||||
mod insert_anchor;
|
||||
mod merge;
|
||||
mod move_waypoint;
|
||||
mod new_waypoint;
|
||||
mod reverse;
|
||||
mod splice_trackpoints;
|
||||
mod route;
|
||||
mod split;
|
||||
mod time;
|
||||
|
||||
pub use change_loop_start::*;
|
||||
pub use clean::*;
|
||||
pub use crop::*;
|
||||
pub use edit_waypoint::*;
|
||||
pub use elevation::*;
|
||||
pub use extract::*;
|
||||
pub use insert_anchor::*;
|
||||
pub use merge::*;
|
||||
pub use move_waypoint::*;
|
||||
pub use new_waypoint::*;
|
||||
pub use reverse::*;
|
||||
pub use splice_trackpoints::*;
|
||||
pub use route::*;
|
||||
pub use split::*;
|
||||
pub use time::*;
|
||||
@@ -114,8 +114,18 @@ mod tests {
|
||||
#[test]
|
||||
fn test_new_waypoint_in_the_selected_file() {
|
||||
let mut fx = Fixture::default();
|
||||
New { name: "a" }.apply(&mut fx.state()).unwrap();
|
||||
New { name: "b" }.apply(&mut fx.state()).unwrap();
|
||||
New {
|
||||
name: "a",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
New {
|
||||
name: "b",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let (a, b) = (fx.order.0[0], fx.order.0[1]);
|
||||
let rev = fx.files[&b].wpt_rev_id;
|
||||
|
||||
@@ -155,7 +165,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_empty_fields_are_not_set() {
|
||||
let mut fx = Fixture::default();
|
||||
New { name: "a" }.apply(&mut fx.state()).unwrap();
|
||||
New {
|
||||
name: "a",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let a = fx.order.0[0];
|
||||
NewWaypoint {
|
||||
lng: 1.0,
|
||||
@@ -177,7 +192,12 @@ mod tests {
|
||||
fn test_each_selected_file_gets_its_own_waypoint() {
|
||||
let mut fx = Fixture::default();
|
||||
for name in ["a", "b", "c"] {
|
||||
New { name }.apply(&mut fx.state()).unwrap();
|
||||
New {
|
||||
name,
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
}
|
||||
let ids = fx.order.0.clone();
|
||||
fx.selection = Selection::File {
|
||||
@@ -210,7 +230,12 @@ mod tests {
|
||||
},
|
||||
] {
|
||||
let mut fx = Fixture::default();
|
||||
New { name: "a" }.apply(&mut fx.state()).unwrap();
|
||||
New {
|
||||
name: "a",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let a = fx.order.0[0];
|
||||
fx.selection = selection(a);
|
||||
new_waypoint("w").apply(&mut fx.state()).unwrap();
|
||||
@@ -221,7 +246,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_nothing_selected_or_unknown_file() {
|
||||
let mut fx = Fixture::default();
|
||||
New { name: "a" }.apply(&mut fx.state()).unwrap();
|
||||
New {
|
||||
name: "a",
|
||||
trackpoint: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
fx.selection = Selection::Empty;
|
||||
assert_eq!(
|
||||
new_waypoint("w").apply(&mut fx.state()),
|
||||
|
||||
@@ -0,0 +1,810 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Apply, Categories, CommandError, FileId, SegmentLocation, Selection, State, Statistics, Track,
|
||||
TrackSegment, Trackpoint, distance, replace_trackpoints,
|
||||
};
|
||||
|
||||
/// The values of an OSM attribute (surface, highway...) of the new trackpoints of a [`Route`], as
|
||||
/// the intervals of consecutive trackpoints that share one: `starts[i]` is the index of the first
|
||||
/// trackpoint of the interval `i`, and `values[i]` its value, 0 when unknown, else 1 + the index
|
||||
/// of its name in `names`. No intervals means that the attribute is unknown everywhere.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct RouteCategory<'a> {
|
||||
pub starts: &'a [u32],
|
||||
pub values: &'a [u8],
|
||||
pub names: &'a [String],
|
||||
}
|
||||
|
||||
impl RouteCategory<'_> {
|
||||
/// The code in `categories` of the value of each of the `len` new trackpoints. A name is
|
||||
/// added to the table of the categories if it is new.
|
||||
fn codes(
|
||||
&self,
|
||||
categories: &mut Categories,
|
||||
len: usize,
|
||||
) -> Result<Vec<Option<u8>>, CommandError> {
|
||||
if self.starts.len() != self.values.len() {
|
||||
return Err(invalid("intervals of different lengths"));
|
||||
}
|
||||
let mut codes = vec![None; len];
|
||||
for (i, (start, value)) in self.starts.iter().zip(self.values).enumerate() {
|
||||
let end = self.starts.get(i + 1).map_or(len, |end| *end as usize);
|
||||
let start = *start as usize;
|
||||
if start > end || end > len {
|
||||
return Err(invalid("intervals out of the trackpoints"));
|
||||
}
|
||||
if *value == 0 {
|
||||
continue;
|
||||
}
|
||||
let name = self
|
||||
.names
|
||||
.get(usize::from(*value) - 1)
|
||||
.ok_or_else(|| invalid("unknown name of category"))?;
|
||||
codes[start..end].fill(categories.code(name));
|
||||
}
|
||||
Ok(codes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the trackpoints `start..end` of a segment of the selection by the given ones (a pure
|
||||
/// insertion when `start == end`, a pure removal when there are no new points). It is how the
|
||||
/// routing tool reroutes a part of a segment, extends it, or removes the end of it.
|
||||
///
|
||||
/// `start` and `end` are indices in the trackpoints of the selection (see `RoutingBuffer`): the
|
||||
/// range has to be in a single segment, and `start == end == the number of trackpoints` appends
|
||||
/// to the last one. If the selection has no trackpoints at all, they go to its last segment, which
|
||||
/// is created if needed (in the last track, or in a new one, of the last selected file).
|
||||
///
|
||||
/// The timestamps follow: the new points get some if the segment has, at the speed it had on the
|
||||
/// replaced part (see [`replace_trackpoints`]).
|
||||
///
|
||||
/// The new points listed in `anchors` become anchors of the routing tool, shown at every zoom
|
||||
/// level. The anchors of the segment out of the range are kept, and the ends of the segment are
|
||||
/// anchors anyway. The ones right before and after the range, between which the route goes, become
|
||||
/// shown at every zoom level as well.
|
||||
#[derive(Debug)]
|
||||
pub struct Route<'a> {
|
||||
pub start: u32,
|
||||
pub end: u32,
|
||||
pub lng: &'a [f64],
|
||||
pub lat: &'a [f64],
|
||||
pub ele: &'a [f64],
|
||||
pub surface: RouteCategory<'a>,
|
||||
pub highway: RouteCategory<'a>,
|
||||
pub sac_scale: RouteCategory<'a>,
|
||||
pub mtb_scale: RouteCategory<'a>,
|
||||
/// Indices among the new trackpoints.
|
||||
pub anchors: &'a [u32],
|
||||
}
|
||||
|
||||
fn invalid(message: &str) -> CommandError {
|
||||
CommandError::InvalidData(message.into())
|
||||
}
|
||||
|
||||
/// A segment of the selection, and the range of its trackpoints that is concerned.
|
||||
pub(super) struct Target {
|
||||
pub location: SegmentLocation,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
impl Apply for Route<'_> {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let (start, end) = (self.start as usize, self.end as usize);
|
||||
let len = self.lng.len();
|
||||
if start > end || self.lat.len() != len || self.ele.len() != len {
|
||||
return Err(invalid("invalid trackpoints range"));
|
||||
}
|
||||
if self.anchors.iter().any(|anchor| *anchor as usize >= len) {
|
||||
return Err(invalid("anchor out of the new trackpoints"));
|
||||
}
|
||||
if start == end && len == 0 {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
let categories = &mut *state.categories;
|
||||
let surface = self.surface.codes(&mut categories.surface, len)?;
|
||||
let highway = self.highway.codes(&mut categories.highway, len)?;
|
||||
let sac_scale = self.sac_scale.codes(&mut categories.sac_scale, len)?;
|
||||
let mtb_scale = self.mtb_scale.codes(&mut categories.mtb_scale, len)?;
|
||||
let points: Vec<Trackpoint> = (0..len)
|
||||
.map(|i| Trackpoint {
|
||||
coordinates: crate::LngLat {
|
||||
lng: self.lng[i],
|
||||
lat: self.lat[i],
|
||||
},
|
||||
ele: self.ele[i],
|
||||
surface: surface[i],
|
||||
highway: highway[i],
|
||||
sac_scale: sac_scale[i],
|
||||
mtb_scale: mtb_scale[i],
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
let mut points = points;
|
||||
for anchor in self.anchors {
|
||||
points[*anchor as usize].anchor = Some(0);
|
||||
}
|
||||
|
||||
let mut file;
|
||||
let target = match find_target(state, start, end)? {
|
||||
Some(target) => {
|
||||
file = (*state.files[&target.location.file_id]).clone();
|
||||
target
|
||||
}
|
||||
None => {
|
||||
let (file_id, trk) = container(state).ok_or(CommandError::NothingToDo)?;
|
||||
if start != 0 || end != 0 {
|
||||
return Err(invalid("trackpoints range out of bounds"));
|
||||
}
|
||||
file = (*state.files[&file_id]).clone();
|
||||
// the segment of the last track, which may be new
|
||||
let trk = match trk.or_else(|| file.trk.len().checked_sub(1)) {
|
||||
Some(trk) => trk,
|
||||
None => {
|
||||
file.trk.push(Track::default());
|
||||
file.trk.len() - 1
|
||||
}
|
||||
};
|
||||
if file.trk[trk].trkseg.is_empty() {
|
||||
file.trk[trk].trkseg.push(TrackSegment::default());
|
||||
}
|
||||
let seg = file.trk[trk].trkseg.len() - 1;
|
||||
Target {
|
||||
location: SegmentLocation { file_id, trk, seg },
|
||||
start: 0,
|
||||
end: 0,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let segment = &mut file.trk[target.location.trk].trkseg[target.location.seg];
|
||||
let new_len = points.len();
|
||||
let (speed, start_time) = speed_and_start_time(segment, &target, &points);
|
||||
let has_times = !segment.is_empty() && segment[0].time.is_some();
|
||||
if speed.is_some() || has_times {
|
||||
// the timestamps of the whole segment can change
|
||||
let mut all: Vec<Trackpoint> = segment.iter().cloned().collect();
|
||||
let old_len = all.len();
|
||||
replace_trackpoints(
|
||||
&mut all,
|
||||
target.start,
|
||||
target.end,
|
||||
points,
|
||||
speed,
|
||||
start_time,
|
||||
false,
|
||||
);
|
||||
segment.splice(0, old_len, all);
|
||||
} else {
|
||||
segment.splice(target.start, target.end, points);
|
||||
}
|
||||
// the anchors next to the new points are the ones that were moved or removed around:
|
||||
// they become permanent
|
||||
for index in [target.start.checked_sub(1), Some(target.start + new_len)]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
if index < segment.len() && segment[index].anchor.is_some_and(|zoom| zoom != 0) {
|
||||
segment.set_anchor(index, 0);
|
||||
}
|
||||
}
|
||||
segment.rev_id = Default::default();
|
||||
state.files.insert(target.location.file_id, Rc::new(file));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The segment of the trackpoints `start..end` of the selection.
|
||||
pub(super) fn find_target(
|
||||
state: &State,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) -> Result<Option<Target>, CommandError> {
|
||||
let locations = state
|
||||
.selection
|
||||
.segment_locations(state.files, &state.order.0);
|
||||
let segments: Vec<(SegmentLocation, usize)> = locations
|
||||
.into_iter()
|
||||
.map(|location| {
|
||||
let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg];
|
||||
(location, segment.len())
|
||||
})
|
||||
.collect();
|
||||
let total: usize = segments.iter().map(|(_, len)| len).sum();
|
||||
|
||||
if total == 0 {
|
||||
// an empty segment of the selection, if there is one
|
||||
return match segments.last() {
|
||||
Some((location, _)) if start == 0 && end == 0 => Ok(Some(Target {
|
||||
location: *location,
|
||||
start: 0,
|
||||
end: 0,
|
||||
})),
|
||||
Some(_) => Err(invalid("trackpoints range out of bounds")),
|
||||
None => Ok(None),
|
||||
};
|
||||
}
|
||||
|
||||
let mut offset = 0;
|
||||
for (location, len) in segments.iter().filter(|(_, len)| *len > 0) {
|
||||
// appending to the selection is appending to its last segment
|
||||
let appends = offset + len == total && start == total;
|
||||
if start >= offset && (start < offset + len || appends) {
|
||||
if end > offset + len {
|
||||
return Err(invalid("trackpoints range over several segments"));
|
||||
}
|
||||
return Ok(Some(Target {
|
||||
location: *location,
|
||||
start: start - offset,
|
||||
end: end - offset,
|
||||
}));
|
||||
}
|
||||
offset += len;
|
||||
}
|
||||
Err(invalid("trackpoints range out of bounds"))
|
||||
}
|
||||
|
||||
/// The file, and the track (if the selection is one), where to create a segment in a selection
|
||||
/// that has none.
|
||||
fn container(state: &State) -> Option<(FileId, Option<usize>)> {
|
||||
match &*state.selection {
|
||||
Selection::File { file_ids } => {
|
||||
let id = state
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|id| file_ids.contains(id))
|
||||
.or_else(|| file_ids.iter().next())?;
|
||||
state.files.contains_key(id).then_some((*id, None))
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let trk = state
|
||||
.files
|
||||
.get(file_id)?
|
||||
.trk
|
||||
.iter()
|
||||
.rposition(|trk| trk_ids.contains(&trk.id))?;
|
||||
Some((*file_id, Some(trk)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The speed at which the new points are reached and the time of the first one, when the segment
|
||||
/// has times to keep consistent: the speed is the one that the segment had on the part that is
|
||||
/// replaced, adjusted so that the moving speed of the whole segment stays what it was.
|
||||
///
|
||||
/// The replaced part goes from the trackpoint before the range (the anchor that stays) to the one
|
||||
/// after it, or to the ends of the segment: the new points are the route between them.
|
||||
///
|
||||
/// This is what the routing tool computed from the statistics of the segment before.
|
||||
fn speed_and_start_time(
|
||||
segment: &TrackSegment,
|
||||
target: &Target,
|
||||
points: &[Trackpoint],
|
||||
) -> (Option<f64>, Option<i64>) {
|
||||
let time_at = |index: usize| {
|
||||
(index < segment.len())
|
||||
.then(|| segment[index].time)
|
||||
.flatten()
|
||||
};
|
||||
let start_time = time_at(target.start);
|
||||
if points.is_empty() || segment.is_empty() {
|
||||
return (None, start_time);
|
||||
}
|
||||
let stats = Statistics::compute(segment);
|
||||
let Some(moving_speed) = stats.global.moving_speed().filter(|speed| *speed > 0.0) else {
|
||||
return (None, start_time);
|
||||
};
|
||||
|
||||
// the statistics where the replaced part starts and ends
|
||||
let first_index = target.start.saturating_sub(1);
|
||||
let last_index = target.end.min(segment.len() - 1);
|
||||
let (first, last) = (&stats.local[first_index], &stats.local[last_index]);
|
||||
let moving_distance =
|
||||
|stats: &crate::TrackpointStatistics| stats.moving_distance.unwrap_or(0.0);
|
||||
let moving_time = |stats: &crate::TrackpointStatistics| stats.moving_time.unwrap_or(0);
|
||||
let total_time = |stats: &crate::TrackpointStatistics| stats.total_time.unwrap_or(0);
|
||||
|
||||
let replacing_distance: f64 = points
|
||||
.windows(2)
|
||||
.map(|pair| distance(pair[0].coordinates, pair[1].coordinates))
|
||||
.sum();
|
||||
let replaced_distance = moving_distance(last) - moving_distance(first);
|
||||
let global_moving_distance = stats.global.moving_distance.unwrap_or(0.0);
|
||||
let new_distance = global_moving_distance + replacing_distance - replaced_distance;
|
||||
// in milliseconds
|
||||
let new_time = new_distance / moving_speed * 3_600_000.0;
|
||||
let remaining_time = stats.global.moving_time.unwrap_or(0) as f64
|
||||
- (moving_time(last) - moving_time(first)) as f64;
|
||||
let mut replacing_time = new_time - remaining_time;
|
||||
if replacing_time <= 0.0 {
|
||||
// fall back to the time that the replaced part took
|
||||
replacing_time = (total_time(last) - total_time(first)) as f64;
|
||||
}
|
||||
let speed = replacing_distance / (replacing_time / 3_600_000.0);
|
||||
|
||||
let start_time = start_time.or_else(|| {
|
||||
// The first trackpoint has no time: the new points end when the last replaced one did,
|
||||
// after the time they take and the time that the segment was not moving until then.
|
||||
// (Suspicious: without a time for the last one either, it is "0", the year 1970.)
|
||||
let end_time = time_at(last_index);
|
||||
let stopped = (total_time(last) - moving_time(last)) as f64;
|
||||
Some(end_time.unwrap_or(0) - (replacing_time + stopped) as i64)
|
||||
});
|
||||
(Some(speed), start_time)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{File, Load, TrackSegmentId, engine::command::fixture::Fixture};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn load(fx: &mut Fixture, path: &str) -> FileId {
|
||||
let data = std::fs::read(path).unwrap();
|
||||
Load {
|
||||
data: &data,
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
*fx.order.0.last().unwrap()
|
||||
}
|
||||
|
||||
fn select_file(fx: &mut Fixture, id: FileId) {
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([id]),
|
||||
};
|
||||
}
|
||||
|
||||
/// The first segment of the file with segments, selected.
|
||||
fn loaded() -> (Fixture, FileId) {
|
||||
let mut fx = Fixture::default();
|
||||
let id = load(&mut fx, "data/with_tracks_and_segments.gpx");
|
||||
let trk = &fx.files[&id].trk[0];
|
||||
fx.selection = Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: trk.id,
|
||||
trkseg_ids: HashSet::from([trk.trkseg[0].id]),
|
||||
};
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
fn route<'a>(
|
||||
start: u32,
|
||||
end: u32,
|
||||
lng: &'a [f64],
|
||||
lat: &'a [f64],
|
||||
ele: &'a [f64],
|
||||
) -> Route<'a> {
|
||||
Route {
|
||||
start,
|
||||
end,
|
||||
lng,
|
||||
lat,
|
||||
ele,
|
||||
surface: Default::default(),
|
||||
highway: Default::default(),
|
||||
sac_scale: Default::default(),
|
||||
mtb_scale: Default::default(),
|
||||
anchors: &[],
|
||||
}
|
||||
}
|
||||
|
||||
fn segment<'a>(fx: &'a Fixture, id: FileId, trk: usize, seg: usize) -> &'a TrackSegment {
|
||||
&fx.files[&id].trk[trk].trkseg[seg]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_replaces_points_of_the_selected_segment_only() {
|
||||
let (mut fx, id) = loaded();
|
||||
let before = fx.files[&id].clone();
|
||||
let len = before.trk[0].trkseg[0].len();
|
||||
assert!(len >= 3);
|
||||
|
||||
route(1, 3, &[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0], &[7.0, 8.0, 9.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let after = &fx.files[&id];
|
||||
let (old, new) = (&before.trk[0].trkseg[0], &after.trk[0].trkseg[0]);
|
||||
assert_eq!(new.len(), len + 1);
|
||||
assert_eq!(new[0].coordinates.lng, old[0].coordinates.lng);
|
||||
assert_eq!(new[1].coordinates.lng, 1.0);
|
||||
assert_eq!(new[3].coordinates.lat, 6.0);
|
||||
assert_eq!(new[3].ele, 9.0);
|
||||
assert_eq!(new[4].coordinates.lng, old[3].coordinates.lng);
|
||||
assert_ne!(new.rev_id, old.rev_id);
|
||||
assert_eq!(new.id, old.id);
|
||||
// other segments and tracks keep their revision
|
||||
for (b, a) in before.trk.iter().zip(&after.trk) {
|
||||
for (b, a) in b.trkseg.iter().zip(&a.trkseg) {
|
||||
if a.id != new.id {
|
||||
assert_eq!(a.rev_id, b.rev_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_append_and_remove() {
|
||||
let (mut fx, id) = loaded();
|
||||
let len = fx.files[&id].trk[0].trkseg[0].len() as u32;
|
||||
route(len, len, &[1.0], &[2.0], &[3.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let seg = segment(&fx, id, 0, 0);
|
||||
assert_eq!(seg.len(), len as usize + 1);
|
||||
assert_eq!(seg[len as usize].ele, 3.0);
|
||||
|
||||
route(0, len + 1, &[], &[], &[])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(segment(&fx, id, 0, 0).len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_invalid_arguments_change_nothing() {
|
||||
let (mut fx, id) = loaded();
|
||||
let before = fx.files[&id].clone();
|
||||
let len = before.trk[0].trkseg[0].len() as u32;
|
||||
let results = [
|
||||
route(2, 1, &[], &[], &[]).apply(&mut fx.state()),
|
||||
route(0, 0, &[1.0], &[], &[]).apply(&mut fx.state()),
|
||||
route(0, len + 1, &[], &[], &[]).apply(&mut fx.state()),
|
||||
route(len + 1, len + 1, &[1.0], &[1.0], &[1.0]).apply(&mut fx.state()),
|
||||
Route {
|
||||
anchors: &[1],
|
||||
..route(0, 0, &[1.0], &[1.0], &[1.0])
|
||||
}
|
||||
.apply(&mut fx.state()),
|
||||
];
|
||||
assert!(
|
||||
results
|
||||
.iter()
|
||||
.all(|r| matches!(r, Err(CommandError::InvalidData(_)))),
|
||||
"{results:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
route(0, 0, &[], &[], &[]).apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
assert!(Rc::ptr_eq(&fx.files[&id], &before));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_needs_a_selection_with_a_file() {
|
||||
let (mut fx, _) = loaded();
|
||||
fx.selection = Selection::Empty;
|
||||
assert_eq!(
|
||||
route(0, 0, &[1.0], &[1.0], &[1.0]).apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_indices_are_in_the_selection() {
|
||||
let (mut fx, id) = loaded();
|
||||
let file = fx.files[&id].clone();
|
||||
select_file(&mut fx, id);
|
||||
let lens: Vec<_> = file
|
||||
.trk
|
||||
.iter()
|
||||
.flat_map(|trk| trk.trkseg.iter().map(TrackSegment::len))
|
||||
.collect();
|
||||
assert!(lens.len() >= 2);
|
||||
|
||||
// inside the second segment
|
||||
let second = lens[0] as u32;
|
||||
route(second + 1, second + 2, &[1.0], &[2.0], &[3.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let (trk, seg) = (0, 1);
|
||||
let segments_before: Vec<_> = file.trk.iter().flat_map(|t| &t.trkseg).collect();
|
||||
let files = fx.files[&id].clone();
|
||||
let segments_after: Vec<_> = files.trk.iter().flat_map(|t| &t.trkseg).collect();
|
||||
for (i, (b, a)) in segments_before.iter().zip(&segments_after).enumerate() {
|
||||
assert_eq!(a.rev_id != b.rev_id, i == 1, "segment {i}");
|
||||
}
|
||||
assert_eq!(segment(&fx, id, trk, seg)[1].coordinates.lng, 1.0);
|
||||
|
||||
// a range over two segments
|
||||
let total: usize = lens.iter().sum();
|
||||
assert!(matches!(
|
||||
route(second - 1, second + 1, &[], &[], &[]).apply(&mut fx.state()),
|
||||
Err(CommandError::InvalidData(_))
|
||||
));
|
||||
// after the last point: the last segment
|
||||
let before = segments_after.last().unwrap().len();
|
||||
let total = total as u32;
|
||||
route(total, total, &[1.0], &[2.0], &[3.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let last = fx.files[&id].trk.last().unwrap().trkseg.last().unwrap();
|
||||
assert_eq!(last.len(), before + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_creates_the_segment_of_a_selection_that_has_none() {
|
||||
let mut fx = Fixture::default();
|
||||
let file = File::default();
|
||||
let id = file.id;
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.order.0.push(id);
|
||||
select_file(&mut fx, id);
|
||||
|
||||
route(0, 0, &[1.0, 2.0], &[1.0, 2.0], &[3.0, 4.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(fx.files[&id].trk.len(), 1);
|
||||
assert_eq!(segment(&fx, id, 0, 0).len(), 2);
|
||||
// the ends are anchors
|
||||
assert_eq!(segment(&fx, id, 0, 0)[0].anchor, Some(0));
|
||||
assert_eq!(segment(&fx, id, 0, 0)[1].anchor, Some(0));
|
||||
|
||||
// a track with no segments
|
||||
let mut file = (*fx.files[&id]).clone();
|
||||
file.trk[0].trkseg.clear();
|
||||
let trk_id = file.trk[0].id;
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.selection = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: HashSet::from([trk_id]),
|
||||
};
|
||||
route(0, 0, &[5.0], &[5.0], &[5.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(fx.files[&id].trk.len(), 1);
|
||||
assert_eq!(segment(&fx, id, 0, 0).len(), 1);
|
||||
|
||||
// an empty segment is the target as well
|
||||
let mut file = (*fx.files[&id]).clone();
|
||||
file.trk[0].trkseg[0] = TrackSegment::default();
|
||||
let seg_id: TrackSegmentId = file.trk[0].trkseg[0].id;
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.selection = Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id,
|
||||
trkseg_ids: HashSet::from([seg_id]),
|
||||
};
|
||||
route(0, 0, &[6.0], &[6.0], &[6.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(segment(&fx, id, 0, 0).len(), 1);
|
||||
assert_eq!(fx.files[&id].trk[0].trkseg.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_anchors() {
|
||||
let (mut fx, id) = loaded();
|
||||
let len = segment(&fx, id, 0, 0).len();
|
||||
assert!(len > 4);
|
||||
let anchors_before: Vec<_> = segment(&fx, id, 0, 0).iter().map(|p| p.anchor).collect();
|
||||
|
||||
// 5 new points replacing the 2 in the middle, two of them are anchors
|
||||
let (lng, lat, ele) = ([1.0; 5], [2.0; 5], [3.0; 5]);
|
||||
Route {
|
||||
anchors: &[1, 3],
|
||||
..route(1, 3, &lng, &lat, &ele)
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let anchors: Vec<_> = segment(&fx, id, 0, 0).iter().map(|p| p.anchor).collect();
|
||||
assert_eq!(anchors.len(), len + 3);
|
||||
assert_eq!(anchors[2], Some(0));
|
||||
assert_eq!(anchors[4], Some(0));
|
||||
assert_eq!(anchors[1], None);
|
||||
assert_eq!(anchors[3], None);
|
||||
assert_eq!(anchors[5], None);
|
||||
// the others stay as they were, the ends are always anchors
|
||||
assert_eq!(anchors[0], Some(0));
|
||||
assert_eq!(anchors[len + 2], Some(0));
|
||||
assert_eq!(anchors[len + 2 - 1], anchors_before[len - 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_makes_the_anchors_around_the_range_permanent() {
|
||||
let (mut fx, id) = loaded();
|
||||
let mut file = (*fx.files[&id]).clone();
|
||||
let len = file.trk[0].trkseg[0].len();
|
||||
assert!(len > 8);
|
||||
// anchors shown from different zoom levels, around and away from the range 4..6
|
||||
for (index, zoom) in [(1, 7), (3, 9), (6, 12), (8, 14)] {
|
||||
file.trk[0].trkseg[0].set_anchor(index, zoom);
|
||||
}
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
|
||||
route(4, 6, &[1.0, 2.0], &[3.0, 4.0], &[5.0, 6.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let anchors: Vec<_> = segment(&fx, id, 0, 0).iter().map(|p| p.anchor).collect();
|
||||
assert_eq!(anchors.len(), len);
|
||||
// right before and after the new points: permanent
|
||||
assert_eq!(anchors[3], Some(0));
|
||||
assert_eq!(anchors[6], Some(0));
|
||||
// the others are as they were
|
||||
assert_eq!(anchors[1], Some(7));
|
||||
assert_eq!(anchors[8], Some(14));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_categories() {
|
||||
let (mut fx, id) = loaded();
|
||||
let names = ["asphalt".to_string(), "gravel".to_string()];
|
||||
let (lng, lat, ele) = ([1.0; 4], [2.0; 4], [3.0; 4]);
|
||||
// asphalt, asphalt, unknown, gravel
|
||||
Route {
|
||||
surface: RouteCategory {
|
||||
starts: &[0, 2, 3],
|
||||
values: &[1, 0, 2],
|
||||
names: &names,
|
||||
},
|
||||
highway: RouteCategory {
|
||||
starts: &[0],
|
||||
values: &[1],
|
||||
names: &["track".to_string()],
|
||||
},
|
||||
..route(0, 0, &lng, &lat, &ele)
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = fx.files[&id].clone();
|
||||
let seg = &file.trk[0].trkseg[0];
|
||||
let asphalt = fx.categories.surface.code("asphalt");
|
||||
let gravel = fx.categories.surface.code("gravel");
|
||||
let surfaces: Vec<_> = (0..4).map(|i| seg[i].surface).collect();
|
||||
assert_eq!(surfaces, [asphalt, asphalt, None, gravel]);
|
||||
let track = fx.categories.highway.code("track");
|
||||
assert!((0..4).all(|i| seg[i].highway == track));
|
||||
assert!((0..4).all(|i| seg[i].sac_scale.is_none() && seg[i].mtb_scale.is_none()));
|
||||
|
||||
// broken intervals or names
|
||||
let broken = [
|
||||
RouteCategory {
|
||||
starts: &[0, 1],
|
||||
values: &[1],
|
||||
names: &names,
|
||||
},
|
||||
RouteCategory {
|
||||
starts: &[5],
|
||||
values: &[1],
|
||||
names: &names,
|
||||
},
|
||||
RouteCategory {
|
||||
starts: &[0],
|
||||
values: &[3],
|
||||
names: &names,
|
||||
},
|
||||
];
|
||||
for surface in broken {
|
||||
let result = Route {
|
||||
surface,
|
||||
..route(0, 0, &lng, &lat, &ele)
|
||||
}
|
||||
.apply(&mut fx.state());
|
||||
assert!(matches!(result, Err(CommandError::InvalidData(_))));
|
||||
}
|
||||
}
|
||||
|
||||
/// A segment of `data/with_time.gpx`, selected: 80 trackpoints at 20 km/h.
|
||||
fn timed() -> (Fixture, FileId) {
|
||||
let mut fx = Fixture::default();
|
||||
let id = load(&mut fx, "data/with_time.gpx");
|
||||
select_file(&mut fx, id);
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
fn times(seg: &TrackSegment) -> Vec<i64> {
|
||||
seg.iter().map(|p| p.time.unwrap()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_gives_times_to_the_new_points() {
|
||||
let (mut fx, id) = timed();
|
||||
let seg = segment(&fx, id, 0, 0);
|
||||
let (n, before) = (seg.len(), times(seg));
|
||||
let duration = before[n - 1] - before[0];
|
||||
|
||||
// replace the points 20..40 by the straight line between them, with 5 points
|
||||
let (a, b) = (seg[20].coordinates, seg[39].coordinates);
|
||||
let at = |t: f64| (a.lng + t * (b.lng - a.lng), a.lat + t * (b.lat - a.lat));
|
||||
let positions: Vec<_> = (0..5).map(|i| at(i as f64 / 4.0)).collect();
|
||||
let lng: Vec<_> = positions.iter().map(|p| p.0).collect();
|
||||
let lat: Vec<_> = positions.iter().map(|p| p.1).collect();
|
||||
route(20, 40, &lng, &lat, &[0.0; 5])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let seg = segment(&fx, id, 0, 0);
|
||||
assert_eq!(seg.len(), n - 15);
|
||||
let after = times(seg);
|
||||
assert!(after.windows(2).all(|w| w[0] < w[1]), "{after:?}");
|
||||
// the points before are untouched
|
||||
assert_eq!(after[..20], before[..20]);
|
||||
// the speed of the replaced part is the one of the segment: the duration of the segment
|
||||
// is about the same as on the same distance
|
||||
let stats = Statistics::compute(seg);
|
||||
let speed = stats.global.moving_speed().unwrap();
|
||||
assert!((speed - 20.0).abs() < 1.0, "{speed}");
|
||||
assert!(after[after.len() - 1] - after[0] <= duration);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_between_anchors_that_stay_keeps_them_untouched() {
|
||||
let (mut fx, id) = timed();
|
||||
let seg = segment(&fx, id, 0, 0);
|
||||
let (n, before) = (seg.len(), times(seg));
|
||||
let (a, b) = (seg[20].clone(), seg[39].clone());
|
||||
// 5 points on the line between the anchors 20 and 39, which are not replaced
|
||||
let at = |t: f64| {
|
||||
(
|
||||
a.coordinates.lng + t * (b.coordinates.lng - a.coordinates.lng),
|
||||
a.coordinates.lat + t * (b.coordinates.lat - a.coordinates.lat),
|
||||
)
|
||||
};
|
||||
let positions: Vec<_> = (1..=5).map(|i| at(i as f64 / 6.0)).collect();
|
||||
let lng: Vec<_> = positions.iter().map(|p| p.0).collect();
|
||||
let lat: Vec<_> = positions.iter().map(|p| p.1).collect();
|
||||
route(21, 39, &lng, &lat, &[0.0; 5])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let seg = segment(&fx, id, 0, 0);
|
||||
assert_eq!(seg.len(), n - 18 + 5);
|
||||
let after = times(seg);
|
||||
assert!(after.windows(2).all(|w| w[0] < w[1]), "{after:?}");
|
||||
// the anchors keep their coordinates and times, and what is before them
|
||||
assert_eq!(after[..=20], before[..=20]);
|
||||
assert_eq!(seg[26].coordinates.lng, b.coordinates.lng);
|
||||
assert_eq!(after[26], before[39]);
|
||||
// and what comes after is unchanged as well
|
||||
assert_eq!(after[27..], before[40..]);
|
||||
let speed = Statistics::compute(seg).global.moving_speed().unwrap();
|
||||
assert!((speed - 20.0).abs() < 1.0, "{speed}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_appending_to_a_segment_with_times() {
|
||||
let (mut fx, id) = timed();
|
||||
let seg = segment(&fx, id, 0, 0);
|
||||
let n = seg.len();
|
||||
let last = seg[n - 1].clone();
|
||||
// replace the last point and go 1 km further east
|
||||
let lng = [last.coordinates.lng, last.coordinates.lng + 0.009];
|
||||
let lat = [last.coordinates.lat; 2];
|
||||
route(n as u32 - 1, n as u32, &lng, &lat, &[0.0; 2])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let seg = segment(&fx, id, 0, 0);
|
||||
assert_eq!(seg.len(), n + 1);
|
||||
// the replaced point is reached at the speed of the segment: about at its time
|
||||
assert!((seg[n - 1].time.unwrap() - last.time.unwrap()).abs() < 1000);
|
||||
let (a, b) = (seg[n - 1].time.unwrap(), seg[n].time.unwrap());
|
||||
// the new point is reached at about 20 km/h
|
||||
let km = distance(seg[n - 1].coordinates, seg[n].coordinates);
|
||||
let seconds = (b - a) as f64 / 1000.0;
|
||||
let expected = 3600.0 * km / 20.0;
|
||||
assert!(
|
||||
(seconds - expected).abs() < 0.1 * expected,
|
||||
"{seconds} {expected}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_does_not_invent_times() {
|
||||
let mut fx = Fixture::default();
|
||||
let id = load(&mut fx, "data/simple.gpx");
|
||||
select_file(&mut fx, id);
|
||||
assert!(segment(&fx, id, 0, 0).iter().all(|p| p.time.is_none()));
|
||||
route(0, 2, &[1.0, 2.0], &[3.0, 4.0], &[5.0, 6.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert!(segment(&fx, id, 0, 0).iter().all(|p| p.time.is_none()));
|
||||
}
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Apply, CommandError, File, FileId, LngLat, Selection, State, Trackpoint};
|
||||
|
||||
/// Replaces the trackpoints `start..end` of the last track segment of the selection by the
|
||||
/// given ones (a pure insertion when `start == end`, a pure removal when there are no new
|
||||
/// points).
|
||||
///
|
||||
/// The selection can be made of several files, tracks or track segments: the indices are
|
||||
/// meant for its very last segment, in file order, then track order, then segment order.
|
||||
#[derive(Debug)]
|
||||
pub struct SpliceTrackpoints<'a> {
|
||||
pub start: u32,
|
||||
pub end: u32,
|
||||
pub lng: &'a [f64],
|
||||
pub lat: &'a [f64],
|
||||
pub ele: &'a [f64],
|
||||
}
|
||||
|
||||
impl Apply for SpliceTrackpoints<'_> {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let (start, end) = (self.start as usize, self.end as usize);
|
||||
if start > end || self.lng.len() != self.lat.len() || self.lat.len() != self.ele.len() {
|
||||
return Err(CommandError::InvalidData(
|
||||
"invalid trackpoints range".into(),
|
||||
));
|
||||
}
|
||||
let (file_id, trk, seg) = last_segment(state).ok_or(CommandError::NothingToDo)?;
|
||||
let mut file = (*state.files[&file_id]).clone();
|
||||
let segment = &mut file.trk[trk].trkseg[seg];
|
||||
if end > segment.len() {
|
||||
return Err(CommandError::InvalidData(
|
||||
"trackpoints range out of bounds".into(),
|
||||
));
|
||||
}
|
||||
if start == end && self.lng.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
let points = (0..self.lng.len())
|
||||
.map(|i| Trackpoint {
|
||||
coordinates: LngLat {
|
||||
lng: self.lng[i],
|
||||
lat: self.lat[i],
|
||||
},
|
||||
ele: self.ele[i],
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
segment.splice(start, end, points);
|
||||
segment.rev_id = Default::default();
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Position (file, track index, segment index) of the last segment of the selection.
|
||||
fn last_segment(state: &State) -> Option<(FileId, usize, usize)> {
|
||||
let last_of_track = |file: &File, trk: usize| file.trk[trk].trkseg.len().checked_sub(1);
|
||||
let last_of_file = |file: &File, filter: &dyn Fn(usize) -> bool| {
|
||||
(0..file.trk.len())
|
||||
.rev()
|
||||
.filter(|&trk| filter(trk))
|
||||
.find_map(|trk| last_of_track(file, trk).map(|seg| (trk, seg)))
|
||||
};
|
||||
match &*state.selection {
|
||||
Selection::File { file_ids } => state
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|id| file_ids.contains(id))
|
||||
.find_map(|id| {
|
||||
let (trk, seg) = last_of_file(state.files.get(id)?, &|_| true)?;
|
||||
Some((*id, trk, seg))
|
||||
}),
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = state.files.get(file_id)?;
|
||||
let (trk, seg) = last_of_file(file, &|trk| trk_ids.contains(&file.trk[trk].id))?;
|
||||
Some((*file_id, trk, seg))
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => {
|
||||
let file = state.files.get(file_id)?;
|
||||
let trk = file.trk.iter().position(|trk| trk.id == *trk_id)?;
|
||||
let seg = file.trk[trk]
|
||||
.trkseg
|
||||
.iter()
|
||||
.rposition(|seg| trkseg_ids.contains(&seg.id))?;
|
||||
Some((*file_id, trk, seg))
|
||||
}
|
||||
Selection::Empty | Selection::Waypoints { .. } | Selection::Waypoint { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{FileId, Load, engine::command::fixture::Fixture};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn loaded() -> (Fixture, FileId) {
|
||||
let mut fx = Fixture::default();
|
||||
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||
Load {
|
||||
data: &data,
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let id = fx.order.0[0];
|
||||
let trk = &fx.files[&id].trk[0];
|
||||
fx.selection = Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: trk.id,
|
||||
trkseg_ids: HashSet::from([trk.trkseg[0].id]),
|
||||
};
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
fn splice<'a>(
|
||||
start: u32,
|
||||
end: u32,
|
||||
lng: &'a [f64],
|
||||
lat: &'a [f64],
|
||||
ele: &'a [f64],
|
||||
) -> SpliceTrackpoints<'a> {
|
||||
SpliceTrackpoints {
|
||||
start,
|
||||
end,
|
||||
lng,
|
||||
lat,
|
||||
ele,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_replaces_points_of_selected_segment_only() {
|
||||
let (mut fx, id) = loaded();
|
||||
let before = fx.files[&id].clone();
|
||||
let len = before.trk[0].trkseg[0].len();
|
||||
assert!(len >= 3);
|
||||
|
||||
splice(1, 3, &[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0], &[7.0, 8.0, 9.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let after = &fx.files[&id];
|
||||
let (old, new) = (&before.trk[0].trkseg[0], &after.trk[0].trkseg[0]);
|
||||
assert_eq!(new.len(), len + 1);
|
||||
assert_eq!(new[0].coordinates.lng, old[0].coordinates.lng);
|
||||
assert_eq!(new[1].coordinates.lng, 1.0);
|
||||
assert_eq!(new[3].coordinates.lat, 6.0);
|
||||
assert_eq!(new[3].ele, 9.0);
|
||||
assert_eq!(new[4].coordinates.lng, old[3].coordinates.lng);
|
||||
assert_ne!(new.rev_id, old.rev_id);
|
||||
assert_eq!(new.id, old.id);
|
||||
// other segments and tracks keep their revision
|
||||
for (b, a) in before.trk.iter().zip(&after.trk) {
|
||||
for (b, a) in b.trkseg.iter().zip(&a.trkseg) {
|
||||
if a.id != new.id {
|
||||
assert_eq!(a.rev_id, b.rev_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_append_and_remove() {
|
||||
let (mut fx, id) = loaded();
|
||||
let len = fx.files[&id].trk[0].trkseg[0].len() as u32;
|
||||
splice(len, len, &[1.0], &[2.0], &[3.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let seg = &fx.files[&id].trk[0].trkseg[0];
|
||||
assert_eq!(seg.len(), len as usize + 1);
|
||||
assert_eq!(seg[len as usize].ele, 3.0);
|
||||
|
||||
splice(0, len + 1, &[], &[], &[])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(fx.files[&id].trk[0].trkseg[0].len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_invalid_arguments_change_nothing() {
|
||||
let (mut fx, id) = loaded();
|
||||
let before = fx.files[&id].clone();
|
||||
let len = before.trk[0].trkseg[0].len() as u32;
|
||||
let results = [
|
||||
splice(2, 1, &[], &[], &[]).apply(&mut fx.state()),
|
||||
splice(0, 0, &[1.0], &[], &[]).apply(&mut fx.state()),
|
||||
splice(0, len + 1, &[], &[], &[]).apply(&mut fx.state()),
|
||||
];
|
||||
assert!(
|
||||
results
|
||||
.iter()
|
||||
.all(|r| matches!(r, Err(CommandError::InvalidData(_))))
|
||||
);
|
||||
assert_eq!(
|
||||
splice(0, 0, &[], &[], &[]).apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
assert!(std::rc::Rc::ptr_eq(&fx.files[&id], &before));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_needs_a_segment_selection() {
|
||||
let (mut fx, _) = loaded();
|
||||
fx.selection = Selection::Empty;
|
||||
assert_eq!(
|
||||
splice(0, 0, &[1.0], &[1.0], &[1.0]).apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_targets_last_segment_of_the_selection() {
|
||||
let (mut fx, id) = loaded();
|
||||
let file = fx.files[&id].clone();
|
||||
assert!(file.trk.len() >= 2);
|
||||
let last_trk = file.trk.len() - 1;
|
||||
let last_seg = file.trk[last_trk].trkseg.len() - 1;
|
||||
|
||||
// whole file selected: last segment of its last track
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([id]),
|
||||
};
|
||||
splice(0, 0, &[1.0], &[2.0], &[3.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let after = &fx.files[&id];
|
||||
for (t, (b, a)) in file.trk.iter().zip(&after.trk).enumerate() {
|
||||
for (s, (b, a)) in b.trkseg.iter().zip(&a.trkseg).enumerate() {
|
||||
let target = (t, s) == (last_trk, last_seg);
|
||||
assert_eq!(a.len(), b.len() + usize::from(target));
|
||||
assert_eq!(a.rev_id != b.rev_id, target);
|
||||
}
|
||||
}
|
||||
|
||||
// several tracks selected: last selected one (in file order, whatever the set order)
|
||||
let ids: Vec<_> = file.trk.iter().map(|t| t.id).collect();
|
||||
fx.selection = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: HashSet::from([ids[1], ids[0]]),
|
||||
};
|
||||
let len = |fx: &Fixture, t: usize| {
|
||||
let trk = &fx.files[&id].trk[t];
|
||||
trk.trkseg.last().unwrap().len()
|
||||
};
|
||||
let (l0, l1) = (len(&fx, 0), len(&fx, 1));
|
||||
splice(0, 0, &[1.0], &[2.0], &[3.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(len(&fx, 0), l0);
|
||||
assert_eq!(len(&fx, 1), l1 + 1);
|
||||
|
||||
// several segments selected
|
||||
let trk = &fx.files[&id].trk[0];
|
||||
let seg_ids: Vec<_> = trk.trkseg.iter().map(|s| s.id).collect();
|
||||
if seg_ids.len() >= 2 {
|
||||
fx.selection = Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: trk.id,
|
||||
trkseg_ids: seg_ids.iter().copied().collect(),
|
||||
};
|
||||
let lens: Vec<_> = trk.trkseg.iter().map(|s| s.len()).collect();
|
||||
splice(0, 0, &[1.0], &[2.0], &[3.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let after: Vec<_> = fx.files[&id].trk[0]
|
||||
.trkseg
|
||||
.iter()
|
||||
.map(|s| s.len())
|
||||
.collect();
|
||||
let n = lens.len();
|
||||
assert_eq!(after[..n - 1], lens[..n - 1]);
|
||||
assert_eq!(after[n - 1], lens[n - 1] + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_on_multiple_files_uses_last_in_file_order() {
|
||||
let (mut fx, first) = loaded();
|
||||
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||
Load {
|
||||
data: &data,
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let second = fx.order.0[1];
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([second, first]),
|
||||
};
|
||||
let total = |fx: &Fixture, id: FileId| -> usize {
|
||||
fx.files[&id]
|
||||
.trk
|
||||
.iter()
|
||||
.flat_map(|t| &t.trkseg)
|
||||
.map(|s| s.len())
|
||||
.sum()
|
||||
};
|
||||
let (a, b) = (total(&fx, first), total(&fx, second));
|
||||
splice(0, 0, &[1.0], &[2.0], &[3.0])
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(total(&fx, first), a);
|
||||
assert_eq!(total(&fx, second), b + 1);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
mod coordinates_cache;
|
||||
mod file_structure;
|
||||
mod file_structure_cache;
|
||||
mod routing_buffer;
|
||||
mod statistics_buffer;
|
||||
mod statistics_cache;
|
||||
|
||||
pub use coordinates_cache::*;
|
||||
pub use file_structure::*;
|
||||
pub use file_structure_cache::*;
|
||||
pub use routing_buffer::*;
|
||||
pub use statistics_buffer::*;
|
||||
pub use statistics_cache::*;
|
||||
@@ -0,0 +1,162 @@
|
||||
use crate::{FileId, Selection, StackEntry, TrackSegmentId, TrackSegmentRevisionId};
|
||||
|
||||
/// What the routing tool needs about the selected segments (see
|
||||
/// [`Selection::segment_locations`]): where their anchors are among their trackpoints.
|
||||
///
|
||||
/// Trackpoints are numbered like in the [`crate::StatisticsBuffer`], over all the selected
|
||||
/// segments one after the other, except that the segments that are not covered by the routing
|
||||
/// tool (waypoints) are left out.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct RoutingBuffer {
|
||||
/// The index of each anchor in the selection, in order. The first and last trackpoints of a
|
||||
/// segment always are anchors.
|
||||
pub anchor_indices: Vec<u32>,
|
||||
/// The lowest map zoom level at which each anchor is shown.
|
||||
pub anchor_zooms: Vec<u8>,
|
||||
/// Index in the selection of the first trackpoint of each selected segment that has some,
|
||||
/// which tells which segment an anchor or a trackpoint belongs to.
|
||||
pub segment_starts: Vec<u32>,
|
||||
/// Id of each selected segment that has trackpoints, as `segment_starts`.
|
||||
pub segment_ids: Vec<TrackSegmentId>,
|
||||
/// Changes when the selected segments, or their trackpoints, change. Commands that refer to
|
||||
/// trackpoints by their index in the selection carry it, to be refused if the selection is
|
||||
/// not the one the indices were taken from anymore.
|
||||
pub revision: u32,
|
||||
/// What the revision stands for.
|
||||
signature: Vec<(TrackSegmentId, TrackSegmentRevisionId)>,
|
||||
}
|
||||
|
||||
impl RoutingBuffer {
|
||||
pub fn update(&mut self, files: Option<&StackEntry>, selection: &Selection, order: &[FileId]) {
|
||||
self.anchor_indices.clear();
|
||||
self.anchor_zooms.clear();
|
||||
self.segment_starts.clear();
|
||||
self.segment_ids.clear();
|
||||
|
||||
let mut signature = vec![];
|
||||
let mut index = 0;
|
||||
for location in files
|
||||
.map(|files| selection.segment_locations(files, order))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
let segment = &files.unwrap()[&location.file_id].trk[location.trk].trkseg[location.seg];
|
||||
signature.push((segment.id, segment.rev_id));
|
||||
if segment.is_empty() {
|
||||
continue;
|
||||
}
|
||||
self.segment_starts.push(index as u32);
|
||||
self.segment_ids.push(segment.id);
|
||||
for trkpt in segment.iter() {
|
||||
if let Some(zoom) = trkpt.anchor {
|
||||
self.anchor_indices.push(index as u32);
|
||||
self.anchor_zooms.push(zoom);
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if signature != self.signature {
|
||||
self.revision = self.revision.wrapping_add(1);
|
||||
self.signature = signature;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{TrackSegment, parse};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn loaded() -> (StackEntry, FileId) {
|
||||
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||
let file = parse(&data, &mut Default::default()).unwrap();
|
||||
let id = file.id;
|
||||
let mut files = StackEntry::default();
|
||||
files.insert(id, Rc::new(file));
|
||||
(files, id)
|
||||
}
|
||||
|
||||
fn file_selection(id: FileId) -> Selection {
|
||||
Selection::File {
|
||||
file_ids: HashSet::from([id]),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anchors_and_starts_follow_the_selected_segments() {
|
||||
let (files, id) = loaded();
|
||||
let lengths: Vec<usize> = files[&id]
|
||||
.trk
|
||||
.iter()
|
||||
.flat_map(|trk| trk.trkseg.iter().map(TrackSegment::len))
|
||||
.collect();
|
||||
let mut buffer = RoutingBuffer::default();
|
||||
buffer.update(Some(&files), &file_selection(id), &[id]);
|
||||
|
||||
// an empty segment has no start
|
||||
assert_eq!(buffer.segment_ids.len(), buffer.segment_starts.len());
|
||||
let mut start = 0;
|
||||
let expected: Vec<u32> = lengths
|
||||
.iter()
|
||||
.filter(|len| **len > 0)
|
||||
.map(|len| {
|
||||
start += len;
|
||||
(start - len) as u32
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(buffer.segment_starts, expected);
|
||||
// the ends of the segments are always anchors, shown at every zoom level
|
||||
assert_eq!(buffer.anchor_indices.len(), buffer.anchor_zooms.len());
|
||||
assert!(buffer.anchor_indices.windows(2).all(|w| w[0] < w[1]));
|
||||
let total: usize = lengths.iter().sum();
|
||||
let ends = expected
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(expected.iter().skip(1).map(|start| start - 1))
|
||||
.chain([total as u32 - 1]);
|
||||
for end in ends {
|
||||
let position = buffer.anchor_indices.iter().position(|i| *i == end);
|
||||
assert_eq!(position.map(|p| buffer.anchor_zooms[p]), Some(0), "{end}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nothing_without_segments() {
|
||||
let (files, id) = loaded();
|
||||
let mut buffer = RoutingBuffer::default();
|
||||
buffer.update(Some(&files), &Selection::Empty, &[id]);
|
||||
assert!(buffer.anchor_indices.is_empty() && buffer.segment_starts.is_empty());
|
||||
// waypoints are not covered by the routing tool
|
||||
buffer.update(Some(&files), &Selection::Waypoints { file_id: id }, &[id]);
|
||||
assert!(buffer.anchor_indices.is_empty() && buffer.segment_starts.is_empty());
|
||||
buffer.update(None, &file_selection(id), &[id]);
|
||||
assert!(buffer.anchor_indices.is_empty() && buffer.segment_starts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_revision_changes_with_the_selected_segments() {
|
||||
let (mut files, id) = loaded();
|
||||
let mut buffer = RoutingBuffer::default();
|
||||
buffer.update(Some(&files), &file_selection(id), &[id]);
|
||||
let first = buffer.revision;
|
||||
|
||||
// the same segments: the indices still mean the same trackpoints
|
||||
buffer.update(Some(&files), &file_selection(id), &[id]);
|
||||
assert_eq!(buffer.revision, first);
|
||||
|
||||
// other trackpoints in the same segment
|
||||
let mut file = (*files[&id]).clone();
|
||||
file.trk[0].trkseg[0].rev_id = Default::default();
|
||||
files.insert(id, Rc::new(file));
|
||||
buffer.update(Some(&files), &file_selection(id), &[id]);
|
||||
assert_ne!(buffer.revision, first);
|
||||
let second = buffer.revision;
|
||||
|
||||
// another selection
|
||||
buffer.update(Some(&files), &Selection::Empty, &[id]);
|
||||
assert_ne!(buffer.revision, second);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
use crate::{
|
||||
Action, Apply, Clipboard, Command, CoordinatesCache, Diff, FileId, FileOrder, FileStructure,
|
||||
FileStructureCache, GlobalStatistics, SelectMode, Selection, Stack, State, StatisticsBuffer,
|
||||
StatisticsCache, TrackSegmentId, Trackpoint, TrackpointCategories, Waypoint, WaypointId,
|
||||
FileStructureCache, GlobalStatistics, RoutingBuffer, SelectMode, Selection, Stack, State,
|
||||
StatisticsBuffer, StatisticsCache, TrackSegmentId, Trackpoint, TrackpointCategories, Waypoint,
|
||||
WaypointId,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -18,6 +19,7 @@ pub struct Engine {
|
||||
order_changed: bool,
|
||||
selection_changed: bool,
|
||||
statistics_buffer: StatisticsBuffer,
|
||||
routing_buffer: RoutingBuffer,
|
||||
categories: TrackpointCategories,
|
||||
clipboard: Option<Clipboard>,
|
||||
clipboard_changed: bool,
|
||||
@@ -28,6 +30,11 @@ impl Engine {
|
||||
&self.statistics_buffer
|
||||
}
|
||||
|
||||
/// The anchors of the routing tool among the trackpoints of the selection.
|
||||
pub fn routing(&self) -> &RoutingBuffer {
|
||||
&self.routing_buffer
|
||||
}
|
||||
|
||||
/// The names of the surfaces and highways that the trackpoints refer to by code.
|
||||
pub fn categories(&self) -> &TrackpointCategories {
|
||||
&self.categories
|
||||
@@ -249,6 +256,8 @@ impl Engine {
|
||||
self.statistics_cache.update(current);
|
||||
self.coordinates_cache.update(current);
|
||||
self.structure_cache.update(current, self.diff.as_ref());
|
||||
self.routing_buffer
|
||||
.update(current, &self.selection, &self.order.0);
|
||||
self.statistics_buffer
|
||||
.update(
|
||||
&self
|
||||
@@ -283,7 +292,13 @@ mod tests {
|
||||
}
|
||||
|
||||
fn new(engine: &mut Engine, name: &str) -> bool {
|
||||
edit(engine, Command::New(New { name }))
|
||||
edit(
|
||||
engine,
|
||||
Command::New(New {
|
||||
name,
|
||||
trackpoint: None,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn load(engine: &mut Engine, path: &str) -> bool {
|
||||
|
||||
@@ -40,7 +40,59 @@ pub enum Selection {
|
||||
},
|
||||
}
|
||||
|
||||
/// Where a track segment is: its file, then its track and its own position in it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SegmentLocation {
|
||||
pub file_id: FileId,
|
||||
pub trk: usize,
|
||||
pub seg: usize,
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
/// The track segments covered by the selection, in order: the segments of several selected
|
||||
/// files follow `order` (the files missing from it come last), then the tracks and segments
|
||||
/// follow the file. Waypoints are not segments: selecting only waypoints covers none.
|
||||
pub fn segment_locations(&self, files: &StackEntry, order: &[FileId]) -> Vec<SegmentLocation> {
|
||||
let mut locations = vec![];
|
||||
let mut add = |file_id: FileId,
|
||||
trk_filter: &dyn Fn(&crate::Track) -> bool,
|
||||
seg_filter: &dyn Fn(&crate::TrackSegment) -> bool| {
|
||||
let Some(file) = files.get(&file_id) else {
|
||||
return;
|
||||
};
|
||||
for (trk, track) in file.trk.iter().enumerate().filter(|(_, t)| trk_filter(t)) {
|
||||
for (seg, segment) in track.trkseg.iter().enumerate() {
|
||||
if seg_filter(segment) {
|
||||
locations.push(SegmentLocation { file_id, trk, seg });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
match self {
|
||||
Selection::Empty | Selection::Waypoints { .. } | Selection::Waypoint { .. } => (),
|
||||
Selection::File { file_ids } => {
|
||||
let ordered = order
|
||||
.iter()
|
||||
.filter(|id| file_ids.contains(id))
|
||||
.chain(file_ids.iter().filter(|id| !order.contains(id)));
|
||||
for id in ordered {
|
||||
add(*id, &|_| true, &|_| true);
|
||||
}
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
add(*file_id, &|trk| trk_ids.contains(&trk.id), &|_| true)
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => add(*file_id, &|trk| trk.id == *trk_id, &|seg| {
|
||||
trkseg_ids.contains(&seg.id)
|
||||
}),
|
||||
}
|
||||
locations
|
||||
}
|
||||
|
||||
/// Drops what does not exist anymore (e.g. after an undo): files, and the tracks, segments
|
||||
/// and waypoints of the files that remain. A selection left with nothing becomes empty.
|
||||
pub fn retain_existing(&mut self, files: &StackEntry) {
|
||||
|
||||
Reference in new issue
Block a user