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

visgl / deck.gl / 21338288234

25 Jan 2026 07:30PM UTC coverage: 91.062% (-0.05%) from 91.114%
21338288234

Pull #9963

github

web-flow
Merge a18b98487 into 50df2f259
Pull Request #9963: feat(mapbox): Add widget support to MapboxOverlay via IControl adapter

6878 of 7559 branches covered (90.99%)

Branch coverage included in aggregate %.

88 of 132 new or added lines in 2 files covered. (66.67%)

41 existing lines in 1 file now uncovered.

56949 of 62533 relevant lines covered (91.07%)

14349.81 hits per line

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

71.12
/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
import {DeckWidgetControl} from './deck-widget-control';
1✔
15

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

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

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

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

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

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

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

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

6✔
86
    // Process widgets with viewId: 'mapbox' before updating props
6✔
87
    // This must happen before deck.setProps so _container is set
6✔
88
    if (props.widgets !== undefined) {
6!
NEW
89
      this._processWidgets(props.widgets);
×
NEW
90
    }
×
91

6✔
92
    Object.assign(this._props, this.filterProps(props));
6✔
93

6✔
94
    if (this._deck && this._map) {
6✔
95
      this._deck.setProps({
6✔
96
        ...this._props,
6✔
97
        parameters: {
6✔
98
          ...getDefaultParameters(this._map, this._interleaved),
6✔
99
          ...this._props.parameters
6✔
100
        }
6✔
101
      });
6✔
102
    }
6✔
103
  }
6✔
104

1✔
105
  // The local Map type is for internal typecheck only. It does not necesarily satisefy mapbox/maplibre types at runtime.
1✔
106
  // Do not restrict the argument type here to avoid type conflict.
1✔
107
  /** Called when the control is added to a map */
1✔
108
  onAdd(map: unknown): HTMLDivElement {
1✔
109
    this._map = map as Map;
12✔
110
    return this._interleaved ? this._onAddInterleaved(map as Map) : this._onAddOverlaid(map as Map);
12✔
111
  }
12✔
112

1✔
113
  private _onAddOverlaid(map: Map): HTMLDivElement {
1✔
114
    /* global document */
2✔
115
    const container = document.createElement('div');
2✔
116
    Object.assign(container.style, {
2✔
117
      position: 'absolute',
2✔
118
      left: 0,
2✔
119
      top: 0,
2✔
120
      textAlign: 'initial',
2✔
121
      pointerEvents: 'none'
2✔
122
    });
2✔
123
    this._container = container;
2✔
124

2✔
125
    // Process widgets with viewId: 'mapbox' BEFORE creating Deck
2✔
126
    // so _container is set when WidgetManager initializes
2✔
127
    this._processWidgets(this._props.widgets);
2✔
128

2✔
129
    this._deck = new Deck<any>({
2✔
130
      ...this._props,
2✔
131
      parent: container,
2✔
132
      parameters: {...getDefaultParameters(map, false), ...this._props.parameters},
2✔
133
      views: this._props.views || getDefaultView(map),
2✔
134
      viewState: getViewState(map)
2✔
135
    });
2✔
136

2✔
137
    map.on('resize', this._updateContainerSize);
2✔
138
    map.on('render', this._updateViewState);
2✔
139
    map.on('mousedown', this._handleMouseEvent);
2✔
140
    map.on('dragstart', this._handleMouseEvent);
2✔
141
    map.on('drag', this._handleMouseEvent);
2✔
142
    map.on('dragend', this._handleMouseEvent);
2✔
143
    map.on('mousemove', this._handleMouseEvent);
2✔
144
    map.on('mouseout', this._handleMouseEvent);
2✔
145
    map.on('click', this._handleMouseEvent);
2✔
146
    map.on('dblclick', this._handleMouseEvent);
2✔
147

2✔
148
    this._updateContainerSize();
2✔
149
    return container;
2✔
150
  }
2✔
151

