2026-09-20 13:58:27 +02:00
|
|
|
use crate::{
|
2026-09-24 20:55:30 +02:00
|
|
|
algorithm::ramer_douglas_peucker,
|
2026-09-21 23:49:57 +02:00
|
|
|
for_each_window,
|
2026-09-23 19:53:39 +02:00
|
|
|
gpx::{LngLat, LngLatBounds, TrackSegment, Trackpoint},
|
2026-09-30 19:19:23 +02:00
|
|
|
statistics::sum_options,
|
2026-09-21 23:49:57 +02:00
|
|
|
utils::{distance, slope, speed},
|
2026-09-20 13:58:27 +02:00
|
|
|
};
|
|
|
|
|
|
2026-09-21 23:49:57 +02:00
|
|
|
#[derive(Default, Debug)]
|
2026-09-20 13:58:27 +02:00
|
|
|
pub struct GPXStatistics {
|
2026-09-24 20:55:30 +02:00
|
|
|
pub global: GlobalStatistics,
|
2026-09-20 13:58:27 +02:00
|
|
|
pub local: Vec<TrackpointStatistics>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl GPXStatistics {
|
2026-09-21 23:49:57 +02:00
|
|
|
pub fn total_time(&self) -> Option<i64> {
|
2026-09-30 19:19:23 +02:00
|
|
|
self.global.total_time()
|
2026-09-21 23:49:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn total_speed(&self) -> Option<f64> {
|
2026-09-30 19:19:23 +02:00
|
|
|
self.global.total_speed()
|
2026-09-21 23:49:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn moving_speed(&self) -> Option<f64> {
|
2026-09-30 19:19:23 +02:00
|
|
|
self.global.moving_speed()
|
2026-09-21 23:49:57 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-23 19:53:39 +02:00
|
|
|
pub fn compute(trkseg: &TrackSegment) -> Self {
|
2026-09-21 23:49:57 +02:00
|
|
|
let mut stats = Self::default();
|
2026-09-23 19:53:39 +02:00
|
|
|
if trkseg.len() == 0 {
|
2026-09-21 23:49:57 +02:00
|
|
|
return stats;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-23 19:53:39 +02:00
|
|
|
let mut prev = &trkseg[0];
|
|
|
|
|
for cur in trkseg.iter() {
|
2026-09-21 23:49:57 +02:00
|
|
|
stats.accumulate(prev, cur);
|
|
|
|
|
prev = cur;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-23 19:53:39 +02:00
|
|
|
stats.compute_smoothed_speed(trkseg);
|
|
|
|
|
stats.compute_smoothed_elevation_gain(trkseg);
|
2026-09-21 23:49:57 +02:00
|
|
|
|
|
|
|
|
stats
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-23 19:53:39 +02:00
|
|
|
fn accumulate(&mut self, prev: &Trackpoint, cur: &Trackpoint) {
|
2026-09-20 13:58:27 +02:00
|
|
|
self.accumulate_distance_and_time(prev, cur);
|
|
|
|
|
self.update_time_bounds(cur.time);
|
2026-09-24 20:55:30 +02:00
|
|
|
self.update_bounds(cur.coordinates);
|
2026-09-20 13:58:27 +02:00
|
|
|
self.local
|
|
|
|
|
.push(TrackpointStatistics::from_partial_stats(&self));
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-23 19:53:39 +02:00
|
|
|
fn accumulate_distance_and_time(&mut self, prev: &Trackpoint, cur: &Trackpoint) {
|
2026-09-21 23:49:57 +02:00
|
|
|
let dist = distance(prev.coordinates, cur.coordinates);
|
2026-09-23 19:53:39 +02:00
|
|
|
let time = cur.time_diff(prev);
|
2026-09-20 13:58:27 +02:00
|
|
|
|
2026-09-24 20:55:30 +02:00
|
|
|
self.global.total_distance += dist;
|
2026-09-20 13:58:27 +02:00
|
|
|
|
|
|
|
|
if let Some(time) = time {
|
|
|
|
|
let speed = speed(dist, time);
|
|
|
|
|
if speed >= 0.5 && speed <= 1500.0 {
|
2026-09-24 20:55:30 +02:00
|
|
|
self.global.moving_distance = self
|
|
|
|
|
.global
|
|
|
|
|
.moving_distance
|
|
|
|
|
.map_or(Some(dist), |d| Some(d + dist));
|
|
|
|
|
self.global.moving_time = self
|
|
|
|
|
.global
|
|
|
|
|
.moving_time
|
|
|
|
|
.map_or(Some(time), |t| Some(t + time));
|
2026-09-20 13:58:27 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn update_time_bounds(&mut self, time: Option<i64>) {
|
|
|
|
|
if let Some(time) = time {
|
2026-09-24 20:55:30 +02:00
|
|
|
if self.global.start_time.is_none() {
|
|
|
|
|
self.global.start_time = Some(time);
|
2026-09-20 13:58:27 +02:00
|
|
|
}
|
2026-09-24 20:55:30 +02:00
|
|
|
self.global.end_time = Some(time);
|
2026-09-20 13:58:27 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-24 20:55:30 +02:00
|
|
|
fn update_bounds(&mut self, coordinates: LngLat) {
|
|
|
|
|
self.global.bounds.extend(coordinates);
|
2026-09-20 13:58:27 +02:00
|
|
|
}
|
2026-09-21 23:49:57 +02:00
|
|
|
|
2026-09-23 19:53:39 +02:00
|
|
|
fn compute_smoothed_speed(&mut self, trkseg: &TrackSegment) {
|
2026-09-21 23:49:57 +02:00
|
|
|
for_each_window!(
|
2026-09-23 19:53:39 +02:00
|
|
|
trkseg,
|
|
|
|
|
trkseg.first_index(),
|
|
|
|
|
trkseg.last_index(),
|
2026-09-21 23:49:57 +02:00
|
|
|
Some(10000),
|
2026-09-23 19:53:39 +02:00
|
|
|
|i, j| trkseg[i].time_diff(&trkseg[j]),
|
2026-09-21 23:49:57 +02:00
|
|
|
|i, left, right| {
|
2026-09-23 19:53:39 +02:00
|
|
|
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,
|
|
|
|
|
)
|
|
|
|
|
});
|
2026-09-21 23:49:57 +02:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-23 19:53:39 +02:00
|
|
|
fn compute_smoothed_elevation_gain(&mut self, trkseg: &TrackSegment) {
|
2026-09-21 23:49:57 +02:00
|
|
|
let simplified = ramer_douglas_peucker(
|
2026-09-23 19:53:39 +02:00
|
|
|
trkseg,
|
2026-09-21 23:49:57 +02:00
|
|
|
&|i, j, k| {
|
2026-09-23 19:53:39 +02:00
|
|
|
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;
|
2026-09-21 23:49:57 +02:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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;
|
2026-09-23 19:53:39 +02:00
|
|
|
let mut current_right = Some(start);
|
|
|
|
|
let mut prev_smoothed_ele = trkseg[start].ele;
|
2026-09-21 23:49:57 +02:00
|
|
|
|
|
|
|
|
for_each_window!(
|
2026-09-23 19:53:39 +02:00
|
|
|
trkseg,
|
|
|
|
|
Some(start),
|
|
|
|
|
Some(end),
|
2026-09-21 23:49:57 +02:00
|
|
|
0.1,
|
2026-09-23 19:53:39 +02:00
|
|
|
|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
|
|
|
|
|
},
|
2026-09-21 23:49:57 +02:00
|
|
|
|i, left, right| {
|
2026-09-23 19:53:39 +02:00
|
|
|
while current_left != left {
|
|
|
|
|
cumul_ele -= trkseg[current_left].ele;
|
|
|
|
|
current_left = trkseg.next_index(Some(current_left)).unwrap();
|
2026-09-21 23:49:57 +02:00
|
|
|
}
|
2026-09-23 19:53:39 +02:00
|
|
|
while let Some(current) = current_right {
|
|
|
|
|
if current > right {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
cumul_ele += trkseg[current].ele;
|
|
|
|
|
current_right = trkseg.next_index(current_right);
|
2026-09-21 23:49:57 +02:00
|
|
|
}
|
2026-09-23 19:53:39 +02:00
|
|
|
|
|
|
|
|
let flat_i = trkseg.to_flat_index(i);
|
|
|
|
|
let flat_left = trkseg.to_flat_index(left);
|
|
|
|
|
let flat_right = trkseg.to_flat_index(right);
|
2026-09-21 23:49:57 +02:00
|
|
|
|
|
|
|
|
let smoothed_ele: f64 = if i == start || i == end {
|
2026-09-23 19:53:39 +02:00
|
|
|
trkseg[i].ele
|
2026-09-21 23:49:57 +02:00
|
|
|
} else {
|
2026-09-23 19:53:39 +02:00
|
|
|
cumul_ele / (flat_right - flat_left + 1) as f64
|
2026-09-21 23:49:57 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let delta = smoothed_ele - prev_smoothed_ele;
|
|
|
|
|
if delta > 0.0 {
|
2026-09-24 20:55:30 +02:00
|
|
|
self.global.elevation_gain += delta;
|
2026-09-21 23:49:57 +02:00
|
|
|
} else if delta < 0.0 {
|
2026-09-24 20:55:30 +02:00
|
|
|
self.global.elevation_loss -= delta;
|
2026-09-21 23:49:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if i < end || last {
|
2026-09-24 20:55:30 +02:00
|
|
|
self.local[flat_i].elevation_gain = self.global.elevation_gain;
|
|
|
|
|
self.local[flat_i].elevation_loss = self.global.elevation_loss;
|
2026-09-21 23:49:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prev_smoothed_ele = smoothed_ele;
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2026-09-23 19:53:39 +02:00
|
|
|
let flat_start = trkseg.to_flat_index(start);
|
|
|
|
|
let flat_end = trkseg.to_flat_index(end);
|
|
|
|
|
|
|
|
|
|
let segment_dist =
|
|
|
|
|
self.local[flat_end].total_distance - self.local[flat_start].total_distance;
|
|
|
|
|
let segment_ele = trkseg[end].ele - trkseg[start].ele;
|
2026-09-21 23:49:57 +02:00
|
|
|
let segment_slope = slope(segment_ele, segment_dist);
|
2026-09-23 19:53:39 +02:00
|
|
|
for k in flat_start..(flat_end + last as usize) {
|
2026-09-21 23:49:57 +02:00
|
|
|
self.local[k].slope_segment = SlopeSegment {
|
|
|
|
|
slope: segment_slope,
|
|
|
|
|
distance: segment_dist,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for_each_window!(
|
2026-09-23 19:53:39 +02:00
|
|
|
trkseg,
|
|
|
|
|
trkseg.first_index(),
|
|
|
|
|
trkseg.last_index(),
|
2026-09-21 23:49:57 +02:00
|
|
|
0.05,
|
2026-09-23 19:53:39 +02:00
|
|
|
|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
|
|
|
|
|
},
|
2026-09-21 23:49:57 +02:00
|
|
|
|i, left, right| {
|
2026-09-23 19:53:39 +02:00
|
|
|
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;
|
|
|
|
|
let ele = trkseg[right].ele - trkseg[left].ele;
|
|
|
|
|
self.local[flat_i].slope = slope(ele, dist);
|
2026-09-21 23:49:57 +02:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-09-20 13:58:27 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-21 23:49:57 +02:00
|
|
|
#[derive(Default, Debug)]
|
2026-09-24 20:55:30 +02:00
|
|
|
pub struct GlobalStatistics {
|
|
|
|
|
pub total_distance: f64,
|
|
|
|
|
pub moving_distance: Option<f64>,
|
|
|
|
|
pub moving_time: Option<i64>,
|
|
|
|
|
pub elevation_gain: f64,
|
|
|
|
|
pub elevation_loss: f64,
|
|
|
|
|
pub start_time: Option<i64>,
|
|
|
|
|
pub end_time: Option<i64>,
|
|
|
|
|
pub bounds: LngLatBounds,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl GlobalStatistics {
|
2026-09-30 19:19:23 +02:00
|
|
|
pub fn total_time(&self) -> Option<i64> {
|
|
|
|
|
self.start_time.zip(self.end_time).map(|(t1, t2)| t2 - t1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn total_speed(&self) -> Option<f64> {
|
|
|
|
|
self.total_time().map(|t| speed(self.total_distance, t))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn moving_speed(&self) -> Option<f64> {
|
|
|
|
|
self.moving_distance
|
|
|
|
|
.zip(self.moving_time)
|
|
|
|
|
.map(|(d, t)| speed(d, t))
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-24 20:55:30 +02:00
|
|
|
pub fn merge(&mut self, other: &GlobalStatistics) {
|
|
|
|
|
self.total_distance += other.total_distance;
|
|
|
|
|
self.moving_distance = sum_options(self.moving_distance, other.moving_distance);
|
|
|
|
|
self.moving_time = sum_options(self.moving_time, other.moving_time);
|
|
|
|
|
self.elevation_gain += other.elevation_gain;
|
|
|
|
|
self.elevation_loss += other.elevation_loss;
|
|
|
|
|
self.bounds.merge(&other.bounds);
|
|
|
|
|
}
|
2026-09-20 13:58:27 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-21 23:49:57 +02:00
|
|
|
#[derive(Default, Debug)]
|
2026-09-20 13:58:27 +02:00
|
|
|
pub struct TrackpointStatistics {
|
|
|
|
|
pub total_distance: f64,
|
2026-09-21 23:49:57 +02:00
|
|
|
pub moving_distance: Option<f64>,
|
|
|
|
|
pub total_time: Option<i64>,
|
|
|
|
|
pub moving_time: Option<i64>,
|
|
|
|
|
pub speed: Option<f64>,
|
2026-09-20 13:58:27 +02:00
|
|
|
pub elevation_gain: f64,
|
|
|
|
|
pub elevation_loss: f64,
|
|
|
|
|
pub slope: f64,
|
|
|
|
|
pub slope_segment: SlopeSegment,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TrackpointStatistics {
|
|
|
|
|
fn from_partial_stats(stats: &GPXStatistics) -> Self {
|
|
|
|
|
Self {
|
2026-09-24 20:55:30 +02:00
|
|
|
total_distance: stats.global.total_distance,
|
|
|
|
|
moving_distance: stats.global.moving_distance,
|
|
|
|
|
total_time: stats.total_time(),
|
|
|
|
|
moving_time: stats.global.moving_time,
|
|
|
|
|
// stats below are computed later
|
2026-09-21 23:49:57 +02:00
|
|
|
speed: None,
|
2026-09-24 20:55:30 +02:00
|
|
|
elevation_gain: Default::default(),
|
|
|
|
|
elevation_loss: Default::default(),
|
2026-09-20 13:58:27 +02:00
|
|
|
slope: Default::default(),
|
|
|
|
|
slope_segment: Default::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-24 20:55:30 +02:00
|
|
|
#[derive(Default, Debug)]
|
|
|
|
|
pub struct SlopeSegment {
|
|
|
|
|
pub slope: f64,
|
|
|
|
|
pub distance: f64,
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 23:49:57 +02:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use std::{fs::File, io::Read};
|
2026-09-20 13:58:27 +02:00
|
|
|
|
2026-09-24 20:55:30 +02:00
|
|
|
use crate::io::parse;
|
2026-09-21 23:49:57 +02:00
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-09-23 19:53:39 +02:00
|
|
|
fn test_compute_smoothed_speed() {
|
2026-09-21 23:49:57 +02:00
|
|
|
let mut f = File::open("data/with_time.gpx").unwrap();
|
|
|
|
|
let mut data = String::new();
|
|
|
|
|
let _ = f.read_to_string(&mut data);
|
|
|
|
|
let gpx = parse(data.as_bytes()).unwrap();
|
|
|
|
|
|
2026-09-23 19:53:39 +02:00
|
|
|
let trkseg = &gpx.trk[0].trkseg[0];
|
|
|
|
|
let stats = GPXStatistics::compute(trkseg);
|
|
|
|
|
assert_eq!(stats.local.len(), trkseg.len());
|
|
|
|
|
for trkpt_stats in stats.local.iter() {
|
|
|
|
|
assert!(trkpt_stats.speed.is_some());
|
|
|
|
|
let speed = trkpt_stats.speed.unwrap();
|
|
|
|
|
assert_ne!(speed, f64::NAN);
|
|
|
|
|
assert!((speed - 20.0).abs() < 0.1);
|
|
|
|
|
}
|
2026-09-20 13:58:27 +02:00
|
|
|
}
|
|
|
|
|
}
|