This commit is contained in:
vcoppe
2025-10-05 19:34:05 +02:00
parent 1cc07901f6
commit 0733562c0d
70 changed files with 2641 additions and 2968 deletions

View File

@@ -7,6 +7,7 @@
variant = 'default', variant = 'default',
label, label,
side = 'top', side = 'top',
disabled = false,
class: className = '', class: className = '',
children, children,
onclick, onclick,
@@ -14,6 +15,7 @@
variant?: 'default' | 'secondary' | 'link' | 'destructive' | 'outline' | 'ghost'; variant?: 'default' | 'secondary' | 'link' | 'destructive' | 'outline' | 'ghost';
label: string; label: string;
side?: 'top' | 'right' | 'bottom' | 'left'; side?: 'top' | 'right' | 'bottom' | 'left';
disabled?: boolean;
class?: string; class?: string;
children: Snippet; children: Snippet;
onclick?: (event: MouseEvent) => void; onclick?: (event: MouseEvent) => void;

View File

@@ -55,25 +55,31 @@
// updateSelectionFromKey, // updateSelectionFromKey,
// allHidden, // allHidden,
// } from '$lib/stores'; // } from '$lib/stores';
import { // import { canUndo, canRedo, fileActions, fileObservers, settings } from '$lib/db';
copied,
copySelection,
cutSelection,
pasteSelection,
selectAll,
selection,
} from '$lib/components/file-list/Selection';
// import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db';
import { anySelectedLayer } from '$lib/components/map/layer-control/utils.svelte'; import { anySelectedLayer } from '$lib/components/map/layer-control/utils.svelte';
import { defaultOverlays } from '$lib/assets/layers'; import { defaultOverlays } from '$lib/assets/layers';
// import LayerControlSettings from '$lib/components/map/layer-control/LayerControlSettings.svelte'; // import LayerControlSettings from '$lib/components/map/layer-control/LayerControlSettings.svelte';
import { allowedPastes, ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList'; import {
allowedPastes,
ListFileItem,
ListTrackItem,
} from '$lib/components/file-list/file-list';
import Export from '$lib/components/export/Export.svelte'; import Export from '$lib/components/export/Export.svelte';
import { mode, setMode } from 'mode-watcher'; import { mode, setMode } from 'mode-watcher';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { languages } from '$lib/languages'; import { languages } from '$lib/languages';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { settings } from '$lib/logic/settings.svelte'; import { settings } from '$lib/logic/settings.svelte';
import {
createFile,
fileActions,
loadFiles,
pasteSelection,
triggerFileInput,
} from '$lib/logic/file-actions.svelte';
import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { fileActionManager } from '$lib/logic/file-action-manager.svelte';
import { selection } from '$lib/logic/selection.svelte';
const { const {
distanceUnits, distanceUnits,
@@ -91,9 +97,6 @@
routing, routing,
} = settings; } = settings;
// let undoDisabled = derived(canUndo, ($canUndo) => !$canUndo);
// let redoDisabled = derived(canRedo, ($canRedo) => !$canRedo);
function switchBasemaps() { function switchBasemaps() {
[currentBasemap.value, previousBasemap.value] = [ [currentBasemap.value, previousBasemap.value] = [
previousBasemap.value, previousBasemap.value,
@@ -103,15 +106,11 @@
function toggleOverlays() { function toggleOverlays() {
if (currentOverlays.value && anySelectedLayer(currentOverlays.value)) { if (currentOverlays.value && anySelectedLayer(currentOverlays.value)) {
[currentOverlays.value, previousOverlays.value] = [ previousOverlays.value = JSON.parse(JSON.stringify(currentOverlays.value));
defaultOverlays, currentOverlays.value = defaultOverlays;
currentOverlays.value,
];
} else { } else {
[currentOverlays.value, previousOverlays.value] = [ currentOverlays.value = JSON.parse(JSON.stringify(previousOverlays.value));
previousOverlays.value, previousOverlays.value = defaultOverlays;
defaultOverlays,
];
} }
} }
@@ -126,7 +125,7 @@
<Logo class="h-5 mt-0.5 mx-2 md:hidden" iconOnly={true} width="16" /> <Logo class="h-5 mt-0.5 mx-2 md:hidden" iconOnly={true} width="16" />
<Logo class="h-5 mt-0.5 mx-2 hidden md:block" width="96" /> <Logo class="h-5 mt-0.5 mx-2 hidden md:block" width="96" />
</a> </a>
<Menubar.Root class="border-none h-fit p-0"> <Menubar.Root class="border-none shadow-none h-fit p-0">
<Menubar.Menu> <Menubar.Menu>
<Menubar.Trigger aria-label={i18n._('gpx.file')}> <Menubar.Trigger aria-label={i18n._('gpx.file')}>
<File size="18" class="md:hidden" /> <File size="18" class="md:hidden" />
@@ -146,8 +145,8 @@
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item <Menubar.Item
onclick={dbUtils.duplicateSelection} onclick={fileActions.duplicateSelection}
disabled={$selection.size == 0} disabled={selection.value.size == 0}
> >
<Copy size="16" class="mr-1" /> <Copy size="16" class="mr-1" />
{i18n._('menu.duplicate')} {i18n._('menu.duplicate')}
@@ -155,16 +154,16 @@
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item <Menubar.Item
onclick={dbUtils.deleteSelectedFiles} onclick={fileActions.deleteSelectedFiles}
disabled={$selection.size == 0} disabled={selection.value.size == 0}
> >
<FileX size="16" class="mr-1" /> <FileX size="16" class="mr-1" />
{i18n._('menu.close')} {i18n._('menu.close')}
<Shortcut key="⌫" ctrl={true} /> <Shortcut key="⌫" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Item <Menubar.Item
onclick={dbUtils.deleteAllFiles} onclick={fileActions.deleteAllFiles}
disabled={$fileObservers.size == 0} disabled={fileStateCollection.size == 0}
> >
<FileX size="16" class="mr-1" /> <FileX size="16" class="mr-1" />
{i18n._('menu.close_all')} {i18n._('menu.close_all')}
@@ -173,7 +172,7 @@
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item <Menubar.Item
onclick={() => (exportState.current = ExportState.SELECTION)} onclick={() => (exportState.current = ExportState.SELECTION)}
disabled={$selection.size == 0} disabled={selection.value.size == 0}
> >
<Download size="16" class="mr-1" /> <Download size="16" class="mr-1" />
{i18n._('menu.export')} {i18n._('menu.export')}
@@ -181,7 +180,7 @@
</Menubar.Item> </Menubar.Item>
<Menubar.Item <Menubar.Item
onclick={() => (exportState.current = ExportState.ALL)} onclick={() => (exportState.current = ExportState.ALL)}
disabled={$fileObservers.size == 0} disabled={fileStateCollection.size == 0}
> >
<Download size="16" class="mr-1" /> <Download size="16" class="mr-1" />
{i18n._('menu.export_all')} {i18n._('menu.export_all')}
@@ -195,20 +194,26 @@
<span class="hidden md:block">{i18n._('menu.edit')}</span> <span class="hidden md:block">{i18n._('menu.edit')}</span>
</Menubar.Trigger> </Menubar.Trigger>
<Menubar.Content class="border-none"> <Menubar.Content class="border-none">
<Menubar.Item onclick={dbUtils.undo} disabled={$undoDisabled}> <Menubar.Item
onclick={fileActionManager.undo}
disabled={!fileActionManager.canUndo}
>
<Undo2 size="16" class="mr-1" /> <Undo2 size="16" class="mr-1" />
{i18n._('menu.undo')} {i18n._('menu.undo')}
<Shortcut key="Z" ctrl={true} /> <Shortcut key="Z" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Item onclick={dbUtils.redo} disabled={$redoDisabled}> <Menubar.Item
onclick={fileActionManager.redo}
disabled={!fileActionManager.canRedo}
>
<Redo2 size="16" class="mr-1" /> <Redo2 size="16" class="mr-1" />
{i18n._('menu.redo')} {i18n._('menu.redo')}
<Shortcut key="Z" ctrl={true} shift={true} /> <Shortcut key="Z" ctrl={true} shift={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item <Menubar.Item
disabled={$selection.size !== 1 || disabled={selection.value.size !== 1 ||
!$selection !selection.value
.getSelected() .getSelected()
.every( .every(
(item) => (item) =>
@@ -222,8 +227,8 @@
<Shortcut key="I" ctrl={true} /> <Shortcut key="I" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Item <Menubar.Item
disabled={$selection.size === 0 || disabled={selection.value.size === 0 ||
!$selection !selection.value
.getSelected() .getSelected()
.every( .every(
(item) => (item) =>
@@ -237,44 +242,51 @@
</Menubar.Item> </Menubar.Item>
<Menubar.Item <Menubar.Item
onclick={() => { onclick={() => {
if ($allHidden) { // if ($allHidden) {
dbUtils.setHiddenToSelection(false); // fileActions.setHiddenToSelection(false);
} else { // } else {
dbUtils.setHiddenToSelection(true); // fileActions.setHiddenToSelection(true);
} // }
}} }}
disabled={$selection.size == 0} disabled={selection.value.size == 0}
> >
{#if $allHidden} <!-- {#if $allHidden}
<Eye size="16" class="mr-1" /> <Eye size="16" class="mr-1" />
{i18n._('menu.unhide')} {i18n._('menu.unhide')}
{:else} {:else}
<EyeOff size="16" class="mr-1" /> <EyeOff size="16" class="mr-1" />
{i18n._('menu.hide')} {i18n._('menu.hide')}
{/if} {/if} -->
<Shortcut key="H" ctrl={true} /> <Shortcut key="H" ctrl={true} />
</Menubar.Item> </Menubar.Item>
{#if treeFileView.value} {#if treeFileView.value}
{#if $selection.getSelected().some((item) => item instanceof ListFileItem)} {#if selection.value
.getSelected()
.some((item) => item instanceof ListFileItem)}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item <Menubar.Item
onclick={() => onclick={() =>
dbUtils.addNewTrack($selection.getSelected()[0].getFileId())} fileActions.addNewTrack(
disabled={$selection.size !== 1} selection.value.getSelected()[0].getFileId()
)}
disabled={selection.value.size !== 1}
> >
<Plus size="16" class="mr-1" /> <Plus size="16" class="mr-1" />
{i18n._('menu.new_track')} {i18n._('menu.new_track')}
</Menubar.Item> </Menubar.Item>
{:else if $selection {:else if selection.value
.getSelected() .getSelected()
.some((item) => item instanceof ListTrackItem)} .some((item) => item instanceof ListTrackItem)}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item <Menubar.Item
onclick={() => { onclick={() => {
let item = $selection.getSelected()[0]; let item = selection.value.getSelected()[0];
dbUtils.addNewSegment(item.getFileId(), item.getTrackIndex()); fileActions.addNewSegment(
item.getFileId(),
item.getTrackIndex()
);
}} }}
disabled={$selection.size !== 1} disabled={selection.value.size !== 1}
> >
<Plus size="16" class="mr-1" /> <Plus size="16" class="mr-1" />
{i18n._('menu.new_segment')} {i18n._('menu.new_segment')}
@@ -282,15 +294,18 @@
{/if} {/if}
{/if} {/if}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item onclick={selectAll} disabled={$fileObservers.size == 0}> <Menubar.Item
onclick={selection.selectAll}
disabled={fileStateCollection.size == 0}
>
<FileStack size="16" class="mr-1" /> <FileStack size="16" class="mr-1" />
{i18n._('menu.select_all')} {i18n._('menu.select_all')}
<Shortcut key="A" ctrl={true} /> <Shortcut key="A" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Item <Menubar.Item
onclick={() => { onclick={() => {
if ($selection.size > 0) { if (selection.value.size > 0) {
centerMapOnSelection(); // centerMapOnSelection();
} }
}} }}
> >
@@ -300,22 +315,28 @@
</Menubar.Item> </Menubar.Item>
{#if treeFileView.value} {#if treeFileView.value}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item onclick={copySelection} disabled={$selection.size === 0}> <Menubar.Item
onclick={selection.copySelection}
disabled={selection.value.size === 0}
>
<ClipboardCopy size="16" class="mr-1" /> <ClipboardCopy size="16" class="mr-1" />
{i18n._('menu.copy')} {i18n._('menu.copy')}
<Shortcut key="C" ctrl={true} /> <Shortcut key="C" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Item onclick={cutSelection} disabled={$selection.size === 0}> <Menubar.Item
onclick={selection.cutSelection}
disabled={selection.value.size === 0}
>
<Scissors size="16" class="mr-1" /> <Scissors size="16" class="mr-1" />
{i18n._('menu.cut')} {i18n._('menu.cut')}
<Shortcut key="X" ctrl={true} /> <Shortcut key="X" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Item <Menubar.Item
disabled={$copied === undefined || disabled={selection.copied === undefined ||
$copied.length === 0 || selection.copied.length === 0 ||
($selection.size > 0 && (selection.value.size > 0 &&
!allowedPastes[$copied[0].level].includes( !allowedPastes[selection.copied[0].level].includes(
$selection.getSelected().pop()?.level selection.value.getSelected().pop()?.level
))} ))}
onclick={pasteSelection} onclick={pasteSelection}
> >
@@ -325,7 +346,10 @@
</Menubar.Item> </Menubar.Item>
{/if} {/if}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item onclick={dbUtils.deleteSelection} disabled={$selection.size == 0}> <Menubar.Item
onclick={fileActions.deleteSelection}
disabled={selection.value.size == 0}
>
<Trash2 size="16" class="mr-1" /> <Trash2 size="16" class="mr-1" />
{i18n._('menu.delete')} {i18n._('menu.delete')}
<Shortcut key="⌫" ctrl={true} /> <Shortcut key="⌫" ctrl={true} />
@@ -552,16 +576,16 @@
triggerFileInput(); triggerFileInput();
e.preventDefault(); e.preventDefault();
} else if (e.key === 'd' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'd' && (e.metaKey || e.ctrlKey)) {
dbUtils.duplicateSelection(); fileActions.duplicateSelection();
e.preventDefault(); e.preventDefault();
} else if (e.key === 'c' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'c' && (e.metaKey || e.ctrlKey)) {
if (!targetInput) { if (!targetInput) {
copySelection(); selection.copySelection();
e.preventDefault(); e.preventDefault();
} }
} else if (e.key === 'x' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'x' && (e.metaKey || e.ctrlKey)) {
if (!targetInput) { if (!targetInput) {
cutSelection(); selection.cutSelection();
e.preventDefault(); e.preventDefault();
} }
} else if (e.key === 'v' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'v' && (e.metaKey || e.ctrlKey)) {
@@ -571,38 +595,38 @@
} }
} else if ((e.key === 's' || e.key == 'S') && (e.metaKey || e.ctrlKey)) { } else if ((e.key === 's' || e.key == 'S') && (e.metaKey || e.ctrlKey)) {
if (e.shiftKey) { if (e.shiftKey) {
if ($fileObservers.size > 0) { if (fileStateCollection.size > 0) {
exportState.current = ExportState.ALL; exportState.current = ExportState.ALL;
} }
} else if ($selection.size > 0) { } else if (selection.value.size > 0) {
exportState.current = ExportState.SELECTION; exportState.current = ExportState.SELECTION;
} }
e.preventDefault(); e.preventDefault();
} else if ((e.key === 'z' || e.key == 'Z') && (e.metaKey || e.ctrlKey)) { } else if ((e.key === 'z' || e.key == 'Z') && (e.metaKey || e.ctrlKey)) {
if (e.shiftKey) { if (e.shiftKey) {
dbUtils.redo(); fileActionManager.redo();
} else { } else {
dbUtils.undo(); fileActionManager.undo();
} }
e.preventDefault(); e.preventDefault();
} else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) { } else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) {
if (!targetInput) { if (!targetInput) {
if (e.shiftKey) { if (e.shiftKey) {
dbUtils.deleteAllFiles(); fileActions.deleteAllFiles();
} else { } else {
dbUtils.deleteSelection(); fileActions.deleteSelection();
} }
e.preventDefault(); e.preventDefault();
} }
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) {
if (!targetInput) { if (!targetInput) {
selectAll(); selection.selectAll();
e.preventDefault(); e.preventDefault();
} }
} else if (e.key === 'i' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'i' && (e.metaKey || e.ctrlKey)) {
if ( if (
$selection.size === 1 && selection.value.size === 1 &&
$selection selection.value
.getSelected() .getSelected()
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem) .every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
) { ) {
@@ -610,22 +634,22 @@
} }
e.preventDefault(); e.preventDefault();
} else if (e.key === 'p' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'p' && (e.metaKey || e.ctrlKey)) {
$elevationProfile = !$elevationProfile; elevationProfile.value = !elevationProfile.value;
e.preventDefault(); e.preventDefault();
} else if (e.key === 'l' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'l' && (e.metaKey || e.ctrlKey)) {
$treeFileView = !$treeFileView; treeFileView.value = !treeFileView.value;
e.preventDefault(); e.preventDefault();
} else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) {
if ($allHidden) { // if ($allHidden) {
dbUtils.setHiddenToSelection(false); // fileActions.setHiddenToSelection(false);
} else { // } else {
dbUtils.setHiddenToSelection(true); // fileActions.setHiddenToSelection(true);
} // }
e.preventDefault(); e.preventDefault();
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
if ($selection.size > 0) { // if ($selection.size > 0) {
centerMapOnSelection(); // centerMapOnSelection();
} // }
} else if (e.key === 'F1') { } else if (e.key === 'F1') {
switchBasemaps(); switchBasemaps();
e.preventDefault(); e.preventDefault();
@@ -633,13 +657,13 @@
toggleOverlays(); toggleOverlays();
e.preventDefault(); e.preventDefault();
} else if (e.key === 'F3') { } else if (e.key === 'F3') {
$distanceMarkers = !$distanceMarkers; distanceMarkers.value = !distanceMarkers.value;
e.preventDefault(); e.preventDefault();
} else if (e.key === 'F4') { } else if (e.key === 'F4') {
$directionMarkers = !$directionMarkers; directionMarkers.value = !directionMarkers.value;
e.preventDefault(); e.preventDefault();
} else if (e.key === 'F5') { } else if (e.key === 'F5') {
$routing = !$routing; routing.value = !routing.value;
e.preventDefault(); e.preventDefault();
} else if ( } else if (
e.key === 'ArrowRight' || e.key === 'ArrowRight' ||
@@ -648,7 +672,7 @@
e.key === 'ArrowUp' e.key === 'ArrowUp'
) { ) {
if (!targetInput) { if (!targetInput) {
updateSelectionFromKey(e.key === 'ArrowRight' || e.key === 'ArrowDown', e.shiftKey); // updateSelectionFromKey(e.key === 'ArrowRight' || e.key === 'ArrowDown', e.shiftKey);
e.preventDefault(); e.preventDefault();
} }
} }

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import GPXLayers from '$lib/components/map/gpx-layer/GPXLayers.svelte'; // import GPXLayers from '$lib/components/map/gpx-layer/GPXLayers.svelte';
import ElevationProfile from '$lib/components/ElevationProfile.svelte'; // import ElevationProfile from '$lib/components/ElevationProfile.svelte';
import FileList from '$lib/components/file-list/FileList.svelte'; // import FileList from '$lib/components/file-list/FileList.svelte';
import GPXStatistics from '$lib/components/GPXStatistics.svelte'; // import GPXStatistics from '$lib/components/GPXStatistics.svelte';
import Map from '$lib/components/map/Map.svelte'; import Map from '$lib/components/map/Map.svelte';
import { map } from '$lib/components/map/map.svelte'; import { map } from '$lib/components/map/utils.svelte';
import LayerControl from '$lib/components/map/layer-control/LayerControl.svelte'; // import LayerControl from '$lib/components/map/layer-control/LayerControl.svelte';
import OpenIn from '$lib/components/embedding/OpenIn.svelte'; import OpenIn from '$lib/components/embedding/OpenIn.svelte';
import { import {
gpxStatistics, gpxStatistics,
@@ -14,12 +14,10 @@
loadFile, loadFile,
updateGPXData, updateGPXData,
} from '$lib/stores'; } from '$lib/stores';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount, setContext } from 'svelte';
import { fileObservers, settings, GPXStatisticsTree } from '$lib/db';
import { readable } from 'svelte/store'; import { readable } from 'svelte/store';
import type { GPXFile } from 'gpx'; import type { GPXFile } from 'gpx';
import { selection } from '$lib/components/file-list/Selection'; import { ListFileItem } from '$lib/components/file-list/file-list';
import { ListFileItem } from '$lib/components/file-list/FileList';
import { import {
allowedEmbeddingBasemaps, allowedEmbeddingBasemaps,
getFilesFromEmbeddingOptions, getFilesFromEmbeddingOptions,
@@ -27,8 +25,16 @@
} from './Embedding'; } from './Embedding';
import { mode, setMode } from 'mode-watcher'; import { mode, setMode } from 'mode-watcher';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { settings } from '$lib/logic/settings.svelte';
import { fileStateCollection } from '$lib/logic/file-state.svelte';
$embedding = true; let {
useHash = true,
options = $bindable(),
hash,
}: { useHash?: boolean; options: EmbeddingOptions; hash: string } = $props();
setContext('embedding', true);
const { const {
currentBasemap, currentBasemap,
@@ -40,11 +46,14 @@
directionMarkers, directionMarkers,
} = settings; } = settings;
export let useHash = true; let prevSettings: {
export let options: EmbeddingOptions; distanceMarkers: boolean;
export let hash: string; directionMarkers: boolean;
distanceUnits: 'metric' | 'imperial' | 'nautical';
let prevSettings = { velocityUnits: 'speed' | 'pace';
temperatureUnits: 'celsius' | 'fahrenheit';
theme: 'light' | 'dark' | 'system';
} = {
distanceMarkers: false, distanceMarkers: false,
directionMarkers: false, directionMarkers: false,
distanceUnits: 'metric', distanceUnits: 'metric',
@@ -54,191 +63,173 @@
}; };
function applyOptions() { function applyOptions() {
fileObservers.update(($fileObservers) => { // fileObservers.update(($fileObservers) => {
$fileObservers.clear(); // $fileObservers.clear();
return $fileObservers; // return $fileObservers;
}); // });
// let downloads: Promise<GPXFile | null>[] = [];
let downloads: Promise<GPXFile | null>[] = []; // getFilesFromEmbeddingOptions(options).forEach((url) => {
getFilesFromEmbeddingOptions(options).forEach((url) => { // downloads.push(
downloads.push( // fetch(url)
fetch(url) // .then((response) => response.blob())
.then((response) => response.blob()) // .then((blob) => new File([blob], url.split('/').pop() ?? url))
.then((blob) => new File([blob], url.split('/').pop() ?? url)) // .then(loadFile)
.then(loadFile) // );
); // });
}); // Promise.all(downloads).then((files) => {
// let ids: string[] = [];
Promise.all(downloads).then((files) => { // let bounds = {
let ids: string[] = []; // southWest: {
let bounds = { // lat: 90,
southWest: { // lon: 180,
lat: 90, // },
lon: 180, // northEast: {
}, // lat: -90,
northEast: { // lon: -180,
lat: -90, // },
lon: -180, // };
}, // fileObservers.update(($fileObservers) => {
}; // files.forEach((file, index) => {
// if (file === null) {
fileObservers.update(($fileObservers) => { // return;
files.forEach((file, index) => { // }
if (file === null) { // let id = `gpx-${index}-embed`;
return; // file._data.id = id;
} // let statistics = new GPXStatisticsTree(file);
// $fileObservers.set(
let id = `gpx-${index}-embed`; // id,
file._data.id = id; // readable({
let statistics = new GPXStatisticsTree(file); // file,
// statistics,
$fileObservers.set( // })
id, // );
readable({ // ids.push(id);
file, // let fileBounds = statistics.getStatisticsFor(new ListFileItem(id)).global
statistics, // .bounds;
}) // bounds.southWest.lat = Math.min(bounds.southWest.lat, fileBounds.southWest.lat);
); // bounds.southWest.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon);
// bounds.northEast.lat = Math.max(bounds.northEast.lat, fileBounds.northEast.lat);
ids.push(id); // bounds.northEast.lon = Math.max(bounds.northEast.lon, fileBounds.northEast.lon);
let fileBounds = statistics.getStatisticsFor(new ListFileItem(id)).global // });
.bounds; // return $fileObservers;
// });
bounds.southWest.lat = Math.min(bounds.southWest.lat, fileBounds.southWest.lat); // $fileOrder = [...$fileOrder.filter((id) => !id.includes('embed')), ...ids];
bounds.southWest.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon); // selection.update(($selection) => {
bounds.northEast.lat = Math.max(bounds.northEast.lat, fileBounds.northEast.lat); // $selection.clear();
bounds.northEast.lon = Math.max(bounds.northEast.lon, fileBounds.northEast.lon); // ids.forEach((id) => {
}); // $selection.toggle(new ListFileItem(id));
// });
return $fileObservers; // return $selection;
}); // });
// if (hash.length === 0) {
$fileOrder = [...$fileOrder.filter((id) => !id.includes('embed')), ...ids]; // map.subscribe(($map) => {
// if ($map) {
selection.update(($selection) => { // $map.fitBounds(
$selection.clear(); // [
ids.forEach((id) => { // bounds.southWest.lon,
$selection.toggle(new ListFileItem(id)); // bounds.southWest.lat,
}); // bounds.northEast.lon,
return $selection; // bounds.northEast.lat,
}); // ],
// {
if (hash.length === 0) { // padding: 80,
map.subscribe(($map) => { // linear: true,
if ($map) { // easing: () => 1,
$map.fitBounds( // }
[ // );
bounds.southWest.lon, // }
bounds.southWest.lat, // });
bounds.northEast.lon, // }
bounds.northEast.lat, // });
], // if (
{ // options.basemap !== $currentBasemap &&
padding: 80, // allowedEmbeddingBasemaps.includes(options.basemap)
linear: true, // ) {
easing: () => 1, // $currentBasemap = options.basemap;
} // }
); // if (options.distanceMarkers !== $distanceMarkers) {
} // $distanceMarkers = options.distanceMarkers;
}); // }
} // if (options.directionMarkers !== $directionMarkers) {
}); // $directionMarkers = options.directionMarkers;
// }
if ( // if (options.distanceUnits !== $distanceUnits) {
options.basemap !== $currentBasemap && // $distanceUnits = options.distanceUnits;
allowedEmbeddingBasemaps.includes(options.basemap) // }
) { // if (options.velocityUnits !== $velocityUnits) {
$currentBasemap = options.basemap; // $velocityUnits = options.velocityUnits;
} // }
// if (options.temperatureUnits !== $temperatureUnits) {
if (options.distanceMarkers !== $distanceMarkers) { // $temperatureUnits = options.temperatureUnits;
$distanceMarkers = options.distanceMarkers; // }
} // if (options.theme !== $mode) {
// setMode(options.theme);
if (options.directionMarkers !== $directionMarkers) { // }
$directionMarkers = options.directionMarkers;
}
if (options.distanceUnits !== $distanceUnits) {
$distanceUnits = options.distanceUnits;
}
if (options.velocityUnits !== $velocityUnits) {
$velocityUnits = options.velocityUnits;
}
if (options.temperatureUnits !== $temperatureUnits) {
$temperatureUnits = options.temperatureUnits;
}
if (options.theme !== $mode) {
setMode(options.theme);
}
} }
onMount(() => { onMount(() => {
prevSettings.distanceMarkers = $distanceMarkers; prevSettings.distanceMarkers = distanceMarkers.value;
prevSettings.directionMarkers = $directionMarkers; prevSettings.directionMarkers = directionMarkers.value;
prevSettings.distanceUnits = $distanceUnits; prevSettings.distanceUnits = distanceUnits.value;
prevSettings.velocityUnits = $velocityUnits; prevSettings.velocityUnits = velocityUnits.value;
prevSettings.temperatureUnits = $temperatureUnits; prevSettings.temperatureUnits = temperatureUnits.value;
prevSettings.theme = $mode ?? 'system'; prevSettings.theme = mode.current ?? 'system';
}); });
$: if (browser && options) { // $: if (browser && options) {
applyOptions(); // applyOptions();
} // }
$: if ($fileOrder) { // $: if ($fileOrder) {
updateGPXData(); // updateGPXData();
} // }
onDestroy(() => { onDestroy(() => {
if ($distanceMarkers !== prevSettings.distanceMarkers) { if (distanceMarkers.value !== prevSettings.distanceMarkers) {
$distanceMarkers = prevSettings.distanceMarkers; distanceMarkers.value = prevSettings.distanceMarkers;
} }
if ($directionMarkers !== prevSettings.directionMarkers) { if (directionMarkers.value !== prevSettings.directionMarkers) {
$directionMarkers = prevSettings.directionMarkers; directionMarkers.value = prevSettings.directionMarkers;
} }
if ($distanceUnits !== prevSettings.distanceUnits) { if (distanceUnits.value !== prevSettings.distanceUnits) {
$distanceUnits = prevSettings.distanceUnits; distanceUnits.value = prevSettings.distanceUnits;
} }
if ($velocityUnits !== prevSettings.velocityUnits) { if (velocityUnits.value !== prevSettings.velocityUnits) {
$velocityUnits = prevSettings.velocityUnits; velocityUnits.value = prevSettings.velocityUnits;
} }
if ($temperatureUnits !== prevSettings.temperatureUnits) { if (temperatureUnits.value !== prevSettings.temperatureUnits) {
$temperatureUnits = prevSettings.temperatureUnits; temperatureUnits.value = prevSettings.temperatureUnits;
} }
if ($mode !== prevSettings.theme) { if (mode.current !== prevSettings.theme) {
setMode(prevSettings.theme); setMode(prevSettings.theme);
} }
$selection.clear(); // $selection.clear();
$fileObservers.clear(); // $fileObservers.clear();
$fileOrder = $fileOrder.filter((id) => !id.includes('embed')); fileOrder.value = fileOrder.value.filter((id) => !id.includes('embed'));
}); });
</script> </script>
<div class="absolute flex flex-col h-full w-full border rounded-xl overflow-clip"> <div class="absolute flex flex-col h-full w-full border rounded-xl overflow-clip">
<div class="grow relative"> <div class="grow relative">
<Map <Map
class="h-full {$fileObservers.size > 1 ? 'horizontal' : ''}" class="h-full {fileStateCollection.files.size > 1 ? 'horizontal' : ''}"
accessToken={options.token} accessToken={options.token}
geocoder={false} geocoder={false}
geolocate={false} geolocate={false}
hash={useHash} hash={useHash}
/> />
<OpenIn bind:files={options.files} bind:ids={options.ids} /> <OpenIn files={options.files} ids={options.ids} />
<LayerControl /> <!-- <LayerControl /> -->
<GPXLayers /> <!-- <GPXLayers /> -->
{#if $fileObservers.size > 1} {#if fileStateCollection.files.size > 1}
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30"> <div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
<FileList orientation="horizontal" /> <!-- <FileList orientation="horizontal" /> -->
</div> </div>
{/if} {/if}
</div> </div>
@@ -246,14 +237,14 @@
class="{options.elevation.show ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4" class="{options.elevation.show ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"
style={options.elevation.show ? `height: ${options.elevation.height}px` : ''} style={options.elevation.show ? `height: ${options.elevation.height}px` : ''}
> >
<GPXStatistics <!-- <GPXStatistics
{gpxStatistics} {gpxStatistics}
{slicedGPXStatistics} {slicedGPXStatistics}
panelSize={options.elevation.height} panelSize={options.elevation.height}
orientation={options.elevation.show ? 'vertical' : 'horizontal'} orientation={options.elevation.show ? 'vertical' : 'horizontal'}
/> /> -->
{#if options.elevation.show} {#if options.elevation.show}
<ElevationProfile <!-- <ElevationProfile
{gpxStatistics} {gpxStatistics}
{slicedGPXStatistics} {slicedGPXStatistics}
additionalDatasets={[ additionalDatasets={[
@@ -265,7 +256,7 @@
].filter((dataset) => dataset !== null)} ].filter((dataset) => dataset !== null)}
elevationFill={options.elevation.fill} elevationFill={options.elevation.fill}
showControls={options.elevation.controls} showControls={options.elevation.controls}
/> /> -->
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -11,8 +11,7 @@
exportState, exportState,
} from '$lib/components/export/utils.svelte'; } from '$lib/components/export/utils.svelte';
import { tool } from '$lib/components/toolbar/utils.svelte'; import { tool } from '$lib/components/toolbar/utils.svelte';
import { gpxStatistics } from '$lib/stores'; // import { gpxStatistics } from '$lib/stores';
import { fileObservers } from '$lib/db';
import { import {
Download, Download,
Zap, Zap,
@@ -23,10 +22,10 @@
SquareActivity, SquareActivity,
} from '@lucide/svelte'; } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { selection } from '$lib/components/file-list/Selection';
import { get } from 'svelte/store';
import { GPXStatistics } from 'gpx'; import { GPXStatistics } from 'gpx';
import { ListRootItem } from '$lib/components/file-list/FileList'; import { ListRootItem } from '$lib/components/file-list/file-list';
import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { selection } from '$lib/logic/selection.svelte';
let open = $derived(exportState.current !== ExportState.NONE); let open = $derived(exportState.current !== ExportState.NONE);
let exportOptions: Record<string, boolean> = $state({ let exportOptions: Record<string, boolean> = $state({
@@ -38,7 +37,36 @@
extensions: false, extensions: false,
}); });
let hide: Record<string, boolean> = $derived.by(() => { let hide: Record<string, boolean> = $derived.by(() => {
if (exportState.current === ExportState.NONE) { // if (exportState.current === ExportState.NONE) {
// return {
// time: false,
// hr: false,
// cad: false,
// atemp: false,
// power: false,
// extensions: false,
// };
// } else {
// let statistics = $gpxStatistics;
// if (exportState.current === ExportState.ALL) {
// statistics = Array.from(fileStateCollection.files.values())
// .map((file) => file.statistics)
// .reduce((acc, cur) => {
// if (cur !== undefined) {
// acc.mergeWith(cur.getStatisticsFor(new ListRootItem()));
// }
// return acc;
// }, new GPXStatistics());
// }
// return {
// time: statistics.global.time.total === 0,
// hr: statistics.global.hr.count === 0,
// cad: statistics.global.cad.count === 0,
// atemp: statistics.global.atemp.count === 0,
// power: statistics.global.power.count === 0,
// extensions: Object.keys(statistics.global.extensions).length === 0,
// };
// }
return { return {
time: false, time: false,
hr: false, hr: false,
@@ -47,27 +75,6 @@
power: false, power: false,
extensions: false, extensions: false,
}; };
} else {
let statistics = $gpxStatistics;
if (exportState.current === ExportState.ALL) {
statistics = Array.from($fileObservers.values())
.map((file) => get(file)?.statistics)
.reduce((acc, cur) => {
if (cur !== undefined) {
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()));
}
return acc;
}, new GPXStatistics());
}
return {
time: statistics.global.time.total === 0,
hr: statistics.global.hr.count === 0,
cad: statistics.global.cad.count === 0,
atemp: statistics.global.atemp.count === 0,
power: statistics.global.power.count === 0,
extensions: Object.keys(statistics.global.extensions).length === 0,
};
}
}); });
let exclude = $derived(Object.keys(exportOptions).filter((key) => !exportOptions[key])); let exclude = $derived(Object.keys(exportOptions).filter((key) => !exportOptions[key]));
@@ -118,7 +125,7 @@
}} }}
> >
<Download size="16" class="mr-1" /> <Download size="16" class="mr-1" />
{#if $fileObservers.size === 1 || (exportState.current === ExportState.SELECTION && $selection.size === 1)} {#if fileStateCollection.files.size === 1 || (exportState.current === ExportState.SELECTION && selection.value.size === 1)}
{i18n._('menu.download_file')} {i18n._('menu.download_file')}
{:else} {:else}
{i18n._('menu.download_files')} {i18n._('menu.download_files')}

View File

@@ -1,12 +1,10 @@
import { getFile, settings } from '$lib/db'; import { applyToOrderedSelectedItemsFromFile } from '$lib/logic/selection.svelte';
import { applyToOrderedSelectedItemsFromFile } from '$lib/components/file-list/Selection'; import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { get } from 'svelte/store'; import { settings } from '$lib/logic/settings.svelte';
import { buildGPX, type GPXFile } from 'gpx'; import { buildGPX, type GPXFile } from 'gpx';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import JSZip from 'jszip'; import JSZip from 'jszip';
const { fileOrder } = settings;
export enum ExportState { export enum ExportState {
NONE, NONE,
SELECTION, SELECTION,
@@ -22,7 +20,7 @@ async function exportFiles(fileIds: string[], exclude: string[]) {
} else { } else {
const firstFileId = fileIds.at(0); const firstFileId = fileIds.at(0);
if (firstFileId != null) { if (firstFileId != null) {
const file = getFile(firstFileId); const file = fileStateCollection.getFile(firstFileId);
if (file) { if (file) {
exportFile(file, exclude); exportFile(file, exclude);
} }
@@ -39,7 +37,7 @@ export async function exportSelectedFiles(exclude: string[]) {
} }
export async function exportAllFiles(exclude: string[]) { export async function exportAllFiles(exclude: string[]) {
await exportFiles(get(fileOrder), exclude); await exportFiles(settings.fileOrder.value, exclude);
} }
function exportFile(file: GPXFile, exclude: string[]) { function exportFile(file: GPXFile, exclude: string[]) {
@@ -50,7 +48,7 @@ function exportFile(file: GPXFile, exclude: string[]) {
async function exportFilesAsZip(fileIds: string[], exclude: string[]) { async function exportFilesAsZip(fileIds: string[], exclude: string[]) {
const zip = new JSZip(); const zip = new JSZip();
for (const fileId of fileIds) { for (const fileId of fileIds) {
const file = getFile(fileId); const file = fileStateCollection.getFile(fileId);
if (file) { if (file) {
const gpx = buildGPX(file, exclude); const gpx = buildGPX(file, exclude);
let filename = file.metadata.name; let filename = file.metadata.name;

View File

@@ -4,7 +4,7 @@
import FileListNode from './FileListNode.svelte'; import FileListNode from './FileListNode.svelte';
import { fileObservers, settings } from '$lib/db'; import { fileObservers, settings } from '$lib/db';
import { setContext } from 'svelte'; import { setContext } from 'svelte';
import { ListFileItem, ListLevel, ListRootItem, allowedPastes } from './FileList'; import { ListFileItem, ListLevel, ListRootItem, allowedPastes } from './file-list';
import { copied, pasteSelection, selectAll, selection } from './Selection'; import { copied, pasteSelection, selectAll, selection } from './Selection';
import { ClipboardPaste, FileStack, Plus } from '@lucide/svelte'; import { ClipboardPaste, FileStack, Plus } from '@lucide/svelte';
import Shortcut from '$lib/components/Shortcut.svelte'; import Shortcut from '$lib/components/Shortcut.svelte';

View File

@@ -20,7 +20,7 @@
ListWaypointsItem, ListWaypointsItem,
type ListItem, type ListItem,
type ListTrackItem, type ListTrackItem,
} from './FileList'; } from './file-list';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { selection } from './Selection'; import { selection } from './Selection';

View File

@@ -20,7 +20,7 @@
allowedMoves, allowedMoves,
moveItems, moveItems,
type ListItem, type ListItem,
} from './FileList'; } from './file-list';
import { selection } from './Selection'; import { selection } from './Selection';
import { isMac } from '$lib/utils'; import { isMac } from '$lib/utils';

View File

@@ -27,7 +27,7 @@
ListWaypointItem, ListWaypointItem,
allowedPastes, allowedPastes,
type ListItem, type ListItem,
} from './FileList'; } from './file-list';
import { import {
copied, copied,
copySelection, copySelection,
@@ -40,7 +40,7 @@
} from './Selection'; } from './Selection';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { allHidden, embedding, gpxLayers } from '$lib/stores'; import { allHidden, gpxLayers } from '$lib/stores';
import { map, centerMapOnSelection } from '$lib/components/map/map.svelte'; import { map, centerMapOnSelection } from '$lib/components/map/map.svelte';
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx'; import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
@@ -62,6 +62,7 @@
} = $props(); } = $props();
let orientation = getContext<'vertical' | 'horizontal'>('orientation'); let orientation = getContext<'vertical' | 'horizontal'>('orientation');
let embedding = getContext<boolean>('embedding');
let singleSelection = $derived($selection.size === 1); let singleSelection = $derived($selection.size === 1);
@@ -169,7 +170,7 @@
? 'text-muted-foreground' ? 'text-muted-foreground'
: ''}" : ''}"
oncontextmenu={(e) => { oncontextmenu={(e) => {
if ($embedding) { if (embedding) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
return; return;

View File

@@ -5,7 +5,7 @@
import type { GPXFileWithStatistics } from '$lib/db'; import type { GPXFileWithStatistics } from '$lib/db';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import type { Readable } from 'svelte/store'; import type { Readable } from 'svelte/store';
import { ListFileItem } from './FileList'; import { ListFileItem } from './file-list';
let { let {
file, file,

View File

@@ -1,375 +0,0 @@
import { get, writable } from 'svelte/store';
import {
ListFileItem,
ListItem,
ListRootItem,
ListTrackItem,
ListTrackSegmentItem,
ListWaypointItem,
ListLevel,
sortItems,
ListWaypointsItem,
moveItems,
} from './FileList';
import { fileObservers, getFile, getFileIds } from '$lib/db';
// import { settings } from '$lib/logic/settings.svelte';
export class SelectionTreeType {
item: ListItem;
selected: boolean;
children: {
[key: string | number]: SelectionTreeType;
};
size: number = 0;
constructor(item: ListItem) {
this.item = item;
this.selected = false;
this.children = {};
}
clear() {
this.selected = false;
for (let key in this.children) {
this.children[key].clear();
}
this.size = 0;
}
_setOrToggle(item: ListItem, value?: boolean) {
if (item.level === this.item.level) {
let newSelected = value === undefined ? !this.selected : value;
if (this.selected !== newSelected) {
this.selected = newSelected;
this.size += this.selected ? 1 : -1;
}
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (!this.children.hasOwnProperty(id)) {
this.children[id] = new SelectionTreeType(this.item.extend(id));
}
this.size -= this.children[id].size;
this.children[id]._setOrToggle(item, value);
this.size += this.children[id].size;
}
}
}
set(item: ListItem, value: boolean) {
this._setOrToggle(item, value);
}
toggle(item: ListItem) {
this._setOrToggle(item);
}
has(item: ListItem): boolean {
if (item.level === this.item.level) {
return this.selected;
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].has(item);
}
}
}
return false;
}
hasAnyParent(item: ListItem, self: boolean = true): boolean {
if (
this.selected &&
this.item.level <= item.level &&
(self || this.item.level < item.level)
) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyParent(item, self);
}
}
return false;
}
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
if (
this.selected &&
this.item.level >= item.level &&
(self || this.item.level > item.level)
) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (ignoreIds === undefined || ignoreIds.indexOf(id) === -1) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyChildren(item, self, ignoreIds);
}
}
} else {
for (let key in this.children) {
if (ignoreIds === undefined || ignoreIds.indexOf(key) === -1) {
if (this.children[key].hasAnyChildren(item, self, ignoreIds)) {
return true;
}
}
}
}
return false;
}
getSelected(selection: ListItem[] = []): ListItem[] {
if (this.selected) {
selection.push(this.item);
}
for (let key in this.children) {
this.children[key].getSelected(selection);
}
return selection;
}
forEach(callback: (item: ListItem) => void) {
if (this.selected) {
callback(this.item);
}
for (let key in this.children) {
this.children[key].forEach(callback);
}
}
getChild(id: string | number): SelectionTreeType | undefined {
return this.children[id];
}
deleteChild(id: string | number) {
if (this.children.hasOwnProperty(id)) {
this.size -= this.children[id].size;
delete this.children[id];
}
}
}
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
export function selectItem(item: ListItem) {
selection.update(($selection) => {
$selection.clear();
$selection.set(item, true);
return $selection;
});
}
export function selectFile(fileId: string) {
selectItem(new ListFileItem(fileId));
}
export function addSelectItem(item: ListItem) {
selection.update(($selection) => {
$selection.toggle(item);
return $selection;
});
}
export function addSelectFile(fileId: string) {
addSelectItem(new ListFileItem(fileId));
}
export function selectAll() {
selection.update(($selection) => {
let item: ListItem = new ListRootItem();
$selection.forEach((i) => {
item = i;
});
if (item instanceof ListRootItem || item instanceof ListFileItem) {
$selection.clear();
get(fileObservers).forEach((_file, fileId) => {
$selection.set(new ListFileItem(fileId), true);
});
} else if (item instanceof ListTrackItem) {
let file = getFile(item.getFileId());
if (file) {
file.trk.forEach((_track, trackId) => {
$selection.set(new ListTrackItem(item.getFileId(), trackId), true);
});
}
} else if (item instanceof ListTrackSegmentItem) {
let file = getFile(item.getFileId());
if (file) {
file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
$selection.set(
new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId),
true
);
});
}
} else if (item instanceof ListWaypointItem) {
let file = getFile(item.getFileId());
if (file) {
file.wpt.forEach((_waypoint, waypointId) => {
$selection.set(new ListWaypointItem(item.getFileId(), waypointId), true);
});
}
}
return $selection;
});
}
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
) {
// settings.fileOrder.value.forEach((fileId) => {
// let level: ListLevel | undefined = undefined;
// let items: ListItem[] = [];
// selectedItems.forEach((item) => {
// if (item.getFileId() === fileId) {
// level = item.level;
// if (
// item instanceof ListFileItem ||
// item instanceof ListTrackItem ||
// item instanceof ListTrackSegmentItem ||
// item instanceof ListWaypointsItem ||
// item instanceof ListWaypointItem
// ) {
// items.push(item);
// }
// }
// });
// if (items.length > 0) {
// sortItems(items, reverse);
// callback(fileId, level, items);
// }
// });
}
export function applyToOrderedSelectedItemsFromFile(
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
reverse: boolean = true
) {
applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse);
}
export const copied = writable<ListItem[] | undefined>(undefined);
export const cut = writable(false);
export function copySelection(): boolean {
let selected = get(selection).getSelected();
if (selected.length > 0) {
copied.set(selected);
cut.set(false);
return true;
}
return false;
}
export function cutSelection() {
if (copySelection()) {
cut.set(true);
}
}
function resetCopied() {
copied.set(undefined);
cut.set(false);
}
export function pasteSelection() {
let fromItems = get(copied);
if (fromItems === undefined || fromItems.length === 0) {
return;
}
let selected = get(selection).getSelected();
if (selected.length === 0) {
selected = [new ListRootItem()];
}
let fromParent = fromItems[0].getParent();
let toParent = selected[selected.length - 1];
let startIndex: number | undefined = undefined;
if (fromItems[0].level === toParent.level) {
if (
toParent instanceof ListTrackItem ||
toParent instanceof ListTrackSegmentItem ||
toParent instanceof ListWaypointItem
) {
startIndex = toParent.getId() + 1;
}
toParent = toParent.getParent();
}
let toItems: ListItem[] = [];
if (toParent.level === ListLevel.ROOT) {
let fileIds = getFileIds(fromItems.length);
fileIds.forEach((fileId) => {
toItems.push(new ListFileItem(fileId));
});
} else {
let toFile = getFile(toParent.getFileId());
if (toFile) {
fromItems.forEach((item, index) => {
if (toParent instanceof ListFileItem) {
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
toItems.push(
new ListTrackItem(
toParent.getFileId(),
(startIndex ?? toFile.trk.length) + index
)
);
} else if (item instanceof ListWaypointsItem) {
toItems.push(new ListWaypointsItem(toParent.getFileId()));
} else if (item instanceof ListWaypointItem) {
toItems.push(
new ListWaypointItem(
toParent.getFileId(),
(startIndex ?? toFile.wpt.length) + index
)
);
}
} else if (toParent instanceof ListTrackItem) {
if (item instanceof ListTrackSegmentItem) {
let toTrackIndex = toParent.getTrackIndex();
toItems.push(
new ListTrackSegmentItem(
toParent.getFileId(),
toTrackIndex,
(startIndex ?? toFile.trk[toTrackIndex].trkseg.length) + index
)
);
}
} else if (toParent instanceof ListWaypointsItem) {
if (item instanceof ListWaypointItem) {
toItems.push(
new ListWaypointItem(
toParent.getFileId(),
(startIndex ?? toFile.wpt.length) + index
)
);
}
}
});
}
}
if (fromItems.length === toItems.length) {
moveItems(fromParent, toParent, fromItems, toItems, get(cut));
resetCopied();
}
}

