mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-04 17:32:56 +00:00
Merge branch 'main' into docs
syncing from main
This commit is contained in:
@@ -48,11 +48,8 @@
|
||||
triggerFileInput,
|
||||
createFile,
|
||||
loadFiles,
|
||||
toggleSelectionVisibility,
|
||||
updateSelectionFromKey,
|
||||
showSelection,
|
||||
hideSelection,
|
||||
anyHidden,
|
||||
allHidden,
|
||||
editMetadata,
|
||||
editStyle,
|
||||
exportState,
|
||||
@@ -217,6 +214,7 @@
|
||||
>
|
||||
<Info size="16" class="mr-1" />
|
||||
{$_('menu.metadata.button')}
|
||||
<Shortcut key="I" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
<Menubar.Item
|
||||
disabled={$selection.size === 0 ||
|
||||
@@ -230,15 +228,15 @@
|
||||
</Menubar.Item>
|
||||
<Menubar.Item
|
||||
on:click={() => {
|
||||
if ($anyHidden) {
|
||||
showSelection();
|
||||
if ($allHidden) {
|
||||
dbUtils.setHiddenToSelection(false);
|
||||
} else {
|
||||
hideSelection();
|
||||
dbUtils.setHiddenToSelection(true);
|
||||
}
|
||||
}}
|
||||
disabled={$selection.size == 0}
|
||||
>
|
||||
{#if $anyHidden}
|
||||
{#if $allHidden}
|
||||
<Eye size="16" class="mr-1" />
|
||||
{$_('menu.unhide')}
|
||||
{:else}
|
||||
@@ -248,7 +246,7 @@
|
||||
<Shortcut key="H" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item on:click={selectAll}>
|
||||
<Menubar.Item on:click={selectAll} disabled={$fileObservers.size == 0}>
|
||||
<FileStack size="16" class="mr-1" />
|
||||
{$_('menu.select_all')}
|
||||
<Shortcut key="A" ctrl={true} />
|
||||
@@ -525,6 +523,16 @@
|
||||
selectAll();
|
||||
e.preventDefault();
|
||||
}
|
||||
} else if (e.key === 'i' && (e.metaKey || e.ctrlKey)) {
|
||||
if (
|
||||
$selection.size === 1 &&
|
||||
$selection
|
||||
.getSelected()
|
||||
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
|
||||
) {
|
||||
$editMetadata = true;
|
||||
}
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'p' && (e.metaKey || e.ctrlKey)) {
|
||||
$elevationProfile = !$elevationProfile;
|
||||
e.preventDefault();
|
||||
@@ -532,7 +540,11 @@
|
||||
$verticalFileView = !$verticalFileView;
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) {
|
||||
toggleSelectionVisibility();
|
||||
if ($allHidden) {
|
||||
dbUtils.setHiddenToSelection(false);
|
||||
} else {
|
||||
dbUtils.setHiddenToSelection(true);
|
||||
}
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'F1') {
|
||||
switchBasemaps();
|
||||
|
@@ -5,8 +5,8 @@
|
||||
import { fileObservers, settings } from '$lib/db';
|
||||
import { setContext } from 'svelte';
|
||||
import { ListFileItem, ListLevel, ListRootItem, allowedPastes } from './FileList';
|
||||
import { copied, pasteSelection, selection } from './Selection';
|
||||
import { ClipboardPaste, Plus } from 'lucide-svelte';
|
||||
import { copied, pasteSelection, selectAll, selection } from './Selection';
|
||||
import { ClipboardPaste, FileStack, Plus } from 'lucide-svelte';
|
||||
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { createFile } from '$lib/stores';
|
||||
@@ -66,6 +66,12 @@
|
||||
<Shortcut key="+" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item on:click={selectAll} disabled={$fileObservers.size === 0}>
|
||||
<FileStack size="16" class="mr-1" />
|
||||
{$_('menu.select_all')}
|
||||
<Shortcut key="A" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item
|
||||
disabled={$copied === undefined ||
|
||||
$copied.length === 0 ||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { dbUtils, getFile, getFileIds } from "$lib/db";
|
||||
import { dbUtils, getFile } from "$lib/db";
|
||||
import { castDraft, freeze } from "immer";
|
||||
import { GPXFile, Track, TrackSegment, Waypoint } from "gpx";
|
||||
import { selection } from "./Selection";
|
||||
@@ -327,85 +327,68 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
||||
return;
|
||||
}
|
||||
|
||||
sortItems(fromItems, remove && !(fromParent instanceof ListRootItem));
|
||||
sortItems(fromItems, false);
|
||||
sortItems(toItems, false);
|
||||
|
||||
let context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[] = [];
|
||||
if (!remove || fromParent instanceof ListRootItem) {
|
||||
fromItems.forEach((item) => {
|
||||
let file = getFile(item.getFileId());
|
||||
if (file) {
|
||||
if (item instanceof ListFileItem) {
|
||||
context.push(file.clone());
|
||||
} else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
|
||||
context.push(file.trk[item.getTrackIndex()].clone());
|
||||
} else if (item instanceof ListTrackSegmentItem && item.getTrackIndex() < file.trk.length && item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length) {
|
||||
context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone());
|
||||
} else if (item instanceof ListWaypointsItem) {
|
||||
context.push(file.wpt.map((wpt) => wpt.clone()));
|
||||
} else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) {
|
||||
context.push(file.wpt[item.getWaypointIndex()].clone());
|
||||
}
|
||||
fromItems.forEach((item) => {
|
||||
let file = getFile(item.getFileId());
|
||||
if (file) {
|
||||
if (item instanceof ListFileItem) {
|
||||
context.push(file.clone());
|
||||
} else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
|
||||
context.push(file.trk[item.getTrackIndex()].clone());
|
||||
} else if (item instanceof ListTrackSegmentItem && item.getTrackIndex() < file.trk.length && item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length) {
|
||||
context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone());
|
||||
} else if (item instanceof ListWaypointsItem) {
|
||||
context.push(file.wpt.map((wpt) => wpt.clone()));
|
||||
} else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) {
|
||||
context.push(file.wpt[item.getWaypointIndex()].clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (remove && !(fromParent instanceof ListRootItem)) {
|
||||
sortItems(fromItems, true);
|
||||
}
|
||||
|
||||
let files = [fromParent.getFileId(), toParent.getFileId()];
|
||||
let callbacks = [
|
||||
(file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
|
||||
let newFile = file;
|
||||
fromItems.forEach((item) => {
|
||||
if (item instanceof ListTrackItem) {
|
||||
let [result, removed] = newFile.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
|
||||
newFile = castDraft(result);
|
||||
context.push(...removed);
|
||||
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
|
||||
} else if (item instanceof ListTrackSegmentItem) {
|
||||
let [result, removed] = newFile.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex(), []);
|
||||
newFile = castDraft(result);
|
||||
context.push(...removed);
|
||||
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex(), []);
|
||||
} else if (item instanceof ListWaypointsItem) {
|
||||
let [result, removed] = newFile.replaceWaypoints(0, newFile.wpt.length - 1, []);
|
||||
newFile = castDraft(result);
|
||||
context.push(removed);
|
||||
file.replaceWaypoints(0, file.wpt.length - 1, []);
|
||||
} else if (item instanceof ListWaypointItem) {
|
||||
let [result, removed] = newFile.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex(), []);
|
||||
newFile = castDraft(result);
|
||||
context.push(...removed);
|
||||
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex(), []);
|
||||
}
|
||||
});
|
||||
context.reverse();
|
||||
return newFile;
|
||||
},
|
||||
(file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
|
||||
let newFile = file;
|
||||
toItems.forEach((item, i) => {
|
||||
if (item instanceof ListTrackItem) {
|
||||
if (context[i] instanceof Track) {
|
||||
let [result, _removed] = newFile.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [context[i]]);
|
||||
newFile = castDraft(result);
|
||||
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [context[i]]);
|
||||
} else if (context[i] instanceof TrackSegment) {
|
||||
let [result, _removed] = newFile.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [new Track({
|
||||
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [new Track({
|
||||
trkseg: [context[i]]
|
||||
})]);
|
||||
newFile = castDraft(result);
|
||||
}
|
||||
} else if (item instanceof ListTrackSegmentItem && context[i] instanceof TrackSegment) {
|
||||
let [result, _removed] = newFile.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex() - 1, [context[i]]);
|
||||
newFile = castDraft(result);
|
||||
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex() - 1, [context[i]]);
|
||||
} else if (item instanceof ListWaypointsItem) {
|
||||
if (Array.isArray(context[i]) && context[i].length > 0 && context[i][0] instanceof Waypoint) {
|
||||
let [result, _removed] = newFile.replaceWaypoints(newFile.wpt.length, newFile.wpt.length - 1, context[i]);
|
||||
newFile = castDraft(result);
|
||||
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, context[i]);
|
||||
} else if (context[i] instanceof Waypoint) {
|
||||
let [result, _removed] = newFile.replaceWaypoints(newFile.wpt.length, newFile.wpt.length - 1, [context[i]]);
|
||||
newFile = castDraft(result);
|
||||
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]);
|
||||
}
|
||||
} else if (item instanceof ListWaypointItem && context[i] instanceof Waypoint) {
|
||||
let [result, _removed] = newFile.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [context[i]]);
|
||||
newFile = castDraft(result);
|
||||
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [context[i]]);
|
||||
}
|
||||
});
|
||||
return newFile;
|
||||
}
|
||||
];
|
||||
|
||||
@@ -433,14 +416,14 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
||||
if (context[i].name) {
|
||||
newFile.metadata.name = context[i].name;
|
||||
}
|
||||
newFile = newFile.replaceTracks(0, 0, [context[i]])[0];
|
||||
newFile.replaceTracks(0, 0, [context[i]]);
|
||||
files.set(item.getFileId(), freeze(newFile));
|
||||
} else if (context[i] instanceof TrackSegment) {
|
||||
let newFile = newGPXFile();
|
||||
newFile._data.id = item.getFileId();
|
||||
newFile = newFile.replaceTracks(0, 0, [new Track({
|
||||
newFile.replaceTracks(0, 0, [new Track({
|
||||
trkseg: [context[i]]
|
||||
})])[0];
|
||||
})]);
|
||||
files.set(item.getFileId(), freeze(newFile));
|
||||
}
|
||||
}
|
||||
|
@@ -14,8 +14,10 @@
|
||||
import FileListNodeLabel from './FileListNodeLabel.svelte';
|
||||
import { afterUpdate, getContext } from 'svelte';
|
||||
import {
|
||||
ListFileItem,
|
||||
ListTrackSegmentItem,
|
||||
ListWaypointItem,
|
||||
ListWaypointsItem,
|
||||
type ListItem,
|
||||
type ListTrackItem
|
||||
} from './FileList';
|
||||
@@ -25,8 +27,8 @@
|
||||
export let node:
|
||||
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
||||
| GPXTreeElement<AnyGPXTreeElement>
|
||||
| ReadonlyArray<Readonly<Waypoint>>
|
||||
| Readonly<Waypoint>;
|
||||
| Waypoint[]
|
||||
| Waypoint;
|
||||
export let item: ListItem;
|
||||
|
||||
let recursive = getContext<boolean>('recursive');
|
||||
@@ -34,7 +36,7 @@
|
||||
let collapsible: CollapsibleTreeNode;
|
||||
|
||||
$: label =
|
||||
node instanceof GPXFile
|
||||
node instanceof GPXFile && item instanceof ListFileItem
|
||||
? node.metadata.name
|
||||
: node instanceof Track
|
||||
? node.name ?? `${$_('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}`
|
||||
@@ -42,7 +44,7 @@
|
||||
? `${$_('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
|
||||
: node instanceof Waypoint
|
||||
? node.name ?? `${$_('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`
|
||||
: Array.isArray(node) && node.length > 0 && node[0] instanceof Waypoint
|
||||
: node instanceof GPXFile && item instanceof ListWaypointsItem
|
||||
? $_('gpx.waypoints')
|
||||
: '';
|
||||
|
||||
|
@@ -6,21 +6,28 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
|
||||
import { afterUpdate, getContext, onMount } from 'svelte';
|
||||
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
|
||||
import Sortable from 'sortablejs/Sortable';
|
||||
import { getFileIds, settings, type GPXFileWithStatistics } from '$lib/db';
|
||||
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||
import FileListNodeStore from './FileListNodeStore.svelte';
|
||||
import FileListNode from './FileListNode.svelte';
|
||||
import { ListLevel, ListRootItem, allowedMoves, moveItems, type ListItem } from './FileList';
|
||||
import {
|
||||
ListFileItem,
|
||||
ListLevel,
|
||||
ListRootItem,
|
||||
ListWaypointsItem,
|
||||
allowedMoves,
|
||||
moveItems,
|
||||
type ListItem
|
||||
} from './FileList';
|
||||
import { selection } from './Selection';
|
||||
import { _ } from 'svelte-i18n';
|
||||
|
||||
export let node:
|
||||
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
||||
| GPXTreeElement<AnyGPXTreeElement>
|
||||
| ReadonlyArray<Readonly<Waypoint>>
|
||||
| Readonly<Waypoint>;
|
||||
| Waypoint;
|
||||
export let item: ListItem;
|
||||
export let waypointRoot: boolean = false;
|
||||
|
||||
@@ -32,46 +39,64 @@
|
||||
: node instanceof GPXFile
|
||||
? waypointRoot
|
||||
? ListLevel.WAYPOINTS
|
||||
: ListLevel.TRACK
|
||||
: item instanceof ListWaypointsItem
|
||||
? ListLevel.WAYPOINT
|
||||
: ListLevel.TRACK
|
||||
: node instanceof Track
|
||||
? ListLevel.SEGMENT
|
||||
: ListLevel.WAYPOINT;
|
||||
let sortable: Sortable;
|
||||
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
||||
|
||||
let destroyed = false;
|
||||
let lastUpdateStart = 0;
|
||||
function updateToSelection(e) {
|
||||
if (updating) return;
|
||||
updating = true;
|
||||
// Sortable updates selection
|
||||
let changed = getChangedIds();
|
||||
if (changed.length > 0) {
|
||||
selection.update(($selection) => {
|
||||
$selection.clear();
|
||||
Object.entries(elements).forEach(([id, element]) => {
|
||||
$selection.set(
|
||||
item.extend(getRealId(id)),
|
||||
element.classList.contains('sortable-selected')
|
||||
);
|
||||
});
|
||||
if (destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
e.originalEvent &&
|
||||
$selection.size > 1 &&
|
||||
!(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey)
|
||||
) {
|
||||
// Fix bug that sometimes causes a single select to be treated as a multi-select
|
||||
$selection.clear();
|
||||
$selection.set(item.extend(getRealId(changed[0])), true);
|
||||
lastUpdateStart = Date.now();
|
||||
setTimeout(() => {
|
||||
if (Date.now() - lastUpdateStart >= 40) {
|
||||
if (updating) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $selection;
|
||||
});
|
||||
}
|
||||
updating = false;
|
||||
updating = true;
|
||||
// Sortable updates selection
|
||||
let changed = getChangedIds();
|
||||
if (changed.length > 0) {
|
||||
selection.update(($selection) => {
|
||||
$selection.clear();
|
||||
Object.entries(elements).forEach(([id, element]) => {
|
||||
$selection.set(
|
||||
item.extend(getRealId(id)),
|
||||
element.classList.contains('sortable-selected')
|
||||
);
|
||||
});
|
||||
|
||||
if (
|
||||
e.originalEvent &&
|
||||
$selection.size > 1 &&
|
||||
!(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey)
|
||||
) {
|
||||
// Fix bug that sometimes causes a single select to be treated as a multi-select
|
||||
$selection.clear();
|
||||
$selection.set(item.extend(getRealId(changed[0])), true);
|
||||
}
|
||||
|
||||
return $selection;
|
||||
});
|
||||
}
|
||||
updating = false;
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function updateFromSelection() {
|
||||
if (updating) return;
|
||||
if (destroyed || updating) {
|
||||
return;
|
||||
}
|
||||
updating = true;
|
||||
// Selection updates sortable
|
||||
let changed = getChangedIds();
|
||||
@@ -165,8 +190,9 @@
|
||||
if (Sortable.get(e.from)._waypointRoot) {
|
||||
fromItems = [fromItem.extend('waypoints')];
|
||||
} else {
|
||||
let oldIndices =
|
||||
let oldIndices: number[] =
|
||||
e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex];
|
||||
oldIndices = oldIndices.filter((i) => i >= 0);
|
||||
oldIndices.sort((a, b) => a - b);
|
||||
|
||||
fromItems = oldIndices.map((i) => fromItem.extend(i));
|
||||
@@ -179,8 +205,9 @@
|
||||
toItem = toItem.extend('waypoints');
|
||||
}
|
||||
|
||||
let newIndices =
|
||||
let newIndices: number[] =
|
||||
e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex];
|
||||
newIndices = newIndices.filter((i) => i >= 0);
|
||||
newIndices.sort((a, b) => a - b);
|
||||
|
||||
if (toItem instanceof ListRootItem) {
|
||||
@@ -211,6 +238,7 @@
|
||||
|
||||
onMount(() => {
|
||||
createSortable();
|
||||
destroyed = false;
|
||||
});
|
||||
|
||||
afterUpdate(() => {
|
||||
@@ -232,6 +260,10 @@
|
||||
updateFromSelection();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
destroyed = true;
|
||||
});
|
||||
|
||||
function getChangedIds() {
|
||||
let changed: (string | number)[] = [];
|
||||
Object.entries(elements).forEach(([id, element]) => {
|
||||
@@ -268,10 +300,16 @@
|
||||
</div>
|
||||
{/each}
|
||||
{:else if node instanceof GPXFile}
|
||||
{#if waypointRoot}
|
||||
{#if item instanceof ListWaypointsItem}
|
||||
{#each node.wpt as wpt, i (wpt)}
|
||||
<div data-id={i} class="ml-1">
|
||||
<FileListNode node={wpt} item={item.extend(i)} />
|
||||
</div>
|
||||
{/each}
|
||||
{:else if waypointRoot}
|
||||
{#if node.wpt.length > 0}
|
||||
<div data-id="waypoints">
|
||||
<FileListNode node={node.wpt} item={item.extend('waypoints')} />
|
||||
<FileListNode {node} item={item.extend('waypoints')} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
@@ -287,16 +325,10 @@
|
||||
<FileListNode node={child} item={item.extend(i)} />
|
||||
</div>
|
||||
{/each}
|
||||
{:else if Array.isArray(node) && node.length > 0 && node[0] instanceof Waypoint}
|
||||
{#each node as wpt, i (wpt)}
|
||||
<div data-id={i} class="ml-1">
|
||||
<FileListNode node={wpt} item={item.extend(i)} />
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if node instanceof GPXFile}
|
||||
{#if node instanceof GPXFile && item instanceof ListFileItem}
|
||||
{#if !waypointRoot}
|
||||
<svelte:self {node} {item} waypointRoot={true} />
|
||||
{/if}
|
||||
|
@@ -38,15 +38,7 @@
|
||||
} from './Selection';
|
||||
import { getContext } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import {
|
||||
anyHidden,
|
||||
editMetadata,
|
||||
editStyle,
|
||||
gpxLayers,
|
||||
hideSelection,
|
||||
map,
|
||||
showSelection
|
||||
} from '$lib/stores';
|
||||
import { allHidden, editMetadata, editStyle, gpxLayers, map } from '$lib/stores';
|
||||
import {
|
||||
GPXTreeElement,
|
||||
Track,
|
||||
@@ -59,10 +51,7 @@
|
||||
import MetadataDialog from './MetadataDialog.svelte';
|
||||
import StyleDialog from './StyleDialog.svelte';
|
||||
|
||||
export let node:
|
||||
| GPXTreeElement<AnyGPXTreeElement>
|
||||
| ReadonlyArray<Readonly<Waypoint>>
|
||||
| Readonly<Waypoint>;
|
||||
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
||||
export let item: ListItem;
|
||||
export let label: string | undefined;
|
||||
|
||||
@@ -114,6 +103,7 @@
|
||||
$editStyle &&
|
||||
$selection.has(item) &&
|
||||
$selection.getSelected().findIndex((i) => i.getFullId() === item.getFullId()) === 0;
|
||||
$: hidden = item.level === ListLevel.WAYPOINTS ? node._data.hiddenWpt : node._data.hidden;
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
@@ -153,7 +143,9 @@
|
||||
/>
|
||||
{/if}
|
||||
<span
|
||||
class="w-full text-left truncate py-1 flex flex-row items-center"
|
||||
class="w-full text-left truncate py-1 flex flex-row items-center {hidden
|
||||
? 'text-muted-foreground'
|
||||
: ''}"
|
||||
on:contextmenu={(e) => {
|
||||
if (e.ctrlKey) {
|
||||
// Add to selection instead of opening context menu
|
||||
@@ -189,9 +181,18 @@
|
||||
{:else if item.level === ListLevel.WAYPOINT}
|
||||
<MapPin size="16" class="mr-1 shrink-0" />
|
||||
{/if}
|
||||
<span class="grow select-none truncate {$verticalFileView ? 'mr-2' : ''}">
|
||||
<span class="grow select-none truncate {$verticalFileView ? 'last:mr-2' : ''}">
|
||||
{label}
|
||||
</span>
|
||||
{#if hidden}
|
||||
<EyeOff
|
||||
size="12"
|
||||
class="shrink-0 mt-1 ml-1 {$verticalFileView ? 'mr-2' : ''} {item.level ===
|
||||
ListLevel.SEGMENT || item.level === ListLevel.WAYPOINT
|
||||
? 'mr-3'
|
||||
: ''}"
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
</Button>
|
||||
</ContextMenu.Trigger>
|
||||
@@ -200,41 +201,39 @@
|
||||
<ContextMenu.Item disabled={!singleSelection} on:click={() => ($editMetadata = true)}>
|
||||
<Info size="16" class="mr-1" />
|
||||
{$_('menu.metadata.button')}
|
||||
<Shortcut key="I" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item on:click={() => ($editStyle = true)}>
|
||||
<PaintBucket size="16" class="mr-1" />
|
||||
{$_('menu.style.button')}
|
||||
</ContextMenu.Item>
|
||||
{#if item instanceof ListFileItem}
|
||||
<ContextMenu.Item
|
||||
on:click={() => {
|
||||
if ($anyHidden) {
|
||||
showSelection();
|
||||
} else {
|
||||
hideSelection();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if $anyHidden}
|
||||
<Eye size="16" class="mr-1" />
|
||||
{$_('menu.unhide')}
|
||||
{:else}
|
||||
<EyeOff size="16" class="mr-1" />
|
||||
{$_('menu.hide')}
|
||||
{/if}
|
||||
<Shortcut key="H" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
{/if}
|
||||
<ContextMenu.Separator />
|
||||
{/if}
|
||||
<ContextMenu.Item
|
||||
on:click={() => {
|
||||
if ($allHidden) {
|
||||
dbUtils.setHiddenToSelection(false);
|
||||
} else {
|
||||
dbUtils.setHiddenToSelection(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if $allHidden}
|
||||
<Eye size="16" class="mr-1" />
|
||||
{$_('menu.unhide')}
|
||||
{:else}
|
||||
<EyeOff size="16" class="mr-1" />
|
||||
{$_('menu.hide')}
|
||||
{/if}
|
||||
<Shortcut key="H" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
{#if $verticalFileView}
|
||||
{#if item instanceof ListFileItem}
|
||||
<ContextMenu.Item
|
||||
disabled={!singleSelection}
|
||||
on:click={() =>
|
||||
dbUtils.applyToFile(
|
||||
item.getFileId(),
|
||||
(file) => file.replaceTracks(file.trk.length, file.trk.length, [new Track()])[0]
|
||||
dbUtils.applyToFile(item.getFileId(), (file) =>
|
||||
file.replaceTracks(file.trk.length, file.trk.length, [new Track()])
|
||||
)}
|
||||
>
|
||||
<Plus size="16" class="mr-1" />
|
||||
@@ -246,15 +245,13 @@
|
||||
disabled={!singleSelection}
|
||||
on:click={() => {
|
||||
let trackIndex = item.getTrackIndex();
|
||||
dbUtils.applyToFile(
|
||||
item.getFileId(),
|
||||
(file) =>
|
||||
file.replaceTrackSegments(
|
||||
trackIndex,
|
||||
file.trk[trackIndex].trkseg.length,
|
||||
file.trk[trackIndex].trkseg.length,
|
||||
[new TrackSegment()]
|
||||
)[0]
|
||||
dbUtils.applyToFile(item.getFileId(), (file) =>
|
||||
file.replaceTrackSegments(
|
||||
trackIndex,
|
||||
file.trk[trackIndex].trkseg.length,
|
||||
file.trk[trackIndex].trkseg.length,
|
||||
[new TrackSegment()]
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
@@ -11,10 +11,7 @@
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { editMetadata } from '$lib/stores';
|
||||
|
||||
export let node:
|
||||
| GPXTreeElement<AnyGPXTreeElement>
|
||||
| ReadonlyArray<Readonly<Waypoint>>
|
||||
| Readonly<Waypoint>;
|
||||
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
||||
export let item: ListItem;
|
||||
export let open = false;
|
||||
|
||||
@@ -54,7 +51,6 @@
|
||||
file.trk[item.getTrackIndex()].name = name;
|
||||
file.trk[item.getTrackIndex()].desc = description;
|
||||
}
|
||||
return file;
|
||||
});
|
||||
open = false;
|
||||
}}
|
||||
|
@@ -6,7 +6,6 @@ import { currentPopupWaypoint, deleteWaypoint, waypointPopup } from "./WaypointP
|
||||
import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection";
|
||||
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
|
||||
import type { Waypoint } from "gpx";
|
||||
import { produce } from "immer";
|
||||
import { resetCursor, setCursor, setGrabbingCursor, setPointerCursor } from "$lib/utils";
|
||||
import { font } from "$lib/assets/layers";
|
||||
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
|
||||
@@ -50,7 +49,6 @@ export class GPXLayer {
|
||||
fileId: string;
|
||||
file: Readable<GPXFileWithStatistics | undefined>;
|
||||
layerColor: string;
|
||||
hidden: boolean = false;
|
||||
markers: mapboxgl.Marker[] = [];
|
||||
selected: boolean = false;
|
||||
draggable: boolean;
|
||||
@@ -165,6 +163,15 @@ export class GPXLayer {
|
||||
this.map.removeLayer(this.fileId + '-direction');
|
||||
}
|
||||
}
|
||||
|
||||
let visibleItems: [number, number][] = [];
|
||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||
if (!segment._data.hidden) {
|
||||
visibleItems.push([trackIndex, segmentIndex]);
|
||||
}
|
||||
});
|
||||
|
||||
this.map.setFilter(this.fileId, ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false });
|
||||
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
|
||||
return;
|
||||
}
|
||||
@@ -223,13 +230,13 @@ export class GPXLayer {
|
||||
resetCursor();
|
||||
marker.getElement().style.cursor = '';
|
||||
dbUtils.applyToFile(this.fileId, (file) => {
|
||||
return produce(file, (draft) => {
|
||||
let latLng = marker.getLngLat();
|
||||
draft.wpt[marker._waypoint._data.index].setCoordinates({
|
||||
lat: latLng.lat,
|
||||
lon: latLng.lng
|
||||
});
|
||||
let latLng = marker.getLngLat();
|
||||
let wpt = file.wpt[marker._waypoint._data.index];
|
||||
wpt.setCoordinates({
|
||||
lat: latLng.lat,
|
||||
lon: latLng.lng
|
||||
});
|
||||
wpt.ele = this.map.queryTerrainElevation([latLng.lng, latLng.lat], { exaggerated: false }) ?? 0;
|
||||
});
|
||||
dragEndTimestamp = Date.now()
|
||||
});
|
||||
@@ -244,7 +251,11 @@ export class GPXLayer {
|
||||
}
|
||||
|
||||
this.markers.forEach((marker) => {
|
||||
marker.addTo(this.map);
|
||||
if (!marker._waypoint._data.hidden) {
|
||||
marker.addTo(this.map);
|
||||
} else {
|
||||
marker.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -366,17 +377,6 @@ export class GPXLayer {
|
||||
}
|
||||
}
|
||||
|
||||
toggleVisibility() {
|
||||
this.hidden = !this.hidden;
|
||||
if (this.hidden) {
|
||||
this.map.setLayoutProperty(this.fileId, 'visibility', 'none');
|
||||
this.markers.forEach(marker => marker.remove());
|
||||
} else {
|
||||
this.map.setLayoutProperty(this.fileId, 'visibility', 'visible');
|
||||
this.markers.forEach(marker => marker.addTo(this.map));
|
||||
}
|
||||
}
|
||||
|
||||
getGeoJSON(): GeoJSON.FeatureCollection {
|
||||
let file = get(this.file)?.file;
|
||||
if (!file) {
|
||||
|
@@ -21,5 +21,5 @@ export const waypointPopup = new mapboxgl.Popup({
|
||||
});
|
||||
|
||||
export function deleteWaypoint(fileId: string, waypointIndex: number) {
|
||||
dbUtils.applyToFile(fileId, (file) => file.replaceWaypoints(waypointIndex, waypointIndex, [])[0]);
|
||||
dbUtils.applyToFile(fileId, (file) => file.replaceWaypoints(waypointIndex, waypointIndex, []));
|
||||
}
|
@@ -303,16 +303,16 @@
|
||||
let fileId = item.getFileId();
|
||||
dbUtils.applyToFile(fileId, (file) => {
|
||||
if (item instanceof ListFileItem) {
|
||||
return file.changeTimestamps(getDate(startDate, startTime), effectiveSpeed, ratio);
|
||||
file.changeTimestamps(getDate(startDate, startTime), effectiveSpeed, ratio);
|
||||
} else if (item instanceof ListTrackItem) {
|
||||
return file.changeTimestamps(
|
||||
file.changeTimestamps(
|
||||
getDate(startDate, startTime),
|
||||
effectiveSpeed,
|
||||
ratio,
|
||||
item.getTrackIndex()
|
||||
);
|
||||
} else if (item instanceof ListTrackSegmentItem) {
|
||||
return file.changeTimestamps(
|
||||
file.changeTimestamps(
|
||||
getDate(startDate, startTime),
|
||||
effectiveSpeed,
|
||||
ratio,
|
||||
|
@@ -104,19 +104,16 @@
|
||||
longitude = parseFloat(longitude.toFixed(6));
|
||||
if ($selectedWaypoint) {
|
||||
dbUtils.applyToFile($selectedWaypoint[1], (file) => {
|
||||
let waypoint = $selectedWaypoint[0].clone();
|
||||
waypoint.name = name;
|
||||
waypoint.desc = description;
|
||||
waypoint.cmt = description;
|
||||
waypoint.setCoordinates({
|
||||
let wpt = file.wpt[$selectedWaypoint[0]._data.index];
|
||||
wpt.name = name;
|
||||
wpt.desc = description;
|
||||
wpt.cmt = description;
|
||||
wpt.setCoordinates({
|
||||
lat: latitude,
|
||||
lon: longitude
|
||||
});
|
||||
return file.replaceWaypoints(
|
||||
$selectedWaypoint[0]._data.index,
|
||||
$selectedWaypoint[0]._data.index,
|
||||
[waypoint]
|
||||
)[0];
|
||||
wpt.ele =
|
||||
get(map)?.queryTerrainElevation([longitude, latitude], { exaggerated: false }) ?? 0;
|
||||
});
|
||||
} else {
|
||||
let fileIds = new Set<string>();
|
||||
@@ -134,9 +131,8 @@
|
||||
});
|
||||
waypoint.ele =
|
||||
get(map)?.queryTerrainElevation([longitude, latitude], { exaggerated: false }) ?? 0;
|
||||
dbUtils.applyToFiles(
|
||||
Array.from(fileIds),
|
||||
(file) => file.replaceWaypoints(file.wpt.length, file.wpt.length, [waypoint])[0]
|
||||
dbUtils.applyToFiles(Array.from(fileIds), (file) =>
|
||||
file.replaceWaypoints(file.wpt.length, file.wpt.length, [waypoint])
|
||||
);
|
||||
}
|
||||
selectedWaypoint.set(undefined);
|
||||
|
@@ -35,7 +35,6 @@
|
||||
import { flyAndScale } from '$lib/utils';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { TrackPoint } from 'gpx';
|
||||
import { produce } from 'immer';
|
||||
|
||||
export let popup: mapboxgl.Popup;
|
||||
export let popupElement: HTMLElement;
|
||||
@@ -71,7 +70,7 @@
|
||||
function createFileWithPoint(e: any) {
|
||||
if ($selection.size === 0) {
|
||||
let file = newGPXFile();
|
||||
file = file.replaceTrackPoints(0, 0, 0, 0, [
|
||||
file.replaceTrackPoints(0, 0, 0, 0, [
|
||||
new TrackPoint({
|
||||
attributes: {
|
||||
lat: e.lngLat.lat,
|
||||
@@ -79,9 +78,7 @@
|
||||
}
|
||||
})
|
||||
]);
|
||||
file = produce(file, (draft) => {
|
||||
draft._data.id = getFileIds(1)[0];
|
||||
});
|
||||
file._data.id = getFileIds(1)[0];
|
||||
dbUtils.add(file);
|
||||
selectFileWhenLoaded(file._data.id);
|
||||
}
|
||||
|
@@ -351,7 +351,7 @@ export class RoutingControls {
|
||||
} else if (nextAnchor === null) { // Last point, remove trackpoints from previousAnchor
|
||||
dbUtils.applyToFile(this.fileId, (file) => {
|
||||
let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex);
|
||||
return file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, previousAnchor.point._data.index + 1, segment.trkpt.length - 1, []);
|
||||
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, previousAnchor.point._data.index + 1, segment.trkpt.length - 1, []);
|
||||
});
|
||||
} else { // Route between previousAnchor and nextAnchor
|
||||
this.routeBetweenAnchors([previousAnchor, nextAnchor], [previousAnchor.point.getCoordinates(), nextAnchor.point.getCoordinates()]);
|
||||
@@ -374,8 +374,8 @@ export class RoutingControls {
|
||||
|
||||
let segment = anchor.segment;
|
||||
dbUtils.applyToFile(this.fileId, (file) => {
|
||||
let newFile = file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, segment.trkpt.length, segment.trkpt.length - 1, segment.trkpt.slice(0, anchor.point._data.index), speed > 0 ? speed : undefined);
|
||||
return newFile.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, anchor.point._data.index - 1, []);
|
||||
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, segment.trkpt.length, segment.trkpt.length - 1, segment.trkpt.slice(0, anchor.point._data.index), speed > 0 ? speed : undefined);
|
||||
file.crop(anchor.point._data.index, anchor.point._data.index + segment.trkpt.length - 1, [anchor.trackIndex], [anchor.segmentIndex]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -412,14 +412,14 @@ export class RoutingControls {
|
||||
}
|
||||
if (file.trk.length === 0) {
|
||||
let track = new Track();
|
||||
track = track.replaceTrackPoints(0, 0, 0, [newPoint]);
|
||||
return file.replaceTracks(0, 0, [track])[0];
|
||||
track.replaceTrackPoints(0, 0, 0, [newPoint]);
|
||||
file.replaceTracks(0, 0, [track]);
|
||||
} else if (file.trk[trackIndex].trkseg.length === 0) {
|
||||
let segment = new TrackSegment();
|
||||
segment = segment.replaceTrackPoints(0, 0, [newPoint]);
|
||||
return file.replaceTrackSegments(trackIndex, 0, 0, [segment])[0];
|
||||
segment.replaceTrackPoints(0, 0, [newPoint]);
|
||||
file.replaceTrackSegments(trackIndex, 0, 0, [segment]);
|
||||
} else {
|
||||
return file.replaceTrackPoints(trackIndex, segmentIndex, 0, 0, [newPoint]);
|
||||
file.replaceTrackPoints(trackIndex, segmentIndex, 0, 0, [newPoint]);
|
||||
}
|
||||
});
|
||||
return;
|
||||
@@ -458,11 +458,11 @@ export class RoutingControls {
|
||||
|
||||
let lastAnchor = this.anchors[this.anchors.length - 1];
|
||||
|
||||
let segment = lastAnchor.segment;
|
||||
dbUtils.applyToFile(this.fileId, (file) => {
|
||||
let segment = original(file).getSegment(lastAnchor.trackIndex, lastAnchor.segmentIndex);
|
||||
let newSegment = segment.clone();
|
||||
newSegment = newSegment._reverse(segment.getEndTimestamp(), segment.getEndTimestamp());
|
||||
return file.replaceTrackPoints(lastAnchor.trackIndex, lastAnchor.segmentIndex, segment.trkpt.length, segment.trkpt.length, newSegment.trkpt.map((point) => point));
|
||||
newSegment._reverse(segment.getEndTimestamp(), segment.getEndTimestamp());
|
||||
file.replaceTrackPoints(lastAnchor.trackIndex, lastAnchor.segmentIndex, segment.trkpt.length, segment.trkpt.length, newSegment.trkpt.map((point) => point));
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -23,15 +23,11 @@ export function updateAnchorPoints(file: GPXFile) {
|
||||
}
|
||||
|
||||
if (segment.trkpt.length > 0) {
|
||||
if (!segment.trkpt[0]._data.anchor) { // First point is not an anchor, make it one
|
||||
segment.trkpt[0]._data.anchor = true;
|
||||
segment.trkpt[0]._data.zoom = 0;
|
||||
}
|
||||
|
||||
if (!segment.trkpt[segment.trkpt.length - 1]._data.anchor) { // Last point is not an anchor, make it one
|
||||
segment.trkpt[segment.trkpt.length - 1]._data.anchor = true;
|
||||
segment.trkpt[segment.trkpt.length - 1]._data.zoom = 0;
|
||||
}
|
||||
// Ensure first and last points are anchors and always visible
|
||||
segment.trkpt[0]._data.anchor = true;
|
||||
segment.trkpt[0]._data.zoom = 0;
|
||||
segment.trkpt[segment.trkpt.length - 1]._data.anchor = true;
|
||||
segment.trkpt[segment.trkpt.length - 1]._data.zoom = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user