mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-02-06 00:13:09 +00:00
centralized map layer event listener for better performance
This commit is contained in:
@@ -174,8 +174,9 @@ export class GPXLayer {
|
|||||||
|
|
||||||
update() {
|
update() {
|
||||||
const _map = get(map);
|
const _map = get(map);
|
||||||
|
const layerEventManager = map.layerEventManager;
|
||||||
let file = get(this.file)?.file;
|
let file = get(this.file)?.file;
|
||||||
if (!_map || !file) {
|
if (!_map || !layerEventManager || !file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,11 +221,11 @@ export class GPXLayer {
|
|||||||
ANCHOR_LAYER_KEY.tracks
|
ANCHOR_LAYER_KEY.tracks
|
||||||
);
|
);
|
||||||
|
|
||||||
_map.on('click', this.fileId, this.layerOnClickBinded);
|
layerEventManager.on('click', this.fileId, this.layerOnClickBinded);
|
||||||
_map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
layerEventManager.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
||||||
_map.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
layerEventManager.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
||||||
_map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
layerEventManager.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
_map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
layerEventManager.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
||||||
}
|
}
|
||||||
let waypointSource = _map.getSource(this.fileId + '-waypoints') as
|
let waypointSource = _map.getSource(this.fileId + '-waypoints') as
|
||||||
| GeoJSONSource
|
| GeoJSONSource
|
||||||
@@ -256,23 +257,27 @@ export class GPXLayer {
|
|||||||
ANCHOR_LAYER_KEY.waypoints
|
ANCHOR_LAYER_KEY.waypoints
|
||||||
);
|
);
|
||||||
|
|
||||||
_map.on(
|
layerEventManager.on(
|
||||||
'mouseenter',
|
'mouseenter',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnMouseEnterBinded
|
this.waypointLayerOnMouseEnterBinded
|
||||||
);
|
);
|
||||||
_map.on(
|
layerEventManager.on(
|
||||||
'mouseleave',
|
'mouseleave',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnMouseLeaveBinded
|
this.waypointLayerOnMouseLeaveBinded
|
||||||
);
|
);
|
||||||
_map.on('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
|
layerEventManager.on(
|
||||||
_map.on(
|
'click',
|
||||||
|
this.fileId + '-waypoints',
|
||||||
|
this.waypointLayerOnClickBinded
|
||||||
|
);
|
||||||
|
layerEventManager.on(
|
||||||
'mousedown',
|
'mousedown',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnMouseDownBinded
|
this.waypointLayerOnMouseDownBinded
|
||||||
);
|
);
|
||||||
_map.on(
|
layerEventManager.on(
|
||||||
'touchstart',
|
'touchstart',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnTouchStartBinded
|
this.waypointLayerOnTouchStartBinded
|
||||||
@@ -350,32 +355,47 @@ export class GPXLayer {
|
|||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
const _map = get(map);
|
const _map = get(map);
|
||||||
if (_map) {
|
|
||||||
_map.off('click', this.fileId, this.layerOnClickBinded);
|
|
||||||
_map.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
|
||||||
_map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
|
||||||
_map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
|
||||||
_map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
|
||||||
_map.off('style.load', this.updateBinded);
|
|
||||||
|
|
||||||
_map.off(
|
if (_map) {
|
||||||
|
_map.off('style.load', this.updateBinded);
|
||||||
|
}
|
||||||
|
|
||||||
|
const layerEventManager = map.layerEventManager;
|
||||||
|
if (layerEventManager) {
|
||||||
|
layerEventManager.off('click', this.fileId, this.layerOnClickBinded);
|
||||||
|
layerEventManager.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
||||||
|
layerEventManager.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
||||||
|
layerEventManager.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
|
layerEventManager.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
||||||
|
|
||||||
|
layerEventManager.off(
|
||||||
'mouseenter',
|
'mouseenter',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnMouseEnterBinded
|
this.waypointLayerOnMouseEnterBinded
|
||||||
);
|
);
|
||||||
_map.off(
|
layerEventManager.off(
|
||||||
'mouseleave',
|
'mouseleave',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnMouseLeaveBinded
|
this.waypointLayerOnMouseLeaveBinded
|
||||||
);
|
);
|
||||||
_map.off('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
|
layerEventManager.off(
|
||||||
_map.off('mousedown', this.fileId + '-waypoints', this.waypointLayerOnMouseDownBinded);
|
'click',
|
||||||
_map.off(
|
this.fileId + '-waypoints',
|
||||||
|
this.waypointLayerOnClickBinded
|
||||||
|
);
|
||||||
|
layerEventManager.off(
|
||||||
|
'mousedown',
|
||||||
|
this.fileId + '-waypoints',
|
||||||
|
this.waypointLayerOnMouseDownBinded
|
||||||
|
);
|
||||||
|
layerEventManager.off(
|
||||||
'touchstart',
|
'touchstart',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnTouchStartBinded
|
this.waypointLayerOnTouchStartBinded
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_map) {
|
||||||
if (_map.getLayer(this.fileId + '-direction')) {
|
if (_map.getLayer(this.fileId + '-direction')) {
|
||||||
_map.removeLayer(this.fileId + '-direction');
|
_map.removeLayer(this.fileId + '-direction');
|
||||||
}
|
}
|
||||||
@@ -581,6 +601,7 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
_map.dragPan.disable();
|
||||||
|
|
||||||
this.draggedWaypointIndex = e.features![0].properties!.waypointIndex;
|
this.draggedWaypointIndex = e.features![0].properties!.waypointIndex;
|
||||||
this.draggingStartingPosition = e.point;
|
this.draggingStartingPosition = e.point;
|
||||||
@@ -604,6 +625,7 @@ export class GPXLayer {
|
|||||||
waypointPopup?.hide();
|
waypointPopup?.hide();
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
_map.dragPan.disable();
|
||||||
|
|
||||||
_map.on('touchmove', this.waypointLayerOnMouseMoveBinded);
|
_map.on('touchmove', this.waypointLayerOnMouseMoveBinded);
|
||||||
_map.once('touchend', this.waypointLayerOnMouseUpBinded);
|
_map.once('touchend', this.waypointLayerOnMouseUpBinded);
|
||||||
@@ -631,8 +653,15 @@ export class GPXLayer {
|
|||||||
waypointLayerOnMouseUp(e: MapLayerMouseEvent | MapLayerTouchEvent) {
|
waypointLayerOnMouseUp(e: MapLayerMouseEvent | MapLayerTouchEvent) {
|
||||||
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
|
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
|
||||||
|
|
||||||
get(map)?.off('mousemove', this.waypointLayerOnMouseMoveBinded);
|
const _map = get(map);
|
||||||
get(map)?.off('touchmove', this.waypointLayerOnMouseMoveBinded);
|
if (!_map) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_map.dragPan.enable();
|
||||||
|
|
||||||
|
_map.off('mousemove', this.waypointLayerOnMouseMoveBinded);
|
||||||
|
_map.off('touchmove', this.waypointLayerOnMouseMoveBinded);
|
||||||
|
|
||||||
if (this.draggedWaypointIndex === null) {
|
if (this.draggedWaypointIndex === null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
if (overpassLayer) {
|
if (overpassLayer) {
|
||||||
overpassLayer.remove();
|
overpassLayer.remove();
|
||||||
}
|
}
|
||||||
overpassLayer = new OverpassLayer(_map);
|
overpassLayer = new OverpassLayer(_map, map.layerEventManager!);
|
||||||
overpassLayer.add();
|
overpassLayer.add();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { settings } from '$lib/logic/settings';
|
|||||||
import { db } from '$lib/db';
|
import { db } from '$lib/db';
|
||||||
import type { GeoJSONSource } from 'maplibre-gl';
|
import type { GeoJSONSource } from 'maplibre-gl';
|
||||||
import { ANCHOR_LAYER_KEY } from '../style';
|
import { ANCHOR_LAYER_KEY } from '../style';
|
||||||
|
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
||||||
|
|
||||||
const { currentOverpassQueries } = settings;
|
const { currentOverpassQueries } = settings;
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ export class OverpassLayer {
|
|||||||
queryZoom = 12;
|
queryZoom = 12;
|
||||||
expirationTime = 7 * 24 * 3600 * 1000;
|
expirationTime = 7 * 24 * 3600 * 1000;
|
||||||
map: maplibregl.Map;
|
map: maplibregl.Map;
|
||||||
|
layerEventManager: MapLayerEventManager;
|
||||||
popup: MapPopup;
|
popup: MapPopup;
|
||||||
|
|
||||||
currentQueries: Set<string> = new Set();
|
currentQueries: Set<string> = new Set();
|
||||||
@@ -37,8 +39,9 @@ export class OverpassLayer {
|
|||||||
updateBinded = this.update.bind(this);
|
updateBinded = this.update.bind(this);
|
||||||
onHoverBinded = this.onHover.bind(this);
|
onHoverBinded = this.onHover.bind(this);
|
||||||
|
|
||||||
constructor(map: maplibregl.Map) {
|
constructor(map: maplibregl.Map, layerEventManager: MapLayerEventManager) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
|
this.layerEventManager = layerEventManager;
|
||||||
this.popup = new MapPopup(map, {
|
this.popup = new MapPopup(map, {
|
||||||
closeButton: false,
|
closeButton: false,
|
||||||
focusAfterOpen: false,
|
focusAfterOpen: false,
|
||||||
@@ -102,8 +105,8 @@ export class OverpassLayer {
|
|||||||
ANCHOR_LAYER_KEY.overpass
|
ANCHOR_LAYER_KEY.overpass
|
||||||
);
|
);
|
||||||
|
|
||||||
this.map.on('mouseenter', 'overpass', this.onHoverBinded);
|
this.layerEventManager.on('mouseenter', 'overpass', this.onHoverBinded);
|
||||||
this.map.on('click', 'overpass', this.onHoverBinded);
|
this.layerEventManager.on('click', 'overpass', this.onHoverBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()], {
|
this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()], {
|
||||||
@@ -117,6 +120,8 @@ export class OverpassLayer {
|
|||||||
remove() {
|
remove() {
|
||||||
this.map.off('moveend', this.queryIfNeededBinded);
|
this.map.off('moveend', this.queryIfNeededBinded);
|
||||||
this.map.off('style.load', this.updateBinded);
|
this.map.off('style.load', this.updateBinded);
|
||||||
|
this.layerEventManager.off('mouseenter', 'overpass', this.onHoverBinded);
|
||||||
|
this.layerEventManager.off('click', 'overpass', this.onHoverBinded);
|
||||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
253
website/src/lib/components/map/map-layer-event-manager.ts
Normal file
253
website/src/lib/components/map/map-layer-event-manager.ts
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
import maplibregl from 'maplibre-gl';
|
||||||
|
|
||||||
|
type MapLayerMouseEventListener = (e: maplibregl.MapLayerMouseEvent) => void;
|
||||||
|
type MapLayerTouchEventListener = (e: maplibregl.MapLayerTouchEvent) => void;
|
||||||
|
type MapLayerListener = {
|
||||||
|
features: maplibregl.MapGeoJSONFeature[];
|
||||||
|
mousemoves: MapLayerMouseEventListener[];
|
||||||
|
mouseenters: MapLayerMouseEventListener[];
|
||||||
|
mouseleaves: MapLayerMouseEventListener[];
|
||||||
|
mousedowns: MapLayerMouseEventListener[];
|
||||||
|
clicks: MapLayerMouseEventListener[];
|
||||||
|
contextmenus: MapLayerMouseEventListener[];
|
||||||
|
touchstarts: MapLayerTouchEventListener[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MapLayerEventManager {
|
||||||
|
private _map: maplibregl.Map;
|
||||||
|
private _listeners: Record<string, MapLayerListener> = {};
|
||||||
|
|
||||||
|
constructor(map: maplibregl.Map) {
|
||||||
|
this._map = map;
|
||||||
|
this._map.on('mousemove', this._handleMouseMove.bind(this));
|
||||||
|
this._map.on('click', this._handleMouseClick.bind(this, 'click'));
|
||||||
|
this._map.on('contextmenu', this._handleMouseClick.bind(this, 'contextmenu'));
|
||||||
|
this._map.on('mousedown', this._handleMouseClick.bind(this, 'mousedown'));
|
||||||
|
this._map.on('touchstart', this._handleTouchStart.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
on(
|
||||||
|
eventType:
|
||||||
|
| 'mousemove'
|
||||||
|
| 'mouseenter'
|
||||||
|
| 'mouseleave'
|
||||||
|
| 'mousedown'
|
||||||
|
| 'click'
|
||||||
|
| 'contextmenu'
|
||||||
|
| 'touchstart',
|
||||||
|
|
||||||
|
layerId: string,
|
||||||
|
listener: MapLayerMouseEventListener | MapLayerTouchEventListener
|
||||||
|
) {
|
||||||
|
if (!this._listeners[layerId]) {
|
||||||
|
this._listeners[layerId] = {
|
||||||
|
features: [],
|
||||||
|
mousemoves: [],
|
||||||
|
mouseenters: [],
|
||||||
|
mouseleaves: [],
|
||||||
|
mousedowns: [],
|
||||||
|
clicks: [],
|
||||||
|
contextmenus: [],
|
||||||
|
touchstarts: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
switch (eventType) {
|
||||||
|
case 'mousemove':
|
||||||
|
this._listeners[layerId].mousemoves.push(listener as MapLayerMouseEventListener);
|
||||||
|
break;
|
||||||
|
case 'mouseenter':
|
||||||
|
this._listeners[layerId].mouseenters.push(listener as MapLayerMouseEventListener);
|
||||||
|
break;
|
||||||
|
case 'mouseleave':
|
||||||
|
this._listeners[layerId].mouseleaves.push(listener as MapLayerMouseEventListener);
|
||||||
|
break;
|
||||||
|
case 'mousedown':
|
||||||
|
this._listeners[layerId].mousedowns.push(listener as MapLayerMouseEventListener);
|
||||||
|
break;
|
||||||
|
case 'click':
|
||||||
|
this._listeners[layerId].clicks.push(listener as MapLayerMouseEventListener);
|
||||||
|
break;
|
||||||
|
case 'contextmenu':
|
||||||
|
this._listeners[layerId].contextmenus.push(listener as MapLayerMouseEventListener);
|
||||||
|
break;
|
||||||
|
case 'touchstart':
|
||||||
|
this._listeners[layerId].touchstarts.push(listener as MapLayerTouchEventListener);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
off(
|
||||||
|
eventType:
|
||||||
|
| 'mousemove'
|
||||||
|
| 'mouseenter'
|
||||||
|
| 'mouseleave'
|
||||||
|
| 'mousedown'
|
||||||
|
| 'click'
|
||||||
|
| 'contextmenu'
|
||||||
|
| 'touchstart',
|
||||||
|
layerId: string,
|
||||||
|
listener: MapLayerMouseEventListener | MapLayerTouchEventListener
|
||||||
|
) {
|
||||||
|
if (this._listeners[layerId]) {
|
||||||
|
switch (eventType) {
|
||||||
|
case 'mousemove':
|
||||||
|
this._listeners[layerId].mousemoves = this._listeners[
|
||||||
|
layerId
|
||||||
|
].mousemoves.filter((l) => l !== listener);
|
||||||
|
break;
|
||||||
|
case 'mouseenter':
|
||||||
|
this._listeners[layerId].mouseenters = this._listeners[
|
||||||
|
layerId
|
||||||
|
].mouseenters.filter((l) => l !== listener);
|
||||||
|
break;
|
||||||
|
case 'mouseleave':
|
||||||
|
this._listeners[layerId].mouseleaves = this._listeners[
|
||||||
|
layerId
|
||||||
|
].mouseleaves.filter((l) => l !== listener);
|
||||||
|
break;
|
||||||
|
case 'mousedown':
|
||||||
|
this._listeners[layerId].mousedowns = this._listeners[
|
||||||
|
layerId
|
||||||
|
].mousedowns.filter((l) => l !== listener);
|
||||||
|
break;
|
||||||
|
case 'click':
|
||||||
|
this._listeners[layerId].clicks = this._listeners[layerId].clicks.filter(
|
||||||
|
(l) => l !== listener
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'contextmenu':
|
||||||
|
this._listeners[layerId].contextmenus = this._listeners[
|
||||||
|
layerId
|
||||||
|
].contextmenus.filter((l) => l !== listener);
|
||||||
|
break;
|
||||||
|
case 'touchstart':
|
||||||
|
this._listeners[layerId].touchstarts = this._listeners[
|
||||||
|
layerId
|
||||||
|
].touchstarts.filter((l) => l !== listener);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this._listeners[layerId].mousemoves.length === 0 &&
|
||||||
|
this._listeners[layerId].mouseenters.length === 0 &&
|
||||||
|
this._listeners[layerId].mouseleaves.length === 0 &&
|
||||||
|
this._listeners[layerId].mousedowns.length === 0 &&
|
||||||
|
this._listeners[layerId].clicks.length === 0 &&
|
||||||
|
this._listeners[layerId].contextmenus.length === 0 &&
|
||||||
|
this._listeners[layerId].touchstarts.length === 0
|
||||||
|
) {
|
||||||
|
delete this._listeners[layerId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleMouseMove(e: maplibregl.MapMouseEvent) {
|
||||||
|
const layerIds = Object.keys(this._listeners);
|
||||||
|
const features =
|
||||||
|
layerIds.length > 0
|
||||||
|
? this._map.queryRenderedFeatures(e.point, { layers: layerIds })
|
||||||
|
: [];
|
||||||
|
const featuresByLayer: Record<string, maplibregl.MapGeoJSONFeature[]> = {};
|
||||||
|
features.forEach((f) => {
|
||||||
|
if (!featuresByLayer[f.layer.id]) {
|
||||||
|
featuresByLayer[f.layer.id] = [];
|
||||||
|
}
|
||||||
|
featuresByLayer[f.layer.id].push(f);
|
||||||
|
});
|
||||||
|
Object.keys(this._listeners).forEach((layerId) => {
|
||||||
|
const features = featuresByLayer[layerId] || [];
|
||||||
|
const listener = this._listeners[layerId];
|
||||||
|
if ((features.length == 0) != (listener.features.length == 0)) {
|
||||||
|
if (features.length > 0) {
|
||||||
|
if (listener.mouseenters.length > 0) {
|
||||||
|
const event = new maplibregl.MapMouseEvent(
|
||||||
|
'mouseenter',
|
||||||
|
e.target,
|
||||||
|
e.originalEvent,
|
||||||
|
{
|
||||||
|
features: featuresByLayer[layerId]!,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
listener.mouseenters.forEach((l) => l(event));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (listener.mouseleaves.length > 0) {
|
||||||
|
const event = new maplibregl.MapMouseEvent(
|
||||||
|
'mouseleave',
|
||||||
|
e.target,
|
||||||
|
e.originalEvent
|
||||||
|
);
|
||||||
|
listener.mouseleaves.forEach((l) => l(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listener.features = features;
|
||||||
|
}
|
||||||
|
if (features.length > 0 && listener.mousemoves.length > 0) {
|
||||||
|
const event = new maplibregl.MapMouseEvent('mousemove', e.target, e.originalEvent, {
|
||||||
|
features: featuresByLayer[layerId]!,
|
||||||
|
});
|
||||||
|
listener.mousemoves.forEach((l) => l(event));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleMouseClick(type: string, e: maplibregl.MapMouseEvent) {
|
||||||
|
Object.values(this._listeners).forEach((listener) => {
|
||||||
|
if (listener.features.length > 0) {
|
||||||
|
if (type === 'click' && listener.clicks.length > 0) {
|
||||||
|
const event = new maplibregl.MapMouseEvent('click', e.target, e.originalEvent, {
|
||||||
|
features: listener.features,
|
||||||
|
});
|
||||||
|
listener.clicks.forEach((l) => l(event));
|
||||||
|
} else if (type === 'contextmenu' && listener.contextmenus.length > 0) {
|
||||||
|
const event = new maplibregl.MapMouseEvent(
|
||||||
|
'contextmenu',
|
||||||
|
e.target,
|
||||||
|
e.originalEvent,
|
||||||
|
{
|
||||||
|
features: listener.features,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
listener.contextmenus.forEach((l) => l(event));
|
||||||
|
} else if (type === 'mousedown' && listener.mousedowns.length > 0) {
|
||||||
|
const event = new maplibregl.MapMouseEvent(
|
||||||
|
'mousedown',
|
||||||
|
e.target,
|
||||||
|
e.originalEvent,
|
||||||
|
{
|
||||||
|
features: listener.features,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
listener.mousedowns.forEach((l) => l(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleTouchStart(e: maplibregl.MapTouchEvent) {
|
||||||
|
const layerIds = Object.keys(this._listeners).filter(
|
||||||
|
(layerId) => this._listeners[layerId].touchstarts.length > 0
|
||||||
|
);
|
||||||
|
if (layerIds.length === 0) return;
|
||||||
|
const features = this._map.queryRenderedFeatures(e.points[0], { layers: layerIds });
|
||||||
|
const featuresByLayer: Record<string, maplibregl.MapGeoJSONFeature[]> = {};
|
||||||
|
features.forEach((f) => {
|
||||||
|
if (!featuresByLayer[f.layer.id]) {
|
||||||
|
featuresByLayer[f.layer.id] = [];
|
||||||
|
}
|
||||||
|
featuresByLayer[f.layer.id].push(f);
|
||||||
|
});
|
||||||
|
Object.keys(this._listeners).forEach((layerId) => {
|
||||||
|
const features = featuresByLayer[layerId] || [];
|
||||||
|
const listener = this._listeners[layerId];
|
||||||
|
if (features.length > 0) {
|
||||||
|
const event: maplibregl.MapLayerTouchEvent = new maplibregl.MapTouchEvent(
|
||||||
|
'touchstart',
|
||||||
|
e.target,
|
||||||
|
e.originalEvent
|
||||||
|
);
|
||||||
|
event.features = featuresByLayer[layerId]!;
|
||||||
|
listener.touchstarts.forEach((l) => l(event));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { get, writable, type Writable } from 'svelte/store';
|
|||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { ANCHOR_LAYER_KEY, StyleManager } from '$lib/components/map/style';
|
import { ANCHOR_LAYER_KEY, StyleManager } from '$lib/components/map/style';
|
||||||
|
import { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
||||||
|
|
||||||
const { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings;
|
const { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings;
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ export class MapLibreGLMap {
|
|||||||
private _onLoadCallbacks: ((map: maplibregl.Map) => void)[] = [];
|
private _onLoadCallbacks: ((map: maplibregl.Map) => void)[] = [];
|
||||||
private _unsubscribes: (() => void)[] = [];
|
private _unsubscribes: (() => void)[] = [];
|
||||||
private callOnLoadBinded: () => void = this.callOnLoad.bind(this);
|
private callOnLoadBinded: () => void = this.callOnLoad.bind(this);
|
||||||
|
public layerEventManager: MapLayerEventManager | null = null;
|
||||||
|
|
||||||
subscribe(run: (value: maplibregl.Map | null) => void, invalidate?: () => void) {
|
subscribe(run: (value: maplibregl.Map | null) => void, invalidate?: () => void) {
|
||||||
return this._mapStore.subscribe(run, invalidate);
|
return this._mapStore.subscribe(run, invalidate);
|
||||||
@@ -54,6 +56,7 @@ export class MapLibreGLMap {
|
|||||||
boxZoom: false,
|
boxZoom: false,
|
||||||
maxPitch: 85,
|
maxPitch: 85,
|
||||||
});
|
});
|
||||||
|
this.layerEventManager = new MapLayerEventManager(map);
|
||||||
map.addControl(
|
map.addControl(
|
||||||
new maplibregl.NavigationControl({
|
new maplibregl.NavigationControl({
|
||||||
visualizePitch: true,
|
visualizePitch: true,
|
||||||
|
|||||||
@@ -20,9 +20,14 @@
|
|||||||
let container: HTMLElement;
|
let container: HTMLElement;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
map.onLoad((map: maplibregl.Map) => {
|
map.onLoad((map_: maplibregl.Map) => {
|
||||||
googleRedirect = new GoogleRedirect(map);
|
googleRedirect = new GoogleRedirect(map_);
|
||||||
mapillaryLayer = new MapillaryLayer(map, container, mapillaryOpen);
|
mapillaryLayer = new MapillaryLayer(
|
||||||
|
map_,
|
||||||
|
map.layerEventManager!,
|
||||||
|
container,
|
||||||
|
mapillaryOpen
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.mod
|
|||||||
import 'mapillary-js/dist/mapillary.css';
|
import 'mapillary-js/dist/mapillary.css';
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
import { ANCHOR_LAYER_KEY } from '../style';
|
import { ANCHOR_LAYER_KEY } from '../style';
|
||||||
|
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
||||||
|
|
||||||
const mapillarySource: VectorSourceSpecification = {
|
const mapillarySource: VectorSourceSpecification = {
|
||||||
type: 'vector',
|
type: 'vector',
|
||||||
@@ -43,6 +44,7 @@ const mapillaryImageLayer: LayerSpecification = {
|
|||||||
|
|
||||||
export class MapillaryLayer {
|
export class MapillaryLayer {
|
||||||
map: maplibregl.Map;
|
map: maplibregl.Map;
|
||||||
|
layerEventManager: MapLayerEventManager;
|
||||||
marker: maplibregl.Marker;
|
marker: maplibregl.Marker;
|
||||||
viewer: Viewer;
|
viewer: Viewer;
|
||||||
|
|
||||||
@@ -53,8 +55,14 @@ export class MapillaryLayer {
|
|||||||
onMouseEnterBinded = this.onMouseEnter.bind(this);
|
onMouseEnterBinded = this.onMouseEnter.bind(this);
|
||||||
onMouseLeaveBinded = this.onMouseLeave.bind(this);
|
onMouseLeaveBinded = this.onMouseLeave.bind(this);
|
||||||
|
|
||||||
constructor(map: maplibregl.Map, container: HTMLElement, popupOpen: { value: boolean }) {
|
constructor(
|
||||||
|
map: maplibregl.Map,
|
||||||
|
layerEventManager: MapLayerEventManager,
|
||||||
|
container: HTMLElement,
|
||||||
|
popupOpen: { value: boolean }
|
||||||
|
) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
|
this.layerEventManager = layerEventManager;
|
||||||
|
|
||||||
this.viewer = new Viewer({
|
this.viewer = new Viewer({
|
||||||
accessToken: 'MLY|4381405525255083|3204871ec181638c3c31320490f03011',
|
accessToken: 'MLY|4381405525255083|3204871ec181638c3c31320490f03011',
|
||||||
@@ -103,14 +111,14 @@ export class MapillaryLayer {
|
|||||||
this.map.addLayer(mapillaryImageLayer, ANCHOR_LAYER_KEY.mapillary);
|
this.map.addLayer(mapillaryImageLayer, ANCHOR_LAYER_KEY.mapillary);
|
||||||
}
|
}
|
||||||
this.map.on('style.load', this.addBinded);
|
this.map.on('style.load', this.addBinded);
|
||||||
this.map.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
this.layerEventManager.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
||||||
this.map.on('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
|
this.layerEventManager.on('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.map.off('style.load', this.addBinded);
|
this.map.off('style.load', this.addBinded);
|
||||||
this.map.off('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
this.layerEventManager.off('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
||||||
this.map.off('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
|
this.layerEventManager.off('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
|
||||||
|
|
||||||
if (this.map.getLayer('mapillary-image')) {
|
if (this.map.getLayer('mapillary-image')) {
|
||||||
this.map.removeLayer('mapillary-image');
|
this.map.removeLayer('mapillary-image');
|
||||||
|
|||||||
@@ -45,22 +45,51 @@ export class StyleManager {
|
|||||||
this._maptilerKey = maptilerKey;
|
this._maptilerKey = maptilerKey;
|
||||||
this._map.subscribe((map_) => {
|
this._map.subscribe((map_) => {
|
||||||
if (map_) {
|
if (map_) {
|
||||||
this.update();
|
this.updateBasemap();
|
||||||
map_.on('style.load', () => this.updateOverlays());
|
map_.on('style.load', () => this.updateOverlays());
|
||||||
map_.on('pitch', () => this.updateTerrain());
|
map_.on('pitch', () => this.updateTerrain());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
currentBasemap.subscribe(() => this.update());
|
currentBasemap.subscribe(() => this.updateBasemap());
|
||||||
customBasemapUpdate.subscribe(() => this.update());
|
customBasemapUpdate.subscribe(() => this.updateBasemap());
|
||||||
currentOverlays.subscribe(() => this.updateOverlays());
|
currentOverlays.subscribe(() => this.updateOverlays());
|
||||||
opacities.subscribe(() => this.updateOverlays());
|
opacities.subscribe(() => this.updateOverlays());
|
||||||
terrainSource.subscribe(() => this.updateTerrain());
|
terrainSource.subscribe(() => this.updateTerrain());
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
updateBasemap() {
|
||||||
const map_ = get(this._map);
|
const map_ = get(this._map);
|
||||||
if (!map_) return;
|
if (!map_) return;
|
||||||
this.build().then((style) => map_.setStyle(style));
|
this.buildStyle().then((style) => map_.setStyle(style));
|
||||||
|
}
|
||||||
|
|
||||||
|
async buildStyle(): Promise<maplibregl.StyleSpecification> {
|
||||||
|
const custom = get(customLayers);
|
||||||
|
|
||||||
|
const style: maplibregl.StyleSpecification = {
|
||||||
|
version: 8,
|
||||||
|
projection: {
|
||||||
|
type: 'globe',
|
||||||
|
},
|
||||||
|
sources: {
|
||||||
|
'empty-source': emptySource,
|
||||||
|
},
|
||||||
|
layers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let basemap = get(currentBasemap);
|
||||||
|
const basemapInfo = basemaps[basemap] ?? custom[basemap]?.value ?? basemaps[defaultBasemap];
|
||||||
|
const basemapStyle = await this.get(basemapInfo);
|
||||||
|
|
||||||
|
this.merge(style, basemapStyle);
|
||||||
|
|
||||||
|
const terrain = this.getCurrentTerrain();
|
||||||
|
style.sources[terrain.source] = terrainSources[terrain.source];
|
||||||
|
style.terrain = terrain.exaggeration > 0 ? terrain : undefined;
|
||||||
|
|
||||||
|
style.layers.push(...anchorLayers);
|
||||||
|
|
||||||
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOverlays() {
|
async updateOverlays() {
|
||||||
@@ -127,38 +156,14 @@ export class StyleManager {
|
|||||||
const mapTerrain = map_.getTerrain();
|
const mapTerrain = map_.getTerrain();
|
||||||
const terrain = this.getCurrentTerrain();
|
const terrain = this.getCurrentTerrain();
|
||||||
if (JSON.stringify(mapTerrain) !== JSON.stringify(terrain)) {
|
if (JSON.stringify(mapTerrain) !== JSON.stringify(terrain)) {
|
||||||
map_.setTerrain(terrain);
|
if (terrain.exaggeration > 0) {
|
||||||
|
map_.setTerrain(terrain);
|
||||||
|
} else {
|
||||||
|
map_.setTerrain(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async build(): Promise<maplibregl.StyleSpecification> {
|
|
||||||
const custom = get(customLayers);
|
|
||||||
|
|
||||||
const style: maplibregl.StyleSpecification = {
|
|
||||||
version: 8,
|
|
||||||
projection: {
|
|
||||||
type: 'globe',
|
|
||||||
},
|
|
||||||
sources: {
|
|
||||||
'empty-source': emptySource,
|
|
||||||
},
|
|
||||||
layers: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
let basemap = get(currentBasemap);
|
|
||||||
const basemapInfo = basemaps[basemap] ?? custom[basemap]?.value ?? basemaps[defaultBasemap];
|
|
||||||
const basemapStyle = await this.get(basemapInfo);
|
|
||||||
|
|
||||||
this.merge(style, basemapStyle);
|
|
||||||
|
|
||||||
style.terrain = this.getCurrentTerrain();
|
|
||||||
style.sources[style.terrain.source] = terrainSources[style.terrain.source];
|
|
||||||
|
|
||||||
style.layers.push(...anchorLayers);
|
|
||||||
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(
|
async get(
|
||||||
styleInfo: maplibregl.StyleSpecification | string
|
styleInfo: maplibregl.StyleSpecification | string
|
||||||
): Promise<maplibregl.StyleSpecification> {
|
): Promise<maplibregl.StyleSpecification> {
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ export class RoutingControls {
|
|||||||
|
|
||||||
add() {
|
add() {
|
||||||
const map_ = get(map);
|
const map_ = get(map);
|
||||||
if (!map_) {
|
const layerEventManager = map.layerEventManager;
|
||||||
|
if (!map_ || !layerEventManager) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,8 +103,8 @@ export class RoutingControls {
|
|||||||
|
|
||||||
map_.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
map_.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
||||||
map_.on('click', this.appendAnchorBinded);
|
map_.on('click', this.appendAnchorBinded);
|
||||||
map_.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
layerEventManager.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
||||||
map_.on('click', this.fileId, stopPropagation);
|
layerEventManager.on('click', this.fileId, stopPropagation);
|
||||||
|
|
||||||
this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this));
|
this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this));
|
||||||
}
|
}
|
||||||
@@ -152,20 +153,18 @@ export class RoutingControls {
|
|||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
const map_ = get(map);
|
const map_ = get(map);
|
||||||
if (!map_) {
|
const layerEventManager = map.layerEventManager;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.active = false;
|
this.active = false;
|
||||||
|
|
||||||
for (let anchor of this.anchors) {
|
for (let anchor of this.anchors) {
|
||||||
anchor.marker.remove();
|
anchor.marker.remove();
|
||||||
}
|
}
|
||||||
map_.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
map_?.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
||||||
map_.off('click', this.appendAnchorBinded);
|
map_?.off('click', this.appendAnchorBinded);
|
||||||
map_.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
layerEventManager?.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
||||||
map_.off('click', this.fileId, stopPropagation);
|
layerEventManager?.off('click', this.fileId, stopPropagation);
|
||||||
map_.off('mousemove', this.updateTemporaryAnchorBinded);
|
map_?.off('mousemove', this.updateTemporaryAnchorBinded);
|
||||||
this.temporaryAnchor.marker.remove();
|
this.temporaryAnchor.marker.remove();
|
||||||
|
|
||||||
this.fileUnsubscribe();
|
this.fileUnsubscribe();
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
splitControls = new SplitControls($map);
|
splitControls = new SplitControls($map, map.layerEventManager!);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,20 @@ import { fileActions } from '$lib/logic/file-actions';
|
|||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
import type { GeoJSONSource } from 'maplibre-gl';
|
import type { GeoJSONSource } from 'maplibre-gl';
|
||||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
||||||
|
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
||||||
|
|
||||||
export class SplitControls {
|
export class SplitControls {
|
||||||
map: maplibregl.Map;
|
map: maplibregl.Map;
|
||||||
|
layerEventManager: MapLayerEventManager;
|
||||||
unsubscribes: Function[] = [];
|
unsubscribes: Function[] = [];
|
||||||
|
|
||||||
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
||||||
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
||||||
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
||||||
|
|
||||||
constructor(map: maplibregl.Map) {
|
constructor(map: maplibregl.Map, layerEventManager: MapLayerEventManager) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
|
this.layerEventManager = layerEventManager;
|
||||||
|
|
||||||
if (!this.map.hasImage('split-control')) {
|
if (!this.map.hasImage('split-control')) {
|
||||||
let icon = new Image(100, 100);
|
let icon = new Image(100, 100);
|
||||||
@@ -125,9 +128,17 @@ export class SplitControls {
|
|||||||
ANCHOR_LAYER_KEY.interactions
|
ANCHOR_LAYER_KEY.interactions
|
||||||
);
|
);
|
||||||
|
|
||||||
this.map.on('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
this.layerEventManager.on(
|
||||||
this.map.on('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
'mouseenter',
|
||||||
this.map.on('click', 'split-controls', this.layerOnClickBinded);
|
'split-controls',
|
||||||
|
this.layerOnMouseEnterBinded
|
||||||
|
);
|
||||||
|
this.layerEventManager.on(
|
||||||
|
'mouseleave',
|
||||||
|
'split-controls',
|
||||||
|
this.layerOnMouseLeaveBinded
|
||||||
|
);
|
||||||
|
this.layerEventManager.on('click', 'split-controls', this.layerOnClickBinded);
|
||||||
}
|
}
|
||||||
} 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
|
||||||
@@ -135,9 +146,9 @@ export class SplitControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.map.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
this.layerEventManager.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
||||||
this.map.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
this.layerEventManager.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
||||||
this.map.off('click', 'split-controls', this.layerOnClickBinded);
|
this.layerEventManager.off('click', 'split-controls', this.layerOnClickBinded);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.map.getLayer('split-controls')) {
|
if (this.map.getLayer('split-controls')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user