View File

@@ -1,8 +1,8 @@
import { dbUtils, getFile } from '$lib/db'; // import { dbUtils, getFile } from '$lib/db';
import { freeze } from 'immer'; // import { freeze } from 'immer';
import { GPXFile, Track, TrackSegment, Waypoint } from 'gpx'; // import { GPXFile, Track, TrackSegment, Waypoint } from 'gpx';
import { selection } from './Selection'; // import { selection } from './Selection';
import { newGPXFile } from '$lib/stores'; // import { newGPXFile } from '$lib/stores';
export enum ListLevel { export enum ListLevel {
ROOT, ROOT,
@@ -32,6 +32,7 @@ export const allowedPastes: Record<ListLevel, ListLevel[]> = {
}; };
export abstract class ListItem { export abstract class ListItem {
[x: string]: any;
level: ListLevel; level: ListLevel;
constructor(level: ListLevel) { constructor(level: ListLevel) {
@@ -321,163 +322,3 @@ export function sortItems(items: ListItem[], reverse: boolean = false) {
items.reverse(); items.reverse();
} }
} }
export function moveItems(
fromParent: ListItem,
toParent: ListItem,
fromItems: ListItem[],
toItems: ListItem[],
remove: boolean = true
) {
if (fromItems.length === 0) {
return;
}
sortItems(fromItems, false);
sortItems(toItems, false);
let context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[] = [];
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)[]) => {
fromItems.forEach((item) => {
if (item instanceof ListTrackItem) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
} else if (item instanceof ListTrackSegmentItem) {
file.replaceTrackSegments(
item.getTrackIndex(),
item.getSegmentIndex(),
item.getSegmentIndex(),
[]
);
} else if (item instanceof ListWaypointsItem) {
file.replaceWaypoints(0, file.wpt.length - 1, []);
} else if (item instanceof ListWaypointItem) {
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex(), []);
}
});
},
(file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
toItems.forEach((item, i) => {
if (item instanceof ListTrackItem) {
if (context[i] instanceof Track) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [
context[i],
]);
} else if (context[i] instanceof TrackSegment) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [
new Track({
trkseg: [context[i]],
}),
]);
}
} else if (
item instanceof ListTrackSegmentItem &&
context[i] instanceof TrackSegment
) {
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
) {
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, context[i]);
} else if (context[i] instanceof Waypoint) {
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]);
}
} else if (item instanceof ListWaypointItem && context[i] instanceof Waypoint) {
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [
context[i],
]);
}
});
},
];
if (fromParent instanceof ListRootItem) {
files = [];
callbacks = [];
} else if (!remove) {
files.splice(0, 1);
callbacks.splice(0, 1);
}
dbUtils.applyEachToFilesAndGlobal(
files,
callbacks,
(files, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
toItems.forEach((item, i) => {
if (item instanceof ListFileItem) {
if (context[i] instanceof GPXFile) {
let newFile = context[i];
if (remove) {
files.delete(newFile._data.id);
}
newFile._data.id = item.getFileId();
files.set(item.getFileId(), freeze(newFile));
} else if (context[i] instanceof Track) {
let newFile = newGPXFile();
newFile._data.id = item.getFileId();
if (context[i].name) {
newFile.metadata.name = context[i].name;
}
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.replaceTracks(0, 0, [
new Track({
trkseg: [context[i]],
}),
]);
files.set(item.getFileId(), freeze(newFile));
}
}
});
},
context
);
selection.update(($selection) => {
$selection.clear();
toItems.forEach((item) => {
$selection.set(item, true);
});
return $selection;
});
}

