speed up split controls

This commit is contained in:
vcoppe
2025-11-16 16:46:31 +01:00
parent 09b8aa65fc
commit 6387580626
3 changed files with 119 additions and 133 deletions

View File

@@ -449,7 +449,7 @@ export class GPXLayer {
} }
} }
layerOnClick(e: any) { layerOnClick(e: mapboxgl.MapMouseEvent) {
if ( if (
get(currentTool) === Tool.ROUTING && get(currentTool) === Tool.ROUTING &&
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
@@ -457,8 +457,8 @@ export class GPXLayer {
return; return;
} }
let trackIndex = e.features[0].properties.trackIndex; let trackIndex = e.features![0].properties!.trackIndex;
let segmentIndex = e.features[0].properties.segmentIndex; let segmentIndex = e.features![0].properties!.segmentIndex;
if ( if (
get(currentTool) === Tool.SCISSORS && get(currentTool) === Tool.SCISSORS &&
@@ -466,6 +466,11 @@ export class GPXLayer {
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
) )
) { ) {
if (get(map)?.queryRenderedFeatures(e.point, { layers: ['split-controls'] }).length) {
// Clicked on split control, ignoring
return;
}
fileActions.split(get(splitAs), this.fileId, trackIndex, segmentIndex, { fileActions.split(get(splitAs), this.fileId, trackIndex, segmentIndex, {
lat: e.lngLat.lat, lat: e.lngLat.lat,
lon: e.lngLat.lng, lon: e.lngLat.lng,

View File

@@ -1,5 +1,3 @@
import { TrackPoint, TrackSegment } from 'gpx';
import mapboxgl from 'mapbox-gl';
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list'; import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
import { currentTool, Tool } from '$lib/components/toolbar/tools'; import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors'; import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
@@ -9,20 +7,41 @@ import { gpxStatistics } from '$lib/logic/statistics';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { fileStateCollection } from '$lib/logic/file-state'; import { fileStateCollection } from '$lib/logic/file-state';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
export class SplitControls { export class SplitControls {
active: boolean = false;
map: mapboxgl.Map; map: mapboxgl.Map;
controls: ControlWithMarker[] = [];
shownControls: ControlWithMarker[] = [];
unsubscribes: Function[] = []; unsubscribes: Function[] = [];
toggleControlsForZoomLevelAndBoundsBinded: () => void = layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
this.toggleControlsForZoomLevelAndBounds.bind(this); layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
constructor(map: mapboxgl.Map) { constructor(map: mapboxgl.Map) {
this.map = map; this.map = map;
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>
`);
}
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this))); this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this))); this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this))); this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
@@ -31,29 +50,18 @@ export class SplitControls {
addIfNeeded() { addIfNeeded() {
let scissors = get(currentTool) === Tool.SCISSORS; let scissors = get(currentTool) === Tool.SCISSORS;
if (!scissors) { if (!scissors) {
if (this.active) { this.remove();
this.remove();
}
return; return;
} }
if (this.active) { this.updateControls();
this.updateControls();
} else {
this.add();
}
}
add() {
this.active = true;
this.map.on('zoom', this.toggleControlsForZoomLevelAndBoundsBinded);
this.map.on('move', this.toggleControlsForZoomLevelAndBoundsBinded);
} }
updateControls() { updateControls() {
// Update the markers when the files change let data: GeoJSON.FeatureCollection = {
let controlIndex = 0; type: 'FeatureCollection',
features: [],
};
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let file = fileStateCollection.getFile(fileId); let file = fileStateCollection.getFile(fileId);
@@ -64,30 +72,23 @@ export class SplitControls {
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex) new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
) )
) { ) {
for (let point of segment.trkpt.slice(1, -1)) { for (let i = 1; i < segment.trkpt.length - 1; i++) {
// Update the existing controls (could be improved by matching the existing controls with the new ones?) let point = segment.trkpt[i];
if (point._data.anchor) { if (point._data.anchor) {
if (controlIndex < this.controls.length) { data.features.push({
this.controls[controlIndex].fileId = fileId; type: 'Feature',
this.controls[controlIndex].point = point; geometry: {
this.controls[controlIndex].segment = segment; type: 'Point',
this.controls[controlIndex].trackIndex = trackIndex; coordinates: [point.getLongitude(), point.getLatitude()],
this.controls[controlIndex].segmentIndex = segmentIndex; },
this.controls[controlIndex].marker.setLngLat( properties: {
point.getCoordinates() fileId: fileId,
); trackIndex: trackIndex,
} else { segmentIndex: segmentIndex,
this.controls.push( pointIndex: i,
this.createControl( minZoom: point._data.zoom,
point, },
segment, });
fileId,
trackIndex,
segmentIndex
)
);
}
controlIndex++;
} }
} }
} }
@@ -95,86 +96,77 @@ export class SplitControls {
} }
}, false); }, false);
while (controlIndex < this.controls.length) { try {
// Remove the extra controls let source = this.map.getSource('split-controls') as mapboxgl.GeoJSONSource | undefined;
this.controls.pop()?.marker.remove(); if (source) {
} source.setData(data);
} else {
this.map.addSource('split-controls', {
type: 'geojson',
data: data,
});
}
this.toggleControlsForZoomLevelAndBounds(); if (!this.map.getLayer('split-controls')) {
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']],
});
this.map.on('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
this.map.on('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
this.map.on('click', 'split-controls', this.layerOnClickBinded);
}
this.map.moveLayer('split-controls');
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
}
} }
remove() { remove() {
this.active = false; this.map.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
this.map.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
this.map.off('click', 'split-controls', this.layerOnClickBinded);
for (let control of this.controls) { try {
control.marker.remove(); if (this.map.getLayer('split-controls')) {
} this.map.removeLayer('split-controls');
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();
} }
});
if (this.map.getSource('split-controls')) {
this.map.removeSource('split-controls');
}
} catch (e) {
// No reliable way to check if the map is ready to remove sources and layers
}
} }
createControl( layerOnMouseEnter(e: any) {
point: TrackPoint, mapCursor.notify(MapCursorState.SPLIT_CONTROL, true);
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({ layerOnMouseLeave() {
draggable: true, mapCursor.notify(MapCursorState.SPLIT_CONTROL, false);
className: 'z-10', }
element,
}).setLngLat(point.getCoordinates());
let control = { layerOnClick(e: mapboxgl.MapMouseEvent) {
point, let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates;
segment, fileActions.split(
fileId, get(splitAs),
trackIndex, e.features![0].properties!.fileId,
segmentIndex, e.features![0].properties!.trackIndex,
marker, e.features![0].properties!.segmentIndex,
inZoom: false, { lon: coordinates[0], lat: coordinates[1] },
}; e.features![0].properties!.pointIndex
);
marker.getElement().addEventListener('click', (e) => {
e.stopPropagation();
fileActions.split(
get(splitAs),
control.fileId,
control.trackIndex,
control.segmentIndex,
control.point.getCoordinates(),
control.point._data.index
);
});
return control;
} }
destroy() { destroy() {
@@ -182,16 +174,3 @@ export class SplitControls {
this.unsubscribes.forEach((unsubscribe) => unsubscribe()); 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;
};

View File

@@ -8,6 +8,7 @@ export enum MapCursorState {
TRACKPOINT_DRAGGING, TRACKPOINT_DRAGGING,
TOOL_WITH_CROSSHAIR, TOOL_WITH_CROSSHAIR,
SCISSORS, SCISSORS,
SPLIT_CONTROL,
MAPILLARY_HOVER, MAPILLARY_HOVER,
STREET_VIEW_CROSSHAIR, STREET_VIEW_CROSSHAIR,
} }
@@ -20,6 +21,7 @@ const cursorStyles = {
[MapCursorState.TRACKPOINT_DRAGGING]: 'grabbing', [MapCursorState.TRACKPOINT_DRAGGING]: 'grabbing',
[MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair', [MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair',
[MapCursorState.SCISSORS]: scissorsCursor, [MapCursorState.SCISSORS]: scissorsCursor,
[MapCursorState.SPLIT_CONTROL]: 'pointer',
[MapCursorState.MAPILLARY_HOVER]: 'pointer', [MapCursorState.MAPILLARY_HOVER]: 'pointer',
[MapCursorState.STREET_VIEW_CROSSHAIR]: 'crosshair', [MapCursorState.STREET_VIEW_CROSSHAIR]: 'crosshair',
}; };