From e88dbafead3a34c163eb538353403bd50a8ca463 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Tue, 16 Jul 2024 12:17:23 +0200 Subject: [PATCH] enable routing tool without selection, and support multi-select --- gpx/src/gpx.ts | 46 ++++++++++++--- .../src/lib/components/file-list/Selection.ts | 13 +++-- .../toolbar/tools/routing/Routing.svelte | 56 ++++++++++++++----- .../toolbar/tools/routing/RoutingControls.ts | 45 +++------------ website/src/lib/db.ts | 26 +++++++++ website/src/locales/en.json | 3 +- 6 files changed, 124 insertions(+), 65 deletions(-) diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index f146bc97..1026c7cd 100644 --- a/gpx/src/gpx.ts +++ b/gpx/src/gpx.ts @@ -14,7 +14,7 @@ export abstract class GPXTreeElement> { _data: { [key: string]: any } = {}; abstract isLeaf(): boolean; - abstract get children(): ReadonlyArray; + abstract get children(): Array; abstract getNumberOfTrackPoints(): number; abstract getStartTimestamp(): Date | undefined; @@ -74,16 +74,15 @@ abstract class GPXTreeNode> extends GPXTreeElement newPreviousTimestamp = og.getStartTimestamp(); } - let children = og.children.slice(); - children.reverse(); + this.children.reverse(); for (let i = 0; i < og.children.length; i++) { let originalStartTimestamp = og.children[og.children.length - i - 1].getStartTimestamp(); - children[i]._reverse(originalNextTimestamp, newPreviousTimestamp); + this.children[i]._reverse(originalNextTimestamp, newPreviousTimestamp); originalNextTimestamp = originalStartTimestamp; - newPreviousTimestamp = children[i].getEndTimestamp(); + newPreviousTimestamp = this.children[i].getEndTimestamp(); } if (this instanceof GPXFile) { @@ -102,7 +101,7 @@ abstract class GPXTreeLeaf extends GPXTreeElement { return true; } - get children(): ReadonlyArray { + get children(): Array { return []; } } @@ -149,7 +148,7 @@ export class GPXFile extends GPXTreeNode{ }); } - get children(): ReadonlyArray { + get children(): Array { return this.trk; } @@ -246,6 +245,20 @@ export class GPXFile extends GPXTreeNode{ this.trk[trackIndex].reverseTrackSegment(segmentIndex); } + roundTrip() { + this.trk.forEach((track) => { + track.roundTrip(); + }); + } + + roundTripTrack(trackIndex: number) { + this.trk[trackIndex].roundTrip(); + } + + roundTripTrackSegment(trackIndex: number, segmentIndex: number) { + this.trk[trackIndex].roundTripTrackSegment(segmentIndex); + } + crop(start: number, end: number, trackIndices?: number[], segmentIndices?: number[]) { let i = 0; let trackIndex = 0; @@ -404,7 +417,7 @@ export class Track extends GPXTreeNode { } } - get children(): ReadonlyArray { + get children(): Array { return this.trkseg; } @@ -470,6 +483,16 @@ export class Track extends GPXTreeNode { this.trkseg[segmentIndex]._reverse(this.trkseg[segmentIndex].getEndTimestamp(), this.trkseg[segmentIndex].getStartTimestamp()); } + roundTrip() { + this.trkseg.forEach((segment) => { + segment.roundTrip(); + }); + } + + roundTripTrackSegment(segmentIndex: number) { + this.trkseg[segmentIndex].roundTrip(); + } + crop(start: number, end: number, segmentIndices?: number[]) { let i = 0; let segmentIndex = 0; @@ -826,6 +849,13 @@ export class TrackSegment extends GPXTreeLeaf { } } + roundTrip() { + let og = getOriginal(this); // Read as much as possible from the original object because it is faster + let newSegment = og.clone(); + newSegment._reverse(newSegment.getEndTimestamp(), newSegment.getEndTimestamp()); + this.replaceTrackPoints(this.trkpt.length, this.trkpt.length, newSegment.trkpt); + } + crop(start: number, end: number) { this.trkpt = this.trkpt.slice(start, end + 1); } diff --git a/website/src/lib/components/file-list/Selection.ts b/website/src/lib/components/file-list/Selection.ts index c583d044..7b31b6b4 100644 --- a/website/src/lib/components/file-list/Selection.ts +++ b/website/src/lib/components/file-list/Selection.ts @@ -102,10 +102,7 @@ export class SelectionTreeType { return false; } - getSelected(selection?: ListItem[]): ListItem[] { - if (selection === undefined) { - selection = []; - } + getSelected(selection: ListItem[] = []): ListItem[] { if (this.selected) { selection.push(this.item); } @@ -200,6 +197,14 @@ export function selectAll() { }); } +export function getOrderedSelection(reverse: boolean = false): ListItem[] { + let selected: ListItem[] = []; + applyToOrderedSelectedItemsFromFile((fileId, level, items) => { + selected.push(...items); + }, reverse); + return selected; +} + export function applyToOrderedItemsFromFile(selectedItems: ListItem[], callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) { get(settings.fileOrder).forEach((fileId) => { let level: ListLevel | undefined = undefined; diff --git a/website/src/lib/components/toolbar/tools/routing/Routing.svelte b/website/src/lib/components/toolbar/tools/routing/Routing.svelte index a9a6e698..e66f3be2 100644 --- a/website/src/lib/components/toolbar/tools/routing/Routing.svelte +++ b/website/src/lib/components/toolbar/tools/routing/Routing.svelte @@ -21,7 +21,7 @@ } from 'lucide-svelte'; import { map, newGPXFile, routingControls, selectFileWhenLoaded } from '$lib/stores'; - import { dbUtils, getFileIds, settings } from '$lib/db'; + import { dbUtils, getFile, getFileIds, settings } from '$lib/db'; import { brouterProfiles, routingProfileSelectItem } from './Routing'; import { _ } from 'svelte-i18n'; @@ -30,9 +30,15 @@ import mapboxgl from 'mapbox-gl'; import { fileObservers } from '$lib/db'; import { slide } from 'svelte/transition'; - import { selection } from '$lib/components/file-list/Selection'; - import { ListRootItem, type ListItem } from '$lib/components/file-list/FileList'; - import { flyAndScale } from '$lib/utils'; + import { getOrderedSelection, selection } from '$lib/components/file-list/Selection'; + import { + ListFileItem, + ListRootItem, + ListTrackItem, + ListTrackSegmentItem, + type ListItem + } from '$lib/components/file-list/FileList'; + import { flyAndScale, resetCursor, setCrosshairCursor } from '$lib/utils'; import { onDestroy, onMount } from 'svelte'; import { TrackPoint } from 'gpx'; @@ -86,10 +92,12 @@ } onMount(() => { + setCrosshairCursor(); $map?.on('click', createFileWithPoint); }); onDestroy(() => { + resetCursor(); $map?.off('click', createFileWithPoint); routingControls.forEach((controls) => controls.destroy()); @@ -178,10 +186,33 @@ slot="data" variant="outline" class="flex flex-row gap-1 text-xs px-2" - disabled={$selection.size != 1 || !validSelection} + disabled={!validSelection} on:click={() => { - const fileId = get(selection).getSelected()[0].getFileId(); - routingControls.get(fileId)?.routeToStart(); + const selected = getOrderedSelection(); + if (selected.length > 0) { + const firstFileId = selected[0].getFileId(); + const firstFile = getFile(firstFileId); + if (firstFile) { + let start = (() => { + if (selected[0] instanceof ListFileItem) { + return firstFile.trk[0]?.trkseg[0]?.trkpt[0]; + } else if (selected[0] instanceof ListTrackItem) { + return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[0]?.trkpt[0]; + } else if (selected[0] instanceof ListTrackSegmentItem) { + return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[ + selected[0].getSegmentIndex() + ]?.trkpt[0]; + } + })(); + + if (start !== undefined) { + const lastFileId = selected[selected.length - 1].getFileId(); + routingControls + .get(lastFileId) + ?.appendAnchorWithCoordinates(start.getCoordinates()); + } + } + } }} > {$_('toolbar.routing.route_back_to_start.button')} @@ -193,11 +224,8 @@ slot="data" variant="outline" class="flex flex-row gap-1 text-xs px-2" - disabled={$selection.size != 1 || !validSelection} - on:click={() => { - const fileId = get(selection).getSelected()[0].getFileId(); - routingControls.get(fileId)?.createRoundTrip(); - }} + disabled={!validSelection} + on:click={dbUtils.createRoundTripForSelection} > {$_('toolbar.routing.round_trip.button')} @@ -206,9 +234,7 @@
- {#if $selection.size > 1} -
{$_('toolbar.routing.help_multiple_files')}
- {:else if $selection.size == 0 || !validSelection} + {#if !validSelection}
{$_('toolbar.routing.help_no_file')}
{:else}
{$_('toolbar.routing.help')}
diff --git a/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts b/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts index 8ac1d835..fbc2bcb6 100644 --- a/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts +++ b/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts @@ -7,10 +7,10 @@ import { toast } from "svelte-sonner"; import { _ } from "svelte-i18n"; import { dbUtils, type GPXFileWithStatistics } from "$lib/db"; -import { selection } from "$lib/components/file-list/Selection"; +import { getOrderedSelection, selection } from "$lib/components/file-list/Selection"; import { ListFileItem, ListTrackItem, ListTrackSegmentItem } from "$lib/components/file-list/FileList"; import { currentTool, streetViewEnabled, Tool } from "$lib/stores"; -import { resetCursor, setCrosshairCursor, setGrabbingCursor } from "$lib/utils"; +import { resetCursor, setGrabbingCursor } from "$lib/utils"; export const canChangeStart = writable(false); @@ -61,7 +61,7 @@ export class RoutingControls { return; } - let selected = get(selection).hasAnyChildren(new ListFileItem(this.fileId), true, ['waypoints']) && get(selection).size == 1; + let selected = get(selection).hasAnyChildren(new ListFileItem(this.fileId), true, ['waypoints']); if (selected) { if (this.active) { this.updateControls(); @@ -80,7 +80,6 @@ export class RoutingControls { this.map.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded); this.map.on('click', this.appendAnchorBinded); this.map.on('mousemove', this.fileId, this.showTemporaryAnchorBinded); - setCrosshairCursor(); this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this)); } @@ -130,7 +129,6 @@ export class RoutingControls { this.map.off('mousemove', this.fileId, this.showTemporaryAnchorBinded); this.map.off('mousemove', this.updateTemporaryAnchorBinded); this.temporaryAnchor.marker.remove(); - resetCursor(); this.fileUnsubscribe(); } @@ -398,6 +396,12 @@ export class RoutingControls { } async appendAnchorWithCoordinates(coordinates: Coordinates) { // Add a new anchor to the end of the last segment + let selected = getOrderedSelection(); + if (selected.length === 0 || selected[selected.length - 1].getFileId() !== this.fileId) { + return; + } + let item = selected[selected.length - 1]; + let lastAnchor = this.anchors[this.anchors.length - 1]; let newPoint = new TrackPoint({ @@ -408,7 +412,6 @@ export class RoutingControls { if (!lastAnchor) { dbUtils.applyToFile(this.fileId, (file) => { - let item = get(selection).getSelected()[0]; let trackIndex = file.trk.length > 0 ? file.trk.length - 1 : 0; if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) { trackIndex = item.getTrackIndex(); @@ -443,36 +446,6 @@ export class RoutingControls { await this.routeBetweenAnchors([lastAnchor, newAnchor], [lastAnchor.point.getCoordinates(), newAnchor.point.getCoordinates()]); } - routeToStart() { - if (this.anchors.length === 0) { - return; - } - - let lastAnchor = this.anchors[this.anchors.length - 1]; - let firstAnchor = this.anchors.find((anchor) => anchor.segment === lastAnchor.segment); - - if (!firstAnchor) { - return; - } - - this.appendAnchorWithCoordinates(firstAnchor.point.getCoordinates()); - } - - createRoundTrip() { - if (this.anchors.length === 0) { - return; - } - - let lastAnchor = this.anchors[this.anchors.length - 1]; - - let segment = lastAnchor.segment; - dbUtils.applyToFile(this.fileId, (file) => { - let newSegment = segment.clone(); - newSegment._reverse(segment.getEndTimestamp(), segment.getEndTimestamp()); - file.replaceTrackPoints(lastAnchor.trackIndex, lastAnchor.segmentIndex, segment.trkpt.length, segment.trkpt.length, newSegment.trkpt.map((point) => point)); - }); - } - getNeighbouringAnchors(anchor: Anchor): [Anchor | null, Anchor | null] { let previousAnchor: Anchor | null = null; let nextAnchor: Anchor | null = null; diff --git a/website/src/lib/db.ts b/website/src/lib/db.ts index 3f44184f..e5b42575 100644 --- a/website/src/lib/db.ts +++ b/website/src/lib/db.ts @@ -532,6 +532,32 @@ export const dbUtils = { }); }); }, + createRoundTripForSelection() { + if (!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) { + return; + } + applyGlobal((draft) => { + applyToOrderedSelectedItemsFromFile((fileId, level, items) => { + let file = draft.get(fileId); + if (file) { + if (level === ListLevel.FILE) { + file.roundTrip(); + } else if (level === ListLevel.TRACK) { + for (let item of items) { + let trackIndex = (item as ListTrackItem).getTrackIndex(); + file.roundTripTrack(trackIndex); + } + } else if (level === ListLevel.SEGMENT) { + for (let item of items) { + let trackIndex = (item as ListTrackSegmentItem).getTrackIndex(); + let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex(); + file.roundTripTrackSegment(trackIndex, segmentIndex); + } + } + } + }); + }); + }, mergeSelection: (mergeTraces: boolean) => { applyGlobal((draft) => { let first = true; diff --git a/website/src/locales/en.json b/website/src/locales/en.json index 2e16fad5..dc812d20 100644 --- a/website/src/locales/en.json +++ b/website/src/locales/en.json @@ -94,8 +94,7 @@ "tooltip": "Return to the starting point by the same route" }, "start_loop_here": "Start loop here", - "help_no_file": "Select a file item to use the routing tool, or create a new file from the menu.", - "help_multiple_files": "Select a single file item to use the routing tool.", + "help_no_file": "Select a file item to use the routing tool, or click on the map to start creating a new route.", "help": "Click on the map to add a new anchor point, or drag existing ones to change the route.", "activities": { "bike": "Bike",