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

View File

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

View File

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

View File

@@ -11,8 +11,7 @@
exportState,
} from '$lib/components/export/utils.svelte';
import { tool } from '$lib/components/toolbar/utils.svelte';
import { gpxStatistics } from '$lib/stores';
import { fileObservers } from '$lib/db';
// import { gpxStatistics } from '$lib/stores';
import {
Download,
Zap,
@@ -23,10 +22,10 @@
SquareActivity,
} from '@lucide/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 { 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 exportOptions: Record<string, boolean> = $state({
@@ -38,7 +37,36 @@
extensions: false,
});
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 {
time: false,
hr: false,
@@ -47,27 +75,6 @@
power: 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]));
@@ -118,7 +125,7 @@
}}
>
<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')}
{:else}
{i18n._('menu.download_files')}

View File

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

View File

@@ -4,7 +4,7 @@
import FileListNode from './FileListNode.svelte';
import { fileObservers, settings } from '$lib/db';
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 { ClipboardPaste, FileStack, Plus } from '@lucide/svelte';
import Shortcut from '$lib/components/Shortcut.svelte';

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
import type { GPXFileWithStatistics } from '$lib/db';
import { getContext } from 'svelte';
import type { Readable } from 'svelte/store';
import { ListFileItem } from './FileList';
import { ListFileItem } from './file-list';
let {
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 { freeze } from 'immer';
import { GPXFile, Track, TrackSegment, Waypoint } from 'gpx';
import { selection } from './Selection';
import { newGPXFile } from '$lib/stores';
// import { dbUtils, getFile } from '$lib/db';
// import { freeze } from 'immer';
// import { GPXFile, Track, TrackSegment, Waypoint } from 'gpx';
// import { selection } from './Selection';
// import { newGPXFile } from '$lib/stores';
export enum ListLevel {
ROOT,
@@ -32,6 +32,7 @@ export const allowedPastes: Record<ListLevel, ListLevel[]> = {
};
export abstract class ListItem {
[x: string]: any;
level: ListLevel;
constructor(level: ListLevel) {
@@ -321,163 +322,3 @@ export function sortItems(items: ListItem[], reverse: boolean = false) {
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 { dbUtils } from '$lib/db';
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 { i18n } from '$lib/i18n.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 { dbUtils, getFile, settings } from '$lib/db';
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 { selection } from '../Selection';
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 mapboxgl from 'mapbox-gl';
import { waypointPopup, deleteWaypoint, trackpointPopup } from './GPXLayerPopup';
import { addSelectItem, selectItem, selection } from '$lib/components/file-list/Selection';
import {
ListTrackSegmentItem,
ListWaypointItem,
@@ -11,7 +10,7 @@ import {
ListTrackItem,
ListFileItem,
ListRootItem,
} from '$lib/components/file-list/FileList';
} from '$lib/components/file-list/file-list';
import {
getClosestLinePoint,
getElevation,
@@ -20,7 +19,7 @@ import {
setPointerCursor,
setScissorsCursor,
} 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 { getSymbolKey, symbols } from '$lib/assets/symbols';

View File

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

View File

@@ -1,10 +1,9 @@
<script lang="ts">
import { Tool, tool } from '$lib/components/toolbar/utils.svelte';
import { settings } from '$lib/db';
import * as Card from '$lib/components/ui/card';
import Routing from '$lib/components/toolbar/tools/routing/Routing.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 Merge from '$lib/components/toolbar/tools/Merge.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 { onMount } from 'svelte';
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;
let popupElement: HTMLElement;
let popup: mapboxgl.Popup;
onMount(() => {
popup = new mapboxgl.Popup({
closeButton: false,
@@ -31,12 +38,16 @@
</script>
{#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">
<Card.Root class="rounded-md border-none">
<Card.Content class="p-2.5">
{#if tool.current === Tool.ROUTING}
<Routing {popup} {popupElement} bind:minimized={$minimizeRoutingMenu} />
<Routing
{popup}
{popupElement}
bind:minimized={minimizeRoutingMenu.value}
/>
{:else if tool.current === Tool.SCISSORS}
<Scissors />
{:else if tool.current === Tool.WAYPOINT}

View File

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

View File

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

View File

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

View File

@@ -6,58 +6,58 @@
</script>
<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 { selection } from '$lib/components/file-list/Selection';
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label/index.js';
import { Checkbox } from '$lib/components/ui/checkbox';
import * as RadioGroup from '$lib/components/ui/radio-group';
import { i18n } from '$lib/i18n.svelte';
import { dbUtils, getFile } from '$lib/db';
import { Group } from '@lucide/svelte';
import { getURLForLanguage } from '$lib/utils';
import Shortcut from '$lib/components/Shortcut.svelte';
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 canMergeContents = false;
let removeGaps = false;
let props: {
class?: string;
} = $props();
$: if ($selection.size > 1) {
canMergeTraces = true;
} else if ($selection.size === 1) {
let selected = $selection.getSelected()[0];
let canMergeTraces = $derived.by(() => {
if (selection.value.size > 1) {
return true;
} else if (selection.value.size === 1) {
let selected = selection.value.getSelected()[0];
if (selected instanceof ListFileItem) {
let file = getFile(selected.getFileId());
let file = fileStateCollection.getFile(selected.getFileId());
if (file) {
canMergeTraces = file.getSegments().length > 1;
} else {
canMergeTraces = false;
return file.getSegments().length > 1;
}
} else if (selected instanceof ListTrackItem) {
let trackIndex = selected.getTrackIndex();
let file = getFile(selected.getFileId());
let file = fileStateCollection.getFile(selected.getFileId());
if (file && trackIndex < file.trk.length) {
canMergeTraces = file.trk[trackIndex].getSegments().length > 1;
} else {
canMergeTraces = false;
}
} else {
canMergeContents = false;
return file.trk[trackIndex].getSegments().length > 1;
}
}
return false;
}
});
$: canMergeContents =
$selection.size > 1 &&
$selection
let canMergeContents = $derived(
selection.value.size > 1 &&
selection.value
.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>
<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}>
<Label class="flex flex-row items-center gap-1.5 leading-5">
<RadioGroup.Item value={MergeType.TRACES} />
@@ -80,7 +80,7 @@
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
(mergeType === MergeType.CONTENTS && !canMergeContents)}
onclick={() => {
dbUtils.mergeSelection(
fileActions.mergeSelection(
mergeType === MergeType.TRACES,
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 { Button } from '$lib/components/ui/button';
import { Slider } from '$lib/components/ui/slider';
import { selection } from '$lib/components/file-list/Selection';
import {
ListItem,
ListRootItem,
ListTrackSegmentItem,
} from '$lib/components/file-list/FileList';
} from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte';
import { Funnel } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import WithUnits from '$lib/components/WithUnits.svelte';
import { dbUtils, fileObservers } from '$lib/db';
import { map } from '$lib/components/map/map.svelte';
import { map } from '$lib/components/map/utils.svelte';
import { onDestroy } from 'svelte';
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
import { getURLForLanguage } from '$lib/utils';
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();
@@ -28,7 +28,7 @@
const maxTolerance = 10000;
let validSelection = $derived(
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
);
let tolerance = $derived(
minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)))
@@ -67,18 +67,18 @@
});
});
if (map.current) {
let source: GeoJSONSource | undefined = map.current.getSource('simplified');
if (map.value) {
let source: GeoJSONSource | undefined = map.value.getSource('simplified');
if (source) {
source.setData(data);
} else {
map.current.addSource('simplified', {
map.value.addSource('simplified', {
type: 'geojson',
data: data,
});
}
if (!map.current.getLayer('simplified')) {
map.current.addLayer({
if (!map.value.getLayer('simplified')) {
map.value.addLayer({
id: 'simplified',
type: 'line',
source: 'simplified',
@@ -88,52 +88,52 @@
},
});
} else {
map.current.moveLayer('simplified');
map.value.moveLayer('simplified');
}
}
}
$effect(() => {
if ($fileObservers) {
unsubscribes.forEach((unsubscribe, fileId) => {
if (!$fileObservers.has(fileId)) {
unsubscribe();
unsubscribes.delete(fileId);
}
});
$fileObservers.forEach((fileStore, fileId) => {
if (!unsubscribes.has(fileId)) {
let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [
fs,
sel,
]).subscribe(([fs, sel]) => {
if (fs) {
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
let segmentItem = new ListTrackSegmentItem(
fileId,
trackIndex,
segmentIndex
);
if (sel.hasAnyParent(segmentItem)) {
let statistics = fs.statistics.getStatisticsFor(segmentItem);
simplified.set(segmentItem.getFullId(), [
segmentItem,
statistics.local.points.length,
ramerDouglasPeucker(statistics.local.points, minTolerance),
]);
update();
} else if (simplified.has(segmentItem.getFullId())) {
simplified.delete(segmentItem.getFullId());
update();
}
});
}
});
unsubscribes.set(fileId, unsubscribe);
}
});
}
});
// $effect(() => {
// if ($fileObservers) {
// unsubscribes.forEach((unsubscribe, fileId) => {
// if (!$fileObservers.has(fileId)) {
// unsubscribe();
// unsubscribes.delete(fileId);
// }
// });
// $fileObservers.forEach((fileStore, fileId) => {
// if (!unsubscribes.has(fileId)) {
// let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [
// fs,
// sel,
// ]).subscribe(([fs, sel]) => {
// if (fs) {
// fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
// let segmentItem = new ListTrackSegmentItem(
// fileId,
// trackIndex,
// segmentIndex
// );
// if (sel.hasAnyParent(segmentItem)) {
// let statistics = fs.statistics.getStatisticsFor(segmentItem);
// simplified.set(segmentItem.getFullId(), [
// segmentItem,
// statistics.local.points.length,
// ramerDouglasPeucker(statistics.local.points, minTolerance),
// ]);
// update();
// } else if (simplified.has(segmentItem.getFullId())) {
// simplified.delete(segmentItem.getFullId());
// update();
// }
// });
// }
// });
// unsubscribes.set(fileId, unsubscribe);
// }
// });
// }
// });
$effect(() => {
if (tolerance) {
@@ -142,12 +142,12 @@
});
onDestroy(() => {
if (map.current) {
if (map.current.getLayer('simplified')) {
map.current.removeLayer('simplified');
if (map.value) {
if (map.value.getLayer('simplified')) {
map.value.removeLayer('simplified');
}
if (map.current.getSource('simplified')) {
map.current.removeSource('simplified');
if (map.value.getSource('simplified')) {
map.value.removeSource('simplified');
}
}
unsubscribes.forEach((unsubscribe) => unsubscribe());
@@ -164,7 +164,7 @@
.map((point) => point.point)
);
});
dbUtils.reduce(itemsAndPoints);
fileActions.reduce(itemsAndPoints);
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,12 @@
import { TrackPoint, TrackSegment } from 'gpx';
import { get } from 'svelte/store';
import mapboxgl from 'mapbox-gl';
import { dbUtils, getFile } from '$lib/db';
import {
applyToOrderedSelectedItemsFromFile,
selection,
} from '$lib/components/file-list/Selection';
import { ListTrackSegmentItem } from '$lib/components/file-list/FileList';
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
import { gpxStatistics } from '$lib/stores';
import { tool, Tool } from '$lib/components/toolbar/utils.svelte';
import { splitAs } from '$lib/components/toolbar/tools/scissors/utils.svelte';
import { Scissors } from 'lucide-static';
import { applyToOrderedSelectedItemsFromFile, selection } from '$lib/logic/selection.svelte';
export class SplitControls {
active: boolean = false;
@@ -25,10 +21,9 @@ export class SplitControls {
constructor(map: mapboxgl.Map) {
this.map = map;
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
$effect(() => {
tool.current, this.addIfNeeded.bind(this);
tool.current, selection.value, this.addIfNeeded.bind(this);
});
}
@@ -64,7 +59,7 @@ export class SplitControls {
if (file) {
file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (
get(selection).hasAnyParent(
selection.value.hasAnyParent(
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">
import { Input } from '$lib/components/ui/input';
import { Textarea } from '$lib/components/ui/textarea';
import { Label } from '$lib/components/ui/label/index.js';
import { Button } from '$lib/components/ui/button';
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 { 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 { get } from 'svelte/store';
import Help from '$lib/components/Help.svelte';
@@ -22,61 +14,21 @@
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { MapPin, CircleX, Save } from '@lucide/svelte';
import { getSymbolKey, symbols } from '$lib/assets/symbols';
import { selection } from '$lib/logic/selection.svelte';
import { selectedWaypoint } from './utils.svelte';
let name: string;
let description: string;
let link: string;
let longitude: number;
let latitude: number;
let symbolKey: string;
let props: {
class?: string;
} = $props();
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;
$: 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);
}
}
}
let canCreate = $derived(selection.value.size > 0);
function resetWaypointData() {
name = '';
@@ -87,21 +39,6 @@
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() {
if (typeof latitude === 'string') {
latitude = parseFloat(latitude);
@@ -124,12 +61,12 @@
link: link.length > 0 ? { attributes: { href: link } } : undefined,
sym: symbols[symbolKey]?.value ?? '',
},
$selectedWaypoint
? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index)
selectedWaypoint.wpt && selectedWaypoint.fileId
? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
: undefined
);
selectedWaypoint.set(undefined);
selectedWaypoint.reset();
resetWaypointData();
}
@@ -138,49 +75,46 @@
longitude = e.lngLat.lng.toFixed(6);
}
$: sortedSymbols = Object.entries(symbols).sort((a, b) => {
return i18n._(`gpx.symbol.${a[0]}`).localeCompare(i18n._(`gpx.symbol.${b[0]}`), i18n.lang);
});
let sortedSymbols = $derived(
Object.entries(symbols).sort((a, b) => {
return i18n
._(`gpx.symbol.${a[0]}`)
.localeCompare(i18n._(`gpx.symbol.${b[0]}`), i18n.lang);
})
);
onMount(() => {
let m = get(map);
m?.on('click', setCoordinates);
setCrosshairCursor();
map.value?.on('click', setCoordinates);
// setCrosshairCursor();
});
onDestroy(() => {
let m = get(map);
m?.off('click', setCoordinates);
resetCursor();
if (unsubscribe) {
unsubscribe();
unsubscribe = undefined;
}
map.value?.off('click', setCoordinates);
// resetCursor();
});
</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">
<Label for="name">{i18n._('menu.metadata.name')}</Label>
<Input
bind:value={name}
id="name"
class="font-semibold h-8"
disabled={!canCreate && !$selectedWaypoint}
disabled={!canCreate && !selectedWaypoint.wpt}
/>
<Label for="description">{i18n._('menu.metadata.description')}</Label>
<Textarea
bind:value={description}
id="description"
disabled={!canCreate && !$selectedWaypoint}
disabled={!canCreate && !selectedWaypoint.wpt}
/>
<Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label>
<Select.Root bind:value={symbolKey} type="single">
<Select.Trigger
id="symbol"
class="w-full h-8"
disabled={!canCreate && !$selectedWaypoint}
disabled={!canCreate && !selectedWaypoint.wpt}
>
{#if symbolKey in symbols}
{i18n._(`gpx.symbol.${symbolKey}`)}
@@ -212,7 +146,7 @@
bind:value={link}
id="link"
class="h-8"
disabled={!canCreate && !$selectedWaypoint}
disabled={!canCreate && !selectedWaypoint.wpt}
/>
<div class="flex flex-row gap-2">
<div class="grow">
@@ -225,7 +159,7 @@
min={-90}
max={90}
class="text-xs h-8"
disabled={!canCreate && !$selectedWaypoint}
disabled={!canCreate && !selectedWaypoint.wpt}
/>
</div>
<div class="grow">
@@ -238,7 +172,7 @@
min={-180}
max={180}
class="text-xs h-8"
disabled={!canCreate && !$selectedWaypoint}
disabled={!canCreate && !selectedWaypoint.wpt}
/>
</div>
</div>
@@ -246,11 +180,11 @@
<div class="flex flex-row gap-2 items-center">
<Button
variant="outline"
disabled={!canCreate && !$selectedWaypoint}
disabled={!canCreate && !selectedWaypoint.wpt}
class="grow whitespace-normal h-fit"
onclick={createOrUpdateWaypoint}
>
{#if $selectedWaypoint}
{#if selectedWaypoint.wpt}
<Save size="16" class="mr-1 shrink-0" />
{i18n._('menu.metadata.save')}
{:else}
@@ -261,7 +195,7 @@
<Button
variant="outline"
onclick={() => {
selectedWaypoint.set(undefined);
selectedWaypoint.reset();
resetWaypointData();
}}
>
@@ -269,7 +203,7 @@
</Button>
</div>
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/poi')}>
{#if $selectedWaypoint || canCreate}
{#if selectedWaypoint.wpt || canCreate}
{i18n._('toolbar.waypoint.help')}
{:else}
{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>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Punts d'interès
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Body zájmu
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Interessante Orte
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Puntos de interés
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Interesguneak
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points d'intérêt
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Nevezetes helyek
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Punti di interesse
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Svarbios vietos
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Interessante punten (POI's)
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Pontos de interesse
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Точки интереса
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: İlgi alanları
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: Points of interest
<script>
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>
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }

View File

@@ -4,7 +4,7 @@ title: 创建或编辑兴趣点
<script>
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>
# <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 {
ListFileItem,
ListItem,
@@ -9,217 +8,149 @@ import {
ListLevel,
sortItems,
ListWaypointsItem,
moveItems,
} from './FileList';
import { fileObservers, getFile, getFileIds, settings } from '$lib/db';
} from '$lib/components/file-list/file-list';
import { SelectionTreeType } from '$lib/logic/selection';
import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { settings } from '$lib/logic/settings.svelte';
import type { GPXFile } from 'gpx';
export class SelectionTreeType {
item: ListItem;
selected: boolean;
children: {
[key: string | number]: SelectionTreeType;
};
size: number = 0;
export class Selection {
private _selection: SelectionTreeType;
private _copied: ListItem[] | undefined;
private _cut: boolean;
constructor(item: ListItem) {
this.item = item;
this.selected = false;
this.children = {};
constructor() {
this._selection = $state(new SelectionTreeType(new ListRootItem()));
this._copied = $state(undefined);
this._cut = $state(false);
}
clear() {
this.selected = false;
for (let key in this.children) {
this.children[key].clear();
}
this.size = 0;
get value(): SelectionTreeType {
return this._selection;
}
_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;
}
}
selectItem(item: ListItem) {
let selection = new SelectionTreeType(new ListRootItem());
selection.set(item, true);
this._selection = selection;
}
set(item: ListItem, value: boolean) {
this._setOrToggle(item, value);
selectFile(fileId: string) {
this.selectItem(new ListFileItem(fileId));
}
toggle(item: ListItem) {
this._setOrToggle(item);
addSelectItem(item: ListItem) {
this._selection.toggle(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;
addSelectFile(fileId: string) {
this.addSelectItem(new ListFileItem(fileId));
}
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) => {
selectAll() {
let item: ListItem = new ListRootItem();
$selection.forEach((i) => {
this._selection.forEach((i) => {
item = i;
});
let selection = new SelectionTreeType(new ListRootItem());
if (item instanceof ListRootItem || item instanceof ListFileItem) {
$selection.clear();
get(fileObservers).forEach((_file, fileId) => {
$selection.set(new ListFileItem(fileId), true);
fileStateCollection.files.forEach((_file, fileId) => {
selection.set(new ListFileItem(fileId), true);
});
} else if (item instanceof ListTrackItem) {
let file = getFile(item.getFileId());
let file = fileStateCollection.getFile(item.getFileId());
if (file) {
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) {
let file = getFile(item.getFileId());
let file = fileStateCollection.getFile(item.getFileId());
if (file) {
file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
$selection.set(
selection.set(
new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId),
true
);
});
}
} else if (item instanceof ListWaypointItem) {
let file = getFile(item.getFileId());
let file = fileStateCollection.getFile(item.getFileId());
if (file) {
file.wpt.forEach((_waypoint, waypointId) => {
$selection.set(new ListWaypointItem(item.getFileId(), waypointId), true);
selection.set(new ListWaypointItem(item.getFileId(), waypointId), true);
});
}
}
return $selection;
});
this._selection = selection;
}
export function getOrderedSelection(reverse: boolean = false): ListItem[] {
set(items: ListItem[]) {
let selection = new SelectionTreeType(new ListRootItem());
items.forEach((item) => {
selection.set(item, true);
});
this._selection = selection;
}
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[] = [];
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
selected.push(...items);
@@ -227,6 +158,38 @@ export function getOrderedSelection(reverse: boolean = false): ListItem[] {
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(
selectedItems: ListItem[],
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,
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();
}
applyToOrderedItemsFromFile(selection.value.getSelected(), callback, reverse);
}

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 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) {
// const unsubscribe = fileObservers.subscribe((files) => {
// if (files.has(fileId)) {

View File

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