View File

@@ -6,7 +6,7 @@
import * as Popover from '$lib/components/ui/popover'; import * as Popover from '$lib/components/ui/popover';
import { dbUtils } from '$lib/db'; import { dbUtils } from '$lib/db';
import { Save } from '@lucide/svelte'; import { Save } from '@lucide/svelte';
import { ListFileItem, ListTrackItem, type ListItem } from '../FileList'; import { ListFileItem, ListTrackItem, type ListItem } from '../file-list';
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx'; import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte'; import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';

View File

@@ -6,7 +6,11 @@
import * as Popover from '$lib/components/ui/popover'; import * as Popover from '$lib/components/ui/popover';
import { dbUtils, getFile, settings } from '$lib/db'; import { dbUtils, getFile, settings } from '$lib/db';
import { Save } from '@lucide/svelte'; import { Save } from '@lucide/svelte';
import { ListFileItem, ListTrackItem, type ListItem } from '$lib/components/file-list/FileList'; import {
ListFileItem,
ListTrackItem,
type ListItem,
} from '$lib/components/file-list/file-list';
import { editStyle } from '$lib/components/file-list/style/utils.svelte'; import { editStyle } from '$lib/components/file-list/style/utils.svelte';
import { selection } from '../Selection'; import { selection } from '../Selection';
import { gpxLayers } from '$lib/stores'; import { gpxLayers } from '$lib/stores';

