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

+175
View File
@@ -0,0 +1,175 @@
import type { FileStructure } from 'gpx-wasm';
/**
* Visibility the user set explicitly, by element id (file, route, track, segment, waypoint, or the
* waypoints of the file as a whole, see `waypointsKey`): `false` hides, `true` shows. An element
* without entry follows its parent, and everything is shown by default.
*/
export type Visibility = ReadonlyMap<string, boolean>;
/** What is needed to tell whether the elements of a file are hidden (a `FileState` fits). */
export type VisibilityState = { structure: FileStructure; visibility: Visibility };
/** Id to hide or show all the waypoints of a file at once. */
export function waypointsKey(fileId: string): string {
return `${fileId}:waypoints`;
}
/**
* Ids from the file down to `id` in the file tree (file, routes, tracks, segments / waypoints
* group).
*/
function pathTo(structure: FileStructure, id: string): string[] {
const fileId = structure.id;
if (id === fileId) {
return [fileId];
}
if (id === waypointsKey(fileId)) {
return [fileId, id];
}
if (structure.waypoints.some((waypoint) => waypoint.id === id)) {
return [fileId, waypointsKey(fileId), id];
}
if (structure.routes.some((route) => route.id === id)) {
return [fileId, id];
}
for (const track of structure.tracks) {
if (track.id === id) {
return [fileId, id];
}
if (track.segments.some((segment) => segment.id === id)) {
return [fileId, track.id, id];
}
}
return [];
}
function children(structure: FileStructure, id: string): string[] {
if (id === structure.id) {
return [
waypointsKey(id),
...structure.routes.map((route) => route.id),
...structure.tracks.map((track) => track.id),
];
}
if (id === waypointsKey(structure.id)) {
return structure.waypoints.map((waypoint) => waypoint.id);
}
return structure.tracks.find((track) => track.id === id)?.segments.map((s) => s.id) ?? [];
}
function descendants(structure: FileStructure, id: string): string[] {
return children(structure, id).flatMap((child) => [child, ...descendants(structure, child)]);
}
/** The closest explicit visibility on a path (from the file down): whether it is hidden. */
function followsHidden(visibility: Visibility, path: string[]): boolean {
for (let i = path.length - 1; i >= 0; i--) {
const shown = visibility.get(path[i]);
if (shown !== undefined) {
return !shown;
}
}
return false;
}
/**
* Whether an element is hidden: its closest explicit ancestor (or itself) is hidden, and none
* of its descendants was shown. A file whose only visible segment was shown on its own is not
* hidden, its other segments are. Unknown ids are not hidden.
*/
export function isHidden({ structure, visibility }: VisibilityState, id: string): boolean {
return (
followsHidden(visibility, pathTo(structure, id)) &&
!descendants(structure, id).some((descendant) => visibility.get(descendant) === true)
);
}
// Leaf elements have no descendants: no need to look at the structure.
/** Whether a segment is hidden: `parentId` is its route (which is a leaf), or its track. */
export function isSegmentHidden(
{ structure, visibility }: VisibilityState,
parentId: string,
segmentId: string
): boolean {
return followsHidden(visibility, [structure.id, parentId, segmentId]);
}
export function isWaypointHidden(
{ structure, visibility }: VisibilityState,
waypointId: string
): boolean {
return followsHidden(visibility, [structure.id, waypointsKey(structure.id), waypointId]);
}
/**
* Hides or shows elements of a file, and returns the new visibility. What was set explicitly
* below these elements is reset, so that hiding a file then showing one of its segments leaves
* only that segment visible, and hiding the file again hides all of it.
*/
export function setHidden(
structure: FileStructure,
visibility: Visibility,
ids: string[],
hidden: boolean
): Visibility {
const next = new Map(visibility);
ids.forEach((id) => {
descendants(structure, id).forEach((child) => next.delete(child));
next.set(id, !hidden);
});
return next;
}
/** Name under which the visibility of the files is kept in the storage. */
export const VISIBILITY_KEY = 'visibility';
/**
* The visibility of the files as JSON, to keep it: the explicit visibility of each file by id,
* leaving out the files that `keep` refuses (the ones that are gone) and the ones that have none.
* `undefined` when there is nothing to keep.
*/
export function stringifyVisibility(
files: ReadonlyMap<string, Visibility>,
keep: (fileId: string) => boolean
): string | undefined {
const data: Record<string, Record<string, boolean>> = {};
files.forEach((visibility, fileId) => {
if (visibility.size > 0 && keep(fileId)) {
data[fileId] = Object.fromEntries(visibility);
}
});
return Object.keys(data).length > 0 ? JSON.stringify(data) : undefined;
}
/** What `stringifyVisibility` gave back. What does not make sense is left out. */
export function parseVisibility(json: string | undefined): Map<string, Visibility> {
const files = new Map<string, Visibility>();
if (json === undefined) {
return files;
}
try {
const data: unknown = JSON.parse(json);
if (typeof data !== 'object' || data === null) {
return files;
}
for (const [fileId, entries] of Object.entries(data)) {
if (typeof entries !== 'object' || entries === null) {
continue;
}
const visibility = new Map<string, boolean>();
for (const [id, shown] of Object.entries(entries)) {
if (typeof shown === 'boolean') {
visibility.set(id, shown);
}
}
if (visibility.size > 0) {
files.set(fileId, visibility);
}
}
} catch {
// not something that was kept
}
return files;
}