From a921d4b9cd65b57a259898245ef80eccab428cf4 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Fri, 2 Oct 2026 18:50:10 +0200 Subject: [PATCH] fixes --- gpx-rs/src/algorithm/simplify.rs | 38 ++--- gpx-rs/src/algorithm/smooth.rs | 3 + gpx-rs/src/gpx/segment.rs | 26 ++-- gpx-rs/src/statistics/statistics.rs | 213 +++++++++++++--------------- 4 files changed, 137 insertions(+), 143 deletions(-) diff --git a/gpx-rs/src/algorithm/simplify.rs b/gpx-rs/src/algorithm/simplify.rs index 7951a22a8..e9b95421e 100644 --- a/gpx-rs/src/algorithm/simplify.rs +++ b/gpx-rs/src/algorithm/simplify.rs @@ -1,13 +1,15 @@ -use crate::gpx::{TrackSegment, TrackSegmentIndex}; +use crate::{ + gpx::{TrackSegment, TrackSegmentIndex}, + utils::crossarc, +}; -pub fn ramer_douglas_peucker( +pub fn ramer_douglas_peucker( trkseg: &TrackSegment, - distance: &F, - epsilon: T, + mapping: &F, + epsilon: f64, ) -> Vec where - F: Fn(TrackSegmentIndex, TrackSegmentIndex, TrackSegmentIndex) -> T, - T: Default + PartialOrd + Copy, + F: Fn(TrackSegmentIndex) -> (f64, f64), { match trkseg.len() { 0 => vec![], @@ -18,35 +20,37 @@ where let last = trkseg.last_index().unwrap(); let mut indices = vec![first]; - ramer_douglas_peucker_helper(trkseg, first, last, distance, epsilon, &mut indices); + ramer_douglas_peucker_helper(trkseg, first, last, mapping, epsilon, &mut indices); indices.push(last); indices } } } -fn ramer_douglas_peucker_helper( +fn ramer_douglas_peucker_helper( trkseg: &TrackSegment, start: TrackSegmentIndex, end: TrackSegmentIndex, - distance: &F, - epsilon: T, + mapping: &F, + epsilon: f64, indices: &mut Vec, ) where - F: Fn(TrackSegmentIndex, TrackSegmentIndex, TrackSegmentIndex) -> T, - T: Default + PartialOrd + Copy, + F: Fn(TrackSegmentIndex) -> (f64, f64), { let mut max_idx = None; - let mut max_dist = T::default(); + 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 dist = distance(start, end, idx); + let pt = mapping(idx); + let dist = crossarc(start_pt.0, start_pt.1, end_pt.0, end_pt.1, pt.0, pt.1); if dist > max_dist { max_idx = Some(idx); max_dist = dist; @@ -57,9 +61,9 @@ fn ramer_douglas_peucker_helper( if let Some(idx) = max_idx { if max_dist > epsilon { - ramer_douglas_peucker_helper(trkseg, start, idx, distance, epsilon, indices); + ramer_douglas_peucker_helper(trkseg, start, idx, mapping, epsilon, indices); indices.push(idx); - ramer_douglas_peucker_helper(trkseg, idx, end, distance, epsilon, indices); + ramer_douglas_peucker_helper(trkseg, idx, end, mapping, epsilon, indices); } } } diff --git a/gpx-rs/src/algorithm/smooth.rs b/gpx-rs/src/algorithm/smooth.rs index 9c520419c..92ca96bb8 100644 --- a/gpx-rs/src/algorithm/smooth.rs +++ b/gpx-rs/src/algorithm/smooth.rs @@ -45,6 +45,9 @@ macro_rules! for_each_window { let $r = window_end; $body // go next + if window_center == $right { + break; + } window_center = $trkseg.next_index(window_center); } }}; diff --git a/gpx-rs/src/gpx/segment.rs b/gpx-rs/src/gpx/segment.rs index 7d6104d91..4b3768391 100644 --- a/gpx-rs/src/gpx/segment.rs +++ b/gpx-rs/src/gpx/segment.rs @@ -60,6 +60,7 @@ impl TrackSegment { let mut next = cur.map_or_default(|idx| TrackSegmentIndex { chunk: idx.chunk, pos: idx.pos + 1, + flat: idx.flat + 1, }); loop { if next.chunk >= self.chunks.len() { @@ -78,6 +79,7 @@ impl TrackSegment { let mut prev = cur.unwrap_or(TrackSegmentIndex { chunk: self.chunks.len(), pos: 0, + flat: self.cumul_length.last().copied().unwrap_or_default(), }); loop { if prev.pos == 0 { @@ -86,22 +88,15 @@ impl TrackSegment { } prev.chunk -= 1; prev.pos = self.chunks[prev.chunk].trkpt.len(); + prev.flat -= 1; } else { prev.pos -= 1; + prev.flat -= 1; return Some(prev); } } } - pub fn to_flat_index(&self, idx: TrackSegmentIndex) -> usize { - idx.pos - + if idx.chunk > 0 { - self.cumul_length[idx.chunk - 1] - } else { - 0 - } - } - fn locate(&self, idx: usize) -> Option { let chunk = self.cumul_length.partition_point(|l| idx >= *l); if chunk >= self.chunks.len() { @@ -115,7 +110,11 @@ impl TrackSegment { if pos >= self.chunks[chunk].trkpt.len() { None } else { - Some(TrackSegmentIndex { chunk, pos }) + Some(TrackSegmentIndex { + chunk, + pos, + flat: idx, + }) } } } @@ -140,6 +139,7 @@ impl Index for TrackSegment { pub struct TrackSegmentIndex { pub chunk: usize, pub pos: usize, + pub flat: usize, } #[derive(Debug, Clone)] @@ -166,10 +166,7 @@ impl<'a> Iterator for TrackSegmentIterator<'a> { } fn nth(&mut self, n: usize) -> Option { - let idx = self - .idx - .map_or_default(|idx| self.trkseg.to_flat_index(idx)) - + n; + let idx = self.idx.map_or_default(|idx| idx.flat) + n; self.idx = self.trkseg.locate(idx); self.idx.map(|idx| &self.trkseg[idx]) } @@ -242,6 +239,7 @@ mod tests { trkseg[TrackSegmentIndex { chunk: trkseg.chunks.len(), pos: 0, + flat: 0, }] .ele; } diff --git a/gpx-rs/src/statistics/statistics.rs b/gpx-rs/src/statistics/statistics.rs index 7bf6e1454..6d2eb5f8a 100644 --- a/gpx-rs/src/statistics/statistics.rs +++ b/gpx-rs/src/statistics/statistics.rs @@ -1,19 +1,19 @@ use crate::{ algorithm::ramer_douglas_peucker, for_each_window, - gpx::{LngLat, LngLatBounds, TrackSegment, Trackpoint}, + gpx::{LngLat, LngLatBounds, TrackSegment, TrackSegmentIndex, Trackpoint}, statistics::sum_options, - utils::{distance, slope, speed}, + utils::{distance, slope, speed, time_diff}, }; #[derive(Default, Debug)] -pub struct GPXStatistics { +pub struct Statistics { pub global: GlobalStatistics, pub local: Vec, } -impl GPXStatistics { - pub fn total_time(&self) -> Option { +impl Statistics { + pub fn total_time(&self) -> Option { self.global.total_time() } @@ -38,7 +38,8 @@ impl GPXStatistics { } stats.compute_smoothed_speed(trkseg); - stats.compute_smoothed_elevation_gain(trkseg); + stats.compute_smoothed_elevation_segments(trkseg); + stats.compute_smoothed_slope(trkseg); stats } @@ -53,7 +54,7 @@ impl GPXStatistics { fn accumulate_distance_and_time(&mut self, prev: &Trackpoint, cur: &Trackpoint) { let dist = distance(prev.coordinates, cur.coordinates); - let time = cur.time_diff(prev); + let time = time_diff(&cur.time, &prev.time); self.global.total_distance += dist; @@ -91,144 +92,132 @@ impl GPXStatistics { trkseg.first_index(), trkseg.last_index(), Some(10000), - |i, j| trkseg[i].time_diff(&trkseg[j]), + |i, j| time_diff(&trkseg[i].time, &trkseg[j].time), |i, left, right| { - let i = trkseg.to_flat_index(i); - let left = trkseg.to_flat_index(left); - let right = trkseg.to_flat_index(right); - self.local[i].speed = trkseg[right].time_diff(&trkseg[left]).map(|t| { - speed( - self.local[right].total_distance - self.local[left].total_distance, - t, - ) - }); + self.local[i.flat].speed = + time_diff(&trkseg[right].time, &trkseg[left].time).map(|t| { + speed( + self.local[right.flat].total_distance + - self.local[left.flat].total_distance, + t, + ) + }); }, ); } - fn compute_smoothed_elevation_gain(&mut self, trkseg: &TrackSegment) { - let simplified = ramer_douglas_peucker( - trkseg, - &|i, j, k| { - let x1 = self.local[trkseg.to_flat_index(i)].total_distance * 1000.0; - let x2 = self.local[trkseg.to_flat_index(j)].total_distance * 1000.0; - let x3 = self.local[trkseg.to_flat_index(k)].total_distance * 1000.0; - let y1 = trkseg[i].ele; - let y2 = trkseg[j].ele; - let y3 = trkseg[k].ele; - - let dist = ((y2 - y1).powi(2) + (x2 - x1).powi(2)).sqrt(); - if dist == 0.0 { - ((x3 - x1).powi(2) + (y3 - y1).powi(2)).sqrt() - } else { - ((y2 - y1) * x3 - (x2 - x1) * y3 + x2 * y1 - y2 * x1).abs() / dist - } - }, - 20.0, - ); + fn compute_smoothed_elevation_segments(&mut self, trkseg: &TrackSegment) { + let simplified = self.get_elevation_extremas(trkseg); for i in 0..(simplified.len() - 1) { let start = simplified[i]; let end = simplified[i + 1]; let last = i + 1 == simplified.len() - 1; - let mut cumul_ele = 0.0; - let mut current_left = start; - let mut current_right = Some(start); - let mut prev_smoothed_ele = trkseg[start].ele; - - for_each_window!( - trkseg, - Some(start), - Some(end), - 0.1, - |i, j| { - let i = trkseg.to_flat_index(i); - let j = trkseg.to_flat_index(j); - self.local[j].total_distance - self.local[i].total_distance - }, - |i, left, right| { - while current_left != left { - cumul_ele -= trkseg[current_left].ele; - current_left = trkseg.next_index(Some(current_left)).unwrap(); - } - while let Some(current) = current_right { - if current > right { - break; - } - cumul_ele += trkseg[current].ele; - current_right = trkseg.next_index(current_right); - } - - let flat_i = trkseg.to_flat_index(i); - let flat_left = trkseg.to_flat_index(left); - let flat_right = trkseg.to_flat_index(right); - - let smoothed_ele: f64 = if i == start || i == end { - trkseg[i].ele - } else { - cumul_ele / (flat_right - flat_left + 1) as f64 - }; - - let delta = smoothed_ele - prev_smoothed_ele; - if delta > 0.0 { - self.global.elevation_gain += delta; - } else if delta < 0.0 { - self.global.elevation_loss -= delta; - } - - if i < end || last { - self.local[flat_i].elevation_gain = self.global.elevation_gain; - self.local[flat_i].elevation_loss = self.global.elevation_loss; - } - - prev_smoothed_ele = smoothed_ele; - }, - ); - - let flat_start = trkseg.to_flat_index(start); - let flat_end = trkseg.to_flat_index(end); + self.compute_smoothed_elevation_gain(trkseg, start, end, last); let segment_dist = - self.local[flat_end].total_distance - self.local[flat_start].total_distance; + self.local[end.flat].total_distance - self.local[start.flat].total_distance; let segment_ele = trkseg[end].ele - trkseg[start].ele; let segment_slope = slope(segment_ele, segment_dist); - for k in flat_start..(flat_end + last as usize) { + for k in start.flat..(end.flat + last as usize) { self.local[k].slope_segment = SlopeSegment { slope: segment_slope, distance: segment_dist, }; } } + } + fn compute_smoothed_elevation_gain( + &mut self, + trkseg: &TrackSegment, + start: TrackSegmentIndex, + end: TrackSegmentIndex, + last: bool, + ) { + let mut cumul_ele = 0.0; + let mut current_left = start; + let mut current_right = Some(start); + let mut prev_smoothed_ele = trkseg[start].ele; + + for_each_window!( + trkseg, + Some(start), + Some(end), + 0.1, + |i, j| self.local[j.flat].total_distance - self.local[i.flat].total_distance, + |i, left, right| { + while current_left != left { + cumul_ele -= trkseg[current_left].ele; + current_left = trkseg.next_index(Some(current_left)).unwrap(); + } + while let Some(current) = current_right { + if current > right { + break; + } + cumul_ele += trkseg[current].ele; + current_right = trkseg.next_index(current_right); + } + + let smoothed_ele = if i == start || i == end { + trkseg[i].ele + } else { + cumul_ele / (right.flat - left.flat + 1) as f64 + }; + + let delta = smoothed_ele - prev_smoothed_ele; + if delta > 0.0 { + self.global.elevation_gain += delta; + } else if delta < 0.0 { + self.global.elevation_loss -= delta; + } + + if i < end || last { + self.local[i.flat].elevation_gain = self.global.elevation_gain; + self.local[i.flat].elevation_loss = self.global.elevation_loss; + } + + prev_smoothed_ele = smoothed_ele; + }, + ); + } + + fn compute_smoothed_slope(&mut self, trkseg: &TrackSegment) { for_each_window!( trkseg, trkseg.first_index(), trkseg.last_index(), 0.05, - |i, j| { - let i = trkseg.to_flat_index(i); - let j = trkseg.to_flat_index(j); - self.local[j].total_distance - self.local[i].total_distance - }, + |i, j| self.local[j.flat].total_distance - self.local[i.flat].total_distance, |i, left, right| { - let flat_i = trkseg.to_flat_index(i); - let flat_left = trkseg.to_flat_index(left); - let flat_right = trkseg.to_flat_index(right); let dist = - self.local[flat_right].total_distance - self.local[flat_left].total_distance; + self.local[right.flat].total_distance - self.local[left.flat].total_distance; let ele = trkseg[right].ele - trkseg[left].ele; - self.local[flat_i].slope = slope(ele, dist); + self.local[i.flat].slope = slope(ele, dist); }, ); } + + fn get_elevation_extremas(&self, trkseg: &TrackSegment) -> Vec { + ramer_douglas_peucker( + trkseg, + &|idx| { + ( + self.local[idx.flat].total_distance * 1000.0, + trkseg[idx].ele, + ) + }, + 20.0, + ) + } } #[derive(Default, Debug)] pub struct GlobalStatistics { pub total_distance: f64, pub moving_distance: Option, - pub moving_time: Option, + pub moving_time: Option, pub elevation_gain: f64, pub elevation_loss: f64, pub start_time: Option, @@ -237,8 +226,8 @@ pub struct GlobalStatistics { } impl GlobalStatistics { - pub fn total_time(&self) -> Option { - self.start_time.zip(self.end_time).map(|(t1, t2)| t2 - t1) + pub fn total_time(&self) -> Option { + time_diff(&self.start_time, &self.end_time) } pub fn total_speed(&self) -> Option { @@ -265,8 +254,8 @@ impl GlobalStatistics { pub struct TrackpointStatistics { pub total_distance: f64, pub moving_distance: Option, - pub total_time: Option, - pub moving_time: Option, + pub total_time: Option, + pub moving_time: Option, pub speed: Option, pub elevation_gain: f64, pub elevation_loss: f64, @@ -275,7 +264,7 @@ pub struct TrackpointStatistics { } impl TrackpointStatistics { - fn from_partial_stats(stats: &GPXStatistics) -> Self { + fn from_partial_stats(stats: &Statistics) -> Self { Self { total_distance: stats.global.total_distance, moving_distance: stats.global.moving_distance, @@ -313,7 +302,7 @@ mod tests { let gpx = parse(data.as_bytes()).unwrap(); let trkseg = &gpx.trk[0].trkseg[0]; - let stats = GPXStatistics::compute(trkseg); + let stats = Statistics::compute(trkseg); assert_eq!(stats.local.len(), trkseg.len()); for trkpt_stats in stats.local.iter() { assert!(trkpt_stats.speed.is_some());