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

keplergl / kepler.gl / 14430112347

13 Apr 2025 01:53PM UTC coverage: 61.717% (-4.4%) from 66.129%
14430112347

Pull #3048

github

web-flow
Merge 4d33fb563 into 9de30e2ba
Pull Request #3048: [feat] Raster Tile Layer

6066 of 11656 branches covered (52.04%)

Branch coverage included in aggregate %.

136 of 1263 new or added lines in 45 files covered. (10.77%)

5 existing lines in 3 files now uncovered.

12551 of 18509 relevant lines covered (67.81%)

82.41 hits per line

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

1.94
/src/layers/src/raster-tile/gpu-utils.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
/**
5
 * Functions and constants for handling webgl/luma.gl/deck.gl entities
6
 */
7

8
import {parse, fetchFile, load} from '@loaders.gl/core';
9
import {ImageLoader} from '@loaders.gl/images';
10
import {NPYLoader} from '@loaders.gl/textures';
11
import GL from '@luma.gl/constants';
12
import {Texture2DProps} from '@luma.gl/webgl';
13

14
import {getLoaderOptions} from '@kepler.gl/constants';
15
import {RasterWebGL} from '@kepler.gl/deckgl-layers';
16

17
type ShaderModule = RasterWebGL.ShaderModule;
18
const {
19
  combineBandsFloat,
20
  combineBandsInt,
21
  combineBandsUint,
22
  maskFloat,
23
  maskInt,
24
  maskUint,
25
  linearRescale,
26
  gammaContrast,
27
  sigmoidalContrast,
28
  normalizedDifference,
29
  enhancedVegetationIndex,
30
  soilAdjustedVegetationIndex,
31
  modifiedSoilAdjustedVegetationIndex,
32
  colormap: colormapModule,
33
  filter,
34
  saturation,
35
  reorderBands,
36
  rgbaImage
37
} = RasterWebGL;
13✔
38

39
import {
40
  CATEGORICAL_TEXTURE_WIDTH,
41
  dtypeMaxValue,
42
  generateCategoricalBitmapArray,
43
  isColormapAllowed,
44
  isFilterAllowed,
45
  isRescalingAllowed
46
} from './raster-tile-utils';
47
import {
48
  CategoricalColormapOptions,
49
  ImageData,
50
  NPYLoaderDataTypes,
51
  NPYLoaderResponse,
52
  RenderSubLayersProps
53
} from './types';
54

55
/**
56
 * Describe WebGL2 Texture parameters to use for given input data type
57
 */
58
interface WebGLTextureFormat {
59
  format: number;
60
  dataFormat: number;
61
  type: number;
62
}
63

64
/**
65
 * Convert TypedArray to WebGL2 Texture Parameters
66
 */
67
function getWebGL2TextureParameters(data: NPYLoaderDataTypes): WebGLTextureFormat | never {
NEW
68
  if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) {
×
NEW
69
    return {
×
70
      // Note: texture data has no auto-rescaling; pixel values stay as 0-255
71
      format: GL.R8UI,
72
      dataFormat: GL.RED_INTEGER,
73
      type: GL.UNSIGNED_BYTE
74
    };
75
  }
76

NEW
77
  if (data instanceof Uint16Array) {
×
NEW
78
    return {
×
79
      format: GL.R16UI,
80
      dataFormat: GL.RED_INTEGER,
81
      type: GL.UNSIGNED_SHORT
82
    };
83
  }
84

NEW
85
  if (data instanceof Uint32Array) {
×
NEW
86
    return {
×
87
      format: GL.R32UI,
88
      dataFormat: GL.RED_INTEGER,
89
      type: GL.UNSIGNED_INT
90
    };
91
  }
92

NEW
93
  if (data instanceof Int8Array) {
×
NEW
94
    return {
×
95
      format: GL.R8I,
96
      dataFormat: GL.RED_INTEGER,
97
      type: GL.BYTE
98
    };
99
  }
100

