From 5ab1b77227d7ca12a91349ec701d4b679ff6e3ea Mon Sep 17 00:00:00 2001 From: vcoppe Date: Thu, 25 Jul 2024 16:15:44 +0200 Subject: [PATCH] update scissors tool, closes #20 --- .../src/lib/components/gpx-layer/GPXLayer.ts | 4 +- .../components/toolbar/ToolbarItemMenu.svelte | 2 +- .../tools/{ => scissors}/Scissors.svelte | 16 +- .../toolbar/tools/scissors/SplitControls.ts | 165 ++++++++++++++++++ website/src/lib/db.ts | 22 ++- website/src/lib/docs/en/toolbar/scissors.mdx | 5 +- website/src/lib/stores.ts | 2 +- website/src/lib/utils.ts | 7 +- website/src/locales/en.json | 2 +- 9 files changed, 207 insertions(+), 18 deletions(-) rename website/src/lib/components/toolbar/tools/{ => scissors}/Scissors.svelte (89%) create mode 100644 website/src/lib/components/toolbar/tools/scissors/SplitControls.ts diff --git a/website/src/lib/components/gpx-layer/GPXLayer.ts b/website/src/lib/components/gpx-layer/GPXLayer.ts index 25781f5e..e634e5f0 100644 --- a/website/src/lib/components/gpx-layer/GPXLayer.ts +++ b/website/src/lib/components/gpx-layer/GPXLayer.ts @@ -6,7 +6,7 @@ import { currentPopupWaypoint, deleteWaypoint, waypointPopup } from "./WaypointP import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection"; import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList"; import type { Waypoint } from "gpx"; -import { getElevation, resetCursor, setCursor, setGrabbingCursor, setPointerCursor } from "$lib/utils"; +import { getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils"; import { font } from "$lib/assets/layers"; import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte"; import { MapPin } from "lucide-static"; @@ -320,7 +320,7 @@ export class GPXLayer { let segmentIndex = e.features[0].properties.segmentIndex; if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) { - setCursor(`url('data:image/svg+xml,') 12 12, auto`); + setScissorsCursor(); } else { setPointerCursor(); } diff --git a/website/src/lib/components/toolbar/ToolbarItemMenu.svelte b/website/src/lib/components/toolbar/ToolbarItemMenu.svelte index 2598f53c..312675cb 100644 --- a/website/src/lib/components/toolbar/ToolbarItemMenu.svelte +++ b/website/src/lib/components/toolbar/ToolbarItemMenu.svelte @@ -4,7 +4,7 @@ import { flyAndScale } from '$lib/utils'; import * as Card from '$lib/components/ui/card'; import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte'; - import Scissors from '$lib/components/toolbar/tools/Scissors.svelte'; + import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte'; 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'; diff --git a/website/src/lib/components/toolbar/tools/Scissors.svelte b/website/src/lib/components/toolbar/tools/scissors/Scissors.svelte similarity index 89% rename from website/src/lib/components/toolbar/tools/Scissors.svelte rename to website/src/lib/components/toolbar/tools/scissors/Scissors.svelte index 4fa02e14..bf3ce809 100644 --- a/website/src/lib/components/toolbar/tools/Scissors.svelte +++ b/website/src/lib/components/toolbar/tools/scissors/Scissors.svelte @@ -15,12 +15,22 @@ import { Slider } from '$lib/components/ui/slider'; import * as Select from '$lib/components/ui/select'; import { Separator } from '$lib/components/ui/separator'; - import { gpxStatistics, slicedGPXStatistics, splitAs } from '$lib/stores'; + 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']) && @@ -86,6 +96,10 @@ onDestroy(() => { $slicedGPXStatistics = undefined; + if (splitControls) { + splitControls.destroy(); + splitControls = undefined; + } }); diff --git a/website/src/lib/components/toolbar/tools/scissors/SplitControls.ts b/website/src/lib/components/toolbar/tools/scissors/SplitControls.ts new file mode 100644 index 00000000..de1825d5 --- /dev/null +++ b/website/src/lib/components/toolbar/tools/scissors/SplitControls.ts @@ -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; +}; diff --git a/website/src/lib/db.ts b/website/src/lib/db.ts index c3163c86..dd1837fc 100644 --- a/website/src/lib/db.ts +++ b/website/src/lib/db.ts @@ -7,7 +7,7 @@ import { defaultBasemap, defaultBasemapTree, defaultOverlayTree, defaultOverlays import { applyToOrderedItemsFromFile, applyToOrderedSelectedItemsFromFile, selection } from '$lib/components/file-list/Selection'; import { ListFileItem, ListItem, ListTrackItem, ListLevel, ListTrackSegmentItem, ListWaypointItem, ListRootItem } from '$lib/components/file-list/FileList'; import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify'; -import { SplitType } from '$lib/components/toolbar/tools/Scissors.svelte'; +import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte'; import { getElevation } from '$lib/utils'; import { browser } from '$app/environment'; @@ -790,22 +790,26 @@ export const dbUtils = { }); }); }, - split(fileId: string, trackIndex: number, segmentIndex: number, coordinates: Coordinates) { + split(fileId: string, trackIndex: number, segmentIndex: number, coordinates: Coordinates, trkptIndex?: number) { let splitType = get(splitAs); return applyGlobal((draft) => { let file = getFile(fileId); if (file) { let segment = file.trk[trackIndex].trkseg[segmentIndex]; - // Find the point closest to split - let minDistance = Number.MAX_VALUE; let minIndex = 0; - for (let i = 0; i < segment.trkpt.length; i++) { - let dist = distance(segment.trkpt[i].getCoordinates(), coordinates); - if (dist < minDistance) { - minDistance = dist; - minIndex = i; + if (trkptIndex === undefined) { + // Find the point closest to split + let minDistance = Number.MAX_VALUE; + for (let i = 0; i < segment.trkpt.length; i++) { + let dist = distance(segment.trkpt[i].getCoordinates(), coordinates); + if (dist < minDistance) { + minDistance = dist; + minIndex = i; + } } + } else { + minIndex = trkptIndex; } let absoluteIndex = minIndex; diff --git a/website/src/lib/docs/en/toolbar/scissors.mdx b/website/src/lib/docs/en/toolbar/scissors.mdx index a43a335a..2b5905b0 100644 --- a/website/src/lib/docs/en/toolbar/scissors.mdx +++ b/website/src/lib/docs/en/toolbar/scissors.mdx @@ -24,8 +24,9 @@ Validate the selection when you are satisfied with the result. ## Split -To split the selected trace into two parts, hover over the trace on the map. -Scissors will appear at the cursor position, indicating that you can split the trace at this point. +To split the selected trace into two parts, click on one of the split markers displayed along the trace. +To split at a specific point of your choice, hover over the trace on the map. +Scissors will appear at the cursor position, showing that you can split the trace at that point. You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks or segments](../gpx). diff --git a/website/src/lib/stores.ts b/website/src/lib/stores.ts index bb8f3986..eb0f7235 100644 --- a/website/src/lib/stores.ts +++ b/website/src/lib/stores.ts @@ -9,7 +9,7 @@ import { dbUtils, fileObservers, getFile, getStatistics, settings } from './db'; import { addSelectItem, applyToOrderedSelectedItemsFromFile, selectFile, selectItem, selection } from '$lib/components/file-list/Selection'; import { ListFileItem, ListItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem } from '$lib/components/file-list/FileList'; import type { RoutingControls } from '$lib/components/toolbar/tools/routing/RoutingControls'; -import { SplitType } from '$lib/components/toolbar/tools/Scissors.svelte'; +import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte'; const { fileOrder } = settings; diff --git a/website/src/lib/utils.ts b/website/src/lib/utils.ts index 348c6abe..1d789efd 100644 --- a/website/src/lib/utils.ts +++ b/website/src/lib/utils.ts @@ -8,7 +8,6 @@ import { base } from "$app/paths"; import { browser } from "$app/environment"; import { languages } from "$lib/languages"; import { locale } from "svelte-i18n"; -import type Coordinates from "gpx"; import type mapboxgl from "mapbox-gl"; export function cn(...inputs: ClassValue[]) { @@ -102,6 +101,12 @@ export function setCrosshairCursor() { setCursor('crosshair'); } +export const scissorsCursor = `url('data:image/svg+xml,') 12 12, auto`; + +export function setScissorsCursor() { + setCursor(scissorsCursor); +} + export function getURLForLanguage(lang: string | null | undefined, path?: string): string { let newPath = path ?? (browser ? window.location.pathname.replace(base, '') : ''); let languageInPath = newPath.split('/')[1]; diff --git a/website/src/locales/en.json b/website/src/locales/en.json index ee6e9a88..b7d5ceb3 100644 --- a/website/src/locales/en.json +++ b/website/src/locales/en.json @@ -150,7 +150,7 @@ "crop": "Crop", "split_as": "Split the trace into", "help_invalid_selection": "Select a trace to crop or split.", - "help": "Use the slider to crop the trace, or click on the map to split it at the selected point." + "help": "Use the slider to crop the trace, or split it by clicking on one of the split markers or on the trace itself." }, "time": { "tooltip": "Manage time data",