1✔
152
  private _onAddInterleaved(map: Map): HTMLDivElement {
1✔
153
    // @ts-ignore non-public map property
10✔
154
    const gl = map.painter.context.gl;
10✔
155
    if (gl instanceof WebGLRenderingContext) {
10!
156
      log.warn(
×
157
        'Incompatible basemap library. See: https://deck.gl/docs/api-reference/mapbox/overview#compatibility'
×
158
      )();
×
159
    }
×
160

10✔
161
    // Process widgets with viewId: 'mapbox' BEFORE creating Deck
10✔
162
    // so _container is set when WidgetManager initializes
10✔
163
    this._processWidgets(this._props.widgets);
10✔
164

10✔
165
    this._deck = getDeckInstance({
10✔
166
      map,
10✔
167
      gl,
10✔
168
      deck: new Deck({
10✔
169
        ...this._props,
10✔
170
        gl,
10✔
171
        parameters: {...getDefaultParameters(map, true), ...this._props.parameters}
10✔
172
      })
10✔
173
    });
10✔
174

10✔
175
    map.on('styledata', this._handleStyleChange);
10✔
176
    this._resolveLayers(map, this._deck, [], this._props.layers);
10✔
177

10✔
178
    return document.createElement('div');
10✔
179
  }
10✔
180

1✔
181
  private _resolveLayers(
1✔
182
    map: Map | undefined,
32✔
183
    deck: Deck | undefined,
32✔
184
    prevLayers: LayersList | undefined,
32✔
185
    newLayers: LayersList | undefined
32✔
186
  ): void {
32✔
187
    if (this._renderLayersInGroups) {
32✔
188
      resolveLayerGroups(map, prevLayers, newLayers);
8✔
189
    } else {
32✔
190
      resolveLayers(map, deck, prevLayers, newLayers);
24✔
191
    }
24✔
192
  }
32✔
193

1✔
194
  /**
1✔
195
   * Process widgets and wrap those with viewId: 'mapbox' as IControls.
1✔
196
   * This enables deck widgets to be positioned in Mapbox's control container
1✔
197
   * alongside native map controls, preventing overlap.
1✔
198
   */
1✔
199
  private _processWidgets(widgets: Widget[] | undefined): void {
1✔
200
    const map = this._map;
12✔
201
    if (!map) return;
12!
202

12✔
203
    // Remove old widget controls
12✔
204
    for (const control of this._widgetControls) {
12!
NEW
205
      map.removeControl(control);
×
NEW
206
    }
×
207
    this._widgetControls = [];
12✔
208

12✔
209
    if (!widgets) return;
12!
NEW
210

×
NEW
211
    // Wrap widgets with viewId: 'mapbox' as IControls
×
NEW
212
    for (const widget of widgets) {
×
NEW
213
      if (widget.viewId === 'mapbox') {
×
NEW
214
        const control = new DeckWidgetControl(widget);
×
NEW
215
        // Add to map - this calls onAdd() synchronously, setting _container
×
NEW
216
        // Use control.getDefaultPosition() which handles 'fill' -> 'top-left' conversion
×
NEW
217
        map.addControl(control, control.getDefaultPosition());
×
NEW
218
        this._widgetControls.push(control);
×
NEW
219
      }
×
NEW
220
    }
×
221
  }
12✔
222

1✔
223
  /** Called when the control is removed from a map */
1✔
224
  onRemove(): void {
1✔
225
    const map = this._map;
12✔
226

12✔
227
    if (map) {
12✔
228
      // Remove widget controls
12✔
229
      for (const control of this._widgetControls) {
12!
NEW
230
        map.removeControl(control);
×
NEW
231
      }
×
232
      this._widgetControls = [];
12✔
233

12✔
234
      if (this._interleaved) {
12✔
235
        this._onRemoveInterleaved(map);
10✔
236
      } else {
12✔
237
        this._onRemoveOverlaid(map);
2✔
238
      }
2✔
239
    }
12✔
240

12✔
241
    this._deck = undefined;
12✔
242
    this._map = undefined;
12✔
243
    this._container = undefined;
12✔
244
  }
