• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / deck.gl / 21398838597

27 Jan 2026 01:22PM UTC coverage: 91.089% (-0.01%) from 91.1%
21398838597

Pull #9937

github

web-flow
Merge 7273eb05d into 914c3d2c2
Pull Request #9937: feat(core): Add pickable: '3d' option to all Layers

6862 of 7541 branches covered (91.0%)

Branch coverage included in aggregate %.

32 of 40 new or added lines in 4 files covered. (80.0%)

73 existing lines in 4 files now uncovered.

56815 of 62365 relevant lines covered (91.1%)

14385.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.22
/modules/mapbox/src/mapbox-overlay.ts
1
// deck.gl
1✔
2
// SPDX-License-Identifier: MIT
1✔
3
// Copyright (c) vis.gl contributors
1✔
4

1✔
5
import {Deck, assert, log} from '@deck.gl/core';
1✔
6
import {
1✔
7
  getViewState,
1✔
8
  getDefaultView,
1✔
9
  getDeckInstance,
1✔
10
  removeDeckInstance,
1✔
11
  getDefaultParameters,
1✔
12
  getProjection
1✔
13
} from './deck-utils';
1✔
14

1✔
15
import type {Map, IControl, MapMouseEvent, ControlPosition} from './types';
1✔
16
import type {MjolnirGestureEvent, MjolnirPointerEvent} from 'mjolnir.js';
1✔
17
import type {DeckProps, LayersList} from '@deck.gl/core';
1✔
18

1✔
19
import {resolveLayers} from './resolve-layers';
1✔
20
import {resolveLayerGroups} from './resolve-layer-groups';
1✔
21

1✔
22
export type MapboxOverlayProps = Omit<
1✔
23
  DeckProps,
1✔
24
  | 'width'
1✔
25
  | 'height'
1✔
26
  | 'gl'
1✔
27
  | 'parent'
1✔
28
  | 'canvas'
1✔
29
  | '_customRender'
1✔
30
  | 'viewState'
1✔
31
  | 'initialViewState'
1✔
32
  | 'controller'
1✔
33
> & {
1✔
34
  /**
1✔
35
   * deck.gl layers are inserted into mapbox-gl's layer stack, and share the same WebGL2RenderingContext as the base map.
1✔
36
   */
1✔
37
  interleaved?: boolean;
1✔
38

1✔
39
  /**
1✔
40
   * (experimental) render deck.gl layers in batches grouped by `beforeId` or `slot` to enable cross-layer extension handling.
1✔
41
   * @default false
1✔
42
   */
1✔
43
  _renderLayersInGroups?: boolean;
1✔
44
};
1✔
45

1✔
46
/**
1✔
47
 * Implements Mapbox [IControl](https://docs.mapbox.com/mapbox-gl-js/api/markers/#icontrol) interface
1✔
48
 * Renders deck.gl layers over the base map and automatically synchronizes with the map's camera
1✔
49
 */
