• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

visgl / deck.gl / 21049200541

15 Jan 2026 10:53PM UTC coverage: 90.937% (-0.2%) from 91.1%
21049200541

Pull #9939

github

web-flow
Merge 683e9f44b into 1975e2367
Pull Request #9939: fix(mapbox): MapboxOverlay: render deck layers in batches

6845 of 7515 branches covered (91.08%)

Branch coverage included in aggregate %.

79 of 206 new or added lines in 4 files covered. (38.35%)

4 existing lines in 1 file now uncovered.

56728 of 62394 relevant lines covered (90.92%)

14377.74 hits per line

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

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

1✔
5
import {Deck, MapView, _GlobeView as GlobeView, _flatten as flatten} from '@deck.gl/core';
1✔
6
import type {Viewport, MapViewState, Layer} from '@deck.gl/core';
1✔
7
import type {Parameters} from '@luma.gl/core';
1✔
8
import type MapboxLayer from './mapbox-layer';
1✔
9
import type MapboxLayerGroup from './mapbox-layer-group';
1✔
10
import type {Map, OverlayLayer} from './types';
1✔
11

1✔
12
import {lngLatToWorld, unitsPerMeter} from '@math.gl/web-mercator';
1✔
13

1✔
14
const MAPBOX_VIEW_ID = 'mapbox';
1✔
15

1✔
16
type UserData = {
1✔
17
  isExternal: boolean;
1✔
18
  currentViewport?: Viewport | null;
1✔
19
  mapboxLayers: Set<MapboxLayer<any>>;
1✔
20
  // mapboxVersion: {minor: number; major: number};
1✔
21
};
1✔
22

1✔
23
// Mercator constants
1✔
24
const TILE_SIZE = 512;
1✔
25
const DEGREES_TO_RADIANS = Math.PI / 180;
1✔
26

1✔
27
// Create an interleaved deck instance.
1✔
28
export function getDeckInstance({
1✔
29
  map,
28✔
30
  gl,
28✔
31
  deck
28✔
32
}: {
28✔
33
  map: Map & {__deck?: Deck<any> | null};
28✔
34
  gl: WebGL2RenderingContext;
28✔
35
  deck?: Deck<any>;
28✔
36
}): Deck<any> {
28✔
37
  // Only create one deck instance per context
28✔
38
  if (map.__deck) {
28✔
39
    return map.__deck;
15✔
40
  }
15✔
41

13✔
42
  // Only initialize certain props once per context
13✔
43
  const customRender = deck?.props._customRender;
28✔
44
  const onLoad = deck?.props.onLoad;
28✔
45

28✔
46
  const deckProps = {
28✔
47
    ...deck?.props,
28✔
48
    _customRender: () => {
28✔
49
      map.triggerRepaint();
11✔
50
      // customRender may be subscribed by DeckGL React component to update child props
11✔
51
      // make sure it is still called
11✔
52
      // Hack - do not pass a redraw reason here to prevent the React component from clearing the context
11✔
53
      // Rerender will be triggered by MapboxLayer's render()
11✔
54
      customRender?.('');
11!
55
    }
11✔
56
  };
28✔
57
  deckProps.parameters = {...getDefaultParameters(map, true), ...deckProps.parameters};
28✔
58
  deckProps.views ||= getDefaultView(map);
28✔
59

28✔
60
  let deckInstance: Deck;
28✔
61

28✔
62
  if (!deck || deck.props.gl === gl) {
28✔
63
    // If deck isn't defined (Internal MapboxLayer use case),
9✔
64
    // or if deck is defined and is using the WebGLContext created by mapbox (MapboxOverlay and External MapboxLayer use case),
9✔
65
    // block deck from setting the canvas size, and use the map's viewState to drive deck.
9✔
66
    // Otherwise, we use deck's viewState to drive the map.
9✔
67
    Object.assign(deckProps, {
9✔
68
      gl,
9✔
69
      width: null,
9✔
70
      height: null,
9✔
71
      touchAction: 'unset',
9✔
72
      viewState: getViewState(map)
9✔
73
    });
9✔
74
    if (deck?.isInitialized) {
9!
75
      watchMapMove(deck, map);
×
76
    } else {
9✔
77
      deckProps.onLoad = () => {
9✔
78
        onLoad?.();
6✔
79
        watchMapMove(deckInstance, map);
6✔
80
      };
6✔
81
    }
9✔
82
  }
9✔
83

13✔
84
  if (deck) {
28✔
85
    deckInstance = deck;
12✔
86
    deck.setProps(deckProps);
12✔
87
    (deck.userData as UserData).isExternal = true;
12✔
88
  } else {
28✔
89
    deckInstance = new Deck(deckProps);
1✔
90
    map.on('remove', () => {
1✔
91
      removeDeckInstance(map);
1✔
92
    });
1✔
93
  }
1✔
94

13✔
95
  (deckInstance.userData as UserData).mapboxLayers = new Set();
13✔
96
  // (deckInstance.userData as UserData).mapboxVersion = getMapboxVersion(map);
13✔
97
  map.__deck = deckInstance;
13✔
98
  map.on('render', () => {
13✔
99
    if (deckInstance.isInitialized) afterRender(deckInstance, map);
15✔
100
  });
13✔
101

13✔
102
  return deckInstance;
13✔
103
}
13✔
104

