Migration to new Rust-based engine (#401)

* wasm first steps

* parsing start

* parsing continued

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* fixes

* reorganize

* change type

* fix

* progress

* refactoring

* progress

* progress

* progress

* keep style info

* progress

* progress

* remove (copy) in name

* progress

* merge methods into 1

* splice

* progress

* progress

* progress

* cleaning

* progress

* progress

* progress

* visibility

* progress

* progress

* progress

* progress

* progress

* fixes

* progress

* fix markers

* fix

* progress

* progress

* progress

* progress

* progress

* progress

* small fix

* use engine info

* fix popups

* small fix

* progress

* progress

* progress

* clean tool

* handle self closing elements

* reduce data exchanges across boundary

* copy on wasm side to avoid any data corruption

* speed fix

* routing

* refactoring

* reverse

* reverse

* crop

* split

* merge

* progress

* time tool

* fix bounds for waypoints

* reduce

* fix reactivity

* handle corrupt time data

* export

* progress

* remove gpx js package

* remove todo

* update readme

* cleaning

* progress

* fix link fields

* remove unused fields

* refactoring and tests

* import and export of routes, ref #192

* per-chunk categories storage

* cross-tab sync

* outdated state

* refactoring

* reorder methods

* refactoring

* fix

* improve flow

* renaming

* progress

* progress

* fix

* progress

* per file history

* keep original flag

* use history variant

* fix

* progress

* file explorer

* progress

* fix

* sticky folders

* improved sticky folders

* fix sortable

* fix sortable

* fix

* fix tab size

* no file delete on desktop

* fix routing on other file

* fix sorting

* remember language

* open links in browser

* commit missing changes

* avoid selecting on resize

* clarify filename

* explorer actions

* fix folder styling

* drop anywhere in folder

* cleaning

* fixes

* remove focus-ring

* full screen

* small fix

* consistent highlight color

* improved preview

* performance improvements

* sparser data transfers

* sparser data transfers

* show popup once data is loaded

* fix routing control size

* fix

* sparse desktop build

* remove custom ign styles

* use new endpoint for openhikingmap

* strip native menu

* reorder context menu items

* route full support

* route - track conversion

* create route file action

* route export, filter out anchors at high zoom level

* clarify name handling

* move hide & center to view

* routes continued

* update docs

* fix power format, closes #378

* lint fixes

* fix display of additional data with gaps, closes #361

* fix linting issues

* fix import modules

* fix multidrag

* change base to resolve

* fix type errors

* fix layer typings

* fix last type error

* each keys

* improve typings

* deploy to cloudflare

---------

Co-authored-by: vcoppe <vcoppe@hexaly.com>
This commit is contained in:
vcoppeandvcoppe authored and GitHub committed 2026-10-31 18:08:22 +01:00
1 parent 6a0d434371
commit 8fe7d8d64d
484 files changed
+53949 -41152

No files matched your search

+214
View File
@@ -0,0 +1,214 @@
import { get, writable, type Readable } from 'svelte/store';
import type { CategoryIntervals } from '$lib/engine/trackpoint-categories';
import type { Backend } from './backend';
import {
EMPTY_SELECTION_STATISTICS,
EMPTY_STATISTICS,
type SelectionStatistics,
type StatisticsMetric,
type StatisticsRequest,
} from './types';
/**
* The statistics of the selection, read from the buffers of the engine. Only the metrics that a
* consumer asked for are read (see `request`).
*/
export class StatisticsReader {
private _store = writable<SelectionStatistics>(EMPTY_SELECTION_STATISTICS);
readonly store: Readable<SelectionStatistics> = { subscribe: this._store.subscribe };
/** Identifies the statistics currently in the engine's buffers. */
private _version = 0;
/** What each consumer of the statistics asked for. */
private _requests = new Set<Set<StatisticsMetric>>();
/** The metrics that were read for the current statistics. */
private _loaded = new Set<StatisticsMetric>();
/** The engine, as of the last time the statistics were read. */
private _backend: Backend | null = null;
/** @param enqueue Does a task after the calls to the engine that were made before it. */
constructor(private enqueue: <T>(task: () => Promise<T>) => Promise<T>) {}
/**
* Reads the statistics of the selection again, after it or the files changed. Resolves to what
* puts them in the store, for the caller to do it together with its other updates.
*/
async refresh(backend: Backend): Promise<() => void> {
this._backend = backend;
const statistics = await this.read(backend);
return () => this._store.set(statistics);
}
/**
* Reads the statistics buffers of the engine (the getters copy them), but only the metrics
* that were requested.
*/
private async read(backend: Backend): Promise<SelectionStatistics> {
const version = ++this._version;
this._loaded.clear();
const [global, totalDistance, lng, lat, ele] = await Promise.all([
backend.selection_statistics(),
backend.total_distance(),
backend.lng(),
backend.lat(),
backend.ele(),
]);
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,
totalDistance,
lng,
lat,
ele,
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;
}
private requestedMetrics(): Set<StatisticsMetric> {
const metrics = new Set<StatisticsMetric>();
this._requests.forEach((request) => request.forEach((m) => metrics.add(m)));
return metrics;
}
/** Reads the metrics that were not read yet into `statistics`, which is the current one. */
private async readMetrics(
backend: Backend,
statistics: SelectionStatistics,
metrics: Set<StatisticsMetric>
) {
const categories = async (
starts: Promise<Uint32Array>,
values: Promise<Uint8Array>,
names: Promise<string[]>
): Promise<CategoryIntervals> => {
const [s, v, n] = await Promise.all([starts, values, names]);
return { starts: s, values: v, names: n };
};
const missing = [...metrics].filter((metric) => !this._loaded.has(metric));
missing.forEach((metric) => this._loaded.add(metric));
await Promise.all(
missing.map(async (metric) => {
switch (metric) {
case 'speed':
statistics.speed = await backend.speed();
break;
case 'hr':
statistics.hr = await backend.hr();
break;
case 'cad':
statistics.cad = await backend.cad();
break;
case 'atemp':
statistics.atemp = await backend.atemp();
break;
case 'power':
statistics.power = await backend.power();
break;
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(),
backend.surface_values(),
backend.surfaces()
);
break;
case 'anchors': {
const [indices, zooms, segmentStarts, segmentIds, revision] =
await Promise.all([
backend.anchor_indices(),
backend.anchor_zooms(),
backend.segment_starts(),
backend.segment_ids(),
backend.routing_revision(),
]);
statistics.anchors = {
indices,
zooms,
segmentStarts,
segmentIds,
revision,
};
break;
}
case 'highway':
[statistics.highway, statistics.sacScale, statistics.mtbScale] =
await Promise.all([
categories(
backend.highway_starts(),
backend.highway_values(),
backend.highways()
),
categories(
backend.sac_scale_starts(),
backend.sac_scale_values(),
backend.sac_scales()
),
categories(
backend.mtb_scale_starts(),
backend.mtb_scale_values(),
backend.mtb_scales()
),
]);
break;
}
})
);
}
/**
* Registers a consumer of the statistics that needs some of the metrics that are not part of
* them by default (see `StatisticsMetric`). The metrics asked for by all the consumers are
* added to `statistics`: the ones that are missing are read right away, and then after each
* change of the selection or of the files, and the store is updated. Call `release` when the
* consumer goes away.
*/
request(): StatisticsRequest {
const wanted = new Set<StatisticsMetric>();
this._requests.add(wanted);
return {
set: (metrics) => {
wanted.clear();
for (const metric of metrics) {
wanted.add(metric);
}
return this.loadMissing();
},
release: () => {
this._requests.delete(wanted);
},
};
}
/** Reads the requested metrics that are not in the current statistics yet. */
private async loadMissing() {
const metrics = this.requestedMetrics();
if ([...metrics].every((metric) => this._loaded.has(metric))) {
return;
}
// queued, so that the buffers of the engine still are the ones of the current statistics:
// they are read after every action
await this.enqueue(async () => {
const backend = this._backend;
const requested = this.requestedMetrics();
if (!backend || [...requested].every((metric) => this._loaded.has(metric))) {
return;
}
const statistics = { ...get(this._store) };
await this.readMetrics(backend, statistics, requested);
this._store.set(statistics);
});
}
}