mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-12-02 10:02:12 +00:00
speed up split controls
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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() {
|
if (this.map.getSource('split-controls')) {
|
||||||
// Show markers only if they are in the current zoom level and bounds
|
this.map.removeSource('split-controls');
|
||||||
this.shownControls.splice(0, this.shownControls.length);
|
}
|
||||||
|
} catch (e) {
|
||||||
let southWest = this.map.unproject([0, this.map.getCanvas().height]);
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
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(
|
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,
|
|
||||||
fileId,
|
|
||||||
trackIndex,
|
|
||||||
segmentIndex,
|
|
||||||
marker,
|
|
||||||
inZoom: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
marker.getElement().addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
fileActions.split(
|
fileActions.split(
|
||||||
get(splitAs),
|
get(splitAs),
|
||||||
control.fileId,
|
e.features![0].properties!.fileId,
|
||||||
control.trackIndex,
|
e.features![0].properties!.trackIndex,
|
||||||
control.segmentIndex,
|
e.features![0].properties!.segmentIndex,
|
||||||
control.point.getCoordinates(),
|
{ lon: coordinates[0], lat: coordinates[1] },
|
||||||
control.point._data.index
|
e.features![0].properties!.pointIndex
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user