From 245ba0714191de477bb00ce6ab3d210b324f7e26 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Mon, 3 Jun 2024 19:34:23 +0200 Subject: [PATCH] move tracks, segments and waypoints within same list --- gpx/src/gpx.ts | 45 ++++++++ .../file-list/FileListNodeContent.svelte | 103 +++++++++++++----- website/src/lib/db.ts | 3 + 3 files changed, 122 insertions(+), 29 deletions(-) diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index 2b0385b0..ac97f916 100644 --- a/gpx/src/gpx.ts +++ b/gpx/src/gpx.ts @@ -185,6 +185,18 @@ export class GPXFile extends GPXTreeNode{ }); } + 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[]) { return produce(this, (draft) => { 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{ }); } + 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[]) { return produce(this, (draft) => { 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{ }); } + 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() { return this._reverse(); } @@ -326,6 +359,18 @@ export class Track extends GPXTreeNode { }); } + 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[]) { return produce(this, (draft) => { let og = getOriginal(draft); // Read as much as possible from the original object because it is faster diff --git a/website/src/lib/components/file-list/FileListNodeContent.svelte b/website/src/lib/components/file-list/FileListNodeContent.svelte index fd63bfeb..41156f51 100644 --- a/website/src/lib/components/file-list/FileListNodeContent.svelte +++ b/website/src/lib/components/file-list/FileListNodeContent.svelte @@ -2,12 +2,12 @@ import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx'; import { afterUpdate, getContext, onDestroy, onMount } from 'svelte'; 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 FileListNodeStore from './FileListNodeStore.svelte'; import FileListNode from './FileListNode.svelte'; import FileListNodeLabel from './FileListNodeLabel.svelte'; - import { ListLevel, type ListItem } from './FileList'; + import { ListLevel, ListTrackItem, type ListItem } from './FileList'; import { selection } from './Selection'; import { _ } from 'svelte-i18n'; @@ -45,11 +45,10 @@ selection.update(($selection) => { $selection.clear(); Object.entries(elements).forEach(([id, element]) => { - let realId = - sortableLevel === ListLevel.FILE || sortableLevel === ListLevel.WAYPOINTS - ? id - : parseInt(id); - $selection.set(item.extend(realId), element.classList.contains('sortable-selected')); + $selection.set( + item.extend(getRealId(id)), + element.classList.contains('sortable-selected') + ); }); return $selection; }); @@ -100,32 +99,72 @@ group: { name: sortableLevel }, + direction: orientation, forceAutoScrollFallback: true, multiDrag: true, multiDragKey: 'Meta', avoidImplicitDeselect: true, onSelect: onSelectChange, onDeselect: onSelectChange, - sort: sortableLevel !== ListLevel.WAYPOINT, - onSort: () => { - if (sortableLevel !== ListLevel.FILE) { - return; - } - - 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]) { + onSort: (e) => { + if (sortableLevel === ListLevel.FILE) { + 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); + 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)); }); @@ -154,6 +193,9 @@ } }); } + if (sortableLevel !== ListLevel.FILE) { + sortable.sort(Object.keys(elements)); + } }); const unsubscribe = selection.subscribe(($selection) => { @@ -180,10 +222,7 @@ if (element === null) { return; } - let realId = - sortableLevel === ListLevel.FILE || sortableLevel === ListLevel.WAYPOINTS - ? id - : parseInt(id); + let realId = getRealId(id); let realItem = item.extend(realId); let inSelection = get(selection).has(realItem); let isSelected = element.classList.contains('sortable-selected'); @@ -194,6 +233,12 @@ return changed; } + function getRealId(id: string | number) { + return sortableLevel === ListLevel.FILE || sortableLevel === ListLevel.WAYPOINTS + ? id + : parseInt(id); + } + onDestroy(() => { unsubscribe(); }); @@ -212,26 +257,26 @@ {:else if node instanceof GPXFile} {#if waypointRoot} {#if node.wpt.length > 0} -
+
{/if} {:else} {#each node.children as child, i} -
+
{/each} {/if} {:else if node instanceof Track} {#each node.children as child, i} -
+
{/each} {:else if Array.isArray(node) && node.length > 0 && node[0] instanceof Waypoint} {#each node as wpt, i} -
+
) => GPXFile) => { applyToFiles([id], callback); }, + applyToFiles: (ids: string[], callback: (file: WritableDraft) => GPXFile) => { + applyToFiles(ids, callback); + }, applyToSelection: (callback: (file: WritableDraft) => AnyGPXTreeElement) => { if (get(selection).size === 0) { return;