1✔
105
function watchMapMove(deck: Deck, map: Map & {__deck?: Deck | null}) {
6✔
106
  const _handleMapMove = () => {
6✔
107
    if (deck.isInitialized) {
1!
108
      // call view state methods
×
109
      onMapMove(deck, map);
×
110
    } else {
1✔
111
      // deregister itself when deck is finalized
1✔
112
      map.off('move', _handleMapMove);
1✔
113
    }
1✔
114
  };
1✔
115
  map.on('move', _handleMapMove);
6✔
116
}
6✔
117

1✔
118
export function removeDeckInstance(map: Map & {__deck?: Deck | null}) {
1✔
119
  map.__deck?.finalize();
9✔
120
  map.__deck = null;
9✔
121
}
9✔
122

1✔
123
export function getDefaultParameters(map: Map, interleaved: boolean): Parameters {
1✔
124
  const result: Parameters = interleaved
27✔
125
    ? {
23✔
126
        depthWriteEnabled: true,
23✔
127
        depthCompare: 'less-equal',
23✔
128
        depthBias: 0,
23✔
129
        blend: true,
23✔
130
        blendColorSrcFactor: 'src-alpha',
23✔
131
        blendColorDstFactor: 'one-minus-src-alpha',
23✔
132
        blendAlphaSrcFactor: 'one',
23✔
133
        blendAlphaDstFactor: 'one-minus-src-alpha',
23✔
134
        blendColorOperation: 'add',
23✔
135
        blendAlphaOperation: 'add'
23✔
136
      }
23✔
137
    : {};
4✔
138
  if (getProjection(map) === 'globe') {
27✔
139
    result.cullMode = 'back';
2✔
140
  }
2✔
141
  return result;
27✔
142
}
27✔
143

1✔
144
export function addLayer(deck: Deck, layer: MapboxLayer<any>): void {
1✔
145
  (deck.userData as UserData).mapboxLayers.add(layer);
20✔
146
  updateLayers(deck);
20✔
147
}
20✔
148

1✔
149
export function removeLayer(deck: Deck, layer: MapboxLayer<any>): void {
1✔
150
  (deck.userData as UserData).mapboxLayers.delete(layer);
13✔
151
  updateLayers(deck);
13✔
152
}
13✔
153

1✔
154
export function updateLayer(deck: Deck, layer: MapboxLayer<any>): void {
1✔
155
  updateLayers(deck);
13✔
156
}
13✔
157

1✔
158
export function drawLayer(
1✔
159
  deck: Deck,
11✔
160
  map: Map,
11✔
161
  layer: MapboxLayer<any>,
11✔
162
  renderParameters: any
11✔
163
): void {
11✔
164
  let {currentViewport} = deck.userData as UserData;
11✔
165
  let clearStack: boolean = false;
11✔
166
  if (!currentViewport) {
11✔
167
    // This is the first layer drawn in this render cycle.
10✔
168
    // Generate viewport from the current map state.
10✔
169
    currentViewport = getViewport(deck, map, renderParameters);
10✔
170
    (deck.userData as UserData).currentViewport = currentViewport;
10✔
171
    clearStack = true;
10✔
172
  }
10✔
173

11✔
174
  if (!deck.isInitialized) {
11✔
175
    return;
1✔
176
  }
1✔
177

10✔
178
  deck._drawLayers('mapbox-repaint', {
10✔
179
    viewports: [currentViewport],
10✔
180
    layerFilter: params =>
10✔
181
      (!deck.props.layerFilter || deck.props.layerFilter(params)) &&
13✔
182
      (layer.id === params.layer.id || params.layer.props.operation.includes('terrain')),
10✔
183

10✔
184
    clearStack,
10✔
185
    clearCanvas: false
10✔
186
  });
10✔
187
}
10✔
188

