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

visgl / deck.gl / 27109224650

08 Jun 2026 12:17AM UTC coverage: 83.42% (+0.03%) from 83.39%
27109224650

Pull #10353

github

web-flow
Merge d827ecdcf into 6795cc9ca
Pull Request #10353: [codex] Improve GlobeView bitmap tile effect handling

7964 of 10026 branches covered (79.43%)

Branch coverage included in aggregate %.

7 of 7 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

14204 of 16548 relevant lines covered (85.84%)

19385.01 hits per line

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

99.12
/modules/layers/src/bitmap-layer/bitmap-layer.ts
1
// deck.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {
6
  Layer,
7
  project32,
8
  picking,
9
  CoordinateSystem,
10
  LayerProps,
11
  PickingInfo,
12
  GetPickingInfoParams,
13
  UpdateParameters,
14
  Color,
15
  TextureSource,
16
  Position,
17
  DefaultProps,
18
  Material,
19
  phongMaterial
20
} from '@deck.gl/core';
21
import {Model} from '@luma.gl/engine';
22
import type {SamplerProps, Texture} from '@luma.gl/core';
23
import {lngLatToWorld} from '@math.gl/web-mercator';
24

25
import createMesh from './create-mesh';
26

27
import {bitmapUniforms, BitmapProps} from './bitmap-layer-uniforms';
28
import vs from './bitmap-layer-vertex';
29
import fs from './bitmap-layer-fragment';
30

31
const defaultProps: DefaultProps<BitmapLayerProps> = {
4✔
32
  image: {type: 'image', value: null, async: true},
33
  bounds: {type: 'array', value: [1, 0, 0, 1], compare: true},
34
  _imageCoordinateSystem: 'default',
35

36
  desaturate: {type: 'number', min: 0, max: 1, value: 0},
37
  // More context: because of the blending mode we're using for ground imagery,
38
  // alpha is not effective when blending the bitmap layers with the base map.
39
  // Instead we need to manually dim/blend rgb values with a background color.
40
  transparentColor: {type: 'color', value: [0, 0, 0, 0]},
41
  tintColor: {type: 'color', value: [255, 255, 255]},
42

43
  material: true,
44

45
  textureParameters: {type: 'object', ignore: true, value: null}
46
};
47

48
/** All properties supported by BitmapLayer. */
49
export type BitmapLayerProps = _BitmapLayerProps & LayerProps;
50
export type BitmapBoundingBox =
51
  | [left: number, bottom: number, right: number, top: number]
52
  | [Position, Position, Position, Position];
53

54
/** Properties added by BitmapLayer. */
55
type _BitmapLayerProps = {
56
  data: never;
57
  /**
58
   * The image to display.
59
   *
60
   * @default null
61
   */
62
  image?: string | TextureSource | null;
63

64
  /**
65
   * Supported formats:
66
   *  - Coordinates of the bounding box of the bitmap `[left, bottom, right, top]`
67
   *  - Coordinates of four corners of the bitmap, should follow the sequence of `[[left, bottom], [left, top], [right, top], [right, bottom]]`.
68
   *   Each position could optionally contain a third component `z`.
69
   * @default [1, 0, 0, 1]
70
   */
71
  bounds?: BitmapBoundingBox;
72

73
  /**
74
   * > Note: this prop is experimental.
75
   *
76
   * Specifies how image coordinates should be geographically interpreted.
77
   * @default COORDINATE_SYSTEM.DEFAULT
78
   */
79
  _imageCoordinateSystem?: CoordinateSystem;
80

81
  /**
82
   * The desaturation of the bitmap. Between `[0, 1]`.
83
   * @default 0
84
   */
85
  desaturate?: number;
86

87
  /**
88
   * The color to use for transparent pixels, in `[r, g, b, a]`.
89
   * @default [0, 0, 0, 0]
90
   */
91
  transparentColor?: Color;
92

93
  /**
94
   * The color to tint the bitmap by, in `[r, g, b]`.
95
   * @default [255, 255, 255]
96
   */
97
  tintColor?: Color;
98

99
  /**
100
   * Material settings for lighting effect.
101
   *
102
   * @default true
103
   * @see https://deck.gl/docs/developer-guide/using-lighting
104
   */
105
  material?: Material;
106

107
  /** Customize the [texture parameters](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texParameter). */
108
  textureParameters?: SamplerProps | null;
109
};
110

111
export type BitmapLayerPickingInfo = PickingInfo<
112
  null,
113
  {
114
    bitmap: {
115
      /** Size of the original image */
116
      size: {
117
        width: number;
118
        height: number;
119
      };
120
      /** Hovered pixel uv in 0-1 range */
121
      uv: [number, number];
122
      /** Hovered pixel in the original image */
123
      pixel: [number, number];
124
    } | null;
125
  }
