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

visgl / deck.gl / 21416546016

27 Jan 2026 10:23PM UTC coverage: 91.118% (+0.02%) from 91.101%
21416546016

Pull #9963

github

web-flow
Merge 61466c357 into 914c3d2c2
Pull Request #9963: feat(mapbox): Add widget support to MapboxOverlay via IControl adapter

6874 of 7552 branches covered (91.02%)

Branch coverage included in aggregate %.

128 of 132 new or added lines in 2 files covered. (96.97%)

37 existing lines in 1 file now uncovered.

56927 of 62468 relevant lines covered (91.13%)

14363.35 hits per line

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

76.91
/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
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

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
  /** IControl wrappers for widgets with viewId: 'mapbox' */
1✔
60
  private _widgetControls: DeckWidgetControl[] = [];
1✔
61

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

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

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

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

8✔
91
    Object.assign(this._props, this.filterProps(props));
8✔
92

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

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

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

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

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

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

6✔
147
    this._updateContainerSize();
6✔
148
    return container;
6✔
149
  }
6✔
150

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

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

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

11✔
173
    map.on('styledata', this._handleStyleChange);
11✔
174
    this._resolveLayers(map, this._deck, [], this._props.layers);
11✔
175

11✔
176
    return document.createElement('div');
11✔
177
  }
11✔
178

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

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

19✔
201
    // Remove old widget controls
19✔
202
    for (const control of this._widgetControls) {
19✔
203
      map.removeControl(control);
2✔
204
    }
2✔
205
    this._widgetControls = [];
19✔
206

19✔
207
    if (!widgets) return;
19✔
208

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

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

17✔
225
    if (map) {
17✔
226
      // Remove widget controls
17✔
227
      for (const control of this._widgetControls) {
17✔
228
        map.removeControl(control);
4✔
229
      }
4✔
230
      this._widgetControls = [];
17✔
231

17✔
232
      if (this._interleaved) {
17✔
233
        this._onRemoveInterleaved(map);
11✔
234
      } else {
17✔
235
        this._onRemoveOverlaid(map);
6✔
236
      }
6✔
237
    }
17✔
238

17✔
239
    this._deck = undefined;
17✔
240
    this._map = undefined;
17✔
241
    this._container = undefined;
17✔
242
  }
17✔
243

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

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

1✔
264
  getDefaultPosition(): ControlPosition {
1✔
265
    return 'top-left';
17✔
266
  }
17✔
267

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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