sparser data transfers

This commit is contained in:
vcoppe committed 2026-10-18 12:35:32 +02:00
1 parent 48325e6016
commit 4583874e7e
23 files changed
+509 -229

No files matched your search

+23
View File
@@ -522,6 +522,29 @@ impl TrackpointDetails {
}
}
/// A trackpoint of the selection: what the statistics buffers do not hold for each trackpoint.
/// Absent fields are unknown.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SelectionPoint {
/// ms since the epoch.
#[serde(skip_serializing_if = "Option::is_none")]
pub time: Option<f64>,
/// Slope (%) and length (km) of the smoothed elevation segment of the trackpoint.
#[serde(skip_serializing_if = "Option::is_none")]
pub slope_segment_slope: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub slope_segment_distance: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub surface: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub highway: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sac_scale: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mtb_scale: Option<String>,
}
// Routing
/// A category of the trackpoints of a route, as in `RouteCategory` of the engine.
+42 -5
View File
@@ -154,8 +154,6 @@ buffers!(
elevation_gain,
elevation_loss,
slope,
slope_segment_slope,
slope_segment_distance,
lng,
lat,
ele
@@ -175,9 +173,48 @@ macro_rules! optional_buffers {
optional_buffers!(moving_distance: f64, speed: f64, hr: f64, cad: f64, atemp: f64, power: f64);
optional_buffers!(total_time: i64, moving_time: i64);
/// Timestamps in milliseconds since the epoch (`NO_TIME` when missing), see `optional_buffers`.
pub fn timestamps<H: History>(engine: &Engine<H>) -> Option<&[i64]> {
engine.statistics().time.as_deref()
// The smoothed elevation segments of the selection, over which the slope is averaged, as
// intervals of trackpoints: `slope_segment_starts` holds the index of the first trackpoint of each
// segment, and `slope_segment_slopes` its slope (%). Their length is in `selection_point`.
pub fn slope_segment_starts<H: History>(engine: &Engine<H>) -> &[u32] {
&engine.statistics().slope_segments.starts
}
pub fn slope_segment_slopes<H: History>(engine: &Engine<H>) -> &[f64] {
&engine.statistics().slope_segments.slopes
}
/// What the buffers do not hold for each trackpoint of the selection, for the one at `index`
/// (numbered as in the buffers), `None` if there is none: for example the one that is hovered on
/// the elevation profile.
pub fn selection_point<H: History>(engine: &Engine<H>, index: usize) -> Option<SelectionPoint> {
let stats = engine.statistics();
if index >= stats.len() {
return None;
}
let categories = engine.categories();
let name = |intervals: &engine::Intervals, categories: &engine::Categories| {
let value = intervals.value_at(index);
let code = value.checked_sub(1)?;
categories.name(code).map(str::to_owned)
};
let segment = stats.slope_segments.at(index);
let finite = |value: f64| value.is_finite().then_some(value);
Some(SelectionPoint {
time: stats
.time
.as_ref()
.map(|time| time[index])
.filter(|time| *time != engine::NO_TIME)
.map(|time| time as f64),
slope_segment_slope: segment.and_then(|i| finite(stats.slope_segments.slopes[i])),
slope_segment_distance: segment.and_then(|i| finite(stats.slope_segments.distances[i])),
surface: name(&stats.surface, &categories.surface),
highway: name(&stats.highway, &categories.highway),
sac_scale: name(&stats.sac_scale, &categories.sac_scale),
mtb_scale: name(&stats.mtb_scale, &categories.mtb_scale),
})
}
// Surface, highway, SAC scale and MTB scale of the trackpoints, as intervals of trackpoints that
+81 -28
View File
@@ -1,4 +1,4 @@
use crate::{GlobalStatistics, Statistics, TrackSegment, sum_options};
use crate::{GlobalStatistics, SlopeSegment, Statistics, TrackSegment, sum_options};
/// Value of the timestamps of the trackpoints that have none.
pub const NO_TIME: i64 = i64::MIN;
@@ -27,8 +27,8 @@ pub struct StatisticsBuffer {
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>,
/// The smoothed elevation segments that the trackpoints belong to, see [`SlopeSegments`].
pub slope_segments: SlopeSegments,
pub lng: Vec<f64>,
pub lat: Vec<f64>,
pub ele: Vec<f64>,
@@ -62,6 +62,18 @@ pub struct Intervals {
}
impl Intervals {
/// The value of the trackpoint at `index` (the one of the last interval past the trackpoints),
/// 0 (unknown) when there are no trackpoints.
pub fn value_at(&self, index: usize) -> u8 {
match self
.starts
.partition_point(|start| *start as usize <= index)
{
0 => 0,
next => self.values[next - 1],
}
}
fn clear(&mut self) {
self.starts.clear();
self.values.clear();
@@ -76,6 +88,50 @@ impl Intervals {
}
}
/// The smoothed elevation segments of the selection, over which the slope is averaged, as the
/// intervals of trackpoints that belong to the same one: segment `i` starts at the trackpoint
/// `starts[i]` and ends where the next one starts, or at the last trackpoint. It has a `slope` (%)
/// and a length (`distance`, km). A segment never spans two segments of the selection. All are
/// empty when there are no trackpoints.
#[derive(Debug, Default, PartialEq)]
pub struct SlopeSegments {
pub starts: Vec<u32>,
pub slopes: Vec<f64>,
pub distances: Vec<f64>,
}
impl SlopeSegments {
/// The index of the segment of the trackpoint at `index` (the last one past the trackpoints),
/// `None` when there are no trackpoints.
pub fn at(&self, index: usize) -> Option<usize> {
let next = self
.starts
.partition_point(|start| *start as usize <= index);
next.checked_sub(1)
}
fn clear(&mut self) {
self.starts.clear();
self.slopes.clear();
self.distances.clear();
}
/// Adds the trackpoint at `index`, the indices being pushed in order: it starts a segment if
/// it is the first of a segment of the selection (`first`), or if its segment is not the one
/// of the previous trackpoint.
fn push(&mut self, index: usize, segment: &SlopeSegment, first: bool) {
// compared as bits, so that a NaN slope (no distance) is the same as the previous one
let same = |values: &[f64], value: f64| {
values.last().map(|last| last.to_bits()) == Some(value.to_bits())
};
if first || !same(&self.slopes, segment.slope) || !same(&self.distances, segment.distance) {
self.starts.push(index as u32);
self.slopes.push(segment.slope);
self.distances.push(segment.distance);
}
}
}
/// 0 for an unknown value, else the code plus one. The categories hold at most 255 values, so it
/// fits.
fn unknown_or_next(code: Option<u8>) -> u8 {
@@ -117,8 +173,7 @@ impl StatisticsBuffer {
self.elevation_gain.clear();
self.elevation_loss.clear();
self.slope.clear();
self.slope_segment_slope.clear();
self.slope_segment_distance.clear();
self.slope_segments.clear();
self.lng.clear();
self.lat.clear();
self.ele.clear();
@@ -133,6 +188,7 @@ impl StatisticsBuffer {
self.mtb_scale.clear();
for (segment, stats) in selected {
let segment_start = self.total_distance.len();
for (trkpt, trkpt_stats) in segment.iter().zip(stats.local.iter()) {
let index = self.total_distance.len();
let cumul_stats = &self.global;
@@ -162,10 +218,8 @@ impl StatisticsBuffer {
self.elevation_loss
.push(cumul_stats.elevation_loss + trkpt_stats.elevation_loss);
self.slope.push(trkpt_stats.slope);
self.slope_segment_slope
.push(trkpt_stats.slope_segment.slope);
self.slope_segment_distance
.push(trkpt_stats.slope_segment.distance);
self.slope_segments
.push(index, &trkpt_stats.slope_segment, index == segment_start);
self.lng.push(trkpt.coordinates.lng);
self.lat.push(trkpt.coordinates.lat);
self.ele.push(trkpt.ele);
@@ -229,25 +283,12 @@ mod tests {
(segment, stats)
}
/// The value of the trackpoint at `index`, 0 before the first interval.
fn value_at(intervals: &Intervals, index: usize) -> u8 {
match intervals
.starts
.partition_point(|start| *start as usize <= index)
{
0 => 0,
next => intervals.values[next - 1],
}
}
fn all_lengths(buffer: &StatisticsBuffer) -> Vec<usize> {
vec![
buffer.total_distance.len(),
buffer.elevation_gain.len(),
buffer.elevation_loss.len(),
buffer.slope.len(),
buffer.slope_segment_slope.len(),
buffer.slope_segment_distance.len(),
buffer.lng.len(),
buffer.lat.len(),
buffer.ele.len(),
@@ -276,7 +317,19 @@ mod tests {
for (i, local) in s.local.iter().enumerate() {
assert_eq!(buffer.total_distance[i], local.total_distance);
assert_eq!(buffer.slope[i], local.slope);
let segment = buffer.slope_segments.at(i).unwrap();
assert_eq!(
buffer.slope_segments.slopes[segment].to_bits(),
local.slope_segment.slope.to_bits()
);
assert_eq!(
buffer.slope_segments.distances[segment],
local.slope_segment.distance
);
}
// a segment per smoothed elevation segment, not per trackpoint
assert!(buffer.slope_segments.starts.len() < n);
assert_eq!(buffer.slope_segments.starts[0], 0);
// the trackpoints
for (i, trkpt) in segment.iter().enumerate() {
assert_eq!(buffer.lng[i], trkpt.coordinates.lng);
@@ -454,7 +507,7 @@ mod tests {
assert!(all_lengths(&buffer).iter().all(|len| *len == 5));
// and they can be looked up
let values: Vec<u8> = (0..6).map(|i| value_at(&buffer.surface, i)).collect();
let values: Vec<u8> = (0..6).map(|i| buffer.surface.value_at(i)).collect();
assert_eq!(values, [1, 1, 0, 2, 2, 2]);
}
@@ -494,10 +547,10 @@ mod tests {
buffer.update(&[(&segment, &s)]);
let at = |i: usize| {
(
value_at(&buffer.surface, i),
value_at(&buffer.highway, i),
value_at(&buffer.sac_scale, i),
value_at(&buffer.mtb_scale, i),
buffer.surface.value_at(i),
buffer.highway.value_at(i),
buffer.sac_scale.value_at(i),
buffer.mtb_scale.value_at(i),
)
};
assert_eq!(at(0), (8, 255, 1, 4));
@@ -522,7 +575,7 @@ mod tests {
assert_eq!(buffer.highway, Intervals::default());
assert_eq!(buffer.sac_scale, Intervals::default());
assert_eq!(buffer.mtb_scale, Intervals::default());
assert_eq!(value_at(&buffer.surface, 0), 0);
assert_eq!(buffer.surface.value_at(0), 0);
}
#[test]