mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-31 23:53:25 +00:00
clean tool
This commit is contained in:
@@ -274,6 +274,42 @@ export class GPXFile extends GPXTreeNode<Track>{
|
|||||||
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
|
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clean(bounds: [Coordinates, Coordinates], inside: boolean, deleteTrackPoints: boolean, deleteWaypoints: boolean, trackIndices?: number[], segmentIndices?: number[], waypointIndices?: number[]) {
|
||||||
|
return produce(this, (draft) => {
|
||||||
|
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
||||||
|
if (deleteTrackPoints) {
|
||||||
|
let trk = og.trk.slice();
|
||||||
|
let i = 0;
|
||||||
|
let trackIndex = 0;
|
||||||
|
while (i < trk.length) {
|
||||||
|
if (trackIndices === undefined || trackIndices.includes(trackIndex)) {
|
||||||
|
trk[i] = trk[i].clean(bounds, inside, segmentIndices);
|
||||||
|
if (trk[i].getNumberOfTrackPoints() === 0) {
|
||||||
|
trk.splice(i, 1);
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
trackIndex++;
|
||||||
|
}
|
||||||
|
draft.trk = freeze(trk); // Pre-freeze the array, faster as well
|
||||||
|
}
|
||||||
|
if (deleteWaypoints) {
|
||||||
|
let wpt = og.wpt.filter((point, waypointIndex) => {
|
||||||
|
if (waypointIndices === undefined || waypointIndices.includes(waypointIndex)) {
|
||||||
|
let inBounds = point.attributes.lat >= bounds[0].lat && point.attributes.lat <= bounds[1].lat && point.attributes.lon >= bounds[0].lon && point.attributes.lon <= bounds[1].lon;
|
||||||
|
return inBounds !== inside;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
draft.wpt = freeze(wpt); // Pre-freeze the array, faster as well
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// A class that represents a Track in a GPX file
|
// A class that represents a Track in a GPX file
|
||||||
@@ -414,6 +450,29 @@ export class Track extends GPXTreeNode<TrackSegment> {
|
|||||||
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
|
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clean(bounds: [Coordinates, Coordinates], inside: boolean, segmentIndices?: number[]) {
|
||||||
|
return produce(this, (draft) => {
|
||||||
|
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
||||||
|
let trkseg = og.trkseg.slice();
|
||||||
|
let i = 0;
|
||||||
|
let segmentIndex = 0;
|
||||||
|
while (i < trkseg.length) {
|
||||||
|
if (segmentIndices === undefined || segmentIndices.includes(segmentIndex)) {
|
||||||
|
trkseg[i] = trkseg[i].clean(bounds, inside);
|
||||||
|
if (trkseg[i].getNumberOfTrackPoints() === 0) {
|
||||||
|
trkseg.splice(i, 1);
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
segmentIndex++;
|
||||||
|
}
|
||||||
|
draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// A class that represents a TrackSegment in a GPX file
|
// A class that represents a TrackSegment in a GPX file
|
||||||
@@ -616,6 +675,17 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clean(bounds: [Coordinates, Coordinates], inside: boolean) {
|
||||||
|
return produce(this, (draft) => {
|
||||||
|
let og = getOriginal(draft); // Read as much as possible from the original object because it is faster
|
||||||
|
let trkpt = og.trkpt.filter((point) => {
|
||||||
|
let inBounds = point.attributes.lat >= bounds[0].lat && point.attributes.lat <= bounds[1].lat && point.attributes.lon >= bounds[0].lon && point.attributes.lon <= bounds[1].lon;
|
||||||
|
return inBounds !== inside;
|
||||||
|
});
|
||||||
|
draft.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export class TrackPoint {
|
export class TrackPoint {
|
||||||
|
@@ -52,7 +52,7 @@
|
|||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.CLEAN}>
|
<ToolbarItem tool={Tool.CLEAN}>
|
||||||
<SquareDashedMousePointer slot="icon" size="18" />
|
<SquareDashedMousePointer slot="icon" size="18" />
|
||||||
<span slot="tooltip">{$_('toolbar.clean_tooltip')}</span>
|
<span slot="tooltip">{$_('toolbar.clean.tooltip')}</span>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.STYLE}>
|
<ToolbarItem tool={Tool.STYLE}>
|
||||||
<Palette slot="icon" size="18" />
|
<Palette slot="icon" size="18" />
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
import Scissors from '$lib/components/toolbar/tools/Scissors.svelte';
|
import Scissors from '$lib/components/toolbar/tools/Scissors.svelte';
|
||||||
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte';
|
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte';
|
||||||
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
|
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
|
||||||
|
import Clean from '$lib/components/toolbar/tools/Clean.svelte';
|
||||||
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
|
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
@@ -39,6 +40,8 @@
|
|||||||
<Waypoint />
|
<Waypoint />
|
||||||
{:else if $currentTool === Tool.MERGE}
|
{:else if $currentTool === Tool.MERGE}
|
||||||
<Merge />
|
<Merge />
|
||||||
|
{:else if $currentTool === Tool.CLEAN}
|
||||||
|
<Clean />
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
177
website/src/lib/components/toolbar/tools/Clean.svelte
Normal file
177
website/src/lib/components/toolbar/tools/Clean.svelte
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
enum CleanType {
|
||||||
|
INSIDE = 'inside',
|
||||||
|
OUTSIDE = 'outside'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
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 { Button } from '$lib/components/ui/button';
|
||||||
|
import Help from '$lib/components/Help.svelte';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
import { resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||||
|
import { Trash2 } from 'lucide-svelte';
|
||||||
|
import { map } from '$lib/stores';
|
||||||
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
|
import { dbUtils } from '$lib/db';
|
||||||
|
|
||||||
|
let cleanType = CleanType.INSIDE;
|
||||||
|
let deleteTrackpoints = true;
|
||||||
|
let deleteWaypoints = true;
|
||||||
|
let rectangleCoordinates: mapboxgl.LngLat[] = [];
|
||||||
|
|
||||||
|
function updateRectangle() {
|
||||||
|
if ($map) {
|
||||||
|
if (rectangleCoordinates.length != 2) {
|
||||||
|
if ($map.getLayer('rectangle')) {
|
||||||
|
$map.removeLayer('rectangle');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let data = {
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: [
|
||||||
|
[
|
||||||
|
[rectangleCoordinates[0].lng, rectangleCoordinates[0].lat],
|
||||||
|
[rectangleCoordinates[1].lng, rectangleCoordinates[0].lat],
|
||||||
|
[rectangleCoordinates[1].lng, rectangleCoordinates[1].lat],
|
||||||
|
[rectangleCoordinates[0].lng, rectangleCoordinates[1].lat],
|
||||||
|
[rectangleCoordinates[0].lng, rectangleCoordinates[0].lat]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let source = $map.getSource('rectangle');
|
||||||
|
if (source) {
|
||||||
|
source.setData(data);
|
||||||
|
} else {
|
||||||
|
$map.addSource('rectangle', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!$map.getLayer('rectangle')) {
|
||||||
|
$map.addLayer({
|
||||||
|
id: 'rectangle',
|
||||||
|
type: 'fill',
|
||||||
|
source: 'rectangle',
|
||||||
|
paint: {
|
||||||
|
'fill-color': 'SteelBlue',
|
||||||
|
'fill-opacity': 0.5
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (rectangleCoordinates) {
|
||||||
|
updateRectangle();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mousedown = false;
|
||||||
|
function onMouseDown(e: any) {
|
||||||
|
mousedown = true;
|
||||||
|
rectangleCoordinates = [e.lngLat, e.lngLat];
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(e: any) {
|
||||||
|
if (mousedown) {
|
||||||
|
rectangleCoordinates[1] = e.lngLat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp(e: any) {
|
||||||
|
mousedown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setCrosshairCursor();
|
||||||
|
});
|
||||||
|
|
||||||
|
$: if ($map) {
|
||||||
|
$map.on('mousedown', onMouseDown);
|
||||||
|
$map.on('mousemove', onMouseMove);
|
||||||
|
$map.on('mouseup', onMouseUp);
|
||||||
|
$map.dragPan.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
resetCursor();
|
||||||
|
if ($map) {
|
||||||
|
$map.off('mousedown', onMouseDown);
|
||||||
|
$map.off('mousemove', onMouseMove);
|
||||||
|
$map.off('mouseup', onMouseUp);
|
||||||
|
$map.dragPan.enable();
|
||||||
|
|
||||||
|
if ($map.getLayer('rectangle')) {
|
||||||
|
$map.removeLayer('rectangle');
|
||||||
|
}
|
||||||
|
if ($map.getSource('rectangle')) {
|
||||||
|
$map.removeSource('rectangle');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$: validSelection = $selection.size > 0;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3 max-w-80">
|
||||||
|
<fieldset class="flex flex-col gap-3">
|
||||||
|
<Label class="flex flex-row items-center gap-[6.4px] h-3">
|
||||||
|
<Checkbox bind:checked={deleteTrackpoints} class="scale-90" />
|
||||||
|
{$_('toolbar.clean.delete_trackpoints')}
|
||||||
|
</Label>
|
||||||
|
<Label class="flex flex-row items-center gap-[6.4px] h-3">
|
||||||
|
<Checkbox bind:checked={deleteWaypoints} class="scale-90" />
|
||||||
|
{$_('toolbar.clean.delete_waypoints')}
|
||||||
|
</Label>
|
||||||
|
<RadioGroup.Root bind:value={cleanType}>
|
||||||
|
<Label class="flex flex-row items-center gap-2">
|
||||||
|
<RadioGroup.Item value={CleanType.INSIDE} />
|
||||||
|
{$_('toolbar.clean.delete_inside')}
|
||||||
|
</Label>
|
||||||
|
<Label class="flex flex-row items-center gap-2">
|
||||||
|
<RadioGroup.Item value={CleanType.OUTSIDE} />
|
||||||
|
{$_('toolbar.clean.delete_outside')}
|
||||||
|
</Label>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
</fieldset>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled={!validSelection || rectangleCoordinates.length != 2}
|
||||||
|
on:click={() => {
|
||||||
|
dbUtils.cleanSelection(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
lat: Math.min(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat),
|
||||||
|
lon: Math.min(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lat: Math.max(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat),
|
||||||
|
lon: Math.max(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
cleanType === CleanType.INSIDE,
|
||||||
|
deleteTrackpoints,
|
||||||
|
deleteWaypoints
|
||||||
|
);
|
||||||
|
rectangleCoordinates = [];
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 size="16" class="mr-1" />
|
||||||
|
{$_('toolbar.clean.button')}
|
||||||
|
</Button>
|
||||||
|
<Help>
|
||||||
|
{#if validSelection}
|
||||||
|
{$_('toolbar.clean.help')}
|
||||||
|
{:else}
|
||||||
|
{$_('toolbar.clean.help_no_selection')}
|
||||||
|
{/if}
|
||||||
|
</Help>
|
||||||
|
</div>
|
@@ -647,6 +647,35 @@ export const dbUtils = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
cleanSelection: (bounds: [Coordinates, Coordinates], inside: boolean, deleteTrackPoints: boolean, deleteWaypoints: boolean) => {
|
||||||
|
if (get(selection).size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyGlobal((draft) => {
|
||||||
|
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
|
let file = original(draft)?.get(fileId);
|
||||||
|
if (file) {
|
||||||
|
let newFile = file;
|
||||||
|
if (level === ListLevel.FILE) {
|
||||||
|
newFile = file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints);
|
||||||
|
} else if (level === ListLevel.TRACK) {
|
||||||
|
let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex());
|
||||||
|
newFile = newFile.clean(bounds, inside, deleteTrackPoints, false, trackIndices);
|
||||||
|
} else if (level === ListLevel.SEGMENT) {
|
||||||
|
let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
|
||||||
|
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex());
|
||||||
|
newFile = newFile.clean(bounds, inside, deleteTrackPoints, false, trackIndices, segmentIndices);
|
||||||
|
} else if (level === ListLevel.WAYPOINTS) {
|
||||||
|
newFile = newFile.clean(bounds, inside, false, deleteWaypoints);
|
||||||
|
} else if (level === ListLevel.WAYPOINT) {
|
||||||
|
let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex());
|
||||||
|
newFile = newFile.clean(bounds, inside, false, deleteWaypoints, [], [], waypointIndices);
|
||||||
|
}
|
||||||
|
draft.set(newFile._data.id, freeze(newFile));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
deleteSelection: () => {
|
deleteSelection: () => {
|
||||||
if (get(selection).size === 0) {
|
if (get(selection).size === 0) {
|
||||||
return;
|
return;
|
||||||
|
@@ -131,7 +131,16 @@
|
|||||||
"extract_tooltip": "Extract inner tracks or segments",
|
"extract_tooltip": "Extract inner tracks or segments",
|
||||||
"waypoint_tooltip": "Create and edit points of interest",
|
"waypoint_tooltip": "Create and edit points of interest",
|
||||||
"reduce_tooltip": "Reduce the number of GPS points",
|
"reduce_tooltip": "Reduce the number of GPS points",
|
||||||
"clean_tooltip": "Clean GPS points and points of interest with a rectangle selection",
|
"clean": {
|
||||||
|
"tooltip": "Clean GPS points and points of interest with a rectangle selection",
|
||||||
|
"delete_trackpoints": "Delete GPS points",
|
||||||
|
"delete_waypoints": "Delete points of interest",
|
||||||
|
"delete_inside": "Delete inside selection",
|
||||||
|
"delete_outside": "Delete outside selection",
|
||||||
|
"button": "Delete",
|
||||||
|
"help": "Select a rectangle area on the map to remove GPS points and points of interest",
|
||||||
|
"help_no_selection": "Select a file element to use the tool"
|
||||||
|
},
|
||||||
"style_tooltip": "Change the style of the trace"
|
"style_tooltip": "Change the style of the trace"
|
||||||
},
|
},
|
||||||
"layers": {
|
"layers": {
|
||||||
|
Reference in New Issue
Block a user