@deck.gl/maplibre 9.4.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/overlay.ts ADDED
@@ -0,0 +1,388 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {Deck, assert} from '@deck.gl/core';
6
+ import {
7
+ createMapLibreInterleavedDeck,
8
+ getMapLibreDefaultParameters,
9
+ getMapLibreDefaultView,
10
+ getMapLibreViewState,
11
+ MAPLIBRE_VIEW_ID,
12
+ removeMapLibreDeckInstance
13
+ } from './deck-utils';
14
+ import {getMapLibreProjection} from './compatibility';
15
+ import {resolveMapLibreLayerGroups} from './resolve-layer-groups';
16
+
17
+ import type {DeckProps} from '@deck.gl/core';
18
+ import type {MjolnirGestureEvent, MjolnirPointerEvent} from 'mjolnir.js';
19
+ import type {ControlPosition, IControl, Map as MapLibreMap} from 'maplibre-gl';
20
+
21
+ type MapLibreInputEvent = {
22
+ type: string;
23
+ originalEvent?: Event;
24
+ point?: {x: number; y: number};
25
+ };
26
+
27
+ /** Properties for MapLibreOverlay. */
28
+ export type MapLibreOverlayProps = Omit<
29
+ DeckProps,
30
+ | 'width'
31
+ | 'height'
32
+ | 'gl'
33
+ | 'parent'
34
+ | 'canvas'
35
+ | '_customRender'
36
+ | 'viewState'
37
+ | 'initialViewState'
38
+ | 'controller'
39
+ > & {
40
+ /**
41
+ * If true, deck.gl layers are inserted into MapLibre's layer stack and share its WebGL2 context.
42
+ * @default false
43
+ */
44
+ interleaved?: boolean;
45
+ };
46
+
47
+ /** Renders deck.gl layers over a MapLibre map and synchronizes with its camera. */
48
+ export default class MapLibreOverlay implements IControl {
49
+ private _props: MapLibreOverlayProps;
50
+ private _deck?: Deck<any>;
51
+ private _map?: MapLibreMap;
52
+ private _container?: HTMLDivElement;
53
+ private readonly _interleaved: boolean;
54
+
55
+ private _lastMouseDownPoint?: {x: number; y: number; clientX: number; clientY: number};
56
+
57
+ /** Creates a MapLibre overlay. */
58
+ constructor(props: MapLibreOverlayProps) {
59
+ const {interleaved = false} = props;
60
+ this._interleaved = interleaved;
61
+ this._props = this._filterProps(props);
62
+ }
63
+
64
+ private _filterProps(props: MapLibreOverlayProps): MapLibreOverlayProps {
65
+ const {interleaved: _, useDevicePixels, device, ...deckProps} = props;
66
+ if (!this._interleaved) {
67
+ if (useDevicePixels !== undefined) {
68
+ Object.assign(deckProps, {useDevicePixels});
69
+ }
70
+ if (device !== undefined) {
71
+ Object.assign(deckProps, {device});
72
+ }
73
+ }
74
+ return deckProps as MapLibreOverlayProps;
75
+ }
76
+
77
+ /** Updates partial properties of the underlying Deck instance. */
78
+ setProps(props: MapLibreOverlayProps): void {
79
+ if (this._interleaved && props.layers) {
80
+ resolveMapLibreLayerGroups(this._map, this._props.layers, props.layers);
81
+ }
82
+
83
+ Object.assign(this._props, this._filterProps(props));
84
+
85
+ if (this._deck && this._map) {
86
+ this._deck.setProps({
87
+ ...this._props,
88
+ views: this._getViews(this._map),
89
+ parameters: {
90
+ ...getMapLibreDefaultParameters(this._map, this._interleaved),
91
+ ...this._props.parameters
92
+ }
93
+ });
94
+ }
95
+ }
96
+
97
+ /** Called when the control is added to a map. */
98
+ onAdd(map: MapLibreMap): HTMLDivElement {
99
+ this._map = map;
100
+ try {
101
+ return this._interleaved ? this._onAddInterleaved(map) : this._onAddOverlaid(map);
102
+ } catch (error) {
103
+ if (this._deck) {
104
+ if (this._interleaved) {
105
+ this._onRemoveInterleaved(map);
106
+ } else {
107
+ this._onRemoveOverlaid(map);
108
+ }
109
+ }
110
+ this._deck = undefined;
111
+ this._map = undefined;
112
+ this._container = undefined;
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ /** Called when the control is removed from a map. */
118
+ onRemove(_map: MapLibreMap): void {
119
+ const map = this._map;
120
+
121
+ if (map) {
122
+ if (this._interleaved) {
123
+ this._onRemoveInterleaved(map);
124
+ } else {
125
+ this._onRemoveOverlaid(map);
126
+ }
127
+ }
128
+
129
+ this._deck = undefined;
130
+ this._map = undefined;
131
+ this._container = undefined;
132
+ }
133
+
134
+ /** Default control position. */
135
+ getDefaultPosition(): ControlPosition {
136
+ return 'top-left';
137
+ }
138
+
139
+ /** Forwards to Deck.pickObject. */
140
+ pickObject(params: Parameters<Deck['pickObject']>[0]): ReturnType<Deck['pickObject']> {
141
+ assert(this._deck);
142
+ return this._deck.pickObject(params);
143
+ }
144
+
145
+ /** Forwards to Deck.pickMultipleObjects. */
146
+ pickMultipleObjects(
147
+ params: Parameters<Deck['pickMultipleObjects']>[0]
148
+ ): ReturnType<Deck['pickMultipleObjects']> {
149
+ assert(this._deck);
150
+ return this._deck.pickMultipleObjects(params);
151
+ }
152
+
153
+ /** Forwards to Deck.pickObjects. */
154
+ pickObjects(params: Parameters<Deck['pickObjects']>[0]): ReturnType<Deck['pickObjects']> {
155
+ assert(this._deck);
156
+ return this._deck.pickObjects(params);
157
+ }
158
+
159
+ /** Removes the control from its map and releases resources. */
160
+ finalize(): void {
161
+ this._map?.removeControl(this);
162
+ }
163
+
164
+ /** Returns the basemap canvas in interleaved mode and Deck's canvas otherwise. */
165
+ getCanvas(): HTMLCanvasElement | null {
166
+ if (!this._map) {
167
+ return null;
168
+ }
169
+ return this._interleaved ? this._map.getCanvas() : this._deck!.getCanvas();
170
+ }
171
+
172
+ private _onAddOverlaid(map: MapLibreMap): HTMLDivElement {
173
+ const container = document.createElement('div');
174
+ Object.assign(container.style, {
175
+ position: 'absolute',
176
+ left: 0,
177
+ top: 0,
178
+ textAlign: 'initial',
179
+ pointerEvents: 'none'
180
+ });
181
+ this._container = container;
182
+
183
+ this._deck = new Deck<any>({
184
+ ...this._props,
185
+ parent: container,
186
+ deviceProps: {
187
+ ...this._props.deviceProps,
188
+ createCanvasContext: {
189
+ ...(typeof this._props.deviceProps?.createCanvasContext === 'object'
190
+ ? this._props.deviceProps.createCanvasContext
191
+ : undefined),
192
+ pixelSizeSource: 'css-dpr'
193
+ }
194
+ },
195
+ parameters: {
196
+ ...getMapLibreDefaultParameters(map, false),
197
+ ...this._props.parameters
198
+ },
199
+ views: this._getViews(map),
200
+ viewState: getMapLibreViewState(map)
201
+ });
202
+
203
+ map.on('resize', this._updateContainerSize);
204
+ map.on('render', this._updateViewState);
205
+ map.on('mousedown', this._handleMouseEvent);
206
+ map.on('dragstart', this._handleMouseEvent);
207
+ map.on('drag', this._handleMouseEvent);
208
+ map.on('dragend', this._handleMouseEvent);
209
+ map.on('mousemove', this._handleMouseEvent);
210
+ map.on('mouseout', this._handleMouseEvent);
211
+ map.on('click', this._handleMouseEvent);
212
+ map.on('dblclick', this._handleMouseEvent);
213
+
214
+ this._updateContainerSize();
215
+ return container;
216
+ }
217
+
218
+ private _onAddInterleaved(map: MapLibreMap): HTMLDivElement {
219
+ this._deck = createMapLibreInterleavedDeck(map, {
220
+ ...this._props,
221
+ views: this._getViews(map),
222
+ parameters: {
223
+ ...getMapLibreDefaultParameters(map, true),
224
+ ...this._props.parameters
225
+ }
226
+ });
227
+
228
+ map.on('styledata', this._handleStyleChange);
229
+ resolveMapLibreLayerGroups(map, [], this._props.layers);
230
+
231
+ return document.createElement('div');
232
+ }
233
+
234
+ private _onRemoveOverlaid(map: MapLibreMap): void {
235
+ map.off('resize', this._updateContainerSize);
236
+ map.off('render', this._updateViewState);
237
+ map.off('mousedown', this._handleMouseEvent);
238
+ map.off('dragstart', this._handleMouseEvent);
239
+ map.off('drag', this._handleMouseEvent);
240
+ map.off('dragend', this._handleMouseEvent);
241
+ map.off('mousemove', this._handleMouseEvent);
242
+ map.off('mouseout', this._handleMouseEvent);
243
+ map.off('click', this._handleMouseEvent);
244
+ map.off('dblclick', this._handleMouseEvent);
245
+ this._deck?.finalize();
246
+ }
247
+
248
+ private _onRemoveInterleaved(map: MapLibreMap): void {
249
+ map.off('styledata', this._handleStyleChange);
250
+ try {
251
+ resolveMapLibreLayerGroups(map, this._props.layers, []);
252
+ } finally {
253
+ removeMapLibreDeckInstance(map);
254
+ }
255
+ }
256
+
257
+ private _handleStyleChange = () => {
258
+ resolveMapLibreLayerGroups(this._map, this._props.layers, this._props.layers);
259
+ if (!this._map) {
260
+ return;
261
+ }
262
+
263
+ if (getMapLibreProjection(this._map)) {
264
+ this._deck?.setProps({views: this._getViews(this._map)});
265
+ }
266
+ };
267
+
268
+ private _updateContainerSize = () => {
269
+ if (this._map && this._container) {
270
+ const {clientWidth, clientHeight} = this._map.getContainer();
271
+ Object.assign(this._container.style, {
272
+ width: `${clientWidth}px`,
273
+ height: `${clientHeight}px`
274
+ });
275
+ }
276
+ };
277
+
278
+ private _getViews(map: MapLibreMap): any {
279
+ if (!this._props.views) {
280
+ return getMapLibreDefaultView(map);
281
+ }
282
+
283
+ const views = Array.isArray(this._props.views) ? this._props.views : [this._props.views];
284
+ const hasMapLibreView = views.some((view: any) => view.id === MAPLIBRE_VIEW_ID);
285
+ if (hasMapLibreView) {
286
+ return this._props.views;
287
+ }
288
+ return [getMapLibreDefaultView(map), ...views];
289
+ }
290
+
291
+ private _updateViewState = () => {
292
+ const deck = this._deck;
293
+ const map = this._map;
294
+ if (deck && map) {
295
+ deck.setProps({
296
+ views: this._getViews(map),
297
+ viewState: getMapLibreViewState(map)
298
+ });
299
+ if (deck.isInitialized) {
300
+ deck.redraw();
301
+ }
302
+ }
303
+ };
304
+
305
+ // eslint-disable-next-line complexity
306
+ private _handleMouseEvent = (event: MapLibreInputEvent) => {
307
+ const deck = this._deck;
308
+ if (!deck || !deck.isInitialized) {
309
+ return;
310
+ }
311
+
312
+ const mockEvent: {
313
+ type: string;
314
+ deltaX?: number;
315
+ deltaY?: number;
316
+ offsetCenter: {x: number; y: number};
317
+ srcEvent: MapLibreInputEvent;
318
+ tapCount?: number;
319
+ } = {
320
+ type: event.type,
321
+ offsetCenter: event.point || {x: 0, y: 0},
322
+ srcEvent: event
323
+ };
324
+
325
+ const lastDown = this._lastMouseDownPoint;
326
+ if (!event.point && lastDown && event.originalEvent instanceof MouseEvent) {
327
+ mockEvent.deltaX = event.originalEvent.clientX - lastDown.clientX;
328
+ mockEvent.deltaY = event.originalEvent.clientY - lastDown.clientY;
329
+ mockEvent.offsetCenter = {
330
+ x: lastDown.x + mockEvent.deltaX,
331
+ y: lastDown.y + mockEvent.deltaY
332
+ };
333
+ }
334
+
335
+ switch (mockEvent.type) {
336
+ case 'mousedown':
337
+ deck._onPointerDown(mockEvent as unknown as MjolnirPointerEvent);
338
+ if (!(event.originalEvent instanceof MouseEvent)) {
339
+ return;
340
+ }
341
+ this._lastMouseDownPoint = {
342
+ ...mockEvent.offsetCenter,
343
+ clientX: event.originalEvent.clientX,
344
+ clientY: event.originalEvent.clientY
345
+ };
346
+ break;
347
+
348
+ case 'dragstart':
349
+ mockEvent.type = 'panstart';
350
+ deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
351
+ break;
352
+
353
+ case 'drag':
354
+ mockEvent.type = 'panmove';
355
+ deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
356
+ break;
357
+
358
+ case 'dragend':
359
+ mockEvent.type = 'panend';
360
+ deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
361
+ break;
362
+
363
+ case 'click':
364
+ mockEvent.tapCount = 1;
365
+ deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
366
+ break;
367
+
368
+ case 'dblclick':
369
+ mockEvent.type = 'click';
370
+ mockEvent.tapCount = 2;
371
+ deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
372
+ break;
373
+
374
+ case 'mousemove':
375
+ mockEvent.type = 'pointermove';
376
+ deck._onPointerMove(mockEvent as unknown as MjolnirPointerEvent);
377
+ break;
378
+
379
+ case 'mouseout':
380
+ mockEvent.type = 'pointerleave';
381
+ deck._onPointerMove(mockEvent as unknown as MjolnirPointerEvent);
382
+ break;
383
+
384
+ default:
385
+ return;
386
+ }
387
+ };
388
+ }
@@ -0,0 +1,84 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {_flatten as flatten} from '@deck.gl/core';
6
+
7
+ import MapLibreLayerGroup from './layer-group';
8
+ import {getMapLibreLayerGroupId} from './layer-utils';
9
+
10
+ import type {Layer, LayersList} from '@deck.gl/core';
11
+ import type {Map as MapLibreMap} from 'maplibre-gl';
12
+ import type {MapLibreLayerProps} from './layer-utils';
13
+
14
+ const LAYER_GROUPS = new WeakMap<MapLibreMap, Map<string, MapLibreLayerGroup>>();
15
+
16
+ // eslint-disable-next-line complexity, max-statements
17
+ export function resolveMapLibreLayerGroups(
18
+ map?: MapLibreMap,
19
+ oldLayers?: LayersList,
20
+ newLayers?: LayersList
21
+ ): void {
22
+ if (!map || !map.isStyleLoaded()) {
23
+ return;
24
+ }
25
+
26
+ let layerGroups = LAYER_GROUPS.get(map);
27
+ if (!layerGroups) {
28
+ layerGroups = new Map();
29
+ LAYER_GROUPS.set(map, layerGroups);
30
+ }
31
+
32
+ const layers = flatten(newLayers, Boolean) as Layer<MapLibreLayerProps>[];
33
+ const newLayerGroupIds = new Set(layers.map(getMapLibreLayerGroupId));
34
+
35
+ if (oldLayers !== newLayers) {
36
+ const previousLayers = flatten(oldLayers, Boolean) as Layer<MapLibreLayerProps>[];
37
+ const previousLayerGroupIds = new Set(previousLayers.map(getMapLibreLayerGroupId));
38
+ for (const groupId of previousLayerGroupIds) {
39
+ if (!newLayerGroupIds.has(groupId) && layerGroups.has(groupId)) {
40
+ if (map.getLayer(groupId)) {
41
+ map.removeLayer(groupId);
42
+ }
43
+ layerGroups.delete(groupId);
44
+ }
45
+ }
46
+ }
47
+
48
+ for (const layer of layers) {
49
+ const groupId = getMapLibreLayerGroupId(layer);
50
+ if (layerGroups.has(groupId)) {
51
+ if (!map.getLayer(groupId)) {
52
+ map.addLayer(layerGroups.get(groupId)!, layer.props.beforeId);
53
+ }
54
+ continue;
55
+ }
56
+
57
+ if (map.getLayer(groupId)) {
58
+ throw new Error(`MapLibre style already contains a non-deck layer with id ${groupId}`);
59
+ }
60
+
61
+ const group = new MapLibreLayerGroup({
62
+ id: groupId,
63
+ beforeId: layer.props.beforeId
64
+ });
65
+ layerGroups.set(groupId, group);
66
+ map.addLayer(group, layer.props.beforeId);
67
+ }
68
+
69
+ for (const groupId of newLayerGroupIds) {
70
+ const group = layerGroups.get(groupId)!;
71
+ const mapLayers = map.getLayersOrder();
72
+ const expectedGroupIndex = group.beforeId
73
+ ? mapLayers.indexOf(group.beforeId)
74
+ : mapLayers.length;
75
+ if (expectedGroupIndex < 0) {
76
+ continue;
77
+ }
78
+
79
+ const currentGroupIndex = mapLayers.indexOf(groupId);
80
+ if (currentGroupIndex !== expectedGroupIndex - 1) {
81
+ map.moveLayer(groupId, group.beforeId);
82
+ }
83
+ }
84
+ }