split routing controls in zoom-specific layers to improve performance

This commit is contained in:
vcoppe
2026-02-14 15:05:23 +01:00
parent 88abd72a41
commit d6c9fb1025
3 changed files with 111 additions and 66 deletions

View File

@@ -5,7 +5,13 @@
map.onLoad((map_) => {
map_.on('contextmenu', (e) => {
if (map_.queryRenderedFeatures(e.point, { layers: ['routing-controls'] }).length) {
if (
map_.queryRenderedFeatures(e.point, {
layers: map_
.getLayersOrder()
.filter((layerId) => layerId.startsWith('routing-controls')),
}).length
) {
// Clicked on routing control, ignoring
return;
}

View File

@@ -24,6 +24,7 @@ import { fileActionManager } from '$lib/logic/file-action-manager';
import { i18n } from '$lib/i18n.svelte';
import { map } from '$lib/components/map/map';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
import { MAX_ANCHOR_ZOOM, MIN_ANCHOR_ZOOM } from './simplify';
const { streetViewSource } = settings;
export const canChangeStart = writable(false);
@@ -41,6 +42,13 @@ export class RoutingControls {
active: boolean = false;
fileId: string = '';
file: Readable<GPXFileWithStatistics | undefined>;
layers: Map<
number,
{
id: string;
anchors: GeoJSON.Feature<GeoJSON.Point, AnchorProperties>[];
}
> = new Map();
anchors: GeoJSON.Feature<GeoJSON.Point, AnchorProperties>[] = [];
popup: maplibregl.Popup;
popupElement: HTMLElement;
@@ -74,6 +82,12 @@ export class RoutingControls {
) {
this.fileId = fileId;
this.file = file;
for (let zoom = MIN_ANCHOR_ZOOM; zoom <= MAX_ANCHOR_ZOOM; zoom++) {
this.layers.set(zoom, {
id: `routing-controls-${zoom}`,
anchors: [],
});
}
this.popup = popup;
this.popupElement = popupElement;
@@ -129,6 +143,7 @@ export class RoutingControls {
return;
}
this.layers.forEach((layer) => (layer.anchors = []));
this.anchors = [];
file.forEachSegment((segment, trackIndex, segmentIndex) => {
@@ -140,7 +155,7 @@ export class RoutingControls {
for (let i = 0; i < segment.trkpt.length; i++) {
const point = segment.trkpt[i];
if (point._data.anchor) {
this.anchors.push({
const anchor: Anchor = {
type: 'Feature',
geometry: {
type: 'Point',
@@ -153,58 +168,62 @@ export class RoutingControls {
anchorIndex: this.anchors.length,
minZoom: point._data.zoom,
},
});
};
this.layers.get(point._data.zoom)?.anchors.push(anchor);
this.anchors.push(anchor);
}
}
}
});
this.layers.forEach((layer, zoom) => {
try {
let source = map_.getSource('routing-controls') as maplibregl.GeoJSONSource | undefined;
let source = map_.getSource(layer.id) as maplibregl.GeoJSONSource | undefined;
if (source) {
source.setData({
type: 'FeatureCollection',
features: this.anchors,
features: layer.anchors,
});
} else {
map_.addSource('routing-controls', {
map_.addSource(layer.id, {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: this.anchors,
features: layer.anchors,
},
promoteId: 'anchorIndex',
});
}
if (!map_.getLayer('routing-controls')) {
if (!map_.getLayer(layer.id)) {
map_.addLayer(
{
id: 'routing-controls',
id: layer.id,
type: 'symbol',
source: 'routing-controls',
source: layer.id,
layout: {
'icon-image': 'routing-control',
'icon-size': 0.25,
'icon-padding': 0,
'icon-allow-overlap': true,
},
filter: ['<=', ['get', 'minZoom'], ['zoom']],
minzoom: zoom,
},
ANCHOR_LAYER_KEY.routingControls
);
layerEventManager.on('mouseenter', 'routing-controls', this.onMouseEnterBinded);
layerEventManager.on('mouseleave', 'routing-controls', this.onMouseLeaveBinded);
layerEventManager.on('click', 'routing-controls', this.onClickBinded);
layerEventManager.on('contextmenu', 'routing-controls', this.onClickBinded);
layerEventManager.on('mousedown', 'routing-controls', this.onMouseDownBinded);
layerEventManager.on('touchstart', 'routing-controls', this.onTouchStartBinded);
layerEventManager.on('mouseenter', layer.id, this.onMouseEnterBinded);
layerEventManager.on('mouseleave', layer.id, this.onMouseLeaveBinded);
layerEventManager.on('click', layer.id, this.onClickBinded);
layerEventManager.on('contextmenu', layer.id, this.onClickBinded);
layerEventManager.on('mousedown', layer.id, this.onMouseDownBinded);
layerEventManager.on('touchstart', layer.id, this.onTouchStartBinded);
}
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
return;
}
});
}
remove() {
@@ -217,24 +236,26 @@ export class RoutingControls {
layerEventManager?.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
map_?.off('mousemove', this.updateTemporaryAnchorBinded);
this.layers.forEach((layer) => {
try {
layerEventManager?.off('mouseenter', 'routing-controls', this.onMouseEnterBinded);
layerEventManager?.off('mouseleave', 'routing-controls', this.onMouseLeaveBinded);
layerEventManager?.off('click', 'routing-controls', this.onClickBinded);
layerEventManager?.off('contextmenu', 'routing-controls', this.onClickBinded);
layerEventManager?.off('mousedown', 'routing-controls', this.onMouseDownBinded);
layerEventManager?.off('touchstart', 'routing-controls', this.onTouchStartBinded);
layerEventManager?.off('mouseenter', layer.id, this.onMouseEnterBinded);
layerEventManager?.off('mouseleave', layer.id, this.onMouseLeaveBinded);
layerEventManager?.off('click', layer.id, this.onClickBinded);
layerEventManager?.off('contextmenu', layer.id, this.onClickBinded);
layerEventManager?.off('mousedown', layer.id, this.onMouseDownBinded);
layerEventManager?.off('touchstart', layer.id, this.onTouchStartBinded);
if (map_?.getLayer('routing-controls')) {
map_?.removeLayer('routing-controls');
if (map_?.getLayer(layer.id)) {
map_?.removeLayer(layer.id);
}
if (map_?.getSource('routing-controls')) {
map_?.removeSource('routing-controls');
if (map_?.getSource(layer.id)) {
map_?.removeSource(layer.id);
}
} catch (e) {
// No reliable way to check if the map is ready to remove sources and layers
}
});
this.popup.remove();
@@ -497,7 +518,14 @@ export class RoutingControls {
if (get(streetViewEnabled) && get(streetViewSource) === 'google') {
return;
}
if (e.target.queryRenderedFeatures(e.point, { layers: ['routing-controls'] }).length) {
if (
e.target.queryRenderedFeatures(e.point, {
layers: this.layers
.values()
.map((layer) => layer.id)
.toArray(),
}).length
) {
// Clicked on routing control, ignoring
return;
}
@@ -578,6 +606,7 @@ export class RoutingControls {
for (let i = 0; i < this.anchors.length; i++) {
if (
this.anchors[i].properties.trackIndex === anchor.properties.trackIndex &&
this.anchors[i].properties.segmentIndex === anchor.properties.segmentIndex &&
zoom >= this.anchors[i].properties.minZoom
) {
@@ -1030,7 +1059,11 @@ export class RoutingControls {
}
moveAnchorFeature(anchorIndex: number, coordinates: Coordinates) {
let source = get(map)?.getSource('routing-controls') as GeoJSONSource | undefined;
const anchor =
anchorIndex === this.anchors.length ? this.temporaryAnchor : this.anchors[anchorIndex];
let source = get(map)?.getSource(
this.layers.get(anchor?.properties.minZoom ?? MIN_ANCHOR_ZOOM)?.id ?? ''
) as GeoJSONSource | undefined;
if (source) {
source.updateData({
update: [
@@ -1050,7 +1083,7 @@ export class RoutingControls {
if (!this.temporaryAnchor) {
return;
}
let source = get(map)?.getSource('routing-controls') as GeoJSONSource | undefined;
let source = get(map)?.getSource('routing-controls-0') as GeoJSONSource | undefined;
if (source) {
if (this.temporaryAnchor) {
source.updateData({
@@ -1065,7 +1098,7 @@ export class RoutingControls {
return;
}
const map_ = get(map);
let source = map_?.getSource('routing-controls') as GeoJSONSource | undefined;
let source = map_?.getSource('routing-controls-0') as GeoJSONSource | undefined;
if (source) {
if (this.temporaryAnchor) {
source.updateData({

View File

@@ -2,15 +2,21 @@ import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from 'gpx';
const earthRadius = 6371008.8;
export const MIN_ANCHOR_ZOOM = 0;
export const MAX_ANCHOR_ZOOM = 22;
export function getZoomLevelForDistance(latitude: number, distance?: number): number {
if (distance === undefined) {
return 0;
return MIN_ANCHOR_ZOOM;
}
const rad = Math.PI / 180;
const lat = latitude * rad;
return Math.min(22, Math.max(0, Math.log2((earthRadius * Math.cos(lat)) / distance)));
return Math.min(
MAX_ANCHOR_ZOOM,
Math.max(MIN_ANCHOR_ZOOM, Math.round(Math.log2((earthRadius * Math.cos(lat)) / distance)))
);
}
export function updateAnchorPoints(file: GPXFile) {