View File

@@ -3,7 +3,6 @@ import { settings, type GPXFileWithStatistics, dbUtils } from '$lib/db';
import { get, type Readable } from 'svelte/store'; import { get, type Readable } from 'svelte/store';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { waypointPopup, deleteWaypoint, trackpointPopup } from './GPXLayerPopup'; import { waypointPopup, deleteWaypoint, trackpointPopup } from './GPXLayerPopup';
import { addSelectItem, selectItem, selection } from '$lib/components/file-list/Selection';
import { import {
ListTrackSegmentItem, ListTrackSegmentItem,
ListWaypointItem, ListWaypointItem,
@@ -11,7 +10,7 @@ import {
ListTrackItem, ListTrackItem,
ListFileItem, ListFileItem,
ListRootItem, ListRootItem,
} from '$lib/components/file-list/FileList'; } from '$lib/components/file-list/file-list';
import { import {
getClosestLinePoint, getClosestLinePoint,
getElevation, getElevation,
@@ -20,7 +19,7 @@ import {
setPointerCursor, setPointerCursor,
setScissorsCursor, setScissorsCursor,
} from '$lib/utils'; } from '$lib/utils';
import { selectedWaypoint } from '$lib/components/toolbar/tools/Waypoint.svelte'; import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/utils.svelte';
import { MapPin, Square } from 'lucide-static'; import { MapPin, Square } from 'lucide-static';
import { getSymbolKey, symbols } from '$lib/assets/symbols'; import { getSymbolKey, symbols } from '$lib/assets/symbols';

View File

@@ -1,5 +1,4 @@
import type { LayerTreeType } from '$lib/assets/layers'; import type { LayerTreeType } from '$lib/assets/layers';
import { writable } from 'svelte/store';
export function anySelectedLayer(node: LayerTreeType) { export function anySelectedLayer(node: LayerTreeType) {
return ( return (

View File

@@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import { Tool, tool } from '$lib/components/toolbar/utils.svelte'; import { Tool, tool } from '$lib/components/toolbar/utils.svelte';
import { settings } from '$lib/db';
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte'; import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte'; import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
import Time from '$lib/components/toolbar/tools/Time.svelte'; import Time from '$lib/components/toolbar/tools/Time.svelte';
import Merge from '$lib/components/toolbar/tools/Merge.svelte'; import Merge from '$lib/components/toolbar/tools/Merge.svelte';
import Extract from '$lib/components/toolbar/tools/Extract.svelte'; import Extract from '$lib/components/toolbar/tools/Extract.svelte';
@@ -14,12 +13,20 @@
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte'; import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { settings } from '$lib/logic/settings.svelte';
let {
popupElement,
popup,
class: className = '',
}: {
popupElement: HTMLDivElement;
popup: mapboxgl.Popup;
class: string;
} = $props();
const { minimizeRoutingMenu } = settings; const { minimizeRoutingMenu } = settings;
let popupElement: HTMLElement;
let popup: mapboxgl.Popup;
onMount(() => { onMount(() => {
popup = new mapboxgl.Popup({ popup = new mapboxgl.Popup({
closeButton: false, closeButton: false,
@@ -31,12 +38,16 @@
</script> </script>
{#if tool.current !== null} {#if tool.current !== null}
<div class="translate-x-1 h-full animate-in animate-out {$$props.class ?? ''}"> <div class="translate-x-1 h-full animate-in animate-out {className}">
<div class="rounded-md shadow-md pointer-events-auto"> <div class="rounded-md shadow-md pointer-events-auto">
<Card.Root class="rounded-md border-none"> <Card.Root class="rounded-md border-none">
<Card.Content class="p-2.5"> <Card.Content class="p-2.5">
{#if tool.current === Tool.ROUTING} {#if tool.current === Tool.ROUTING}
<Routing {popup} {popupElement} bind:minimized={$minimizeRoutingMenu} /> <Routing
{popup}
{popupElement}
bind:minimized={minimizeRoutingMenu.value}
/>
{:else if tool.current === Tool.SCISSORS} {:else if tool.current === Tool.SCISSORS}
<Scissors /> <Scissors />
{:else if tool.current === Tool.WAYPOINT} {:else if tool.current === Tool.WAYPOINT}

View File

@@ -15,21 +15,25 @@
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils'; import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { Trash2 } from '@lucide/svelte'; import { Trash2 } from '@lucide/svelte';
import { map } from '$lib/components/map/map.svelte'; import { map } from '$lib/components/map/utils.svelte';
import { selection } from '$lib/components/file-list/Selection';
import { dbUtils } from '$lib/db';
import type { GeoJSONSource } from 'mapbox-gl'; import type { GeoJSONSource } from 'mapbox-gl';
import { selection } from '$lib/logic/selection.svelte';
import { fileActions } from '$lib/logic/file-actions.svelte';
let cleanType = CleanType.INSIDE; let props: {
let deleteTrackpoints = true; class?: string;
let deleteWaypoints = true; } = $props();
let rectangleCoordinates: mapboxgl.LngLat[] = [];
function updateRectangle() { let cleanType = $state(CleanType.INSIDE);
if (map.current) { let deleteTrackpoints = $state(true);
let deleteWaypoints = $state(true);
let rectangleCoordinates: mapboxgl.LngLat[] = $state([]);
$effect(() => {
if (map.value) {
if (rectangleCoordinates.length != 2) { if (rectangleCoordinates.length != 2) {
if (map.current.getLayer('rectangle')) { if (map.value.getLayer('rectangle')) {
map.current.removeLayer('rectangle'); map.value.removeLayer('rectangle');
} }
} else { } else {
let data: GeoJSON.Feature = { let data: GeoJSON.Feature = {
@@ -48,17 +52,17 @@
}, },
properties: {}, properties: {},
}; };
let source: GeoJSONSource | undefined = map.current.getSource('rectangle'); let source: GeoJSONSource | undefined = map.value.getSource('rectangle');
if (source) { if (source) {
source.setData(data); source.setData(data);
} else { } else {
map.current.addSource('rectangle', { map.value.addSource('rectangle', {
type: 'geojson', type: 'geojson',
data: data, data: data,
}); });
} }
if (!map.current.getLayer('rectangle')) { if (!map.value.getLayer('rectangle')) {
map.current.addLayer({ map.value.addLayer({
id: 'rectangle', id: 'rectangle',
type: 'fill', type: 'fill',
source: 'rectangle', source: 'rectangle',
@@ -70,11 +74,7 @@
} }
} }
} }
} });
$: if (rectangleCoordinates) {
updateRectangle();
}
let mousedown = false; let mousedown = false;
function onMouseDown(e: any) { function onMouseDown(e: any) {
@@ -93,42 +93,42 @@
} }
onMount(() => { onMount(() => {
if (map.current) { if (map.value) {
setCrosshairCursor(map.current.getCanvas()); setCrosshairCursor(map.value.getCanvas());
map.current.on('mousedown', onMouseDown); map.value.on('mousedown', onMouseDown);
map.current.on('mousemove', onMouseMove); map.value.on('mousemove', onMouseMove);
map.current.on('mouseup', onMouseUp); map.value.on('mouseup', onMouseUp);
map.current.on('touchstart', onMouseDown); map.value.on('touchstart', onMouseDown);
map.current.on('touchmove', onMouseMove); map.value.on('touchmove', onMouseMove);
map.current.on('touchend', onMouseUp); map.value.on('touchend', onMouseUp);
map.current.dragPan.disable(); map.value.dragPan.disable();
} }
}); });
onDestroy(() => { onDestroy(() => {
if (map.current) { if (map.value) {
resetCursor(map.current.getCanvas()); resetCursor(map.value.getCanvas());
map.current.off('mousedown', onMouseDown); map.value.off('mousedown', onMouseDown);
map.current.off('mousemove', onMouseMove); map.value.off('mousemove', onMouseMove);
map.current.off('mouseup', onMouseUp); map.value.off('mouseup', onMouseUp);
map.current.off('touchstart', onMouseDown); map.value.off('touchstart', onMouseDown);
map.current.off('touchmove', onMouseMove); map.value.off('touchmove', onMouseMove);
map.current.off('touchend', onMouseUp); map.value.off('touchend', onMouseUp);
map.current.dragPan.enable(); map.value.dragPan.enable();
if (map.current.getLayer('rectangle')) { if (map.value.getLayer('rectangle')) {
map.current.removeLayer('rectangle'); map.value.removeLayer('rectangle');
} }
if (map.current.getSource('rectangle')) { if (map.value.getSource('rectangle')) {
map.current.removeSource('rectangle'); map.value.removeSource('rectangle');
} }
} }
}); });
$: validSelection = $selection.size > 0; let validSelection = $derived(selection.value.size > 0);
</script> </script>
<div class="flex flex-col gap-3 w-full max-w-80 items-center {$$props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 items-center {props.class ?? ''}">
<fieldset class="flex flex-col gap-3"> <fieldset class="flex flex-col gap-3">
<div class="flex flex-row items-center gap-[6.4px] h-3"> <div class="flex flex-row items-center gap-[6.4px] h-3">
<Checkbox id="delete-trkpt" bind:checked={deleteTrackpoints} class="scale-90" /> <Checkbox id="delete-trkpt" bind:checked={deleteTrackpoints} class="scale-90" />
@@ -158,7 +158,7 @@
class="w-full" class="w-full"
disabled={!validSelection || rectangleCoordinates.length != 2} disabled={!validSelection || rectangleCoordinates.length != 2}
onclick={() => { onclick={() => {
dbUtils.cleanSelection( fileActions.cleanSelection(
[ [
{ {
lat: Math.min(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat), lat: Math.min(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat),

View File

@@ -1,18 +1,18 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { selection } from '$lib/components/file-list/Selection';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { MountainSnow } from '@lucide/svelte'; import { MountainSnow } from '@lucide/svelte';
import { dbUtils } from '$lib/db'; import { map } from '$lib/components/map/utils.svelte';
import { map } from '$lib/components/map/map.svelte';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection.svelte';
import { fileActions } from '$lib/logic/file-actions.svelte';
let props: { let props: {
class?: string; class?: string;
} = $props(); } = $props();
let validSelection = $derived($selection.size > 0); let validSelection = $derived(selection.value.size > 0);
</script> </script>
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
@@ -21,8 +21,8 @@
class="whitespace-normal h-fit" class="whitespace-normal h-fit"
disabled={!validSelection} disabled={!validSelection}
onclick={async () => { onclick={async () => {
if (map.current) { if (map.value) {
dbUtils.addElevationToSelection(map.current); fileActions.addElevationToSelection(map.value);
} }
}} }}
> >

View File

@@ -1,22 +1,27 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Ungroup } from '@lucide/svelte'; import { Ungroup } from '@lucide/svelte';
import { selection } from '$lib/components/file-list/Selection';
import { import {
ListFileItem, ListFileItem,
ListTrackItem, ListTrackItem,
ListTrackSegmentItem, ListTrackSegmentItem,
ListWaypointItem, ListWaypointItem,
ListWaypointsItem, ListWaypointsItem,
} from '$lib/components/file-list/FileList'; } from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { dbUtils, getFile } from '$lib/db';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection.svelte';
import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { fileActions } from '$lib/logic/file-actions.svelte';
$: validSelection = let props: {
$selection.size > 0 && class?: string;
$selection.getSelected().every((item) => { } = $props();
let validSelection = $derived(
selection.value.size > 0 &&
selection.value.getSelected().every((item) => {
if ( if (
item instanceof ListWaypointsItem || item instanceof ListWaypointsItem ||
item instanceof ListWaypointItem || item instanceof ListWaypointItem ||
@@ -24,7 +29,7 @@
) { ) {
return false; return false;
} }
let file = getFile(item.getFileId()); let file = fileStateCollection.getFile(item.getFileId());
if (file) { if (file) {
if (item instanceof ListFileItem) { if (item instanceof ListFileItem) {
return file.getSegments().length > 1; return file.getSegments().length > 1;
@@ -35,11 +40,12 @@
} }
} }
return false; return false;
}); })
);
</script> </script>
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
<Button variant="outline" disabled={!validSelection} onclick={dbUtils.extractSelection}> <Button variant="outline" disabled={!validSelection} onclick={fileActions.extractSelection}>
<Ungroup size="16" class="mr-1" /> <Ungroup size="16" class="mr-1" />
{i18n._('toolbar.extract.button')} {i18n._('toolbar.extract.button')}
</Button> </Button>

View File

@@ -6,58 +6,58 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList'; import { ListFileItem, ListTrackItem } from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { selection } from '$lib/components/file-list/Selection';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label/index.js'; import { Label } from '$lib/components/ui/label/index.js';
import { Checkbox } from '$lib/components/ui/checkbox'; import { Checkbox } from '$lib/components/ui/checkbox';
import * as RadioGroup from '$lib/components/ui/radio-group'; import * as RadioGroup from '$lib/components/ui/radio-group';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { dbUtils, getFile } from '$lib/db';
import { Group } from '@lucide/svelte'; import { Group } from '@lucide/svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import Shortcut from '$lib/components/Shortcut.svelte'; import Shortcut from '$lib/components/Shortcut.svelte';
import { gpxStatistics } from '$lib/stores'; import { gpxStatistics } from '$lib/stores';
import { selection } from '$lib/logic/selection.svelte';
import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { fileActions } from '$lib/logic/file-actions.svelte';
let canMergeTraces = false; let props: {
let canMergeContents = false; class?: string;
let removeGaps = false; } = $props();
$: if ($selection.size > 1) { let canMergeTraces = $derived.by(() => {
canMergeTraces = true; if (selection.value.size > 1) {
} else if ($selection.size === 1) { return true;
let selected = $selection.getSelected()[0]; } else if (selection.value.size === 1) {
let selected = selection.value.getSelected()[0];
if (selected instanceof ListFileItem) { if (selected instanceof ListFileItem) {
let file = getFile(selected.getFileId()); let file = fileStateCollection.getFile(selected.getFileId());
if (file) { if (file) {
canMergeTraces = file.getSegments().length > 1; return file.getSegments().length > 1;
} else {
canMergeTraces = false;
} }
} else if (selected instanceof ListTrackItem) { } else if (selected instanceof ListTrackItem) {
let trackIndex = selected.getTrackIndex(); let trackIndex = selected.getTrackIndex();
let file = getFile(selected.getFileId()); let file = fileStateCollection.getFile(selected.getFileId());
if (file && trackIndex < file.trk.length) { if (file && trackIndex < file.trk.length) {
canMergeTraces = file.trk[trackIndex].getSegments().length > 1; return file.trk[trackIndex].getSegments().length > 1;
} else {
canMergeTraces = false;
}
} else {
canMergeContents = false;
} }
} }
return false;
}
});
$: canMergeContents = let canMergeContents = $derived(
$selection.size > 1 && selection.value.size > 1 &&
$selection selection.value
.getSelected() .getSelected()
.some((item) => item instanceof ListFileItem || item instanceof ListTrackItem); .some((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
);
let mergeType = MergeType.TRACES; let removeGaps = $state(false);
let mergeType = $state(MergeType.TRACES);
</script> </script>
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
<RadioGroup.Root bind:value={mergeType}> <RadioGroup.Root bind:value={mergeType}>
<Label class="flex flex-row items-center gap-1.5 leading-5"> <Label class="flex flex-row items-center gap-1.5 leading-5">
<RadioGroup.Item value={MergeType.TRACES} /> <RadioGroup.Item value={MergeType.TRACES} />
@@ -80,7 +80,7 @@
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) || disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
(mergeType === MergeType.CONTENTS && !canMergeContents)} (mergeType === MergeType.CONTENTS && !canMergeContents)}
onclick={() => { onclick={() => {
dbUtils.mergeSelection( fileActions.mergeSelection(
mergeType === MergeType.TRACES, mergeType === MergeType.TRACES,
mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0 && removeGaps mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0 && removeGaps
); );

View File

@@ -2,22 +2,22 @@
import { Label } from '$lib/components/ui/label/index.js'; import { Label } from '$lib/components/ui/label/index.js';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Slider } from '$lib/components/ui/slider'; import { Slider } from '$lib/components/ui/slider';
import { selection } from '$lib/components/file-list/Selection';
import { import {
ListItem, ListItem,
ListRootItem, ListRootItem,
ListTrackSegmentItem, ListTrackSegmentItem,
} from '$lib/components/file-list/FileList'; } from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import WithUnits from '$lib/components/WithUnits.svelte'; import WithUnits from '$lib/components/WithUnits.svelte';
import { dbUtils, fileObservers } from '$lib/db'; import { map } from '$lib/components/map/utils.svelte';
import { map } from '$lib/components/map/map.svelte';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx'; import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import type { GeoJSONSource } from 'mapbox-gl'; import type { GeoJSONSource } from 'mapbox-gl';
import { selection } from '$lib/logic/selection.svelte';
import { fileActions } from '$lib/logic/file-actions.svelte';
let props: { class?: string } = $props(); let props: { class?: string } = $props();
@@ -28,7 +28,7 @@
const maxTolerance = 10000; const maxTolerance = 10000;
let validSelection = $derived( let validSelection = $derived(
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
); );
let tolerance = $derived( let tolerance = $derived(
minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance))) minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)))
@@ -67,18 +67,18 @@
}); });
}); });
if (map.current) { if (map.value) {
let source: GeoJSONSource | undefined = map.current.getSource('simplified'); let source: GeoJSONSource | undefined = map.value.getSource('simplified');
if (source) { if (source) {
source.setData(data); source.setData(data);
} else { } else {
map.current.addSource('simplified', { map.value.addSource('simplified', {
type: 'geojson', type: 'geojson',
data: data, data: data,
}); });
} }
if (!map.current.getLayer('simplified')) { if (!map.value.getLayer('simplified')) {
map.current.addLayer({ map.value.addLayer({
id: 'simplified', id: 'simplified',
type: 'line', type: 'line',
source: 'simplified', source: 'simplified',
@@ -88,52 +88,52 @@
}, },
}); });
} else { } else {
map.current.moveLayer('simplified'); map.value.moveLayer('simplified');
} }
} }
} }
$effect(() => { // $effect(() => {
if ($fileObservers) { // if ($fileObservers) {
unsubscribes.forEach((unsubscribe, fileId) => { // unsubscribes.forEach((unsubscribe, fileId) => {
if (!$fileObservers.has(fileId)) { // if (!$fileObservers.has(fileId)) {
unsubscribe(); // unsubscribe();
unsubscribes.delete(fileId); // unsubscribes.delete(fileId);
} // }
}); // });
$fileObservers.forEach((fileStore, fileId) => { // $fileObservers.forEach((fileStore, fileId) => {
if (!unsubscribes.has(fileId)) { // if (!unsubscribes.has(fileId)) {
let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [ // let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [
fs, // fs,
sel, // sel,
]).subscribe(([fs, sel]) => { // ]).subscribe(([fs, sel]) => {
if (fs) { // if (fs) {
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => { // fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
let segmentItem = new ListTrackSegmentItem( // let segmentItem = new ListTrackSegmentItem(
fileId, // fileId,
trackIndex, // trackIndex,
segmentIndex // segmentIndex
); // );
if (sel.hasAnyParent(segmentItem)) { // if (sel.hasAnyParent(segmentItem)) {
let statistics = fs.statistics.getStatisticsFor(segmentItem); // let statistics = fs.statistics.getStatisticsFor(segmentItem);
simplified.set(segmentItem.getFullId(), [ // simplified.set(segmentItem.getFullId(), [
segmentItem, // segmentItem,
statistics.local.points.length, // statistics.local.points.length,
ramerDouglasPeucker(statistics.local.points, minTolerance), // ramerDouglasPeucker(statistics.local.points, minTolerance),
]); // ]);
update(); // update();
} else if (simplified.has(segmentItem.getFullId())) { // } else if (simplified.has(segmentItem.getFullId())) {
simplified.delete(segmentItem.getFullId()); // simplified.delete(segmentItem.getFullId());
update(); // update();
} // }
}); // });
} // }
}); // });
unsubscribes.set(fileId, unsubscribe); // unsubscribes.set(fileId, unsubscribe);
} // }
}); // });
} // }
}); // });
$effect(() => { $effect(() => {
if (tolerance) { if (tolerance) {
@@ -142,12 +142,12 @@
}); });
onDestroy(() => { onDestroy(() => {
if (map.current) { if (map.value) {
if (map.current.getLayer('simplified')) { if (map.value.getLayer('simplified')) {
map.current.removeLayer('simplified'); map.value.removeLayer('simplified');
} }
if (map.current.getSource('simplified')) { if (map.value.getSource('simplified')) {
map.current.removeSource('simplified'); map.value.removeSource('simplified');
} }
} }
unsubscribes.forEach((unsubscribe) => unsubscribe()); unsubscribes.forEach((unsubscribe) => unsubscribe());
@@ -164,7 +164,7 @@
.map((point) => point.point) .map((point) => point.point)
); );
}); });
dbUtils.reduce(itemsAndPoints); fileActions.reduce(itemsAndPoints);
} }
</script> </script>

View File

@@ -5,7 +5,6 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Checkbox } from '$lib/components/ui/checkbox'; import { Checkbox } from '$lib/components/ui/checkbox';
import TimePicker from '$lib/components/ui/time-picker/TimePicker.svelte'; import TimePicker from '$lib/components/ui/time-picker/TimePicker.svelte';
import { dbUtils, settings } from '$lib/db';
import { gpxStatistics } from '$lib/stores'; import { gpxStatistics } from '$lib/stores';
import { import {
distancePerHourToSecondsPerDistance, distancePerHourToSecondsPerDistance,
@@ -17,15 +16,22 @@
import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from '@lucide/svelte'; import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from '@lucide/svelte';
import { tick } from 'svelte'; import { tick } from 'svelte';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { selection } from '$lib/components/file-list/Selection';
import { import {
ListFileItem, ListFileItem,
ListRootItem, ListRootItem,
ListTrackItem, ListTrackItem,
ListTrackSegmentItem, ListTrackSegmentItem,
} from '$lib/components/file-list/FileList'; } from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection.svelte';
import { settings } from '$lib/logic/settings.svelte';
import { fileActions } from '$lib/logic/file-actions.svelte';
import { fileActionManager } from '$lib/logic/file-action-manager.svelte';
let props: {
class?: string;
} = $props();
let startDate: DateValue | undefined = undefined; let startDate: DateValue | undefined = undefined;
let startTime: string | undefined = undefined; let startTime: string | undefined = undefined;
@@ -47,7 +53,7 @@
function setSpeed(value: number) { function setSpeed(value: number) {
let speedValue = getConvertedVelocity(value); let speedValue = getConvertedVelocity(value);
if ($velocityUnits === 'speed') { if (velocityUnits.value === 'speed') {
speedValue = parseFloat(speedValue.toFixed(2)); speedValue = parseFloat(speedValue.toFixed(2));
} }
speed = speedValue; speed = speedValue;
@@ -80,9 +86,9 @@
} }
} }
$: if ($gpxStatistics && $velocityUnits && $distanceUnits) { // $: if ($gpxStatistics && $velocityUnits && $distanceUnits) {
setGPXData(); // setGPXData();
} // }
function getDate(date: DateValue, time: string): Date { function getDate(date: DateValue, time: string): Date {
if (date === undefined) { if (date === undefined) {
@@ -133,12 +139,12 @@
} }
let speedValue = speed; let speedValue = speed;
if ($velocityUnits === 'pace') { if (velocityUnits.value === 'pace') {
speedValue = distancePerHourToSecondsPerDistance(speed); speedValue = distancePerHourToSecondsPerDistance(speed);
} }
if ($distanceUnits === 'imperial') { if (distanceUnits.value === 'imperial') {
speedValue = milesToKilometers(speedValue); speedValue = milesToKilometers(speedValue);
} else if ($distanceUnits === 'nautical') { } else if (distanceUnits.value === 'nautical') {
speedValue = nauticalMilesToKilometers(speedValue); speedValue = nauticalMilesToKilometers(speedValue);
} }
return speedValue; return speedValue;
@@ -171,24 +177,26 @@
updateEnd(); updateEnd();
} }
$: canUpdate = let canUpdate = $derived(
$selection.size === 1 && $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']); selection.value.size === 1 &&
selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
);
</script> </script>
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
<fieldset class="flex flex-col gap-2"> <fieldset class="flex flex-col gap-2">
<div class="flex flex-row gap-2 justify-center"> <div class="flex flex-row gap-2 justify-center">
<div class="flex flex-col gap-2 grow"> <div class="flex flex-col gap-2 grow">
<Label for="speed" class="flex flex-row"> <Label for="speed" class="flex flex-row">
<Zap size="16" class="mr-1" /> <Zap size="16" class="mr-1" />
{#if $velocityUnits === 'speed'} {#if velocityUnits.value === 'speed'}
{i18n._('quantities.speed')} {i18n._('quantities.speed')}
{:else} {:else}
{i18n._('quantities.pace')} {i18n._('quantities.pace')}
{/if} {/if}
</Label> </Label>
<div class="flex flex-row gap-1 items-center"> <div class="flex flex-row gap-1 items-center">
{#if $velocityUnits === 'speed'} {#if velocityUnits.value === 'speed'}
<Input <Input
id="speed" id="speed"
type="number" type="number"
@@ -199,11 +207,11 @@
onchange={updateDataFromSpeed} onchange={updateDataFromSpeed}
/> />
<span class="text-sm shrink-0"> <span class="text-sm shrink-0">
{#if $distanceUnits === 'imperial'} {#if distanceUnits.value === 'imperial'}
{i18n._('units.miles_per_hour')} {i18n._('units.miles_per_hour')}
{:else if $distanceUnits === 'metric'} {:else if distanceUnits.value === 'metric'}
{i18n._('units.kilometers_per_hour')} {i18n._('units.kilometers_per_hour')}
{:else if $distanceUnits === 'nautical'} {:else if distanceUnits.value === 'nautical'}
{i18n._('units.knots')} {i18n._('units.knots')}
{/if} {/if}
</span> </span>
@@ -215,11 +223,11 @@
onChange={updateDataFromSpeed} onChange={updateDataFromSpeed}
/> />
<span class="text-sm shrink-0"> <span class="text-sm shrink-0">
{#if $distanceUnits === 'imperial'} {#if distanceUnits.value === 'imperial'}
{i18n._('units.minutes_per_mile')} {i18n._('units.minutes_per_mile')}
{:else if $distanceUnits === 'metric'} {:else if distanceUnits.value === 'metric'}
{i18n._('units.minutes_per_kilometer')} {i18n._('units.minutes_per_kilometer')}
{:else if $distanceUnits === 'nautical'} {:else if distanceUnits.value === 'nautical'}
{i18n._('units.minutes_per_nautical_mile')} {i18n._('units.minutes_per_nautical_mile')}
{/if} {/if}
</span> </span>
@@ -260,7 +268,7 @@
disabled={!canUpdate} disabled={!canUpdate}
bind:value={startTime} bind:value={startTime}
class="w-fit" class="w-fit"
on:change={updateEnd} onchange={updateEnd}
/> />
</div> </div>
<Label class="flex flex-row"> <Label class="flex flex-row">
@@ -285,7 +293,7 @@
disabled={!canUpdate} disabled={!canUpdate}
bind:value={endTime} bind:value={endTime}
class="w-fit" class="w-fit"
on:change={updateStart} onchange={updateStart}
/> />
</div> </div>
{#if $gpxStatistics.global.time.moving === 0 || $gpxStatistics.global.time.moving === undefined} {#if $gpxStatistics.global.time.moving === 0 || $gpxStatistics.global.time.moving === undefined}
@@ -324,9 +332,9 @@
ratio = $gpxStatistics.global.speed.moving / effectiveSpeed; ratio = $gpxStatistics.global.speed.moving / effectiveSpeed;
} }
let item = $selection.getSelected()[0]; let item = selection.value.getSelected()[0];
let fileId = item.getFileId(); let fileId = item.getFileId();
dbUtils.applyToFile(fileId, (file) => { fileActionManager.applyToFile(fileId, (file) => {
if (item instanceof ListFileItem) { if (item instanceof ListFileItem) {
if (artificial || !$gpxStatistics.global.time.moving) { if (artificial || !$gpxStatistics.global.time.moving) {
file.createArtificialTimestamps( file.createArtificialTimestamps(

View File

@@ -21,65 +21,75 @@
SquareArrowUpLeft, SquareArrowUpLeft,
SquareArrowOutDownRight, SquareArrowOutDownRight,
} from '@lucide/svelte'; } from '@lucide/svelte';
import { brouterProfiles } from '$lib/components/toolbar/tools/routing/utils.svelte';
import { map, newGPXFile, routingControls, selectFileWhenLoaded } from '$lib/stores';
import { dbUtils, getFile, getFileIds, settings } from '$lib/db';
import { brouterProfiles } from '$lib/components/toolbar/tools/routing/routing.svelte';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { RoutingControls } from './RoutingControls'; // import { RoutingControls } from './RoutingControls';
import mapboxgl from 'mapbox-gl';
import { fileObservers } from '$lib/db';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { getOrderedSelection, selection } from '$lib/components/file-list/Selection';
import { import {
ListFileItem, ListFileItem,
ListRootItem, ListRootItem,
ListTrackItem, ListTrackItem,
ListTrackSegmentItem, ListTrackSegmentItem,
type ListItem, type ListItem,
} from '$lib/components/file-list/FileList'; } from '$lib/components/file-list/file-list';
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils'; import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { TrackPoint } from 'gpx'; import { TrackPoint } from 'gpx';
import { settings } from '$lib/logic/settings.svelte';
import { map } from '$lib/components/map/utils.svelte';
import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { selection } from '$lib/logic/selection.svelte';
import { fileActions, getFileIds, newGPXFile } from '$lib/logic/file-actions.svelte';
let {
minimized = $bindable(false),
minimizable = true,
popup = undefined,
popupElement = undefined,
class: className = '',
}: {
minimized?: boolean;
minimizable?: boolean;
popup?: mapboxgl.Popup;
popupElement?: HTMLDivElement;
class?: string;
} = $props();
export let minimized = false;
export let minimizable = true;
export let popup: mapboxgl.Popup | undefined = undefined;
export let popupElement: HTMLElement | undefined = undefined;
let selectedItem: ListItem | null = null; let selectedItem: ListItem | null = null;
const { privateRoads, routing, routingProfile } = settings; const { privateRoads, routing, routingProfile } = settings;
$: if ($map && popup && popupElement) { // $: if (map && popup && popupElement) {
// remove controls for deleted files // // remove controls for deleted files
routingControls.forEach((controls, fileId) => { // routingControls.forEach((controls, fileId) => {
if (!$fileObservers.has(fileId)) { // if (!$fileObservers.has(fileId)) {
controls.destroy(); // controls.destroy();
routingControls.delete(fileId); // routingControls.delete(fileId);
if (selectedItem && selectedItem.getFileId() === fileId) { // if (selectedItem && selectedItem.getFileId() === fileId) {
selectedItem = null; // selectedItem = null;
} // }
} else if ($map !== controls.map) { // } else if ($map !== controls.map) {
controls.updateMap($map); // controls.updateMap($map);
} // }
}); // });
// add controls for new files // // add controls for new files
$fileObservers.forEach((file, fileId) => { // fileStateCollection.files.forEach((file, fileId) => {
if (!routingControls.has(fileId)) { // if (!routingControls.has(fileId)) {
routingControls.set( // routingControls.set(
fileId, // fileId,
new RoutingControls($map, fileId, file, popup, popupElement) // new RoutingControls($map, fileId, file, popup, popupElement)
// );
// }
// });
// }
let validSelection = $derived(
selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
); );
}
});
}
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
function createFileWithPoint(e: any) { function createFileWithPoint(e: any) {
if ($selection.size === 0) { if (selection.value.size === 0) {
let file = newGPXFile(); let file = newGPXFile();
file.replaceTrackPoints(0, 0, 0, 0, [ file.replaceTrackPoints(0, 0, 0, 0, [
new TrackPoint({ new TrackPoint({
@@ -90,22 +100,22 @@
}), }),
]); ]);
file._data.id = getFileIds(1)[0]; file._data.id = getFileIds(1)[0];
dbUtils.add(file); fileActions.add(file);
selectFileWhenLoaded(file._data.id); // selectFileWhenLoaded(file._data.id);
} }
} }
onMount(() => { onMount(() => {
setCrosshairCursor(); // setCrosshairCursor();
$map?.on('click', createFileWithPoint); map.value?.on('click', createFileWithPoint);
}); });
onDestroy(() => { onDestroy(() => {
resetCursor(); // resetCursor();
$map?.off('click', createFileWithPoint); map.value?.off('click', createFileWithPoint);
routingControls.forEach((controls) => controls.destroy()); // routingControls.forEach((controls) => controls.destroy());
routingControls.clear(); // routingControls.clear();
}); });
</script> </script>
@@ -116,11 +126,11 @@
</Button> </Button>
</div> </div>
{:else} {:else}
<div class="flex flex-col gap-3 w-full max-w-80 animate-in animate-out {$$props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 animate-in animate-out {className ?? ''}">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<Label class="flex flex-row justify-between items-center gap-2"> <Label class="flex flex-row justify-between items-center gap-2">
<span class="flex flex-row items-center gap-1"> <span class="flex flex-row items-center gap-1">
{#if $routing} {#if routing.value}
<Route size="16" /> <Route size="16" />
{:else} {:else}
<RouteOff size="16" /> <RouteOff size="16" />
@@ -128,28 +138,28 @@
{i18n._('toolbar.routing.use_routing')} {i18n._('toolbar.routing.use_routing')}
</span> </span>
<Tooltip label={i18n._('toolbar.routing.use_routing_tooltip')}> <Tooltip label={i18n._('toolbar.routing.use_routing_tooltip')}>
<Switch class="scale-90" bind:checked={$routing} /> <Switch class="scale-90" bind:checked={routing.value} />
<Shortcut slot="extra" key="F5" /> <Shortcut slot="extra" key="F5" />
</Tooltip> </Tooltip>
</Label> </Label>
{#if $routing} {#if routing.value}
<div class="flex flex-col gap-3" in:slide> <div class="flex flex-col gap-3" in:slide>
<Label class="flex flex-row justify-between items-center gap-2"> <Label class="flex flex-row justify-between items-center gap-2">
<span class="shrink-0 flex flex-row items-center gap-1"> <span class="shrink-0 flex flex-row items-center gap-1">
{#if $routingProfile.includes('bike') || $routingProfile.includes('motorcycle')} {#if routingProfile.value.includes('bike') || routingProfile.value.includes('motorcycle')}
<Bike size="16" /> <Bike size="16" />
{:else if $routingProfile.includes('foot')} {:else if routingProfile.value.includes('foot')}
<Footprints size="16" /> <Footprints size="16" />
{:else if $routingProfile.includes('water')} {:else if routingProfile.value.includes('water')}
<Waves size="16" /> <Waves size="16" />
{:else if $routingProfile.includes('railway')} {:else if routingProfile.value.includes('railway')}
<TrainFront size="16" /> <TrainFront size="16" />
{/if} {/if}
{i18n._('toolbar.routing.activity')} {i18n._('toolbar.routing.activity')}
</span> </span>
<Select.Root type="single" bind:value={$routingProfile}> <Select.Root type="single" bind:value={routingProfile.value}>
<Select.Trigger class="h-8 grow"> <Select.Trigger class="h-8 grow">
{i18n._(`toolbar.routing.activities.${$routingProfile}`)} {i18n._(`toolbar.routing.activities.${routingProfile.value}`)}
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
{#each Object.keys(brouterProfiles) as profile} {#each Object.keys(brouterProfiles) as profile}
@@ -167,7 +177,7 @@
<TriangleAlert size="16" /> <TriangleAlert size="16" />
{i18n._('toolbar.routing.allow_private')} {i18n._('toolbar.routing.allow_private')}
</span> </span>
<Switch class="scale-90" bind:checked={$privateRoads} /> <Switch class="scale-90" bind:checked={privateRoads.value} />
</Label> </Label>
</div> </div>
{/if} {/if}
@@ -178,7 +188,7 @@
variant="outline" variant="outline"
class="flex flex-row gap-1 text-xs px-2" class="flex flex-row gap-1 text-xs px-2"
disabled={!validSelection} disabled={!validSelection}
onclick={dbUtils.reverseSelection} onclick={fileActions.reverseSelection}
> >
<ArrowRightLeft size="12" />{i18n._('toolbar.routing.reverse.button')} <ArrowRightLeft size="12" />{i18n._('toolbar.routing.reverse.button')}
</ButtonWithTooltip> </ButtonWithTooltip>
@@ -188,10 +198,10 @@
class="flex flex-row gap-1 text-xs px-2" class="flex flex-row gap-1 text-xs px-2"
disabled={!validSelection} disabled={!validSelection}
onclick={() => { onclick={() => {
const selected = getOrderedSelection(); const selected = selection.getOrderedSelection();
if (selected.length > 0) { if (selected.length > 0) {
const firstFileId = selected[0].getFileId(); const firstFileId = selected[0].getFileId();
const firstFile = getFile(firstFileId); const firstFile = fileStateCollection.getFile(firstFileId);
if (firstFile) { if (firstFile) {
let start = (() => { let start = (() => {
if (selected[0] instanceof ListFileItem) { if (selected[0] instanceof ListFileItem) {
@@ -208,9 +218,9 @@
if (start !== undefined) { if (start !== undefined) {
const lastFileId = selected[selected.length - 1].getFileId(); const lastFileId = selected[selected.length - 1].getFileId();
routingControls // routingControls
.get(lastFileId) // .get(lastFileId)
?.appendAnchorWithCoordinates(start.getCoordinates()); // ?.appendAnchorWithCoordinates(start.getCoordinates());
} }
} }
} }

View File

@@ -1,18 +1,15 @@
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx'; import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx';
import { get, writable, type Readable } from 'svelte/store'; import { get, writable, type Readable } from 'svelte/store';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { route } from './routing.svelte'; import { route } from './utils.svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { i18n } from '$lib/i18n.svelte';
import { dbUtils, settings, type GPXFileWithStatistics } from '$lib/db';
import { getOrderedSelection, selection } from '$lib/components/file-list/Selection';
import { import {
ListFileItem, ListFileItem,
ListTrackItem, ListTrackItem,
ListTrackSegmentItem, ListTrackSegmentItem,
} from '$lib/components/file-list/FileList'; } from '$lib/components/file-list/file-list';
import { currentTool, streetViewEnabled, Tool } from '$lib/stores';
import { getClosestLinePoint, resetCursor, setGrabbingCursor } from '$lib/utils'; import { getClosestLinePoint, resetCursor, setGrabbingCursor } from '$lib/utils';
import type { GPXFileWithStatistics } from '$lib/logic/statistics';
// const { streetViewSource } = settings; // const { streetViewSource } = settings;
export const canChangeStart = writable(false); export const canChangeStart = writable(false);

View File

@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import { splitAs, SplitType } from '$lib/components/toolbar/tools/scissors/utils.svelte'; import { splitAs, SplitType } from '$lib/components/toolbar/tools/scissors/utils.svelte';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { ListRootItem } from '$lib/components/file-list/FileList'; import { ListRootItem } from '$lib/components/file-list/file-list';
import { selection } from '$lib/components/file-list/Selection';
import { Label } from '$lib/components/ui/label/index.js'; import { Label } from '$lib/components/ui/label/index.js';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Slider } from '$lib/components/ui/slider'; import { Slider } from '$lib/components/ui/slider';
@@ -15,8 +14,9 @@
import { onDestroy, tick } from 'svelte'; import { onDestroy, tick } from 'svelte';
import { Crop } from '@lucide/svelte'; import { Crop } from '@lucide/svelte';
import { dbUtils } from '$lib/db'; import { dbUtils } from '$lib/db';
import { SplitControls } from './SplitControls.svelte'; import { SplitControls } from './split-controls';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection.svelte';
let props: { let props: {
class?: string; class?: string;
@@ -35,7 +35,7 @@
}); });
let validSelection = $derived( let validSelection = $derived(
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) && selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
$gpxStatistics.local.points.length > 0 $gpxStatistics.local.points.length > 0
); );

View File

@@ -1,16 +1,12 @@
import { TrackPoint, TrackSegment } from 'gpx'; import { TrackPoint, TrackSegment } from 'gpx';
import { get } from 'svelte/store';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { dbUtils, getFile } from '$lib/db'; import { dbUtils, getFile } from '$lib/db';
import { import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
applyToOrderedSelectedItemsFromFile,
selection,
} from '$lib/components/file-list/Selection';
import { ListTrackSegmentItem } from '$lib/components/file-list/FileList';
import { gpxStatistics } from '$lib/stores'; import { gpxStatistics } from '$lib/stores';
import { tool, Tool } from '$lib/components/toolbar/utils.svelte'; import { tool, Tool } from '$lib/components/toolbar/utils.svelte';
import { splitAs } from '$lib/components/toolbar/tools/scissors/utils.svelte'; import { splitAs } from '$lib/components/toolbar/tools/scissors/utils.svelte';
import { Scissors } from 'lucide-static'; import { Scissors } from 'lucide-static';
import { applyToOrderedSelectedItemsFromFile, selection } from '$lib/logic/selection.svelte';
export class SplitControls { export class SplitControls {
active: boolean = false; active: boolean = false;
@@ -25,10 +21,9 @@ export class SplitControls {
constructor(map: mapboxgl.Map) { constructor(map: mapboxgl.Map) {
this.map = map; this.map = map;
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this))); this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
$effect(() => { $effect(() => {
tool.current, this.addIfNeeded.bind(this); tool.current, selection.value, this.addIfNeeded.bind(this);
}); });
} }
@@ -64,7 +59,7 @@ export class SplitControls {
if (file) { if (file) {
file.forEachSegment((segment, trackIndex, segmentIndex) => { file.forEachSegment((segment, trackIndex, segmentIndex) => {
if ( if (
get(selection).hasAnyParent( selection.value.hasAnyParent(
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex) new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
) )
) { ) {

View File

@@ -1,19 +1,11 @@
<script lang="ts" context="module">
import { writable } from 'svelte/store';
export const selectedWaypoint = writable<[Waypoint, string] | undefined>(undefined);
</script>
<script lang="ts"> <script lang="ts">
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import { Textarea } from '$lib/components/ui/textarea'; import { Textarea } from '$lib/components/ui/textarea';
import { Label } from '$lib/components/ui/label/index.js'; import { Label } from '$lib/components/ui/label/index.js';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Select from '$lib/components/ui/select'; import * as Select from '$lib/components/ui/select';
import { selection } from '$lib/components/file-list/Selection';
import { Waypoint } from 'gpx';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { ListWaypointItem } from '$lib/components/file-list/FileList'; import { ListWaypointItem } from '$lib/components/file-list/file-list';
import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db'; import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
@@ -22,61 +14,21 @@
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils'; import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { MapPin, CircleX, Save } from '@lucide/svelte'; import { MapPin, CircleX, Save } from '@lucide/svelte';
import { getSymbolKey, symbols } from '$lib/assets/symbols'; import { getSymbolKey, symbols } from '$lib/assets/symbols';
import { selection } from '$lib/logic/selection.svelte';
import { selectedWaypoint } from './utils.svelte';
let name: string; let props: {
let description: string; class?: string;
let link: string; } = $props();
let longitude: number;
let latitude: number;
let symbolKey: string;
const { treeFileView } = settings; let name = $state('');
let description = $state('');
let link = $state('');
let symbolKey = $state('');
let longitude = $state(0);
let latitude = $state(0);
$: canCreate = $selection.size > 0; let canCreate = $derived(selection.value.size > 0);
$: if ($treeFileView && $selection) {
selectedWaypoint.update(() => {
if ($selection.size === 1) {
let item = $selection.getSelected()[0];
if (item instanceof ListWaypointItem) {
let file = getFile(item.getFileId());
let waypoint = file?.wpt[item.getWaypointIndex()];
if (waypoint) {
return [waypoint, item.getFileId()];
}
}
}
return undefined;
});
}
let unsubscribe: (() => void) | undefined = undefined;
function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) {
if ($selectedWaypoint) {
if (fileStore) {
if ($selectedWaypoint[0]._data.index < fileStore.file.wpt.length) {
$selectedWaypoint[0] = fileStore.file.wpt[$selectedWaypoint[0]._data.index];
name = $selectedWaypoint[0].name ?? '';
description = $selectedWaypoint[0].desc ?? '';
if (
$selectedWaypoint[0].cmt !== undefined &&
$selectedWaypoint[0].cmt !== $selectedWaypoint[0].desc
) {
description += '\n\n' + $selectedWaypoint[0].cmt;
}
link = $selectedWaypoint[0].link?.attributes?.href ?? '';
let symbol = $selectedWaypoint[0].sym ?? '';
symbolKey = getSymbolKey(symbol) ?? symbol ?? '';
longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
} else {
selectedWaypoint.set(undefined);
}
} else {
selectedWaypoint.set(undefined);
}
}
}
function resetWaypointData() { function resetWaypointData() {
name = ''; name = '';
@@ -87,21 +39,6 @@
latitude = 0; latitude = 0;
} }
$: {
if (unsubscribe) {
unsubscribe();
unsubscribe = undefined;
}
if ($selectedWaypoint) {
let fileStore = get(fileObservers).get($selectedWaypoint[1]);
if (fileStore) {
unsubscribe = fileStore.subscribe(updateWaypointData);
}
} else {
resetWaypointData();
}
}
function createOrUpdateWaypoint() { function createOrUpdateWaypoint() {
if (typeof latitude === 'string') { if (typeof latitude === 'string') {
latitude = parseFloat(latitude); latitude = parseFloat(latitude);
@@ -124,12 +61,12 @@
link: link.length > 0 ? { attributes: { href: link } } : undefined, link: link.length > 0 ? { attributes: { href: link } } : undefined,
sym: symbols[symbolKey]?.value ?? '', sym: symbols[symbolKey]?.value ?? '',
}, },
$selectedWaypoint selectedWaypoint.wpt && selectedWaypoint.fileId
? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index) ? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
: undefined : undefined
); );
selectedWaypoint.set(undefined); selectedWaypoint.reset();
resetWaypointData(); resetWaypointData();
} }
@@ -138,49 +75,46 @@
longitude = e.lngLat.lng.toFixed(6); longitude = e.lngLat.lng.toFixed(6);
} }
$: sortedSymbols = Object.entries(symbols).sort((a, b) => { let sortedSymbols = $derived(
return i18n._(`gpx.symbol.${a[0]}`).localeCompare(i18n._(`gpx.symbol.${b[0]}`), i18n.lang); Object.entries(symbols).sort((a, b) => {
}); return i18n
._(`gpx.symbol.${a[0]}`)
.localeCompare(i18n._(`gpx.symbol.${b[0]}`), i18n.lang);
})
);
onMount(() => { onMount(() => {
let m = get(map); map.value?.on('click', setCoordinates);
m?.on('click', setCoordinates); // setCrosshairCursor();
setCrosshairCursor();
}); });
onDestroy(() => { onDestroy(() => {
let m = get(map); map.value?.off('click', setCoordinates);
m?.off('click', setCoordinates); // resetCursor();
resetCursor();
if (unsubscribe) {
unsubscribe();
unsubscribe = undefined;
}
}); });
</script> </script>
<div class="flex flex-col gap-3 w-full max-w-96 {$$props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-96 {props.class ?? ''}">
<fieldset class="flex flex-col gap-2"> <fieldset class="flex flex-col gap-2">
<Label for="name">{i18n._('menu.metadata.name')}</Label> <Label for="name">{i18n._('menu.metadata.name')}</Label>
<Input <Input
bind:value={name} bind:value={name}
id="name" id="name"
class="font-semibold h-8" class="font-semibold h-8"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !selectedWaypoint.wpt}
/> />
<Label for="description">{i18n._('menu.metadata.description')}</Label> <Label for="description">{i18n._('menu.metadata.description')}</Label>
<Textarea <Textarea
bind:value={description} bind:value={description}
id="description" id="description"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !selectedWaypoint.wpt}
/> />
<Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label> <Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label>
<Select.Root bind:value={symbolKey} type="single"> <Select.Root bind:value={symbolKey} type="single">
<Select.Trigger <Select.Trigger
id="symbol" id="symbol"
class="w-full h-8" class="w-full h-8"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !selectedWaypoint.wpt}
> >
{#if symbolKey in symbols} {#if symbolKey in symbols}
{i18n._(`gpx.symbol.${symbolKey}`)} {i18n._(`gpx.symbol.${symbolKey}`)}
@@ -212,7 +146,7 @@
bind:value={link} bind:value={link}
id="link" id="link"
class="h-8" class="h-8"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !selectedWaypoint.wpt}
/> />
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<div class="grow"> <div class="grow">
@@ -225,7 +159,7 @@
min={-90} min={-90}
max={90} max={90}
class="text-xs h-8" class="text-xs h-8"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !selectedWaypoint.wpt}
/> />
</div> </div>
<div class="grow"> <div class="grow">
@@ -238,7 +172,7 @@
min={-180} min={-180}
max={180} max={180}
class="text-xs h-8" class="text-xs h-8"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !selectedWaypoint.wpt}
/> />
</div> </div>
</div> </div>
@@ -246,11 +180,11 @@
<div class="flex flex-row gap-2 items-center"> <div class="flex flex-row gap-2 items-center">
<Button <Button
variant="outline" variant="outline"
disabled={!canCreate && !$selectedWaypoint} disabled={!canCreate && !selectedWaypoint.wpt}
class="grow whitespace-normal h-fit" class="grow whitespace-normal h-fit"
onclick={createOrUpdateWaypoint} onclick={createOrUpdateWaypoint}
> >
{#if $selectedWaypoint} {#if selectedWaypoint.wpt}
<Save size="16" class="mr-1 shrink-0" /> <Save size="16" class="mr-1 shrink-0" />
{i18n._('menu.metadata.save')} {i18n._('menu.metadata.save')}
{:else} {:else}
@@ -261,7 +195,7 @@
<Button <Button
variant="outline" variant="outline"
onclick={() => { onclick={() => {
selectedWaypoint.set(undefined); selectedWaypoint.reset();
resetWaypointData(); resetWaypointData();
}} }}
> >
@@ -269,7 +203,7 @@
</Button> </Button>
</div> </div>
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/poi')}> <Help link={getURLForLanguage(i18n.lang, '/help/toolbar/poi')}>
{#if $selectedWaypoint || canCreate} {#if selectedWaypoint.wpt || canCreate}
{i18n._('toolbar.waypoint.help')} {i18n._('toolbar.waypoint.help')}
{:else} {:else}
{i18n._('toolbar.waypoint.help_no_selection')} {i18n._('toolbar.waypoint.help_no_selection')}

View File

@@ -0,0 +1,67 @@
import { ListWaypointItem } from '$lib/components/file-list/file-list';
import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { selection } from '$lib/logic/selection.svelte';
import { settings } from '$lib/logic/settings.svelte';
import type { Waypoint } from 'gpx';
export class WaypointSelection {
private _selection: [Waypoint, string] | undefined;
constructor() {
this._selection = $derived.by(() => {
if (settings.treeFileView.value && selection.value.size === 1) {
let item = selection.value.getSelected()[0];
if (item instanceof ListWaypointItem) {
let file = fileStateCollection.getFile(item.getFileId());
let waypoint = file?.wpt[item.getWaypointIndex()];
if (waypoint) {
return [waypoint, item.getFileId()];
}
}
}
return undefined;
});
}
reset() {
this._selection = undefined;
}
get wpt(): Waypoint | undefined {
return this._selection ? this._selection[0] : undefined;
}
get fileId(): string | undefined {
return this._selection ? this._selection[1] : undefined;
}
// TODO update the waypoint data if the file changes
// function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) {
// if (selectedWaypoint.wpt) {
// if (fileStore) {
// if ($selectedWaypoint[0]._data.index < fileStore.file.wpt.length) {
// $selectedWaypoint[0] = fileStore.file.wpt[$selectedWaypoint[0]._data.index];
// name = $selectedWaypoint[0].name ?? '';
// description = $selectedWaypoint[0].desc ?? '';
// if (
// $selectedWaypoint[0].cmt !== undefined &&
// $selectedWaypoint[0].cmt !== $selectedWaypoint[0].desc
// ) {
// description += '\n\n' + $selectedWaypoint[0].cmt;
// }
// link = $selectedWaypoint[0].link?.attributes?.href ?? '';
// let symbol = $selectedWaypoint[0].sym ?? '';
// symbolKey = getSymbolKey(symbol) ?? symbol ?? '';
// longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
// latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
// } else {
// selectedWaypoint.reset();
// }
// } else {
// selectedWaypoint.reset();
// }
// }
// }
}
export const selectedWaypoint = new WaypointSelection();

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Punts d'interès
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Body zájmu
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Interessante Orte
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Puntos de interés
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Interesguneak
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points d'intérêt
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Nevezetes helyek
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Punti di interesse
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Svarbios vietos
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Interessante punten (POI's)
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Pontos de interesse
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Точки интереса
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: İlgi alanları
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: 创建或编辑兴趣点
<script> <script>
import { MapPin } from '@lucide/svelte'; import { MapPin } from '@lucide/svelte';
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
</script> </script>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title } # <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -0,0 +1,206 @@
import { db, type Database } from '$lib/db';
import { liveQuery } from 'dexie';
import type { GPXFile } from 'gpx';
import { applyPatches, produceWithPatches, type Patch, type WritableDraft } from 'immer';
import { fileStateCollection, type GPXFileStateCollection } from '$lib/logic/file-state.svelte';
const MAX_PATCHES = 100;
export class FileActionManager {
private _db: Database;
private _files: Map<string, GPXFile>;
private _patchIndex: number;
private _patchMinIndex: number;
private _patchMaxIndex: number;
constructor(db: Database, fileStateCollection: GPXFileStateCollection) {
this._db = db;
this._files = $derived.by(() => {
let files = new Map<string, GPXFile>();
fileStateCollection.files.forEach((state, id) => {
if (state.file) {
files.set(id, state.file);
}
});
return files;
});
this._patchIndex = $state(-1);
this._patchMinIndex = $state(0);
this._patchMaxIndex = $state(0);
liveQuery(() => db.settings.get('patchIndex')).subscribe((value) => {
if (value !== undefined) {
this._patchIndex = value;
}
});
liveQuery(() =>
(db.patches.orderBy(':id').keys() as Promise<number[]>).then((keys) => {
if (keys.length === 0) {
return { min: 0, max: 0 };
} else {
return { min: keys[0], max: keys[keys.length - 1] + 1 };
}
})
).subscribe((value) => {
this._patchMinIndex = value.min;
this._patchMaxIndex = value.max;
});
}
async store(patch: Patch[], inversePatch: Patch[]) {
this._db.patches.where(':id').above(this._patchIndex).delete(); // Delete all patches after the current patch to avoid redoing them
if (this._patchMaxIndex - this._patchMinIndex + 1 > MAX_PATCHES) {
this._db.patches
.where(':id')
.belowOrEqual(this._patchMaxIndex - MAX_PATCHES)
.delete();
}
this._db.transaction('rw', this._db.patches, this._db.settings, async () => {
let index = this._patchIndex + 1;
await this._db.patches.put(
{
patch,
inversePatch,
index,
},
index
);
await this._db.settings.put(index, 'patchIndex');
});
}
get canUndo(): boolean {
return this._patchIndex >= this._patchMinIndex;
}
get canRedo(): boolean {
return this._patchIndex < this._patchMaxIndex - 1;
}
undo() {
if (this.canUndo) {
this._db.patches.get(this._patchIndex).then((patch) => {
if (patch) {
this.apply(patch.inversePatch);
this._db.settings.put(this._patchIndex - 1, 'patchIndex');
}
});
}
}
redo() {
if (this.canRedo) {
this._db.patches.get(this._patchIndex + 1).then((patch) => {
if (patch) {
this.apply(patch.patch);
this._db.settings.put(this._patchIndex + 1, 'patchIndex');
}
});
}
}
apply(patch: Patch[]) {
let newFiles = applyPatches(this._files, patch);
return this.commitFileStateChange(newFiles, patch);
}
commitFileStateChange(newFiles: ReadonlyMap<string, GPXFile>, patch: Patch[]) {
let changedFileIds = getChangedFileIds(patch);
let updatedFileIds: string[] = [],
deletedFileIds: string[] = [];
changedFileIds.forEach((id) => {
if (newFiles.has(id)) {
updatedFileIds.push(id);
} else {
deletedFileIds.push(id);
}
});
let updatedFiles = updatedFileIds
.map((id) => newFiles.get(id))
.filter((file) => file !== undefined) as GPXFile[];
updatedFileIds = updatedFiles.map((file) => file._data.id);
// updateSelection(updatedFiles, deletedFileIds);
// @ts-ignore
return db.transaction('rw', db.fileids, db.files, async () => {
if (updatedFileIds.length > 0) {
await this._db.fileids.bulkPut(updatedFileIds, updatedFileIds);
await this._db.files.bulkPut(updatedFiles, updatedFileIds);
}
if (deletedFileIds.length > 0) {
await this._db.fileids.bulkDelete(deletedFileIds);
await this._db.files.bulkDelete(deletedFileIds);
}
});
}
applyGlobal(callback: (files: Map<string, GPXFile>) => void) {
const [newFileCollection, patch, inversePatch] = produceWithPatches(this._files, callback);
this.store(patch, inversePatch);
return this.commitFileStateChange(newFileCollection, patch);
}
applyToFiles(fileIds: string[], callback: (file: WritableDraft<GPXFile>) => void) {
const [newFileCollection, patch, inversePatch] = produceWithPatches(
this._files,
(draft) => {
fileIds.forEach((fileId) => {
let file = draft.get(fileId);
if (file) {
callback(file);
}
});
}
);
this.store(patch, inversePatch);
return this.commitFileStateChange(newFileCollection, patch);
}
applyToFile(fileId: string, callback: (file: WritableDraft<GPXFile>) => void) {
return this.applyToFiles([fileId], callback);
}
applyEachToFilesAndGlobal(
fileIds: string[],
callbacks: ((file: WritableDraft<GPXFile>, context?: any) => void)[],
globalCallback: (files: Map<string, GPXFile>, context?: any) => void,
context?: any
) {
const [newFileCollection, patch, inversePatch] = produceWithPatches(
this._files,
(draft) => {
fileIds.forEach((fileId, index) => {
let file = draft.get(fileId);
if (file) {
callbacks[index](file, context);
}
});
globalCallback(draft, context);
}
);
this.store(patch, inversePatch);
return this.commitFileStateChange(newFileCollection, patch);
}
}
// Get the file ids of the files that have changed in the patch
function getChangedFileIds(patch: Patch[]): string[] {
let changedFileIds = new Set<string>();
for (let p of patch) {
changedFileIds.add(p.path[0] as string);
}
return Array.from(changedFileIds);
}
export const fileActionManager = new FileActionManager(db, fileStateCollection);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,128 @@
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify';
import { db, type Database } from '$lib/db';
import { liveQuery } from 'dexie';
import { GPXFile } from 'gpx';
import { GPXStatisticsTree, type GPXFileWithStatistics } from '$lib/logic/statistics';
import { settings } from '$lib/logic/settings.svelte';
// Observe a single file from the database, and maintain its statistics
class GPXFileState {
private _file: GPXFileWithStatistics | undefined;
private _subscription: { unsubscribe: () => void } | undefined;
constructor(db: Database, fileId: string) {
this._file = $state(undefined);
let first = true;
this._subscription = liveQuery(() => db.files.get(fileId)).subscribe((value) => {
if (value !== undefined) {
let file = new GPXFile(value);
updateAnchorPoints(file);
let statistics = new GPXStatisticsTree(file);
if (first) {
// Update the map bounds for new files
// updateTargetMapBounds(
// id,
// statistics.getStatisticsFor(new ListFileItem(id)).global.bounds
// );
first = false;
}
this._file = { file, statistics };
// if (get(selection).hasAnyChildren(new ListFileItem(id))) {
// updateAllHidden();
// }
}
});
}
destroy() {
this._subscription?.unsubscribe();
this._subscription = undefined;
this._file = undefined;
}
get file(): GPXFile | undefined {
return this._file?.file;
}
get statistics(): GPXStatisticsTree | undefined {
return this._file?.statistics;
}
}
// Observe the file ids in the database, and maintain a map of file states for the corresponding files
export class GPXFileStateCollection {
private _db: Database;
private _files: Map<string, GPXFileState>;
constructor(db: Database) {
this._db = db;
this._files = $state(new Map());
}
initialize(fitBounds: boolean) {
let initialize = true;
liveQuery(() => this._db.fileids.toArray()).subscribe((dbFileIds) => {
if (initialize) {
// if (fitBounds && dbFileIds.length > 0) {
// initTargetMapBounds(dbFileIds);
// }
initialize = false;
}
// Find new files to observe
let newFiles = dbFileIds
.filter((id) => !this._files.has(id))
.sort((a, b) => parseInt(a.split('-')[1]) - parseInt(b.split('-')[1]));
// Find deleted files to stop observing
let deletedFiles = Array.from(this._files.keys()).filter(
(id) => !dbFileIds.find((fileId) => fileId === id)
);
if (newFiles.length > 0 || deletedFiles.length > 0) {
// Update the map of file states
let files = new Map(this._files);
newFiles.forEach((id) => {
files.set(id, new GPXFileState(this._db, id));
});
deletedFiles.forEach((id) => {
files.get(id)?.destroy();
files.delete(id);
});
this._files = files;
// Update the file order
let fileOrder = settings.fileOrder.value.filter((id) => !deletedFiles.includes(id));
newFiles.forEach((id) => {
if (!fileOrder.includes(id)) {
fileOrder.push(id);
}
});
settings.fileOrder.value = fileOrder;
}
});
}
get files(): ReadonlyMap<string, GPXFileState> {
return this._files;
}
get size(): number {
return this._files.size;
}
getFile(fileId: string): GPXFile | undefined {
let fileState = this._files.get(fileId);
return fileState?.file;
}
getStatistics(fileId: string): GPXStatisticsTree | undefined {
let fileState = this._files.get(fileId);
return fileState?.statistics;
}
}
// Collection of all file states
export const fileStateCollection = new GPXFileStateCollection(db);

View File

@@ -1,40 +0,0 @@
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify';
import { GPXStatisticsTree, type Database } from '$lib/db';
import { liveQuery } from 'dexie';
import { GPXFile } from 'gpx';
class GPXFileState {
private _db: Database;
private _file: GPXFile | undefined;
constructor(db: Database, fileId: string, file: GPXFile) {
this._db = db;
this._file = $state(undefined);
liveQuery(() => db.files.get(fileId)).subscribe((value) => {
if (value !== undefined) {
let gpx = new GPXFile(value);
updateAnchorPoints(gpx);
let statistics = new GPXStatisticsTree(gpx);
if (!fileState.has(id)) {
// Update the map bounds for new files
updateTargetMapBounds(
id,
statistics.getStatisticsFor(new ListFileItem(id)).global.bounds
);
}
fileState.set(id, gpx);
store.set({
file: gpx,
statistics,
});
if (get(selection).hasAnyChildren(new ListFileItem(id))) {
updateAllHidden();
}
}
});
}
}

View File

@@ -1,4 +1,3 @@
import { get, writable } from 'svelte/store';
import { import {
ListFileItem, ListFileItem,
ListItem, ListItem,
@@ -9,224 +8,188 @@ import {
ListLevel, ListLevel,
sortItems, sortItems,
ListWaypointsItem, ListWaypointsItem,
moveItems, } from '$lib/components/file-list/file-list';
} from './FileList'; import { SelectionTreeType } from '$lib/logic/selection';
import { fileObservers, getFile, getFileIds, settings } from '$lib/db'; import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { settings } from '$lib/logic/settings.svelte';
import type { GPXFile } from 'gpx';
export class SelectionTreeType { export class Selection {
item: ListItem; private _selection: SelectionTreeType;
selected: boolean; private _copied: ListItem[] | undefined;
children: { private _cut: boolean;
[key: string | number]: SelectionTreeType;
};
size: number = 0;
constructor(item: ListItem) { constructor() {
this.item = item; this._selection = $state(new SelectionTreeType(new ListRootItem()));
this.selected = false; this._copied = $state(undefined);
this.children = {}; this._cut = $state(false);
} }
clear() { get value(): SelectionTreeType {
this.selected = false; return this._selection;
for (let key in this.children) {
this.children[key].clear();
}
this.size = 0;
} }
_setOrToggle(item: ListItem, value?: boolean) { selectItem(item: ListItem) {
if (item.level === this.item.level) { let selection = new SelectionTreeType(new ListRootItem());
let newSelected = value === undefined ? !this.selected : value; selection.set(item, true);
if (this.selected !== newSelected) { this._selection = selection;
this.selected = newSelected;
this.size += this.selected ? 1 : -1;
}
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (!this.children.hasOwnProperty(id)) {
this.children[id] = new SelectionTreeType(this.item.extend(id));
}
this.size -= this.children[id].size;
this.children[id]._setOrToggle(item, value);
this.size += this.children[id].size;
}
}
} }
set(item: ListItem, value: boolean) { selectFile(fileId: string) {
this._setOrToggle(item, value); this.selectItem(new ListFileItem(fileId));
} }
toggle(item: ListItem) { addSelectItem(item: ListItem) {
this._setOrToggle(item); this._selection.toggle(item);
} }
has(item: ListItem): boolean { addSelectFile(fileId: string) {
if (item.level === this.item.level) { this.addSelectItem(new ListFileItem(fileId));
return this.selected;
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].has(item);
}
}
}
return false;
} }
hasAnyParent(item: ListItem, self: boolean = true): boolean { selectAll() {
if (
this.selected &&
this.item.level <= item.level &&
(self || this.item.level < item.level)
) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyParent(item, self);
}
}
return false;
}
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
if (
this.selected &&
this.item.level >= item.level &&
(self || this.item.level > item.level)
) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (ignoreIds === undefined || ignoreIds.indexOf(id) === -1) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyChildren(item, self, ignoreIds);
}
}
} else {
for (let key in this.children) {
if (ignoreIds === undefined || ignoreIds.indexOf(key) === -1) {
if (this.children[key].hasAnyChildren(item, self, ignoreIds)) {
return true;
}
}
}
}
return false;
}
getSelected(selection: ListItem[] = []): ListItem[] {
if (this.selected) {
selection.push(this.item);
}
for (let key in this.children) {
this.children[key].getSelected(selection);
}
return selection;
}
forEach(callback: (item: ListItem) => void) {
if (this.selected) {
callback(this.item);
}
for (let key in this.children) {
this.children[key].forEach(callback);
}
}
getChild(id: string | number): SelectionTreeType | undefined {
return this.children[id];
}
deleteChild(id: string | number) {
if (this.children.hasOwnProperty(id)) {
this.size -= this.children[id].size;
delete this.children[id];
}
}
}
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
export function selectItem(item: ListItem) {
selection.update(($selection) => {
$selection.clear();
$selection.set(item, true);
return $selection;
});
}
export function selectFile(fileId: string) {
selectItem(new ListFileItem(fileId));
}
export function addSelectItem(item: ListItem) {
selection.update(($selection) => {
$selection.toggle(item);
return $selection;
});
}
export function addSelectFile(fileId: string) {
addSelectItem(new ListFileItem(fileId));
}
export function selectAll() {
selection.update(($selection) => {
let item: ListItem = new ListRootItem(); let item: ListItem = new ListRootItem();
$selection.forEach((i) => { this._selection.forEach((i) => {
item = i; item = i;
}); });
let selection = new SelectionTreeType(new ListRootItem());
if (item instanceof ListRootItem || item instanceof ListFileItem) { if (item instanceof ListRootItem || item instanceof ListFileItem) {
$selection.clear(); fileStateCollection.files.forEach((_file, fileId) => {
get(fileObservers).forEach((_file, fileId) => { selection.set(new ListFileItem(fileId), true);
$selection.set(new ListFileItem(fileId), true);
}); });
} else if (item instanceof ListTrackItem) { } else if (item instanceof ListTrackItem) {
let file = getFile(item.getFileId()); let file = fileStateCollection.getFile(item.getFileId());
if (file) { if (file) {
file.trk.forEach((_track, trackId) => { file.trk.forEach((_track, trackId) => {
$selection.set(new ListTrackItem(item.getFileId(), trackId), true); selection.set(new ListTrackItem(item.getFileId(), trackId), true);
}); });
} }
} else if (item instanceof ListTrackSegmentItem) { } else if (item instanceof ListTrackSegmentItem) {
let file = getFile(item.getFileId()); let file = fileStateCollection.getFile(item.getFileId());
if (file) { if (file) {
file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => { file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
$selection.set( selection.set(
new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId), new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId),
true true
); );
}); });
} }
} else if (item instanceof ListWaypointItem) { } else if (item instanceof ListWaypointItem) {
let file = getFile(item.getFileId()); let file = fileStateCollection.getFile(item.getFileId());
if (file) { if (file) {
file.wpt.forEach((_waypoint, waypointId) => { file.wpt.forEach((_waypoint, waypointId) => {
$selection.set(new ListWaypointItem(item.getFileId(), waypointId), true); selection.set(new ListWaypointItem(item.getFileId(), waypointId), true);
}); });
} }
} }
this._selection = selection;
}
return $selection; set(items: ListItem[]) {
let selection = new SelectionTreeType(new ListRootItem());
items.forEach((item) => {
selection.set(item, true);
}); });
} this._selection = selection;
}
export function getOrderedSelection(reverse: boolean = false): ListItem[] { update(updatedFiles: GPXFile[], deletedFileIds: string[]) {
// TODO do it the other way around: get all selected items, and check if they still exist?
// let removedItems: ListItem[] = [];
// applyToOrderedItemsFromFile(selection.value.getSelected(), (fileId, level, items) => {
// let file = updatedFiles.find((file) => file._data.id === fileId);
// if (file) {
// items.forEach((item) => {
// if (item instanceof ListTrackItem) {
// let newTrackIndex = file.trk.findIndex(
// (track) => track._data.trackIndex === item.getTrackIndex()
// );
// if (newTrackIndex === -1) {
// removedItems.push(item);
// }
// } else if (item instanceof ListTrackSegmentItem) {
// let newTrackIndex = file.trk.findIndex(
// (track) => track._data.trackIndex === item.getTrackIndex()
// );
// if (newTrackIndex === -1) {
// removedItems.push(item);
// } else {
// let newSegmentIndex = file.trk[newTrackIndex].trkseg.findIndex(
// (segment) => segment._data.segmentIndex === item.getSegmentIndex()
// );
// if (newSegmentIndex === -1) {
// removedItems.push(item);
// }
// }
// } else if (item instanceof ListWaypointItem) {
// let newWaypointIndex = file.wpt.findIndex(
// (wpt) => wpt._data.index === item.getWaypointIndex()
// );
// if (newWaypointIndex === -1) {
// removedItems.push(item);
// }
// }
// });
// } else if (deletedFileIds.includes(fileId)) {
// items.forEach((item) => {
// removedItems.push(item);
// });
// }
// });
// if (removedItems.length > 0) {
// selection.update(($selection) => {
// removedItems.forEach((item) => {
// if (item instanceof ListFileItem) {
// $selection.deleteChild(item.getFileId());
// } else {
// $selection.set(item, false);
// }
// });
// return $selection;
// });
// }
}
getOrderedSelection(reverse: boolean = false): ListItem[] {
let selected: ListItem[] = []; let selected: ListItem[] = [];
applyToOrderedSelectedItemsFromFile((fileId, level, items) => { applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
selected.push(...items); selected.push(...items);
}, reverse); }, reverse);
return selected; return selected;
}
copySelection(): boolean {
let selected = this._selection.getSelected();
if (selected.length > 0) {
this._copied = selected;
this._cut = false;
return true;
}
return false;
}
cutSelection() {
if (this.copySelection()) {
this._cut = true;
}
}
resetCopied() {
this._copied = undefined;
this._cut = false;
}
get copied(): ListItem[] | undefined {
return this._copied;
}
get cut(): boolean {
return this._cut;
}
} }
export const selection = new Selection();
export function applyToOrderedItemsFromFile( export function applyToOrderedItemsFromFile(
selectedItems: ListItem[], selectedItems: ListItem[],
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
@@ -261,115 +224,5 @@ export function applyToOrderedSelectedItemsFromFile(
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
reverse: boolean = true reverse: boolean = true
) { ) {
applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse); applyToOrderedItemsFromFile(selection.value.getSelected(), callback, reverse);
}
export const copied = writable<ListItem[] | undefined>(undefined);
export const cut = writable(false);
export function copySelection(): boolean {
let selected = get(selection).getSelected();
if (selected.length > 0) {
copied.set(selected);
cut.set(false);
return true;
}
return false;
}
export function cutSelection() {
if (copySelection()) {
cut.set(true);
}
}
function resetCopied() {
copied.set(undefined);
cut.set(false);
}
export function pasteSelection() {
let fromItems = get(copied);
if (fromItems === undefined || fromItems.length === 0) {
return;
}
let selected = get(selection).getSelected();
if (selected.length === 0) {
selected = [new ListRootItem()];
}
let fromParent = fromItems[0].getParent();
let toParent = selected[selected.length - 1];
let startIndex: number | undefined = undefined;
if (fromItems[0].level === toParent.level) {
if (
toParent instanceof ListTrackItem ||
toParent instanceof ListTrackSegmentItem ||
toParent instanceof ListWaypointItem
) {
startIndex = toParent.getId() + 1;
}
toParent = toParent.getParent();
}
let toItems: ListItem[] = [];
if (toParent.level === ListLevel.ROOT) {
let fileIds = getFileIds(fromItems.length);
fileIds.forEach((fileId) => {
toItems.push(new ListFileItem(fileId));
});
} else {
let toFile = getFile(toParent.getFileId());
if (toFile) {
fromItems.forEach((item, index) => {
if (toParent instanceof ListFileItem) {
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
toItems.push(
new ListTrackItem(
toParent.getFileId(),
(startIndex ?? toFile.trk.length) + index
)
);
} else if (item instanceof ListWaypointsItem) {
toItems.push(new ListWaypointsItem(toParent.getFileId()));
} else if (item instanceof ListWaypointItem) {
toItems.push(
new ListWaypointItem(
toParent.getFileId(),
(startIndex ?? toFile.wpt.length) + index
)
);
}
} else if (toParent instanceof ListTrackItem) {
if (item instanceof ListTrackSegmentItem) {
let toTrackIndex = toParent.getTrackIndex();
toItems.push(
new ListTrackSegmentItem(
toParent.getFileId(),
toTrackIndex,
(startIndex ?? toFile.trk[toTrackIndex].trkseg.length) + index
)
);
}
} else if (toParent instanceof ListWaypointsItem) {
if (item instanceof ListWaypointItem) {
toItems.push(
new ListWaypointItem(
toParent.getFileId(),
(startIndex ?? toFile.wpt.length) + index
)
);
}
}
});
}
}
if (fromItems.length === toItems.length) {
moveItems(fromParent, toParent, fromItems, toItems, get(cut));
resetCopied();
}
} }

View File

@@ -0,0 +1,140 @@
import type { ListItem } from '$lib/components/file-list/file-list';
export class SelectionTreeType {
item: ListItem;
selected: boolean;
children: {
[key: string | number]: SelectionTreeType;
};
size: number = 0;
constructor(item: ListItem) {
this.item = item;
this.selected = false;
this.children = {};
}
clear() {
this.selected = false;
for (let key in this.children) {
this.children[key].clear();
}
this.size = 0;
}
_setOrToggle(item: ListItem, value?: boolean) {
if (item.level === this.item.level) {
let newSelected = value === undefined ? !this.selected : value;
if (this.selected !== newSelected) {
this.selected = newSelected;
this.size += this.selected ? 1 : -1;
}
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (!this.children.hasOwnProperty(id)) {
this.children[id] = new SelectionTreeType(this.item.extend(id));
}
this.size -= this.children[id].size;
this.children[id]._setOrToggle(item, value);
this.size += this.children[id].size;
}
}
}
set(item: ListItem, value: boolean) {
this._setOrToggle(item, value);
}
toggle(item: ListItem) {
this._setOrToggle(item);
}
has(item: ListItem): boolean {
if (item.level === this.item.level) {
return this.selected;
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].has(item);
}
}
}
return false;
}
hasAnyParent(item: ListItem, self: boolean = true): boolean {
if (
this.selected &&
this.item.level <= item.level &&
(self || this.item.level < item.level)
) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyParent(item, self);
}
}
return false;
}
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
if (
this.selected &&
this.item.level >= item.level &&
(self || this.item.level > item.level)
) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (ignoreIds === undefined || ignoreIds.indexOf(id) === -1) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyChildren(item, self, ignoreIds);
}
}
} else {
for (let key in this.children) {
if (ignoreIds === undefined || ignoreIds.indexOf(key) === -1) {
if (this.children[key].hasAnyChildren(item, self, ignoreIds)) {
return true;
}
}
}
}
return false;
}
getSelected(selection: ListItem[] = []): ListItem[] {
if (this.selected) {
selection.push(this.item);
}
for (let key in this.children) {
this.children[key].getSelected(selection);
}
return selection;
}
forEach(callback: (item: ListItem) => void) {
if (this.selected) {
callback(this.item);
}
for (let key in this.children) {
this.children[key].forEach(callback);
}
}
getChild(id: string | number): SelectionTreeType | undefined {
return this.children[id];
}
deleteChild(id: string | number) {
if (this.children.hasOwnProperty(id)) {
this.size -= this.children[id].size;
delete this.children[id];
}
}
}

View File

@@ -0,0 +1,46 @@
import { ListItem, ListLevel } from '$lib/components/file-list/file-list';
import { GPXFile, GPXStatistics, type Track } from 'gpx';
export class GPXStatisticsTree {
level: ListLevel;
statistics: {
[key: string]: GPXStatisticsTree | GPXStatistics;
} = {};
constructor(element: GPXFile | Track) {
if (element instanceof GPXFile) {
this.level = ListLevel.FILE;
element.children.forEach((child, index) => {
this.statistics[index] = new GPXStatisticsTree(child);
});
} else {
this.level = ListLevel.TRACK;
element.children.forEach((child, index) => {
this.statistics[index] = child.getStatistics();
});
}
}
getStatisticsFor(item: ListItem): GPXStatistics {
let statistics = new GPXStatistics();
let id = item.getIdAtLevel(this.level);
if (id === undefined || id === 'waypoints') {
Object.keys(this.statistics).forEach((key) => {
if (this.statistics[key] instanceof GPXStatistics) {
statistics.mergeWith(this.statistics[key]);
} else {
statistics.mergeWith(this.statistics[key].getStatisticsFor(item));
}
});
} else {
let child = this.statistics[id];
if (child instanceof GPXStatistics) {
statistics.mergeWith(child);
} else if (child !== undefined) {
statistics.mergeWith(child.getStatisticsFor(item));
}
}
return statistics;
}
}
export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree };

