diff --git a/website/src/lib/components/toolbar/ToolbarItemMenu.svelte b/website/src/lib/components/toolbar/ToolbarItemMenu.svelte
index c19abe0a..d44f5277 100644
--- a/website/src/lib/components/toolbar/ToolbarItemMenu.svelte
+++ b/website/src/lib/components/toolbar/ToolbarItemMenu.svelte
@@ -7,6 +7,7 @@
import Waypoint from '$lib/components/toolbar/tools/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';
import Clean from '$lib/components/toolbar/tools/Clean.svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
@@ -44,6 +45,8 @@
{:else if $currentTool === Tool.MERGE}
+ {:else if $currentTool === Tool.EXTRACT}
+
{:else if $currentTool === Tool.CLEAN}
{:else if $currentTool === Tool.REDUCE}
diff --git a/website/src/lib/components/toolbar/tools/Extract.svelte b/website/src/lib/components/toolbar/tools/Extract.svelte
new file mode 100644
index 00000000..3a705839
--- /dev/null
+++ b/website/src/lib/components/toolbar/tools/Extract.svelte
@@ -0,0 +1,52 @@
+
+
+
+
+
+ {#if validSelection}
+ {$_('toolbar.extract.help')}
+ {:else}
+ {$_('toolbar.extract.help_invalid_selection')}
+ {/if}
+
+
diff --git a/website/src/lib/components/toolbar/tools/Merge.svelte b/website/src/lib/components/toolbar/tools/Merge.svelte
index 628a8fd0..28431710 100644
--- a/website/src/lib/components/toolbar/tools/Merge.svelte
+++ b/website/src/lib/components/toolbar/tools/Merge.svelte
@@ -13,8 +13,7 @@
import { Label } from '$lib/components/ui/label/index.js';
import * as RadioGroup from '$lib/components/ui/radio-group';
import { _ } from 'svelte-i18n';
- import { dbUtils, fileObservers } from '$lib/db';
- import { get } from 'svelte/store';
+ import { dbUtils, getFile } from '$lib/db';
import { Group } from 'lucide-svelte';
let canMergeTraces = false;
@@ -25,29 +24,17 @@
} else if ($selection.size === 1) {
let selected = $selection.getSelected()[0];
if (selected instanceof ListFileItem) {
- let fileId = selected.getFileId();
- let fileStore = $fileObservers.get(fileId);
- if (fileStore) {
- let file = get(fileStore)?.file;
- if (file) {
- canMergeTraces = file.getSegments().length > 1;
- } else {
- canMergeTraces = false;
- }
+ let file = getFile(selected.getFileId());
+ if (file) {
+ canMergeTraces = file.getSegments().length > 1;
} else {
canMergeTraces = false;
}
} else if (selected instanceof ListTrackItem) {
- let fileId = selected.getFileId();
let trackIndex = selected.getTrackIndex();
- let fileStore = $fileObservers.get(fileId);
- if (fileStore) {
- let file = get(fileStore)?.file;
- if (file && trackIndex < file.trk.length) {
- canMergeTraces = file.trk[trackIndex].getSegments().length > 1;
- } else {
- canMergeTraces = false;
- }
+ let file = getFile(selected.getFileId());
+ if (file && trackIndex < file.trk.length) {
+ canMergeTraces = file.trk[trackIndex].getSegments().length > 1;
} else {
canMergeTraces = false;
}
diff --git a/website/src/lib/components/toolbar/tools/Waypoint.svelte b/website/src/lib/components/toolbar/tools/Waypoint.svelte
index c7d27a05..6bf190cb 100644
--- a/website/src/lib/components/toolbar/tools/Waypoint.svelte
+++ b/website/src/lib/components/toolbar/tools/Waypoint.svelte
@@ -13,7 +13,7 @@
import { Waypoint } from 'gpx';
import { _ } from 'svelte-i18n';
import { ListWaypointItem } from '$lib/components/file-list/FileList';
- import { dbUtils, fileObservers, settings, type GPXFileWithStatistics } from '$lib/db';
+ import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db';
import { get } from 'svelte/store';
import Help from '$lib/components/Help.svelte';
import { onDestroy, onMount } from 'svelte';
@@ -36,12 +36,10 @@
if ($selection.size === 1) {
let item = $selection.getSelected()[0];
if (item instanceof ListWaypointItem) {
- let fileStore = get(fileObservers).get(item.getFileId());
- if (fileStore) {
- let waypoint = get(fileStore)?.file.wpt[item.getWaypointIndex()];
- if (waypoint) {
- return [waypoint, item.getFileId()];
- }
+ let file = getFile(item.getFileId());
+ let waypoint = file?.wpt[item.getWaypointIndex()];
+ if (waypoint) {
+ return [waypoint, item.getFileId()];
}
}
}
diff --git a/website/src/lib/db.ts b/website/src/lib/db.ts
index 2b5674cf..2070b90d 100644
--- a/website/src/lib/db.ts
+++ b/website/src/lib/db.ts
@@ -1,5 +1,5 @@
import Dexie, { liveQuery } from 'dexie';
-import { GPXFile, GPXStatistics, Track, type AnyGPXTreeElement, TrackSegment, Waypoint, TrackPoint, type Coordinates, distance } from 'gpx';
+import { GPXFile, GPXStatistics, Track, TrackSegment, Waypoint, TrackPoint, type Coordinates, distance } from 'gpx';
import { enableMapSet, enablePatches, applyPatches, type Patch, type WritableDraft, castDraft, freeze, produceWithPatches, original, produce } from 'immer';
import { writable, get, derived, type Readable, type Writable } from 'svelte/store';
import { initTargetMapBounds, splitAs, updateTargetMapBounds } from './stores';
@@ -267,6 +267,16 @@ liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => {
}
});
+export function getFile(fileId: string): GPXFile | undefined {
+ let fileStore = get(fileObservers).get(fileId);
+ return fileStore ? get(fileStore)?.file : undefined;
+}
+
+export function getStatistics(fileId: string): GPXStatisticsTree | undefined {
+ let fileStore = get(fileObservers).get(fileId);
+ return fileStore ? get(fileStore)?.statistics : undefined;
+}
+
const patchIndex: Readable = dexieStore(() => db.settings.get('patchIndex'), -1);
const patchMinMaxIndex: Readable<{ min: number, max: number }> = dexieStore(() => db.patches.orderBy(':id').keys().then(keys => {
if (keys.length === 0) {
@@ -605,6 +615,111 @@ export const dbUtils = {
}, false);
});
},
+ extractSelection: () => {
+ return applyGlobal((draft) => {
+ applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
+ let file = original(draft)?.get(fileId);
+ if (file) {
+ if (level === ListLevel.FILE) {
+ if (file.trk.length > 1) {
+ let fileIds = getFileIds(file.trk.length);
+
+ let closest = file.wpt.map((wpt, wptIndex) => {
+ return {
+ wptIndex: wptIndex,
+ index: [0],
+ distance: Number.MAX_VALUE
+ };
+ })
+ file.trk.forEach((track, index) => {
+ track.getSegments().forEach((segment) => {
+ segment.trkpt.forEach((point) => {
+ file.wpt.forEach((wpt, wptIndex) => {
+ let dist = distance(point.getCoordinates(), wpt.getCoordinates());
+ if (dist < closest[wptIndex].distance) {
+ closest[wptIndex].distance = dist;
+ closest[wptIndex].index = [index];
+ } else if (dist === closest[wptIndex].distance) {
+ closest[wptIndex].index.push(index);
+ }
+ });
+ })
+ });
+ });
+
+ file.trk.forEach((track, index) => {
+ let tracks = track.trkseg.map((segment, segmentIndex) => {
+ let t = track.replaceTrackSegments(0, track.trkseg.length - 1, [segment])[0];
+ if (track.name) {
+ t.name = `${track.name} (${segmentIndex + 1})`;
+ }
+ return t;
+ });
+ let newFile = file.replaceTracks(0, file.trk.length - 1, tracks)[0];
+ newFile = newFile.replaceWaypoints(0, file.wpt.length - 1, closest.filter((c) => c.index.includes(index)).map((c) => file.wpt[c.wptIndex]))[0];
+ newFile = produce(newFile, (f) => {
+ f._data.id = fileIds[index];
+ f.metadata.name = track.name ?? `${file.metadata.name} (${index + 1})`;
+ });
+ draft.set(newFile._data.id, freeze(newFile));
+ });
+ } else if (file.trk.length === 1) {
+ let fileIds = getFileIds(file.trk[0].trkseg.length);
+
+
+ let closest = file.wpt.map((wpt, wptIndex) => {
+ return {
+ wptIndex: wptIndex,
+ index: [0],
+ distance: Number.MAX_VALUE
+ };
+ })
+ file.trk[0].trkseg.forEach((segment, index) => {
+ segment.trkpt.forEach((point) => {
+ file.wpt.forEach((wpt, wptIndex) => {
+ let dist = distance(point.getCoordinates(), wpt.getCoordinates());
+ if (dist < closest[wptIndex].distance) {
+ closest[wptIndex].distance = dist;
+ closest[wptIndex].index = [index];
+ } else if (dist === closest[wptIndex].distance) {
+ closest[wptIndex].index.push(index);
+ }
+ });
+ });
+ });
+
+ file.trk[0].trkseg.forEach((segment, index) => {
+ let newFile = file.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [segment])[0];
+ newFile = newFile.replaceWaypoints(0, file.wpt.length - 1, closest.filter((c) => c.index.includes(index)).map((c) => file.wpt[c.wptIndex]))[0];
+ newFile = produce(newFile, (f) => {
+ f._data.id = fileIds[index];
+ f.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`;
+ });
+ draft.set(newFile._data.id, freeze(newFile));
+ });
+ }
+ // TODO waypoints
+ draft.delete(fileId);
+ } else if (level === ListLevel.TRACK) {
+ let newFile = file;
+ for (let item of items) {
+ let trackIndex = (item as ListTrackItem).getTrackIndex();
+ let track = file.trk[trackIndex];
+ let tracks = track.trkseg.map((segment, segmentIndex) => {
+ let t = track.clone().replaceTrackSegments(0, track.trkseg.length - 1, [segment])[0];
+ if (track.name) {
+ t.name = `${track.name} (${segmentIndex + 1})`;
+ }
+ return t;
+ });
+ newFile = newFile.replaceTracks(trackIndex, trackIndex, tracks)[0];
+ }
+ draft.set(newFile._data.id, freeze(newFile));
+ }
+ }
+ });
+ });
+ },
split(fileId: string, trackIndex: number, segmentIndex: number, coordinates: Coordinates) {
let splitType = get(splitAs);
return applyGlobal((draft) => {
diff --git a/website/src/lib/stores.ts b/website/src/lib/stores.ts
index 8b444244..f951898f 100644
--- a/website/src/lib/stores.ts
+++ b/website/src/lib/stores.ts
@@ -5,7 +5,7 @@ import { GPXFile, buildGPX, parseGPX, GPXStatistics, type Coordinates } from 'gp
import { tick } from 'svelte';
import { _ } from 'svelte-i18n';
import type { GPXLayer } from '$lib/components/gpx-layer/GPXLayer';
-import { dbUtils, fileObservers, settings } from './db';
+import { dbUtils, fileObservers, getFile, getStatistics, settings } from './db';
import { applyToOrderedSelectedItemsFromFile, selectFile, selection } from '$lib/components/file-list/Selection';
import { ListFileItem, ListWaypointItem, ListWaypointsItem } from '$lib/components/file-list/FileList';
import type { RoutingControls } from '$lib/components/toolbar/tools/routing/RoutingControls';
@@ -21,18 +21,15 @@ export const slicedGPXStatistics: Writable<[GPXStatistics, number, number] | und
function updateGPXData() {
let statistics = new GPXStatistics();
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
- let fileStore = get(fileObservers).get(fileId);
- if (fileStore) {
- let stats = get(fileStore)?.statistics;
- if (stats) {
- let first = true;
- items.forEach((item) => {
- if (!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) || first) {
- statistics.mergeWith(stats.getStatisticsFor(item));
- first = false;
- }
- });
- }
+ let stats = getStatistics(fileId);
+ if (stats) {
+ let first = true;
+ items.forEach((item) => {
+ if (!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) || first) {
+ statistics.mergeWith(stats.getStatisticsFor(item));
+ first = false;
+ }
+ });
}
}, false);
gpxStatistics.set(statistics);
@@ -210,13 +207,10 @@ function selectFileWhenLoaded(fileId: string) {
export function exportSelectedFiles() {
get(selection).forEach(async (item) => {
if (item instanceof ListFileItem) {
- let fileStore = get(fileObservers).get(item.getFileId());
- if (fileStore) {
- let file = get(fileStore)?.file;
- if (file) {
- exportFile(file);
- await new Promise(resolve => setTimeout(resolve, 200));
- }
+ let file = getFile(item.getFileId());
+ if (file) {
+ exportFile(file);
+ await new Promise(resolve => setTimeout(resolve, 200));
}
}
});
diff --git a/website/src/locales/en.json b/website/src/locales/en.json
index 09781343..a6c941be 100644
--- a/website/src/locales/en.json
+++ b/website/src/locales/en.json
@@ -141,7 +141,12 @@
"help_merge_contents": "Merging the contents of the selected file items will group all the contents inside the first file item",
"help_cannot_merge_contents": "Your selection needs to contain several file items to merge their contents"
},
- "extract_tooltip": "Extract contents",
+ "extract": {
+ "tooltip": "Extract contents to separate file items",
+ "button": "Extract",
+ "help": "Extracting the contents of the selected file items will create a separate file item for each of their contents",
+ "help_invalid_selection": "Your selection needs to contain file items with multiple traces to extract them"
+ },
"waypoint": {
"tooltip": "Create and edit points of interest",
"name": "Name",