mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-02 08:42:31 +00:00
move tracks, segments and waypoints within same list
This commit is contained in:
@@ -185,6 +185,18 @@ export class GPXFile extends GPXTreeNode<Track>{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveTracks(indices: number[], dest: number) {
|
||||||
|
return produce(this, (draft) => {
|
||||||
|
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
||||||
|
let trk = og.trk.slice();
|
||||||
|
let tracks = indices.map((index) => trk[index]);
|
||||||
|
indices.sort((a, b) => b - a);
|
||||||
|
indices.forEach((index) => trk.splice(index, 1));
|
||||||
|
trk.splice(dest, 0, ...tracks);
|
||||||
|
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
replaceTrackSegments(trackIndex: number, start: number, end: number, segments: TrackSegment[]) {
|
replaceTrackSegments(trackIndex: number, start: number, end: number, segments: TrackSegment[]) {
|
||||||
return produce(this, (draft) => {
|
return produce(this, (draft) => {
|
||||||
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
||||||
@@ -194,6 +206,15 @@ export class GPXFile extends GPXTreeNode<Track>{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveTrackSegments(trackIndex: number, indices: number[], dest: number) {
|
||||||
|
return produce(this, (draft) => {
|
||||||
|
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
||||||
|
let trk = og.trk.slice();
|
||||||
|
trk[trackIndex] = trk[trackIndex].moveTrackSegments(indices, dest);
|
||||||
|
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
replaceTrackPoints(trackIndex: number, segmentIndex: number, start: number, end: number, points: TrackPoint[]) {
|
replaceTrackPoints(trackIndex: number, segmentIndex: number, start: number, end: number, points: TrackPoint[]) {
|
||||||
return produce(this, (draft) => {
|
return produce(this, (draft) => {
|
||||||
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
||||||
@@ -212,6 +233,18 @@ export class GPXFile extends GPXTreeNode<Track>{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveWaypoints(indices: number[], dest: number) {
|
||||||
|
return produce(this, (draft) => {
|
||||||
|
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
||||||
|
let wpt = og.wpt.slice();
|
||||||
|
let waypoints = indices.map((index) => wpt[index]);
|
||||||
|
indices.sort((a, b) => b - a);
|
||||||
|
indices.forEach((index) => wpt.splice(index, 1));
|
||||||
|
wpt.splice(dest, 0, ...waypoints);
|
||||||
|
draft.wpt = freeze(wpt); // Pre-freeze the array, faster as well
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
reverse() {
|
reverse() {
|
||||||
return this._reverse();
|
return this._reverse();
|
||||||
}
|
}
|
||||||
@@ -326,6 +359,18 @@ export class Track extends GPXTreeNode<TrackSegment> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveTrackSegments(indices: number[], dest: number) {
|
||||||
|
return produce(this, (draft) => {
|
||||||
|
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
||||||
|
let trkseg = og.trkseg.slice();
|
||||||
|
let segments = indices.map((index) => trkseg[index]);
|
||||||
|
indices.sort((a, b) => b - a);
|
||||||
|
indices.forEach((index) => trkseg.splice(index, 1));
|
||||||
|
trkseg.splice(dest, 0, ...segments);
|
||||||
|
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
replaceTrackPoints(segmentIndex: number, start: number, end: number, points: TrackPoint[]) {
|
replaceTrackPoints(segmentIndex: number, start: number, end: number, points: TrackPoint[]) {
|
||||||
return produce(this, (draft) => {
|
return produce(this, (draft) => {
|
||||||
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
||||||
|
@@ -2,12 +2,12 @@
|
|||||||
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
|
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
|
||||||
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
|
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
|
||||||
import Sortable from 'sortablejs/Sortable';
|
import Sortable from 'sortablejs/Sortable';
|
||||||
import { fileObservers, settings, type GPXFileWithStatistics } from '$lib/db';
|
import { dbUtils, fileObservers, settings, type GPXFileWithStatistics } from '$lib/db';
|
||||||
import { get, type Readable } from 'svelte/store';
|
import { get, type Readable } from 'svelte/store';
|
||||||
import FileListNodeStore from './FileListNodeStore.svelte';
|
import FileListNodeStore from './FileListNodeStore.svelte';
|
||||||
import FileListNode from './FileListNode.svelte';
|
import FileListNode from './FileListNode.svelte';
|
||||||
import FileListNodeLabel from './FileListNodeLabel.svelte';
|
import FileListNodeLabel from './FileListNodeLabel.svelte';
|
||||||
import { ListLevel, type ListItem } from './FileList';
|
import { ListLevel, ListTrackItem, type ListItem } from './FileList';
|
||||||
import { selection } from './Selection';
|
import { selection } from './Selection';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
@@ -45,11 +45,10 @@
|
|||||||
selection.update(($selection) => {
|
selection.update(($selection) => {
|
||||||
$selection.clear();
|
$selection.clear();
|
||||||
Object.entries(elements).forEach(([id, element]) => {
|
Object.entries(elements).forEach(([id, element]) => {
|
||||||
let realId =
|
$selection.set(
|
||||||
sortableLevel === ListLevel.FILE || sortableLevel === ListLevel.WAYPOINTS
|
item.extend(getRealId(id)),
|
||||||
? id
|
element.classList.contains('sortable-selected')
|
||||||
: parseInt(id);
|
);
|
||||||
$selection.set(item.extend(realId), element.classList.contains('sortable-selected'));
|
|
||||||
});
|
});
|
||||||
return $selection;
|
return $selection;
|
||||||
});
|
});
|
||||||
@@ -100,32 +99,72 @@
|
|||||||
group: {
|
group: {
|
||||||
name: sortableLevel
|
name: sortableLevel
|
||||||
},
|
},
|
||||||
|
direction: orientation,
|
||||||
forceAutoScrollFallback: true,
|
forceAutoScrollFallback: true,
|
||||||
multiDrag: true,
|
multiDrag: true,
|
||||||
multiDragKey: 'Meta',
|
multiDragKey: 'Meta',
|
||||||
avoidImplicitDeselect: true,
|
avoidImplicitDeselect: true,
|
||||||
onSelect: onSelectChange,
|
onSelect: onSelectChange,
|
||||||
onDeselect: onSelectChange,
|
onDeselect: onSelectChange,
|
||||||
sort: sortableLevel !== ListLevel.WAYPOINT,
|
onSort: (e) => {
|
||||||
onSort: () => {
|
if (sortableLevel === ListLevel.FILE) {
|
||||||
if (sortableLevel !== ListLevel.FILE) {
|
let newFileOrder = sortable.toArray();
|
||||||
return;
|
if (newFileOrder.length !== get(fileOrder).length) {
|
||||||
}
|
|
||||||
|
|
||||||
let newFileOrder = sortable.toArray();
|
|
||||||
if (newFileOrder.length !== get(fileOrder).length) {
|
|
||||||
fileOrder.set(newFileOrder);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < newFileOrder.length; i++) {
|
|
||||||
if (newFileOrder[i] !== get(fileOrder)[i]) {
|
|
||||||
fileOrder.set(newFileOrder);
|
fileOrder.set(newFileOrder);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < newFileOrder.length; i++) {
|
||||||
|
if (newFileOrder[i] !== get(fileOrder)[i]) {
|
||||||
|
fileOrder.set(newFileOrder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let fromItem = Sortable.get(e.from)._item;
|
||||||
|
let toItem = Sortable.get(e.to)._item;
|
||||||
|
let oldIndices =
|
||||||
|
e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex];
|
||||||
|
let newIndices =
|
||||||
|
e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex];
|
||||||
|
oldIndices.sort((a, b) => a - b);
|
||||||
|
newIndices.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
let oldItems = oldIndices.map((i) => item.extend(i));
|
||||||
|
let newItems = newIndices.map((i) => item.extend(i));
|
||||||
|
|
||||||
|
if (fromItem === toItem) {
|
||||||
|
if (sortableLevel === ListLevel.TRACK) {
|
||||||
|
dbUtils.applyToFile(item.getFileId(), (draft) =>
|
||||||
|
draft.moveTracks(oldIndices, newIndices[0])
|
||||||
|
);
|
||||||
|
} else if (item instanceof ListTrackItem) {
|
||||||
|
dbUtils.applyToFile(item.getFileId(), (draft) =>
|
||||||
|
draft.moveTrackSegments(item.getTrackIndex(), oldIndices, newIndices[0])
|
||||||
|
);
|
||||||
|
} else if (sortableLevel === ListLevel.WAYPOINT) {
|
||||||
|
dbUtils.applyToFile(item.getFileId(), (draft) =>
|
||||||
|
draft.moveWaypoints(oldIndices, newIndices[0])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
selection.update(($selection) => {
|
||||||
|
$selection.clear();
|
||||||
|
newItems.forEach((newItem) => {
|
||||||
|
console.log('newItem', newItem);
|
||||||
|
$selection.set(newItem, true);
|
||||||
|
});
|
||||||
|
return $selection;
|
||||||
|
});
|
||||||
|
} else if (item === toItem) {
|
||||||
|
// Move between lists
|
||||||
|
console.log('Move between lists');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Object.defineProperty(sortable, '_item', {
|
||||||
|
value: item
|
||||||
|
});
|
||||||
selection.set(get(selection));
|
selection.set(get(selection));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,6 +193,9 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (sortableLevel !== ListLevel.FILE) {
|
||||||
|
sortable.sort(Object.keys(elements));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const unsubscribe = selection.subscribe(($selection) => {
|
const unsubscribe = selection.subscribe(($selection) => {
|
||||||
@@ -180,10 +222,7 @@
|
|||||||
if (element === null) {
|
if (element === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let realId =
|
let realId = getRealId(id);
|
||||||
sortableLevel === ListLevel.FILE || sortableLevel === ListLevel.WAYPOINTS
|
|
||||||
? id
|
|
||||||
: parseInt(id);
|
|
||||||
let realItem = item.extend(realId);
|
let realItem = item.extend(realId);
|
||||||
let inSelection = get(selection).has(realItem);
|
let inSelection = get(selection).has(realItem);
|
||||||
let isSelected = element.classList.contains('sortable-selected');
|
let isSelected = element.classList.contains('sortable-selected');
|
||||||
@@ -194,6 +233,12 @@
|
|||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRealId(id: string | number) {
|
||||||
|
return sortableLevel === ListLevel.FILE || sortableLevel === ListLevel.WAYPOINTS
|
||||||
|
? id
|
||||||
|
: parseInt(id);
|
||||||
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
});
|
});
|
||||||
@@ -212,26 +257,26 @@
|
|||||||
{:else if node instanceof GPXFile}
|
{:else if node instanceof GPXFile}
|
||||||
{#if waypointRoot}
|
{#if waypointRoot}
|
||||||
{#if node.wpt.length > 0}
|
{#if node.wpt.length > 0}
|
||||||
<div bind:this={elements['waypoints']}>
|
<div bind:this={elements['waypoints']} data-id="waypoints">
|
||||||
<FileListNode node={node.wpt} item={item.extend('waypoints')} />
|
<FileListNode node={node.wpt} item={item.extend('waypoints')} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
{#each node.children as child, i}
|
{#each node.children as child, i}
|
||||||
<div bind:this={elements[i]}>
|
<div bind:this={elements[i]} data-id={i}>
|
||||||
<FileListNode node={child} item={item.extend(i)} />
|
<FileListNode node={child} item={item.extend(i)} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{:else if node instanceof Track}
|
{:else if node instanceof Track}
|
||||||
{#each node.children as child, i}
|
{#each node.children as child, i}
|
||||||
<div bind:this={elements[i]} class="ml-1">
|
<div bind:this={elements[i]} data-id={i} class="ml-1">
|
||||||
<FileListNodeLabel item={item.extend(i)} label={`${$_('gpx.segment')} ${i + 1}`} />
|
<FileListNodeLabel item={item.extend(i)} label={`${$_('gpx.segment')} ${i + 1}`} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else if Array.isArray(node) && node.length > 0 && node[0] instanceof Waypoint}
|
{:else if Array.isArray(node) && node.length > 0 && node[0] instanceof Waypoint}
|
||||||
{#each node as wpt, i}
|
{#each node as wpt, i}
|
||||||
<div bind:this={elements[i]} class="ml-1">
|
<div bind:this={elements[i]} data-id={i} class="ml-1">
|
||||||
<FileListNodeLabel
|
<FileListNodeLabel
|
||||||
item={item.extend(i)}
|
item={item.extend(i)}
|
||||||
label={wpt.name ?? `${$_('gpx.waypoint')} ${i + 1}`}
|
label={wpt.name ?? `${$_('gpx.waypoint')} ${i + 1}`}
|
||||||
|
@@ -359,6 +359,9 @@ export const dbUtils = {
|
|||||||
applyToFile: (id: string, callback: (file: WritableDraft<GPXFile>) => GPXFile) => {
|
applyToFile: (id: string, callback: (file: WritableDraft<GPXFile>) => GPXFile) => {
|
||||||
applyToFiles([id], callback);
|
applyToFiles([id], callback);
|
||||||
},
|
},
|
||||||
|
applyToFiles: (ids: string[], callback: (file: WritableDraft<GPXFile>) => GPXFile) => {
|
||||||
|
applyToFiles(ids, callback);
|
||||||
|
},
|
||||||
applyToSelection: (callback: (file: WritableDraft<AnyGPXTreeElement>) => AnyGPXTreeElement) => {
|
applyToSelection: (callback: (file: WritableDraft<AnyGPXTreeElement>) => AnyGPXTreeElement) => {
|
||||||
if (get(selection).size === 0) {
|
if (get(selection).size === 0) {
|
||||||
return;
|
return;
|
||||||
|
Reference in New Issue
Block a user