View File

@@ -82,88 +82,6 @@
// export const gpxLayers: Map<string, GPXLayer> = new Map(); // export const gpxLayers: Map<string, GPXLayer> = new Map();
// export const routingControls: Map<string, RoutingControls> = new Map(); // export const routingControls: Map<string, RoutingControls> = new Map();
// export function newGPXFile() {
// const newFileName = i18n._('menu.new_file');
// let file = new GPXFile();
// let maxNewFileNumber = 0;
// get(fileObservers).forEach((f) => {
// let file = get(f)?.file;
// if (file && file.metadata.name && file.metadata.name.startsWith(newFileName)) {
// let number = parseInt(file.metadata.name.split(' ').pop() ?? '0');
// if (!isNaN(number) && number > maxNewFileNumber) {
// maxNewFileNumber = number;
// }
// }
// });
// file.metadata.name = `${newFileName} ${maxNewFileNumber + 1}`;
// return file;
// }
// export function createFile() {
// let file = newGPXFile();
// dbUtils.add(file);
// selectFileWhenLoaded(file._data.id);
// currentTool.set(Tool.ROUTING);
// }
// export function triggerFileInput() {
// const input = document.createElement('input');
// input.type = 'file';
// input.accept = '.gpx';
// input.multiple = true;
// input.className = 'hidden';
// input.onchange = () => {
// if (input.files) {
// loadFiles(input.files);
// }
// };
// input.click();
// }
// export async function loadFiles(list: FileList | File[]) {
// let files: GPXFile[] = [];
// for (let i = 0; i < list.length; i++) {
// let file = await loadFile(list[i]);
// if (file) {
// files.push(file);
// }
// }
// let ids = dbUtils.addMultiple(files);
// initTargetMapBounds(ids);
// selectFileWhenLoaded(ids[0]);
// }
// export async function loadFile(file: File): Promise<GPXFile | null> {
// let result = await new Promise<GPXFile | null>((resolve) => {
// const reader = new FileReader();
// reader.onload = () => {
// let data = reader.result?.toString() ?? null;
// if (data) {
// let gpx = parseGPX(data);
// if (gpx.metadata === undefined) {
// gpx.metadata = {};
// }
// if (gpx.metadata.name === undefined || gpx.metadata.name.trim() === '') {
// gpx.metadata.name = file.name.split('.').slice(0, -1).join('.');
// }
// resolve(gpx);
// } else {
// resolve(null);
// }
// };
// reader.readAsText(file);
// });
// return result;
// }
// export function selectFileWhenLoaded(fileId: string) { // export function selectFileWhenLoaded(fileId: string) {
// const unsubscribe = fileObservers.subscribe((files) => { // const unsubscribe = fileObservers.subscribe((files) => {
// if (files.has(fileId)) { // if (files.has(fileId)) {

