move tracks, segments and waypoints within same list

This commit is contained in:
vcoppe
2024-06-03 19:34:23 +02:00
parent c9363de0ab
commit 245ba07141
3 changed files with 122 additions and 29 deletions

View File

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

View File

@@ -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,18 +99,15 @@
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) {
return;
}
let newFileOrder = sortable.toArray(); let newFileOrder = sortable.toArray();
if (newFileOrder.length !== get(fileOrder).length) { if (newFileOrder.length !== get(fileOrder).length) {
fileOrder.set(newFileOrder); fileOrder.set(newFileOrder);
@@ -124,7 +120,50 @@
return; 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}`}

View File

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