mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-31 15:43:25 +00:00
enable routing tool without selection, and support multi-select
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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",
|
||||
|
Reference in New Issue
Block a user