mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-09 23:26:31 +00:00
reduce data exchanges across boundary
This commit is contained in:
1 parent
da8a9e3d84
commit
b843a18441
8 files changed
+572
-233
No files matched your search
@@ -10,6 +10,9 @@ pub const NO_TIME: i64 = i64::MIN;
|
|||||||
/// trackpoints are the difference between its two ends: see [`StatisticsBuffer::slice`].
|
/// 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
|
/// Times are in milliseconds, distances in kilometers, speeds in km/h. Missing times are
|
||||||
/// [`NO_TIME`], missing measures are NaN.
|
/// [`NO_TIME`], missing measures are NaN.
|
||||||
|
///
|
||||||
|
/// The optional values (`time`, `hr`, `cad`, `atemp`, `power`) are `None` when no trackpoint of the
|
||||||
|
/// selection has one, and the OSM attributes are stored as [`Intervals`].
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct StatisticsBuffer {
|
pub struct StatisticsBuffer {
|
||||||
/// Statistics of the whole selection.
|
/// Statistics of the whole selection.
|
||||||
@@ -28,20 +31,47 @@ pub struct StatisticsBuffer {
|
|||||||
pub lat: Vec<f64>,
|
pub lat: Vec<f64>,
|
||||||
pub ele: Vec<f64>,
|
pub ele: Vec<f64>,
|
||||||
/// Timestamps, in milliseconds since the epoch.
|
/// Timestamps, in milliseconds since the epoch.
|
||||||
pub time: Vec<i64>,
|
pub time: Option<Vec<i64>>,
|
||||||
pub hr: Vec<f64>,
|
pub hr: Option<Vec<f64>>,
|
||||||
pub cad: Vec<f64>,
|
pub cad: Option<Vec<f64>>,
|
||||||
pub atemp: Vec<f64>,
|
pub atemp: Option<Vec<f64>>,
|
||||||
pub power: Vec<f64>,
|
pub power: Option<Vec<f64>>,
|
||||||
/// Surface of the trackpoints: 0 when unknown, else 1 + its code in the categories of the
|
/// Surface of the trackpoints, see [`Intervals`].
|
||||||
/// engine (so that it is never 0 for a known surface).
|
pub surface: Intervals,
|
||||||
pub surface: Vec<u8>,
|
|
||||||
/// Highway of the trackpoints, as the surface.
|
/// Highway of the trackpoints, as the surface.
|
||||||
pub highway: Vec<u8>,
|
pub highway: Intervals,
|
||||||
/// SAC hiking scale of the trackpoints, as the surface.
|
/// SAC hiking scale of the trackpoints, as the surface.
|
||||||
pub sac_scale: Vec<u8>,
|
pub sac_scale: Intervals,
|
||||||
/// Mountain biking scale of the trackpoints, as the surface.
|
/// Mountain biking scale of the trackpoints, as the surface.
|
||||||
pub mtb_scale: Vec<u8>,
|
pub mtb_scale: Intervals,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A value per trackpoint that rarely changes from one trackpoint to the next, stored as the
|
||||||
|
/// intervals of trackpoints that share it.
|
||||||
|
///
|
||||||
|
/// The values are 0 when unknown, else 1 + the code in the categories of the engine (so that it
|
||||||
|
/// is never 0 for a known value). `starts[i]` is the index of the first trackpoint of the
|
||||||
|
/// interval `i`, which ends where the next one starts, or at the last trackpoint for the last one.
|
||||||
|
/// Both are empty when there are no trackpoints.
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct Intervals {
|
||||||
|
pub starts: Vec<u32>,
|
||||||
|
pub values: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Intervals {
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.starts.clear();
|
||||||
|
self.values.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the value of the trackpoint at `index`, the indices being pushed in order.
|
||||||
|
fn push(&mut self, index: usize, value: u8) {
|
||||||
|
if self.values.last() != Some(&value) {
|
||||||
|
self.starts.push(index as u32);
|
||||||
|
self.values.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 0 for an unknown value, else the code plus one. The categories hold at most 255 values, so it
|
/// 0 for an unknown value, else the code plus one. The categories hold at most 255 values, so it
|
||||||
@@ -50,6 +80,21 @@ fn unknown_or_next(code: Option<u8>) -> u8 {
|
|||||||
code.and_then(|code| code.checked_add(1)).unwrap_or(0)
|
code.and_then(|code| code.checked_add(1)).unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds the value of the trackpoint at `index` to an optional column, the indices being pushed in
|
||||||
|
/// order. The column is created at the first present value, the previous trackpoints getting
|
||||||
|
/// `missing`.
|
||||||
|
fn push_optional<T: Copy>(column: &mut Option<Vec<T>>, index: usize, value: Option<T>, missing: T) {
|
||||||
|
match (column.as_mut(), value) {
|
||||||
|
(Some(column), value) => column.push(value.unwrap_or(missing)),
|
||||||
|
(None, Some(value)) => {
|
||||||
|
let mut new_column = vec![missing; index];
|
||||||
|
new_column.push(value);
|
||||||
|
*column = Some(new_column);
|
||||||
|
}
|
||||||
|
(None, None) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl StatisticsBuffer {
|
impl StatisticsBuffer {
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.total_distance.len()
|
self.total_distance.len()
|
||||||
@@ -75,20 +120,19 @@ impl StatisticsBuffer {
|
|||||||
self.lng.clear();
|
self.lng.clear();
|
||||||
self.lat.clear();
|
self.lat.clear();
|
||||||
self.ele.clear();
|
self.ele.clear();
|
||||||
self.time.clear();
|
self.time = None;
|
||||||
self.hr.clear();
|
self.hr = None;
|
||||||
self.cad.clear();
|
self.cad = None;
|
||||||
self.atemp.clear();
|
self.atemp = None;
|
||||||
self.power.clear();
|
self.power = None;
|
||||||
self.surface.clear();
|
self.surface.clear();
|
||||||
self.highway.clear();
|
self.highway.clear();
|
||||||
self.sac_scale.clear();
|
self.sac_scale.clear();
|
||||||
self.mtb_scale.clear();
|
self.mtb_scale.clear();
|
||||||
|
|
||||||
let optional = |value: Option<f64>| value.unwrap_or(f64::NAN);
|
|
||||||
|
|
||||||
for (segment, stats) in selected {
|
for (segment, stats) in selected {
|
||||||
for (trkpt, trkpt_stats) in segment.iter().zip(stats.local.iter()) {
|
for (trkpt, trkpt_stats) in segment.iter().zip(stats.local.iter()) {
|
||||||
|
let index = self.total_distance.len();
|
||||||
let cumul_stats = &self.global;
|
let cumul_stats = &self.global;
|
||||||
self.total_distance
|
self.total_distance
|
||||||
.push(cumul_stats.total_distance + trkpt_stats.total_distance);
|
.push(cumul_stats.total_distance + trkpt_stats.total_distance);
|
||||||
@@ -116,15 +160,15 @@ impl StatisticsBuffer {
|
|||||||
self.lng.push(trkpt.coordinates.lng);
|
self.lng.push(trkpt.coordinates.lng);
|
||||||
self.lat.push(trkpt.coordinates.lat);
|
self.lat.push(trkpt.coordinates.lat);
|
||||||
self.ele.push(trkpt.ele);
|
self.ele.push(trkpt.ele);
|
||||||
self.time.push(trkpt.time.unwrap_or(NO_TIME));
|
push_optional(&mut self.time, index, trkpt.time, NO_TIME);
|
||||||
self.hr.push(optional(trkpt.hr.map(f64::from)));
|
push_optional(&mut self.hr, index, trkpt.hr.map(f64::from), f64::NAN);
|
||||||
self.cad.push(optional(trkpt.cad.map(f64::from)));
|
push_optional(&mut self.cad, index, trkpt.cad.map(f64::from), f64::NAN);
|
||||||
self.atemp.push(optional(trkpt.atemp.map(f64::from)));
|
push_optional(&mut self.atemp, index, trkpt.atemp.map(f64::from), f64::NAN);
|
||||||
self.power.push(optional(trkpt.power.map(f64::from)));
|
push_optional(&mut self.power, index, trkpt.power.map(f64::from), f64::NAN);
|
||||||
self.surface.push(unknown_or_next(trkpt.surface));
|
self.surface.push(index, unknown_or_next(trkpt.surface));
|
||||||
self.highway.push(unknown_or_next(trkpt.highway));
|
self.highway.push(index, unknown_or_next(trkpt.highway));
|
||||||
self.sac_scale.push(unknown_or_next(trkpt.sac_scale));
|
self.sac_scale.push(index, unknown_or_next(trkpt.sac_scale));
|
||||||
self.mtb_scale.push(unknown_or_next(trkpt.mtb_scale));
|
self.mtb_scale.push(index, unknown_or_next(trkpt.mtb_scale));
|
||||||
}
|
}
|
||||||
self.global.merge(&stats.global);
|
self.global.merge(&stats.global);
|
||||||
}
|
}
|
||||||
@@ -143,7 +187,10 @@ impl StatisticsBuffer {
|
|||||||
|
|
||||||
let delta = |values: &[f64]| values[end] - values[start];
|
let delta = |values: &[f64]| values[end] - values[start];
|
||||||
let delta_time = |values: &[i64]| 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]);
|
let time = |i: usize| {
|
||||||
|
let time = self.time.as_ref()?[i];
|
||||||
|
(time != NO_TIME).then_some(time)
|
||||||
|
};
|
||||||
Some(GlobalStatistics {
|
Some(GlobalStatistics {
|
||||||
total_distance: delta(&self.total_distance),
|
total_distance: delta(&self.total_distance),
|
||||||
moving_distance: self
|
moving_distance: self
|
||||||
@@ -182,6 +229,17 @@ mod tests {
|
|||||||
(segment, stats)
|
(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> {
|
fn all_lengths(buffer: &StatisticsBuffer) -> Vec<usize> {
|
||||||
vec![
|
vec![
|
||||||
buffer.total_distance.len(),
|
buffer.total_distance.len(),
|
||||||
@@ -197,15 +255,6 @@ mod tests {
|
|||||||
buffer.lng.len(),
|
buffer.lng.len(),
|
||||||
buffer.lat.len(),
|
buffer.lat.len(),
|
||||||
buffer.ele.len(),
|
buffer.ele.len(),
|
||||||
buffer.time.len(),
|
|
||||||
buffer.hr.len(),
|
|
||||||
buffer.cad.len(),
|
|
||||||
buffer.atemp.len(),
|
|
||||||
buffer.power.len(),
|
|
||||||
buffer.surface.len(),
|
|
||||||
buffer.highway.len(),
|
|
||||||
buffer.sac_scale.len(),
|
|
||||||
buffer.mtb_scale.len(),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,33 +339,121 @@ mod tests {
|
|||||||
assert_eq!(buffer.total_time[n], duration);
|
assert_eq!(buffer.total_time[n], duration);
|
||||||
assert_eq!(buffer.total_time[2 * n - 1], 2 * 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.total_time.windows(2).all(|w| w[0] <= w[1]));
|
||||||
assert!(buffer.time.iter().all(|t| *t != NO_TIME));
|
assert!(buffer.time.unwrap().iter().all(|t| *t != NO_TIME));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_missing_times_and_measures_are_nan() {
|
fn test_absent_optional_values_have_no_buffer() {
|
||||||
let (segment, s) = computed("data/simple.gpx");
|
let (segment, s) = computed("data/simple.gpx");
|
||||||
assert!(segment.iter().all(|trkpt| trkpt.time.is_none()));
|
assert!(segment.iter().all(|trkpt| trkpt.time.is_none()));
|
||||||
let mut buffer = StatisticsBuffer::default();
|
let mut buffer = StatisticsBuffer::default();
|
||||||
buffer.update(&[(&segment, &s)]);
|
buffer.update(&[(&segment, &s)]);
|
||||||
assert!(buffer.time.iter().all(|t| *t == NO_TIME));
|
assert!(buffer.time.is_none());
|
||||||
assert!(buffer.hr.iter().all(|v| v.is_nan()));
|
assert!(buffer.hr.is_none());
|
||||||
|
assert!(buffer.cad.is_none());
|
||||||
|
assert!(buffer.atemp.is_none());
|
||||||
|
assert!(buffer.power.is_none());
|
||||||
assert_eq!(buffer.global.hr.count, 0);
|
assert_eq!(buffer.global.hr.count, 0);
|
||||||
assert_eq!(buffer.global.total_time, None);
|
assert_eq!(buffer.global.total_time, None);
|
||||||
|
assert_eq!(buffer.len(), s.local.len());
|
||||||
|
// the slices do not need them
|
||||||
|
let slice = buffer.slice(0, 2).unwrap();
|
||||||
|
assert_eq!((slice.start_time, slice.end_time), (None, None));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_surface_and_highway_codes() {
|
fn test_present_optional_values_have_one_entry_per_trackpoint() {
|
||||||
|
let (segment, s) = computed("data/with_time.gpx");
|
||||||
|
let mut buffer = StatisticsBuffer::default();
|
||||||
|
buffer.update(&[(&segment, &s)]);
|
||||||
|
assert_eq!(buffer.time.as_ref().unwrap().len(), buffer.len());
|
||||||
|
assert!(buffer.hr.is_none());
|
||||||
|
|
||||||
|
let data = std::fs::read("data/with_hr.gpx").unwrap();
|
||||||
|
let file = parse(&data, &mut Default::default()).unwrap();
|
||||||
|
let segment = file.trk[0].trkseg[0].clone();
|
||||||
|
let s = Statistics::compute(&segment);
|
||||||
|
buffer.update(&[(&segment, &s)]);
|
||||||
|
let hr = buffer.hr.as_ref().unwrap();
|
||||||
|
assert_eq!(hr.len(), buffer.len());
|
||||||
|
assert!(hr.iter().any(|v| !v.is_nan()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_a_value_missing_for_some_trackpoints_is_a_sentinel() {
|
||||||
|
let (mut segment, _) = computed("data/with_time.gpx");
|
||||||
|
let n = segment.len();
|
||||||
|
let mut point = segment[1].clone();
|
||||||
|
point.time = None;
|
||||||
|
point.hr = Some(120);
|
||||||
|
segment.splice(1, 2, vec![point]);
|
||||||
|
let s = Statistics::compute(&segment);
|
||||||
|
let mut buffer = StatisticsBuffer::default();
|
||||||
|
buffer.update(&[(&segment, &s)]);
|
||||||
|
let time = buffer.time.as_ref().unwrap();
|
||||||
|
assert_eq!(time.len(), n);
|
||||||
|
assert_eq!(time[1], NO_TIME);
|
||||||
|
assert_ne!(time[0], NO_TIME);
|
||||||
|
// the first trackpoint has no heart rate: it is filled once the first one is found
|
||||||
|
let hr = buffer.hr.as_ref().unwrap();
|
||||||
|
assert_eq!(hr.len(), n);
|
||||||
|
assert_eq!(hr[1], 120.0);
|
||||||
|
assert!(hr[0].is_nan() && hr[2].is_nan());
|
||||||
|
assert!(buffer.cad.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_surface_and_highway_intervals() {
|
||||||
let (segment, s) = computed("data/with_highway.gpx");
|
let (segment, s) = computed("data/with_highway.gpx");
|
||||||
let mut buffer = StatisticsBuffer::default();
|
let mut buffer = StatisticsBuffer::default();
|
||||||
buffer.update(&[(&segment, &s)]);
|
buffer.update(&[(&segment, &s)]);
|
||||||
|
|
||||||
// 0 for the trackpoints that have none, else the code of the engine plus one
|
// 0 for the trackpoints that have none, else the code of the engine plus one
|
||||||
assert_eq!(buffer.surface, [1, 1, 0, 2, 2]);
|
let surface = &buffer.surface;
|
||||||
assert_eq!(buffer.highway, [1, 1, 0, 2, 0]);
|
assert_eq!(surface.starts, [0, 2, 3]);
|
||||||
assert_eq!(buffer.sac_scale, [0, 0, 0, 1, 2]);
|
assert_eq!(surface.values, [1, 0, 2]);
|
||||||
assert_eq!(buffer.mtb_scale, [0, 0, 0, 1, 1]);
|
let highway = &buffer.highway;
|
||||||
|
assert_eq!(
|
||||||
|
(&highway.starts[..], &highway.values[..]),
|
||||||
|
(&[0, 2, 3, 4][..], &[1, 0, 2, 0][..])
|
||||||
|
);
|
||||||
|
let sac_scale = &buffer.sac_scale;
|
||||||
|
assert_eq!(
|
||||||
|
(&sac_scale.starts[..], &sac_scale.values[..]),
|
||||||
|
(&[0, 3, 4][..], &[0, 1, 2][..])
|
||||||
|
);
|
||||||
|
let mtb_scale = &buffer.mtb_scale;
|
||||||
|
assert_eq!(
|
||||||
|
(&mtb_scale.starts[..], &mtb_scale.values[..]),
|
||||||
|
(&[0, 3][..], &[0, 1][..])
|
||||||
|
);
|
||||||
assert!(all_lengths(&buffer).iter().all(|len| *len == 5));
|
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();
|
||||||
|
assert_eq!(values, [1, 1, 0, 2, 2, 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_intervals_merge_equal_neighbours_across_segments() {
|
||||||
|
let (mut segment, _) = computed("data/simple.gpx");
|
||||||
|
let points: Vec<_> = segment
|
||||||
|
.iter()
|
||||||
|
.map(|trkpt| {
|
||||||
|
let mut trkpt = trkpt.clone();
|
||||||
|
trkpt.surface = Some(4);
|
||||||
|
trkpt
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
segment.splice(0, points.len(), points);
|
||||||
|
let s = Statistics::compute(&segment);
|
||||||
|
let mut buffer = StatisticsBuffer::default();
|
||||||
|
buffer.update(&[(&segment, &s), (&segment, &s)]);
|
||||||
|
// one interval for the two segments
|
||||||
|
assert_eq!(buffer.surface.starts, [0]);
|
||||||
|
assert_eq!(buffer.surface.values, [5]);
|
||||||
|
// and a single one of unknown values when nothing is known
|
||||||
|
assert_eq!(buffer.highway.values, [0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -331,26 +468,37 @@ mod tests {
|
|||||||
let s = Statistics::compute(&segment);
|
let s = Statistics::compute(&segment);
|
||||||
let mut buffer = StatisticsBuffer::default();
|
let mut buffer = StatisticsBuffer::default();
|
||||||
buffer.update(&[(&segment, &s)]);
|
buffer.update(&[(&segment, &s)]);
|
||||||
assert_eq!((buffer.surface[0], buffer.highway[0]), (8, 255));
|
let at = |i: usize| {
|
||||||
assert_eq!((buffer.sac_scale[0], buffer.mtb_scale[0]), (1, 4));
|
(
|
||||||
assert_eq!((buffer.surface[1], buffer.highway[1]), (0, 0));
|
value_at(&buffer.surface, i),
|
||||||
assert_eq!((buffer.sac_scale[1], buffer.mtb_scale[1]), (0, 0));
|
value_at(&buffer.highway, i),
|
||||||
|
value_at(&buffer.sac_scale, i),
|
||||||
|
value_at(&buffer.mtb_scale, i),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
assert_eq!(at(0), (8, 255, 1, 4));
|
||||||
|
assert_eq!(at(1), (0, 0, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_no_codes_without_surface_and_highway() {
|
fn test_unknown_intervals_without_surface_and_highway() {
|
||||||
let (segment, s) = computed("data/simple.gpx");
|
let (segment, s) = computed("data/simple.gpx");
|
||||||
let mut buffer = StatisticsBuffer::default();
|
let mut buffer = StatisticsBuffer::default();
|
||||||
buffer.update(&[(&segment, &s)]);
|
buffer.update(&[(&segment, &s)]);
|
||||||
assert_eq!(buffer.surface.len(), s.local.len());
|
// a single interval of unknown values
|
||||||
assert!(buffer.surface.iter().all(|c| *c == 0));
|
assert_eq!(buffer.surface.starts, [0]);
|
||||||
assert!(buffer.highway.iter().all(|c| *c == 0));
|
assert_eq!(buffer.surface.values, [0]);
|
||||||
assert!(buffer.sac_scale.iter().all(|c| *c == 0));
|
assert_eq!(buffer.highway.starts, [0]);
|
||||||
assert!(buffer.mtb_scale.iter().all(|c| *c == 0));
|
assert_eq!(buffer.highway.values, [0]);
|
||||||
|
assert_eq!(buffer.sac_scale.values, [0]);
|
||||||
|
assert_eq!(buffer.mtb_scale.values, [0]);
|
||||||
// and they are reset by the next update
|
// and they are reset by the next update
|
||||||
buffer.update(&[]);
|
buffer.update(&[]);
|
||||||
assert!(buffer.surface.is_empty() && buffer.highway.is_empty());
|
assert_eq!(buffer.surface, Intervals::default());
|
||||||
assert!(buffer.sac_scale.is_empty() && buffer.mtb_scale.is_empty());
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -405,8 +553,8 @@ mod tests {
|
|||||||
slice.moving_time,
|
slice.moving_time,
|
||||||
Some(buffer.moving_time[end] - buffer.moving_time[start])
|
Some(buffer.moving_time[end] - buffer.moving_time[start])
|
||||||
);
|
);
|
||||||
assert_eq!(slice.start_time, Some(buffer.time[start]));
|
assert_eq!(slice.start_time, Some(buffer.time.as_ref().unwrap()[start]));
|
||||||
assert_eq!(slice.end_time, Some(buffer.time[end]));
|
assert_eq!(slice.end_time, Some(buffer.time.as_ref().unwrap()[end]));
|
||||||
assert!(slice.total_distance < buffer.global.total_distance);
|
assert!(slice.total_distance < buffer.global.total_distance);
|
||||||
assert!(slice.total_speed().is_some());
|
assert!(slice.total_speed().is_some());
|
||||||
|
|
||||||
|
|||||||
@@ -269,6 +269,15 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn expand(intervals: &crate::Intervals, len: usize) -> Vec<u8> {
|
||||||
|
let mut values = Vec::new();
|
||||||
|
for (i, value) in intervals.values.iter().enumerate() {
|
||||||
|
let end = intervals.starts.get(i + 1).map_or(len, |end| *end as usize);
|
||||||
|
values.resize(end, *value);
|
||||||
|
}
|
||||||
|
values
|
||||||
|
}
|
||||||
|
|
||||||
fn edit(engine: &mut Engine, command: Command) -> bool {
|
fn edit(engine: &mut Engine, command: Command) -> bool {
|
||||||
engine.execute(Action::Edit(command))
|
engine.execute(Action::Edit(command))
|
||||||
}
|
}
|
||||||
@@ -1250,14 +1259,14 @@ mod tests {
|
|||||||
["residential", "track"]
|
["residential", "track"]
|
||||||
);
|
);
|
||||||
// the trackpoints of the selection refer to them
|
// the trackpoints of the selection refer to them
|
||||||
assert_eq!(engine.statistics().surface, [1, 1, 0, 2, 2]);
|
assert_eq!(expand(&engine.statistics().surface, 5), [1, 1, 0, 2, 2]);
|
||||||
assert_eq!(engine.statistics().highway, [1, 1, 0, 2, 0]);
|
assert_eq!(expand(&engine.statistics().highway, 5), [1, 1, 0, 2, 0]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
engine.categories().sac_scale.names(),
|
engine.categories().sac_scale.names(),
|
||||||
["mountain_hiking", "hiking"]
|
["mountain_hiking", "hiking"]
|
||||||
);
|
);
|
||||||
assert_eq!(engine.statistics().sac_scale, [0, 0, 0, 1, 2]);
|
assert_eq!(expand(&engine.statistics().sac_scale, 5), [0, 0, 0, 1, 2]);
|
||||||
assert_eq!(engine.statistics().mtb_scale, [0, 0, 0, 1, 1]);
|
assert_eq!(expand(&engine.statistics().mtb_scale, 5), [0, 0, 0, 1, 1]);
|
||||||
|
|
||||||
// a second file goes on with the same table
|
// a second file goes on with the same table
|
||||||
load(&mut engine, "data/with_surface.gpx");
|
load(&mut engine, "data/with_surface.gpx");
|
||||||
@@ -1265,17 +1274,10 @@ mod tests {
|
|||||||
engine.categories().surface.names(),
|
engine.categories().surface.names(),
|
||||||
["asphalt", "gravel", "cobblestone"]
|
["asphalt", "gravel", "cobblestone"]
|
||||||
);
|
);
|
||||||
assert_eq!(engine.statistics().surface.len(), 80);
|
assert_eq!(engine.statistics().len(), 80);
|
||||||
assert_eq!(engine.statistics().surface[0], 1);
|
let surface = expand(&engine.statistics().surface, 80);
|
||||||
assert_eq!(
|
assert_eq!(surface[0], 1);
|
||||||
engine
|
assert_eq!(surface.iter().filter(|c| **c == 3).count(), 1);
|
||||||
.statistics()
|
|
||||||
.surface
|
|
||||||
.iter()
|
|
||||||
.filter(|c| **c == 3)
|
|
||||||
.count(),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
// undoing does not forget what was learned: the codes stay valid in every state
|
// undoing does not forget what was learned: the codes stay valid in every state
|
||||||
assert!(engine.execute(Action::Undo));
|
assert!(engine.execute(Action::Undo));
|
||||||
@@ -1287,7 +1289,7 @@ mod tests {
|
|||||||
assert!(engine.execute(Action::Redo));
|
assert!(engine.execute(Action::Redo));
|
||||||
let first = engine.order()[0];
|
let first = engine.order()[0];
|
||||||
select_files(&mut engine, &[first]);
|
select_files(&mut engine, &[first]);
|
||||||
assert_eq!(engine.statistics().surface, [1, 1, 0, 2, 2]);
|
assert_eq!(expand(&engine.statistics().surface, 5), [1, 1, 0, 2, 2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+50
-19
@@ -14,7 +14,7 @@ use std::cell::RefCell;
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use gpx_engine::{self as engine, Action, Command, Engine, FileId, LngLat, LngLatBounds};
|
use gpx_engine::{self as engine, Action, Command, Engine, FileId, LngLat, LngLatBounds};
|
||||||
use js_sys::{Array, BigInt64Array, Float64Array, Object, Reflect, Uint8Array};
|
use js_sys::{Array, BigInt64Array, Float64Array, Object, Reflect, Uint8Array, Uint32Array};
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -146,12 +146,24 @@ stats_getter!(lng, Float64Array);
|
|||||||
stats_getter!(lat, Float64Array);
|
stats_getter!(lat, Float64Array);
|
||||||
stats_getter!(ele, Float64Array);
|
stats_getter!(ele, Float64Array);
|
||||||
|
|
||||||
/// Timestamps in milliseconds since the epoch (`NO_TIME`, the smallest i64, when missing).
|
// The optional values are `undefined` when no trackpoint of the selection has one, else they have
|
||||||
|
// an entry per trackpoint, the missing ones being NaN (`NO_TIME` for the timestamps).
|
||||||
|
macro_rules! optional_stats_getter {
|
||||||
|
($name:ident, $field:ident, $array:ident) => {
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn timestamps() -> BigInt64Array {
|
pub fn $name() -> Option<$array> {
|
||||||
with_stats(|s| unsafe { BigInt64Array::view(&s.time) })
|
with_stats(|s| {
|
||||||
.unwrap_or_else(|| BigInt64Array::new_with_length(0))
|
s.$field
|
||||||
|
.as_ref()
|
||||||
|
.map(|values| unsafe { $array::view(values) })
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamps in milliseconds since the epoch (`NO_TIME`, the smallest i64, when missing).
|
||||||
|
optional_stats_getter!(timestamps, time, BigInt64Array);
|
||||||
|
|
||||||
/// Value of the timestamps of the trackpoints that have none.
|
/// Value of the timestamps of the trackpoints that have none.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@@ -159,19 +171,38 @@ pub fn no_time() -> i64 {
|
|||||||
engine::NO_TIME
|
engine::NO_TIME
|
||||||
}
|
}
|
||||||
|
|
||||||
stats_getter!(hr, Float64Array);
|
optional_stats_getter!(hr, hr, Float64Array);
|
||||||
stats_getter!(cad, Float64Array);
|
optional_stats_getter!(cad, cad, Float64Array);
|
||||||
stats_getter!(atemp, Float64Array);
|
optional_stats_getter!(atemp, atemp, Float64Array);
|
||||||
stats_getter!(power, Float64Array);
|
optional_stats_getter!(power, power, Float64Array);
|
||||||
|
|
||||||
// Surface, highway, SAC scale and MTB scale of the trackpoints: 0 when unknown, else 1 + the index
|
// Surface, highway, SAC scale and MTB scale of the trackpoints, as intervals of trackpoints that
|
||||||
// in `surfaces()`, `highways()`, `sac_scales()` or `mtb_scales()`.
|
// have the same value: `<name>_starts` holds the index of the first trackpoint of each interval,
|
||||||
stats_getter!(surface, Uint8Array);
|
// and `<name>_values` its value (0 when unknown, else 1 + the index in `surfaces()`,
|
||||||
stats_getter!(highway, Uint8Array);
|
// `highways()`, `sac_scales()` or `mtb_scales()`). A value that is unknown for every trackpoint is
|
||||||
stats_getter!(sac_scale, Uint8Array);
|
// a single interval.
|
||||||
stats_getter!(mtb_scale, Uint8Array);
|
macro_rules! intervals_getters {
|
||||||
|
($starts:ident, $values:ident, $field:ident) => {
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn $starts() -> Uint32Array {
|
||||||
|
with_stats(|s| unsafe { Uint32Array::view(&s.$field.starts) })
|
||||||
|
.unwrap_or_else(|| Uint32Array::new_with_length(0))
|
||||||
|
}
|
||||||
|
|
||||||
/// The names of the surfaces, in the order of their codes (see `surface`). The list only grows:
|
#[wasm_bindgen]
|
||||||
|
pub fn $values() -> Uint8Array {
|
||||||
|
with_stats(|s| unsafe { Uint8Array::view(&s.$field.values) })
|
||||||
|
.unwrap_or_else(|| Uint8Array::new_with_length(0))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
intervals_getters!(surface_starts, surface_values, surface);
|
||||||
|
intervals_getters!(highway_starts, highway_values, highway);
|
||||||
|
intervals_getters!(sac_scale_starts, sac_scale_values, sac_scale);
|
||||||
|
intervals_getters!(mtb_scale_starts, mtb_scale_values, mtb_scale);
|
||||||
|
|
||||||
|
/// The names of the surfaces, in the order of their codes (see `surface_values`). The list only grows:
|
||||||
/// a name keeps its index.
|
/// a name keeps its index.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn surfaces() -> StringList {
|
pub fn surfaces() -> StringList {
|
||||||
@@ -180,7 +211,7 @@ pub fn surfaces() -> StringList {
|
|||||||
.unchecked_into()
|
.unchecked_into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The names of the highways, in the order of their codes (see `highway`).
|
/// The names of the highways, in the order of their codes (see `highway_values`).
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn highways() -> StringList {
|
pub fn highways() -> StringList {
|
||||||
with_engine(|e| array(e.categories().highway.names(), |name| name.as_str().into()))
|
with_engine(|e| array(e.categories().highway.names(), |name| name.as_str().into()))
|
||||||
@@ -188,7 +219,7 @@ pub fn highways() -> StringList {
|
|||||||
.unchecked_into()
|
.unchecked_into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The names of the SAC scales, in the order of their codes (see `sac_scale`).
|
/// The names of the SAC scales, in the order of their codes (see `sac_scale_values`).
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn sac_scales() -> StringList {
|
pub fn sac_scales() -> StringList {
|
||||||
with_engine(|e| {
|
with_engine(|e| {
|
||||||
@@ -200,7 +231,7 @@ pub fn sac_scales() -> StringList {
|
|||||||
.unchecked_into()
|
.unchecked_into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The names of the MTB scales, in the order of their codes (see `mtb_scale`).
|
/// The names of the MTB scales, in the order of their codes (see `mtb_scale_values`).
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn mtb_scales() -> StringList {
|
pub fn mtb_scales() -> StringList {
|
||||||
with_engine(|e| {
|
with_engine(|e| {
|
||||||
|
|||||||
@@ -22,10 +22,16 @@ import Chart, {
|
|||||||
} from 'chart.js/auto';
|
} from 'chart.js/auto';
|
||||||
import { get, type Readable, type Writable } from 'svelte/store';
|
import { get, type Readable, type Writable } from 'svelte/store';
|
||||||
import type { Coordinates } from 'gpx';
|
import type { Coordinates } from 'gpx';
|
||||||
import { NO_TIME, type SelectionStatistics } from '$lib/engine';
|
import {
|
||||||
|
engine,
|
||||||
|
NO_TIME,
|
||||||
|
type SelectionStatistics,
|
||||||
|
type StatisticsMetric,
|
||||||
|
type StatisticsRequest,
|
||||||
|
} from '$lib/engine';
|
||||||
import type { SlicedStatistics } from '$lib/logic/selection-statistics';
|
import type { SlicedStatistics } from '$lib/logic/selection-statistics';
|
||||||
import { mode } from 'mode-watcher';
|
import { mode } from 'mode-watcher';
|
||||||
import { categoryAt } from '$lib/trackpoint-categories';
|
import { categoryValues } from '$lib/trackpoint-categories';
|
||||||
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
|
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
|
||||||
|
|
||||||
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
|
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
|
||||||
@@ -62,6 +68,8 @@ export class ElevationProfile {
|
|||||||
private _hoveredPoint: Writable<Coordinates | null>;
|
private _hoveredPoint: Writable<Coordinates | null>;
|
||||||
private _additionalDatasets: Readable<string[]>;
|
private _additionalDatasets: Readable<string[]>;
|
||||||
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
|
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
|
||||||
|
/** The statistics that are only read from the engine while the profile shows them. */
|
||||||
|
private _request: StatisticsRequest;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
statistics: Readable<SelectionStatistics>,
|
statistics: Readable<SelectionStatistics>,
|
||||||
@@ -79,6 +87,8 @@ export class ElevationProfile {
|
|||||||
this._elevationFill = elevationFill;
|
this._elevationFill = elevationFill;
|
||||||
this._canvas = canvas;
|
this._canvas = canvas;
|
||||||
this._overlay = overlay;
|
this._overlay = overlay;
|
||||||
|
this._request = engine.requestStatistics();
|
||||||
|
this.updateRequest();
|
||||||
|
|
||||||
import('chartjs-plugin-zoom').then((module) => {
|
import('chartjs-plugin-zoom').then((module) => {
|
||||||
Chart.register(module.default);
|
Chart.register(module.default);
|
||||||
@@ -100,14 +110,34 @@ export class ElevationProfile {
|
|||||||
this.updateData();
|
this.updateData();
|
||||||
});
|
});
|
||||||
this._additionalDatasets.subscribe(() => {
|
this._additionalDatasets.subscribe(() => {
|
||||||
|
// requesting a missing metric updates the data, and then the visibility
|
||||||
|
this.updateRequest();
|
||||||
this.updateDataVisibility();
|
this.updateDataVisibility();
|
||||||
});
|
});
|
||||||
this._elevationFill.subscribe(() => {
|
this._elevationFill.subscribe(() => {
|
||||||
|
this.updateRequest();
|
||||||
this.updateFill();
|
this.updateFill();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Asks the engine for what the profile currently shows, and nothing more. */
|
||||||
|
updateRequest() {
|
||||||
|
const metrics = new Set<StatisticsMetric>();
|
||||||
|
for (const dataset of get(this._additionalDatasets)) {
|
||||||
|
if (['speed', 'hr', 'cad', 'atemp', 'power'].includes(dataset)) {
|
||||||
|
metrics.add(dataset as StatisticsMetric);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const elevationFill = get(this._elevationFill);
|
||||||
|
if (elevationFill === 'slope') {
|
||||||
|
metrics.add('slopeSegment');
|
||||||
|
} else if (elevationFill) {
|
||||||
|
metrics.add(elevationFill);
|
||||||
|
}
|
||||||
|
this._request.set(metrics);
|
||||||
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
let options: ChartOptions<'line'> = {
|
let options: ChartOptions<'line'> = {
|
||||||
animation: false,
|
animation: false,
|
||||||
@@ -400,47 +430,51 @@ export class ElevationProfile {
|
|||||||
|
|
||||||
const datasets: Array<Array<any>> = [[], [], [], [], [], []];
|
const datasets: Array<Array<any>> = [[], [], [], [], [], []];
|
||||||
const { global } = data;
|
const { global } = data;
|
||||||
|
const surfaces = categoryValues(data.surface, data.length);
|
||||||
|
const highways = categoryValues(data.highway, data.length);
|
||||||
|
const sacScales = categoryValues(data.sacScale, data.length);
|
||||||
|
const mtbScales = categoryValues(data.mtbScale, data.length);
|
||||||
for (let index = 0; index < data.length; index++) {
|
for (let index = 0; index < data.length; index++) {
|
||||||
const x = getConvertedDistance(data.totalDistance[index], units.distance);
|
const x = getConvertedDistance(data.totalDistance[index], units.distance);
|
||||||
const ele = data.ele[index];
|
const ele = data.ele[index];
|
||||||
const timestamp = data.timestamps[index];
|
const timestamp = data.timestamps?.[index] ?? NO_TIME;
|
||||||
datasets[0].push({
|
datasets[0].push({
|
||||||
x,
|
x,
|
||||||
y: ele ? getConvertedElevation(ele, units.distance) : 0,
|
y: ele ? getConvertedElevation(ele, units.distance) : 0,
|
||||||
time: timestamp === NO_TIME ? undefined : new Date(Number(timestamp)),
|
time: timestamp === NO_TIME ? undefined : new Date(Number(timestamp)),
|
||||||
slope: {
|
slope: {
|
||||||
at: data.slope[index],
|
at: data.slope[index],
|
||||||
segment: data.slopeSegmentSlope[index],
|
segment: data.slopeSegmentSlope?.[index] ?? NaN,
|
||||||
length: data.slopeSegmentDistance[index],
|
length: data.slopeSegmentDistance?.[index] ?? NaN,
|
||||||
},
|
},
|
||||||
surface: categoryAt(data.surface, data.surfaces, index),
|
surface: surfaces[index],
|
||||||
highway: categoryAt(data.highway, data.highways, index),
|
highway: highways[index],
|
||||||
sacScale: categoryAt(data.sacScale, data.sacScales, index),
|
sacScale: sacScales[index],
|
||||||
mtbScale: categoryAt(data.mtbScale, data.mtbScales, index),
|
mtbScale: mtbScales[index],
|
||||||
coordinates: { lat: data.lat[index], lon: data.lng[index] },
|
coordinates: { lat: data.lat[index], lon: data.lng[index] },
|
||||||
index,
|
index,
|
||||||
});
|
});
|
||||||
if ((global.totalTime ?? 0) > 0) {
|
if (data.speed && (global.totalTime ?? 0) > 0) {
|
||||||
datasets[1].push({
|
datasets[1].push({
|
||||||
x,
|
x,
|
||||||
y: getConvertedVelocity(data.speed[index], units.velocity, units.distance),
|
y: getConvertedVelocity(data.speed[index], units.velocity, units.distance),
|
||||||
index,
|
index,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (global.hr) {
|
if (data.hr) {
|
||||||
datasets[2].push({ x, y: data.hr[index], index });
|
datasets[2].push({ x, y: data.hr[index], index });
|
||||||
}
|
}
|
||||||
if (global.cad) {
|
if (data.cad) {
|
||||||
datasets[3].push({ x, y: data.cad[index], index });
|
datasets[3].push({ x, y: data.cad[index], index });
|
||||||
}
|
}
|
||||||
if (global.atemp) {
|
if (data.atemp) {
|
||||||
datasets[4].push({
|
datasets[4].push({
|
||||||
x,
|
x,
|
||||||
y: getConvertedTemperature(data.atemp[index], units.temperature),
|
y: getConvertedTemperature(data.atemp[index], units.temperature),
|
||||||
index,
|
index,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (global.power) {
|
if (data.power) {
|
||||||
datasets[5].push({ x, y: data.power[index], index });
|
datasets[5].push({ x, y: data.power[index], index });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -613,6 +647,7 @@ export class ElevationProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
this._request.release();
|
||||||
if (this._chart) {
|
if (this._chart) {
|
||||||
this._chart.destroy();
|
this._chart.destroy();
|
||||||
this._chart = null;
|
this._chart = null;
|
||||||
|
|||||||
+176
-79
@@ -2,6 +2,7 @@ import { browser } from '$app/environment';
|
|||||||
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||||
import { FileColorAllocator, normalizeColor } from '$lib/file-colors';
|
import { FileColorAllocator, normalizeColor } from '$lib/file-colors';
|
||||||
import { setHidden, type Visibility } from '$lib/file-visibility';
|
import { setHidden, type Visibility } from '$lib/file-visibility';
|
||||||
|
import type { CategoryIntervals } from '$lib/trackpoint-categories';
|
||||||
import { selectedElementIds, type FileTreeNode } from '$lib/selection-helpers';
|
import { selectedElementIds, type FileTreeNode } from '$lib/selection-helpers';
|
||||||
import type { Feature, FeatureCollection, LineString, Point } from 'geojson';
|
import type { Feature, FeatureCollection, LineString, Point } from 'geojson';
|
||||||
import type {
|
import type {
|
||||||
@@ -106,56 +107,62 @@ const EMPTY_STATISTICS: GlobalStatistics = { totalDistance: 0, elevationGain: 0,
|
|||||||
*/
|
*/
|
||||||
export const NO_TIME = -(2n ** 63n);
|
export const NO_TIME = -(2n ** 63n);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistics of the selection that are only read from the engine when something asks for them
|
||||||
|
* (see `Engine.requestStatistics`). A metric can bring several arrays.
|
||||||
|
*/
|
||||||
|
export type StatisticsMetric =
|
||||||
|
| 'speed'
|
||||||
|
| 'hr'
|
||||||
|
| 'cad'
|
||||||
|
| 'atemp'
|
||||||
|
| 'power'
|
||||||
|
/** `slopeSegmentSlope` and `slopeSegmentDistance`. */
|
||||||
|
| 'slopeSegment'
|
||||||
|
| 'surface'
|
||||||
|
/** `highway`, `sacScale` and `mtbScale`. */
|
||||||
|
| 'highway';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The statistics of the selection: its global statistics, and the values at each of its
|
* The statistics of the selection: its global statistics, and the values at each of its
|
||||||
* trackpoints (all the selected segments, one after the other). The arrays are copies.
|
* trackpoints (all the selected segments, one after the other). The arrays are copies.
|
||||||
*
|
*
|
||||||
* Distances, times, moving values and elevation gain / loss are cumulative over the whole
|
* Distances are cumulative over the whole selection. Distances are in km, speeds in km/h, times in
|
||||||
* selection. Distances are in km, speeds in km/h, times in ms (timestamps since the epoch, as
|
* ms (timestamps since the epoch, as 64-bit integers), elevations in m and slopes in %.
|
||||||
* 64-bit integers), elevations in m and slopes in %. Missing timestamps are `NO_TIME`, missing
|
*
|
||||||
* measures are NaN.
|
* The values of the trackpoints that the selection does not have at all (for example the heart
|
||||||
|
* rates, when no trackpoint has any) are `undefined`, and so are the ones that were not requested
|
||||||
|
* (see `StatisticsMetric`). Otherwise they have one entry per trackpoint: missing timestamps are
|
||||||
|
* `NO_TIME`, missing measures are NaN.
|
||||||
*/
|
*/
|
||||||
export type SelectionStatistics = {
|
export type SelectionStatistics = {
|
||||||
global: GlobalStatistics;
|
global: GlobalStatistics;
|
||||||
/** Number of trackpoints. */
|
/** Number of trackpoints. */
|
||||||
length: number;
|
length: number;
|
||||||
totalDistance: Float64Array;
|
totalDistance: Float64Array;
|
||||||
movingDistance: Float64Array;
|
|
||||||
/** Duration since the start of the selection, in ms. */
|
|
||||||
totalTime: BigInt64Array;
|
|
||||||
movingTime: BigInt64Array;
|
|
||||||
speed: Float64Array;
|
|
||||||
elevationGain: Float64Array;
|
|
||||||
elevationLoss: Float64Array;
|
|
||||||
/** Slope at the trackpoint. */
|
/** Slope at the trackpoint. */
|
||||||
slope: Float64Array;
|
slope: Float64Array;
|
||||||
/** Slope and length (km) of the smoothed elevation segment the trackpoint belongs to. */
|
|
||||||
slopeSegmentSlope: Float64Array;
|
|
||||||
slopeSegmentDistance: Float64Array;
|
|
||||||
lng: Float64Array;
|
lng: Float64Array;
|
||||||
lat: Float64Array;
|
lat: Float64Array;
|
||||||
ele: Float64Array;
|
ele: Float64Array;
|
||||||
timestamps: BigInt64Array;
|
timestamps?: BigInt64Array;
|
||||||
hr: Float64Array;
|
|
||||||
cad: Float64Array;
|
// the following need to be requested
|
||||||
atemp: Float64Array;
|
speed?: Float64Array;
|
||||||
power: Float64Array;
|
hr?: Float64Array;
|
||||||
/** Surface of the trackpoints: 0 when unknown, else 1 + the index in `surfaces`. */
|
cad?: Float64Array;
|
||||||
surface: Uint8Array;
|
atemp?: Float64Array;
|
||||||
/** Names of the surfaces, by code: a name keeps its index. */
|
power?: Float64Array;
|
||||||
surfaces: string[];
|
/** Slope and length (km) of the smoothed elevation segment the trackpoint belongs to. */
|
||||||
/** Highway of the trackpoints: 0 when unknown, else 1 + the index in `highways`. */
|
slopeSegmentSlope?: Float64Array;
|
||||||
highway: Uint8Array;
|
slopeSegmentDistance?: Float64Array;
|
||||||
/** Names of the highways, by code. */
|
surface?: CategoryIntervals;
|
||||||
highways: string[];
|
highway?: CategoryIntervals;
|
||||||
/** SAC hiking scale of the trackpoints: 0 when unknown, else 1 + the index in `sacScales`. */
|
/** SAC hiking scale. */
|
||||||
sacScale: Uint8Array;
|
sacScale?: CategoryIntervals;
|
||||||
/** Names of the SAC scales, by code. */
|
/** Mountain biking scale. */
|
||||||
sacScales: string[];
|
mtbScale?: CategoryIntervals;
|
||||||
/** Mountain biking scale of the trackpoints: 0 when unknown, else 1 + the index in `mtbScales`. */
|
|
||||||
mtbScale: Uint8Array;
|
|
||||||
/** Names of the mountain biking scales, by code. */
|
|
||||||
mtbScales: string[];
|
|
||||||
/**
|
/**
|
||||||
* Global statistics of the trackpoints from `start` to `end` (both included), for example
|
* Global statistics of the trackpoints from `start` to `end` (both included), for example
|
||||||
* the part of the elevation profile that was dragged over. `undefined` if the range is not
|
* the part of the elevation profile that was dragged over. `undefined` if the range is not
|
||||||
@@ -168,34 +175,21 @@ const EMPTY_SELECTION_STATISTICS: SelectionStatistics = {
|
|||||||
global: EMPTY_STATISTICS,
|
global: EMPTY_STATISTICS,
|
||||||
length: 0,
|
length: 0,
|
||||||
totalDistance: new Float64Array(),
|
totalDistance: new Float64Array(),
|
||||||
movingDistance: new Float64Array(),
|
|
||||||
totalTime: new BigInt64Array(),
|
|
||||||
movingTime: new BigInt64Array(),
|
|
||||||
speed: new Float64Array(),
|
|
||||||
elevationGain: new Float64Array(),
|
|
||||||
elevationLoss: new Float64Array(),
|
|
||||||
slope: new Float64Array(),
|
slope: new Float64Array(),
|
||||||
slopeSegmentSlope: new Float64Array(),
|
|
||||||
slopeSegmentDistance: new Float64Array(),
|
|
||||||
lng: new Float64Array(),
|
lng: new Float64Array(),
|
||||||
lat: new Float64Array(),
|
lat: new Float64Array(),
|
||||||
ele: new Float64Array(),
|
ele: new Float64Array(),
|
||||||
timestamps: new BigInt64Array(),
|
|
||||||
hr: new Float64Array(),
|
|
||||||
cad: new Float64Array(),
|
|
||||||
atemp: new Float64Array(),
|
|
||||||
power: new Float64Array(),
|
|
||||||
surface: new Uint8Array(),
|
|
||||||
surfaces: [],
|
|
||||||
highway: new Uint8Array(),
|
|
||||||
highways: [],
|
|
||||||
sacScale: new Uint8Array(),
|
|
||||||
sacScales: [],
|
|
||||||
mtbScale: new Uint8Array(),
|
|
||||||
mtbScales: [],
|
|
||||||
slice: () => undefined,
|
slice: () => undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** What a consumer of the statistics needs, see `Engine.requestStatistics`. */
|
||||||
|
export type StatisticsRequest = {
|
||||||
|
/** Replaces the metrics needed by this consumer. */
|
||||||
|
set(metrics: Iterable<StatisticsMetric>): void;
|
||||||
|
/** The consumer does not need any metric anymore. */
|
||||||
|
release(): void;
|
||||||
|
};
|
||||||
|
|
||||||
function toPositions(flat: Float64Array): [number, number][] {
|
function toPositions(flat: Float64Array): [number, number][] {
|
||||||
const positions: [number, number][] = new Array(flat.length / 2);
|
const positions: [number, number][] = new Array(flat.length / 2);
|
||||||
for (let i = 0; i < positions.length; i++) {
|
for (let i = 0; i < positions.length; i++) {
|
||||||
@@ -245,6 +239,10 @@ class Engine {
|
|||||||
private _statistics = writable<SelectionStatistics>(EMPTY_SELECTION_STATISTICS);
|
private _statistics = writable<SelectionStatistics>(EMPTY_SELECTION_STATISTICS);
|
||||||
/** Identifies the statistics currently in the engine's buffers. */
|
/** Identifies the statistics currently in the engine's buffers. */
|
||||||
private _statisticsVersion = 0;
|
private _statisticsVersion = 0;
|
||||||
|
/** What each consumer of the statistics asked for. */
|
||||||
|
private _statisticsRequests = new Set<Set<StatisticsMetric>>();
|
||||||
|
/** The metrics that were read for the current statistics. */
|
||||||
|
private _loadedMetrics = new Set<StatisticsMetric>();
|
||||||
|
|
||||||
/** Ids of the files in display order. */
|
/** Ids of the files in display order. */
|
||||||
readonly order: Readable<string[]> = { subscribe: this._order.subscribe };
|
readonly order: Readable<string[]> = { subscribe: this._order.subscribe };
|
||||||
@@ -641,42 +639,141 @@ class Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Copies the statistics buffers of the engine (they are overwritten by the next action). */
|
/**
|
||||||
|
* Copies the statistics buffers of the engine (they are overwritten by the next action), but
|
||||||
|
* only the metrics that were requested.
|
||||||
|
*/
|
||||||
private readStatistics(wasm: Wasm): SelectionStatistics {
|
private readStatistics(wasm: Wasm): SelectionStatistics {
|
||||||
const version = ++this._statisticsVersion;
|
const version = ++this._statisticsVersion;
|
||||||
|
this._loadedMetrics.clear();
|
||||||
const totalDistance = wasm.total_distance().slice();
|
const totalDistance = wasm.total_distance().slice();
|
||||||
return {
|
const statistics: SelectionStatistics = {
|
||||||
global: wasm.selection_statistics() ?? EMPTY_STATISTICS,
|
global: wasm.selection_statistics() ?? EMPTY_STATISTICS,
|
||||||
length: totalDistance.length,
|
length: totalDistance.length,
|
||||||
totalDistance,
|
totalDistance,
|
||||||
movingDistance: wasm.moving_distance().slice(),
|
|
||||||
totalTime: wasm.total_time().slice(),
|
|
||||||
movingTime: wasm.moving_time().slice(),
|
|
||||||
speed: wasm.speed().slice(),
|
|
||||||
elevationGain: wasm.elevation_gain().slice(),
|
|
||||||
elevationLoss: wasm.elevation_loss().slice(),
|
|
||||||
slope: wasm.slope().slice(),
|
slope: wasm.slope().slice(),
|
||||||
slopeSegmentSlope: wasm.slope_segment_slope().slice(),
|
|
||||||
slopeSegmentDistance: wasm.slope_segment_distance().slice(),
|
|
||||||
lng: wasm.lng().slice(),
|
lng: wasm.lng().slice(),
|
||||||
lat: wasm.lat().slice(),
|
lat: wasm.lat().slice(),
|
||||||
ele: wasm.ele().slice(),
|
ele: wasm.ele().slice(),
|
||||||
timestamps: wasm.timestamps().slice(),
|
timestamps: wasm.timestamps()?.slice(),
|
||||||
hr: wasm.hr().slice(),
|
|
||||||
cad: wasm.cad().slice(),
|
|
||||||
atemp: wasm.atemp().slice(),
|
|
||||||
power: wasm.power().slice(),
|
|
||||||
surface: wasm.surface().slice(),
|
|
||||||
surfaces: wasm.surfaces(),
|
|
||||||
highway: wasm.highway().slice(),
|
|
||||||
highways: wasm.highways(),
|
|
||||||
sacScale: wasm.sac_scale().slice(),
|
|
||||||
sacScales: wasm.sac_scales(),
|
|
||||||
mtbScale: wasm.mtb_scale().slice(),
|
|
||||||
mtbScales: wasm.mtb_scales(),
|
|
||||||
slice: (start, end) =>
|
slice: (start, end) =>
|
||||||
version === this._statisticsVersion ? wasm.slice_statistics(start, end) : undefined,
|
version === this._statisticsVersion ? wasm.slice_statistics(start, end) : undefined,
|
||||||
};
|
};
|
||||||
|
this.readMetrics(wasm, statistics, this.requestedMetrics());
|
||||||
|
return statistics;
|
||||||
|
}
|
||||||
|
|
||||||
|
private requestedMetrics(): Set<StatisticsMetric> {
|
||||||
|
const metrics = new Set<StatisticsMetric>();
|
||||||
|
this._statisticsRequests.forEach((request) => request.forEach((m) => metrics.add(m)));
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads the metrics that were not read yet into `statistics`, which is the current one. */
|
||||||
|
private readMetrics(
|
||||||
|
wasm: Wasm,
|
||||||
|
statistics: SelectionStatistics,
|
||||||
|
metrics: Set<StatisticsMetric>
|
||||||
|
) {
|
||||||
|
const categories = (
|
||||||
|
starts: Uint32Array,
|
||||||
|
values: Uint8Array,
|
||||||
|
names: () => string[]
|
||||||
|
): CategoryIntervals => ({
|
||||||
|
starts: starts.slice(),
|
||||||
|
values: values.slice(),
|
||||||
|
names: names(),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const metric of metrics) {
|
||||||
|
if (this._loadedMetrics.has(metric)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this._loadedMetrics.add(metric);
|
||||||
|
switch (metric) {
|
||||||
|
case 'speed':
|
||||||
|
statistics.speed = wasm.speed().slice();
|
||||||
|
break;
|
||||||
|
case 'hr':
|
||||||
|
statistics.hr = wasm.hr()?.slice();
|
||||||
|
break;
|
||||||
|
case 'cad':
|
||||||
|
statistics.cad = wasm.cad()?.slice();
|
||||||
|
break;
|
||||||
|
case 'atemp':
|
||||||
|
statistics.atemp = wasm.atemp()?.slice();
|
||||||
|
break;
|
||||||
|
case 'power':
|
||||||
|
statistics.power = wasm.power()?.slice();
|
||||||
|
break;
|
||||||
|
case 'slopeSegment':
|
||||||
|
statistics.slopeSegmentSlope = wasm.slope_segment_slope().slice();
|
||||||
|
statistics.slopeSegmentDistance = wasm.slope_segment_distance().slice();
|
||||||
|
break;
|
||||||
|
case 'surface':
|
||||||
|
statistics.surface = categories(
|
||||||
|
wasm.surface_starts(),
|
||||||
|
wasm.surface_values(),
|
||||||
|
wasm.surfaces
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'highway':
|
||||||
|
statistics.highway = categories(
|
||||||
|
wasm.highway_starts(),
|
||||||
|
wasm.highway_values(),
|
||||||
|
wasm.highways
|
||||||
|
);
|
||||||
|
statistics.sacScale = categories(
|
||||||
|
wasm.sac_scale_starts(),
|
||||||
|
wasm.sac_scale_values(),
|
||||||
|
wasm.sac_scales
|
||||||
|
);
|
||||||
|
statistics.mtbScale = categories(
|
||||||
|
wasm.mtb_scale_starts(),
|
||||||
|
wasm.mtb_scale_values(),
|
||||||
|
wasm.mtb_scales
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a consumer of the statistics that needs some of the metrics that are not part of
|
||||||
|
* them by default (see `StatisticsMetric`). The metrics asked for by all the consumers are
|
||||||
|
* added to `statistics`: the ones that are missing are read right away, and then after each
|
||||||
|
* change of the selection or of the files, and the store is updated. Call `release` when the
|
||||||
|
* consumer goes away.
|
||||||
|
*/
|
||||||
|
requestStatistics(): StatisticsRequest {
|
||||||
|
const wanted = new Set<StatisticsMetric>();
|
||||||
|
this._statisticsRequests.add(wanted);
|
||||||
|
return {
|
||||||
|
set: (metrics) => {
|
||||||
|
wanted.clear();
|
||||||
|
for (const metric of metrics) {
|
||||||
|
wanted.add(metric);
|
||||||
|
}
|
||||||
|
this.loadMissingMetrics();
|
||||||
|
},
|
||||||
|
release: () => {
|
||||||
|
this._statisticsRequests.delete(wanted);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads the requested metrics that are not in the current statistics yet. */
|
||||||
|
private loadMissingMetrics() {
|
||||||
|
const wasm = this.wasm;
|
||||||
|
const metrics = this.requestedMetrics();
|
||||||
|
if (!wasm || [...metrics].every((metric) => this._loadedMetrics.has(metric))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// the buffers of the engine still are the ones of the current statistics, as they are
|
||||||
|
// read after every action
|
||||||
|
const statistics = { ...get(this._statistics) };
|
||||||
|
this.readMetrics(wasm, statistics, metrics);
|
||||||
|
this._statistics.set(statistics);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reads the state of a file, reusing from `previous` what did not change. */
|
/** Reads the state of a file, reusing from `previous` what did not change. */
|
||||||
|
|||||||
@@ -1,24 +1,45 @@
|
|||||||
import { describe, it } from 'node:test';
|
import { describe, it } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { categoryAt } from './trackpoint-categories';
|
import { categoryValues, type CategoryIntervals } from './trackpoint-categories';
|
||||||
|
|
||||||
describe('categoryAt', () => {
|
describe('categoryValues', () => {
|
||||||
const codes = new Uint8Array([1, 1, 0, 2, 0]);
|
// asphalt, asphalt, unknown, gravel, gravel, gravel, unknown
|
||||||
const names = ['asphalt', 'gravel'];
|
const intervals: CategoryIntervals = {
|
||||||
|
starts: new Uint32Array([0, 2, 3, 6]),
|
||||||
|
values: new Uint8Array([1, 0, 2, 0]),
|
||||||
|
names: ['asphalt', 'gravel'],
|
||||||
|
};
|
||||||
|
|
||||||
it('is the name of the code', () => {
|
it('is the name of the value of the interval of each trackpoint', () => {
|
||||||
assert.equal(categoryAt(codes, names, 0), 'asphalt');
|
assert.deepEqual(categoryValues(intervals, 7), [
|
||||||
assert.equal(categoryAt(codes, names, 3), 'gravel');
|
'asphalt',
|
||||||
|
'asphalt',
|
||||||
|
undefined,
|
||||||
|
'gravel',
|
||||||
|
'gravel',
|
||||||
|
'gravel',
|
||||||
|
undefined,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is undefined when the value is unknown', () => {
|
it('goes on with the last interval until the last trackpoint', () => {
|
||||||
assert.equal(categoryAt(codes, names, 2), undefined);
|
const last = { ...intervals, values: new Uint8Array([1, 0, 2, 2]) };
|
||||||
assert.equal(categoryAt(codes, names, 4), undefined);
|
assert.deepEqual(categoryValues(last, 9).slice(5), [
|
||||||
|
'gravel',
|
||||||
|
'gravel',
|
||||||
|
'gravel',
|
||||||
|
'gravel',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is undefined out of the trackpoints, or for a code without name', () => {
|
it('does not go past the trackpoints', () => {
|
||||||
assert.equal(categoryAt(codes, names, 5), undefined);
|
assert.deepEqual(categoryValues(intervals, 3), ['asphalt', 'asphalt', undefined]);
|
||||||
assert.equal(categoryAt(new Uint8Array([3]), names, 0), undefined);
|
assert.deepEqual(categoryValues(intervals, 0), []);
|
||||||
assert.equal(categoryAt(new Uint8Array(), [], 0), undefined);
|
});
|
||||||
|
|
||||||
|
it('is undefined without intervals, or for a code without name', () => {
|
||||||
|
assert.deepEqual(categoryValues(undefined, 2), [undefined, undefined]);
|
||||||
|
const unnamed = { starts: new Uint32Array([0]), values: new Uint8Array([3]), names: [] };
|
||||||
|
assert.deepEqual(categoryValues(unnamed, 2), [undefined, undefined]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,12 +1,37 @@
|
|||||||
/**
|
/**
|
||||||
* The value of a trackpoint for a category of the engine (surface, highway): `codes` has one entry
|
* A category of the engine (surface, highway...) for each trackpoint, as the intervals of
|
||||||
* per trackpoint, 0 when the value is unknown, else 1 + the index of the value in `names`.
|
* consecutive trackpoints that share the same value: `starts[i]` is the index of the first
|
||||||
|
* trackpoint of the interval `i`, which lasts until the next start (or the last trackpoint). Its
|
||||||
|
* `values[i]` is 0 when the value is unknown, else 1 + the index of the value in `names`.
|
||||||
*/
|
*/
|
||||||
export function categoryAt(
|
export type CategoryIntervals = {
|
||||||
codes: ArrayLike<number>,
|
starts: Uint32Array;
|
||||||
names: readonly string[],
|
values: Uint8Array;
|
||||||
index: number
|
/** Names of the values, by code: a name keeps its index. */
|
||||||
): string | undefined {
|
names: string[];
|
||||||
const code = codes[index];
|
};
|
||||||
return code ? names[code - 1] : undefined;
|
|
||||||
|
/**
|
||||||
|
* The value of each of the `length` trackpoints for a category of the engine, `undefined` when it
|
||||||
|
* is unknown, or when the category is missing (it is unknown for every trackpoint). The intervals
|
||||||
|
* are scanned once, so it takes a time linear in `length`.
|
||||||
|
*/
|
||||||
|
export function categoryValues(
|
||||||
|
intervals: CategoryIntervals | undefined,
|
||||||
|
length: number
|
||||||
|
): (string | undefined)[] {
|
||||||
|
const result: (string | undefined)[] = new Array(length).fill(undefined);
|
||||||
|
if (!intervals) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const { starts, values, names } = intervals;
|
||||||
|
for (let i = 0; i < starts.length; i++) {
|
||||||
|
if (!values[i]) {
|
||||||
|
continue; // unknown, already `undefined`
|
||||||
|
}
|
||||||
|
const name = names[values[i] - 1];
|
||||||
|
const end = i + 1 < starts.length ? Math.min(starts[i + 1], length) : length;
|
||||||
|
result.fill(name, starts[i], end);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
|
import type { SelectionStatistics } from '$lib/engine';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
|
import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
|
||||||
@@ -24,35 +25,14 @@
|
|||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
|
||||||
// TODO the example comes from the previous implementation
|
// TODO the example comes from the previous implementation
|
||||||
let statistics = writable({
|
let statistics = writable<SelectionStatistics>({
|
||||||
global: { totalDistance: 0, elevationGain: 0, elevationLoss: 0 },
|
global: { totalDistance: 0, elevationGain: 0, elevationLoss: 0 },
|
||||||
length: 0,
|
length: 0,
|
||||||
totalDistance: new Float64Array(),
|
totalDistance: new Float64Array(),
|
||||||
movingDistance: new Float64Array(),
|
|
||||||
totalTime: new BigInt64Array(),
|
|
||||||
movingTime: new BigInt64Array(),
|
|
||||||
speed: new Float64Array(),
|
|
||||||
elevationGain: new Float64Array(),
|
|
||||||
elevationLoss: new Float64Array(),
|
|
||||||
slope: new Float64Array(),
|
slope: new Float64Array(),
|
||||||
slopeSegmentSlope: new Float64Array(),
|
|
||||||
slopeSegmentDistance: new Float64Array(),
|
|
||||||
lng: new Float64Array(),
|
lng: new Float64Array(),
|
||||||
lat: new Float64Array(),
|
lat: new Float64Array(),
|
||||||
ele: new Float64Array(),
|
ele: new Float64Array(),
|
||||||
timestamps: new BigInt64Array(),
|
|
||||||
hr: new Float64Array(),
|
|
||||||
cad: new Float64Array(),
|
|
||||||
atemp: new Float64Array(),
|
|
||||||
power: new Float64Array(),
|
|
||||||
surface: new Uint8Array(),
|
|
||||||
surfaces: [],
|
|
||||||
highway: new Uint8Array(),
|
|
||||||
highways: [],
|
|
||||||
sacScale: new Uint8Array(),
|
|
||||||
sacScales: [],
|
|
||||||
mtbScale: new Uint8Array(),
|
|
||||||
mtbScales: [],
|
|
||||||
slice: () => undefined,
|
slice: () => undefined,
|
||||||
});
|
});
|
||||||
let slicedStatistics = writable(undefined);
|
let slicedStatistics = writable(undefined);
|
||||||
|
|||||||
Reference in new issue
Block a user