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

visgl / deck.gl / 21240525729

22 Jan 2026 07:56AM UTC coverage: 91.103% (-0.01%) from 91.115%
21240525729

Pull #9955

github

web-flow
Merge 4d5321543 into 83ac41278
Pull Request #9955: chore(mapbox): Remove internal deck mode from MapboxLayer

6861 of 7538 branches covered (91.02%)

Branch coverage included in aggregate %.

63 of 63 new or added lines in 4 files covered. (100.0%)

2 existing lines in 1 file now uncovered.

56821 of 62363 relevant lines covered (91.11%)

14386.52 hits per line

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

79.82
/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 {LayerOverlayProps, Map} 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
  currentViewport?: Viewport | null;
1✔
18
  mapboxLayers: Set<MapboxLayer<any>>;
1✔
19
  // mapboxVersion: {minor: number; major: number};
1✔
20
};
1✔
21

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

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

14✔
41
  // Only initialize certain props once per context
14✔
42
  const customRender = deck.props._customRender;
14✔
43
  const onLoad = deck.props.onLoad;
14✔
44

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

14✔
59
  if (deck.props.gl === gl) {
14✔
60
    // deck is using the WebGLContext created by mapbox,
12✔
61
    // block deck from setting the canvas size, and use the map's viewState to drive deck.
12✔
62
    // Otherwise, we use deck's viewState to drive the map.
12✔
63
    Object.assign(deckProps, {
12✔
64
      gl,
12✔
65
      width: null,
12✔
66
      height: null,
12✔
67
      touchAction: 'unset',
12✔
68
      viewState: getViewState(map)
12✔
69
    });
12✔
70
    if (deck.isInitialized) {
12!
71
      watchMapMove(deck, map);
×
72
    } else {
12✔
73
      deckProps.onLoad = () => {
12✔
74
        onLoad?.();
10✔
75
        watchMapMove(deck, map);
10✔
76
      };
10✔
77
    }
12✔
78
  }
12✔
79

14✔
80
  deck.setProps(deckProps);
14✔
81

14✔
82
  (deck.userData as UserData).mapboxLayers = new Set();
14✔
83
  map.__deck = deck;
14✔
84
  map.on('render', () => {
14✔
85
    if (deck.isInitialized) afterRender(deck, map);
17✔
86
  });
14✔
87

14✔
88
  return deck;
14✔
89
}
14✔
90

1✔
91
function watchMapMove(deck: Deck, map: Map & {__deck?: Deck | null}) {
10✔
92
  const _handleMapMove = () => {
10✔
93
    if (deck.isInitialized) {
1!
94
      // call view state methods
×
95
      onMapMove(deck, map);
×
96
    } else {
1✔
97
      // deregister itself when deck is finalized
1✔
98
      map.off('move', _handleMapMove);
1✔
99
    }
1✔
100
  };
1✔
101
  map.on('move', _handleMapMove);
10✔
102
}
10✔
103

1✔
104
export function removeDeckInstance(map: Map & {__deck?: Deck | null}) {
1✔
105
  map.__deck?.finalize();
10✔
106
  map.__deck = null;
10✔
107
}
10✔
108

1✔
109
export function getDefaultParameters(map: Map, interleaved: boolean): Parameters {
1✔
110
  const result: Parameters = interleaved
32✔
111
    ? {
28✔
112
        depthWriteEnabled: true,
28✔
113
        depthCompare: 'less-equal',
28✔
114
        depthBias: 0,
28✔
115
        blend: true,
28✔
116
        blendColorSrcFactor: 'src-alpha',
28✔
117
        blendColorDstFactor: 'one-minus-src-alpha',
28✔
118
        blendAlphaSrcFactor: 'one',
28✔
119
        blendAlphaDstFactor: 'one-minus-src-alpha',
28✔
120
        blendColorOperation: 'add',
28✔
121
        blendAlphaOperation: 'add'
28✔
122
      }
28✔
123
    : {};
4✔
124
  if (getProjection(map) === 'globe') {
32✔
125
    result.cullMode = 'back';
2✔
126
  }
2✔
127
  return result;
32✔
128
}
32✔
129

