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.onLoad((map_) => {
map_.on('contextmenu', (e) => { 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 // Clicked on routing control, ignoring
return; return;
} }

View File

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

View File

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