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

keplergl / kepler.gl / 24727469777

21 Apr 2026 02:16PM UTC coverage: 59.54% (+0.02%) from 59.525%
24727469777

Pull #3380

github

web-flow
Merge bde19634d into 595bd909a
Pull Request #3380: fix: polygon tool regression

6816 of 13712 branches covered (49.71%)

Branch coverage included in aggregate %.

4 of 18 new or added lines in 2 files covered. (22.22%)

176 existing lines in 3 files now uncovered.

14029 of 21298 relevant lines covered (65.87%)

76.15 hits per line

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

5.99
/src/layers/src/tile3d-layer/tile3d-layer.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import {Tile3DLayer as DeckTile3DLayer} from '@deck.gl/geo-layers';
5
import {LightingEffect as DeckLightingEffect} from '@deck.gl/core';
6
import {Tiles3DLoader, CesiumIonLoader} from '@loaders.gl/3d-tiles';
7
import {I3SLoader} from '@loaders.gl/i3s';
8
import {Tileset3D, Tile3D} from '@loaders.gl/tiles';
9
import {parsePBRMaterial} from '@luma.gl/gltf';
10

11
import Layer from '../base-layer';
12
import Tile3DLayerIcon from './tile3d-layer-icon';
13
import {FindDefaultLayerPropsReturnValue} from '../layer-utils';
14
import {
15
  LAYER_VIS_CONFIGS,
16
  LAYER_TYPES,
17
  DatasetType,
18
  TILE3D_PROVIDERS,
19
  Tile3DDatasetMetadata
20
} from '@kepler.gl/constants';
21
import {KeplerTable as KeplerDataset, Datasets as KeplerDatasets} from '@kepler.gl/table';
22
import {VisConfigNumber, BindedLayerCallbacks} from '@kepler.gl/types';
23

24
// Lazily created patched sublayer classes (see getSubLayerClass below).
25
let _PatchedMeshLayer: any = null;
13✔
26
let _PatchedScenegraphLayer: any = null;
13✔
27

28
/**
29
 * Returns true when the deck.gl instance has an active LightingEffect with
30
 * shadow rendering enabled (i.e. the kepler Light & Shadow effect is on).
31
 * Used by patched sub-layers to decide whether to override the material's
32
 * unlit flag: when no lighting effect is active, meshes stay unlit.
33
 */
34
function hasActiveLightingEffect(layer: any): boolean {
35
  const effects: any[] | undefined = layer.context?.deck?.props?.effects;
×
36
  if (!effects) return false;
×
37
  return effects.some(e => e instanceof DeckLightingEffect && (e as any).shadow);
×
38
}
39

40
/**
41
 * Same check but from the top-level KeplerTile3DLayer's own context.
42
 * Used in updateState to detect lighting on/off transitions.
43
 */
44
function _checkLightingActive(context: any): boolean {
45
  const effects: any[] | undefined = context?.deck?.props?.effects;
×
46
  if (!effects) return false;
×
47
  return effects.some(e => e instanceof DeckLightingEffect && (e as any).shadow);
×
48
}
49

50
/**
51
 * Custom DeckTile3DLayer that catches exceptions in _loadTileset and
52
 * _updateTileset (which calls selectTiles – an unhandled-promise path
53
 * where errors like "boundingVolume must contain …" escape).
54
 * See: https://github.com/visgl/deck.gl/issues/8755
55
 */
