This commit is contained in:
vcoppe
2026-10-04 11:52:46 +02:00
parent 9015da4876
commit b7ca462f58
15 changed files with 859 additions and 217 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ pub struct LngLat {
pub lat: f64,
}
#[derive(Debug)]
#[derive(Debug, Clone, Copy)]
pub struct LngLatBounds {
pub sw: LngLat,
pub ne: LngLat,
@@ -41,6 +41,10 @@ impl Statistics {
self.accumulate_distance_and_time(prev, cur);
self.update_time_bounds(cur.time);
self.update_bounds(cur.coordinates);
self.global.hr.add(cur.hr.map(f64::from));
self.global.cad.add(cur.cad.map(f64::from));
self.global.atemp.add(cur.atemp.map(f64::from));
self.global.power.add(cur.power.map(f64::from));
self.local
.push(TrackpointStatistics::from_partial_stats(self));
}
@@ -207,17 +211,47 @@ impl Statistics {
}
}
/// Average of an optional measure (heart rate, cadence...) over the trackpoints that have it.
/// Keeps the sum so that averages can be merged.
#[derive(Default, Debug, Clone, Copy, PartialEq)]
pub struct Average {
pub sum: f64,
pub count: u32,
}
impl Average {
pub fn add(&mut self, value: Option<f64>) {
if let Some(value) = value {
self.sum += value;
self.count += 1;
}
}
pub fn merge(&mut self, other: &Average) {
self.sum += other.sum;
self.count += other.count;
}
pub fn avg(&self) -> Option<f64> {
(self.count > 0).then(|| self.sum / self.count as f64)
}
}
#[derive(Default, Debug)]
pub struct GlobalStatistics {
pub total_distance: f64,
pub moving_distance: Option<f64>,
pub total_time: Option<i32>,
pub moving_time: Option<i32>,
pub total_time: Option<i64>,
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,
pub hr: Average,
pub cad: Average,
pub atemp: Average,
pub power: Average,
}
impl GlobalStatistics {
@@ -241,6 +275,10 @@ impl GlobalStatistics {
self.start_time = min_options(self.start_time, other.start_time);
self.end_time = max_options(self.end_time, other.end_time);
self.bounds.merge(&other.bounds);
self.hr.merge(&other.hr);
self.cad.merge(&other.cad);
self.atemp.merge(&other.atemp);
self.power.merge(&other.power);
}
}
@@ -248,8 +286,8 @@ impl GlobalStatistics {
pub struct TrackpointStatistics {
pub total_distance: f64,
pub moving_distance: Option<f64>,
pub total_time: Option<i32>,
pub moving_time: Option<i32>,
pub total_time: Option<i64>,
pub moving_time: Option<i64>,
pub speed: Option<f64>,
pub elevation_gain: f64,
pub elevation_loss: f64,
@@ -417,4 +455,50 @@ mod tests {
assert_eq!(cumul.total_time, Some(7_000));
assert_eq!((a.bounds.ne.lng, a.bounds.ne.lat), (2.0, 3.0));
}
#[test]
fn test_average() {
let mut average = Average::default();
assert_eq!(average.avg(), None);
average.add(None);
assert_eq!(average.avg(), None);
average.add(Some(100.0));
average.add(Some(110.0));
assert_eq!(average.avg(), Some(105.0));
let mut other = Average::default();
other.add(Some(130.0));
average.merge(&other);
assert_eq!(average.count, 3);
assert_eq!(average.avg(), Some(113.33333333333333));
}
#[test]
fn test_measures_of_the_trackpoints_are_averaged() {
for (path, measure) in [
("data/with_hr.gpx", 0),
("data/with_cad.gpx", 1),
("data/with_temp.gpx", 2),
("data/with_power_1.gpx", 3),
] {
let data = std::fs::read(path).unwrap();
let gpx = parse(&data).unwrap();
let trkseg = &gpx.trk[0].trkseg[0];
let global = Statistics::compute(trkseg).global;
let values: Vec<f64> = trkseg
.iter()
.filter_map(|trkpt| match measure {
0 => trkpt.hr.map(f64::from),
1 => trkpt.cad.map(f64::from),
2 => trkpt.atemp.map(f64::from),
_ => trkpt.power.map(f64::from),
})
.collect();
let average = [global.hr, global.cad, global.atemp, global.power][measure];
assert!(!values.is_empty(), "{path}");
assert_eq!(average.count as usize, values.len(), "{path}");
let expected = values.iter().sum::<f64>() / values.len() as f64;
assert!((average.avg().unwrap() - expected).abs() < 1e-9, "{path}");
}
}
}
+16 -3
View File
@@ -18,12 +18,12 @@ pub fn distance(p1: LngLat, p2: LngLat) -> f64 {
EARTH_RADIUS * c
}
pub fn time_diff(a: Option<i64>, b: Option<i64>) -> Option<i32> {
Some((a? - b?) as i32)
pub fn time_diff(a: Option<i64>, b: Option<i64>) -> Option<i64> {
Some(a? - b?)
}
/// Computes the speed for a given distance in kilometers and a time in milliseconds
pub fn speed(distance: f64, time: i32) -> f64 {
pub fn speed(distance: f64, time: i64) -> f64 {
distance / (time as f64 / 3_600_000.0)
}
@@ -115,6 +115,19 @@ mod tests {
LngLat { lng, lat }
}
#[test]
fn test_time_diff_and_speed_with_long_durations() {
// 40 days in milliseconds, more than an i32 can hold
let forty_days = 40 * 24 * 3_600_000_i64;
assert!(forty_days > i64::from(i32::MAX));
assert_eq!(time_diff(Some(forty_days), Some(0)), Some(forty_days));
assert_eq!(time_diff(Some(0), Some(forty_days)), Some(-forty_days));
assert_eq!(time_diff(None, Some(1)), None);
assert_eq!(time_diff(Some(1), None), None);
// 1 km in one hour
assert!((speed(1.0, 3_600_000) - 1.0).abs() < 1e-12);
}
#[test]
fn test_distance() {
assert_eq!(distance(p(4.0, 50.0), p(4.0, 50.0)), 0.0);
@@ -1,21 +1,52 @@
use crate::{GlobalStatistics, Statistics, sum_options};
use crate::{GlobalStatistics, Statistics, TrackSegment, sum_options};
/// Value of the timestamps of the trackpoints that have none.
pub const NO_TIME: i64 = i64::MIN;
/// Flattened statistics of the selected segments, one entry per trackpoint, in selection order.
///
/// The distance, time, moving and elevation values are cumulative over the whole selection (the
/// second segment continues where the first one ended), so the statistics of any range of
/// trackpoints are the difference between its two ends: see [`StatisticsBuffer::slice`].
/// Times are in milliseconds, distances in kilometers, speeds in km/h. Missing times are
/// [`NO_TIME`], missing measures are NaN.
#[derive(Debug, Default)]
pub struct StatisticsBuffer {
/// Statistics of the whole selection.
pub global: GlobalStatistics,
pub total_distance: Vec<f64>,
pub moving_distance: Vec<f64>,
pub total_time: Vec<i32>,
pub moving_time: Vec<i32>,
pub total_time: Vec<i64>,
pub moving_time: Vec<i64>,
pub speed: Vec<f64>,
pub elevation_gain: Vec<f64>,
pub elevation_loss: Vec<f64>,
pub slope: Vec<f64>,
pub slope_segment_slope: Vec<f64>,
pub slope_segment_distance: Vec<f64>,
pub lng: Vec<f64>,
pub lat: Vec<f64>,
pub ele: Vec<f64>,
/// Timestamps, in milliseconds since the epoch.
pub time: Vec<i64>,
pub hr: Vec<f64>,
pub cad: Vec<f64>,
pub atemp: Vec<f64>,
pub power: Vec<f64>,
}
impl StatisticsBuffer {
pub fn update(&mut self, stats: &[&Statistics]) {
pub fn len(&self) -> usize {
self.total_distance.len()
}
pub fn is_empty(&self) -> bool {
self.total_distance.is_empty()
}
/// `selected`: the segments and their statistics, in order.
pub fn update(&mut self, selected: &[(&TrackSegment, &Statistics)]) {
self.global = GlobalStatistics::default();
self.total_distance.clear();
self.moving_distance.clear();
self.total_time.clear();
@@ -26,10 +57,20 @@ impl StatisticsBuffer {
self.slope.clear();
self.slope_segment_slope.clear();
self.slope_segment_distance.clear();
self.lng.clear();
self.lat.clear();
self.ele.clear();
self.time.clear();
self.hr.clear();
self.cad.clear();
self.atemp.clear();
self.power.clear();
let mut cumul_stats = GlobalStatistics::default();
for stats in stats {
for trkpt_stats in stats.local.iter() {
let optional = |value: Option<f64>| value.unwrap_or(f64::NAN);
for (segment, stats) in selected {
for (trkpt, trkpt_stats) in segment.iter().zip(stats.local.iter()) {
let cumul_stats = &self.global;
self.total_distance
.push(cumul_stats.total_distance + trkpt_stats.total_distance);
self.moving_distance.push(
@@ -53,10 +94,55 @@ impl StatisticsBuffer {
.push(trkpt_stats.slope_segment.slope);
self.slope_segment_distance
.push(trkpt_stats.slope_segment.distance);
self.lng.push(trkpt.coordinates.lng);
self.lat.push(trkpt.coordinates.lat);
self.ele.push(trkpt.ele);
self.time.push(trkpt.time.unwrap_or(NO_TIME));
self.hr.push(optional(trkpt.hr.map(f64::from)));
self.cad.push(optional(trkpt.cad.map(f64::from)));
self.atemp.push(optional(trkpt.atemp.map(f64::from)));
self.power.push(optional(trkpt.power.map(f64::from)));
}
cumul_stats.merge(&stats.global);
self.global.merge(&stats.global);
}
}
/// Statistics of the trackpoints from `start` to `end` (both included) of the selection, for
/// example the part of an elevation profile the user dragged over. `None` if the range is
/// not inside the selection.
///
/// It is done in constant time, from the cumulative values at both ends: the bounds and
/// the averages are the ones of the whole selection, they are not computed for the range.
pub fn slice(&self, start: usize, end: usize) -> Option<GlobalStatistics> {
if start > end || end >= self.len() {
return None;
}
let delta = |values: &[f64]| values[end] - values[start];
let delta_time = |values: &[i64]| values[end] - values[start];
let time = |i: usize| (self.time[i] != NO_TIME).then_some(self.time[i]);
Some(GlobalStatistics {
total_distance: delta(&self.total_distance),
moving_distance: self
.global
.moving_distance
.map(|_| delta(&self.moving_distance)),
total_time: self.global.total_time.map(|_| delta_time(&self.total_time)),
moving_time: self
.global
.moving_time
.map(|_| delta_time(&self.moving_time)),
elevation_gain: delta(&self.elevation_gain),
elevation_loss: delta(&self.elevation_loss),
start_time: time(start),
end_time: time(end),
bounds: self.global.bounds,
hr: self.global.hr,
cad: self.global.cad,
atemp: self.global.atemp,
power: self.global.power,
})
}
}
#[cfg(test)]
@@ -65,28 +151,16 @@ mod tests {
use super::*;
fn stats(path: &str) -> Statistics {
fn computed(path: &str) -> (TrackSegment, Statistics) {
let data = std::fs::read(path).unwrap();
let file = parse(&data).unwrap();
Statistics::compute(&file.trk[0].trkseg[0])
let segment = file.trk[0].trkseg[0].clone();
let stats = Statistics::compute(&segment);
(segment, stats)
}
#[test]
fn test_empty() {
let mut buffer = StatisticsBuffer::default();
buffer.update(&[]);
assert!(buffer.total_distance.is_empty());
assert!(buffer.speed.is_empty());
}
#[test]
fn test_single_segment_matches_local_stats() {
let s = stats("data/simple.gpx");
let mut buffer = StatisticsBuffer::default();
buffer.update(&[&s]);
let n = s.local.len();
for len in [
fn all_lengths(buffer: &StatisticsBuffer) -> Vec<usize> {
vec![
buffer.total_distance.len(),
buffer.moving_distance.len(),
buffer.total_time.len(),
@@ -97,21 +171,76 @@ mod tests {
buffer.slope.len(),
buffer.slope_segment_slope.len(),
buffer.slope_segment_distance.len(),
] {
assert_eq!(len, n);
}
buffer.lng.len(),
buffer.lat.len(),
buffer.ele.len(),
buffer.time.len(),
buffer.hr.len(),
buffer.cad.len(),
buffer.atemp.len(),
buffer.power.len(),
]
}
#[test]
fn test_empty() {
let mut buffer = StatisticsBuffer::default();
buffer.update(&[]);
assert!(buffer.is_empty());
assert!(buffer.speed.is_empty());
assert_eq!(buffer.global.total_distance, 0.0);
assert!(buffer.slice(0, 0).is_none());
}
#[test]
fn test_single_segment_matches_local_stats() {
let (segment, s) = computed("data/simple.gpx");
let mut buffer = StatisticsBuffer::default();
buffer.update(&[(&segment, &s)]);
let n = s.local.len();
assert!(all_lengths(&buffer).iter().all(|len| *len == n));
assert_eq!(buffer.len(), n);
for (i, local) in s.local.iter().enumerate() {
assert_eq!(buffer.total_distance[i], local.total_distance);
assert_eq!(buffer.slope[i], local.slope);
}
// the trackpoints
for (i, trkpt) in segment.iter().enumerate() {
assert_eq!(buffer.lng[i], trkpt.coordinates.lng);
assert_eq!(buffer.lat[i], trkpt.coordinates.lat);
assert_eq!(buffer.ele[i], trkpt.ele);
}
}
#[test]
fn test_global_is_the_merge_of_the_segments() {
let (segment, s) = computed("data/simple.gpx");
let mut buffer = StatisticsBuffer::default();
buffer.update(&[(&segment, &s), (&segment, &s)]);
let global = &buffer.global;
assert!((global.total_distance - 2.0 * s.global.total_distance).abs() < 1e-9);
assert!((global.elevation_gain - 2.0 * s.global.elevation_gain).abs() < 1e-9);
assert!((global.elevation_loss - 2.0 * s.global.elevation_loss).abs() < 1e-9);
// and it is the last cumulative value of the points
assert_eq!(
global.total_distance,
*buffer.total_distance.last().unwrap()
);
buffer.update(&[(&segment, &s)]);
assert_eq!(buffer.global.total_distance, s.global.total_distance);
buffer.update(&[]);
assert_eq!(buffer.global.total_distance, 0.0);
}
#[test]
fn test_distances_accumulate_over_segments() {
let s = stats("data/simple.gpx");
let (segment, s) = computed("data/simple.gpx");
let n = s.local.len();
let mut buffer = StatisticsBuffer::default();
buffer.update(&[&s, &s]);
buffer.update(&[(&segment, &s), (&segment, &s)]);
assert_eq!(buffer.total_distance.len(), 2 * n);
// the second segment starts where the first one ended
@@ -123,27 +252,156 @@ mod tests {
#[test]
fn test_total_time_accumulates_over_segments() {
let s = stats("data/with_time.gpx");
let (segment, s) = computed("data/with_time.gpx");
let n = s.local.len();
let duration = s.global.total_time.unwrap();
assert!(duration > 0);
let mut buffer = StatisticsBuffer::default();
buffer.update(&[&s, &s]);
buffer.update(&[(&segment, &s), (&segment, &s)]);
assert_eq!(buffer.total_time[n - 1], duration);
assert_eq!(buffer.total_time[n], duration);
assert_eq!(buffer.total_time[2 * n - 1], 2 * duration);
assert!(buffer.total_time.windows(2).all(|w| w[0] <= w[1]));
assert!(buffer.time.iter().all(|t| *t != NO_TIME));
}
#[test]
fn test_missing_times_and_measures_are_nan() {
let (segment, s) = computed("data/simple.gpx");
assert!(segment.iter().all(|trkpt| trkpt.time.is_none()));
let mut buffer = StatisticsBuffer::default();
buffer.update(&[(&segment, &s)]);
assert!(buffer.time.iter().all(|t| *t == NO_TIME));
assert!(buffer.hr.iter().all(|v| v.is_nan()));
assert_eq!(buffer.global.hr.count, 0);
assert_eq!(buffer.global.total_time, None);
}
#[test]
fn test_update_replaces_previous_content() {
let s = stats("data/simple.gpx");
let (segment, s) = computed("data/simple.gpx");
let mut buffer = StatisticsBuffer::default();
buffer.update(&[&s]);
buffer.update(&[&s]);
buffer.update(&[(&segment, &s)]);
buffer.update(&[(&segment, &s)]);
assert_eq!(buffer.total_distance.len(), s.local.len());
assert!(all_lengths(&buffer).iter().all(|len| *len == s.local.len()));
buffer.update(&[]);
assert!(buffer.total_distance.is_empty());
assert!(all_lengths(&buffer).iter().all(|len| *len == 0));
}
#[test]
fn test_slice_of_the_whole_selection_is_the_global_statistics() {
let (segment, s) = computed("data/with_time.gpx");
let mut buffer = StatisticsBuffer::default();
buffer.update(&[(&segment, &s), (&segment, &s)]);
let slice = buffer.slice(0, buffer.len() - 1).unwrap();
let global = &buffer.global;
assert!((slice.total_distance - global.total_distance).abs() < 1e-9);
assert!((slice.elevation_gain - global.elevation_gain).abs() < 1e-9);
assert!((slice.elevation_loss - global.elevation_loss).abs() < 1e-9);
assert_eq!(slice.total_time, global.total_time);
assert_eq!(slice.moving_time, global.moving_time);
assert_eq!(slice.start_time, global.start_time);
assert_eq!(slice.end_time, global.end_time);
}
#[test]
fn test_slice_of_a_range() {
let (segment, s) = computed("data/with_time.gpx");
let n = s.local.len();
let mut buffer = StatisticsBuffer::default();
buffer.update(&[(&segment, &s)]);
let (start, end) = (n / 4, n / 2);
let slice = buffer.slice(start, end).unwrap();
assert!(
(slice.total_distance - (buffer.total_distance[end] - buffer.total_distance[start]))
.abs()
< 1e-12
);
assert_eq!(
slice.total_time,
Some(buffer.total_time[end] - buffer.total_time[start])
);
assert_eq!(
slice.moving_time,
Some(buffer.moving_time[end] - buffer.moving_time[start])
);
assert_eq!(slice.start_time, Some(buffer.time[start]));
assert_eq!(slice.end_time, Some(buffer.time[end]));
assert!(slice.total_distance < buffer.global.total_distance);
assert!(slice.total_speed().is_some());
// a single point has nothing between its ends
let point = buffer.slice(start, start).unwrap();
assert_eq!(point.total_distance, 0.0);
assert_eq!(point.total_time, Some(0));
}
#[test]
fn test_slice_keeps_the_bounds_and_averages_of_the_selection() {
let data = std::fs::read("data/with_hr.gpx").unwrap();
let file = parse(&data).unwrap();
let segment = file.trk[0].trkseg[0].clone();
let s = Statistics::compute(&segment);
let mut buffer = StatisticsBuffer::default();
buffer.update(&[(&segment, &s)]);
assert!(buffer.global.hr.count > 0);
let slice = buffer.slice(1, 3).unwrap();
let (global, bounds) = (&buffer.global, &slice.bounds);
assert_eq!(slice.hr, global.hr);
assert_eq!(slice.cad, global.cad);
assert_eq!(slice.atemp, global.atemp);
assert_eq!(slice.power, global.power);
assert_eq!(
(bounds.sw.lng, bounds.sw.lat),
(global.bounds.sw.lng, global.bounds.sw.lat)
);
assert_eq!(
(bounds.ne.lng, bounds.ne.lat),
(global.bounds.ne.lng, global.bounds.ne.lat)
);
}
#[test]
fn test_durations_are_not_limited_to_24_days() {
// 30 days of recording, in milliseconds
let (segment, mut s) = computed("data/with_time.gpx");
let month = 30 * 24 * 3_600_000_i64;
s.global.total_time = Some(month);
let mut buffer = StatisticsBuffer::default();
buffer.update(&[(&segment, &s), (&segment, &s)]);
assert_eq!(buffer.global.total_time, Some(2 * month));
assert!(2 * month > i64::from(i32::MAX));
}
#[test]
fn test_slice_across_segments() {
let (segment, s) = computed("data/simple.gpx");
let n = s.local.len();
let mut buffer = StatisticsBuffer::default();
buffer.update(&[(&segment, &s), (&segment, &s)]);
// from the middle of the first segment to the middle of the second one
let slice = buffer.slice(n / 2, n + n / 2).unwrap();
let expected = buffer.total_distance[n + n / 2] - buffer.total_distance[n / 2];
assert!((slice.total_distance - expected).abs() < 1e-9);
assert_eq!(slice.total_time, None);
}
#[test]
fn test_slice_out_of_range() {
let (segment, s) = computed("data/simple.gpx");
let mut buffer = StatisticsBuffer::default();
buffer.update(&[(&segment, &s)]);
let n = buffer.len();
assert!(buffer.slice(0, n).is_none());
assert!(buffer.slice(n, n).is_none());
assert!(buffer.slice(5, 4).is_none());
assert!(buffer.slice(0, n - 1).is_some());
}
}
@@ -1,10 +1,13 @@
use std::collections::{HashMap, HashSet, hash_map::Entry};
use crate::{
File, GlobalStatistics, Selection, StackEntry, Statistics, Track, TrackSegment,
File, FileId, GlobalStatistics, Selection, StackEntry, Statistics, Track, TrackSegment,
TrackSegmentRevisionId,
};
/// A segment and its statistics.
pub type SelectedSegment<'a> = (&'a TrackSegment, &'a Statistics);
#[derive(Debug, Default)]
pub struct StatisticsCache {
map: HashMap<TrackSegmentRevisionId, Statistics>,
@@ -34,19 +37,38 @@ impl StatisticsCache {
}
}
/// Statistics of the segments of the selection.
pub fn get<'a>(
&'a self,
state: Option<&StackEntry>,
state: Option<&'a StackEntry>,
selection: &Selection,
) -> Vec<&'a Statistics> {
let mut stats = vec![];
self.selected(state, selection, &[])
.into_iter()
.map(|(_, stats)| stats)
.collect()
}
/// The segments of the selection with their statistics. The segments of several selected
/// files follow `order` (the files missing from it come last).
pub fn selected<'a>(
&'a self,
state: Option<&'a StackEntry>,
selection: &Selection,
order: &[FileId],
) -> Vec<SelectedSegment<'a>> {
let mut selected = vec![];
if let Some(state) = state {
match selection {
Selection::Empty => (),
Selection::File { file_ids } => {
for id in file_ids.iter() {
let ordered = order
.iter()
.filter(|id| file_ids.contains(id))
.chain(file_ids.iter().filter(|id| !order.contains(id)));
for id in ordered {
if let Some(file) = state.get(id) {
self.add_file_stats(file, &mut stats);
self.add_file_stats(file, &mut selected);
}
}
}
@@ -54,7 +76,7 @@ impl StatisticsCache {
if let Some(file) = state.get(file_id) {
for trk in file.trk.iter() {
if trk_ids.contains(&trk.id) {
self.add_track_stats(trk, &mut stats);
self.add_track_stats(trk, &mut selected);
}
}
}
@@ -69,7 +91,7 @@ impl StatisticsCache {
if *trk_id == trk.id {
for trkseg in trk.trkseg.iter() {
if trkseg_ids.contains(&trkseg.id) {
self.add_segment_stats(trkseg, &mut stats);
self.add_segment_stats(trkseg, &mut selected);
}
}
break;
@@ -79,7 +101,7 @@ impl StatisticsCache {
}
Selection::Waypoints { file_id } => {
if let Some(file) = state.get(file_id) {
self.add_file_stats(file, &mut stats);
self.add_file_stats(file, &mut selected);
}
}
Selection::Waypoint {
@@ -87,40 +109,44 @@ impl StatisticsCache {
wpt_ids: _,
} => {
if let Some(file) = state.get(file_id) {
self.add_file_stats(file, &mut stats);
self.add_file_stats(file, &mut selected);
}
}
}
}
stats
selected
}
/// Global statistics of a file: its segments merged, in order.
pub fn file_global(&self, file: &File) -> GlobalStatistics {
let mut stats = vec![];
self.add_file_stats(file, &mut stats);
let mut selected = vec![];
self.add_file_stats(file, &mut selected);
let mut global = GlobalStatistics::default();
for s in stats {
global.merge(&s.global);
for (_, stats) in selected {
global.merge(&stats.global);
}
global
}
fn add_file_stats<'a>(&'a self, file: &File, stats: &mut Vec<&'a Statistics>) {
fn add_file_stats<'a>(&'a self, file: &'a File, selected: &mut Vec<SelectedSegment<'a>>) {
for trk in file.trk.iter() {
self.add_track_stats(trk, stats);
self.add_track_stats(trk, selected);
}
}
fn add_track_stats<'a>(&'a self, trk: &Track, stats: &mut Vec<&'a Statistics>) {
fn add_track_stats<'a>(&'a self, trk: &'a Track, selected: &mut Vec<SelectedSegment<'a>>) {
for trkseg in trk.trkseg.iter() {
self.add_segment_stats(trkseg, stats);
self.add_segment_stats(trkseg, selected);
}
}
fn add_segment_stats<'a>(&'a self, trkseg: &TrackSegment, stats: &mut Vec<&'a Statistics>) {
if let Some(s) = self.map.get(&trkseg.rev_id) {
stats.push(s);
fn add_segment_stats<'a>(
&'a self,
trkseg: &'a TrackSegment,
selected: &mut Vec<SelectedSegment<'a>>,
) {
if let Some(stats) = self.map.get(&trkseg.rev_id) {
selected.push((trkseg, stats));
}
}
}
@@ -241,4 +267,33 @@ mod tests {
cache.update(None);
assert!(cache.map.is_empty());
}
#[test]
fn test_segments_of_several_files_follow_the_given_order() {
let (mut state, first) = state("data/simple.gpx");
let (other, second) = {
let data = std::fs::read("data/with_time.gpx").unwrap();
let file = Rc::new(parse(&data).unwrap());
(file.clone(), file)
};
state.insert(other.id, other);
let mut cache = StatisticsCache::default();
cache.update(Some(&state));
let selection = Selection::File {
file_ids: HashSet::from([first.id, second.id]),
};
let ids = |order: &[FileId]| -> Vec<_> {
cache
.selected(Some(&state), &selection, order)
.iter()
.map(|(segment, _)| segment.id)
.collect()
};
let (a, b) = (first.trk[0].trkseg[0].id, second.trk[0].trkseg[0].id);
assert_eq!(ids(&[first.id, second.id]), vec![a, b]);
assert_eq!(ids(&[second.id, first.id]), vec![b, a]);
// the files that are not in the order come last
assert_eq!(ids(&[second.id]), vec![b, a]);
}
}
+5 -1
View File
@@ -144,7 +144,11 @@ impl Engine {
self.coordinates_cache.update(current);
self.structure_cache.update(current, self.diff.as_ref());
self.statistics_buffer
.update(&self.statistics_cache.get(current, &self.selection));
.update(
&self
.statistics_cache
.selected(current, &self.selection, &self.order.0),
);
}
}