enable routing tool without selection, and support multi-select

This commit is contained in:
vcoppe
2024-07-16 12:17:23 +02:00
parent 7f143bf843
commit e88dbafead
6 changed files with 124 additions and 65 deletions

View File

@@ -14,7 +14,7 @@ export abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
_data: { [key: string]: any } = {};
abstract isLeaf(): boolean;
abstract get children(): ReadonlyArray<T>;
abstract get children(): Array<T>;
abstract getNumberOfTrackPoints(): number;
abstract getStartTimestamp(): Date | undefined;
@@ -74,16 +74,15 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> 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<GPXTreeLeaf> {
return true;
}
get children(): ReadonlyArray<GPXTreeLeaf> {
get children(): Array<GPXTreeLeaf> {
return [];
}
}
@@ -149,7 +148,7 @@ export class GPXFile extends GPXTreeNode<Track>{
});
}
get children(): ReadonlyArray<Track> {
get children(): Array<Track> {
return this.trk;
}
@@ -246,6 +245,20 @@ export class GPXFile extends GPXTreeNode<Track>{
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<TrackSegment> {
}
}
get children(): ReadonlyArray<TrackSegment> {
get children(): Array<TrackSegment> {
return this.trkseg;
}
@@ -470,6 +483,16 @@ export class Track extends GPXTreeNode<TrackSegment> {
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);
}

View File

@@ -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;

View File

@@ -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());
}
}
}
}}
>
<Home size="12" />{$_('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}
>
<Repeat size="12" />{$_('toolbar.routing.round_trip.button')}
</Button>
@@ -206,9 +234,7 @@
</div>
<div class="w-full flex flex-row gap-2 items-end justify-between">
<Help>
{#if $selection.size > 1}
<div>{$_('toolbar.routing.help_multiple_files')}</div>
{:else if $selection.size == 0 || !validSelection}
{#if !validSelection}
<div>{$_('toolbar.routing.help_no_file')}</div>
{:else}
<div>{$_('toolbar.routing.help')}</div>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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",