1✔
50
export default class MapboxOverlay implements IControl {
1✔
51
  private _props: MapboxOverlayProps;
1✔
52
  private _deck?: Deck<any>;
1✔
53
  private _map?: Map;
1✔
54
  private _container?: HTMLDivElement;
1✔
55
  private _interleaved: boolean;
1✔
56
  private _renderLayersInGroups: boolean;
1✔
57
  private _lastMouseDownPoint?: {x: number; y: number; clientX: number; clientY: number};
1✔
58

1✔
59
  constructor(props: MapboxOverlayProps) {
1✔
60
    const {interleaved = false} = props;
11✔
61
    this._interleaved = interleaved;
11✔
62
    this._renderLayersInGroups = props._renderLayersInGroups || false;
11✔
63
    this._props = this.filterProps(props);
11✔
64
  }
11✔
65

1✔
66
  /** Filter out props to pass to Deck **/
1✔
67
  filterProps(props: MapboxOverlayProps): MapboxOverlayProps {
1✔
68
    const {interleaved = false, useDevicePixels, ...deckProps} = props;
17✔
69
    if (!interleaved && useDevicePixels !== undefined) {
17!
UNCOV
70
      // useDevicePixels cannot be used in interleaved mode
×
UNCOV
71
      (deckProps as MapboxOverlayProps).useDevicePixels = useDevicePixels;
×
UNCOV
72
    }
×
73
    return deckProps;
17✔
74
  }
17✔
75

1✔
76
  /** Update (partial) props of the underlying Deck instance. */
1✔
77
  setProps(props: MapboxOverlayProps): void {
1✔
78
    if (this._interleaved && props.layers) {
6✔
79
      this._resolveLayers(this._map, this._deck, this._props.layers, props.layers);
4✔
80
    }
4✔
81

6✔
82
    Object.assign(this._props, this.filterProps(props));
6✔
83

6✔
84
    if (this._deck && this._map) {
6✔
85
      this._deck.setProps({
6✔
86
        ...this._props,
6✔
87
        parameters: {
6✔
88
          ...getDefaultParameters(this._map, this._interleaved),
6✔
89
          ...this._props.parameters
6✔
90
        }
6✔
91
      });
6✔
92
    }
6✔
93
  }
6✔
94

1✔
95
  // The local Map type is for internal typecheck only. It does not necesarily satisefy mapbox/maplibre types at runtime.
1✔
96
  // Do not restrict the argument type here to avoid type conflict.
1✔
97
  /** Called when the control is added to a map */
1✔
98
  onAdd(map: unknown): HTMLDivElement {
1✔
99
    this._map = map as Map;
12✔
100
    return this._interleaved ? this._onAddInterleaved(map as Map) : this._onAddOverlaid(map as Map);
12✔
101
  }
12✔
102

1✔
103
  private _onAddOverlaid(map: Map): HTMLDivElement {
1✔
104
    /* global document */
2✔
105
    const container = document.createElement('div');
2✔
106
    Object.assign(container.style, {
2✔
107
      position: 'absolute',
2✔
108
      left: 0,
2✔
109
      top: 0,
2✔
110
      textAlign: 'initial',
2✔
111
      pointerEvents: 'none'
2✔
112
    });
2✔
113
    this._container = container;
2✔
114

2✔
115
    this._deck = new Deck<any>({
2✔
116
      ...this._props,
2✔
117
      parent: container,
2✔
118
      parameters: {...getDefaultParameters(map, false), ...this._props.parameters},
2✔
119
      views: this._props.views || getDefaultView(map),
2✔
120
      viewState: getViewState(map)
2✔
121
    });
2✔
122

2✔
123
    map.on('resize', this._updateContainerSize);
2✔
124
    map.on('render', this._updateViewState);
2✔
125
    map.on('mousedown', this._handleMouseEvent);
2✔
126
    map.on('dragstart', this._handleMouseEvent);
2✔
127
    map.on('drag', this._handleMouseEvent);
2✔
128
    map.on('dragend', this._handleMouseEvent);
2✔
129
    map.on('mousemove', this._handleMouseEvent);
2✔
130
    map.on('mouseout', this._handleMouseEvent);
2✔
131
    map.on('click', this._handleMouseEvent);
2✔
132
    map.on('dblclick', this._handleMouseEvent);
2✔
133

2✔
134
    this._updateContainerSize();
2✔
135
    return container;
2✔
136
  }
2✔
137

1✔
138
  private _onAddInterleaved(map: Map): HTMLDivElement {
1✔
139
    // @ts-ignore non-public map property
10✔
140
    const gl: WebGL2RenderingContext = map.painter.context.gl;
10✔
141
    if (gl instanceof WebGLRenderingContext) {
10!
UNCOV
142
      log.warn(
×
UNCOV
143
        'Incompatible basemap library. See: https://deck.gl/docs/api-reference/mapbox/overview#compatibility'
×
UNCOV
144
      )();
×
UNCOV
145
    }
×
146
    this._deck = getDeckInstance({
10✔
147
      map,
10✔
148
      deck: new Deck({
10✔
149
        ...this._props,
10✔
150
        gl,
10✔
151
        parameters: {...getDefaultParameters(map, true), ...this._props.parameters}
10✔
152
      })
10✔
153
    });
10✔
154

10✔
155
    map.on('styledata', this._handleStyleChange);
10✔
156
    this._resolveLayers(map, this._deck, [], this._props.layers);
10✔
157

10✔
158
    return document.createElement('div');
10✔
159
  }
10✔
160

1✔
161
  private _resolveLayers(
1✔
162
    map: Map | undefined,
32✔
163
    deck: Deck | undefined,
32✔
164
    prevLayers: LayersList | undefined,
32✔
165
    newLayers: LayersList | undefined
32✔
166
  ): void {
32✔
167
    if (this._renderLayersInGroups) {
32✔
168
      resolveLayerGroups(map, prevLayers, newLayers);
8✔
169
    } else {
32✔
170
      resolveLayers(map, deck, prevLayers, newLayers);
24✔
171
    }
24✔
172
  }
32✔
173

1✔
174
  /** Called when the control is removed from a map */
1✔
175
  onRemove(): void {
1✔
176
    const map = this._map;
12✔
177

12✔
178
    if (map) {
12✔
179
      if (this._interleaved) {
12✔
180
        this._onRemoveInterleaved(map);
10✔
181
      } else {
12✔
182
        this._onRemoveOverlaid(map);
2✔
183
      }
2✔
184
    }
12✔
185

12✔
186
    this._deck = undefined;
12✔
187
    this._map = undefined;
12✔
188
    this._container = undefined;
12✔
189
  }
12✔
190

1✔
191
  private _onRemoveOverlaid(map: Map): void {
1✔
192
    map.off('resize', this._updateContainerSize);
2✔
193
    map.off('render', this._updateViewState);
2✔
194
    map.off('mousedown', this._handleMouseEvent);
2✔
195
    map.off('dragstart', this._handleMouseEvent);
2✔
196
    map.off('drag', this._handleMouseEvent);
2✔
197
    map.off('dragend', this._handleMouseEvent);
2✔
198
    map.off('mousemove', this._handleMouseEvent);
2✔
199
    map.off('mouseout', this._handleMouseEvent);
2✔
200
    map.off('click', this._handleMouseEvent);
2✔
201
    map.off('dblclick', this._handleMouseEvent);
2✔
202
    this._deck?.finalize();
2✔
203
  }
2✔
204

1✔
205
  private _onRemoveInterleaved(map: Map): void {
1✔
206
    map.off('styledata', this._handleStyleChange);
10✔
207
    this._resolveLayers(map, this._deck, this._props.layers, []);
10✔
208
    removeDeckInstance(map);
10✔
209
  }
10✔
210

1✔
211
  getDefaultPosition(): ControlPosition {
1✔
UNCOV
212
    return 'top-left';
×
UNCOV
213
  }
×
214

1✔
215
  /** Forwards the Deck.pickObject method */
1✔
216
  pickObject(params: Parameters<Deck['pickObject']>[0]): ReturnType<Deck['pickObject']> {
1✔
UNCOV
217
    assert(this._deck);
×
UNCOV
218
    return this._deck.pickObject(params);
×
UNCOV
219
  }
×
220

1✔
221
  /** Forwards the Deck.pickMultipleObjects method */
1✔
222
  pickMultipleObjects(
1✔
223
    params: Parameters<Deck['pickMultipleObjects']>[0]
×
224
  ): ReturnType<Deck['pickMultipleObjects']> {
×
225
    assert(this._deck);
×
UNCOV
226
    return this._deck.pickMultipleObjects(params);
×
UNCOV
227
  }
×
228

1✔
229
  /** Forwards the Deck.pickObjects method */
1✔
230
  pickObjects(params: Parameters<Deck['pickObjects']>[0]): ReturnType<Deck['pickObjects']> {
1✔
UNCOV
231
    assert(this._deck);
×
UNCOV
232
    return this._deck.pickObjects(params);
×
UNCOV
233
  }
×
234

1✔
235
  /** Remove from map and releases all resources */
1✔
236
  finalize() {
1✔
237
    if (this._map) {
1✔
238
      this._map.removeControl(this);
1✔
239
    }
1✔
240
  }
1✔
241

1✔
242
  /** If interleaved: true, returns base map's canvas, otherwise forwards the Deck.getCanvas method. */
1✔
243
  getCanvas(): HTMLCanvasElement | null {
1✔
UNCOV
244
    if (!this._map) {
×
UNCOV
245
      return null;
×
UNCOV
246
    }
×
UNCOV
247
    return this._interleaved ? this._map.getCanvas() : this._deck!.getCanvas();
×
UNCOV
248
  }
×
249

1✔
250
  private _handleStyleChange = () => {
1✔
251
    this._resolveLayers(this._map, this._deck, this._props.layers, this._props.layers);
8✔
252
    if (!this._map) return;
8!
253

8✔
254
    // getProjection() returns undefined before style is loaded
8✔
255
    const projection = getProjection(this._map);
8✔
256
    if (projection && !this._props.views) {
8✔
257
      this._deck?.setProps({views: getDefaultView(this._map)});
8✔
258
    }
8✔
259
  };
8✔
260

1✔
261
  private _updateContainerSize = () => {
1✔
262
    if (this._map && this._container) {
2✔
263
      const {clientWidth, clientHeight} = this._map.getContainer();
2✔
264
      Object.assign(this._container.style, {
2✔
265
        width: `${clientWidth}px`,
2✔
266
        height: `${clientHeight}px`
2✔
267
      });
2✔
268
    }
2✔
269
  };
2✔
270

1✔
271
  private _updateViewState = () => {
1✔
272
    const deck = this._deck;
2✔
273
    const map = this._map;
2✔
274
    if (deck && map) {
2✔
275
      deck.setProps({
2✔
276
        views: this._props.views || getDefaultView(map),
2✔
277
        viewState: getViewState(map)
2✔
278
      });
2✔
279
      // Redraw immediately if view state has changed
2✔
280
      if (deck.isInitialized) {
2!
281
        deck.redraw();
×
282
      }
×
283
    }
2✔
284
  };
2✔
285

1✔
286
  // eslint-disable-next-line complexity
1✔
287
  private _handleMouseEvent = (event: MapMouseEvent) => {
1✔
288
    const deck = this._deck;
×
289
    if (!deck || !deck.isInitialized) {
×
290
      return;
×
291
    }
×
292

×
293
    const mockEvent: {
×
294
      type: string;
×
295
      deltaX?: number;
×
296
      deltaY?: number;
×
297
      offsetCenter: {x: number; y: number};
×
298
      srcEvent: MapMouseEvent;
×
299
      tapCount?: number;
×
300
    } = {
×
301
      type: event.type,
×
302
      offsetCenter: event.point,
×
303
      srcEvent: event
×
304
    };
×
305

×
306
    const lastDown = this._lastMouseDownPoint;
×
307
    if (!event.point && lastDown) {
×
308
      // drag* events do not contain a `point` field
×
309
      mockEvent.deltaX = event.originalEvent.clientX - lastDown.clientX;
×
310
      mockEvent.deltaY = event.originalEvent.clientY - lastDown.clientY;
×
311
      mockEvent.offsetCenter = {
×
312
        x: lastDown.x + mockEvent.deltaX,
×
313
        y: lastDown.y + mockEvent.deltaY
×
314
      };
×
315
    }
×
316

×
317
    switch (mockEvent.type) {
×
318
      case 'mousedown':
×
319
        deck._onPointerDown(mockEvent as unknown as MjolnirPointerEvent);
×
320
        this._lastMouseDownPoint = {
×
321
          ...event.point,
×
322
          clientX: event.originalEvent.clientX,
×
323
          clientY: event.originalEvent.clientY
×
324
        };
×
325
        break;
×
326

×
327
      case 'dragstart':
×
328
        mockEvent.type = 'panstart';
×
329
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
330
        break;
×
331

×
332
      case 'drag':
×
333
        mockEvent.type = 'panmove';
×
334
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
335
        break;
×
336

×
337
      case 'dragend':
×
338
        mockEvent.type = 'panend';
×
339
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
340
        break;
×
341

×
342
      case 'click':
×
343
        mockEvent.tapCount = 1;
×
UNCOV
344
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
UNCOV
345
        break;
×
UNCOV
346

×
UNCOV
347
      case 'dblclick':
×
UNCOV
348
        mockEvent.type = 'click';
×
UNCOV
349
        mockEvent.tapCount = 2;
×
UNCOV
350
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
UNCOV
351
        break;
×
UNCOV
352

×
UNCOV
353
      case 'mousemove':
×
UNCOV
354
        mockEvent.type = 'pointermove';
×
UNCOV
355
        deck._onPointerMove(mockEvent as unknown as MjolnirPointerEvent);
×
UNCOV
356
        break;
×
UNCOV
357

×
UNCOV
358
      case 'mouseout':
×
UNCOV
359
        mockEvent.type = 'pointerleave';
×
UNCOV
360
        deck._onPointerMove(mockEvent as unknown as MjolnirPointerEvent);
×
UNCOV
361
        break;
×
UNCOV
362

×
UNCOV
363
      default:
×
UNCOV
364
        return;
×
UNCOV
365
    }
×
UNCOV
366
  };
×
367
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc