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 ): 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 ): '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; }