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
@@ -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 = {
|
||||
|
||||
Reference in new issue
Block a user