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

visgl / deck.gl / 21186609184

20 Jan 2026 08:45PM UTC coverage: 91.106% (-0.009%) from 91.115%
21186609184

Pull #9947

github

web-flow
Merge 71960a312 into 83ac41278
Pull Request #9947: fix(mapbox): automatically inject 'mapbox' view in overlaid mode for multi-view consistency

6882 of 7561 branches covered (91.02%)

Branch coverage included in aggregate %.

11 of 16 new or added lines in 1 file covered. (68.75%)

16 existing lines in 1 file now uncovered.

56876 of 62421 relevant lines covered (91.12%)

14373.82 hits per line

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

71.63
/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} 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
import {log} from '@deck.gl/core';
1✔
19

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

10✔
160
    return document.createElement('div');
10✔
161
  }
10✔
162

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

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

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

12✔
188
    this._deck = undefined;
12✔
189
    this._map = undefined;
12✔
190
    this._container = undefined;
12✔
191
  }
12✔
192

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

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

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

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

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

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

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

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

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

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

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

1✔
273
  private _getViews(map: Map) {
1✔
274
    if (!this._props.views) {
4✔
275
      return getDefaultView(map);
4✔
276
    }
4!
NEW
277
    // Check if custom views include a view with id 'mapbox'
×
278
    const views = Array.isArray(this._props.views) ? this._props.views : [this._props.views];
4!
279
    const hasMapboxView = views.some((v: any) => v.id === 'mapbox');
4✔
280
    if (hasMapboxView) {
4!
NEW
281
      return this._props.views;
×
NEW
282
    }
×
NEW
283
    // Add default 'mapbox' view to custom views for consistency with interleaved mode
×
NEW
284
    return [getDefaultView(map), ...views];
×
285
  }
4✔
286

1✔
287
  private _updateViewState = () => {
1✔
288
    const deck = this._deck;
2✔
289
    const map = this._map;
2✔
290
    if (deck && map) {
2✔
291
      deck.setProps({
2✔
292
        views: this._getViews(map),
2✔
293
        viewState: getViewState(map)
2✔
294
      });
2✔
295
      // Redraw immediately if view state has changed
2✔
296
      if (deck.isInitialized) {
2!
UNCOV
297
        deck.redraw();
×
UNCOV
298
      }
×
299
    }
2✔
300
  };
2✔
301

1✔
302
  // eslint-disable-next-line complexity
1✔
303
  private _handleMouseEvent = (event: MapMouseEvent) => {
1✔
UNCOV
304
    const deck = this._deck;
×
UNCOV
305
    if (!deck || !deck.isInitialized) {
×
UNCOV
306
      return;
×
307
    }
×
308

×
309
    const mockEvent: {
×
310
      type: string;
×
311
      deltaX?: number;
×
312
      deltaY?: number;
×
313
      offsetCenter: {x: number; y: number};
×
314
      srcEvent: MapMouseEvent;
×
315
      tapCount?: number;
×
316
    } = {
×
317
      type: event.type,
×
318
      offsetCenter: event.point,
×
319
      srcEvent: event
×
320
    };
×
321

×
322
    const lastDown = this._lastMouseDownPoint;
×
323
    if (!event.point && lastDown) {
×
324
      // drag* events do not contain a `point` field
×
325
      mockEvent.deltaX = event.originalEvent.clientX - lastDown.clientX;
×
326
      mockEvent.deltaY = event.originalEvent.clientY - lastDown.clientY;
×
327
      mockEvent.offsetCenter = {
×
328
        x: lastDown.x + mockEvent.deltaX,
×
329
        y: lastDown.y + mockEvent.deltaY
×
330
      };
×
331
    }
×
332

×
333
    switch (mockEvent.type) {
×
334
      case 'mousedown':
×
335
        deck._onPointerDown(mockEvent as unknown as MjolnirPointerEvent);
×
336
        this._lastMouseDownPoint = {
×
337
          ...event.point,
×
338
          clientX: event.originalEvent.clientX,
×
339
          clientY: event.originalEvent.clientY
×
340
        };
×
341
        break;
×
342

×
343
      case 'dragstart':
×
344
        mockEvent.type = 'panstart';
×
345
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
346
        break;
×
347

×
348
      case 'drag':
×
349
        mockEvent.type = 'panmove';
×
350
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
351
        break;
×
352

×
353
      case 'dragend':
×
354
        mockEvent.type = 'panend';
×
355
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
356
        break;
×
357

×
358
      case 'click':
×
359
        mockEvent.tapCount = 1;
×
360
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
361
        break;
×
362

×
363
      case 'dblclick':
×
364
        mockEvent.type = 'click';
×
365
        mockEvent.tapCount = 2;
×
366
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
367
        break;
×
368

×
369
      case 'mousemove':
×
370
        mockEvent.type = 'pointermove';
×
371
        deck._onPointerMove(mockEvent as unknown as MjolnirPointerEvent);
×
372
        break;
×
373

×
374
      case 'mouseout':
×
375
        mockEvent.type = 'pointerleave';
×
376
        deck._onPointerMove(mockEvent as unknown as MjolnirPointerEvent);
×
377
        break;
×
378

×
379
      default:
×
380
        return;
×
381
    }
×
382
  };
×
383
}
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