mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-01 08:12:32 +00:00
extract tool
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as ContextMenu from '$lib/components/ui/context-menu';
|
||||
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 {
|
||||
ListFileItem,
|
||||
@@ -56,9 +56,9 @@
|
||||
on:mouseenter={() => {
|
||||
if (item instanceof ListWaypointItem) {
|
||||
let layer = gpxLayers.get(item.getFileId());
|
||||
let fileStore = get(fileObservers).get(item.getFileId());
|
||||
if (layer && fileStore) {
|
||||
let waypoint = get(fileStore)?.file.wpt[item.getWaypointIndex()];
|
||||
let file = getFile(item.getFileId());
|
||||
if (layer && file) {
|
||||
let waypoint = file.wpt[item.getWaypointIndex()];
|
||||
if (waypoint) {
|
||||
layer.showWaypointPopup(waypoint);
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { get, writable } from "svelte/store";
|
||||
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 {
|
||||
item: ListItem;
|
||||
@@ -174,23 +174,23 @@ export function selectAll() {
|
||||
$selection.set(new ListFileItem(fileId), true);
|
||||
});
|
||||
} else if (item instanceof ListTrackItem) {
|
||||
let fileStore = get(fileObservers).get(item.getFileId());
|
||||
if (fileStore) {
|
||||
get(fileStore)?.file.trk.forEach((_track, trackId) => {
|
||||
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 fileStore = get(fileObservers).get(item.getFileId());
|
||||
if (fileStore) {
|
||||
get(fileStore)?.file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
|
||||
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 fileStore = get(fileObservers).get(item.getFileId());
|
||||
if (fileStore) {
|
||||
get(fileStore)?.file.wpt.forEach((_waypoint, waypointId) => {
|
||||
let file = getFile(item.getFileId());
|
||||
if (file) {
|
||||
file.wpt.forEach((_waypoint, waypointId) => {
|
||||
$selection.set(new ListWaypointItem(item.getFileId(), waypointId), true);
|
||||
});
|
||||
}
|
||||
|
@@ -43,7 +43,7 @@
|
||||
</ToolbarItem>
|
||||
<ToolbarItem tool={Tool.EXTRACT}>
|
||||
<Ungroup slot="icon" size="18" />
|
||||
<span slot="tooltip">{$_('toolbar.extract_tooltip')}</span>
|
||||
<span slot="tooltip">{$_('toolbar.extract.tooltip')}</span>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem tool={Tool.REDUCE}>
|
||||
<Filter slot="icon" size="18" />
|
||||
|
@@ -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 @@
|
||||
<Time />
|
||||
{:else if $currentTool === Tool.MERGE}
|
||||
<Merge />
|
||||
{:else if $currentTool === Tool.EXTRACT}
|
||||
<Extract />
|
||||
{:else if $currentTool === Tool.CLEAN}
|
||||
<Clean />
|
||||
{:else if $currentTool === Tool.REDUCE}
|
||||
|
52
website/src/lib/components/toolbar/tools/Extract.svelte
Normal file
52
website/src/lib/components/toolbar/tools/Extract.svelte
Normal 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>
|
@@ -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,32 +24,20 @@
|
||||
} 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;
|
||||
let file = getFile(selected.getFileId());
|
||||
if (file) {
|
||||
canMergeTraces = file.getSegments().length > 1;
|
||||
} else {
|
||||
canMergeTraces = false;
|
||||
}
|
||||
} 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;
|
||||
let file = getFile(selected.getFileId());
|
||||
if (file && trackIndex < file.trk.length) {
|
||||
canMergeTraces = file.trk[trackIndex].getSegments().length > 1;
|
||||
} else {
|
||||
canMergeTraces = false;
|
||||
}
|
||||
} else {
|
||||
canMergeTraces = false;
|
||||
}
|
||||
} else {
|
||||
canMergeContents = false;
|
||||
}
|
||||
|
@@ -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,15 +36,13 @@
|
||||
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()];
|
||||
let file = getFile(item.getFileId());
|
||||
let waypoint = file?.wpt[item.getWaypointIndex()];
|
||||
if (waypoint) {
|
||||
return [waypoint, item.getFileId()];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
@@ -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<number> = 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) => {
|
||||
|
@@ -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,9 +21,7 @@ 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;
|
||||
let stats = getStatistics(fileId);
|
||||
if (stats) {
|
||||
let first = true;
|
||||
items.forEach((item) => {
|
||||
@@ -33,7 +31,6 @@ function updateGPXData() {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
gpxStatistics.set(statistics);
|
||||
}
|
||||
@@ -210,15 +207,12 @@ 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;
|
||||
let file = getFile(item.getFileId());
|
||||
if (file) {
|
||||
exportFile(file);
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -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",
|
||||
|
Reference in New Issue
Block a user