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

visgl / deck.gl / 18013722656

25 Sep 2025 04:11PM UTC coverage: 91.129% (+0.02%) from 91.113%
18013722656

push

github

web-flow
fix(maplibre): Update default view once projection is known (#9794)

6797 of 7468 branches covered (91.01%)

Branch coverage included in aggregate %.

10 of 10 new or added lines in 2 files covered. (100.0%)

2 existing lines in 1 file now uncovered.

56165 of 61623 relevant lines covered (91.14%)

14314.66 hits per line

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

82.83
/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 {Map} from './types';
1✔
10

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

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

1✔
15
type UserData = {
1✔
16
  isExternal: boolean;
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,
28✔
29
  gl,
28✔
30
  deck
28✔
31
}: {
28✔
32
  map: Map & {__deck?: Deck<any> | null};
28✔
33
  gl: WebGL2RenderingContext;
28✔
34
  deck?: Deck<any>;
28✔
35
}): Deck<any> {
28✔
36
  // Only create one deck instance per context
28✔
37
  if (map.__deck) {
28✔
38
    return map.__deck;
15✔
39
  }
15✔
40

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

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

28✔
59
  let deckInstance: Deck;
28✔
60

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
187
export function getProjection(map: Map): 'mercator' | 'globe' {
1✔
188
  const projection = map.getProjection?.();
47✔
189
  const type =
47✔
190
    // maplibre projection spec
47✔
191
    projection?.type ||
47!
UNCOV
192
    // mapbox projection spec
×
UNCOV
193
    projection?.name;
×
194
  if (type === 'globe') {
47✔
195
    return 'globe';
4✔
196
  }
4✔
197
  if (type && type !== 'mercator') {
47!
198
    throw new Error('Unsupported projection');
×
199
  }
✔
200
  return 'mercator';
43✔
201
}
43✔
202

1✔
203
export function getDefaultView(map: Map): GlobeView | MapView {
1✔
204
  if (getProjection(map) === 'globe') {
22✔
205
    return new GlobeView({id: MAPBOX_VIEW_ID});
2✔
206
  }
2✔
207
  return new MapView({id: MAPBOX_VIEW_ID});
20✔
208
}
20✔
209

1✔
210
export function getViewState(map: Map): MapViewState & {
1✔
211
  repeat: boolean;
24✔
212
  padding: {
24✔
213
    left: number;
24✔
214
    right: number;
24✔
215
    top: number;
24✔
216
    bottom: number;
24✔
217
  };
24✔
218
} {
24✔
219
  const {lng, lat} = map.getCenter();
24✔
220

24✔
221
  const viewState: MapViewState & {
24✔
222
    repeat: boolean;
24✔
223
    padding: {
24✔
224
      left: number;
24✔
225
      right: number;
24✔
226
      top: number;
24✔
227
      bottom: number;
24✔
228
    };
24✔
229
  } = {
24✔
230
    // Longitude returned by getCenter can be outside of [-180, 180] when zooming near the anti meridian
24✔
231
    // https://github.com/visgl/deck.gl/issues/6894
24✔
232
    longitude: ((lng + 540) % 360) - 180,
24✔
233
    latitude: lat,
24✔
234
    zoom: map.getZoom(),
24✔
235
    bearing: map.getBearing(),
24✔
236
    pitch: map.getPitch(),
24✔
237
    padding: map.getPadding(),
24✔
238
    repeat: map.getRenderWorldCopies()
24✔
239
  };
24✔
240

24✔
241
  if (map.getTerrain?.()) {
24!
242
    // When the base map has terrain, we need to target the camera at the terrain surface
×
243
    centerCameraOnTerrain(map, viewState);
×
244
  }
×
245

24✔
246
  return viewState;
24✔
247
}
24✔
248

1✔
249
function centerCameraOnTerrain(map: Map, viewState: MapViewState) {
×
250
  if (map.getFreeCameraOptions) {
×
251
    // mapbox-gl v2
×
252
    const {position} = map.getFreeCameraOptions();
×
253
    if (!position || position.z === undefined) {
×
254
      return;
×
255
    }
×
256

×
257
    // @ts-ignore transform is not typed
×
258
    const height = map.transform.height;
×
259
    const {longitude, latitude, pitch} = viewState;
×
260

×
261
    // Convert mapbox mercator coordinate to deck common space
×
262
    const cameraX = position.x * TILE_SIZE;
×
263
    const cameraY = (1 - position.y) * TILE_SIZE;
×
264
    const cameraZ = position.z * TILE_SIZE;
×
265

×
266
    // Mapbox manipulates zoom in terrain mode, see discussion here: https://github.com/mapbox/mapbox-gl-js/issues/12040
×
267
    const center = lngLatToWorld([longitude, latitude]);
×
268
    const dx = cameraX - center[0];
×
269
    const dy = cameraY - center[1];
×
270
    const cameraToCenterDistanceGround = Math.sqrt(dx * dx + dy * dy);
×
271

×
272
    const pitchRadians = pitch! * DEGREES_TO_RADIANS;
×
273
    const altitudePixels = 1.5 * height;
×
274
    const scale =
×
275
      pitchRadians < 0.001
×
276
        ? // Pitch angle too small to deduce the look at point, assume elevation is 0
×
277
          (altitudePixels * Math.cos(pitchRadians)) / cameraZ
×
278
        : (altitudePixels * Math.sin(pitchRadians)) / cameraToCenterDistanceGround;
×
279
    viewState.zoom = Math.log2(scale);
×
280

×
281
    const cameraZFromSurface = (altitudePixels * Math.cos(pitchRadians)) / scale;
×
282
    const surfaceElevation = cameraZ - cameraZFromSurface;
×
283
    viewState.position = [0, 0, surfaceElevation / unitsPerMeter(latitude)];
×
284
  }
×
285
  // @ts-ignore transform is not typed
×
286
  else if (typeof map.transform.elevation === 'number') {
×
287
    // maplibre-gl
×
288
    // @ts-ignore transform is not typed
×
289
    viewState.position = [0, 0, map.transform.elevation];
×
290
  }
×
291
}
×
292

1✔
293
// Since maplibre-gl@5
1✔
294
// https://github.com/maplibre/maplibre-gl-js/blob/main/src/style/style_layer/custom_style_layer.ts
1✔
295
type MaplibreRenderParameters = {
1✔
296
  farZ: number;
1✔
297
  nearZ: number;
1✔
298
  fov: number;
1✔
299
  modelViewProjectionMatrix: number[];
1✔
300
  projectionMatrix: number[];
1✔
301
};
1✔
302

1✔
303
function getViewport(deck: Deck, map: Map, renderParameters?: unknown): Viewport {
11✔
304
  const viewState = getViewState(map);
11✔
305
  const {views} = deck.props;
11✔
306
  const view =
11✔
307
    (views && flatten(views).find((v: {id: string}) => v.id === MAPBOX_VIEW_ID)) ||
11✔
308
    getDefaultView(map);
1✔
309

11✔
310
  if (renderParameters) {
11!
311
    // Called from MapboxLayer.render
×
312
    // Magic number, matches mapbox-gl@>=1.3.0's projection matrix
×
313
    view.props.nearZMultiplier = 0.2;
×
314
  }
×
315

11✔
316
  // Get the base map near/far plane
11✔
317
  // renderParameters is maplibre API but not mapbox
11✔
318
  // Transform is not an official API, properties could be undefined for older versions
11✔
319
  const nearZ = (renderParameters as MaplibreRenderParameters)?.nearZ ?? map.transform._nearZ;
11!
320
  const farZ = (renderParameters as MaplibreRenderParameters)?.farZ ?? map.transform._farZ;
11!
321
  if (Number.isFinite(nearZ)) {
11!
322
    viewState.nearZ = nearZ / map.transform.height;
×
323
    viewState.farZ = farZ / map.transform.height;
×
324
  }
×
325
  // Otherwise fallback to default calculation using nearZMultiplier/farZMultiplier
11✔
326

11✔
327
  return view.makeViewport({
11✔
328
    width: deck.width,
11✔
329
    height: deck.height,
11✔
330
    viewState
11✔
331
  }) as Viewport;
11✔
332
}
11✔
333

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

13✔
337
  if (isExternal) {
13✔
338
    // Draw non-Mapbox layers
12✔
339
    const mapboxLayerIds = Array.from(mapboxLayers, layer => layer.id);
12✔
340
    const deckLayers = flatten(deck.props.layers, Boolean) as Layer[];
12✔
341
    const hasNonMapboxLayers = deckLayers.some(
12✔
342
      layer => layer && !mapboxLayerIds.includes(layer.id)
12✔
343
    );
12✔
344
    let viewports = deck.getViewports();
12✔
345
    const mapboxViewportIdx = viewports.findIndex(vp => vp.id === MAPBOX_VIEW_ID);
12✔
346
    const hasNonMapboxViews = viewports.length > 1 || mapboxViewportIdx < 0;
12✔
347

12✔
348
    if (hasNonMapboxLayers || hasNonMapboxViews) {
12✔
349
      if (mapboxViewportIdx >= 0) {
2✔
350
        viewports = viewports.slice();
1✔
351
        viewports[mapboxViewportIdx] = getViewport(deck, map);
1✔
352
      }
1✔
353

2✔
354
      deck._drawLayers('mapbox-repaint', {
2✔
355
        viewports,
2✔
356
        layerFilter: params =>
2✔
357
          (!deck.props.layerFilter || deck.props.layerFilter(params)) &&
5✔
358
          (params.viewport.id !== MAPBOX_VIEW_ID || !mapboxLayerIds.includes(params.layer.id)),
3✔
359
        clearCanvas: false
2✔
360
      });
2✔
361
    }
2✔
362
  }
12✔
363

13✔
364
  // End of render cycle, clear generated viewport
13✔
365
  (deck.userData as UserData).currentViewport = null;
13✔
366
}
13✔
367

1✔
368
function onMapMove(deck: Deck, map: Map): void {
×
369
  deck.setProps({
×
370
    viewState: getViewState(map)
×
371
  });
×
372
  // Camera changed, will trigger a map repaint right after this
×
373
  // Clear any change flag triggered by setting viewState so that deck does not request
×
374
  // a second repaint
×
375
  deck.needsRedraw({clearRedrawFlags: true});
×
376
}
×
377

1✔
378
function updateLayers(deck: Deck): void {
46✔
379
  if ((deck.userData as UserData).isExternal) {
46✔
380
    return;
42✔
381
  }
42✔
382

4✔
383
  const layers: Layer[] = [];
4✔
384
  (deck.userData as UserData).mapboxLayers.forEach(deckLayer => {
4✔
385
    const LayerType = deckLayer.props.type;
3✔
386
    const layer = new LayerType(deckLayer.props);
3✔
387
    layers.push(layer);
3✔
388
  });
4✔
389
  deck.setProps({layers});
4✔
390
}
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