Files
gpx.studio/website/src/lib/components/toolbar/tools/scissors/split-controls.ts

179 lines
6.8 KiB
TypeScript
Raw Normal View History

2025-10-05 19:34:05 +02:00
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
2025-10-18 16:10:08 +02:00
import { currentTool, Tool } from '$lib/components/toolbar/tools';
2025-10-17 23:54:45 +02:00
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
import { Scissors } from 'lucide-static';
2025-10-17 23:54:45 +02:00
import { selection } from '$lib/logic/selection';
2025-10-18 16:10:08 +02:00
import { gpxStatistics } from '$lib/logic/statistics';
import { get } from 'svelte/store';
import { fileStateCollection } from '$lib/logic/file-state';
import { fileActions } from '$lib/logic/file-actions';
2025-11-16 16:46:31 +01:00
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
2026-01-30 21:30:37 +01:00
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
2024-07-25 16:15:44 +02:00
export class SplitControls {
map: mapboxgl.Map;
unsubscribes: Function[] = [];
2025-11-16 16:46:31 +01:00
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
2024-07-25 16:15:44 +02:00
constructor(map: mapboxgl.Map) {
this.map = map;
2025-11-16 16:46:31 +01:00
if (!this.map.hasImage('split-control')) {
let icon = new Image(100, 100);
icon.onload = () => {
if (!this.map.hasImage('split-control')) {
this.map.addImage('split-control', icon);
}
};
// Lucide icons are SVG files with a 24x24 viewBox
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
icon.src =
'data:image/svg+xml,' +
encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
<circle cx="20" cy="20" r="20" fill="white" />
<g transform="translate(8 8)">
${Scissors.replace('stroke="currentColor"', 'stroke="black"')}
</g>
</svg>
`);
}
2024-07-25 16:15:44 +02:00
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
2025-10-18 16:10:08 +02:00
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
2024-07-25 16:15:44 +02:00
}
addIfNeeded() {
2025-10-18 16:10:08 +02:00
let scissors = get(currentTool) === Tool.SCISSORS;
2024-07-25 16:15:44 +02:00
if (!scissors) {
2025-11-16 16:46:31 +01:00
this.remove();
2024-07-25 16:15:44 +02:00
return;
}
2025-11-16 16:46:31 +01:00
this.updateControls();
2024-07-25 16:15:44 +02:00
}
updateControls() {
2025-11-16 16:46:31 +01:00
let data: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
features: [],
};
2025-10-17 23:54:45 +02:00
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
2025-10-18 16:10:08 +02:00
let file = fileStateCollection.getFile(fileId);
2024-07-25 16:15:44 +02:00
if (file) {
file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (
2025-10-18 16:10:08 +02:00
get(selection).hasAnyParent(
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
)
) {
2025-11-16 16:46:31 +01:00
for (let i = 1; i < segment.trkpt.length - 1; i++) {
let point = segment.trkpt[i];
2024-07-25 16:15:44 +02:00
if (point._data.anchor) {
2025-11-16 16:46:31 +01:00
data.features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [point.getLongitude(), point.getLatitude()],
},
properties: {
fileId: fileId,
trackIndex: trackIndex,
segmentIndex: segmentIndex,
pointIndex: i,
minZoom: point._data.zoom,
},
});
2024-07-25 16:15:44 +02:00
}
}
}
});
}
}, false);
2025-11-16 16:46:31 +01:00
try {
let source = this.map.getSource('split-controls') as mapboxgl.GeoJSONSource | undefined;
if (source) {
source.setData(data);
} else {
this.map.addSource('split-controls', {
type: 'geojson',
data: data,
});
}
2024-07-25 16:15:44 +02:00
2025-11-16 16:46:31 +01:00
if (!this.map.getLayer('split-controls')) {
2026-01-30 21:30:37 +01:00
this.map.addLayer(
{
id: 'split-controls',
type: 'symbol',
source: 'split-controls',
layout: {
'icon-image': 'split-control',
'icon-size': 0.25,
'icon-padding': 0,
},
filter: ['<=', ['get', 'minZoom'], ['zoom']],
2025-11-16 16:46:31 +01:00
},
2026-01-30 21:30:37 +01:00
ANCHOR_LAYER_KEY.interactions
);
2024-07-25 16:15:44 +02:00
2025-11-16 16:46:31 +01:00
this.map.on('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
this.map.on('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
this.map.on('click', 'split-controls', this.layerOnClickBinded);
}
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
2024-07-25 16:15:44 +02:00
}
}
2025-11-16 16:46:31 +01:00
remove() {
this.map.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
this.map.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
this.map.off('click', 'split-controls', this.layerOnClickBinded);
2024-07-25 16:15:44 +02:00
2025-11-16 16:46:31 +01:00
try {
if (this.map.getLayer('split-controls')) {
this.map.removeLayer('split-controls');
}
2024-07-25 16:15:44 +02:00
2025-11-16 16:46:31 +01:00
if (this.map.getSource('split-controls')) {
this.map.removeSource('split-controls');
2024-07-25 16:15:44 +02:00
}
2025-11-16 16:46:31 +01:00
} catch (e) {
// No reliable way to check if the map is ready to remove sources and layers
}
2024-07-25 16:15:44 +02:00
}
2025-11-16 16:46:31 +01:00
layerOnMouseEnter(e: any) {
mapCursor.notify(MapCursorState.SPLIT_CONTROL, true);
}
2024-07-25 16:15:44 +02:00
2025-11-16 16:46:31 +01:00
layerOnMouseLeave() {
mapCursor.notify(MapCursorState.SPLIT_CONTROL, false);
}
layerOnClick(e: mapboxgl.MapMouseEvent) {
let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates;
fileActions.split(
get(splitAs),
e.features![0].properties!.fileId,
e.features![0].properties!.trackIndex,
e.features![0].properties!.segmentIndex,
{ lon: coordinates[0], lat: coordinates[1] },
e.features![0].properties!.pointIndex
);
2024-07-25 16:15:44 +02:00
}
destroy() {
this.remove();
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
}
}