mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-10 07:36:29 +00:00
merge
This commit is contained in:
1 parent
24b877c4ee
commit
f642d60f42
6 files changed
+635
-183
No files matched your search
@@ -6,7 +6,6 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { ListFileItem, ListTrackItem } from '$lib/components/file-list/file-list';
|
||||
import Help from '$lib/components/Help.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
@@ -16,43 +15,43 @@
|
||||
import { Group } from '@lucide/svelte';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { fileActions } from '$lib/logic/file-actions';
|
||||
import { gpxStatistics } from '$lib/logic/statistics';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { engine } from '$lib/engine';
|
||||
|
||||
let props: {
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
const selection = engine.selection;
|
||||
const statistics = engine.statistics;
|
||||
|
||||
// the number of segments of the selection that have trackpoints
|
||||
const request = engine.requestStatistics();
|
||||
request.set(['anchors']);
|
||||
onDestroy(() => request.release());
|
||||
let segmentCount = $derived($statistics.anchors?.segmentStarts.length ?? 0);
|
||||
|
||||
// several files, tracks or segments, or a single file or track with several segments
|
||||
let canMergeTraces = $derived.by(() => {
|
||||
if ($selection.size > 1) {
|
||||
return true;
|
||||
} else if ($selection.size === 1) {
|
||||
let selected = $selection.getSelected()[0];
|
||||
if (selected instanceof ListFileItem) {
|
||||
let file = fileStateCollection.getFile(selected.getFileId());
|
||||
if (file) {
|
||||
return file.getSegments().length > 1;
|
||||
}
|
||||
} else if (selected instanceof ListTrackItem) {
|
||||
let trackIndex = selected.getTrackIndex();
|
||||
let file = fileStateCollection.getFile(selected.getFileId());
|
||||
if (file && trackIndex < file.trk.length) {
|
||||
return file.trk[trackIndex].getSegments().length > 1;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
switch ($selection.type) {
|
||||
case 'file':
|
||||
return $selection.fileIds.length > 1 || segmentCount > 1;
|
||||
case 'track':
|
||||
return $selection.trackIds.length > 1 || segmentCount > 1;
|
||||
case 'segment':
|
||||
return $selection.segmentIds.length > 1;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// several files or tracks, that can be put together
|
||||
let canMergeContents = $derived(
|
||||
$selection.size > 1 &&
|
||||
$selection
|
||||
.getSelected()
|
||||
.some((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
|
||||
($selection.type === 'file' && $selection.fileIds.length > 1) ||
|
||||
($selection.type === 'track' && $selection.trackIds.length > 1)
|
||||
);
|
||||
|
||||
let hasTimes = $derived(($statistics.global.totalTime ?? 0) > 0);
|
||||
let removeGaps = $state(false);
|
||||
let mergeType = $state(MergeType.TRACES);
|
||||
</script>
|
||||
@@ -68,7 +67,7 @@
|
||||
{i18n._('toolbar.merge.merge_contents')}
|
||||
</Label>
|
||||
</RadioGroup.Root>
|
||||
{#if mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0}
|
||||
{#if mergeType === MergeType.TRACES && hasTimes}
|
||||
<div class="flex flex-row items-center gap-1.5">
|
||||
<Checkbox id="remove-gaps" bind:checked={removeGaps} />
|
||||
<Label for="remove-gaps">{i18n._('toolbar.merge.remove_gaps')}</Label>
|
||||
@@ -80,9 +79,9 @@
|
||||
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
|
||||
(mergeType === MergeType.CONTENTS && !canMergeContents)}
|
||||
onclick={() => {
|
||||
fileActions.mergeSelection(
|
||||
mergeType === MergeType.TRACES,
|
||||
mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0 && removeGaps
|
||||
engine.merge(
|
||||
mergeType === MergeType.TRACES ? 'connect' : 'group',
|
||||
mergeType === MergeType.TRACES && hasTimes && removeGaps
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -32,6 +32,12 @@ export type {
|
||||
|
||||
type Wasm = typeof import('gpx-rs');
|
||||
|
||||
/**
|
||||
* How a merge puts the selected elements together: `connect` makes a single segment of their
|
||||
* trackpoints, `group` keeps them as they are, in the first one.
|
||||
*/
|
||||
export type MergeType = 'connect' | 'group';
|
||||
|
||||
/** What a split cuts in two: the file, the track or the segment of the trackpoint. */
|
||||
export type SplitType = 'files' | 'tracks' | 'segments';
|
||||
|
||||
@@ -648,6 +654,17 @@ class Engine {
|
||||
return this.run((w) => w.reverse());
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the selected files, tracks or segments into the first of them (see `MergeType`). With
|
||||
* `removeGaps`, the parts that are connected get closer in time if they have timestamps.
|
||||
* Resolves to `false` when there is nothing to merge.
|
||||
*/
|
||||
merge(type: MergeType, removeGaps = false) {
|
||||
return this.run((w) =>
|
||||
w.merge(type === 'connect' ? w.MergeType.Connect : w.MergeType.Group, removeGaps)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cuts the file, the track or the segment (see `SplitType`) of the trackpoint `at` of the
|
||||
* selection in two, there: the trackpoint ends the first part and starts the second one. The
|
||||
|
||||
@@ -99,154 +99,6 @@ export async function loadFiles(list: FileList | File[]): Promise<string[]> {
|
||||
|
||||
// Helper functions for file operations
|
||||
export const fileActions = {
|
||||
mergeSelection: (mergeTraces: boolean, removeGaps: boolean) => {
|
||||
fileActionManager.applyGlobal((draft) => {
|
||||
let first = true;
|
||||
let target: ListItem = new ListRootItem();
|
||||
let targetFile: GPXFile | undefined = undefined;
|
||||
let toMerge: {
|
||||
trk: Track[];
|
||||
trkseg: TrackSegment[];
|
||||
wpt: Waypoint[];
|
||||
} = {
|
||||
trk: [],
|
||||
trkseg: [],
|
||||
wpt: [],
|
||||
};
|
||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||
let file = draft.get(fileId);
|
||||
let originalFile = fileStateCollection.getFile(fileId);
|
||||
if (file && originalFile) {
|
||||
if (level === ListLevel.FILE) {
|
||||
toMerge.trk.push(...originalFile.trk.map((track) => track.clone()));
|
||||
for (const wpt of originalFile.wpt) {
|
||||
if (!toMerge.wpt.some((w) => w.equals(wpt))) {
|
||||
toMerge.wpt.push(wpt.clone());
|
||||
}
|
||||
}
|
||||
if (first) {
|
||||
target = items[0];
|
||||
targetFile = file;
|
||||
} else {
|
||||
draft.delete(fileId);
|
||||
}
|
||||
} else {
|
||||
if (level === ListLevel.TRACK) {
|
||||
items.forEach((item, index) => {
|
||||
let trackIndex = (item as ListTrackItem).getTrackIndex();
|
||||
toMerge.trkseg.splice(
|
||||
0,
|
||||
0,
|
||||
...originalFile.trk[trackIndex].trkseg.map((segment) =>
|
||||
segment.clone()
|
||||
)
|
||||
);
|
||||
if (index === items.length - 1) {
|
||||
// Order is reversed, so the last track is the first one and the one to keep
|
||||
target = item;
|
||||
file.trk[trackIndex].trkseg = [];
|
||||
} else {
|
||||
file.trk.splice(trackIndex, 1);
|
||||
}
|
||||
});
|
||||
} else if (level === ListLevel.SEGMENT) {
|
||||
items.forEach((item, index) => {
|
||||
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
|
||||
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
|
||||
if (index === items.length - 1) {
|
||||
// Order is reversed, so the last segment is the first one and the one to keep
|
||||
target = item;
|
||||
}
|
||||
toMerge.trkseg.splice(
|
||||
0,
|
||||
0,
|
||||
originalFile.trk[trackIndex].trkseg[segmentIndex].clone()
|
||||
);
|
||||
file.trk[trackIndex].trkseg.splice(segmentIndex, 1);
|
||||
});
|
||||
}
|
||||
targetFile = file;
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
});
|
||||
if (mergeTraces) {
|
||||
let statistics = get(gpxStatistics);
|
||||
let speed =
|
||||
statistics.global.speed.moving > 0 ? statistics.global.speed.moving : undefined;
|
||||
let startTime: Date | undefined = undefined;
|
||||
if (speed !== undefined) {
|
||||
if (
|
||||
statistics.global.length > 0 &&
|
||||
statistics.getTrackPoint(0)!.trkpt.time !== undefined
|
||||
) {
|
||||
startTime = statistics.getTrackPoint(0)!.trkpt.time;
|
||||
} else {
|
||||
for (let i = 0; i < statistics.global.length; i++) {
|
||||
const point = statistics.getTrackPoint(i)!;
|
||||
if (point.trkpt.time !== undefined) {
|
||||
startTime = new Date(
|
||||
point.trkpt.time.getTime() -
|
||||
(1000 * 3600 * point.distance.total) / speed
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (toMerge.trk.length > 0 && toMerge.trk[0].trkseg.length > 0) {
|
||||
let s = new TrackSegment();
|
||||
toMerge.trk.map((track) => {
|
||||
track.trkseg.forEach((segment) => {
|
||||
s.replaceTrackPoints(
|
||||
s.trkpt.length,
|
||||
s.trkpt.length,
|
||||
segment.trkpt.slice(),
|
||||
speed,
|
||||
startTime,
|
||||
removeGaps
|
||||
);
|
||||
});
|
||||
});
|
||||
toMerge.trk = [toMerge.trk[0]];
|
||||
toMerge.trk[0].trkseg = [s];
|
||||
}
|
||||
if (toMerge.trkseg.length > 0) {
|
||||
let s = new TrackSegment();
|
||||
toMerge.trkseg.forEach((segment) => {
|
||||
s.replaceTrackPoints(
|
||||
s.trkpt.length,
|
||||
s.trkpt.length,
|
||||
segment.trkpt.slice(),
|
||||
speed,
|
||||
startTime,
|
||||
removeGaps
|
||||
);
|
||||
});
|
||||
toMerge.trkseg = [s];
|
||||
}
|
||||
}
|
||||
if (targetFile) {
|
||||
targetFile = targetFile as GPXFile;
|
||||
if (target instanceof ListFileItem) {
|
||||
targetFile.replaceTracks(0, targetFile.trk.length - 1, toMerge.trk);
|
||||
targetFile.replaceWaypoints(0, targetFile.wpt.length - 1, toMerge.wpt);
|
||||
} else if (target instanceof ListTrackItem) {
|
||||
let trackIndex = target.getTrackIndex();
|
||||
targetFile.replaceTrackSegments(trackIndex, 0, -1, toMerge.trkseg);
|
||||
} else if (target instanceof ListTrackSegmentItem) {
|
||||
let trackIndex = target.getTrackIndex();
|
||||
let segmentIndex = target.getSegmentIndex();
|
||||
targetFile.replaceTrackSegments(
|
||||
trackIndex,
|
||||
segmentIndex,
|
||||
segmentIndex - 1,
|
||||
toMerge.trkseg
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
extractSelection: () => {
|
||||
return fileActionManager.applyGlobal((draft) => {
|
||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||
|
||||
Reference in new issue
Block a user