1✔
189
export function drawLayerGroup(
1✔
NEW
190
  deck: Deck,
×
NEW
191
  map: Map,
×
NEW
192
  group: MapboxLayerGroup,
×
NEW
193
  renderParameters: any
×
NEW
194
): void {
×
NEW
195
  let {currentViewport} = deck.userData as UserData;
×
NEW
196
  let clearStack: boolean = false;
×
NEW
197
  if (!currentViewport) {
×
NEW
198
    // This is the first layer drawn in this render cycle.
×
NEW
199
    // Generate viewport from the current map state.
×
NEW
200
    currentViewport = getViewport(deck, map, renderParameters);
×
NEW
201
    (deck.userData as UserData).currentViewport = currentViewport;
×
NEW
202
    clearStack = true;
×
NEW
203
  }
×
NEW
204

×
NEW
205
  if (!deck.isInitialized) {
×
NEW
206
    return;
×
NEW
207
  }
×
NEW
208

×
NEW
209
  deck._drawLayers('mapbox-repaint', {
×
NEW
210
    viewports: [currentViewport],
×
NEW
211
    layerFilter: params => {
×
NEW
212
      if (deck.props.layerFilter && !deck.props.layerFilter(params)) {
×
NEW
213
        return false;
×
NEW
214
      }
×
NEW
215

×
NEW
216
      const layer = params.layer as OverlayLayer;
×
NEW
217
      if (group.beforeId && layer.props.beforeId === group.beforeId) {
×
NEW
218
        return true;
×
NEW
219
      }
×
NEW
220
      if (group.slot && layer.props.slot === group.slot) {
×
NEW
221
        return true;
×
NEW
222
      }
×
NEW
223
      if (layer.props.operation.includes('terrain')) {
×
NEW
224
        return true;
×
NEW
225
      }
×
NEW
226
      return false;
×
NEW
227
    },
×
UNCOV
228
    clearStack,
×
UNCOV
229
    clearCanvas: false
×
UNCOV
230
  });
×
UNCOV
231
}
×
232

1✔
233
export function getProjection(map: Map): 'mercator' | 'globe' {
1✔
234
  const projection = map.getProjection?.();
55✔
235
  const type =
55✔
236
    // maplibre projection spec
55✔
237
    projection?.type ||
55!
238
    // mapbox projection spec
×
239
    projection?.name;
×
240
  if (type === 'globe') {
55✔
241
    return 'globe';
5✔
242
  }
5✔
243
  if (type && type !== 'mercator') {
55!
244
    throw new Error('Unsupported projection');
×
245
  }
✔
246
  return 'mercator';
50✔
247
}
50✔
248

1✔
249
export function getDefaultView(map: Map): GlobeView | MapView {
1✔
250
  if (getProjection(map) === 'globe') {
22✔
251
    return new GlobeView({id: MAPBOX_VIEW_ID});
2✔
252
  }
2✔
253
  return new MapView({id: MAPBOX_VIEW_ID});
20✔
254
}
20✔
255

