extract tool

This commit is contained in:
vcoppe
2024-06-15 18:44:17 +02:00
parent 6ed2d3e406
commit ea53a82451
10 changed files with 218 additions and 64 deletions

View File

@@ -2,7 +2,7 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as ContextMenu from '$lib/components/ui/context-menu'; import * as ContextMenu from '$lib/components/ui/context-menu';
import Shortcut from '$lib/components/Shortcut.svelte'; import Shortcut from '$lib/components/Shortcut.svelte';
import { dbUtils, fileObservers, settings } from '$lib/db'; import { dbUtils, getFile, settings } from '$lib/db';
import { Copy, MapPin, Plus, Trash2, Waypoints } from 'lucide-svelte'; import { Copy, MapPin, Plus, Trash2, Waypoints } from 'lucide-svelte';
import { import {
ListFileItem, ListFileItem,
@@ -56,9 +56,9 @@
on:mouseenter={() => { on:mouseenter={() => {
if (item instanceof ListWaypointItem) { if (item instanceof ListWaypointItem) {
let layer = gpxLayers.get(item.getFileId()); let layer = gpxLayers.get(item.getFileId());
let fileStore = get(fileObservers).get(item.getFileId()); let file = getFile(item.getFileId());
if (layer && fileStore) { if (layer && file) {
let waypoint = get(fileStore)?.file.wpt[item.getWaypointIndex()]; let waypoint = file.wpt[item.getWaypointIndex()];
if (waypoint) { if (waypoint) {
layer.showWaypointPopup(waypoint); layer.showWaypointPopup(waypoint);
} }

View File

@@ -1,6 +1,6 @@
import { get, writable } from "svelte/store"; import { get, writable } from "svelte/store";
import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, type ListLevel, sortItems, ListWaypointsItem } from "./FileList"; import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, type ListLevel, sortItems, ListWaypointsItem } from "./FileList";
import { fileObservers, settings } from "$lib/db"; import { fileObservers, getFile, settings } from "$lib/db";
export class SelectionTreeType { export class SelectionTreeType {
item: ListItem; item: ListItem;
@@ -174,23 +174,23 @@ export function selectAll() {
$selection.set(new ListFileItem(fileId), true); $selection.set(new ListFileItem(fileId), true);
}); });
} else if (item instanceof ListTrackItem) { } else if (item instanceof ListTrackItem) {
let fileStore = get(fileObservers).get(item.getFileId()); let file = getFile(item.getFileId());
if (fileStore) { if (file) {
get(fileStore)?.file.trk.forEach((_track, trackId) => { file.trk.forEach((_track, trackId) => {
$selection.set(new ListTrackItem(item.getFileId(), trackId), true); $selection.set(new ListTrackItem(item.getFileId(), trackId), true);
}); });
} }
} else if (item instanceof ListTrackSegmentItem) { } else if (item instanceof ListTrackSegmentItem) {
let fileStore = get(fileObservers).get(item.getFileId()); let file = getFile(item.getFileId());
if (fileStore) { if (file) {
get(fileStore)?.file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => { file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
$selection.set(new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId), true); $selection.set(new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId), true);
}); });
} }
} else if (item instanceof ListWaypointItem) { } else if (item instanceof ListWaypointItem) {
let fileStore = get(fileObservers).get(item.getFileId()); let file = getFile(item.getFileId());
if (fileStore) { if (file) {
get(fileStore)?.file.wpt.forEach((_waypoint, waypointId) => { file.wpt.forEach((_waypoint, waypointId) => {
$selection.set(new ListWaypointItem(item.getFileId(), waypointId), true); $selection.set(new ListWaypointItem(item.getFileId(), waypointId), true);
}); });
} }

View File

@@ -43,7 +43,7 @@
</ToolbarItem> </ToolbarItem>
<ToolbarItem tool={Tool.EXTRACT}> <ToolbarItem tool={Tool.EXTRACT}>
<Ungroup slot="icon" size="18" /> <Ungroup slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.extract_tooltip')}</span> <span slot="tooltip">{$_('toolbar.extract.tooltip')}</span>
</ToolbarItem> </ToolbarItem>
<ToolbarItem tool={Tool.REDUCE}> <ToolbarItem tool={Tool.REDUCE}>
<Filter slot="icon" size="18" /> <Filter slot="icon" size="18" />

View File

@@ -7,6 +7,7 @@
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte';
import Time from '$lib/components/toolbar/tools/Time.svelte'; import Time from '$lib/components/toolbar/tools/Time.svelte';
import Merge from '$lib/components/toolbar/tools/Merge.svelte'; import Merge from '$lib/components/toolbar/tools/Merge.svelte';
import Extract from '$lib/components/toolbar/tools/Extract.svelte';
import Clean from '$lib/components/toolbar/tools/Clean.svelte'; import Clean from '$lib/components/toolbar/tools/Clean.svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte'; import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
@@ -44,6 +45,8 @@
<Time /> <Time />
{:else if $currentTool === Tool.MERGE} {:else if $currentTool === Tool.MERGE}
<Merge /> <Merge />
{:else if $currentTool === Tool.EXTRACT}
<Extract />
{:else if $currentTool === Tool.CLEAN} {:else if $currentTool === Tool.CLEAN}
<Clean /> <Clean />
{:else if $currentTool === Tool.REDUCE} {:else if $currentTool === Tool.REDUCE}

View File

@@ -0,0 +1,52 @@
<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';
import Help from '$lib/components/Help.svelte';
import { dbUtils, getFile } from '$lib/db';
import { _ } from 'svelte-i18n';
$: validSelection =
$selection.size > 0 &&
$selection.getSelected().every((item) => {
if (
item instanceof ListWaypointsItem ||
item instanceof ListWaypointItem ||
item instanceof ListTrackSegmentItem
) {
return false;
}
let file = getFile(item.getFileId());
if (file) {
if (item instanceof ListFileItem) {
return file.getSegments().length > 1;
} else if (item instanceof ListTrackItem) {
if (item.getTrackIndex() < file.trk.length) {
return file.trk[item.getTrackIndex()].getSegments().length > 1;
}
}
}
return false;
});
</script>
<div class="flex flex-col gap-3 w-80">
<Button variant="outline" disabled={!validSelection} on:click={dbUtils.extractSelection}>
<Ungroup size="16" class="mr-1" />
{$_('toolbar.extract.button')}
</Button>
<Help>
{#if validSelection}
{$_('toolbar.extract.help')}
{:else}
{$_('toolbar.extract.help_invalid_selection')}
{/if}
</Help>
</div>

View File

@@ -13,8 +13,7 @@
import { Label } from '$lib/components/ui/label/index.js'; import { Label } from '$lib/components/ui/label/index.js';
import * as RadioGroup from '$lib/components/ui/radio-group'; import * as RadioGroup from '$lib/components/ui/radio-group';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { dbUtils, fileObservers } from '$lib/db'; import { dbUtils, getFile } from '$lib/db';
import { get } from 'svelte/store';
import { Group } from 'lucide-svelte'; import { Group } from 'lucide-svelte';
let canMergeTraces = false; let canMergeTraces = false;
@@ -25,29 +24,17 @@
} else if ($selection.size === 1) { } else if ($selection.size === 1) {
let selected = $selection.getSelected()[0]; let selected = $selection.getSelected()[0];
if (selected instanceof ListFileItem) { if (selected instanceof ListFileItem) {
let fileId = selected.getFileId(); let file = getFile(selected.getFileId());
let fileStore = $fileObservers.get(fileId); if (file) {
if (fileStore) { canMergeTraces = file.getSegments().length > 1;
let file = get(fileStore)?.file;
if (file) {
canMergeTraces = file.getSegments().length > 1;
} else {
canMergeTraces = false;
}
} else { } else {
canMergeTraces = false; canMergeTraces = false;
} }
} else if (selected instanceof ListTrackItem) { } else if (selected instanceof ListTrackItem) {
let fileId = selected.getFileId();
let trackIndex = selected.getTrackIndex(); let trackIndex = selected.getTrackIndex();
let fileStore = $fileObservers.get(fileId); let file = getFile(selected.getFileId());
if (fileStore) { if (file && trackIndex < file.trk.length) {
let file = get(fileStore)?.file; canMergeTraces = file.trk[trackIndex].getSegments().length > 1;
if (file && trackIndex < file.trk.length) {
canMergeTraces = file.trk[trackIndex].getSegments().length > 1;
} else {
canMergeTraces = false;
}
} else { } else {
canMergeTraces = false; canMergeTraces = false;
} }

View File

@@ -13,7 +13,7 @@
import { Waypoint } from 'gpx'; import { Waypoint } from 'gpx';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { ListWaypointItem } from '$lib/components/file-list/FileList'; 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 { get } from 'svelte/store';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
@@ -36,12 +36,10 @@
if ($selection.size === 1) { if ($selection.size === 1) {
let item = $selection.getSelected()[0]; let item = $selection.getSelected()[0];
if (item instanceof ListWaypointItem) { if (item instanceof ListWaypointItem) {
let fileStore = get(fileObservers).get(item.getFileId()); let file = getFile(item.getFileId());
if (fileStore) { let waypoint = file?.wpt[item.getWaypointIndex()];
let waypoint = get(fileStore)?.file.wpt[item.getWaypointIndex()]; if (waypoint) {
if (waypoint) { return [waypoint, item.getFileId()];
return [waypoint, item.getFileId()];
}
} }
} }
} }

View File

@@ -1,5 +1,5 @@
import Dexie, { liveQuery } from 'dexie'; 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 { 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 { writable, get, derived, type Readable, type Writable } from 'svelte/store';
import { initTargetMapBounds, splitAs, updateTargetMapBounds } from './stores'; 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<number> = dexieStore(() => db.settings.get('patchIndex'), -1); const patchIndex: Readable<number> = dexieStore(() => db.settings.get('patchIndex'), -1);
const patchMinMaxIndex: Readable<{ min: number, max: number }> = dexieStore(() => db.patches.orderBy(':id').keys().then(keys => { const patchMinMaxIndex: Readable<{ min: number, max: number }> = dexieStore(() => db.patches.orderBy(':id').keys().then(keys => {
if (keys.length === 0) { if (keys.length === 0) {
@@ -605,6 +615,111 @@ export const dbUtils = {
}, false); }, 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) { split(fileId: string, trackIndex: number, segmentIndex: number, coordinates: Coordinates) {
let splitType = get(splitAs); let splitType = get(splitAs);
return applyGlobal((draft) => { return applyGlobal((draft) => {

View File

@@ -5,7 +5,7 @@ import { GPXFile, buildGPX, parseGPX, GPXStatistics, type Coordinates } from 'gp
import { tick } from 'svelte'; import { tick } from 'svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import type { GPXLayer } from '$lib/components/gpx-layer/GPXLayer'; 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 { applyToOrderedSelectedItemsFromFile, selectFile, selection } from '$lib/components/file-list/Selection';
import { ListFileItem, ListWaypointItem, ListWaypointsItem } from '$lib/components/file-list/FileList'; import { ListFileItem, ListWaypointItem, ListWaypointsItem } from '$lib/components/file-list/FileList';
import type { RoutingControls } from '$lib/components/toolbar/tools/routing/RoutingControls'; import type { RoutingControls } from '$lib/components/toolbar/tools/routing/RoutingControls';
@@ -21,18 +21,15 @@ export const slicedGPXStatistics: Writable<[GPXStatistics, number, number] | und
function updateGPXData() { function updateGPXData() {
let statistics = new GPXStatistics(); let statistics = new GPXStatistics();
applyToOrderedSelectedItemsFromFile((fileId, level, items) => { applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let fileStore = get(fileObservers).get(fileId); let stats = getStatistics(fileId);
if (fileStore) { if (stats) {
let stats = get(fileStore)?.statistics; let first = true;
if (stats) { items.forEach((item) => {
let first = true; if (!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) || first) {
items.forEach((item) => { statistics.mergeWith(stats.getStatisticsFor(item));
if (!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) || first) { first = false;
statistics.mergeWith(stats.getStatisticsFor(item)); }
first = false; });
}
});
}
} }
}, false); }, false);
gpxStatistics.set(statistics); gpxStatistics.set(statistics);
@@ -210,13 +207,10 @@ function selectFileWhenLoaded(fileId: string) {
export function exportSelectedFiles() { export function exportSelectedFiles() {
get(selection).forEach(async (item) => { get(selection).forEach(async (item) => {
if (item instanceof ListFileItem) { if (item instanceof ListFileItem) {
let fileStore = get(fileObservers).get(item.getFileId()); let file = getFile(item.getFileId());
if (fileStore) { if (file) {
let file = get(fileStore)?.file; exportFile(file);
if (file) { await new Promise(resolve => setTimeout(resolve, 200));
exportFile(file);
await new Promise(resolve => setTimeout(resolve, 200));
}
} }
} }
}); });

View File

@@ -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_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" "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": { "waypoint": {
"tooltip": "Create and edit points of interest", "tooltip": "Create and edit points of interest",
"name": "Name", "name": "Name",