12✔
245

1✔
246
  private _onRemoveOverlaid(map: Map): void {
1✔
247
    map.off('resize', this._updateContainerSize);
2✔
248
    map.off('render', this._updateViewState);
2✔
249
    map.off('mousedown', this._handleMouseEvent);
2✔
250
    map.off('dragstart', this._handleMouseEvent);
2✔
251
    map.off('drag', this._handleMouseEvent);
2✔
252
    map.off('dragend', this._handleMouseEvent);
2✔
253
    map.off('mousemove', this._handleMouseEvent);
2✔
254
    map.off('mouseout', this._handleMouseEvent);
2✔
255
    map.off('click', this._handleMouseEvent);
2✔
256
    map.off('dblclick', this._handleMouseEvent);
2✔
257
    this._deck?.finalize();
2✔
258
  }
2✔
259

1✔
260
  private _onRemoveInterleaved(map: Map): void {
1✔
261
    map.off('styledata', this._handleStyleChange);
10✔
262
    this._resolveLayers(map, this._deck, this._props.layers, []);
10✔
263
    removeDeckInstance(map);
10✔
264
  }
10✔
265

1✔
266
  getDefaultPosition(): ControlPosition {
1✔
UNCOV
267
    return 'top-left';
×
UNCOV
268
  }
×
269

1✔
270
  /** Forwards the Deck.pickObject method */
1✔
271
  pickObject(params: Parameters<Deck['pickObject']>[0]): ReturnType<Deck['pickObject']> {
1✔
UNCOV
272
    assert(this._deck);
×
UNCOV
273
    return this._deck.pickObject(params);
×
UNCOV
274
  }
×
275

1✔
276
  /** Forwards the Deck.pickMultipleObjects method */
1✔
277
  pickMultipleObjects(
1✔
UNCOV
278
    params: Parameters<Deck['pickMultipleObjects']>[0]
×
UNCOV
279
  ): ReturnType<Deck['pickMultipleObjects']> {
×
UNCOV
280
    assert(this._deck);
×
UNCOV
281
    return this._deck.pickMultipleObjects(params);
×
UNCOV
282
  }
×
283

1✔
284
  /** Forwards the Deck.pickObjects method */
1✔
285
  pickObjects(params: Parameters<Deck['pickObjects']>[0]): ReturnType<Deck['pickObjects']> {
1✔
UNCOV
286
    assert(this._deck);
×
UNCOV
287
    return this._deck.pickObjects(params);
×
UNCOV
288
  }
×
289

1✔
290
  /** Remove from map and releases all resources */
1✔
291
  finalize() {
1✔
292
    if (this._map) {
1✔
293
      this._map.removeControl(this);
1✔
294
    }
1✔
295
  }
1✔
296

1✔
297
  /** If interleaved: true, returns base map's canvas, otherwise forwards the Deck.getCanvas method. */
1✔
298
  getCanvas(): HTMLCanvasElement | null {
1✔
299
    if (!this._map) {
×
UNCOV
300
      return null;
×
UNCOV
301
    }
×
UNCOV
302
    return this._interleaved ? this._map.getCanvas() : this._deck!.getCanvas();
×
303
  }
×
304

1✔
305
  private _handleStyleChange = () => {
1✔
306
    this._resolveLayers(this._map, this._deck, this._props.layers, this._props.layers);
8✔
307
    if (!this._map) return;
8!
308

8✔
309
    // getProjection() returns undefined before style is loaded
8✔
310
    const projection = getProjection(this._map);
8✔
311
    if (projection && !this._props.views) {
8✔
312
      this._deck?.setProps({views: getDefaultView(this._map)});
8✔
313
    }
8✔
314
  };
8✔
315

1✔
316
  private _updateContainerSize = () => {
1✔
317
    if (this._map && this._container) {
2✔
318
      const {clientWidth, clientHeight} = this._map.getContainer();
2✔
319
      Object.assign(this._container.style, {
2✔
320
        width: `${clientWidth}px`,
2✔
321
        height: `${clientHeight}px`
2✔
322
      });
2✔
323
    }
2✔
324
  };