1✔
130
export function addLayer(deck: Deck, layer: MapboxLayer<any>): void {
1✔
131
  (deck.userData as UserData).mapboxLayers.add(layer);
18✔
132
}
18✔
133

1✔
134
export function removeLayer(deck: Deck, layer: MapboxLayer<any>): void {
1✔
135
  (deck.userData as UserData).mapboxLayers.delete(layer);
12✔
136
}
12✔
137

1✔
138
export function drawLayer(
1✔
139
  deck: Deck,
11✔
140
  map: Map,
11✔
141
  layer: MapboxLayer<any>,
11✔
142
  renderParameters: any
11✔
143
): void {
11✔
144
  let {currentViewport} = deck.userData as UserData;
11✔
145
  let clearStack: boolean = false;
11✔
146
  if (!currentViewport) {
11✔
147
    // This is the first layer drawn in this render cycle.
10✔
148
    // Generate viewport from the current map state.
10✔
149
    currentViewport = getViewport(deck, map, renderParameters);
10✔
150
    (deck.userData as UserData).currentViewport = currentViewport;
10✔
151
    clearStack = true;
10✔
152
  }
10✔
153

11✔
154
  if (!deck.isInitialized) {
11✔
155
    return;
1✔
156
  }
1✔
157

10✔
158
  deck._drawLayers('mapbox-repaint', {
10✔
159
    viewports: [currentViewport],
10✔
160
    layerFilter: params =>
10✔
161
      (!deck.props.layerFilter || deck.props.layerFilter(params)) &&
13✔
162
      (layer.id === params.layer.id || params.layer.props.operation.includes('terrain')),
10✔
163
    clearStack,
10✔
164
    clearCanvas: false
10✔
165
  });
10✔
166
}
10✔
167

1✔
168
export function drawLayerGroup(
1✔
169
  deck: Deck,
3✔
170
  map: Map,
3✔
171
  group: MapboxLayerGroup,
3✔
172
  renderParameters: any
3✔
173
): void {
3✔
174
  let {currentViewport} = deck.userData as UserData;
3✔
175
  let clearStack: boolean = false;
3✔
176
  if (!currentViewport) {
3✔
177
    // This is the first layer drawn in this render cycle.
3✔
178
    // Generate viewport from the current map state.
3✔
179
    currentViewport = getViewport(deck, map, renderParameters);
3✔
180
    (deck.userData as UserData).currentViewport = currentViewport;
3✔
181
    clearStack = true;
3✔
182
  }
3✔
183

3✔
184
  if (!deck.isInitialized) {
3!
185
    return;
×
186
  }
×
187

3✔
188
  deck._drawLayers('mapbox-repaint', {
3✔
189
    viewports: [currentViewport],
3✔
190
    layerFilter: params => {
3✔
191
      if (deck.props.layerFilter && !deck.props.layerFilter(params)) {
4!
192
        return false;
×
193
      }
×
194

4✔
195
      const layer = params.layer as Layer<LayerOverlayProps>;
4✔
196
      if (layer.props.beforeId === group.beforeId && layer.props.slot === group.slot) {
4✔
197
        return true;
4✔
198
      }
4!
199
      return false;
×
200
    },
4✔
201
    clearStack,
3✔
202
    clearCanvas: false
3✔
203
  });
3✔
204
}
3✔
205

1✔
206
export function getProjection(map: Map): 'mercator' | 'globe' {
1✔
207
  const projection = map.getProjection?.();
65✔
208
  const type =
65✔
209
    // maplibre projection spec
65✔
210
    projection?.type ||
65!
211
    // mapbox projection spec
×
212
    projection?.name;
×
213
  if (type === 'globe') {
65✔
214
    return 'globe';
5✔
215
  }
5✔
216
  if (type && type !== 'mercator') {
65!
217
    throw new Error('Unsupported projection');
×
218
  }
✔
219
  return 'mercator';
60✔
220
}
60✔
221

1✔
222
export function getDefaultView(map: Map): GlobeView | MapView {
1✔
223
  if (getProjection(map) === 'globe') {
25✔
224
    return new GlobeView({id: MAPBOX_VIEW_ID});
2✔
225
  }
2✔
226
  return new MapView({id: MAPBOX_VIEW_ID});
23✔
227
}
23✔
228

