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].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(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; };