mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-19 09:22:12 +00:00
sparser data transfers
This commit is contained in:
1 parent
48325e6016
commit
4583874e7e
23 files changed
+509
-229
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user