1✔
256
export function getViewState(map: Map): MapViewState & {
1✔
257
  repeat: boolean;
24✔
258
  padding: {
24✔
259
    left: number;
24✔
260
    right: number;
24✔
261
    top: number;
24✔
262
    bottom: number;
24✔
263
  };
24✔
264
} {
24✔
265
  const {lng, lat} = map.getCenter();
24✔
266

24✔
267
  const viewState: MapViewState & {
24✔
268
    repeat: boolean;
24✔
269
    padding: {
24✔
270
      left: number;
24✔
271
      right: number;
24✔
272
      top: number;
24✔
273
      bottom: number;
24✔
274
    };
24✔
275
  } = {
24✔
276
    // Longitude returned by getCenter can be outside of [-180, 180] when zooming near the anti meridian
24✔
277
    // https://github.com/visgl/deck.gl/issues/6894
24✔
278
    longitude: ((lng + 540) % 360) - 180,
24✔
279
    latitude: lat,
24✔
280
    zoom: map.getZoom(),
24✔
281
    bearing: map.getBearing(),
24✔
282
    pitch: map.getPitch(),
24✔
283
    padding: map.getPadding(),
24✔
284
    repeat: map.getRenderWorldCopies()
24✔
285
  };
24✔
286

24✔
287
  if (map.getTerrain?.()) {
24!
288
    // When the base map has terrain, we need to target the camera at the terrain surface
×
289
    centerCameraOnTerrain(map, viewState);
×
290
  }
×
291

24✔
292
  return viewState;
24✔
293
}
24✔
294

1✔
295
function centerCameraOnTerrain(map: Map, viewState: MapViewState) {
×
296
  if (map.getFreeCameraOptions) {
×
297
    // mapbox-gl v2
×
298
    const {position} = map.getFreeCameraOptions();
×
299
    if (!position || position.z === undefined) {
×
300
      return;
×
301
    }
×
302

×
303
    // @ts-ignore transform is not typed
×
304
    const height = map.transform.height;
×
305
    const {longitude, latitude, pitch} = viewState;
×
306

×
307
    // Convert mapbox mercator coordinate to deck common space
×
308
    const cameraX = position.x * TILE_SIZE;
×
309
    const cameraY = (1 - position.y) * TILE_SIZE;
×
310
    const cameraZ = position.z * TILE_SIZE;
×
311

×
312
    // Mapbox manipulates zoom in terrain mode, see discussion here: https://github.com/mapbox/mapbox-gl-js/issues/12040
×
313
    const center = lngLatToWorld([longitude, latitude]);
×
314
    const dx = cameraX - center[0];
×
315
    const dy = cameraY - center[1];
×
316
    const cameraToCenterDistanceGround = Math.sqrt(dx * dx + dy * dy);
×
317

×
318
    const pitchRadians = pitch! * DEGREES_TO_RADIANS;
×
319
    const altitudePixels = 1.5 * height;
×
320
    const scale =
×
321
      pitchRadians < 0.001
×
322
        ? // Pitch angle too small to deduce the look at point, assume elevation is 0
×
323
          (altitudePixels * Math.cos(pitchRadians)) / cameraZ
×
324
        : (altitudePixels * Math.sin(pitchRadians)) / cameraToCenterDistanceGround;
×
325
    viewState.zoom = Math.log2(scale);
×
326

×
327
    const cameraZFromSurface = (altitudePixels * Math.cos(pitchRadians)) / scale;
×
328
    const surfaceElevation = cameraZ - cameraZFromSurface;
×
329
    viewState.position = [0, 0, surfaceElevation / unitsPerMeter(latitude)];
×
330
  }
×
331
  // @ts-ignore transform is not typed
×
332
  else if (typeof map.transform.elevation === 'number') {
×
333
    // maplibre-gl
×
334
    // @ts-ignore transform is not typed
×
335
    viewState.position = [0, 0, map.transform.elevation];
×
336
  }
×
337
}
×
338

1✔
339
// Since maplibre-gl@5
1✔
340
// https://github.com/maplibre/maplibre-gl-js/blob/main/src/style/style_layer/custom_style_layer.ts
1✔
341
type MaplibreRenderParameters = {
1✔
342
  farZ: number;
1✔
343
  nearZ: number;
1✔
344
  fov: number;
1✔
345
  modelViewProjectionMatrix: number[];
1✔
346
  projectionMatrix: number[];
1✔
347
};
1✔
348

