mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-07 06:14:07 +00:00
progress
This commit is contained in:
@@ -6,8 +6,9 @@
|
||||
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from '@lucide/svelte';
|
||||
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||
import type { Readable } from 'svelte/store';
|
||||
import type { SelectionStatistics } from '$lib/engine';
|
||||
import type { SlicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
|
||||
const { velocityUnits } = settings;
|
||||
@@ -16,18 +17,17 @@
|
||||
let panelWidth: number = $state(0);
|
||||
|
||||
let {
|
||||
gpxStatistics,
|
||||
slicedGPXStatistics,
|
||||
statistics: selectionStatistics,
|
||||
slicedStatistics,
|
||||
orientation,
|
||||
}: {
|
||||
gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||
slicedGPXStatistics: Readable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||
statistics: Readable<SelectionStatistics>;
|
||||
slicedStatistics: Readable<SlicedStatistics | undefined>;
|
||||
orientation: 'horizontal' | 'vertical';
|
||||
} = $props();
|
||||
|
||||
let statistics = $derived(
|
||||
$slicedGPXStatistics !== undefined ? $slicedGPXStatistics[0] : $gpxStatistics.global
|
||||
);
|
||||
// the highlighted part of the selection, or all of it
|
||||
let statistics = $derived($slicedStatistics?.global ?? $selectionStatistics.global);
|
||||
</script>
|
||||
|
||||
<Card.Root
|
||||
@@ -46,15 +46,15 @@
|
||||
<Tooltip label={i18n._('quantities.distance')}>
|
||||
<span class="flex flex-row items-center">
|
||||
<Ruler size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.distance.total} type="distance" />
|
||||
<WithUnits value={statistics.totalDistance} type="distance" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label={i18n._('quantities.elevation_gain_loss')}>
|
||||
<span class="flex flex-row items-center">
|
||||
<MoveUpRight size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.elevation.gain} type="elevation" />
|
||||
<WithUnits value={statistics.elevationGain} type="elevation" />
|
||||
<MoveDownRight size="16" class="mx-1" />
|
||||
<WithUnits value={statistics.elevation.loss} type="elevation" />
|
||||
<WithUnits value={statistics.elevationLoss} type="elevation" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
{#if panelHeight > 120 || (orientation === 'horizontal' && panelWidth > 450)}
|
||||
@@ -67,9 +67,13 @@
|
||||
>
|
||||
<span class="flex flex-row items-center">
|
||||
<Zap size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.speed.moving} type="speed" showUnits={false} />
|
||||
<WithUnits
|
||||
value={statistics.movingSpeed ?? 0}
|
||||
type="speed"
|
||||
showUnits={false}
|
||||
/>
|
||||
<span class="mx-1">/</span>
|
||||
<WithUnits value={statistics.speed.total} type="speed" />
|
||||
<WithUnits value={statistics.totalSpeed ?? 0} type="speed" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
@@ -81,9 +85,9 @@
|
||||
>
|
||||
<span class="flex flex-row items-center">
|
||||
<Timer size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.time.moving} type="time" />
|
||||
<WithUnits value={statistics.movingTime ?? 0} type="time" />
|
||||
<span class="mx-1">/</span>
|
||||
<WithUnits value={statistics.time.total} type="time" />
|
||||
<WithUnits value={statistics.totalTime ?? 0} type="time" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
Construction,
|
||||
} from '@lucide/svelte';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import type { Coordinates, GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||
import type { Coordinates } from 'gpx';
|
||||
import type { SelectionStatistics } from '$lib/engine';
|
||||
import type { SlicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
|
||||
@@ -26,15 +28,15 @@
|
||||
const { velocityUnits } = settings;
|
||||
|
||||
let {
|
||||
gpxStatistics,
|
||||
slicedGPXStatistics,
|
||||
statistics,
|
||||
slicedStatistics,
|
||||
hoveredPoint,
|
||||
additionalDatasets,
|
||||
elevationFill,
|
||||
showControls = true,
|
||||
}: {
|
||||
gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||
statistics: Readable<SelectionStatistics>;
|
||||
slicedStatistics: Writable<SlicedStatistics | undefined>;
|
||||
hoveredPoint: Writable<Coordinates | null>;
|
||||
additionalDatasets: Writable<string[]>;
|
||||
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
|
||||
@@ -47,8 +49,8 @@
|
||||
|
||||
onMount(() => {
|
||||
elevationProfile = new ElevationProfile(
|
||||
gpxStatistics,
|
||||
slicedGPXStatistics,
|
||||
statistics,
|
||||
slicedStatistics,
|
||||
hoveredPoint,
|
||||
additionalDatasets,
|
||||
elevationFill,
|
||||
|
||||
@@ -21,7 +21,9 @@ import Chart, {
|
||||
type TooltipItem,
|
||||
} from 'chart.js/auto';
|
||||
import { get, type Readable, type Writable } from 'svelte/store';
|
||||
import type { Coordinates, GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||
import type { Coordinates } from 'gpx';
|
||||
import { NO_TIME, type SelectionStatistics } from '$lib/engine';
|
||||
import type { SlicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { mode } from 'mode-watcher';
|
||||
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
|
||||
|
||||
@@ -51,23 +53,23 @@ export class ElevationProfile {
|
||||
private _dragging = false;
|
||||
private _panning = false;
|
||||
|
||||
private _gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||
private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||
private _statistics: Readable<SelectionStatistics>;
|
||||
private _slicedStatistics: Writable<SlicedStatistics | undefined>;
|
||||
private _hoveredPoint: Writable<Coordinates | null>;
|
||||
private _additionalDatasets: Readable<string[]>;
|
||||
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
|
||||
|
||||
constructor(
|
||||
gpxStatistics: Readable<GPXStatisticsGroup>,
|
||||
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>,
|
||||
statistics: Readable<SelectionStatistics>,
|
||||
slicedStatistics: Writable<SlicedStatistics | undefined>,
|
||||
hoveredPoint: Writable<Coordinates | null>,
|
||||
additionalDatasets: Readable<string[]>,
|
||||
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
|
||||
canvas: HTMLCanvasElement,
|
||||
overlay: HTMLCanvasElement
|
||||
) {
|
||||
this._gpxStatistics = gpxStatistics;
|
||||
this._slicedGPXStatistics = slicedGPXStatistics;
|
||||
this._statistics = statistics;
|
||||
this._slicedStatistics = slicedStatistics;
|
||||
this._hoveredPoint = hoveredPoint;
|
||||
this._additionalDatasets = additionalDatasets;
|
||||
this._elevationFill = elevationFill;
|
||||
@@ -78,10 +80,10 @@ export class ElevationProfile {
|
||||
Chart.register(module.default);
|
||||
this.initialize();
|
||||
|
||||
this._gpxStatistics.subscribe(() => {
|
||||
this._statistics.subscribe(() => {
|
||||
this.updateData();
|
||||
});
|
||||
this._slicedGPXStatistics.subscribe(() => {
|
||||
this._slicedStatistics.subscribe(() => {
|
||||
this.updateOverlay();
|
||||
});
|
||||
distanceUnits.subscribe(() => {
|
||||
@@ -235,7 +237,7 @@ export class ElevationProfile {
|
||||
modifierKey: 'shift',
|
||||
onPanStart: () => {
|
||||
this._panning = true;
|
||||
this._slicedGPXStatistics.set(undefined);
|
||||
this._slicedStatistics.set(undefined);
|
||||
return true;
|
||||
},
|
||||
onPanComplete: () => {
|
||||
@@ -260,7 +262,7 @@ export class ElevationProfile {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._slicedGPXStatistics.set(undefined);
|
||||
this._slicedStatistics.set(undefined);
|
||||
},
|
||||
},
|
||||
limits: {
|
||||
@@ -361,14 +363,12 @@ export class ElevationProfile {
|
||||
if (startIndex === undefined) {
|
||||
startIndex = endIndex;
|
||||
} else if (startIndex !== endIndex) {
|
||||
this._slicedGPXStatistics.set([
|
||||
get(this._gpxStatistics).sliced(
|
||||
Math.min(startIndex, endIndex),
|
||||
Math.max(startIndex, endIndex)
|
||||
),
|
||||
Math.min(startIndex, endIndex),
|
||||
Math.max(startIndex, endIndex),
|
||||
]);
|
||||
const start = Math.min(startIndex, endIndex);
|
||||
const end = Math.max(startIndex, endIndex);
|
||||
const global = get(this._statistics).slice(start, end);
|
||||
if (global) {
|
||||
this._slicedStatistics.set({ global, start, end });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -379,7 +379,7 @@ export class ElevationProfile {
|
||||
this._canvas.style.cursor = '';
|
||||
endIndex = getIndex(evt);
|
||||
if (startIndex === endIndex) {
|
||||
this._slicedGPXStatistics.set(undefined);
|
||||
this._slicedStatistics.set(undefined);
|
||||
}
|
||||
};
|
||||
this._canvas.addEventListener('pointerdown', onMouseDown);
|
||||
@@ -391,7 +391,7 @@ export class ElevationProfile {
|
||||
if (!this._chart) {
|
||||
return;
|
||||
}
|
||||
const data = get(this._gpxStatistics);
|
||||
const data = get(this._statistics);
|
||||
const units = {
|
||||
distance: get(distanceUnits),
|
||||
velocity: get(velocityUnits),
|
||||
@@ -399,52 +399,49 @@ export class ElevationProfile {
|
||||
};
|
||||
|
||||
const datasets: Array<Array<any>> = [[], [], [], [], [], []];
|
||||
data.forEachTrackPoint((trkpt, distance, speed, slope, index) => {
|
||||
const { global } = data;
|
||||
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];
|
||||
datasets[0].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: trkpt.ele ? getConvertedElevation(trkpt.ele, units.distance) : 0,
|
||||
time: trkpt.time,
|
||||
slope: slope,
|
||||
extensions: trkpt.getExtensions(),
|
||||
coordinates: trkpt.getCoordinates(),
|
||||
index: index,
|
||||
x,
|
||||
y: ele ? getConvertedElevation(ele, units.distance) : 0,
|
||||
time: timestamp === NO_TIME ? undefined : new Date(Number(timestamp)),
|
||||
slope: {
|
||||
at: data.slope[index],
|
||||
segment: data.slopeSegmentSlope[index],
|
||||
length: data.slopeSegmentDistance[index],
|
||||
},
|
||||
// TODO the engine does not store the extensions (surface, highway...) yet
|
||||
extensions: data.extensions[index] ?? {},
|
||||
coordinates: { lat: data.lat[index], lon: data.lng[index] },
|
||||
index,
|
||||
});
|
||||
if (data.global.time.total > 0) {
|
||||
if ((global.totalTime ?? 0) > 0) {
|
||||
datasets[1].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: getConvertedVelocity(speed, units.velocity, units.distance),
|
||||
index: index,
|
||||
x,
|
||||
y: getConvertedVelocity(data.speed[index], units.velocity, units.distance),
|
||||
index,
|
||||
});
|
||||
}
|
||||
if (data.global.hr.count > 0) {
|
||||
datasets[2].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: trkpt.getHeartRate(),
|
||||
index: index,
|
||||
});
|
||||
if (global.hr) {
|
||||
datasets[2].push({ x, y: data.hr[index], index });
|
||||
}
|
||||
if (data.global.cad.count > 0) {
|
||||
datasets[3].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: trkpt.getCadence(),
|
||||
index: index,
|
||||
});
|
||||
if (global.cad) {
|
||||
datasets[3].push({ x, y: data.cad[index], index });
|
||||
}
|
||||
if (data.global.atemp.count > 0) {
|
||||
if (global.atemp) {
|
||||
datasets[4].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: getConvertedTemperature(trkpt.getTemperature(), units.temperature),
|
||||
index: index,
|
||||
x,
|
||||
y: getConvertedTemperature(data.atemp[index], units.temperature),
|
||||
index,
|
||||
});
|
||||
}
|
||||
if (data.global.power.count > 0) {
|
||||
datasets[5].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: trkpt.getPower(),
|
||||
index: index,
|
||||
});
|
||||
if (global.power) {
|
||||
datasets[5].push({ x, y: data.power[index], index });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this._chart.data.datasets[0] = {
|
||||
label: i18n._('quantities.elevation'),
|
||||
@@ -482,7 +479,7 @@ export class ElevationProfile {
|
||||
|
||||
this._chart.options.scales!.x!['min'] = 0;
|
||||
this._chart.options.scales!.x!['max'] = getConvertedDistance(
|
||||
data.global.distance.total,
|
||||
global.totalDistance,
|
||||
units.distance
|
||||
);
|
||||
|
||||
@@ -563,10 +560,10 @@ export class ElevationProfile {
|
||||
this._overlay.style.width = `${this._overlay.width}px`;
|
||||
this._overlay.style.height = `${this._overlay.height}px`;
|
||||
|
||||
const slicedGPXStatistics = get(this._slicedGPXStatistics);
|
||||
if (slicedGPXStatistics) {
|
||||
let startIndex = slicedGPXStatistics[1];
|
||||
let endIndex = slicedGPXStatistics[2];
|
||||
const slicedStatistics = get(this._slicedStatistics);
|
||||
if (slicedStatistics) {
|
||||
let startIndex = slicedStatistics.start;
|
||||
let endIndex = slicedStatistics.end;
|
||||
|
||||
// Draw selection rectangle
|
||||
let selectionContext = this._overlay.getContext('2d');
|
||||
@@ -575,14 +572,12 @@ export class ElevationProfile {
|
||||
selectionContext.globalAlpha = mode.current === 'dark' ? 0.2 : 0.1;
|
||||
selectionContext.clearRect(0, 0, this._overlay.width, this._overlay.height);
|
||||
|
||||
const gpxStatistics = get(this._gpxStatistics);
|
||||
const statistics = get(this._statistics);
|
||||
let startPixel = this._chart.scales.x.getPixelForValue(
|
||||
getConvertedDistance(
|
||||
gpxStatistics.getTrackPoint(startIndex)?.distance.total ?? 0
|
||||
)
|
||||
getConvertedDistance(statistics.totalDistance[startIndex] ?? 0)
|
||||
);
|
||||
let endPixel = this._chart.scales.x.getPixelForValue(
|
||||
getConvertedDistance(gpxStatistics.getTrackPoint(endIndex)?.distance.total ?? 0)
|
||||
getConvertedDistance(statistics.totalDistance[endIndex] ?? 0)
|
||||
);
|
||||
|
||||
selectionContext.fillRect(
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
import { setMode } from 'mode-watcher';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { gpxStatistics, hoveredPoint, slicedGPXStatistics } from '$lib/logic/statistics';
|
||||
import { loadFile } from '$lib/logic/file-actions';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { untrack } from 'svelte';
|
||||
import { isSelected, toggle } from '$lib/components/map/layer-control/utils';
|
||||
import { engine } from '$lib/engine';
|
||||
import { hoveredPoint } from '$lib/logic/statistics';
|
||||
import { slicedStatistics } from '$lib/logic/selection-statistics';
|
||||
|
||||
let {
|
||||
useHash = true,
|
||||
@@ -31,6 +33,7 @@
|
||||
let additionalDatasets = writable<string[]>([]);
|
||||
let elevationFill = writable<'slope' | 'surface' | 'highway' | undefined>(undefined);
|
||||
|
||||
const { statistics } = engine;
|
||||
const {
|
||||
currentBasemap,
|
||||
selectedBasemapTree,
|
||||
@@ -122,14 +125,14 @@
|
||||
style={options.elevation.show ? `height: ${options.elevation.height}px` : ''}
|
||||
>
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={options.elevation.show ? 'vertical' : 'horizontal'}
|
||||
/>
|
||||
{#if options.elevation.show}
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
|
||||
+127
-4
@@ -4,10 +4,10 @@ import { FileColorAllocator, normalizeColor } from '$lib/file-colors';
|
||||
import { setHidden, type Visibility } from '$lib/file-visibility';
|
||||
import { selectedElementIds, type FileTreeNode } from '$lib/selection-helpers';
|
||||
import type { Feature, FeatureCollection, LineString, Point } from 'geojson';
|
||||
import type { FileStatistics, FileStructure, Selection } from 'gpx-rs';
|
||||
import type { GlobalStatistics, FileStructure, Selection } from 'gpx-rs';
|
||||
|
||||
export type {
|
||||
FileStatistics,
|
||||
GlobalStatistics,
|
||||
FileStructure,
|
||||
FilesUpdate,
|
||||
Selection,
|
||||
@@ -63,7 +63,7 @@ export type FileState = {
|
||||
/** Color of the file: the one defined by its tracks if any, otherwise one from the palette. */
|
||||
color: string;
|
||||
/** Global statistics of the file. */
|
||||
statistics: FileStatistics;
|
||||
statistics: GlobalStatistics;
|
||||
/** One LineString feature per track segment, in file order. */
|
||||
segments: FeatureCollection<LineString, SegmentProperties>;
|
||||
/** One Point feature per waypoint, in file order. */
|
||||
@@ -75,7 +75,85 @@ export type FileState = {
|
||||
visibility: Visibility;
|
||||
};
|
||||
|
||||
const EMPTY_STATISTICS: FileStatistics = { totalDistance: 0, elevationGain: 0, elevationLoss: 0 };
|
||||
const EMPTY_STATISTICS: GlobalStatistics = { totalDistance: 0, elevationGain: 0, elevationLoss: 0 };
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Distances, times, moving values and elevation gain / loss 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 %. Missing timestamps are `NO_TIME`, missing
|
||||
* measures are NaN.
|
||||
*/
|
||||
export type SelectionStatistics = {
|
||||
global: GlobalStatistics;
|
||||
/** Number of trackpoints. */
|
||||
length: number;
|
||||
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: Float64Array;
|
||||
/** Slope and length (km) of the smoothed elevation segment the trackpoint belongs to. */
|
||||
slopeSegmentSlope: Float64Array;
|
||||
slopeSegmentDistance: Float64Array;
|
||||
lng: Float64Array;
|
||||
lat: Float64Array;
|
||||
ele: Float64Array;
|
||||
timestamps: BigInt64Array;
|
||||
hr: Float64Array;
|
||||
cad: Float64Array;
|
||||
atemp: Float64Array;
|
||||
power: Float64Array;
|
||||
/**
|
||||
* Extensions of the trackpoints (surface, highway...), or an empty array when they are not
|
||||
* known. TODO the engine does not store them yet.
|
||||
*/
|
||||
extensions: Record<string, string>[];
|
||||
/**
|
||||
* 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
|
||||
* in the selection, or if the selection is not the current one anymore.
|
||||
*/
|
||||
slice(start: number, end: number): GlobalStatistics | undefined;
|
||||
};
|
||||
|
||||
const EMPTY_SELECTION_STATISTICS: SelectionStatistics = {
|
||||
global: EMPTY_STATISTICS,
|
||||
length: 0,
|
||||
totalDistance: new Float64Array(),
|
||||
movingDistance: new Float64Array(),
|
||||
totalTime: new BigInt64Array(),
|
||||
movingTime: new BigInt64Array(),
|
||||
speed: new Float64Array(),
|
||||
elevationGain: new Float64Array(),
|
||||
elevationLoss: new Float64Array(),
|
||||
slope: new Float64Array(),
|
||||
slopeSegmentSlope: new Float64Array(),
|
||||
slopeSegmentDistance: new Float64Array(),
|
||||
lng: new Float64Array(),
|
||||
lat: new Float64Array(),
|
||||
ele: new Float64Array(),
|
||||
timestamps: new BigInt64Array(),
|
||||
hr: new Float64Array(),
|
||||
cad: new Float64Array(),
|
||||
atemp: new Float64Array(),
|
||||
power: new Float64Array(),
|
||||
extensions: [],
|
||||
slice: () => undefined,
|
||||
};
|
||||
|
||||
function toPositions(flat: Float64Array): [number, number][] {
|
||||
const positions: [number, number][] = new Array(flat.length / 2);
|
||||
@@ -119,9 +197,14 @@ class Engine {
|
||||
private _visibility = new Map<string, Visibility>();
|
||||
private _files = writable<Map<string, Writable<FileState>>>(new Map());
|
||||
private _selection = writable<Selection>({ type: 'empty' });
|
||||
private _statistics = writable<SelectionStatistics>(EMPTY_SELECTION_STATISTICS);
|
||||
/** Identifies the statistics currently in the engine's buffers. */
|
||||
private _statisticsVersion = 0;
|
||||
|
||||
/** Ids of the files in display order. */
|
||||
readonly order: Readable<string[]> = { subscribe: this._order.subscribe };
|
||||
/** Statistics of what is currently selected. */
|
||||
readonly statistics: Readable<SelectionStatistics> = { subscribe: this._statistics.subscribe };
|
||||
/** What is currently selected. */
|
||||
readonly selection: Readable<Selection> = { subscribe: this._selection.subscribe };
|
||||
/**
|
||||
@@ -365,6 +448,46 @@ class Engine {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the statistics depend on the selection, on its order, and on the files
|
||||
if (
|
||||
update.selectionChanged ||
|
||||
update.orderChanged ||
|
||||
update.added.length + update.modified.length + update.removed.length > 0
|
||||
) {
|
||||
this._statistics.set(this.readStatistics(wasm));
|
||||
}
|
||||
}
|
||||
|
||||
/** Copies the statistics buffers of the engine (they are overwritten by the next action). */
|
||||
private readStatistics(wasm: Wasm): SelectionStatistics {
|
||||
const version = ++this._statisticsVersion;
|
||||
const totalDistance = wasm.total_distance().slice();
|
||||
return {
|
||||
global: wasm.selection_statistics() ?? EMPTY_STATISTICS,
|
||||
length: totalDistance.length,
|
||||
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(),
|
||||
slopeSegmentSlope: wasm.slope_segment_slope().slice(),
|
||||
slopeSegmentDistance: wasm.slope_segment_distance().slice(),
|
||||
lng: wasm.lng().slice(),
|
||||
lat: wasm.lat().slice(),
|
||||
ele: wasm.ele().slice(),
|
||||
timestamps: wasm.timestamps().slice(),
|
||||
hr: wasm.hr().slice(),
|
||||
cad: wasm.cad().slice(),
|
||||
atemp: wasm.atemp().slice(),
|
||||
power: wasm.power().slice(),
|
||||
extensions: [],
|
||||
slice: (start, end) =>
|
||||
version === this._statisticsVersion ? wasm.slice_statistics(start, end) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** Reads the state of a file, reusing from `previous` what did not change. */
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { engine, type GlobalStatistics } from '$lib/engine';
|
||||
|
||||
/** The part of the selection that is highlighted in the elevation profile. */
|
||||
export type SlicedStatistics = {
|
||||
/** Statistics of the part. */
|
||||
global: GlobalStatistics;
|
||||
/** Indices of its first and last trackpoints in the statistics of the selection. */
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
|
||||
export const slicedStatistics = writable<SlicedStatistics | undefined>(undefined);
|
||||
|
||||
// the indices only make sense for the selection they were made on
|
||||
engine.statistics.subscribe(() => slicedStatistics.set(undefined));
|
||||
@@ -18,14 +18,37 @@
|
||||
} from '@lucide/svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
import { exampleGPXFile } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
|
||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
// TODO the example comes from the previous implementation
|
||||
let statistics = writable({
|
||||
global: { totalDistance: 0, elevationGain: 0, elevationLoss: 0 },
|
||||
length: 0,
|
||||
totalDistance: new Float64Array(),
|
||||
movingDistance: new Float64Array(),
|
||||
totalTime: new BigInt64Array(),
|
||||
movingTime: new BigInt64Array(),
|
||||
speed: new Float64Array(),
|
||||
elevationGain: new Float64Array(),
|
||||
elevationLoss: new Float64Array(),
|
||||
slope: new Float64Array(),
|
||||
slopeSegmentSlope: new Float64Array(),
|
||||
slopeSegmentDistance: new Float64Array(),
|
||||
lng: new Float64Array(),
|
||||
lat: new Float64Array(),
|
||||
ele: new Float64Array(),
|
||||
timestamps: new BigInt64Array(),
|
||||
hr: new Float64Array(),
|
||||
cad: new Float64Array(),
|
||||
atemp: new Float64Array(),
|
||||
power: new Float64Array(),
|
||||
extensions: [],
|
||||
slice: () => undefined,
|
||||
});
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -138,18 +161,14 @@
|
||||
>
|
||||
<div class="grow">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
/>
|
||||
</div>
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
<GPXStatistics {statistics} {slicedStatistics} orientation={'horizontal'} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid md:grid-rows-subgrid md:row-start-3 md:row-end-5 gap-4">
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
import { loadFiles } from '$lib/logic/file-actions';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { gpxStatistics, hoveredPoint, slicedGPXStatistics } from '$lib/logic/statistics';
|
||||
import { hoveredPoint } from '$lib/logic/statistics';
|
||||
import { slicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { engine } from '$lib/engine';
|
||||
import { getURLForGoogleDriveFile } from '$lib/components/embedding/embedding';
|
||||
import { db } from '$lib/db';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
@@ -140,14 +142,14 @@
|
||||
style={$elevationProfile ? `height: ${$bottomPanelSize}px` : ''}
|
||||
>
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
statistics={engine.statistics}
|
||||
{slicedStatistics}
|
||||
orientation={bottomPanelOrientation == 'horizontal' ? 'vertical' : 'horizontal'}
|
||||
/>
|
||||
{#if $elevationProfile}
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
statistics={engine.statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
|
||||
Reference in New Issue
Block a user