NEW
101
  if (data instanceof Int16Array) {
×
NEW
102
    return {
×
103
      format: GL.R16I,
104
      dataFormat: GL.RED_INTEGER,
105
      type: GL.SHORT
106
    };
107
  }
NEW
108
  if (data instanceof Int32Array) {
×
NEW
109
    return {
×
110
      format: GL.R32I,
111
      dataFormat: GL.RED_INTEGER,
112
      type: GL.INT
113
    };
114
  }
NEW
115
  if (data instanceof Float32Array) {
×
NEW
116
    return {
×
117
      format: GL.R32F,
118
      dataFormat: GL.RED,
119
      type: GL.FLOAT
120
    };
121
  }
122

NEW
123
  if (data instanceof Float64Array) {
×
NEW
124
    return {
×
125
      format: GL.R32F,
126
      dataFormat: GL.RED,
127
      type: GL.FLOAT
128
    };
129
  }
130

131
  // For exhaustive check above; following should never occur
132
  // https://stackoverflow.com/a/58009992
NEW
133
  const unexpectedInput: never = data;
×
NEW
134
  throw new Error(unexpectedInput);
×
135
}
136

137
/**
138
 * Discrete-valued colormaps (e.g. from the output of
139
 * classification algorithms) in the raster layer. Previously, the values passed to
140
 * `TEXTURE_MIN_FILTER` and `TEXTURE_MAG_FILTER` were `GL.LINEAR`, which meant that the GPU would
141
 * linearly interpolate values between two neighboring colormap pixel values. Setting these values
142
 * to NEAREST means that the GPU will choose the nearest value on the texture2D lookup operation,
143
 * which fixes precision issues for discrete-valued colormaps. This should be ok for continuous
144
 * colormaps as long as the color difference between each pixel on the colormap is small.
145
 */