2✔
325

1✔
326
  private _updateViewState = () => {
1✔
327
    const deck = this._deck;
2✔
328
    const map = this._map;
2✔
329
    if (deck && map) {
2✔
330
      deck.setProps({
2✔
331
        views: this._props.views || getDefaultView(map),
2✔
332
        viewState: getViewState(map)
2✔
333
      });
2✔
334
      // Redraw immediately if view state has changed
2✔
335
      if (deck.isInitialized) {
2!
UNCOV
336
        deck.redraw();
×
UNCOV
337
      }
×
338
    }
2✔
339
  };
2✔
340

1✔
341
  // eslint-disable-next-line complexity
1✔
342
  private _handleMouseEvent = (event: MapMouseEvent) => {
1✔
UNCOV
343
    const deck = this._deck;
×
UNCOV
344
    if (!deck || !deck.isInitialized) {
×
UNCOV
345
      return;
×
UNCOV
346
    }
×
UNCOV
347

×
UNCOV
348
    const mockEvent: {
×
UNCOV
349
      type: string;
×
UNCOV
350
      deltaX?: number;
×
UNCOV
351
      deltaY?: number;
×
UNCOV
352
      offsetCenter: {x: number; y: number};
×
UNCOV
353
      srcEvent: MapMouseEvent;
×
UNCOV
354
      tapCount?: number;
×
UNCOV
355
    } = {
×
UNCOV
356
      type: event.type,
×
UNCOV
357
      offsetCenter: event.point,
×
UNCOV
358
      srcEvent: event
×
UNCOV
359
    };
×
UNCOV
360

×
361
    const lastDown = this._lastMouseDownPoint;
×
362
    if (!event.point && lastDown) {
×
UNCOV
363
      // drag* events do not contain a `point` field
×
UNCOV
364
      mockEvent.deltaX = event.originalEvent.clientX - lastDown.clientX;
×
UNCOV
365
      mockEvent.deltaY = event.originalEvent.clientY - lastDown.clientY;
×
UNCOV
366
      mockEvent.offsetCenter = {
×
UNCOV
367
        x: lastDown.x + mockEvent.deltaX,
×
368
        y: lastDown.y + mockEvent.deltaY
×
369
      };
×
370
    }
×
371

×
372
    switch (mockEvent.type) {
×
373
      case 'mousedown':
×
374
        deck._onPointerDown(mockEvent as unknown as MjolnirPointerEvent);
×
375
        this._lastMouseDownPoint = {
×
376
          ...event.point,
×
377
          clientX: event.originalEvent.clientX,
×
378
          clientY: event.originalEvent.clientY
×
379
        };
×
380
        break;
×
381

×
382
      case 'dragstart':
×
383
        mockEvent.type = 'panstart';
×
384
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
385
        break;
×
386

×
387
      case 'drag':
×
388
        mockEvent.type = 'panmove';
×
389
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
390
        break;
×
391

×
392
      case 'dragend':
×
393
        mockEvent.type = 'panend';
×
394
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
395
        break;
×
396

×
397
      case 'click':
×
398
        mockEvent.tapCount = 1;
×
399
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
400
        break;
×
401

×
402
      case 'dblclick':
×
403
        mockEvent.type = 'click';
×
404
        mockEvent.tapCount = 2;
×
405
        deck._onEvent(mockEvent as unknown as MjolnirGestureEvent);
×
406
        break;
×
407

×
408
      case 'mousemove':
×
409
        mockEvent.type = 'pointermove';
×
410
        deck._onPointerMove(mockEvent as unknown as MjolnirPointerEvent);
×
411
        break;
×
412

×
413
      case 'mouseout':
×
414
        mockEvent.type = 'pointerleave';
×
415
        deck._onPointerMove(mockEvent as unknown as MjolnirPointerEvent);
×
416
        break;
×
417

×
418
      default:
×
419
        return;
×
420
    }
×
421
  };
×
422
}
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