1✔
229
export function getViewState(map: Map): MapViewState & {
1✔
230
  repeat: boolean;
33✔
231
  padding: {
33✔
232
    left: number;
33✔
233
    right: number;
33✔
234
    top: number;
33✔
235
    bottom: number;
33✔
236
  };
33✔
237
} {
33✔
238
  const {lng, lat} = map.getCenter();
33✔
239

33✔
240
  const viewState: MapViewState & {
33✔
241
    repeat: boolean;
33✔
242
    padding: {
33✔
243
      left: number;
33✔
244
      right: number;
33✔
245
      top: number;
33✔
246
      bottom: number;
33✔
247
    };
33✔
248
  } = {
33✔
249
    // Longitude returned by getCenter can be outside of [-180, 180] when zooming near the anti meridian
33✔
250
    // https://github.com/visgl/deck.gl/issues/6894
33✔
251
    longitude: ((lng + 540) % 360) - 180,
33✔
252
    latitude: lat,
33✔
253
    zoom: map.getZoom(),
33✔
254
    bearing: map.getBearing(),
33✔
255
    pitch: map.getPitch(),
33✔
256
    padding: map.getPadding(),
33✔
257
    repeat: map.getRenderWorldCopies()
33✔
258
  };
33✔
259

33✔
260
  if (map.getTerrain?.()) {
33!
261
    // When the base map has terrain, we need to target the camera at the terrain surface
×
262
    centerCameraOnTerrain(map, viewState);
×
263
  }
×
264

33✔
265
  return viewState;
33✔
266
}
33✔
267

1✔
268
function centerCameraOnTerrain(map: Map, viewState: MapViewState) {
×
269
  if (map.getFreeCameraOptions) {
×
270
    // mapbox-gl v2
×
271
    const {position} = map.getFreeCameraOptions();
×
272
    if (!position || position.z === undefined) {
×
273
      return;
×
274
    }
×
275

×
276
    // @ts-ignore transform is not typed
×
277
    const height = map.transform.height;
×
278
    const {longitude, latitude, pitch} = viewState;
×
279

×
280
    // Convert mapbox mercator coordinate to deck common space
×
281
    const cameraX = position.x * TILE_SIZE;
×
282
    const cameraY = (1 - position.y) * TILE_SIZE;
×
283
    const cameraZ = position.z * TILE_SIZE;
×
284

×
285
    // Mapbox manipulates zoom in terrain mode, see discussion here: https://github.com/mapbox/mapbox-gl-js/issues/12040
×
286
    const center = lngLatToWorld([longitude, latitude]);
×
287
    const dx = cameraX - center[0];
×
288
    const dy = cameraY - center[1];
×
289
    const cameraToCenterDistanceGround = Math.sqrt(dx * dx + dy * dy);
×
290

×
291
    const pitchRadians = pitch! * DEGREES_TO_RADIANS;
×
292
    const altitudePixels = 1.5 * height;
×
293
    const scale =
×
294
      pitchRadians < 0.001
×
295
        ? // Pitch angle too small to deduce the look at point, assume elevation is 0
×
296
          (altitudePixels * Math.cos(pitchRadians)) / cameraZ
×
297
        : (altitudePixels * Math.sin(pitchRadians)) / cameraToCenterDistanceGround;
×
298
    viewState.zoom = Math.log2(scale);
×
299

×
300
    const cameraZFromSurface = (altitudePixels * Math.cos(pitchRadians)) / scale;
×
301
    const surfaceElevation = cameraZ - cameraZFromSurface;
×
302
    viewState.position = [0, 0, surfaceElevation / unitsPerMeter(latitude)];
×
303
  }
×
304
  // @ts-ignore transform is not typed
×
305
  else if (typeof map.transform.elevation === 'number') {
×
306
    // maplibre-gl
×
307
    // @ts-ignore transform is not typed
×
308
    viewState.position = [0, 0, map.transform.elevation];
×
309
  }
×
310
}
×
311