56
// @ts-expect-error Types have separate declarations of a private property '_loadTileset'.
57
class KeplerTile3DLayer extends DeckTile3DLayer {
58
  shouldUpdateState(params: any): boolean {
NEW
59
    if (super.shouldUpdateState(params)) return true;
×
NEW
60
    const lightingActive = _checkLightingActive(this.context);
×
NEW
61
    return lightingActive !== ((this.state as any)?._lightingWasActive ?? false);
×
62
  }
63

64
  // deck.gl Tile3DLayer.updateState only sets `needsUpdate` on cached tile
65
  // sub-layers when `propsChanged` fires, but NOT when `extensionsChanged`
66
  // fires (e.g. the shadow module being added/removed). The LayerManager sets
67
  // `extensionsChanged` only on top-level layers, and it never reaches the
68
  // MeshLayer / ScenegraphLayer sub-layers inside the tile cache. Override
69
  // updateState to propagate the flag so every cached tile gets re-rendered
70
  // with the correct material (lit vs. unlit).
71
  //
72
  // Additionally, when the Light & Shadow effect is disabled (but kept in
73
  // the effects array to avoid cleanup/shader-module removal), no
74
  // extensionsChanged fires. Detect the lighting active→inactive transition
75
  // and force sublayer re-creation so models revert to their default (unlit)
76
  // materials.
77
  updateState(params: any): void {
78
    super.updateState(params);
×
79

80
    const lightingActive = _checkLightingActive(this.context);
×
81
    const prevLightingActive = (this.state as any)?._lightingWasActive ?? false;
×
82

83
    const lightingChanged = lightingActive !== prevLightingActive;
×
84
    if (lightingChanged) {
×
85
      this.setState({_lightingWasActive: lightingActive});
×
86
    }
87

88
    if (params.changeFlags.extensionsChanged || lightingChanged) {
×
89
      const {layerMap} = this.state as any;
×
90
      if (layerMap) {
×
91
        for (const key in layerMap) {
×
NEW
92
          layerMap[key].needsUpdate = true;
×
93
        }
94
      }
95
    }
96
  }
97

98
  // @ts-ignore override of private method called by deck.gl internals
99
  private async _loadTileset(tilesetUrl: string) {
100
    try {
×
101
      // @ts-expect-error _loadTileset is private in DeckTile3DLayer
102
      await super._loadTileset(tilesetUrl);
×
103
    } catch (error: any) {
104
      if (error?.message?.includes("reading 'refine'")) {
×
105
        this.raiseError(new Error('Bad tileset format or invalid API key'), '_loadTileset');
×
106
      } else {
107
        console.error('Tile3DLayer: tileset load error', error);
×
108
      }
109
    }
110
  }
111

112
  /**
113
   * Override _updateTileset to catch errors from selectTiles(),
114
   * which deck.gl calls with .then() but no .catch().
115
   * Errors from tilesetInitializationPromise (e.g. invalid boundingVolume
116
   * in child tiles) surface here as unhandled promise rejections.
117
   */
118
  // @ts-ignore override of private method
119
  private _updateTileset(viewports: Record<string, any> | null): void {
120
    if (!viewports) return;
×
121
    const {tileset3d} = this.state as any;
×
122
    const {timeline} = this.context;
×
123
    const viewportsNumber = Object.keys(viewports).length;
×
124
    if (!timeline || !viewportsNumber || !tileset3d) return;
×
125

126
    tileset3d
×
127
      .selectTiles(Object.values(viewports))
128
      .then((frameNumber: number) => {
129
        const tilesetChanged = (this.state as any).frameNumber !== frameNumber;
×
130
        if (tilesetChanged) {
×
131
          this.setState({frameNumber});
×
132
        }
133
      })
134
      .catch((error: any) => {
135
        console.error('Tile3DLayer: selectTiles error', error);
×
136
      });
137
  }
138

139
  // deck.gl's ScenegraphLayer and MeshLayer do not correctly handle
140
  // defaultShaderModule changes (extensionsChanged flag):
141
  //
142
  // ScenegraphLayer (Google/Cesium 3D tiles): models are created by
143
  // luma.gl's GLTF pipeline with shader modules baked in.
144
  // updateState only recreates models when the scenegraph *prop*
145
  // changes, completely ignoring extensionsChanged. When the shadow
146
  // module is added/removed, existing models keep their old shaders.
147
  //
148
  // MeshLayer (ArcGIS/I3S): getModel() creates the GPU model with PBR
149
  // shader defines (HAS_BASECOLORMAP etc.) but does not set the
150
  // corresponding texture bindings (pbr_baseColorSampler etc.).
151
  // updateState only calls updatePbrMaterialUniforms when pbrMaterial
152
  // *prop* changes, not when the model is recreated via
153
  // extensionsChanged. luma.gl skips rendering ("Binding … not found").
154
  //
155
  // Fix: override getSubLayerClass to return patched sublayer classes.
156
  // @ts-ignore protected override
157
  protected getSubLayerClass(id: string, DefaultClass: any): any {
158
    if (id === 'mesh') {
×
159
      if (!_PatchedMeshLayer) {
×
160
        _PatchedMeshLayer = class extends DefaultClass {
×
161
          shouldUpdateState(params) {
162
            if (super.shouldUpdateState(params)) return true;
×
163
            return hasActiveLightingEffect(this) !== (this.state?._lightingWasActive ?? false);
×
164
          }
165

166
          updateState(params) {
167
            super.updateState(params);
×
168

169
            const lightingActive = hasActiveLightingEffect(this);
×
170
            const prevLighting = this.state?._lightingWasActive ?? false;
×
171
            if (lightingActive !== prevLighting) {
×
172
              this.setState({_lightingWasActive: lightingActive});
×
173
              // Re-parse the PBR material so that parseMaterial's
174
              // lit/unlit decision is re-evaluated with the new
175
              // lighting state. This updates shader inputs and
176
              // textures without rebuilding the entire GPU model.
177
              if (this.state?.model) {
×
178
                this.updatePbrMaterialUniforms(this.props.pbrMaterial);
×
179
              }
180
            }
181

182
            if (params.changeFlags.extensionsChanged && this.state?.model) {
×
183
              this.updatePbrMaterialUniforms(this.props.pbrMaterial);
×
184
            }
185
          }
186

187
          // deck.gl MeshLayer.parseMaterial forces `unlit = true` whenever
188
          // the material has a baseColorTexture. This incorrectly disables
189
          // all lighting for textured I3S (ArcGIS) tiles. When the Light &
190
          // Shadow effect is active, respect the material's own `unlit`
191
          // property instead; otherwise fall back to the default (unlit) behavior.
192
          parseMaterial(material, mesh) {
193
            if (
×
194
              !hasActiveLightingEffect(this) ||
×
195
              !material ||
196
              !mesh?.attributes ||
197
              !mesh.attributes.normals
198
            ) {
199
              return super.parseMaterial(material, mesh);
×
200
            }
201
            const unlit = Boolean(material.unlit);
×
202
            return parsePBRMaterial(
×
203
              this.context.device,
204
              {unlit, ...material},
205
              {NORMAL: mesh.attributes.normals, TEXCOORD_0: mesh.attributes.texCoords},
206
              {pbrDebug: false, lights: true, useTangents: false}
207
            );
208
          }
209
        };
210
        (_PatchedMeshLayer as any).layerName = DefaultClass.layerName || 'MeshLayer';
×
211
        (_PatchedMeshLayer as any).defaultProps = DefaultClass.defaultProps;
×
212
      }
213
      return _PatchedMeshLayer;
×
214
    }
215
    if (id === 'scenegraph') {
×
216
      if (!_PatchedScenegraphLayer) {
×
217
        _PatchedScenegraphLayer = class extends DefaultClass {
×
218
          shouldUpdateState(params) {
219
            if (super.shouldUpdateState(params)) return true;
×
220
            return hasActiveLightingEffect(this) !== (this.state?._lightingWasActive ?? false);
×
221
          }
222

223
          _setUnlitOnModels(unlit: number) {
224
            const models = this.state?.models;
×
225
            if (!models) return;
×
226
            for (const model of models) {
×
227
              if (model.shaderInputs?.props?.pbrMaterial !== undefined) {
×
228
                model.shaderInputs.setProps({pbrMaterial: {unlit}});
×
229
              }
230
            }
231
          }
232

233
          updateState(params) {
234
            super.updateState(params);
×
235

236
            const lightingActive = hasActiveLightingEffect(this);
×
237
            const prevLighting = this.state?._lightingWasActive ?? false;
×
238
            if (lightingActive !== prevLighting) {
×
239
              this.setState({_lightingWasActive: lightingActive});
×
240
              // When lighting is turned off, reset models to their
241
              // default unlit state (1); when turned on, enable
242
              // lighting (0). Avoids rebuilding the scenegraph.
243
              this._setUnlitOnModels(lightingActive ? 0 : 1);
×
244
            }
245

246
            // extensionsChanged fires when the shadow shader module is
247
            // added/removed from defaultShaderModules. Models must be
248
            // rebuilt so the new module is compiled into their shaders.
249
            if (params.changeFlags.extensionsChanged && this.state?.scenegraph) {
×
250
              this._updateScenegraph();
×
251
            }
252
          }
253

254
          // Google Photorealistic 3D Tiles (and some other glTF sources)
255
          // mark materials with KHR_materials_unlit, which causes the PBR
256
          // shader to skip all lighting. When the Light & Shadow effect is
257
          // active, override model options to force unlit=0 so that the
258
          // effect can influence the scene. Without the effect, keep the
259
          // default unlit behavior.
260
          _getModelOptions() {
261
            const opts = super._getModelOptions();
×
262
            if (!hasActiveLightingEffect(this)) {
×
263
              return opts;
×
264
            }
265
            opts.modelOptions = {
×
266
              ...opts.modelOptions,
267
              uniforms: {...(opts.modelOptions?.uniforms || {}), unlit: 0}
×
268
            };
269
            return opts;
×
270
          }
271
        };
272
        (_PatchedScenegraphLayer as any).layerName = DefaultClass.layerName || 'ScenegraphLayer';
×
273
        (_PatchedScenegraphLayer as any).defaultProps = DefaultClass.defaultProps;
×
274
      }
275
      return _PatchedScenegraphLayer;
×
276
    }
277
    return super.getSubLayerClass(id, DefaultClass);
×
278
  }
279

280
  /**
281
   * During video export (preserveDrawingBuffer), report the layer as not loaded
282
   * until every selected tile has a renderable sublayer.  Hubble.gl's
283
   * DeckAdapter.onAfterRender checks layer.isLoaded on every top-level layer
284
   * before capturing a frame, so returning false here makes it wait until the
285
   * LOD transition is complete — no frame is captured with missing tiles.
286
   */
287
  get isLoaded(): boolean {
288
    const baseLoaded = super.isLoaded;
×
289
    if (!baseLoaded) {
×
290
      return false;
×
291
    }
292

293
    const gl = this.context?.gl;
×
294
    const isExporting = gl?.getContextAttributes?.()?.preserveDrawingBuffer;
×
295
    if (!isExporting) {
×
296
      return true;
×
297
    }
298

299
    const {tileset3d, layerMap} = this.state as any;
×
300
    if (!tileset3d) {
×
301
      return true;
×
302
    }
303

304
    for (const tile of tileset3d.tiles as Tile3D[]) {
×
305
      if (!tile.selected) {
×
306
        continue;
×
307
      }
308
      const cache = layerMap[tile.id];
×
309
      if (!cache?.layer) {
×
310
        return false;
×
311
      }
312
    }
313

314
    return true;
×
315
  }
316
}
317
(KeplerTile3DLayer as any).layerName = 'KeplerTile3DLayer';
13✔
318