126
>;
127

128
/** Render a bitmap at specified boundaries. */
129
export default class BitmapLayer<ExtraPropsT extends {} = {}> extends Layer<
130
  ExtraPropsT & Required<_BitmapLayerProps>
131
> {
132
  static layerName = 'BitmapLayer';
4✔
133
  static defaultProps = defaultProps;
4✔
134

135
  state!: {
136
    disablePicking?: boolean;
137
    model?: Model;
138
    mesh?: any;
139
    coordinateConversion: number;
140
    bounds: [number, number, number, number];
141
  };
142

143
  getShaders() {
144
    return super.getShaders({vs, fs, modules: [project32, phongMaterial, picking, bitmapUniforms]});
41✔
145
  }
146

147
  initializeState() {
148
    const attributeManager = this.getAttributeManager()!;
41✔
149

150
    const noAlloc = true;
41✔
151

152
    attributeManager.add({
41✔
153
      indices: {
154
        size: 1,
155
        isIndexed: true,
156
        update: attribute => (attribute.value = this.state.mesh.indices),
40✔
157
        noAlloc
158
      },
159
      positions: {
160
        size: 3,
161
        type: 'float64',
162
        fp64: this.use64bitPositions(),
163
        update: attribute => (attribute.value = this.state.mesh.positions),
42✔
164
        noAlloc
165
      },
166
      texCoords: {
167
        size: 2,
168
        update: attribute => (attribute.value = this.state.mesh.texCoords),
40✔
169
        noAlloc
170
      }
171
    });
172
  }
173

174
  updateState({props, oldProps, changeFlags}: UpdateParameters<this>): void {
175
    // setup model first
176
    const attributeManager = this.getAttributeManager()!;
81✔
177

178
    if (changeFlags.extensionsChanged) {
81✔
179
      this.state.model?.destroy();
41✔
180
      this.state.model = this._getModel();
41✔
181
      attributeManager.invalidateAll();
41✔
182
    }
183

184
    if (props.bounds !== oldProps.bounds) {
81✔
185
      const oldMesh = this.state.mesh;
43✔
186
      const mesh = this._createMesh();
43✔
187
      this.state.model!.setVertexCount(mesh.vertexCount);
43✔
188
      for (const key in mesh) {
43✔
189
        if (oldMesh && oldMesh[key] !== mesh[key]) {
172✔
190
          attributeManager.invalidate(key);
2✔
191
        }
192
      }
193
      this.setState({mesh, ...this._getCoordinateUniforms()});
43✔
194
    } else if (props._imageCoordinateSystem !== oldProps._imageCoordinateSystem) {
38✔
195
      this.setState(this._getCoordinateUniforms());
4✔
196
    }
197
  }
198

199
  getPickingInfo(params: GetPickingInfoParams): BitmapLayerPickingInfo {
200
    const {image} = this.props;
2✔
201
    const info = params.info as BitmapLayerPickingInfo;
2✔
202

203
    if (!info.color || !image) {
2✔
204
      info.bitmap = null;
1✔
205
      return info;
1✔
206
    }
207

208
    const {width, height} = image as Texture;
1✔
209

210
    // Picking color doesn't represent object index in this layer
211
    info.index = 0;
1✔
212

213
    // Calculate uv and pixel in bitmap
214
    const uv = unpackUVsFromRGB(info.color);
1✔
215

216
    info.bitmap = {
1✔
217
      size: {width, height},
218
      uv,
219
      pixel: [Math.floor(uv[0] * width), Math.floor(uv[1] * height)]
220
    };
221

222
    return info;
1✔
223
  }
224

225
  // Override base Layer multi-depth picking logic
226
  disablePickingIndex() {
227
    this.setState({disablePicking: true});
1✔
228
  }
229

230
  restorePickingColors() {
UNCOV
231
    this.setState({disablePicking: false});
×
232
  }
233

234
  protected _updateAutoHighlight(info) {
235
    super._updateAutoHighlight({
2✔
236
      ...info,
237
      color: this.encodePickingColor(0)
238
    });
239
  }
240

241
  protected _createMesh() {
242
    const {bounds} = this.props;
43✔
243

244
    let normalizedBounds = bounds;
43✔
245
    // bounds as [minX, minY, maxX, maxY]
246
    if (isRectangularBounds(bounds)) {
43✔
247
      /*
248
        (minX0, maxY3) ---- (maxX2, maxY3)
249
               |                  |
250
               |                  |
251
               |                  |
252
        (minX0, minY1) ---- (maxX2, minY1)
253
     */
254
      normalizedBounds = [
41✔
255
        [bounds[0], bounds[1]],
256
        [bounds[0], bounds[3]],
257
        [bounds[2], bounds[3]],
258
        [bounds[2], bounds[1]]
259
      ];
260
    }
261

262
    return createMesh(normalizedBounds, this.context.viewport.resolution);
43✔
263
  }
264

265
  protected _getModel(): Model {
266
    /*
267
      0,0 --- 1,0
268
       |       |
269
      0,1 --- 1,1
270
    */
271
    return new Model(this.context.device, {
41✔
272
      ...this.getShaders(),
273
      id: this.props.id,
274
      bufferLayout: this.getAttributeManager()!.getBufferLayouts(),
275
      topology: 'triangle-list',
276
      isInstanced: false
277
    });
278
  }
279

280
  draw(opts) {
281
    const {shaderModuleProps} = opts;
83✔
282
    const {model, coordinateConversion, bounds, disablePicking} = this.state;
83✔
283
    const {image, desaturate, transparentColor, tintColor} = this.props;
83✔
284

285
    if (shaderModuleProps.picking.isActive && disablePicking) {
83✔
286
      return;
1✔
287
    }
288

289
    // // TODO fix zFighting
290
    // Render the image
291
    if (image && model) {
82✔
292
      const bitmapProps: BitmapProps = {
36✔
293
        bitmapTexture: image as Texture,
294
        bounds,
295
        coordinateConversion,
296
        desaturate,
297
        tintColor: tintColor.slice(0, 3).map(x => x / 255) as [number, number, number],
108✔
298
        transparentColor: transparentColor.map(x => x / 255) as [number, number, number, number]
144✔
299
      };
300
      model.shaderInputs.setProps({bitmap: bitmapProps});
36✔
301
      model.draw(this.context.renderPass);
36✔
302
    }
303
  }
304

305
  _getCoordinateUniforms() {
306
    let {_imageCoordinateSystem: imageCoordinateSystem} = this.props;
47✔
307
    if (imageCoordinateSystem !== 'default') {
47✔
308
      const {bounds} = this.props;
38✔
309
      if (!isRectangularBounds(bounds)) {
38✔
310
        throw new Error('_imageCoordinateSystem only supports rectangular bounds');
1✔
311
      }
312

313
      // The default behavior (linearly interpolated tex coords)
314
      const defaultImageCoordinateSystem = this.context.viewport.resolution
37✔
315
        ? 'lnglat'
316
        : 'cartesian';
317
      imageCoordinateSystem = imageCoordinateSystem === 'lnglat' ? 'lnglat' : 'cartesian';
38✔
318

319
      if (imageCoordinateSystem === 'lnglat' && defaultImageCoordinateSystem === 'cartesian') {
38✔
320
        // LNGLAT in Mercator, e.g. display LNGLAT-encoded image in WebMercator projection
321
        return {coordinateConversion: -1, bounds};
2✔
322
      }
323
      if (imageCoordinateSystem === 'cartesian' && defaultImageCoordinateSystem === 'lnglat') {
35✔
324
        // Mercator in LNGLAT, e.g. display WebMercator encoded image in Globe projection
325
        const bottomLeft = lngLatToWorld([bounds[0], bounds[1]]);
35✔
326
        const topRight = lngLatToWorld([bounds[2], bounds[3]]);
35✔
327
        return {
25✔
328
          coordinateConversion: 1,
329
          bounds: [bottomLeft[0], bottomLeft[1], topRight[0], topRight[1]]
330
        };
331
      }
332
    }
333
    return {
19✔
334
      coordinateConversion: 0,
335
      bounds: [0, 0, 0, 0]
336
    };
337
  }
338
}
339

340
/**
341
 * Decode uv floats from rgb bytes where b contains 4-bit fractions of uv
342
 * @param {number[]} color
343
 * @returns {number[]} uvs
344
 * https://stackoverflow.com/questions/30242013/glsl-compressing-packing-multiple-0-1-colours-var4-into-a-single-var4-variab
345
 */
346
function unpackUVsFromRGB(color: Uint8Array): [number, number] {
347
  const [u, v, fracUV] = color;
1✔
348
  const vFrac = (fracUV & 0xf0) / 256;
1✔
349
  const uFrac = (fracUV & 0x0f) / 16;
1✔
350
  return [(u + uFrac) / 256, (v + vFrac) / 256];
1✔
351
}
352

353
function isRectangularBounds(
354
  bounds: [number, number, number, number] | [Position, Position, Position, Position]
355
): bounds is [number, number, number, number] {
356
  return Number.isFinite(bounds[0]);
81✔
357
}
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