gpx_rs/gpx/analysis/
waypoint_path.rs1use std::time::Duration;
2
3use crate::gpx::geo::distance_between;
4use crate::gpx::types::{Route, Track, TrackSegment, Waypoint};
5
6use super::{AnalysisOptions, ProfilePoint, SpeedProfilePoint};
7
8#[derive(Debug, Clone, PartialEq)]
10pub struct WaypointPath {
11 points: Vec<Waypoint>,
12 options: AnalysisOptions,
13}
14
15impl WaypointPath {
16 pub fn new(points: Vec<Waypoint>) -> Self {
18 Self {
19 points,
20 options: AnalysisOptions::default(),
21 }
22 }
23
24 pub fn from_slice(points: &[Waypoint]) -> Self {
26 Self::new(points.to_vec())
27 }
28
29 pub fn with_options(mut self, options: AnalysisOptions) -> Self {
31 self.options = options;
32 self
33 }
34
35 pub fn from_track(track: &Track) -> Vec<WaypointPath> {
37 track.segments.iter().map(WaypointPath::from).collect()
38 }
39
40 pub fn points(&self) -> &[Waypoint] {
42 &self.points
43 }
44
45 pub fn len(&self) -> usize {
47 self.points.len()
48 }
49
50 pub fn is_empty(&self) -> bool {
52 self.points.is_empty()
53 }
54
55 pub fn total_distance(&self) -> f64 {
57 self.leg_distances().iter().sum()
58 }
59
60 pub fn duration(&self) -> Option<Duration> {
62 let first = self.points.first()?.time?;
63 let last = self.points.last()?.time?;
64 duration_between(first, last)
65 }
66
67 pub fn moving_duration(&self) -> Option<Duration> {
69 let speeds = self.leg_speeds();
70 let durations = self.leg_durations();
71
72 let mut total_ms = 0u64;
73 let mut has_timed_leg = false;
74
75 for (speed, duration) in speeds.iter().zip(durations.iter()) {
76 let Some(duration) = duration else {
77 continue;
78 };
79 has_timed_leg = true;
80 if speed.is_some_and(|s| s > self.options.moving_speed_threshold_mps) {
81 total_ms += duration.as_millis() as u64;
82 }
83 }
84
85 if has_timed_leg {
86 Some(Duration::from_millis(total_ms))
87 } else {
88 None
89 }
90 }
91
92 pub fn average_speed(&self) -> Option<f64> {
94 let secs = self.duration()?.as_secs_f64();
95 if secs > 0.0 {
96 Some(self.total_distance() / secs)
97 } else {
98 None
99 }
100 }
101
102 pub fn average_moving_speed(&self) -> Option<f64> {
104 let secs = self.moving_duration()?.as_secs_f64();
105 if secs > 0.0 {
106 Some(self.total_distance() / secs)
107 } else {
108 None
109 }
110 }
111
112 pub fn speeds(&self) -> Vec<Option<f64>> {
114 self.leg_speeds()
115 }
116
117 pub fn max_speed(&self) -> Option<f64> {
119 self.speeds().iter().filter_map(|s| *s).reduce(f64::max)
120 }
121
122 pub fn min_speed(&self) -> Option<f64> {
124 self.speeds().iter().filter_map(|s| *s).reduce(f64::min)
125 }
126
127 pub fn speed_profile(&self) -> Vec<SpeedProfilePoint> {
129 self.points
130 .windows(2)
131 .enumerate()
132 .filter_map(|(i, window)| {
133 let time = window[0].time?;
134 let speed = self.leg_speeds().get(i).copied()??;
135 Some(SpeedProfilePoint {
136 time,
137 speed_mps: speed,
138 })
139 })
140 .collect()
141 }
142
143 pub fn elevations(&self) -> Vec<Option<f64>> {
145 self.points.iter().map(|p| p.ele).collect()
146 }
147
148 pub fn average_elevation(&self) -> Option<f64> {
150 let elevations: Vec<f64> = self.points.iter().filter_map(|p| p.ele).collect();
151 if elevations.is_empty() {
152 None
153 } else {
154 Some(elevations.iter().sum::<f64>() / elevations.len() as f64)
155 }
156 }
157
158 pub fn max_elevation(&self) -> Option<f64> {
160 self.points.iter().filter_map(|p| p.ele).reduce(f64::max)
161 }
162
163 pub fn min_elevation(&self) -> Option<f64> {
165 self.points.iter().filter_map(|p| p.ele).reduce(f64::min)
166 }
167
168 pub fn elevation_difference(&self) -> Option<f64> {
170 Some(self.max_elevation()? - self.min_elevation()?)
171 }
172
173 pub fn diff_elevation(&self) -> Option<f64> {
175 self.elevation_difference()
176 }
177
178 pub fn total_ascent(&self) -> Option<f64> {
180 self.elevation_gains().map(|gains| {
181 let threshold = self.options.elevation_noise_threshold_m;
182 gains
183 .iter()
184 .filter(|&&gain| gain > threshold)
185 .sum()
186 })
187 }
188
189 pub fn total_descent(&self) -> Option<f64> {
191 self.elevation_gains().map(|gains| {
192 let threshold = self.options.elevation_noise_threshold_m;
193 gains
194 .iter()
195 .filter(|&&gain| gain < -threshold)
196 .map(|gain| -gain)
197 .sum()
198 })
199 }
200
201 pub fn elevation_profile(&self) -> Vec<ProfilePoint> {
205 let points_with_ele: Vec<&Waypoint> =
206 self.points.iter().filter(|point| point.ele.is_some()).collect();
207 if points_with_ele.is_empty() {
208 return Vec::new();
209 }
210
211 let mut distance_m = 0.0;
212 let mut profile = vec![ProfilePoint {
213 distance_m,
214 value: points_with_ele[0].ele.unwrap(),
215 }];
216
217 for i in 1..points_with_ele.len() {
218 distance_m += distance_between(points_with_ele[i - 1], points_with_ele[i]);
219 profile.push(ProfilePoint {
220 distance_m,
221 value: points_with_ele[i].ele.unwrap(),
222 });
223 }
224
225 profile
226 }
227
228 fn leg_distances(&self) -> Vec<f64> {
229 self.points
230 .windows(2)
231 .map(|window| distance_between(&window[0], &window[1]))
232 .collect()
233 }
234
235 fn leg_durations(&self) -> Vec<Option<Duration>> {
236 self.points
237 .windows(2)
238 .map(|window| match (window[0].time, window[1].time) {
239 (Some(start), Some(end)) => duration_between(start, end),
240 _ => None,
241 })
242 .collect()
243 }
244
245 fn leg_speeds(&self) -> Vec<Option<f64>> {
246 self.leg_distances()
247 .iter()
248 .zip(self.leg_durations())
249 .map(|(distance, duration)| {
250 duration.and_then(|duration| {
251 let secs = duration.as_secs_f64();
252 if secs > 0.0 {
253 Some(distance / secs)
254 } else {
255 None
256 }
257 })
258 })
259 .collect()
260 }
261
262 fn elevation_gains(&self) -> Option<Vec<f64>> {
263 let points_with_ele: Vec<&Waypoint> =
264 self.points.iter().filter(|point| point.ele.is_some()).collect();
265 if points_with_ele.len() < 2 {
266 return None;
267 }
268 Some(
269 points_with_ele
270 .windows(2)
271 .map(|window| window[1].ele.unwrap() - window[0].ele.unwrap())
272 .collect(),
273 )
274 }
275}
276
277impl From<&TrackSegment> for WaypointPath {
278 fn from(segment: &TrackSegment) -> Self {
279 Self::from_slice(&segment.points)
280 }
281}
282
283impl From<&Route> for WaypointPath {
284 fn from(route: &Route) -> Self {
285 Self::from_slice(&route.points)
286 }
287}
288
289impl<'a> IntoIterator for &'a WaypointPath {
290 type Item = &'a Waypoint;
291 type IntoIter = std::slice::Iter<'a, Waypoint>;
292
293 fn into_iter(self) -> Self::IntoIter {
294 self.points.iter()
295 }
296}
297
298fn duration_between(
299 start: chrono::DateTime<chrono::Utc>,
300 end: chrono::DateTime<chrono::Utc>,
301) -> Option<Duration> {
302 let millis = (end - start).num_milliseconds();
303 if millis >= 0 {
304 Some(Duration::from_millis(millis as u64))
305 } else {
306 None
307 }
308}