319
export const TILE3D_LAYER_TYPE = LAYER_TYPES.tile3d;
13✔
320

321
export type Tile3DLayerVisConfigSettings = {
322
  opacity: VisConfigNumber;
323
  pointSize: VisConfigNumber;
324
};
325

326
export type Tile3DLayerVisConfig = {
327
  opacity: number;
328
  pointSize: number;
329
};
330

331
export const tile3DVisConfigs = {
13✔
332
  opacity: {
333
    ...LAYER_VIS_CONFIGS.opacity,
334
    defaultValue: 1,
335
    property: 'opacity'
336
  } as VisConfigNumber,
337
  pointSize: {
338
    type: 'number' as const,
339
    defaultValue: 2,
340
    label: 'layerVisConfigs.pointSize',
341
    isRanged: false,
342
    range: [0.5, 20],
343
    step: 0.5,
344
    group: 'display' as const,
345
    property: 'pointSize',
346
    allowCustomValue: false
347
  } as VisConfigNumber
348
};
349

350
const EMPTY_EXTENSIONS: any[] = [];
13✔
351
const DEPTH_TEST_PARAMS = {depthTest: true};
13✔
352

353
function getTile3DProviderFromUrl(url = ''): (typeof TILE3D_PROVIDERS)[string] | null {
×
354
  for (const key of Object.keys(TILE3D_PROVIDERS)) {
×
355
    if (url.includes(TILE3D_PROVIDERS[key].urlKey)) {
×
356
      return TILE3D_PROVIDERS[key];
×
357
    }
358
  }
359
  return null;
×
360
}
361