1✔
349
function getViewport(deck: Deck, map: Map, renderParameters?: unknown): Viewport {
11✔
350
  const viewState = getViewState(map);
11✔
351
  const {views} = deck.props;
11✔
352
  const view =
11✔
353
    (views && flatten(views).find((v: {id: string}) => v.id === MAPBOX_VIEW_ID)) ||
11✔
354
    getDefaultView(map);
1✔
355

11✔
356
  if (renderParameters) {
11!
357
    // Called from MapboxLayer.render
×
358
    // Magic number, matches mapbox-gl@>=1.3.0's projection matrix
×
359
    view.props.nearZMultiplier = 0.2;
×
360
  }
×
361

11✔
362
  // Get the base map near/far plane
11✔
363
  // renderParameters is maplibre API but not mapbox
11✔
364
  // Transform is not an official API, properties could be undefined for older versions
11✔
365
  const nearZ = (renderParameters as MaplibreRenderParameters)?.nearZ ?? map.transform._nearZ;
11!
366
  const farZ = (renderParameters as MaplibreRenderParameters)?.farZ ?? map.transform._farZ;
11!
367
  if (Number.isFinite(nearZ)) {
11!
368
    viewState.nearZ = nearZ / map.transform.height;
×
369
    viewState.farZ = farZ / map.transform.height;
×
370
  }
×
371
  // Otherwise fallback to default calculation using nearZMultiplier/farZMultiplier
11✔
372

11✔
373
  return view.makeViewport({
11✔
374
    width: deck.width,
11✔
375
    height: deck.height,
11✔
376
    viewState
11✔
377
  }) as Viewport;
11✔
378
}
11✔
379

1✔
380
function afterRender(deck: Deck, map: Map): void {
13✔
381
  const {mapboxLayers, isExternal} = deck.userData as UserData;
13✔
382

13✔
383
  if (isExternal) {
13✔
384
    // Draw non-Mapbox layers
12✔
385
    const mapboxLayerIds = Array.from(mapboxLayers, layer => layer.id);
12✔
386
    const deckLayers = flatten(deck.props.layers, Boolean) as Layer[];
12✔
387
    const hasNonMapboxLayers = deckLayers.some(
12✔
388
      layer => layer && !mapboxLayerIds.includes(layer.id)
12✔
389
    );
12✔
390
    let viewports = deck.getViewports();
12✔
391
    const mapboxViewportIdx = viewports.findIndex(vp => vp.id === MAPBOX_VIEW_ID);
12✔
392
    const hasNonMapboxViews = viewports.length > 1 || mapboxViewportIdx < 0;
12✔
393

12✔
394
    if (hasNonMapboxLayers || hasNonMapboxViews) {
12✔
395
      if (mapboxViewportIdx >= 0) {
2✔
396
        viewports = viewports.slice();
1✔
397
        viewports[mapboxViewportIdx] = getViewport(deck, map);
1✔
398
      }
1✔
399

2✔
400
      deck._drawLayers('mapbox-repaint', {
2✔
401
        viewports,
2✔
402
        layerFilter: params =>
2✔
403
          (!deck.props.layerFilter || deck.props.layerFilter(params)) &&
5✔
404
          (params.viewport.id !== MAPBOX_VIEW_ID || !mapboxLayerIds.includes(params.layer.id)),
3✔
405
        clearCanvas: false
2✔
406
      });
2✔
407
    }
2✔
408
  }
12✔
409

13✔
410
  // End of render cycle, clear generated viewport
13✔
411
  (deck.userData as UserData).currentViewport = null;
13✔
412
}
13✔
413

1✔
414
function onMapMove(deck: Deck, map: Map): void {
×
415
  deck.setProps({
×
416
    viewState: getViewState(map)
×
417
  });
×
418
  // Camera changed, will trigger a map repaint right after this
×
419
  // Clear any change flag triggered by setting viewState so that deck does not request
×
420
  // a second repaint
×
421
  deck.needsRedraw({clearRedrawFlags: true});
×
422
}
×
423

1✔
424
function updateLayers(deck: Deck): void {
46✔
425
  if ((deck.userData as UserData).isExternal) {
46✔
426
    return;
42✔
427
  }
42✔
428

4✔
429
  const layers: Layer[] = [];
4✔
430
  (deck.userData as UserData).mapboxLayers.forEach(deckLayer => {
4✔
431
    const LayerType = deckLayer.props.type;
3✔
432
    const layer = new LayerType(deckLayer.props);
3✔
433
    layers.push(layer);
3✔
434
  });
4✔
435
  deck.setProps({layers});
4✔
436
}
4✔
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