mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-10 07:36:29 +00:00
reduce
This commit is contained in:
1 parent
b29c4cdb08
commit
bec1ce4c9b
13 files changed
+372
-215
No files matched your search
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
core::gpx::{TrackSegment, TrackSegmentIndex},
|
||||
core::utils::crossarc,
|
||||
core::utils::{crossarc, crossarc_lnglat},
|
||||
};
|
||||
|
||||
pub fn ramer_douglas_peucker<F>(
|
||||
@@ -86,6 +86,85 @@ fn ramer_douglas_peucker_helper<M>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Lowest tolerance (in meters) of the reduction of a segment, see [`reduction_distances`].
|
||||
pub const MIN_REDUCE_TOLERANCE: f64 = 0.1;
|
||||
|
||||
/// The distance (in meters) from a trackpoint to the line between two others of a segment.
|
||||
fn lnglat_measure(
|
||||
trkseg: &TrackSegment,
|
||||
) -> impl Fn(TrackSegmentIndex, TrackSegmentIndex, TrackSegmentIndex) -> f64 + '_ {
|
||||
move |start, end, idx| {
|
||||
crossarc_lnglat(
|
||||
trkseg[start].coordinates,
|
||||
trkseg[end].coordinates,
|
||||
trkseg[idx].coordinates,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The indices of the trackpoints of a segment that are kept when it is reduced with `tolerance`
|
||||
/// meters (Ramer-Douglas-Peucker), in order. The ends are always kept.
|
||||
pub fn reduce_indices(trkseg: &TrackSegment, tolerance: f64) -> Vec<usize> {
|
||||
ramer_douglas_peucker_by(trkseg, &lnglat_measure(trkseg), tolerance)
|
||||
.into_iter()
|
||||
.map(|(idx, _)| idx.flat)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// For each trackpoint of a segment, the tolerance (in meters) up to which it is kept by
|
||||
/// [`reduce_indices`]: infinite for the ends, which are always kept, zero for the trackpoints that
|
||||
/// are not selected even at [`MIN_REDUCE_TOLERANCE`]. A trackpoint is kept with a tolerance if its
|
||||
/// value is greater than it.
|
||||
///
|
||||
/// A trackpoint is only selected if the ones that split the path before it are, so its value is
|
||||
/// the smallest distance at which it or one of them was selected.
|
||||
pub fn reduction_distances(trkseg: &TrackSegment) -> Vec<f64> {
|
||||
let mut distances = vec![0.0; trkseg.len()];
|
||||
let (Some(first), Some(last)) = (trkseg.first_index(), trkseg.last_index()) else {
|
||||
return distances;
|
||||
};
|
||||
distances[first.flat] = f64::INFINITY;
|
||||
distances[last.flat] = f64::INFINITY;
|
||||
if trkseg.len() > 2 {
|
||||
let measure = lnglat_measure(trkseg);
|
||||
reduction_distances_helper(trkseg, first, last, &measure, f64::INFINITY, &mut distances);
|
||||
}
|
||||
distances
|
||||
}
|
||||
|
||||
fn reduction_distances_helper<M>(
|
||||
trkseg: &TrackSegment,
|
||||
start: TrackSegmentIndex,
|
||||
end: TrackSegmentIndex,
|
||||
measure: &M,
|
||||
bound: f64,
|
||||
distances: &mut [f64],
|
||||
) where
|
||||
M: Fn(TrackSegmentIndex, TrackSegmentIndex, TrackSegmentIndex) -> f64,
|
||||
{
|
||||
let mut max_idx = None;
|
||||
let mut max_dist = 0.0;
|
||||
let mut cur = trkseg.next_index(Some(start));
|
||||
while let Some(idx) = cur {
|
||||
if idx == end {
|
||||
break;
|
||||
}
|
||||
let dist = measure(start, end, idx);
|
||||
if dist > max_dist {
|
||||
max_idx = Some(idx);
|
||||
max_dist = dist;
|
||||
}
|
||||
cur = trkseg.next_index(cur);
|
||||
}
|
||||
|
||||
if let Some(idx) = max_idx.filter(|_| max_dist > MIN_REDUCE_TOLERANCE) {
|
||||
let distance = max_dist.min(bound);
|
||||
distances[idx.flat] = distance;
|
||||
reduction_distances_helper(trkseg, start, idx, measure, distance, distances);
|
||||
reduction_distances_helper(trkseg, idx, end, measure, distance, distances);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Trackpoint, TrackpointChunk};
|
||||
|
||||
@@ -2,7 +2,8 @@ use crate::{Apply, CommandError, State};
|
||||
use crate::{
|
||||
ChangeLoopStart, Clean, Crop, Delete, DeleteAll, DeleteWaypoint, Duplicate, EditWaypoint,
|
||||
Elevation, Extract, InsertAnchor, Load, LoadFiles, Merge, Metadata, Move, MoveWaypoint, New,
|
||||
NewTrack, NewTrackSegment, NewWaypoint, Paste, Reverse, RoundTrip, Route, Split, Style, Time,
|
||||
NewTrack, NewTrackSegment, NewWaypoint, Paste, Reduce, Reverse, RoundTrip, Route, Split, Style,
|
||||
Time,
|
||||
};
|
||||
|
||||
/// A user action that edits the files. Each variant wraps the command's own struct, whose
|
||||
@@ -32,6 +33,7 @@ pub enum Command<'a> {
|
||||
EditWaypoint(EditWaypoint<'a>),
|
||||
MoveWaypoint(MoveWaypoint),
|
||||
Crop(Crop),
|
||||
Reduce(Reduce),
|
||||
Split(Split),
|
||||
Time(Time),
|
||||
Merge(Merge),
|
||||
@@ -65,6 +67,7 @@ impl Apply for Command<'_> {
|
||||
Command::EditWaypoint(c) => c.apply(state),
|
||||
Command::MoveWaypoint(c) => c.apply(state),
|
||||
Command::Crop(c) => c.apply(state),
|
||||
Command::Reduce(c) => c.apply(state),
|
||||
Command::Split(c) => c.apply(state),
|
||||
Command::Time(c) => c.apply(state),
|
||||
Command::Merge(c) => c.apply(state),
|
||||
|
||||
@@ -8,6 +8,7 @@ mod insert_anchor;
|
||||
mod merge;
|
||||
mod move_waypoint;
|
||||
mod new_waypoint;
|
||||
mod reduce;
|
||||
mod reverse;
|
||||
mod round_trip;
|
||||
mod route;
|
||||
@@ -24,6 +25,7 @@ pub use insert_anchor::*;
|
||||
pub use merge::*;
|
||||
pub use move_waypoint::*;
|
||||
pub use new_waypoint::*;
|
||||
pub use reduce::*;
|
||||
pub use reverse::*;
|
||||
pub use round_trip::*;
|
||||
pub use route::*;
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Apply, CommandError, File, State, Trackpoint, reduce_indices, reduction_distances};
|
||||
|
||||
/// 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 reduced = 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;
|
||||
}
|
||||
let points: Vec<Trackpoint> = kept.iter().map(|&i| segment[i].clone()).collect();
|
||||
reduced.push((*location, points));
|
||||
}
|
||||
if reduced.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
// the segments of a file are consecutive
|
||||
for group in reduced.chunk_by(|a, b| a.0.file_id == b.0.file_id) {
|
||||
let file_id = group[0].0.file_id;
|
||||
let mut file: File = (*state.files[&file_id]).clone();
|
||||
for (location, points) in group {
|
||||
let segment = &mut file.trk[location.trk].trkseg[location.seg];
|
||||
let len = segment.len();
|
||||
segment.splice(0, len, points.clone());
|
||||
segment.rev_id = Default::default();
|
||||
}
|
||||
state.files.insert(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 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)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, File, SegmentLocation, State, Statistics, TrackSegment, Trackpoint, artificial_weights,
|
||||
shifted_and_compressed, with_artificial_timestamps, with_timestamps,
|
||||
Apply, CommandError, File, SegmentLocation, State, Statistics, TrackSegment, Trackpoint,
|
||||
artificial_weights, shifted_and_compressed, with_artificial_timestamps, with_timestamps,
|
||||
};
|
||||
|
||||
/// How the timestamps of the selection are set.
|
||||
@@ -50,7 +50,10 @@ impl Apply for Time {
|
||||
let segment_at = |location: &SegmentLocation| -> &TrackSegment {
|
||||
&state.files[&location.file_id].trk[location.trk].trkseg[location.seg]
|
||||
};
|
||||
if locations.iter().all(|location| segment_at(location).is_empty()) {
|
||||
if locations
|
||||
.iter()
|
||||
.all(|location| segment_at(location).is_empty())
|
||||
{
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
@@ -164,7 +167,11 @@ mod tests {
|
||||
fx.files[&id]
|
||||
.trk
|
||||
.iter()
|
||||
.flat_map(|trk| trk.trkseg.iter().flat_map(|seg| seg.iter().map(|pt| pt.time)))
|
||||
.flat_map(|trk| {
|
||||
trk.trkseg
|
||||
.iter()
|
||||
.flat_map(|seg| seg.iter().map(|pt| pt.time))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
Action, Apply, Clipboard, Command, CoordinatesCache, Diff, FileId, FileOrder, FileStructure,
|
||||
FileStructureCache, GlobalStatistics, RoutingBuffer, SelectMode, Selection, Stack, State,
|
||||
StatisticsBuffer, StatisticsCache, TrackSegmentId, Trackpoint, TrackpointCategories, Waypoint,
|
||||
WaypointId,
|
||||
WaypointId, reduction_distances,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -101,6 +101,23 @@ impl Engine {
|
||||
(index < segment.len()).then(|| &segment[index])
|
||||
}
|
||||
|
||||
/// For each trackpoint of the selection (numbered as in the statistics), the tolerance up to
|
||||
/// which `Reduce` keeps it, see [`reduction_distances`].
|
||||
pub fn reduction_distances(&self) -> Vec<f64> {
|
||||
let Some(files) = self.stack.current() else {
|
||||
return vec![];
|
||||
};
|
||||
self.selection
|
||||
.segment_locations(files, &self.order.0)
|
||||
.iter()
|
||||
.flat_map(|location| {
|
||||
reduction_distances(
|
||||
&files[&location.file_id].trk[location.trk].trkseg[location.seg],
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Whether there is something to undo.
|
||||
pub fn can_undo(&self) -> bool {
|
||||
self.stack.can_undo()
|
||||
|
||||
Reference in new issue
Block a user