route full support

This commit is contained in:
vcoppe committed 2026-10-26 18:51:56 +01:00
1 parent c842da8dc1
commit b6c0f5190a
91 files changed
+2523 -847

No files matched your search

+11 -2
View File
@@ -105,8 +105,10 @@
let desktop = $derived($workspace !== undefined);
let selectedIds = $derived(selectedFileIds($selection));
let anyUnsaved = $derived(Object.values($workspace?.files ?? {}).some((file) => file.dirty));
// metadata and style can be edited for files and tracks only
let filesOrTracksSelected = $derived($selection.type === 'file' || $selection.type === 'track');
// metadata and style can be edited for files, routes and tracks only
let filesOrTracksSelected = $derived(
$selection.type === 'file' || $selection.type === 'route' || $selection.type === 'track'
);
function switchBasemaps() {
[$currentBasemap, $previousBasemap] = [$previousBasemap, $currentBasemap];
@@ -308,6 +310,13 @@
<Plus size="16" />
{i18n._('menu.new_track')}
</Menubar.Item>
<Menubar.Item
onclick={() => engine.newRoute()}
disabled={selectionCount !== 1}
>
<Plus size="16" />
{i18n._('menu.new_route')}
</Menubar.Item>
{:else if $selection.type === 'track'}
<Menubar.Separator />
<Menubar.Item
@@ -21,7 +21,6 @@
Orbit,
Thermometer,
SquareActivity,
Route,
} from '@lucide/svelte';
import { i18n } from '$lib/i18n/i18n.svelte';
import { engine, type ExportOptions } from '$lib/engine';
@@ -38,7 +37,6 @@
atemp: true,
power: true,
osm: false,
asRoute: false,
});
// the files that are exported
let fileIds: string[] = $derived.by(() => {
@@ -61,7 +59,6 @@
atemp: false,
power: false,
osm: false,
asRoute: false,
});
$effect(() => {
const ids = fileIds;
@@ -75,8 +72,6 @@
atemp: !available.atemp,
power: !available.power,
osm: !available.osm,
// always possible
asRoute: false,
};
}
});
@@ -131,7 +126,6 @@
atemp: exportOptions.atemp && !hide.atemp,
power: exportOptions.power && !hide.power,
osm: exportOptions.osm && !hide.osm,
asRoute: exportOptions.asRoute,
};
if (exportState.current === ExportState.SELECTION) {
exportSelectedFiles(options);
@@ -211,13 +205,6 @@
{i18n._('quantities.osm_extensions')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5">
<Checkbox id="export-as-route" bind:checked={exportOptions.asRoute} />
<Label for="export-as-route" class="flex flex-row items-center gap-1">
<Route size="16" />
{i18n._('menu.export_as_route', 'Export as route')}
</Label>
</div>
</div>
</div>
</Dialog.Content>
@@ -30,6 +30,12 @@
switch (node.type) {
case 'file':
return structure.name;
case 'route': {
const routeIndex = structure.routes.findIndex((r) => r.id === node.routeId);
return (
structure.routes[routeIndex]?.name ?? `${i18n._('gpx.route')} ${routeIndex + 1}`
);
}
case 'track': {
const trackIndex = structure.tracks.findIndex((t) => t.id === node.trackId);
return (
@@ -75,7 +81,7 @@
});
</script>
{#if node.type === 'segment' || node.type === 'waypoint'}
{#if node.type === 'route' || node.type === 'segment' || node.type === 'waypoint'}
<FileListNodeLabel {fileState} {node} {label} />
{:else if recursive}
<CollapsibleTreeNode {id} bind:this={collapsible}>
@@ -84,7 +90,11 @@
{/snippet}
{#snippet content()}
<div class="ml-4">
<FileListNodeContent {fileState} {node} />
{#if node.type === 'file' && fileState.structure.routes.length > 0}
<FileListNodeContent {fileState} {node} part="routes" />
{:else}
<FileListNodeContent {fileState} {node} />
{/if}
</div>
{/snippet}
</CollapsibleTreeNode>
@@ -10,16 +10,21 @@
let {
fileState,
node,
waypointRoot = false,
part = 'tracks',
}: {
/** State of the file of the node (not needed for the root). */
fileState?: FileState;
/** The node whose children are listed, `null` for the list of the files. */
node: FileTreeNode | null;
/** List the node standing for the waypoints of the file, instead of its tracks. */
waypointRoot?: boolean;
/**
* For a file, which of its lists this is: its routes, its tracks, or the node standing for
* its waypoints. Each one is followed by the next ones.
*/
part?: 'routes' | 'tracks' | 'waypoints';
} = $props();
let waypointRoot = part === 'waypoints';
const { files, order } = engine;
let container: HTMLElement;
@@ -27,9 +32,11 @@
node === null
? 'file'
: node.type === 'file'
? waypointRoot
? part === 'waypoints'
? 'waypoints'
: 'track'
: part === 'routes'
? 'route'
: 'track'
: node.type === 'waypoints'
? 'waypoint'
: 'segment';
@@ -74,7 +81,7 @@
{/each}
{:else if fileState}
{#if node.type === 'file'}
{#if waypointRoot}
{#if part === 'waypoints'}
{#if fileState.structure.waypoints.length > 0}
<div data-id="waypoints">
<FileListNode
@@ -83,6 +90,15 @@
/>
</div>
{/if}
{:else if part === 'routes'}
{#each fileState.structure.routes as route (route.id)}
<div data-id={route.id}>
<FileListNode
{fileState}
node={{ type: 'route', fileId: node.fileId, routeId: route.id }}
/>
</div>
{/each}
{:else}
{#each fileState.structure.tracks as track (track.id)}
<div data-id={track.id}>
@@ -120,8 +136,10 @@
{/if}
</div>
{#if fileState && node?.type === 'file' && !waypointRoot}
<FileListNodeContent {fileState} {node} waypointRoot={true} />
{#if fileState && node?.type === 'file' && part === 'routes'}
<FileListNodeContent {fileState} {node} part="tracks" />
{:else if fileState && node?.type === 'file' && part === 'tracks'}
<FileListNodeContent {fileState} {node} part="waypoints" />
{/if}
<style lang="postcss">
@@ -18,6 +18,7 @@
Scissors,
FileStack,
FileX,
Route,
} from '@lucide/svelte';
import { getContext, untrack } from 'svelte';
import { i18n } from '$lib/i18n/i18n.svelte';
@@ -34,6 +35,7 @@
elementId,
isInClipboard,
isSelected,
pathInfo,
selectedElementIds,
selectedFileIds,
selectionSize,
@@ -62,20 +64,23 @@
let singleSelection = $derived(selectionSize($selection) === 1);
// a route or a track: it has a name, a description and a style, like a file
let isPath = $derived(node.type === 'route' || node.type === 'track');
let nodeColors: string[] = $derived.by(() => {
const { tracks } = fileState.structure;
const { routes, tracks } = fileState.structure;
if (node.type === 'file') {
// the colors defined by the tracks, or else the one of the file
// the colors defined by the routes and tracks, or else the one of the file
const colors = [
...new Set(
tracks.flatMap((track) =>
track.color !== undefined ? [normalizeColor(track.color)] : []
[...routes, ...tracks].flatMap((path) =>
path.color !== undefined ? [normalizeColor(path.color)] : []
)
),
];
return colors.length > 0 ? colors : [fileState.color];
} else if (node.type === 'track') {
const color = tracks.find((track) => track.id === node.trackId)?.color;
} else if (isPath) {
const color = pathInfo(fileState.structure, node)?.color;
return [color !== undefined ? normalizeColor(color) : fileState.color];
}
return [];
@@ -141,11 +146,11 @@
? 'w-full h-7'
: 'w-max h-9 px-1.5'} pointer-events-auto"
>
{#if node.type === 'file' || node.type === 'track'}
{#if node.type === 'file' || isPath}
<MetadataDialog bind:open={openEditMetadata} {fileState} {node} />
<StyleDialog bind:open={openEditStyle} {fileState} {node} />
{/if}
{#if level === 'file' || level === 'track'}
{#if level === 'file' || isPath}
<div
class="absolute {orientation === 'vertical'
? 'top-0 bottom-0 right-0 w-1'
@@ -197,7 +202,9 @@
}
}}
>
{#if level === 'segment'}
{#if level === 'route'}
<Route size="16" class="mx-1 shrink-0" />
{:else if level === 'segment'}
<Waypoints size="16" class="mx-1 shrink-0" />
{:else if level === 'waypoint'}
{#if symbolKey && symbols[symbolKey].icon}
@@ -238,7 +245,7 @@
</Button>
</ContextMenu.Trigger>
<ContextMenu.Content>
{#if node.type === 'file' || node.type === 'track'}
{#if node.type === 'file' || isPath}
<ContextMenu.Item
disabled={!singleSelection}
onclick={() => (editMetadata.current = true)}
@@ -269,6 +276,10 @@
<Plus size="16" />
{i18n._('menu.new_track')}
</ContextMenu.Item>
<ContextMenu.Item disabled={!singleSelection} onclick={() => engine.newRoute()}>
<Plus size="16" />
{i18n._('menu.new_route')}
</ContextMenu.Item>
<ContextMenu.Separator />
{:else if node.type === 'track'}
<ContextMenu.Item
@@ -8,7 +8,7 @@
import { i18n } from '$lib/i18n/i18n.svelte';
import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';
import { engine, type FileState } from '$lib/engine';
import type { FileTreeNode } from '$lib/engine/selection-helpers';
import { pathInfo, type FileTreeNode } from '$lib/engine/selection-helpers';
import { showEngineError } from '$lib/actions/file-actions';
let {
@@ -21,16 +21,13 @@
open: boolean;
} = $props();
let track = $derived(
node.type === 'track'
? fileState.structure.tracks.find((track) => track.id === node.trackId)
: undefined
);
// the route or the track
let path = $derived(pathInfo(fileState.structure, node));
let name: string = $derived(
node.type === 'file' ? fileState.structure.name : (track?.name ?? '')
node.type === 'file' ? fileState.structure.name : (path?.name ?? '')
);
let description: string = $derived(
node.type === 'file' ? (fileState.structure.desc ?? '') : (track?.desc ?? '')
node.type === 'file' ? (fileState.structure.desc ?? '') : (path?.desc ?? '')
);
$effect(() => {
@@ -5,13 +5,22 @@ import { tick } from 'svelte';
import { engine, type MoveTarget, type Selection } from '$lib/engine';
import { isSelected, type FileTreeNode } from '$lib/engine/selection-helpers';
/** What a list holds: the files, the tracks of a file, the segments of a track, or the waypoints. */
export type ListLevel = 'file' | 'track' | 'segment' | 'waypoints' | 'waypoint';
/**
* What a list holds: the files, the routes or the tracks of a file, the segments of a track, or the
* waypoints.
*/
export type ListLevel = 'file' | 'route' | 'track' | 'segment' | 'waypoints' | 'waypoint';
/**
* Where the elements of a list can be moved to (a segment on its own can become a route). Routes
* and tracks can be dropped among each other, but keep their kind: the engine puts them with the
* others of their kind, the routes coming first.
*/
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
file: ['file'],
track: ['file', 'track'],
segment: ['file', 'track', 'segment'],
route: ['file', 'route', 'track'],
track: ['file', 'route', 'track'],
segment: ['file', 'route', 'track', 'segment'],
waypoints: ['waypoints'],
waypoint: ['waypoints', 'waypoint'],
};
@@ -103,6 +112,8 @@ export class SortableFileList {
switch (this._sortableLevel) {
case 'file':
return { type: 'file', fileId: id };
case 'route':
return parent ? { type: 'route', fileId: parent.fileId, routeId: id } : undefined;
case 'track':
return parent ? { type: 'track', fileId: parent.fileId, trackId: id } : undefined;
case 'segment':
@@ -129,6 +140,8 @@ export class SortableFileList {
switch (this._sortableLevel) {
case 'file':
return { type: 'file', fileIds: ids };
case 'route':
return parent ? { type: 'route', fileId: parent.fileId, routeIds: ids } : undefined;
case 'track':
return parent ? { type: 'track', fileId: parent.fileId, trackIds: ids } : undefined;
case 'segment':
@@ -155,6 +168,8 @@ export class SortableFileList {
switch (this._sortableLevel) {
case 'file':
return { type: 'files', index };
case 'route':
return parent ? { type: 'routes', fileId: parent.fileId, index } : undefined;
case 'track':
return parent ? { type: 'tracks', fileId: parent.fileId, index } : undefined;
case 'segment':
@@ -275,6 +290,9 @@ export class SortableFileList {
case 'file':
request = engine.select(selected);
break;
case 'route':
request = engine.selectRoutes(parent!.fileId, selected);
break;
case 'track':
request = engine.selectTracks(parent!.fileId, selected);
break;
@@ -10,7 +10,7 @@
import { settings } from '$lib/state/settings';
import { engine, type FileState } from '$lib/engine';
import { normalizeColor } from '$lib/engine/file-colors';
import { selectedFileIds, type FileTreeNode } from '$lib/engine/selection-helpers';
import { pathInfo, selectedFileIds, type FileTreeNode } from '$lib/engine/selection-helpers';
import { untrack } from 'svelte';
let {
@@ -38,17 +38,15 @@
width = $defaultWidth;
color = fileState.color;
const { tracks } = fileState.structure;
const paths = [...fileState.structure.routes, ...fileState.structure.tracks];
const path = pathInfo(fileState.structure, node);
if (node.type === 'file') {
opacity = tracks.find((track) => track.opacity !== undefined)?.opacity ?? opacity;
width = tracks.find((track) => track.width !== undefined)?.width ?? width;
} else if (node.type === 'track') {
const track = tracks.find((track) => track.id === node.trackId);
if (track) {
color = track.color !== undefined ? normalizeColor(track.color) : color;
opacity = track.opacity ?? opacity;
width = track.width ?? width;
}
opacity = paths.find((path) => path.opacity !== undefined)?.opacity ?? opacity;
width = paths.find((path) => path.width !== undefined)?.width ?? width;
} else if (path) {
color = path.color !== undefined ? normalizeColor(path.color) : color;
opacity = path.opacity ?? opacity;
width = path.width ?? width;
}
colorChanged = false;
@@ -118,7 +118,11 @@ export class GPXLayer {
return { type: 'file', fileId: this.fileId };
}
/** The node of a segment of the map: its route, which has no other, or the segment itself. */
segmentNode(properties: GeoJSON.GeoJsonProperties): FileTreeNode {
if (properties!.routeId !== undefined) {
return { type: 'route', fileId: this.fileId, routeId: properties!.routeId };
}
return {
type: 'segment',
fileId: this.fileId,
@@ -181,7 +185,11 @@ export class GPXLayer {
const visibleSegmentIds = state.segments.features
.filter(
({ properties }) =>
!isSegmentHidden(state, properties.trackId, properties.segmentId)
!isSegmentHidden(
state,
properties.routeId ?? properties.trackId!,
properties.segmentId
)
)
.map(({ properties }) => properties.segmentId);
const segmentFilter: FilterSpecification = [
@@ -415,7 +423,7 @@ export class GPXLayer {
layerOnClick(e: MapLayerMouseEvent) {
const properties = e.features![0].properties!;
const { trackId, segmentId } = properties;
const { routeId, trackId, segmentId } = properties;
const selectionType = this.selectionState.type;
if (
@@ -441,10 +449,15 @@ export class GPXLayer {
}
const mode = e.originalEvent.ctrlKey || e.originalEvent.metaKey ? 'add' : 'replace';
const { tracks } = get(this.file).structure;
const segmentCount = tracks.reduce((count, track) => count + track.segments.length, 0);
const { routes, tracks } = get(this.file).structure;
const segmentCount =
routes.length + tracks.reduce((count, track) => count + track.segments.length, 0);
if (get(treeFileView) && segmentCount > 1) {
// Select inner item
if (routeId !== undefined) {
engine.selectRoutes(this.fileId, [routeId], mode);
return;
}
const track = tracks.find((track) => track.id === trackId);
if (track && track.segments.length > 1) {
engine.selectSegments(this.fileId, trackId, [segmentId], mode);
@@ -15,13 +15,14 @@
const files = engine.files;
const statistics = engine.statistics;
/** The number of segments of a file or of one of its tracks. */
/** The number of segments of a file (its routes have one each) or of one of its tracks. */
function segmentCount(fileId: string, trackId?: string) {
const file = $files.get(fileId);
const tracks = file ? get(file).structure.tracks : [];
return tracks
const structure = file ? get(file).structure : undefined;
const routes = trackId === undefined ? (structure?.routes.length ?? 0) : 0;
return (structure?.tracks ?? [])
.filter((track) => trackId === undefined || track.id === trackId)
.reduce((count, track) => count + track.segments.length, 0);
.reduce((count, track) => count + track.segments.length, routes);
}
// files or tracks, which all have several segments
@@ -31,11 +31,13 @@
onDestroy(() => request.release());
let segmentCount = $derived($statistics.anchors?.segmentStarts.length ?? 0);
// several files, tracks or segments, or a single file or track with several segments
// several files, routes, tracks or segments, or a single file or track with several segments
let canMergeTraces = $derived.by(() => {
switch ($selection.type) {
case 'file':
return $selection.fileIds.length > 1 || segmentCount > 1;
case 'route':
return $selection.routeIds.length > 1;
case 'track':
return $selection.trackIds.length > 1 || segmentCount > 1;
case 'segment':
@@ -45,7 +47,8 @@
}
});
// several files or tracks, that can be put together
// several files or tracks, that can be put together (a route has a single segment: routes
// cannot)
let canMergeContents = $derived(
($selection.type === 'file' && $selection.fileIds.length > 1) ||
($selection.type === 'track' && $selection.trackIds.length > 1)
@@ -172,9 +172,10 @@
updateEnd();
}
// a single file, track or segment
// a single file, route, track or segment
let canUpdate = $derived(
($selection.type === 'file' && $selection.fileIds.length === 1) ||
($selection.type === 'route' && $selection.routeIds.length === 1) ||
($selection.type === 'track' && $selection.trackIds.length === 1) ||
($selection.type === 'segment' && $selection.segmentIds.length === 1)
);
@@ -9,6 +9,7 @@
import { onDestroy } from 'svelte';
import { getURLForLanguage } from '$lib/utils';
import { engine } from '$lib/engine';
import { coversSegments } from '$lib/engine/selection-helpers';
import { minTolerance, ReducedLayer, tolerance } from './utils.svelte';
let props: { class?: string } = $props();
@@ -23,9 +24,7 @@
const request = engine.requestStatistics();
request.set(['anchors']);
let validSelection = $derived(
$selection.type === 'file' || $selection.type === 'track' || $selection.type === 'segment'
);
let validSelection = $derived(coversSegments($selection));
let reducedLayer = new ReducedLayer();
@@ -31,6 +31,7 @@
import { settings } from '$lib/state/settings';
import { map } from '$lib/components/map/map';
import { engine } from '$lib/engine';
import { coversSegments } from '$lib/engine/selection-helpers';
import { newFileName } from '$lib/actions/file-actions';
import { mapCursor, MapCursorState } from '$lib/state/map-cursor';
import { RoutingControls } from './routing-controls';
@@ -54,10 +55,8 @@
const selection = engine.selection;
let routingControls: RoutingControls | undefined = undefined;
// the tool works on files, tracks and segments, not on waypoints
let validSelection = $derived(
$selection.type === 'file' || $selection.type === 'track' || $selection.type === 'segment'
);
// the tool works on files, routes, tracks and segments, not on waypoints
let validSelection = $derived(coversSegments($selection));
function createFileWithPoint(e: any) {
if ($selection.type === 'empty') {
@@ -18,6 +18,7 @@ import { map } from '$lib/components/map/map';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
import { settings } from '$lib/state/settings';
import { closestPointIndexIn } from '$lib/geo/closest-point';
import { coversSegments } from '$lib/engine/selection-helpers';
import {
engine,
type Selection,
@@ -53,7 +54,7 @@ type AnchorFeature = GeoJSON.Feature<GeoJSON.Point, AnchorProperties>;
/** Whether the tool has to show controls for a selection: it covers segments, or can get some. */
function isRoutable(selection: Selection) {
return selection.type === 'file' || selection.type === 'track' || selection.type === 'segment';
return coversSegments(selection);
}
/**
@@ -1,5 +1,5 @@
<script lang="ts">
import { splitAs, SplitType } from '$lib/components/toolbar/tools/scissors/scissors';
import { availableSplitTypes, splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
import Help from '$lib/components/Help.svelte';
import { Label } from '$lib/components/ui/label/index.js';
import { Button } from '$lib/components/ui/button';
@@ -14,6 +14,7 @@
import { getURLForLanguage } from '$lib/utils';
import { slicedStatistics } from '$lib/state/selection-statistics';
import { engine } from '$lib/engine';
import { coversSegments } from '$lib/engine/selection-helpers';
let props: {
class?: string;
@@ -24,13 +25,20 @@
const selection = engine.selection;
const statistics = engine.statistics;
// the tool works on files, tracks and segments, not on waypoints
let validSelection = $derived(
($selection.type === 'file' ||
$selection.type === 'track' ||
$selection.type === 'segment') &&
$statistics.length > 0
);
// the ways the selection can be split, which follow its content
let splitTypes = $derived.by(() => {
// what is selected can change without the selection: the files are edited
void $statistics;
return availableSplitTypes($selection);
});
$effect(() => {
if (!splitTypes.includes($splitAs)) {
$splitAs = splitTypes[0];
}
});
// the tool works on files, routes, tracks and segments, not on waypoints
let validSelection = $derived(coversSegments($selection) && $statistics.length > 0);
// The slider does work for each of its steps every time it changes, which is too slow with
// the thousands of trackpoints of a long track: it has a limited number of steps, which are
// spread over the trackpoints.
@@ -123,7 +131,7 @@
{i18n._('gpx.' + $splitAs)}
</Select.Trigger>
<Select.Content>
{#each Object.values(SplitType) as splitType}
{#each splitTypes as splitType}
<Select.Item value={splitType}>{i18n._('gpx.' + splitType)}</Select.Item>
{/each}
</Select.Content>
@@ -1,10 +1,12 @@
import { get, writable, type Writable } from 'svelte/store';
import { engine, type SplitType as EngineSplitType } from '$lib/engine';
import { engine, type Selection, type SplitType as EngineSplitType } from '$lib/engine';
import { selectedFileIds } from '$lib/engine/selection-helpers';
import { closestPointIndexIn } from '$lib/geo/closest-point';
/** What a split cuts in two, the values being the ones of the engine. */
export const SplitType = {
FILES: 'files',
ROUTES: 'routes',
TRACKS: 'tracks',
SEGMENTS: 'segments',
} as const satisfies Record<string, EngineSplitType>;
@@ -12,11 +14,62 @@ export type SplitType = (typeof SplitType)[keyof typeof SplitType];
export let splitAs: Writable<SplitType> = writable(SplitType.FILES);
/** The ids of the segments of the routes of the files of the selection. */
export function routeSegmentIds(selection: Selection): Set<string> {
const files = get(engine.files);
return new Set(
selectedFileIds(selection).flatMap((fileId) => {
const file = files.get(fileId);
return file ? get(file).structure.routes.map((route) => route.segment.id) : [];
})
);
}
/**
* Splits at the trackpoint of a selected segment that is the closest to `point`, as `splitAs` says.
* The ways the selection can be split: as files always, as routes if it has routes, as tracks or
* segments if it has tracks with segments (the engine does nothing for a trackpoint that is not in
* what it is asked to cut).
*/
export function availableSplitTypes(selection: Selection): SplitType[] {
let routes = false;
let tracks = false;
if (selection.type === 'route') {
routes = true;
} else if (selection.type === 'track' || selection.type === 'segment') {
tracks = true;
} else {
const files = get(engine.files);
for (const fileId of selectedFileIds(selection)) {
const file = files.get(fileId);
if (file) {
const { structure } = get(file);
routes ||= structure.routes.length > 0;
tracks ||= structure.tracks.some((track) => track.segments.length > 0);
}
}
}
return [
SplitType.FILES,
...(routes ? [SplitType.ROUTES] : []),
...(tracks ? [SplitType.TRACKS, SplitType.SEGMENTS] : []),
];
}
/** Whether splitting as `type` cuts something at a trackpoint of a segment, which is a route's or not. */
export function cuts(type: SplitType, inRoute: boolean): boolean {
return type === SplitType.FILES || (type === SplitType.ROUTES) === inRoute;
}
/**
* Splits at the trackpoint of a selected segment that is the closest to `point`, as `splitAs` says
* (nothing happens if the segment cannot be split that way).
* The statistics of the engine need to have the anchors (the scissors tool asks for them).
*/
export function splitAtPoint(segmentId: string, point: { lng: number; lat: number }) {
const inRoute = routeSegmentIds(get(engine.selection)).has(segmentId);
if (!cuts(get(splitAs), inRoute)) {
return;
}
const statistics = get(engine.statistics);
const anchors = statistics.anchors;
const segment = anchors?.segmentIds.indexOf(segmentId) ?? -1;
@@ -1,5 +1,5 @@
import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
import { cuts, routeSegmentIds, splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
import { Scissors } from 'lucide-static';
import { get } from 'svelte/store';
import { mapCursor, MapCursorState } from '$lib/state/map-cursor';
@@ -7,6 +7,7 @@ import type { GeoJSONSource, Map, MapLayerMouseEvent } from 'maplibre-gl';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
import { loadSVGIcon } from '$lib/utils';
import { coversSegments } from '$lib/engine/selection-helpers';
import {
engine,
type Selection,
@@ -15,9 +16,9 @@ import {
} from '$lib/engine';
/**
* The anchors inside the segments of the selection, where a click splits the file, the track or
* the segment: a command of the engine, which finds them by the index of their trackpoint in the
* selection.
* The anchors inside the segments of the selection, where a click splits the file, the route, the
* track or the segment: a command of the engine, which finds them by the index of their trackpoint
* in the selection. Only the segments that can be split as `splitAs` says have them.
*/
export class SplitControls {
map: Map;
@@ -53,6 +54,7 @@ export class SplitControls {
})
);
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
this.unsubscribes.push(splitAs.subscribe(this.addIfNeeded.bind(this)));
this.unsubscribes.push(
engine.selection.subscribe((selection) => {
this.selection = selection;
@@ -62,12 +64,8 @@ export class SplitControls {
}
addIfNeeded() {
// the tool works on files, tracks and segments, not on waypoints
const scissors =
get(currentTool) === Tool.SCISSORS &&
(this.selection.type === 'file' ||
this.selection.type === 'track' ||
this.selection.type === 'segment');
// the tool works on files, routes, tracks and segments, not on waypoints
const scissors = get(currentTool) === Tool.SCISSORS && coversSegments(this.selection);
this.request.set(scissors ? ['anchors'] : []);
if (!scissors) {
this.remove();
@@ -85,6 +83,8 @@ export class SplitControls {
return [];
}
const starts = anchors.segmentStarts;
const routeSegments = routeSegmentIds(this.selection);
const type = get(splitAs);
const features: GeoJSON.Feature[] = [];
let segment = 0;
for (let i = 0; i < anchors.indices.length; i++) {
@@ -94,7 +94,8 @@ export class SplitControls {
segment++;
}
const end = segment + 1 < starts.length ? starts[segment + 1] : statistics.length;
if (index > starts[segment] && index < end - 1) {
const inRoute = routeSegments.has(anchors.segmentIds[segment]);
if (index > starts[segment] && index < end - 1 && cuts(type, inRoute)) {
features.push({
type: 'Feature',
geometry: {