mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-05 18:02:55 +00:00
update scissors tool, closes #20
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
<script lang="ts" context="module">
|
||||
export enum SplitType {
|
||||
FILES = 'files',
|
||||
TRACKS = 'tracks',
|
||||
SEGMENTS = 'segments'
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Help from '$lib/components/Help.svelte';
|
||||
import { ListRootItem } from '$lib/components/file-list/FileList';
|
||||
import { selection } from '$lib/components/file-list/Selection';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Slider } from '$lib/components/ui/slider';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { gpxStatistics, map, slicedGPXStatistics, splitAs } from '$lib/stores';
|
||||
import { get } from 'svelte/store';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { onDestroy, tick } from 'svelte';
|
||||
import { Crop } from 'lucide-svelte';
|
||||
import { dbUtils } from '$lib/db';
|
||||
import { SplitControls } from './SplitControls';
|
||||
|
||||
let splitControls: SplitControls | undefined = undefined;
|
||||
|
||||
$: if ($map) {
|
||||
if (splitControls) {
|
||||
splitControls.destroy();
|
||||
}
|
||||
splitControls = new SplitControls($map);
|
||||
}
|
||||
|
||||
$: validSelection =
|
||||
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
||||
$gpxStatistics.local.points.length > 0;
|
||||
|
||||
let maxSliderValue = 100;
|
||||
let sliderValues = [0, 100];
|
||||
|
||||
$: canCrop = sliderValues[0] != 0 || sliderValues[1] != maxSliderValue;
|
||||
|
||||
function updateSlicedGPXStatistics() {
|
||||
if (validSelection && canCrop) {
|
||||
$slicedGPXStatistics = [
|
||||
get(gpxStatistics).slice(sliderValues[0], sliderValues[1]),
|
||||
sliderValues[0],
|
||||
sliderValues[1]
|
||||
];
|
||||
} else {
|
||||
$slicedGPXStatistics = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSliderValues() {
|
||||
if ($slicedGPXStatistics !== undefined) {
|
||||
sliderValues = [$slicedGPXStatistics[1], $slicedGPXStatistics[2]];
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSliderLimits() {
|
||||
if (validSelection && $gpxStatistics.local.points.length > 0) {
|
||||
maxSliderValue = $gpxStatistics.local.points.length - 1;
|
||||
} else {
|
||||
maxSliderValue = 100;
|
||||
}
|
||||
await tick();
|
||||
sliderValues = [0, maxSliderValue];
|
||||
}
|
||||
|
||||
$: if ($gpxStatistics.local.points.length - 1 != maxSliderValue) {
|
||||
updateSliderLimits();
|
||||
}
|
||||
|
||||
$: if (sliderValues) {
|
||||
updateSlicedGPXStatistics();
|
||||
}
|
||||
|
||||
$: if (
|
||||
$slicedGPXStatistics !== undefined &&
|
||||
($slicedGPXStatistics[1] !== sliderValues[0] || $slicedGPXStatistics[2] !== sliderValues[1])
|
||||
) {
|
||||
updateSliderValues();
|
||||
}
|
||||
|
||||
const splitTypes = [
|
||||
{ value: SplitType.FILES, label: $_('gpx.files') },
|
||||
{ value: SplitType.TRACKS, label: $_('gpx.tracks') },
|
||||
{ value: SplitType.SEGMENTS, label: $_('gpx.segments') }
|
||||
];
|
||||
|
||||
let splitType = splitTypes[0];
|
||||
|
||||
$: splitAs.set(splitType.value);
|
||||
|
||||
onDestroy(() => {
|
||||
$slicedGPXStatistics = undefined;
|
||||
if (splitControls) {
|
||||
splitControls.destroy();
|
||||
splitControls = undefined;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
||||
<div class="p-2">
|
||||
<Slider bind:value={sliderValues} max={maxSliderValue} step={1} disabled={!validSelection} />
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={!validSelection || !canCrop}
|
||||
on:click={() => dbUtils.cropSelection(sliderValues[0], sliderValues[1])}
|
||||
>
|
||||
<Crop size="16" class="mr-1" />{$_('toolbar.scissors.crop')}
|
||||
</Button>
|
||||
<Separator />
|
||||
<Label class="flex flex-row flex-wrap gap-3 items-center">
|
||||
<span class="shrink-0">
|
||||
{$_('toolbar.scissors.split_as')}
|
||||
</span>
|
||||
<Select.Root bind:selected={splitType}>
|
||||
<Select.Trigger class="h-8 w-fit grow">
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each splitTypes as { value, label }}
|
||||
<Select.Item {value}>{label}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Label>
|
||||
<Help>
|
||||
{#if validSelection}
|
||||
{$_('toolbar.scissors.help')}
|
||||
{:else}
|
||||
{$_('toolbar.scissors.help_invalid_selection')}
|
||||
{/if}
|
||||
</Help>
|
||||
</div>
|
@@ -0,0 +1,165 @@
|
||||
import { TrackPoint, TrackSegment } from "gpx";
|
||||
import { get } from "svelte/store";
|
||||
import mapboxgl from "mapbox-gl";
|
||||
import { dbUtils, getFile } from "$lib/db";
|
||||
import { applyToOrderedSelectedItemsFromFile, selection } from "$lib/components/file-list/Selection";
|
||||
import { ListTrackSegmentItem } from "$lib/components/file-list/FileList";
|
||||
import { currentTool, gpxStatistics, Tool } from "$lib/stores";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { Scissors } from "lucide-static";
|
||||
|
||||
export class SplitControls {
|
||||
active: boolean = false;
|
||||
map: mapboxgl.Map;
|
||||
controls: ControlWithMarker[] = [];
|
||||
shownControls: ControlWithMarker[] = [];
|
||||
unsubscribes: Function[] = [];
|
||||
|
||||
toggleControlsForZoomLevelAndBoundsBinded: () => void = this.toggleControlsForZoomLevelAndBounds.bind(this);
|
||||
|
||||
constructor(map: mapboxgl.Map) {
|
||||
this.map = map;
|
||||
|
||||
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
|
||||
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
|
||||
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
|
||||
}
|
||||
|
||||
addIfNeeded() {
|
||||
let scissors = get(currentTool) === Tool.SCISSORS;
|
||||
if (!scissors) {
|
||||
if (this.active) {
|
||||
this.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.active) {
|
||||
this.updateControls();
|
||||
} else {
|
||||
this.add();
|
||||
}
|
||||
}
|
||||
|
||||
add() {
|
||||
this.active = true;
|
||||
|
||||
this.map.on('zoom', this.toggleControlsForZoomLevelAndBoundsBinded);
|
||||
this.map.on('move', this.toggleControlsForZoomLevelAndBoundsBinded);
|
||||
}
|
||||
|
||||
updateControls() { // Update the markers when the files change
|
||||
|
||||
let controlIndex = 0;
|
||||
|
||||
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||
let file = getFile(fileId);
|
||||
|
||||
if (file) {
|
||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||
if (get(selection).hasAnyParent(new ListTrackSegmentItem(fileId, trackIndex, segmentIndex))) {
|
||||
for (let point of segment.trkpt.slice(1, -1)) { // Update the existing controls (could be improved by matching the existing controls with the new ones?)
|
||||
if (point._data.anchor) {
|
||||
if (controlIndex < this.controls.length) {
|
||||
this.controls[controlIndex].point = point;
|
||||
this.controls[controlIndex].segment = segment;
|
||||
this.controls[controlIndex].trackIndex = trackIndex;
|
||||
this.controls[controlIndex].segmentIndex = segmentIndex;
|
||||
this.controls[controlIndex].marker.setLngLat(point.getCoordinates());
|
||||
} else {
|
||||
this.controls.push(this.createControl(point, segment, fileId, trackIndex, segmentIndex));
|
||||
}
|
||||
controlIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}, false);
|
||||
|
||||
while (controlIndex < this.controls.length) { // Remove the extra controls
|
||||
this.controls.pop()?.marker.remove();
|
||||
}
|
||||
|
||||
this.toggleControlsForZoomLevelAndBounds();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.active = false;
|
||||
|
||||
for (let control of this.controls) {
|
||||
control.marker.remove();
|
||||
}
|
||||
this.map.off('zoom', this.toggleControlsForZoomLevelAndBoundsBinded);
|
||||
this.map.off('move', this.toggleControlsForZoomLevelAndBoundsBinded);
|
||||
}
|
||||
|
||||
toggleControlsForZoomLevelAndBounds() { // Show markers only if they are in the current zoom level and bounds
|
||||
this.shownControls.splice(0, this.shownControls.length);
|
||||
|
||||
let southWest = this.map.unproject([0, this.map.getCanvas().height]);
|
||||
let northEast = this.map.unproject([this.map.getCanvas().width, 0]);
|
||||
let bounds = new mapboxgl.LngLatBounds(southWest, northEast);
|
||||
|
||||
let zoom = this.map.getZoom();
|
||||
this.controls.forEach((control) => {
|
||||
control.inZoom = control.point._data.zoom <= zoom;
|
||||
if (control.inZoom && bounds.contains(control.marker.getLngLat())) {
|
||||
control.marker.addTo(this.map);
|
||||
this.shownControls.push(control);
|
||||
} else {
|
||||
control.marker.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createControl(point: TrackPoint, segment: TrackSegment, fileId: string, trackIndex: number, segmentIndex: number): ControlWithMarker {
|
||||
let element = document.createElement('div');
|
||||
element.className = `h-6 w-6 p-0.5 rounded-full bg-white border-2 border-black cursor-pointer`;
|
||||
element.innerHTML = Scissors.replace('width="24"', "").replace('height="24"', "");
|
||||
console.log(element.innerHTML);
|
||||
|
||||
let marker = new mapboxgl.Marker({
|
||||
draggable: true,
|
||||
className: 'z-10',
|
||||
element
|
||||
}).setLngLat(point.getCoordinates());
|
||||
|
||||
let control = {
|
||||
point,
|
||||
segment,
|
||||
fileId,
|
||||
trackIndex,
|
||||
segmentIndex,
|
||||
marker,
|
||||
inZoom: false
|
||||
};
|
||||
|
||||
marker.getElement().addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
console.log('click', fileId, trackIndex, segmentIndex, point.getCoordinates(), point._data.index);
|
||||
dbUtils.split(fileId, trackIndex, segmentIndex, point.getCoordinates(), point._data.index);
|
||||
});
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.remove();
|
||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
}
|
||||
}
|
||||
|
||||
type Control = {
|
||||
segment: TrackSegment;
|
||||
fileId: string;
|
||||
trackIndex: number;
|
||||
segmentIndex: number;
|
||||
point: TrackPoint;
|
||||
};
|
||||
|
||||
type ControlWithMarker = Control & {
|
||||
marker: mapboxgl.Marker;
|
||||
inZoom: boolean;
|
||||
};
|
Reference in New Issue
Block a user