Files
gpx.studio/website/src/lib/engine/selection-helpers.ts
T
vcoppeandvcoppe 8fe7d8d64d 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>
2026-10-31 18:08:22 +01:00

281 lines
9.8 KiB
TypeScript

import type { Clipboard, FileStructure, PathInfo, Selection } from 'gpx-wasm';
import { isHidden, waypointsKey, type VisibilityState } from '$lib/engine/file-visibility';
/**
* An element of the file tree, to ask questions about the selection:
*
* file ─┬─ route
* ├─ track ─── segment
* └─ waypoints ─── waypoint
*
* A route is like a track of a single segment, which is not a node of its own. `waypoints` is the
* node standing for all the waypoints of a file.
*/
export type FileTreeNode =
| { type: 'file'; fileId: string }
| { type: 'route'; fileId: string; routeId: string }
| { type: 'track'; fileId: string; trackId: string }
| { type: 'segment'; fileId: string; trackId: string; segmentId: string }
| { type: 'waypoints'; fileId: string }
| { type: 'waypoint'; fileId: string; waypointId: string };
/**
* The id of the element of a node, as used by the visibility (`waypointsKey` for the waypoints
* node).
*/
export function elementId(node: FileTreeNode): string {
switch (node.type) {
case 'file':
return node.fileId;
case 'route':
return node.routeId;
case 'track':
return node.trackId;
case 'segment':
return node.segmentId;
case 'waypoints':
return waypointsKey(node.fileId);
case 'waypoint':
return node.waypointId;
}
}
/** The name, description and style of a route or track node, `undefined` for the other nodes. */
export function pathInfo(structure: FileStructure, node: FileTreeNode): PathInfo | undefined {
switch (node.type) {
case 'route':
return structure.routes.find((route) => route.id === node.routeId);
case 'track':
return structure.tracks.find((track) => track.id === node.trackId);
default:
return undefined;
}
}
/**
* Whether the selection is made of segments: files, routes, tracks or segments, not waypoints (the
* selection the tools that work on trackpoints work on).
*/
export function coversSegments(selection: Selection): boolean {
return (
selection.type === 'file' ||
selection.type === 'route' ||
selection.type === 'track' ||
selection.type === 'segment'
);
}
export function isEmpty(selection: Selection): boolean {
return selection.type === 'empty';
}
/** Ids of the files holding something selected (or selected themselves). */
export function selectedFileIds(selection: Selection): string[] {
switch (selection.type) {
case 'empty':
return [];
case 'file':
return selection.fileIds;
default:
return [selection.fileId];
}
}
/** Whether the node is one of the elements in the clipboard. */
export function isInClipboard(clipboard: Clipboard | undefined, node: FileTreeNode): boolean {
if (!clipboard) {
return false;
}
switch (node.type) {
case 'file':
return clipboard.type === 'files' && clipboard.ids.includes(node.fileId);
case 'route':
return clipboard.type === 'routes' && clipboard.ids.includes(node.routeId);
case 'track':
return clipboard.type === 'tracks' && clipboard.ids.includes(node.trackId);
case 'segment':
return clipboard.type === 'segments' && clipboard.ids.includes(node.segmentId);
case 'waypoint':
return clipboard.type === 'waypoints' && clipboard.ids.includes(node.waypointId);
case 'waypoints':
return false;
}
}
/** Number of selected elements (the waypoints node counts for one). */
export function selectionSize(selection: Selection): number {
switch (selection.type) {
case 'empty':
return 0;
case 'file':
return selection.fileIds.length;
case 'route':
return selection.routeIds.length;
case 'track':
return selection.trackIds.length;
case 'segment':
return selection.segmentIds.length;
case 'waypoints':
return 1;
case 'waypoint':
return selection.waypointIds.length;
}
}
/**
* The ids of the selected elements, by file, as used by the visibility (a file, its routes, its
* tracks, its segments, its waypoints, or `waypointsKey` for the waypoints node).
*/
export function selectedElementIds(selection: Selection): { fileId: string; ids: string[] }[] {
switch (selection.type) {
case 'empty':
return [];
case 'file':
return selection.fileIds.map((fileId) => ({ fileId, ids: [fileId] }));
case 'route':
return [{ fileId: selection.fileId, ids: selection.routeIds }];
case 'track':
return [{ fileId: selection.fileId, ids: selection.trackIds }];
case 'segment':
return [{ fileId: selection.fileId, ids: selection.segmentIds }];
case 'waypoints':
return [{ fileId: selection.fileId, ids: [waypointsKey(selection.fileId)] }];
case 'waypoint':
return [{ fileId: selection.fileId, ids: selection.waypointIds }];
}
}
/** Whether the node itself is selected. */
export function isSelected(selection: Selection, node: FileTreeNode): boolean {
switch (node.type) {
case 'file':
return selection.type === 'file' && selection.fileIds.includes(node.fileId);
case 'route':
return (
selection.type === 'route' &&
selection.fileId === node.fileId &&
selection.routeIds.includes(node.routeId)
);
case 'track':
return (
selection.type === 'track' &&
selection.fileId === node.fileId &&
selection.trackIds.includes(node.trackId)
);
case 'segment':
return (
selection.type === 'segment' &&
selection.fileId === node.fileId &&
selection.trackId === node.trackId &&
selection.segmentIds.includes(node.segmentId)
);
case 'waypoints':
return selection.type === 'waypoints' && selection.fileId === node.fileId;
case 'waypoint':
return (
selection.type === 'waypoint' &&
selection.fileId === node.fileId &&
selection.waypointIds.includes(node.waypointId)
);
}
}
/** The parent of a node, `undefined` for a file. */
function parent(node: FileTreeNode): FileTreeNode | undefined {
switch (node.type) {
case 'file':
return undefined;
case 'route':
case 'track':
case 'waypoints':
return { type: 'file', fileId: node.fileId };
case 'segment':
return { type: 'track', fileId: node.fileId, trackId: node.trackId };
case 'waypoint':
return { type: 'waypoints', fileId: node.fileId };
}
}
/** Whether the node, or one of its ancestors, is selected: the node is covered by the selection. */
export function isCovered(selection: Selection, node: FileTreeNode): boolean {
for (let n: FileTreeNode | undefined = node; n; n = parent(n)) {
if (isSelected(selection, n)) {
return true;
}
}
return false;
}
/** Whether the node, or one of its descendants, is selected. */
export function hasSelectionWithin(selection: Selection, node: FileTreeNode): boolean {
switch (node.type) {
case 'file':
return selectedFileIds(selection).includes(node.fileId);
case 'track':
return (
isSelected(selection, node) ||
(selection.type === 'segment' &&
selection.fileId === node.fileId &&
selection.trackId === node.trackId)
);
case 'waypoints':
return (
isSelected(selection, node) ||
(selection.type === 'waypoint' && selection.fileId === node.fileId)
);
case 'route':
case 'segment':
case 'waypoint':
return isSelected(selection, node);
}
}
/**
* Whether all the selected elements are hidden (false when nothing is selected). `states` holds
* the files by id: an element of an unknown file is not hidden.
*/
export function isSelectionHidden(
selection: Selection,
states: ReadonlyMap<string, VisibilityState>
): boolean {
const selected = selectedElementIds(selection);
return (
selected.length > 0 &&
selected.every(({ fileId, ids }) => {
const state = states.get(fileId);
return state !== undefined && ids.every((id) => isHidden(state, id));
})
);
}
/**
* What the selected files convert to: `'route'` when they all have tracks only (with segments),
* `'track'` when they all have routes only, `undefined` otherwise (other selections, files with
* both or neither, or a mix of the two). `states` holds the files by id.
*/
export function fileConversion(
selection: Selection,
states: ReadonlyMap<string, { structure: FileStructure }>
): 'route' | 'track' | undefined {
if (selection.type !== 'file' || selection.fileIds.length === 0) {
return undefined;
}
const targets = new Set(
selection.fileIds.map((fileId) => {
const structure = states.get(fileId)?.structure;
if (structure === undefined) {
return undefined;
} else if (structure.routes.length > 0 && structure.tracks.length === 0) {
return 'track' as const;
} else if (
structure.routes.length === 0 &&
structure.tracks.some((track) => track.segments.length > 0)
) {
return 'route' as const;
}
return undefined;
})
);
return targets.size === 1 ? targets.values().next().value : undefined;
}