146
export const COLORMAP_TEXTURE_PARAMETERS = {
13✔
147
  [GL.TEXTURE_MIN_FILTER]: GL.NEAREST,
148
  [GL.TEXTURE_MAG_FILTER]: GL.NEAREST,
149
  [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE,
150
  [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE
151
};
152

153
const DEFAULT_8BIT_TEXTURE_PARAMETERS = {
13✔
154
  [GL.TEXTURE_MIN_FILTER]: GL.LINEAR_MIPMAP_LINEAR,
155
  [GL.TEXTURE_MAG_FILTER]: GL.LINEAR,
156
  [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE,
157
  [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE
158
};
159

160
const DEFAULT_HIGH_BIT_TEXTURE_PARAMETERS = {
13✔
161
  [GL.TEXTURE_MIN_FILTER]: GL.NEAREST,
162
  [GL.TEXTURE_MAG_FILTER]: GL.NEAREST,
163
  [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE,
164
  [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE
165
};
166

167
/**
168
 * Select correct module type for "combineBands"
169
 *
170
 * combineBands joins up to four 2D arrays (contained in imageBands) into a single "rgba" image
171
 * texture on the GPU. That shader code needs to have the same data type as the actual image data.
172
 * E.g. for float data the texture needs to be `sampler2D`, for uint data the texture needs to be
173
 * `usampler2D` and for int data the texture needs to be `isampler2D`.
174
 */
175
export function getCombineBandsModule(imageBands: Texture2DProps[]): ShaderModule {
176
  // Each image array is expected/required to be of the same data type
NEW
177
  switch (imageBands[0].format) {
×
178
    case GL.R8UI:
NEW
179
      return combineBandsUint;
×
180
    case GL.R16UI:
NEW
181
      return combineBandsUint;
×
182
    case GL.R32UI:
NEW
183
      return combineBandsUint;
×
184
    case GL.R8I:
NEW
185
      return combineBandsInt;
×
186
    case GL.R16I:
NEW
187
      return combineBandsInt;
×
188
    case GL.R32I:
NEW
189
      return combineBandsInt;
×
190
    case GL.R32F:
NEW
191
      return combineBandsFloat;
×
192
    default:
NEW
193
      throw new Error('bad format');
×
194
  }
195
}
196

197
/** Select correct image masking shader module for mask data type
198
 * The imageMask could (at least in the future, theoretically) be of a different data format than
199
 * the imageBands data itself.
200
 */
201
export function getImageMaskModule(imageMask: Texture2DProps): ShaderModule {
NEW
202
  switch (imageMask.format) {
×
203
    case GL.R8UI:
NEW
204
      return maskUint;
×
205
    case GL.R16UI:
NEW
206
      return maskUint;
×
207
    case GL.R32UI:
NEW
208
      return maskUint;
×
209
    case GL.R8I:
NEW
210
      return maskInt;
×
211
    case GL.R16I:
NEW
212
      return maskInt;
×
213
    case GL.R32I:
NEW
214
      return maskInt;
×
215
    case GL.R32F:
NEW
216
      return maskFloat;
×
217
    default:
NEW
218
      throw new Error('bad format');
×
219
  }
220
}
221

222
/**
223
 * Load image and wrap with default WebGL texture parameters
224
 *
225
 * @param url URL to load image
226
 * @param textureParams parameters to pass to Texture2D
227
 *
228
 * @return image object to pass to Texture2D constructor
229
 */
230
export async function loadImage(
231
  url: string,
232
  textureParams: Texture2DProps = {},
×
233
  requestOptions: RequestInit = {}
×
234
): Promise<Texture2DProps> {
NEW
235
  const response = await fetchFile(url, requestOptions);
×
NEW
236
  const image = await parse(response, ImageLoader);
×
237

NEW
238
  return {
×
239
    data: image,
240
    parameters: DEFAULT_8BIT_TEXTURE_PARAMETERS,
241
    format: GL.RGB,
242
    ...textureParams
243
  };
244
}
245

246
type FetchLike = (url: string, options?: RequestInit) => Promise<Response>;
247
type LoadingOptions = {
248
  fetch?: typeof fetch | FetchLike;
249
};
250

251
/**
252
 * Load NPY Array
253
 *
254
 * The NPY format is described here: https://numpy.org/doc/stable/reference/generated/numpy.lib.format.html.
255
 * It's designed to be a very simple file format to hold an N-dimensional block of data. The header describes the data type, shape, and order (either C or Fortran) of the array.
256
 *
257
 * @param url URL to load NPY Array
258
 * @param split Whether to split single typed array representing an N-dimensional array into an Array with each dimension as its own typed array
259
 *
260
 * @return image object to pass to Texture2D constructor
261
 */
262
export async function loadNpyArray(
263
  request: {url: string; options: RequestInit},
264
  split: true,
265
  options?: LoadingOptions
266
): Promise<Texture2DProps[] | null>;
267
export async function loadNpyArray(
268
  request: {url: string; options: RequestInit},
269
  split: false,
270
  options?: LoadingOptions
271
): Promise<Texture2DProps | null>;
272
export async function loadNpyArray(
273
  request: {url: string; options: RequestInit},
274
  split: boolean,
275
  options?: LoadingOptions
276
): Promise<Texture2DProps | Texture2DProps[] | null> {
NEW
277
  try {
×
NEW
278
    const {npy: npyOptions} = getLoaderOptions();
×
NEW
279
    const response: NPYLoaderResponse = await load(request.url, NPYLoader, {
×
280
      npy: npyOptions,
281
      fetch: options?.fetch
282
    });
283

NEW
284
    if (!response || !response.data || request.options.signal?.aborted) {
×
NEW
285
      return null;
×
286
    }
287

288
    // Float64 data needs to be coerced to Float32 for the GPU
NEW
289
    if (response.data instanceof Float64Array) {
×
NEW
290
      response.data = Float32Array.from(response.data);
×
291
    }
292

NEW
293
    const {data, header} = response;
×
NEW
294
    const {shape} = header;
×
NEW
295
    const {format, dataFormat, type} = getWebGL2TextureParameters(data);
×
296

297
    // TODO: check height-width or width-height
298
    // Regardless, images usually square
299
    // TODO: handle cases of 256x256x1 instead of 1x256x256
NEW
300
    const [z, height, width] = shape;
×
301

302
    // Since we now use WebGL2 data types for 8-bit textures, we set the following for all textures
NEW
303
    const mipmaps = false;
×
NEW
304
    const parameters = DEFAULT_HIGH_BIT_TEXTURE_PARAMETERS;
×
305

NEW
306
    if (!split) {
×
NEW
307
      return {
×
308
        data,
309
        width,
310
        height,
311
        format,
312
        dataFormat,
313
        type,
314
        parameters,
315
        mipmaps
316
      };
317
    }
318

319
    // Split into individual arrays
NEW
320
    const channels: Texture2DProps[] = [];
×
NEW
321
    const channelSize = height * width;
×
NEW
322
    for (let i = 0; i < z; i++) {
×
NEW
323
      channels.push({
×
324
        data: data.subarray(i * channelSize, (i + 1) * channelSize),
325
        width,
326
        height,
327
        format,
328
        dataFormat,
329
        type,
330
        parameters,
331
        mipmaps
332
      });
333
    }
NEW
334
    return channels;
×
335
  } catch {
NEW
336
    return null;
×
337
  }
338
}
339

340
/**
341
 * Create texture data for categorical colormap scale
342
 * @param categoricalOptions - color map configuration and min-max values of categorical band
343
 * @returns texture data
344
 */
345
export function generateCategoricalColormapTexture(
346
  categoricalOptions: CategoricalColormapOptions
347
): Texture2DProps {
NEW
348
  const data = generateCategoricalBitmapArray(categoricalOptions);
×
NEW
349
  return {
×
350
    data,
351
    width: CATEGORICAL_TEXTURE_WIDTH,
352
    height: 1,
353
    format: GL.RGBA,
354
    dataFormat: GL.RGBA,
355
    type: GL.UNSIGNED_BYTE,
356
    parameters: COLORMAP_TEXTURE_PARAMETERS,
357
    mipmaps: false
358
  };
359
}
360

361
// TODO: would probably be simpler to only pass in the props actually used by this function. That
362
// would mean a smaller object than RenderSubLayersProps
363
// eslint-disable-next-line max-statements, complexity
364
export function getModules({
365
  images,
366
  props
367
}: {
368
  images: Partial<ImageData>;
369
  props?: RenderSubLayersProps;
370
}): {
371
  modules: ShaderModule[];
372
  moduleProps: Record<string, any>;
373
} {
NEW
374
  const moduleProps: Record<string, any> = {};
×
375
  // Array of luma.gl WebGL modules to pass to the RasterLayer
NEW
376
  const modules: ShaderModule[] = [];
×
377

378
  // use rgba image directly. Used for raster .pmtiles rendering
NEW
379
  if (images.imageRgba) {
×
NEW
380
    modules.push(rgbaImage);
×
381

382
    // no support for other modules atm for direct rgba mode
NEW
383
    return {modules, moduleProps};
×
384
  }
385

NEW
386
  if (!props) {
×
NEW
387
    return {modules, moduleProps};
×
388
  }
389

390
  const {
391
    renderBandIndexes,
392
    nonLinearRescaling,
393
    linearRescalingFactor,
394
    minPixelValue,
395
    maxPixelValue,
396
    gammaContrastFactor,
397
    sigmoidalContrastFactor,
398
    sigmoidalBiasFactor,
399
    saturationValue,
400
    bandCombination,
401
    filterEnabled,
402
    filterRange,
403
    dataType,
404
    minCategoricalBandValue,
405
    maxCategoricalBandValue,
406
    hasCategoricalColorMap
NEW
407
  } = props;
×
408

NEW
409
  if (Array.isArray(images.imageBands) && images.imageBands.length > 0) {
×
NEW
410
    modules.push(getCombineBandsModule(images.imageBands));
×
411
  }
412

NEW
413
  if (images.imageMask) {
×
NEW
414
    modules.push(getImageMaskModule(images.imageMask));
×
415
    // In general, data masks are 0 for nodata and the maximum value for valid data, e.g. 255 or
416
    // 65535 for uint8 or uint16 data, respectively
NEW
417
    moduleProps.maskKeepMin = 1;
×
418
  }
419

NEW
420
  if (Array.isArray(renderBandIndexes)) {
×
NEW
421
    modules.push(reorderBands);
×
NEW
422
    moduleProps.ordering = renderBandIndexes;
×
423
  }
424

NEW
425
  const globalRange = maxPixelValue - minPixelValue;
×
426
  // Fix rescaling if we are sure that dataset is categorical
NEW
427
  if (hasCategoricalColorMap) {
×
NEW
428
    modules.push(linearRescale);
×
NEW
429
    moduleProps.linearRescaleScaler = 1 / maxPixelValue;
×
NEW
430
    moduleProps.linearRescaleOffset = 0;
×
NEW
431
  } else if (isRescalingAllowed(bandCombination)) {
×
NEW
432
    if (!nonLinearRescaling) {
×
NEW
433
      const [min, max] = linearRescalingFactor;
×
NEW
434
      const localRange = max - min;
×
435

436
      // Add linear rescaling module
NEW
437
      modules.push(linearRescale);
×
438

439
      // Divide by local range * global range
NEW
440
      moduleProps.linearRescaleScaler = 1 / (localRange * globalRange);
×
441

442
      // Subtract off the local min
NEW
443
      moduleProps.linearRescaleOffset = -min;
×
444

445
      // Clamp to [0, 1] done automatically?
446
    } else {
NEW
447
      modules.push(linearRescale);
×
NEW
448
      moduleProps.linearRescaleScaler = 1 / maxPixelValue;
×
NEW
449
      moduleProps.linearRescaleOffset = 0;
×
450

NEW
451
      modules.push(gammaContrast);
×
NEW
452
      moduleProps.gammaContrastValue = gammaContrastFactor;
×
453

NEW
454
      modules.push(sigmoidalContrast);
×
NEW
455
      moduleProps.sigmoidalContrast = sigmoidalContrastFactor;
×
NEW
456
      moduleProps.sigmoidalBias = sigmoidalBiasFactor;
×
457
    }
458

NEW
459
    if (Number.isFinite(saturationValue) && saturationValue !== 1) {
×
NEW
460
      modules.push(saturation);
×
NEW
461
      moduleProps.saturationValue = saturationValue;
×
462
    }
463
  }
464

NEW
465
  switch (bandCombination) {
×
466
    case 'normalizedDifference':
NEW
467
      modules.push(normalizedDifference);
×
NEW
468
      break;
×
469
    case 'enhancedVegetationIndex':
NEW
470
      modules.push(enhancedVegetationIndex);
×
NEW
471
      break;
×
472
    case 'soilAdjustedVegetationIndex':
NEW
473
      modules.push(soilAdjustedVegetationIndex);
×
NEW
474
      break;
×
475
    case 'modifiedSoilAdjustedVegetationIndex':
NEW
476
      modules.push(modifiedSoilAdjustedVegetationIndex);
×
NEW
477
      break;
×
478
    default:
NEW
479
      break;
×
480
  }
481

NEW
482
  if (isFilterAllowed(bandCombination) && filterEnabled) {
×
NEW
483
    modules.push(filter);
×
NEW
484
    moduleProps.filterMin1 = filterRange[0];
×
NEW
485
    moduleProps.filterMax1 = filterRange[1];
×
486
  }
487

488
  // Apply colormap
NEW
489
  if (isColormapAllowed(bandCombination) && images.imageColormap) {
×
NEW
490
    modules.push(colormapModule);
×
NEW
491
    moduleProps.minCategoricalBandValue = minCategoricalBandValue;
×
NEW
492
    moduleProps.maxCategoricalBandValue = maxCategoricalBandValue;
×
NEW
493
    moduleProps.dataTypeMaxValue = dtypeMaxValue[dataType];
×
NEW
494
    moduleProps.maxPixelValue = maxPixelValue;
×
495
  }
496

NEW
497
  return {modules, moduleProps};
×
498
}
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