1✔
312
// Since maplibre-gl@5
1✔
313
// https://github.com/maplibre/maplibre-gl-js/blob/main/src/style/style_layer/custom_style_layer.ts
1✔
314
type MaplibreRenderParameters = {
1✔
315
  farZ: number;
1✔
316
  nearZ: number;
1✔
317
  fov: number;
1✔
318
  modelViewProjectionMatrix: number[];
1✔
319
  projectionMatrix: number[];
1✔
320
};
1✔
321

1✔
322
function getViewport(deck: Deck, map: Map, renderParameters?: unknown): Viewport {
17✔
323
  const viewState = getViewState(map);
17✔
324
  const {views} = deck.props;
17✔
325
  const view =
17✔
326
    (views && flatten(views).find((v: {id: string}) => v.id === MAPBOX_VIEW_ID)) ||
17✔
327
    getDefaultView(map);
1✔
328

17✔
329
  if (renderParameters) {
17!
330
    // Called from MapboxLayer.render
×
331
    // Magic number, matches mapbox-gl@>=1.3.0's projection matrix
×
332
    view.props.nearZMultiplier = 0.2;
×
333
  }
×
334

17✔
335
  // Get the base map near/far plane
17✔
336
  // renderParameters is maplibre API but not mapbox
17✔
337
  // Transform is not an official API, properties could be undefined for older versions
17✔
338
  const nearZ = (renderParameters as MaplibreRenderParameters)?.nearZ ?? map.transform._nearZ;
17!
339
  const farZ = (renderParameters as MaplibreRenderParameters)?.farZ ?? map.transform._farZ;
17!
340
  if (Number.isFinite(nearZ)) {
17!
341
    viewState.nearZ = nearZ / map.transform.height;
×
342
    viewState.farZ = farZ / map.transform.height;
×
343
  }
×
344
  // Otherwise fallback to default calculation using nearZMultiplier/farZMultiplier
17✔
345

17✔
346
  return view.makeViewport({
17✔
347
    width: deck.width,
17✔
348
    height: deck.height,
17✔
349
    viewState
17✔
350
  }) as Viewport;
17✔
351
}
17✔
352

1✔
353
function afterRender(deck: Deck, map: Map): void {
16✔
354
  const {mapboxLayers} = deck.userData as UserData;
16✔
355

16✔
356
  // Draw non-Mapbox layers
16✔
357
  const mapboxLayerIds = Array.from(mapboxLayers, layer => layer.id);
16✔
358
  const deckLayers = flatten(deck.props.layers, Boolean) as Layer[];
16✔
359
  const hasNonMapboxLayers = deckLayers.some(layer => layer && !mapboxLayerIds.includes(layer.id));
16✔
360
  let viewports = deck.getViewports();
16✔
361
  const mapboxViewportIdx = viewports.findIndex(vp => vp.id === MAPBOX_VIEW_ID);
16✔
362
  const hasNonMapboxViews = viewports.length > 1 || mapboxViewportIdx < 0;
16✔
363

16✔
364
  if (hasNonMapboxLayers || hasNonMapboxViews) {
16✔
365
    if (mapboxViewportIdx >= 0) {
5✔
366
      viewports = viewports.slice();
4✔
367
      viewports[mapboxViewportIdx] = getViewport(deck, map);
4✔
368
    }
4✔
369

5✔
370
    deck._drawLayers('mapbox-repaint', {
5✔
371
      viewports,
5✔
372
      layerFilter: params =>
5✔
373
        (!deck.props.layerFilter || deck.props.layerFilter(params)) &&
9✔
374
        (params.viewport.id !== MAPBOX_VIEW_ID || !mapboxLayerIds.includes(params.layer.id)),
7✔
375
      clearCanvas: false
5✔
376
    });
5✔
377
  }
5✔
378

16✔
379
  // End of render cycle, clear generated viewport
16✔
380
  (deck.userData as UserData).currentViewport = null;
16✔
381
}
16✔
382

1✔
383
function onMapMove(deck: Deck, map: Map): void {
×
384
  deck.setProps({
×
385
    viewState: getViewState(map)
×
386
  });
×
387
  // Camera changed, will trigger a map repaint right after this
×
388
  // Clear any change flag triggered by setting viewState so that deck does not request
×
389
  // a second repaint
×
390
  deck.needsRedraw({clearRedrawFlags: true});
×
391
}
×
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