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]
|
||||
|
||||
@@ -34,7 +34,3 @@ pub fn optional(array: Option<Vec<u8>>) -> Bytes {
|
||||
pub fn f64_bytes(values: &[f64]) -> Vec<u8> {
|
||||
values.iter().flat_map(|v| v.to_le_bytes()).collect()
|
||||
}
|
||||
|
||||
pub fn i64_bytes(values: &[i64]) -> Vec<u8> {
|
||||
values.iter().flat_map(|v| v.to_le_bytes()).collect()
|
||||
}
|
||||
@@ -17,15 +17,7 @@ macro_rules! f64_getters {
|
||||
)*};
|
||||
}
|
||||
|
||||
f64_getters!(
|
||||
total_distance,
|
||||
slope,
|
||||
slope_segment_slope,
|
||||
slope_segment_distance,
|
||||
lng,
|
||||
lat,
|
||||
ele
|
||||
);
|
||||
f64_getters!(total_distance, slope, slope_segment_slopes, lng, lat, ele);
|
||||
|
||||
macro_rules! optional_f64_getters {
|
||||
($($name:ident),*) => {$(
|
||||
@@ -38,10 +30,16 @@ macro_rules! optional_f64_getters {
|
||||
|
||||
optional_f64_getters!(speed, hr, cad, atemp, power);
|
||||
|
||||
/// Timestamps in milliseconds since the epoch (`NO_TIME` when missing), as `i64`.
|
||||
/// The index of the first trackpoint of each smoothed elevation segment of the selection.
|
||||
#[tauri::command(async)]
|
||||
pub fn timestamps(state: State<AppState>) -> Bytes {
|
||||
optional(state.read(|e| api::timestamps(e).map(i64_bytes)))
|
||||
pub fn slope_segment_starts(state: State<AppState>) -> Bytes {
|
||||
u32s(&state.read(|e| api::slope_segment_starts(e).to_vec()))
|
||||
}
|
||||
|
||||
/// What the buffers do not hold for each trackpoint of the selection, for the one at `index`.
|
||||
#[tauri::command(async)]
|
||||
pub fn selection_point(state: State<AppState>, index: usize) -> Option<api::SelectionPoint> {
|
||||
state.read(move |e| api::selection_point(e, index))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
|
||||
+3
-3
@@ -146,8 +146,8 @@ fn register_commands<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::Bu
|
||||
// statistics
|
||||
statistics::total_distance,
|
||||
statistics::slope,
|
||||
statistics::slope_segment_slope,
|
||||
statistics::slope_segment_distance,
|
||||
statistics::slope_segment_starts,
|
||||
statistics::slope_segment_slopes,
|
||||
statistics::lng,
|
||||
statistics::lat,
|
||||
statistics::ele,
|
||||
@@ -156,7 +156,7 @@ fn register_commands<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::Bu
|
||||
statistics::cad,
|
||||
statistics::atemp,
|
||||
statistics::power,
|
||||
statistics::timestamps,
|
||||
statistics::selection_point,
|
||||
statistics::selection_statistics,
|
||||
statistics::slice_statistics,
|
||||
statistics::surface_starts,
|
||||
|
||||
@@ -142,6 +142,18 @@ fn reads_the_statistics_of_the_selection() {
|
||||
// optional arrays start with a byte telling whether they are there
|
||||
let hr = app.bytes("hr", json!({}));
|
||||
assert_eq!(hr[0], 0);
|
||||
// the slope segments are intervals of trackpoints
|
||||
let starts = app.bytes("slope_segment_starts", json!({}));
|
||||
let slopes = f64s(&app.bytes("slope_segment_slopes", json!({})));
|
||||
assert_eq!(starts.len() / 4, slopes.len());
|
||||
assert!(!slopes.is_empty() && slopes.len() < distance.len());
|
||||
let point = app.call("selection_point", json!({ "index": 1 }));
|
||||
assert!(point["slopeSegmentDistance"].is_number());
|
||||
assert!(point.get("time").is_none());
|
||||
assert_eq!(
|
||||
app.call("selection_point", json!({ "index": distance.len() })),
|
||||
Value::Null
|
||||
);
|
||||
assert!(
|
||||
app.call("slice_statistics", json!({ "start": 1, "end": 3 }))["totalDistance"].is_number()
|
||||
);
|
||||
|
||||
@@ -26,8 +26,6 @@ stats_getter!(total_distance, Float64Array);
|
||||
stats_getter!(elevation_gain, Float64Array);
|
||||
stats_getter!(elevation_loss, Float64Array);
|
||||
stats_getter!(slope, Float64Array);
|
||||
stats_getter!(slope_segment_slope, Float64Array);
|
||||
stats_getter!(slope_segment_distance, Float64Array);
|
||||
stats_getter!(lng, Float64Array);
|
||||
stats_getter!(lat, Float64Array);
|
||||
stats_getter!(ele, Float64Array);
|
||||
@@ -49,14 +47,6 @@ optional_stats_getter!(moving_distance, Float64Array);
|
||||
optional_stats_getter!(total_time, BigInt64Array);
|
||||
optional_stats_getter!(moving_time, BigInt64Array);
|
||||
optional_stats_getter!(speed, Float64Array);
|
||||
// Timestamps in milliseconds since the epoch (`NO_TIME`, the smallest i64, when missing).
|
||||
optional_stats_getter!(timestamps, BigInt64Array);
|
||||
|
||||
/// Value of the timestamps of the trackpoints that have none.
|
||||
#[wasm_bindgen]
|
||||
pub fn no_time() -> i64 {
|
||||
gpx_engine::NO_TIME
|
||||
}
|
||||
|
||||
optional_stats_getter!(hr, Float64Array);
|
||||
optional_stats_getter!(cad, Float64Array);
|
||||
@@ -96,6 +86,19 @@ categories_getters!(highway_starts, highway_values, highways);
|
||||
categories_getters!(sac_scale_starts, sac_scale_values, sac_scales);
|
||||
categories_getters!(mtb_scale_starts, mtb_scale_values, mtb_scales);
|
||||
|
||||
// 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`.
|
||||
stats_getter!(slope_segment_starts, Uint32Array);
|
||||
stats_getter!(slope_segment_slopes, Float64Array);
|
||||
|
||||
/// What the buffers do not hold for each trackpoint of the selection, for the one at `index`
|
||||
/// (numbered as in the buffers), `undefined` if there is none.
|
||||
#[wasm_bindgen]
|
||||
pub fn selection_point(index: usize) -> SelectionPoint {
|
||||
optional_js(with_engine(|e| api::selection_point(e, index)).flatten()).unchecked_into()
|
||||
}
|
||||
|
||||
/// For each trackpoint of the selection (numbered as in the statistics), the highest tolerance
|
||||
/// (m) of `reduce` that keeps it: infinite for the ends of the segments.
|
||||
#[wasm_bindgen]
|
||||
|
||||
+15
-4
@@ -4,7 +4,7 @@
|
||||
|
||||
#![allow(clippy::needless_borrows_for_generic_args)]
|
||||
|
||||
use js_sys::{Array, BigInt64Array, Float64Array, Object, Reflect};
|
||||
use js_sys::{Array, Float64Array, Object, Reflect};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
@@ -144,7 +144,10 @@ fn test_loaded_file_has_statistics_and_coordinates() {
|
||||
assert_eq!(crate::queries::statistics::lng().length(), n);
|
||||
assert_eq!(crate::queries::statistics::ele().length(), n);
|
||||
// no timestamps in this file
|
||||
assert!(crate::queries::statistics::timestamps().is_none());
|
||||
let point = crate::queries::statistics::selection_point(0);
|
||||
assert!(get(&point, "slopeSegmentDistance").as_f64().is_some());
|
||||
assert!(get(&point, "time").is_undefined());
|
||||
assert!(crate::queries::statistics::selection_point(n as usize).is_undefined());
|
||||
|
||||
let structure = crate::queries::files::file_structure(&id);
|
||||
let segment = get(
|
||||
@@ -165,8 +168,8 @@ fn test_loaded_file_has_statistics_and_coordinates() {
|
||||
#[wasm_bindgen_test]
|
||||
fn test_timestamps_and_categories() {
|
||||
started_with(&[(WITH_TIME, "time")]);
|
||||
let times: BigInt64Array = crate::queries::statistics::timestamps().unwrap();
|
||||
assert!(times.length() > 0);
|
||||
let point = crate::queries::statistics::selection_point(0);
|
||||
assert!(get(&point, "time").as_f64().unwrap() > 0.0);
|
||||
assert!(crate::queries::statistics::total_time().is_some());
|
||||
|
||||
started_with(&[(WITH_SURFACE, "surface")]);
|
||||
@@ -179,6 +182,14 @@ fn test_timestamps_and_categories() {
|
||||
crate::queries::statistics::surface_starts().length(),
|
||||
values.len() as u32
|
||||
);
|
||||
// the name of the surface of a trackpoint that has one
|
||||
let starts = crate::queries::statistics::surface_starts().to_vec();
|
||||
let known = values.iter().position(|v| *v > 0).unwrap();
|
||||
let point = crate::queries::statistics::selection_point(starts[known] as usize);
|
||||
assert_eq!(
|
||||
get(&point, "surface").as_string(),
|
||||
Some(surfaces[values[known] as usize - 1].clone())
|
||||
);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
|
||||
@@ -64,6 +64,21 @@ export interface TrackpointDetails {
|
||||
/** ms since epoch */
|
||||
time?: number;
|
||||
}
|
||||
/**
|
||||
* A trackpoint of the selection: what the statistics buffers do not hold for each trackpoint.
|
||||
* Absent fields are unknown.
|
||||
*/
|
||||
export interface SelectionPoint {
|
||||
/** ms since epoch */
|
||||
time?: number;
|
||||
/** Slope (%) and length (km) of the smoothed elevation segment of the trackpoint. */
|
||||
slopeSegmentSlope?: number;
|
||||
slopeSegmentDistance?: number;
|
||||
surface?: string;
|
||||
highway?: string;
|
||||
sacScale?: string;
|
||||
mtbScale?: string;
|
||||
}
|
||||
/** What was copied or cut, to be pasted: the kind of the elements and their ids. */
|
||||
export interface Clipboard {
|
||||
type: 'files' | 'tracks' | 'segments' | 'waypoints';
|
||||
@@ -161,6 +176,8 @@ extern "C" {
|
||||
pub type WaypointDetails;
|
||||
#[wasm_bindgen(typescript_type = "TrackpointDetails | undefined")]
|
||||
pub type TrackpointDetails;
|
||||
#[wasm_bindgen(typescript_type = "SelectionPoint | undefined")]
|
||||
pub type SelectionPoint;
|
||||
#[wasm_bindgen(typescript_type = "MoveTarget")]
|
||||
pub type MoveTarget;
|
||||
|
||||
|
||||
@@ -24,14 +24,15 @@ import { get, type Readable, type Writable } from 'svelte/store';
|
||||
import type { Coordinates } from '$lib/geo/coordinates';
|
||||
import {
|
||||
engine,
|
||||
NO_TIME,
|
||||
EMPTY_SELECTION_STATISTICS,
|
||||
type SelectionPoint,
|
||||
type SelectionStatistics,
|
||||
type StatisticsMetric,
|
||||
type StatisticsRequest,
|
||||
} from '$lib/engine';
|
||||
import type { SlicedStatistics } from '$lib/state/selection-statistics';
|
||||
import { mode } from 'mode-watcher';
|
||||
import { categoryValues } from '$lib/engine/trackpoint-categories';
|
||||
import { categoryValue, intervalAt } from '$lib/engine/trackpoint-categories';
|
||||
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/data/colors';
|
||||
|
||||
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
|
||||
@@ -44,20 +45,13 @@ function optional(value: number, convert: (value: number) => number = (v) => v)
|
||||
return Number.isNaN(value) ? undefined : convert(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A point of the chart. The rest of the data of the trackpoint is only read when it is hovered
|
||||
* (see `SelectionStatistics.point`), and the categories of the fill from their intervals.
|
||||
*/
|
||||
interface ElevationProfilePoint {
|
||||
x: number;
|
||||
y: number;
|
||||
time?: Date;
|
||||
slope: {
|
||||
at: number;
|
||||
segment: number;
|
||||
length: number;
|
||||
};
|
||||
surface?: string;
|
||||
highway?: string;
|
||||
sacScale?: string;
|
||||
mtbScale?: string;
|
||||
coordinates: Coordinates;
|
||||
index: number;
|
||||
}
|
||||
|
||||
@@ -69,6 +63,14 @@ export class ElevationProfile {
|
||||
private _panning = false;
|
||||
|
||||
private _statistics: Readable<SelectionStatistics>;
|
||||
/** The statistics that the chart shows, which the fills read for each of its segments. */
|
||||
private _data: SelectionStatistics = EMPTY_SELECTION_STATISTICS;
|
||||
/**
|
||||
* The hovered trackpoint, and its data once it is read from the engine (`undefined` if the
|
||||
* statistics changed in the meantime).
|
||||
*/
|
||||
private _hovered: { index: number; loaded: boolean; point?: SelectionPoint } | null = null;
|
||||
private _loadingHovered = false;
|
||||
private _slicedStatistics: Writable<SlicedStatistics | undefined>;
|
||||
private _hoveredPoint: Writable<Coordinates | null>;
|
||||
private _additionalDatasets: Readable<string[]>;
|
||||
@@ -200,7 +202,7 @@ export class ElevationProfile {
|
||||
if (this._dragging) {
|
||||
this._hoveredPoint.set(null);
|
||||
} else {
|
||||
this._hoveredPoint.set(point.coordinates);
|
||||
this.hover(point.index);
|
||||
}
|
||||
return `${i18n._('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
|
||||
} else if (context.datasetIndex === 1) {
|
||||
@@ -219,28 +221,40 @@ export class ElevationProfile {
|
||||
let context = contexts.filter((context) => context.datasetIndex === 0);
|
||||
if (context.length === 0) return;
|
||||
let point = context[0].raw as ElevationProfilePoint;
|
||||
let slope = {
|
||||
at: point.slope.at.toFixed(1),
|
||||
segment: point.slope.segment.toFixed(1),
|
||||
length: getDistanceWithUnits(point.slope.length),
|
||||
};
|
||||
let surface = point.surface ?? 'unknown';
|
||||
let highway = point.highway ?? 'unknown';
|
||||
let sacScale = point.sacScale;
|
||||
let mtbScale = point.mtbScale;
|
||||
// the rest of the data is there once it is read (see `hover`)
|
||||
const hovered = this._hovered;
|
||||
const details =
|
||||
hovered?.index === point.index ? hovered.point : undefined;
|
||||
const elevationFill = get(this._elevationFill);
|
||||
let slope = `${this._data.slope[point.index]?.toFixed(1)} %`;
|
||||
if (
|
||||
elevationFill === 'slope' &&
|
||||
details?.slopeSegmentSlope !== undefined &&
|
||||
details.slopeSegmentDistance !== undefined
|
||||
) {
|
||||
slope += ` (${getDistanceWithUnits(details.slopeSegmentDistance)} @${details.slopeSegmentSlope.toFixed(1)} %)`;
|
||||
}
|
||||
|
||||
let labels = [
|
||||
` ${i18n._('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
|
||||
` ${i18n._('quantities.slope')}: ${slope.at} %${get(this._elevationFill) === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}`,
|
||||
` ${i18n._('quantities.slope')}: ${slope}`,
|
||||
];
|
||||
|
||||
if (get(this._elevationFill) === 'surface') {
|
||||
if (!details) {
|
||||
return labels;
|
||||
}
|
||||
let surface = details.surface ?? 'unknown';
|
||||
let highway = details.highway ?? 'unknown';
|
||||
let sacScale = details.sacScale;
|
||||
let mtbScale = details.mtbScale;
|
||||
|
||||
if (elevationFill === 'surface') {
|
||||
labels.push(
|
||||
` ${i18n._('quantities.surface')}: ${i18n._(`toolbar.routing.surface.${surface}`)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (get(this._elevationFill) === 'highway') {
|
||||
if (elevationFill === 'highway') {
|
||||
labels.push(
|
||||
` ${i18n._('quantities.highway')}: ${i18n._(`toolbar.routing.highway.${highway}`)}${
|
||||
sacScale
|
||||
@@ -255,9 +269,9 @@ export class ElevationProfile {
|
||||
}
|
||||
}
|
||||
|
||||
if (point.time) {
|
||||
if (details.time !== undefined) {
|
||||
labels.push(
|
||||
` ${i18n._('quantities.time')}: ${i18n.df.format(point.time)}`
|
||||
` ${i18n._('quantities.time')}: ${i18n.df.format(new Date(details.time))}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -339,6 +353,7 @@ export class ElevationProfile {
|
||||
events: ['mouseout'],
|
||||
afterEvent: (chart: Chart, args: { event: ChartEvent }) => {
|
||||
if (args.event.type === 'mouseout') {
|
||||
this._hovered = null;
|
||||
this._hoveredPoint.set(null);
|
||||
}
|
||||
},
|
||||
@@ -430,6 +445,9 @@ export class ElevationProfile {
|
||||
return;
|
||||
}
|
||||
const data = get(this._statistics);
|
||||
this._data = data;
|
||||
// the indices of the trackpoints are the ones of other statistics
|
||||
this._hovered = null;
|
||||
const units = {
|
||||
distance: get(distanceUnits),
|
||||
velocity: get(velocityUnits),
|
||||
@@ -438,28 +456,11 @@ export class ElevationProfile {
|
||||
|
||||
const datasets: Array<Array<any>> = [[], [], [], [], [], []];
|
||||
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++) {
|
||||
const x = getConvertedDistance(data.totalDistance[index], units.distance);
|
||||
const ele = data.ele[index];
|
||||
const timestamp = data.timestamps?.[index] ?? NO_TIME;
|
||||
datasets[0].push({
|
||||
x,
|
||||
y: getConvertedElevation(ele, units.distance),
|
||||
time: timestamp === NO_TIME ? undefined : new Date(Number(timestamp)),
|
||||
slope: {
|
||||
at: data.slope[index],
|
||||
segment: data.slopeSegmentSlope?.[index] ?? NaN,
|
||||
length: data.slopeSegmentDistance?.[index] ?? NaN,
|
||||
},
|
||||
surface: surfaces[index],
|
||||
highway: highways[index],
|
||||
sacScale: sacScales[index],
|
||||
mtbScale: mtbScales[index],
|
||||
coordinates: { lat: data.lat[index], lng: data.lng[index] },
|
||||
y: getConvertedElevation(data.ele[index], units.distance),
|
||||
index,
|
||||
});
|
||||
if (data.speed && (global.totalTime ?? 0) > 0) {
|
||||
@@ -643,21 +644,71 @@ export class ElevationProfile {
|
||||
}
|
||||
}
|
||||
|
||||
slopeFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||
const point = context.p0.raw as ElevationProfilePoint;
|
||||
return getSlopeColor(point.slope.segment);
|
||||
/**
|
||||
* Shows the hovered trackpoint on the map, and reads the rest of its data from the engine for
|
||||
* the tooltip. One trackpoint is read at a time, the last one hovered.
|
||||
*/
|
||||
hover(index: number) {
|
||||
this._hoveredPoint.set({ lat: this._data.lat[index], lng: this._data.lng[index] });
|
||||
if (this._hovered?.index === index) {
|
||||
return;
|
||||
}
|
||||
this._hovered = { index, loaded: false };
|
||||
this.loadHovered();
|
||||
}
|
||||
|
||||
surfaceFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||
const point = context.p0.raw as ElevationProfilePoint;
|
||||
return getSurfaceColor(point.surface ?? '');
|
||||
private async loadHovered() {
|
||||
if (this._loadingHovered) {
|
||||
return; // the one being read is followed by the last one hovered
|
||||
}
|
||||
this._loadingHovered = true;
|
||||
try {
|
||||
while (this._hovered && !this._hovered.loaded) {
|
||||
const hovered = this._hovered;
|
||||
const point = await this._data.point(hovered.index);
|
||||
if (this._hovered !== hovered) {
|
||||
continue; // another trackpoint is hovered, or none
|
||||
}
|
||||
hovered.loaded = true;
|
||||
hovered.point = point;
|
||||
const chart = this._chart;
|
||||
if (chart?.tooltip && chart.tooltip.getActiveElements().length > 0) {
|
||||
// the tooltip asks its callbacks again (the method is not in the types)
|
||||
(chart.tooltip as unknown as { update(changed: boolean): void }).update(true);
|
||||
chart.draw();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this._loadingHovered = false;
|
||||
}
|
||||
}
|
||||
|
||||
highwayFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||
const point = context.p0.raw as ElevationProfilePoint;
|
||||
return getHighwayColor(point.highway ?? '', point.sacScale, point.mtbScale);
|
||||
/** The trackpoint that a segment of the chart starts at. */
|
||||
private static segmentStart(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||
return (context.p0.raw as ElevationProfilePoint).index;
|
||||
}
|
||||
|
||||
slopeFillCallback = (context: ScriptableLineSegmentContext & { p0: { raw: any } }) => {
|
||||
const segments = this._data.slopeSegments;
|
||||
const index = ElevationProfile.segmentStart(context);
|
||||
const segment = segments ? intervalAt(segments.starts, index) : -1;
|
||||
return getSlopeColor(segment >= 0 ? segments!.slopes[segment] : NaN);
|
||||
};
|
||||
|
||||
surfaceFillCallback = (context: ScriptableLineSegmentContext & { p0: { raw: any } }) => {
|
||||
const index = ElevationProfile.segmentStart(context);
|
||||
return getSurfaceColor(categoryValue(this._data.surface, index) ?? '');
|
||||
};
|
||||
|
||||
highwayFillCallback = (context: ScriptableLineSegmentContext & { p0: { raw: any } }) => {
|
||||
const index = ElevationProfile.segmentStart(context);
|
||||
return getHighwayColor(
|
||||
categoryValue(this._data.highway, index) ?? '',
|
||||
categoryValue(this._data.sacScale, index),
|
||||
categoryValue(this._data.mtbScale, index)
|
||||
);
|
||||
};
|
||||
|
||||
destroy() {
|
||||
this._request.release();
|
||||
if (this._chart) {
|
||||
|
||||
@@ -26,7 +26,11 @@ get along, so we shut typescript up by casting `value` to `never`.
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ thumbItems })}
|
||||
<!--
|
||||
The thumbs are the values: the snippet does not read the items that bits-ui gives it, which
|
||||
it computes again at each change of the value, with one tick per step of the slider.
|
||||
-->
|
||||
{#snippet children()}
|
||||
<span
|
||||
data-slot="slider-track"
|
||||
data-orientation={orientation}
|
||||
@@ -41,10 +45,10 @@ get along, so we shut typescript up by casting `value` to `never`.
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
{#each thumbItems as thumb (thumb)}
|
||||
{#each Array.isArray(value) ? value : [value] as _, index (index)}
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
index={thumb.index}
|
||||
{index}
|
||||
class="border-ring ring-ring/50 relative size-4 rounded-full border bg-white transition-[color,box-shadow] after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:outline-hidden active:ring-3 block shrink-0 select-none disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
{/each}
|
||||
|
||||
@@ -174,61 +174,22 @@ export const exampleData = {
|
||||
1.5840695473562374, 0.1214109154598013, -4.214883885805801, -4.182735032776085,
|
||||
-5.523076373263165, -10.644434966065772,
|
||||
],
|
||||
slopeSegmentSlope: [
|
||||
7.077037465314024, 7.077037465314024, 7.077037465314024, 7.077037465314024,
|
||||
0.5753520337401837, 0.5753520337401837, 0.5753520337401837, 0.5753520337401837,
|
||||
0.5753520337401837, 5.143330470981779, -5.289467529413177, 3.1409086334150653,
|
||||
3.1409086334150653, -0.553863318953831, -0.553863318953831, -0.553863318953831,
|
||||
-0.553863318953831, -0.553863318953831, -0.553863318953831, -0.553863318953831,
|
||||
-0.553863318953831, -0.553863318953831, -0.553863318953831, -0.553863318953831,
|
||||
-0.553863318953831, -0.553863318953831, -0.553863318953831, -0.553863318953831,
|
||||
-0.553863318953831, -0.553863318953831, 6.771412441716311, 6.771412441716311,
|
||||
6.771412441716311, 6.771412441716311, 6.771412441716311, 6.771412441716311,
|
||||
6.771412441716311, 6.771412441716311, 6.771412441716311, 6.771412441716311,
|
||||
6.771412441716311, 6.771412441716311, 1.246024238407464, 1.246024238407464,
|
||||
5.604351892140715, 5.604351892140715, 5.604351892140715, 2.2941711127147597,
|
||||
2.2941711127147597, 2.2941711127147597, 2.2941711127147597, 2.2941711127147597,
|
||||
2.2941711127147597, 10.861290836693483, 10.861290836693483, 10.861290836693483,
|
||||
10.861290836693483, -4.414487617055606, -4.414487617055606, 6.94339810974512,
|
||||
-3.539036752039, -3.539036752039, -3.539036752039, -13.990509017835178, -13.990509017835178,
|
||||
-13.990509017835178, -13.990509017835178, -2.033792785924166, -2.033792785924166,
|
||||
-2.033792785924166, -2.033792785924166, -2.033792785924166, -2.033792785924166,
|
||||
-2.033792785924166, -2.033792785924166, -2.033792785924166, -2.033792785924166,
|
||||
-2.033792785924166, -2.033792785924166, -2.033792785924166, -2.033792785924166,
|
||||
-2.033792785924166, -2.033792785924166, 0.45298474033593794, 0.45298474033593794,
|
||||
0.45298474033593794, 0.45298474033593794, 0.45298474033593794, 0.45298474033593794,
|
||||
0.45298474033593794, 0.45298474033593794, -7.692745586787598, -7.692745586787598,
|
||||
-0.4142590757867528, -0.4142590757867528, -0.4142590757867528, -0.4142590757867528,
|
||||
-0.4142590757867528, -5.154482373206842, -5.154482373206842, -5.154482373206842,
|
||||
-5.154482373206842,
|
||||
slopeSegmentStarts: [
|
||||
0, 4, 9, 10, 11, 13, 30, 42, 44, 47, 53, 57, 59, 60, 63, 67, 83, 91, 93, 98,
|
||||
],
|
||||
slopeSegmentDistance: [
|
||||
1.0682436029275066, 1.0682436029275066, 1.0682436029275066, 1.0682436029275066,
|
||||
1.8771116406407027, 1.8771116406407027, 1.8771116406407027, 1.8771116406407027,
|
||||
1.8771116406407027, 0.6571617396684242, 0.38189099162956897, 1.2607816597626882,
|
||||
1.2607816597626882, 4.622078972183025, 4.622078972183025, 4.622078972183025,
|
||||
4.622078972183025, 4.622078972183025, 4.622078972183025, 4.622078972183025,
|
||||
4.622078972183025, 4.622078972183025, 4.622078972183025, 4.622078972183025,
|
||||
4.622078972183025, 4.622078972183025, 4.622078972183025, 4.622078972183025,
|
||||
4.622078972183025, 4.622078972183025, 4.040515953724009, 4.040515953724009,
|
||||
4.040515953724009, 4.040515953724009, 4.040515953724009, 4.040515953724009,
|
||||
4.040515953724009, 4.040515953724009, 4.040515953724009, 4.040515953724009,
|
||||
4.040515953724009, 4.040515953724009, 0.802552606262374, 0.802552606262374,
|
||||
3.3545359680865605, 3.3545359680865605, 3.3545359680865605, 2.9030092668715675,
|
||||
2.9030092668715675, 2.9030092668715675, 2.9030092668715675, 2.9030092668715675,
|
||||
2.9030092668715675, 0.8857142437895185, 0.8857142437895185, 0.8857142437895185,
|
||||
0.8857142437895185, 0.8200272181110968, 0.8200272181110968, 0.5818476682663238,
|
||||
1.3280449820963618, 1.3280449820963618, 1.3280449820963618, 1.255136605652762,
|
||||
1.255136605652762, 1.255136605652762, 1.255136605652762, 4.7694141050816405,
|
||||
4.7694141050816405, 4.7694141050816405, 4.7694141050816405, 4.7694141050816405,
|
||||
4.7694141050816405, 4.7694141050816405, 4.7694141050816405, 4.7694141050816405,
|
||||
4.7694141050816405, 4.7694141050816405, 4.7694141050816405, 4.7694141050816405,
|
||||
4.7694141050816405, 4.7694141050816405, 4.7694141050816405, 1.7660639062735584,
|
||||
1.7660639062735584, 1.7660639062735584, 1.7660639062735584, 1.7660639062735584,
|
||||
1.7660639062735584, 1.7660639062735584, 1.7660639062735584, 0.610965220021356,
|
||||
0.610965220021356, 1.738042790330141, 1.738042790330141, 1.738042790330141,
|
||||
1.738042790330141, 1.738042790330141, 1.2493980061849328, 1.2493980061849328,
|
||||
1.2493980061849328, 1.2493980061849328,
|
||||
slopeSegmentSlopes: [
|
||||
7.077037465314024, 0.5753520337401837, 5.143330470981779, -5.289467529413177,
|
||||
3.1409086334150653, -0.553863318953831, 6.771412441716311, 1.246024238407464,
|
||||
5.604351892140715, 2.2941711127147597, 10.861290836693483, -4.414487617055606,
|
||||
6.94339810974512, -3.539036752039, -13.990509017835178, -2.033792785924166,
|
||||
0.45298474033593794, -7.692745586787598, -0.4142590757867528, -5.154482373206842,
|
||||
],
|
||||
slopeSegmentDistances: [
|
||||
1.0682436029275066, 1.8771116406407027, 0.6571617396684242, 0.38189099162956897,
|
||||
1.2607816597626882, 4.622078972183025, 4.040515953724009, 0.802552606262374,
|
||||
3.3545359680865605, 2.9030092668715675, 0.8857142437895185, 0.8200272181110968,
|
||||
0.5818476682663238, 1.3280449820963618, 1.255136605652762, 4.7694141050816405,
|
||||
1.7660639062735584, 0.610965220021356, 1.738042790330141, 1.2493980061849328,
|
||||
],
|
||||
lng: [
|
||||
-66.658624, -66.658127, -66.658472, -66.660451, -66.650629, -66.646183, -66.646388,
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* and the documentation. They are the ones that the engine computes for `example.gpx`, kept as
|
||||
* they are (see `example-data.ts`) so that the example does not need the engine.
|
||||
*/
|
||||
import type { GlobalStatistics, SelectionStatistics } from '$lib/engine';
|
||||
import type { GlobalStatistics, SelectionPoint, SelectionStatistics } from '$lib/engine';
|
||||
import { intervalAt } from '$lib/engine/trackpoint-categories';
|
||||
import { exampleData as data } from '$lib/data/example/example-data';
|
||||
|
||||
const length = data.lng.length;
|
||||
@@ -46,6 +47,21 @@ async function slice(start: number, end: number): Promise<GlobalStatistics | und
|
||||
};
|
||||
}
|
||||
|
||||
const slopeSegmentStarts = Uint32Array.from(data.slopeSegmentStarts);
|
||||
|
||||
/** The data of the trackpoint at `index`, as the engine gives it (the example has no OSM data). */
|
||||
async function point(index: number): Promise<SelectionPoint | undefined> {
|
||||
if (!(index >= 0 && index < length)) {
|
||||
return undefined;
|
||||
}
|
||||
const segment = intervalAt(slopeSegmentStarts, index);
|
||||
return {
|
||||
time: data.timestamps[index],
|
||||
slopeSegmentSlope: data.slopeSegmentSlopes[segment],
|
||||
slopeSegmentDistance: data.slopeSegmentDistances[segment],
|
||||
};
|
||||
}
|
||||
|
||||
export const exampleStatistics: SelectionStatistics = {
|
||||
global: data.global,
|
||||
length,
|
||||
@@ -54,10 +70,12 @@ export const exampleStatistics: SelectionStatistics = {
|
||||
lng: Float64Array.from(data.lng),
|
||||
lat: Float64Array.from(data.lat),
|
||||
ele: Float64Array.from(data.ele),
|
||||
timestamps: BigInt64Array.from(data.timestamps, (time) => BigInt(time)),
|
||||
speed: Float64Array.from(data.speed),
|
||||
atemp: Float64Array.from(data.atemp),
|
||||
slopeSegmentSlope: Float64Array.from(data.slopeSegmentSlope),
|
||||
slopeSegmentDistance: Float64Array.from(data.slopeSegmentDistance),
|
||||
slopeSegments: {
|
||||
starts: slopeSegmentStarts,
|
||||
slopes: Float64Array.from(data.slopeSegmentSlopes),
|
||||
},
|
||||
slice,
|
||||
point,
|
||||
};
|
||||
@@ -182,14 +182,14 @@ export async function createTauriBackend(): Promise<Backend> {
|
||||
lng: () => f64('lng'),
|
||||
lat: () => f64('lat'),
|
||||
ele: () => f64('ele'),
|
||||
timestamps: () => optionalArray('timestamps', BigInt64Array),
|
||||
speed: () => f64Optional('speed'),
|
||||
hr: () => f64Optional('hr'),
|
||||
cad: () => f64Optional('cad'),
|
||||
atemp: () => f64Optional('atemp'),
|
||||
power: () => f64Optional('power'),
|
||||
slope_segment_slope: () => f64('slope_segment_slope'),
|
||||
slope_segment_distance: () => f64('slope_segment_distance'),
|
||||
slope_segment_starts: () => u32('slope_segment_starts'),
|
||||
slope_segment_slopes: () => f64('slope_segment_slopes'),
|
||||
selection_point: async (index) => (await invoke('selection_point', { index })) ?? undefined,
|
||||
surface_starts: () => u32('surface_starts'),
|
||||
surface_values: () => u8('surface_values'),
|
||||
surfaces: () => invoke('surfaces'),
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
Outcome,
|
||||
RouteAttributes,
|
||||
Selection,
|
||||
SelectionPoint,
|
||||
StorageOpened,
|
||||
TrackpointDetails,
|
||||
WaypointDetails,
|
||||
@@ -277,14 +278,14 @@ export interface Backend {
|
||||
lng(): Promise<Float64Array>;
|
||||
lat(): Promise<Float64Array>;
|
||||
ele(): Promise<Float64Array>;
|
||||
timestamps(): Promise<BigInt64Array | undefined>;
|
||||
speed(): Promise<Float64Array | undefined>;
|
||||
hr(): Promise<Float64Array | undefined>;
|
||||
cad(): Promise<Float64Array | undefined>;
|
||||
atemp(): Promise<Float64Array | undefined>;
|
||||
power(): Promise<Float64Array | undefined>;
|
||||
slope_segment_slope(): Promise<Float64Array>;
|
||||
slope_segment_distance(): Promise<Float64Array>;
|
||||
slope_segment_starts(): Promise<Uint32Array>;
|
||||
slope_segment_slopes(): Promise<Float64Array>;
|
||||
selection_point(index: number): Promise<SelectionPoint | undefined>;
|
||||
surface_starts(): Promise<Uint32Array>;
|
||||
surface_values(): Promise<Uint8Array>;
|
||||
surfaces(): Promise<string[]>;
|
||||
|
||||
@@ -135,14 +135,14 @@ export async function createWasmBackend(): Promise<Backend> {
|
||||
lng: lift(w.lng),
|
||||
lat: lift(w.lat),
|
||||
ele: lift(w.ele),
|
||||
timestamps: lift(w.timestamps),
|
||||
speed: lift(w.speed),
|
||||
hr: lift(w.hr),
|
||||
cad: lift(w.cad),
|
||||
atemp: lift(w.atemp),
|
||||
power: lift(w.power),
|
||||
slope_segment_slope: lift(w.slope_segment_slope),
|
||||
slope_segment_distance: lift(w.slope_segment_distance),
|
||||
slope_segment_starts: lift(w.slope_segment_starts),
|
||||
slope_segment_slopes: lift(w.slope_segment_slopes),
|
||||
selection_point: lift(w.selection_point),
|
||||
surface_starts: lift(w.surface_starts),
|
||||
surface_values: lift(w.surface_values),
|
||||
surfaces: lift(w.surfaces),
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ALL_EXPORT_OPTIONS,
|
||||
engine,
|
||||
FileStateCollectionObserver,
|
||||
NO_TIME,
|
||||
type ExportOptions,
|
||||
type FileState,
|
||||
} from '$lib/engine';
|
||||
@@ -195,7 +194,7 @@ describe('statistics', () => {
|
||||
assert.equal(statistics.totalDistance[0], 0);
|
||||
assert.ok(statistics.global.totalDistance > 0);
|
||||
assert.equal(statistics.global.totalDistance, file.statistics.totalDistance);
|
||||
assert.equal(statistics.timestamps, undefined);
|
||||
assert.equal((await statistics.point(0))!.time, undefined);
|
||||
assert.ok(statistics.global.bounds!.west <= statistics.global.bounds!.east);
|
||||
|
||||
await engine.select([]);
|
||||
@@ -206,9 +205,7 @@ describe('statistics', () => {
|
||||
it('has timestamps when the file has some', async () => {
|
||||
await load('with_time');
|
||||
const statistics = get(engine.statistics);
|
||||
assert.ok(statistics.timestamps);
|
||||
assert.equal(statistics.timestamps!.length, statistics.length);
|
||||
assert.ok(statistics.timestamps!.some((t) => t !== NO_TIME));
|
||||
assert.ok((await statistics.point(0))!.time! > 0);
|
||||
assert.ok(statistics.global.totalTime! > 0);
|
||||
assert.ok(statistics.global.movingSpeed === undefined || statistics.global.movingSpeed > 0);
|
||||
});
|
||||
@@ -218,8 +215,13 @@ describe('statistics', () => {
|
||||
assert.equal(get(engine.statistics).hr, undefined);
|
||||
|
||||
const request = engine.requestStatistics();
|
||||
await request.set(['hr', 'anchors']);
|
||||
await request.set(['hr', 'anchors', 'slopeSegment']);
|
||||
const statistics = get(engine.statistics);
|
||||
assert.equal(statistics.slopeSegments!.starts[0], 0);
|
||||
assert.equal(
|
||||
statistics.slopeSegments!.starts.length,
|
||||
statistics.slopeSegments!.slopes.length
|
||||
);
|
||||
assert.equal(statistics.hr!.length, statistics.length);
|
||||
assert.ok(statistics.hr!.some((hr) => !Number.isNaN(hr)));
|
||||
assert.equal(statistics.cad, undefined);
|
||||
@@ -248,6 +250,18 @@ describe('statistics', () => {
|
||||
await engine.select([]);
|
||||
assert.equal(await statistics.slice(2, 5), undefined);
|
||||
});
|
||||
|
||||
it('gives the data of a trackpoint of the selection', async () => {
|
||||
await load('simple');
|
||||
const statistics = get(engine.statistics);
|
||||
const point = (await statistics.point(3))!;
|
||||
assert.ok(point.slopeSegmentDistance! > 0);
|
||||
assert.equal(await statistics.point(statistics.length), undefined);
|
||||
|
||||
// only for the selection it was read for
|
||||
await engine.select([]);
|
||||
assert.equal(await statistics.point(3), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('file state', () => {
|
||||
|
||||
@@ -46,15 +46,16 @@ export class StatisticsReader {
|
||||
private async read(backend: Backend): Promise<SelectionStatistics> {
|
||||
const version = ++this._version;
|
||||
this._loaded.clear();
|
||||
const [global, totalDistance, slope, lng, lat, ele, timestamps] = await Promise.all([
|
||||
const [global, totalDistance, slope, lng, lat, ele] = await Promise.all([
|
||||
backend.selection_statistics(),
|
||||
backend.total_distance(),
|
||||
backend.slope(),
|
||||
backend.lng(),
|
||||
backend.lat(),
|
||||
backend.ele(),
|
||||
backend.timestamps(),
|
||||
]);
|
||||
const current = <T>(read: () => Promise<T | undefined>) =>
|
||||
this.enqueue(async () => (version === this._version ? read() : undefined));
|
||||
const statistics: SelectionStatistics = {
|
||||
global: global ?? EMPTY_STATISTICS,
|
||||
length: totalDistance.length,
|
||||
@@ -63,11 +64,8 @@ export class StatisticsReader {
|
||||
lng,
|
||||
lat,
|
||||
ele,
|
||||
timestamps,
|
||||
slice: (start, end) =>
|
||||
this.enqueue(async () =>
|
||||
version === this._version ? backend.slice_statistics(start, end) : undefined
|
||||
),
|
||||
slice: (start, end) => current(() => backend.slice_statistics(start, end)),
|
||||
point: (index) => current(() => backend.selection_point(index)),
|
||||
};
|
||||
await this.readMetrics(backend, statistics, this.requestedMetrics());
|
||||
return statistics;
|
||||
@@ -114,13 +112,14 @@ export class StatisticsReader {
|
||||
case 'power':
|
||||
statistics.power = await backend.power();
|
||||
break;
|
||||
case 'slopeSegment':
|
||||
[statistics.slopeSegmentSlope, statistics.slopeSegmentDistance] =
|
||||
await Promise.all([
|
||||
backend.slope_segment_slope(),
|
||||
backend.slope_segment_distance(),
|
||||
]);
|
||||
case 'slopeSegment': {
|
||||
const [starts, slopes] = await Promise.all([
|
||||
backend.slope_segment_starts(),
|
||||
backend.slope_segment_slopes(),
|
||||
]);
|
||||
statistics.slopeSegments = { starts, slopes };
|
||||
break;
|
||||
}
|
||||
case 'surface':
|
||||
statistics.surface = await categories(
|
||||
backend.surface_starts(),
|
||||
|
||||
@@ -2,7 +2,9 @@ import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
categoryIntervals,
|
||||
categoryValue,
|
||||
categoryValues,
|
||||
intervalAt,
|
||||
type CategoryIntervals,
|
||||
} from '$lib/engine/trackpoint-categories';
|
||||
|
||||
@@ -68,3 +70,37 @@ describe('categoryIntervals', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('intervalAt', () => {
|
||||
const starts = new Uint32Array([0, 2, 3, 6]);
|
||||
|
||||
it('is the interval that the trackpoint is in', () => {
|
||||
const intervals = [0, 1, 2, 3, 4, 5, 6, 9].map((index) => intervalAt(starts, index));
|
||||
assert.deepEqual(intervals, [0, 0, 1, 2, 2, 2, 3, 3]);
|
||||
});
|
||||
|
||||
it('is -1 when there is no interval', () => {
|
||||
assert.equal(intervalAt(new Uint32Array(), 0), -1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('categoryValue', () => {
|
||||
const intervals: CategoryIntervals = {
|
||||
starts: new Uint32Array([0, 2, 3, 6]),
|
||||
values: new Uint8Array([1, 0, 2, 0]),
|
||||
names: ['asphalt', 'gravel'],
|
||||
};
|
||||
|
||||
it('is the value of each trackpoint, as categoryValues', () => {
|
||||
const values = categoryValues(intervals, 7);
|
||||
values.forEach((value, index) => assert.equal(categoryValue(intervals, index), value));
|
||||
});
|
||||
|
||||
it('is unknown without intervals', () => {
|
||||
assert.equal(categoryValue(undefined, 3), undefined);
|
||||
assert.equal(
|
||||
categoryValue({ starts: new Uint32Array(), values: new Uint8Array(), names: [] }, 0),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -36,6 +36,34 @@ export function categoryValues(
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The index of the interval of `starts` (sorted) that the trackpoint `index` is in, -1 if none. */
|
||||
export function intervalAt(starts: Uint32Array, index: number): number {
|
||||
let low = 0;
|
||||
let high = starts.length;
|
||||
// the first interval that starts after the trackpoint
|
||||
while (low < high) {
|
||||
const middle = (low + high) >>> 1;
|
||||
if (starts[middle] <= index) {
|
||||
low = middle + 1;
|
||||
} else {
|
||||
high = middle;
|
||||
}
|
||||
}
|
||||
return low - 1;
|
||||
}
|
||||
|
||||
/** The value of the trackpoint `index` for a category, `undefined` when it is unknown. */
|
||||
export function categoryValue(
|
||||
intervals: CategoryIntervals | undefined,
|
||||
index: number
|
||||
): string | undefined {
|
||||
if (!intervals) {
|
||||
return undefined;
|
||||
}
|
||||
const value = intervals.values[intervalAt(intervals.starts, index)];
|
||||
return value ? intervals.names[value - 1] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The intervals of the trackpoints that share a value, for a value per trackpoint (`undefined`
|
||||
* when unknown): the inverse of `categoryValues`. The names are in the order of their first
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Readable } from 'svelte/store';
|
||||
import type { Feature, FeatureCollection, LineString, Point } from 'geojson';
|
||||
import type { Visibility } from '$lib/engine/file-visibility';
|
||||
import type { CategoryIntervals } from '$lib/engine/trackpoint-categories';
|
||||
import type { FileStructure, GlobalStatistics } from 'gpx-wasm';
|
||||
import type { FileStructure, GlobalStatistics, SelectionPoint } from 'gpx-wasm';
|
||||
|
||||
export type { FileStatus, FolderEntry, UnsavedChoice, WorkspaceState } from './backend/types';
|
||||
|
||||
@@ -12,6 +12,7 @@ export type {
|
||||
TrackpointDetails,
|
||||
MoveTarget,
|
||||
GlobalStatistics,
|
||||
SelectionPoint,
|
||||
FileStructure,
|
||||
FilesUpdate,
|
||||
Outcome,
|
||||
@@ -136,12 +137,6 @@ export const ALL_EXPORT_OPTIONS: ExportOptions = {
|
||||
asRoute: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Timestamp of the trackpoints that have none, in `SelectionStatistics.timestamps` (the smallest
|
||||
* 64-bit integer, like `NO_TIME` in the engine).
|
||||
*/
|
||||
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.
|
||||
@@ -152,7 +147,7 @@ export type StatisticsMetric =
|
||||
| 'cad'
|
||||
| 'atemp'
|
||||
| 'power'
|
||||
/** `slopeSegmentSlope` and `slopeSegmentDistance`. */
|
||||
/** `slopeSegments`. */
|
||||
| 'slopeSegment'
|
||||
| 'surface'
|
||||
/** `highway`, `sacScale` and `mtbScale`. */
|
||||
@@ -185,17 +180,29 @@ export type SelectionAnchors = {
|
||||
revision: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 lasts until the next start (or the last trackpoint), with a slope of
|
||||
* `slopes[i]` (%). Their length is in `SelectionStatistics.point`.
|
||||
*/
|
||||
export type SlopeSegments = {
|
||||
starts: Uint32Array;
|
||||
slopes: Float64Array;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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. What is
|
||||
* only needed for one trackpoint at a time (its time, its OSM data...) is read with `point`.
|
||||
*
|
||||
* Distances are cumulative over the whole selection. Distances are in km, speeds in km/h, times in
|
||||
* ms (timestamps since the epoch, as 64-bit integers), elevations in m and slopes in %.
|
||||
*
|
||||
* 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.
|
||||
* (see `StatisticsMetric`). Otherwise they have one entry per trackpoint, missing measures are
|
||||
* NaN.
|
||||
*/
|
||||
export type SelectionStatistics = {
|
||||
global: GlobalStatistics;
|
||||
@@ -207,7 +214,6 @@ export type SelectionStatistics = {
|
||||
lng: Float64Array;
|
||||
lat: Float64Array;
|
||||
ele: Float64Array;
|
||||
timestamps?: BigInt64Array;
|
||||
|
||||
// the following need to be requested
|
||||
speed?: Float64Array;
|
||||
@@ -215,9 +221,7 @@ export type SelectionStatistics = {
|
||||
cad?: Float64Array;
|
||||
atemp?: Float64Array;
|
||||
power?: Float64Array;
|
||||
/** Slope and length (km) of the smoothed elevation segment the trackpoint belongs to. */
|
||||
slopeSegmentSlope?: Float64Array;
|
||||
slopeSegmentDistance?: Float64Array;
|
||||
slopeSegments?: SlopeSegments;
|
||||
surface?: CategoryIntervals;
|
||||
highway?: CategoryIntervals;
|
||||
/** SAC hiking scale. */
|
||||
@@ -232,6 +236,12 @@ export type SelectionStatistics = {
|
||||
* in the selection, or if the selection is not the current one anymore.
|
||||
*/
|
||||
slice(start: number, end: number): Promise<GlobalStatistics | undefined>;
|
||||
/**
|
||||
* All the data of the trackpoint at `index`, for example the one hovered on the elevation
|
||||
* profile. `undefined` if it is not in the selection, or if the selection is not the current
|
||||
* one anymore.
|
||||
*/
|
||||
point(index: number): Promise<SelectionPoint | undefined>;
|
||||
};
|
||||
|
||||
export const EMPTY_SELECTION_STATISTICS: SelectionStatistics = {
|
||||
@@ -243,6 +253,7 @@ export const EMPTY_SELECTION_STATISTICS: SelectionStatistics = {
|
||||
lat: new Float64Array(),
|
||||
ele: new Float64Array(),
|
||||
slice: async () => undefined,
|
||||
point: async () => undefined,
|
||||
};
|
||||
|
||||
export type StatisticsRequest = {
|
||||
|
||||
@@ -48,8 +48,11 @@ export async function computeExampleData() {
|
||||
engine.load_files(new Uint8Array(data), Uint32Array.from([data.length]), ['example']);
|
||||
|
||||
// numbers are enough for the times, which are milliseconds
|
||||
const numbers = (values: Values) =>
|
||||
const numbers = (values: Values | Uint32Array) =>
|
||||
values && Array.from(values as ArrayLike<number | bigint>, (value) => Number(value));
|
||||
const length = engine.total_distance().length;
|
||||
const points = Array.from({ length }, (_, index) => engine.selection_point(index)!);
|
||||
const slopeSegmentStarts = Array.from(engine.slope_segment_starts());
|
||||
return {
|
||||
global: engine.selection_statistics(),
|
||||
totalDistance: numbers(engine.total_distance()),
|
||||
@@ -59,12 +62,16 @@ export async function computeExampleData() {
|
||||
elevationGain: numbers(engine.elevation_gain()),
|
||||
elevationLoss: numbers(engine.elevation_loss()),
|
||||
slope: numbers(engine.slope()),
|
||||
slopeSegmentSlope: numbers(engine.slope_segment_slope()),
|
||||
slopeSegmentDistance: numbers(engine.slope_segment_distance()),
|
||||
slopeSegmentStarts,
|
||||
slopeSegmentSlopes: numbers(engine.slope_segment_slopes()),
|
||||
// the length of each segment, which is the one of its trackpoints
|
||||
slopeSegmentDistances: slopeSegmentStarts.map(
|
||||
(start) => points[start].slopeSegmentDistance!
|
||||
),
|
||||
lng: numbers(engine.lng()),
|
||||
lat: numbers(engine.lat()),
|
||||
ele: numbers(engine.ele()),
|
||||
timestamps: numbers(engine.timestamps()),
|
||||
timestamps: points.map((point) => point.time!),
|
||||
speed: numbers(engine.speed()),
|
||||
atemp: numbers(engine.atemp()),
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user