View File

@@ -4,14 +4,13 @@
// import FileList from '$lib/components/file-list/FileList.svelte'; // import FileList from '$lib/components/file-list/FileList.svelte';
// import GPXStatistics from '$lib/components/GPXStatistics.svelte'; // import GPXStatistics from '$lib/components/GPXStatistics.svelte';
import Map from '$lib/components/map/Map.svelte'; import Map from '$lib/components/map/Map.svelte';
// import Menu from '$lib/components/Menu.svelte'; import Menu from '$lib/components/Menu.svelte';
// import Toolbar from '$lib/components/toolbar/Toolbar.svelte'; // import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
import StreetViewControl from '$lib/components/map/street-view-control/StreetViewControl.svelte'; import StreetViewControl from '$lib/components/map/street-view-control/StreetViewControl.svelte';
import LayerControl from '$lib/components/map/layer-control/LayerControl.svelte'; import LayerControl from '$lib/components/map/layer-control/LayerControl.svelte';
// import CoordinatesPopup from '$lib/components/map/CoordinatesPopup.svelte'; // import CoordinatesPopup from '$lib/components/map/CoordinatesPopup.svelte';
import Resizer from '$lib/components/Resizer.svelte'; import Resizer from '$lib/components/Resizer.svelte';
import { Toaster } from '$lib/components/ui/sonner'; import { Toaster } from '$lib/components/ui/sonner';
// import { observeFilesFromDatabase } from '$lib/db';
// import { gpxStatistics, loadFiles, slicedGPXStatistics } from '$lib/stores'; // import { gpxStatistics, loadFiles, slicedGPXStatistics } from '$lib/stores';
// import { onMount } from 'svelte'; // import { onMount } from 'svelte';
// import { page } from '$app/state'; // import { page } from '$app/state';
@@ -20,6 +19,10 @@
// import { getURLForGoogleDriveFile } from '$lib/components/embedding/Embedding'; // import { getURLForGoogleDriveFile } from '$lib/components/embedding/Embedding';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { settings } from '$lib/logic/settings.svelte'; import { settings } from '$lib/logic/settings.svelte';
import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { loadFiles } from '$lib/logic/file-actions.svelte';
import { onMount } from 'svelte';
import { page } from '$app/state';
const { const {
treeFileView, treeFileView,
@@ -30,28 +33,28 @@
elevationFill, elevationFill,
} = settings; } = settings;
// onMount(() => { onMount(() => {
// let files: string[] = JSON.parse(page.url.searchParams.get('files') || '[]'); let files: string[] = JSON.parse(page.url.searchParams.get('files') || '[]');
// let ids: string[] = JSON.parse(page.url.searchParams.get('ids') || '[]'); let ids: string[] = JSON.parse(page.url.searchParams.get('ids') || '[]');
// let urls: string[] = files.concat(ids.map(getURLForGoogleDriveFile)); let urls: string[] = []; //files.concat(ids.map(getURLForGoogleDriveFile));
// observeFilesFromDatabase(urls.length === 0); fileStateCollection.initialize(urls.length === 0);
// if (urls.length > 0) { if (urls.length > 0) {
// let downloads: Promise<File | null>[] = []; let downloads: Promise<File | null>[] = [];
// urls.forEach((url) => { urls.forEach((url) => {
// downloads.push( downloads.push(
// fetch(url) fetch(url)
// .then((response) => response.blob()) .then((response) => response.blob())
// .then((blob) => new File([blob], url.split('/').pop() ?? '')) .then((blob) => new File([blob], url.split('/').pop() ?? ''))
// ); );
// }); });
// Promise.all(downloads).then((files) => { Promise.all(downloads).then((files) => {
// loadFiles(files.filter((file) => file !== null)); loadFiles(files.filter((file) => file !== null));
// }); });
// } }
// }); });
</script> </script>
<div class="fixed -z-10 text-transparent"> <div class="fixed -z-10 text-transparent">
@@ -94,7 +97,7 @@
<div class="fixed flex flex-row w-screen h-screen supports-dvh:h-dvh"> <div class="fixed flex flex-row w-screen h-screen supports-dvh:h-dvh">
<div class="flex flex-col grow h-full min-w-0"> <div class="flex flex-col grow h-full min-w-0">
<div class="grow relative"> <div class="grow relative">
<!-- <Menu /> --> <Menu />
<div <div
class="absolute top-0 bottom-0 left-0 z-20 flex flex-col justify-center pointer-events-none" class="absolute top-0 bottom-0 left-0 z-20 flex flex-col justify-center pointer-events-none"
> >