362
export default class Tile3DLayer extends Layer {
363
  declare visConfigSettings: Tile3DLayerVisConfigSettings;
364

365
  private _cachedDataAndLoader: {
366
    data: string;
367
    loadOptions?: Record<string, unknown>;
368
    loader?: any;
369
    _cacheKey?: string;
370
  } | null = null;
27✔
371

372
  private _layerCallbacks: BindedLayerCallbacks | null = null;
27✔
373
  private _hasFittedBounds = false;
27✔
374

375
  constructor(props: {dataId: string} & Record<string, any>) {
376
    super(props);
27✔
377
    this.registerVisConfig(tile3DVisConfigs);
27✔
378
    this.meta = {};
27✔
379
  }
380

381
  static findDefaultLayerProps(dataset: KeplerDataset): FindDefaultLayerPropsReturnValue {
382
    if (dataset.type !== DatasetType.TILE_3D) {
93!
383
      return {props: []};
93✔
384
    }
385

386
    return {
×
387
      props: [
388
        {
389
          label: dataset.label,
390
          isVisible: true,
391
          color: [255, 255, 255] as [number, number, number]
392
        }
393
      ]
394
    };
395
  }
396

397
  get type(): string {
398
    return TILE3D_LAYER_TYPE;
×
399
  }
400

401
  get name(): string {
402
    return '3D Tile';
27✔
403
  }
404

405
  get requireData(): boolean {
406
    return false;
27✔
407
  }
408

409
  get requiredLayerColumns(): string[] {
410
    return [];
27✔
411
  }
412

413
  get layerIcon(): typeof Tile3DLayerIcon {
414
    return Tile3DLayerIcon;
27✔
415
  }
416

417
  get supportedDatasetTypes(): DatasetType[] {
418
    return [DatasetType.TILE_3D];
×
419
  }
420

421
  get visualChannels() {
422
    return {};
×
423
  }
424

425
  shouldRenderLayer(): boolean {
426
    return Boolean(this.type && this.config.isVisible);
×
427
  }
428

429
  getHoverData(): any {
430
    return null;
×
431
  }
432

433
  formatLayerData(datasets: KeplerDatasets): Record<string, any> {
434
    const {dataId} = this.config;
×
435
    if (!dataId || !datasets[dataId]) {
×
436
      return {};
×
437
    }
438

439
    const dataset = datasets[dataId];
×
440
    this.updateLayerMeta(dataset);
×
441
    const metadata = (dataset.metadata || {}) as Tile3DDatasetMetadata;
×
442

443
    return {
×
444
      tile3dUrl: metadata.tile3dUrl,
445
      tile3dAccessToken: metadata.tile3dAccessToken
446
    };
447
  }
448

449
  updateLayerMeta(dataset: KeplerDataset): void {
450
    if (dataset.type !== DatasetType.TILE_3D) {
×
451
      return;
×
452
    }
453
    const metadata = (dataset.metadata || {}) as Tile3DDatasetMetadata;
×
454
    const provider = getTile3DProviderFromUrl(metadata.tile3dUrl);
×
455
    this.updateMeta({
×
456
      provider,
457
      attribution: provider?.attribution ?? null
×
458
    });
459
  }
460

461
  _getDataAndLoaderOptions(
462
    tileUrl: string,
463
    accessToken?: string
464
  ): {data: string; loadOptions?: Record<string, unknown>; loader?: any} {
465
    const cacheKey = `${tileUrl}::${accessToken || ''}`;
×
466
    if (this._cachedDataAndLoader && this._cachedDataAndLoader._cacheKey === cacheKey) {
×
467
      return this._cachedDataAndLoader;
×
468
    }
469

470
    const provider = getTile3DProviderFromUrl(tileUrl);
×
471
    let result: {data: string; loadOptions?: Record<string, unknown>; loader?: any};
472

473
    if (provider === TILE3D_PROVIDERS.google) {
×
474
      const separator = tileUrl.includes('?') ? '&' : '?';
×
475
      result = {
×
476
        data: `${tileUrl}${separator}key=${accessToken || ''}`,
×
477
        loader: Tiles3DLoader,
478
        loadOptions: {
479
          fetch: {headers: {'X-GOOG-API-KEY': accessToken || ''}}
×
480
        }
481
      };
482
    } else if (provider === TILE3D_PROVIDERS.cesium) {
×
483
      result = {
×
484
        data: tileUrl,
485
        loader: CesiumIonLoader,
486
        loadOptions: {
487
          'cesium-ion': {accessToken}
488
        }
489
      };
490
    } else if (provider === TILE3D_PROVIDERS.arcgis) {
×
491
      result = {
×
492
        data: tileUrl,
493
        loader: I3SLoader,
494
        loadOptions: {
495
          i3s: {useCompressedTextures: false}
496
        }
497
      };
498
    } else {
499
      result = {
×
500
        data: tileUrl,
501
        loader: Tiles3DLoader
502
      };
503
    }
504

505
    this._cachedDataAndLoader = {...result, _cacheKey: cacheKey};
×
506
    return this._cachedDataAndLoader;
×
507
  }
508

509
  _onTilesetLoad = (tileset3d: Tileset3D): void => {
27✔
510
    this._extractBoundsFromTileset(tileset3d);
×
511

512
    const tileUrl = tileset3d.url || '';
×
513
    const provider = getTile3DProviderFromUrl(tileUrl);
×
514
    const isGoogle = provider === TILE3D_PROVIDERS.google;
×
515

516
    if (!this._hasFittedBounds && this.meta?.bounds && !isGoogle) {
×
517
      this._hasFittedBounds = true;
×
518
      this._layerCallbacks?.onFitBounds?.(this.meta.bounds);
×
519
    }
520

521
    if (isGoogle) {
×
522
      tileset3d.options.onTraversalComplete = selectedTiles => {
×
523
        const credits = new Set<string>();
×
524
        selectedTiles.forEach((tile: Tile3D) => {
×
525
          const {copyright} = (tile as any).content?.gltf?.asset || {};
×
526
          if (copyright) copyright.split(';').forEach(c => credits.add(c));
×
527
        });
528
        const title = Array.from(credits).join('; ');
×
529
        if (this.meta?.attribution?.title !== title) {
×
530
          this.updateMeta({
×
531
            attribution: {
532
              ...TILE3D_PROVIDERS.google.attribution,
533
              title
534
            }
535
          });
536
        }
537
        return selectedTiles;
×
538
      };
539
    }
540
  };
541

542
  _extractBoundsFromTileset(tileset3d: Tileset3D): void {
543
    try {
×
544
      const root = tileset3d.root;
×
545
      if (root) {
×
546
        const bbox = (root as any).boundingBox;
×
547
        if (bbox && bbox.length === 2) {
×
548
          const [min, max] = bbox;
×
549
          // bbox = [[westDeg, southDeg, minHeight], [eastDeg, northDeg, maxHeight]]
550
          this.updateMeta({
×
551
            bounds: [min[0], min[1], max[0], max[1]]
552
          });
553
          return;
×
554
        }
555
      }
556
    } catch {
557
      // boundingBox getter may throw for malformed volumes
558
    }
559

560
    // Fallback: approximate bounds from cartographicCenter and zoom
561
    const center = tileset3d.cartographicCenter;
×
562
    if (center) {
×
563
      const lng = center[0];
×
564
      const lat = center[1];
×
565
      const zoom = tileset3d.zoom || 14;
×
566
      // Approximate half-span in degrees based on zoom level
567
      const span = 180 / Math.pow(2, zoom);
×
568
      this.updateMeta({
×
569
        bounds: [lng - span, lat - span, lng + span, lat + span]
570
      });
571
    }
572
  }
573

574
  _onTileLoad = (tile: Tile3D): void => {
27✔
575
    // I3S materials often lack metallicFactor/roughnessFactor.
576
    // PBR spec defaults metallicFactor to 1 (fully metallic),
577
    // causing black surfaces when lighting/shadow effects are active.
578
    const pbr = tile.content?.material?.pbrMetallicRoughness;
×
579
    if (pbr) {
×
580
      if (pbr.metallicFactor === undefined) {
×
581
        pbr.metallicFactor = 0;
×
582
      }
583
      if (pbr.roughnessFactor === undefined) {
×
584
        pbr.roughnessFactor = 1;
×
585
      }
586
    }
587
  };
588

589
  _onTileUnload = (_tile: Tile3D): void => {
27✔
590
    /* noop */
591
  };
592

593
  renderLayer(opts: any): KeplerTile3DLayer[] {
594
    const {data, layerCallbacks} = opts;
×
595
    const {tile3dUrl, tile3dAccessToken} = data || {};
×
596
    if (!tile3dUrl) {
×
597
      return [];
×
598
    }
599

600
    this._layerCallbacks = layerCallbacks || null;
×
601

602
    const {visConfig} = this.config;
×
603
    const defaultLayerProps = this.getDefaultDeckLayerProps(opts);
×
604
    const {
605
      data: tileData,
606
      loadOptions,
607
      loader
608
    } = this._getDataAndLoaderOptions(tile3dUrl, tile3dAccessToken);
×
609

610
    const {color} = this.config;
×
611
    const pointColor: [number, number, number, number] = color
×
612
      ? [color[0], color[1], color[2], 255]
613
      : [255, 255, 255, 255];
614

615
    return [
×
616
      new KeplerTile3DLayer({
617
        id: defaultLayerProps.id,
618
        coordinateSystem: defaultLayerProps.coordinateSystem,
619
        wrapLongitude: defaultLayerProps.wrapLongitude,
620
        visible: defaultLayerProps.visible,
621
        data: tileData,
622
        loader,
623
        loadOptions,
624
        onTilesetLoad: this._onTilesetLoad,
625
        onTileLoad: this._onTileLoad,
626
        onTileUnload: this._onTileUnload,
627
        getPointColor: pointColor,
628
        pointSize: visConfig.pointSize ?? 2,
×
629
        pickable: false,
630
        opacity: visConfig.opacity ?? 1,
×
631
        extensions: EMPTY_EXTENSIONS,
632
        parameters: DEPTH_TEST_PARAMS
633
      })
634
    ];
635
  }
636
}
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