clean tool

This commit is contained in:
vcoppe
2024-06-11 16:33:06 +02:00
parent 14a81a530c
commit c4cc4b179b
6 changed files with 290 additions and 2 deletions

View File

@@ -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 {

View File

@@ -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" />

View File

@@ -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>

View 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>

View File

@@ -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;

View File

@@ -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": {