import { TrackPoint, TrackSegment } from 'gpx'; import mapboxgl from 'mapbox-gl'; import { dbUtils, getFile } from '$lib/db'; import { ListTrackSegmentItem } from '$lib/components/file-list/file-list'; import { gpxStatistics } from '$lib/stores'; import { tool, Tool } from '$lib/components/toolbar/utils.svelte'; import { splitAs } from '$lib/components/toolbar/tools/scissors/utils.svelte'; import { Scissors } from 'lucide-static'; import { applyToOrderedSelectedItemsFromFile, selection } from '$lib/logic/selection.svelte'; 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(gpxStatistics.subscribe(this.addIfNeeded.bind(this))); $effect(() => { tool.current, selection.value, this.addIfNeeded.bind(this); }); } addIfNeeded() { let scissors = tool.current === 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 ( selection.value.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].fileId = fileId; 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"', '') .replace('stroke="currentColor"', 'stroke="black"'); 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(); dbUtils.split( splitAs.current, control.fileId, control.trackIndex, control.segmentIndex, control.point.getCoordinates(), control.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; };