This commit is contained in:
vcoppe committed 2026-10-14 09:22:07 +02:00
1 parent 41b9116c6a
commit 97d597e139
181 files changed
+1970 -2643

No files matched your search

+149
View File
@@ -0,0 +1,149 @@
use crate::{Apply, CommandError, State, reduce_indices, update_segments};
/// Removes the trackpoints of the selected segments that are less than `tolerance` meters away
/// from the line of their neighbours (Ramer-Douglas-Peucker, see [`reduce_indices`]). The ends of the segments are
/// kept.
#[derive(Debug)]
pub struct Reduce {
pub tolerance: f64,
}
impl Apply for Reduce {
fn apply(self, state: &mut State) -> Result<(), CommandError> {
if !(self.tolerance.is_finite() && self.tolerance > 0.0) {
return Err(CommandError::InvalidData(format!(
"tolerance {} should be positive",
self.tolerance
)));
}
let locations = state
.selection
.segment_locations(state.files, &state.order.0);
let mut changed = vec![];
let mut kept_points = vec![];
for location in &locations {
let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg];
let kept = reduce_indices(segment, self.tolerance);
if kept.len() == segment.len() {
continue;
}
changed.push(*location);
kept_points.push(kept.iter().map(|&i| segment[i].clone()).collect::<Vec<_>>());
}
if changed.is_empty() {
return Err(CommandError::NothingToDo);
}
update_segments(state, &changed, |i, segment| {
let len = segment.len();
segment.splice(0, len, std::mem::take(&mut kept_points[i]));
});
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::{FileId, Load, Selection, engine::command::fixture::Fixture, reduction_distances};
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 count(fx: &Fixture, id: FileId) -> usize {
fx.files[&id]
.trk
.iter()
.flat_map(|trk| &trk.trkseg)
.map(|seg| seg.len())
.sum()
}
#[test]
fn test_a_larger_tolerance_keeps_fewer_points_and_the_ends() {
let (mut fx, id) = loaded("data/with_time.gpx");
let before = count(&fx, id);
let first = fx.files[&id].trk[0].trkseg[0][0].coordinates;
Reduce { tolerance: 5.0 }.apply(&mut fx.state()).unwrap();
let medium = count(&fx, id);
assert!(medium < before);
assert_eq!(fx.files[&id].trk[0].trkseg[0][0].coordinates.lng, first.lng);
Reduce { tolerance: 500.0 }.apply(&mut fx.state()).unwrap();
assert!(count(&fx, id) < medium);
}
#[test]
fn test_the_distances_tell_which_points_are_kept() {
for path in ["data/with_time.gpx", "data/with_tracks_and_segments.gpx"] {
let (fx, id) = loaded(path);
for seg in fx.files[&id].trk.iter().flat_map(|trk| &trk.trkseg) {
let distances = reduction_distances(seg);
for tolerance in [0.1, 0.5, 1.0, 3.0, 10.0, 30.0, 100.0, 1000.0] {
let from_distances: Vec<usize> = (0..seg.len())
.filter(|&i| distances[i] > tolerance)
.collect();
assert_eq!(
from_distances,
reduce_indices(seg, tolerance),
"{path} {tolerance}"
);
}
}
}
}
#[test]
fn test_only_the_selected_segments_are_reduced() {
let (mut fx, id) = loaded("data/with_time.gpx");
let others: usize = fx.files[&id]
.trk
.iter()
.flat_map(|t| &t.trkseg)
.skip(1)
.map(|s| s.len())
.sum();
let expected = reduce_indices(&fx.files[&id].trk[0].trkseg[0], 5.0).len();
fx.selection = {
let track = &fx.files[&id].trk[0];
Selection::TrackSegment {
file_id: id,
trk_id: track.id,
trkseg_ids: HashSet::from([track.trkseg[0].id]),
}
};
Reduce { tolerance: 5.0 }.apply(&mut fx.state()).unwrap();
assert_eq!(count(&fx, id), expected + others);
}
#[test]
fn test_invalid_tolerance_and_nothing_to_remove() {
let (mut fx, _) = loaded("data/simple.gpx");
assert!(matches!(
Reduce { tolerance: 0.0 }.apply(&mut fx.state()),
Err(CommandError::InvalidData(_))
));
assert!(matches!(
Reduce { tolerance: 1e-9 }.apply(&mut fx.state()),
Err(CommandError::NothingToDo)
));
}
}