mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-02-20 14:49:08 +00:00
split routing controls in zoom-specific layers to improve performance
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user