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

visgl / loaders.gl / 25070233425

28 Apr 2026 06:20PM UTC coverage: 59.434% (+0.01%) from 59.423%
25070233425

push

github

web-flow
website: Add tabs for navigating between format docs (#3407)

11310 of 20887 branches covered (54.15%)

Branch coverage included in aggregate %.

89 of 136 new or added lines in 13 files covered. (65.44%)

1742 existing lines in 132 files now uncovered.

23500 of 37682 relevant lines covered (62.36%)

16296.63 hits per line

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

15.74
/modules/deck-layers/src/tile-2d-source-layer.ts
1
// loaders.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {
6
  CompositeLayer,
7
  type CompositeLayerProps,
8
  Layer,
9
  type LayersList,
10
  type PickingInfo,
11
  type GetPickingInfoParams,
12
  _flatten as flatten
13
} from '@deck.gl/core';
14
import {MVTLayer, TileLayer, type TileLayerProps} from '@deck.gl/geo-layers';
15
import {BitmapLayer, GeoJsonLayer, PathLayer} from '@deck.gl/layers';
16
import {createDataSource} from '@loaders.gl/core';
17
import type {
18
  DataSourceOptions,
19
  GetTileDataParameters,
20
  SourceLoader,
21
  TileSource,
22
  TileSourceMetadata
23
} from '@loaders.gl/loader-utils';
24
import {SharedTile2DHeader, Tileset2D, type Tileset2DProps} from '@loaders.gl/tiles';
25
import {Matrix4, type NumericArray} from '@math.gl/core';
26
import {sharedTile2DDeckAdapter} from './shared-tile-2d/deck-tileset-adapter';
27
import {SharedTile2DView} from './shared-tile-2d/shared-tile-2d-view';
28

29
/** Runtime shape used by loaders.gl source-backed tile layers. */
30
export type TileSourceRuntime = TileSource & {
31
  /** Indicates that vector tiles can be rendered in local coordinates. */
32
  localCoordinates?: boolean;
33
  /** MIME type that identifies vector or image payload handling. */
34
  mimeType: string | null;
35
  /** Mutable source options forwarded to loaders.gl tile fetches. */
36
  options: {
37
    table?: {
38
      coordinates?: string;
39
    };
40
  };
41
  /** Source URL used for stable layer ids. */
42
  url?: string;
43
};
44

45
type Tile2DSourceLayerData = string | Blob | TileSourceRuntime;
46

47
/** Props for {@link Tile2DSourceLayer}. */
48
export type Tile2DSourceLayerProps<DataT = unknown> = CompositeLayerProps &
49
  Partial<Omit<TileLayerProps, 'data' | 'renderSubLayers'>> & {
50
    /** URL/blob input or a fully constructed loaders.gl tile source. */
51
    data: Tile2DSourceLayerData;
52
    /** Source factories used to auto-create tile sources from URL/blob inputs. */
53
    sources?: Readonly<SourceLoader[]>;
54
    /** Options forwarded to `createDataSource` when `sources` are supplied. */
55
    sourceOptions?: DataSourceOptions;
56
    /** Optional metadata used by the example overlay and zoom bounds. */
57
    metadata?: TileSourceMetadata | null;
58
    /** Show borders around tiles in the generic shared-tile rendering path. */
59
    showTileBorders?: boolean;
60
    /** Called when a tile payload cannot be fetched or parsed. */
61
    onTileError?: (error: unknown, tileParameters?: unknown) => void;
62
    /** Called when the viewport's currently selected tiles are loaded. */
63
    onTilesLoad?: (tiles: SharedTile2DHeader<DataT>[]) => void;
64
    /** Custom sub-layer factory for non-local shared-tile rendering. */
65
    renderSubLayers?: (
66
      props: Tile2DSourceLayerProps<DataT> & {
67
        id: string;
68
        data: DataT;
69
        _offset: number;
70
        tile: SharedTile2DHeader<DataT>;
71
        tileSource: TileSourceRuntime;
72
      }
73
    ) => Layer | null | LayersList;
74
    /** Maximum tile count kept in cache. */
75
    maxCacheSize?: number | null;
76
    /** Maximum tile cache byte size. */
77
    maxCacheByteSize?: number | null;
78
    /** Minimum zoom level to request. */
79
    minZoom?: number | null;
80
    /** Maximum zoom level to request. */
81
    maxZoom?: number | null;
82
    /** Maximum concurrent requests issued by the shared tileset. */
83
    maxRequests?: number;
84
    /** Debounce interval before issuing queued tile requests. */
85
    debounceTime?: number;
86
    /** Integer zoom offset applied during tile selection. */
87
    zoomOffset?: number;
88
    /** Tile size in pixels. */
89
    tileSize?: number;
90
    /** Bounding box limiting tile generation. */
91
    extent?: number[] | null;
92
    /** Elevation range used during tile selection. */
93
    zRange?: [number, number] | null;
94
    /** Optional model matrix applied by the surrounding layer stack. */
95
    modelMatrix?: NumericArray | null;
96
  };
97

98
/** Picking info returned from {@link Tile2DSourceLayer}. */
99
export type Tile2DSourceLayerPickingInfo<
100
  DataT = any,
101
  SubLayerPickingInfo = PickingInfo
102
> = SubLayerPickingInfo & {
103
  /** Picked tile when a tile sub-layer is hit. */
104
  tile?: SharedTile2DHeader<DataT>;
105
  /** Tile that produced the picked sub-layer. */
106
  sourceTile: SharedTile2DHeader<DataT>;
107
  /** Concrete sub-layer instance that handled the pick. */
108
  sourceTileSubLayer: Layer;
109
};
110

111
type Tile2DSourceLayerState<DataT> = {
112
  resolvedData: TileSourceRuntime | null;
113
  tileset: Tileset2D<DataT, any> | null;
114
  tilesetViews: Map<string, SharedTile2DView<DataT>>;
115
  isLoaded: boolean;
116
  frameNumbers: Map<string, number>;
117
  tileLayers: Map<string, any[]>;
118
  unsubscribeTilesetEvents: (() => void) | null;
119
};
120

UNCOV
121
const TILE2D_LAYER_DEFAULT_OPTION_VALUES = {
6✔
122
  maxCacheSize: null,
123
  maxCacheByteSize: null,
124
  maxZoom: null,
125
  minZoom: null,
126
  tileSize: 256,
127
  extent: null,
128
  maxRequests: 6,
129
  debounceTime: 0,
130
  zoomOffset: 0
131
} as const;
132

133
/**
134
 * Internal deck.gl MVT helper used by `Tile2DSourceLayer`.
135
 *
136
 * This class is not part of the supported public API and is documented only through TSDoc.
137
 */
138
class MVTSourceLoaderLayer extends MVTLayer<any> {
139
  /** Sync the cached vector tile source whenever deck.gl reports a data change. */
140
  updateState(params: any): void {
141
    super.updateState(params);
×
142

143
    const {props, changeFlags} = params;
×
144
    if (changeFlags.dataChanged && props.data) {
×
145
      this.setState({
×
146
        vectorTileSource: props.data,
147
        binary: false
148
      });
149
    }
150
  }
151

152
  /** Fetch tile payloads directly from the wrapped loaders.gl tile source. */
153
  async getTileData(parameters: GetTileDataParameters): Promise<any> {
154
    try {
×
155
      const vectorTileSource = (this.state as any).vectorTileSource as TileSourceRuntime | null;
×
156
      return vectorTileSource ? await vectorTileSource.getTileData(parameters) : null;
×
157
    } catch (error) {
158
      this.props.onTileError?.(error, parameters);
×
159
      return null;
×
160
    }
161
  }
162
}
163

164
/**
165
 * Internal deck.gl layer that renders loaders.gl tile sources through a shared 2D tileset.
166
 *
167
 * It resolves URL/blob inputs using loaders.gl source factories, uses `Tileset2D`
168
 * for shared raster and non-local vector traversal, and falls back to `MVTLayer` for
169
 * local-coordinate vector tile sources.
170
 *
171
 * This class is exported for internal repository use and examples, and is not documented
172
 * beyond these TSDoc comments.
173
 */
174
export class Tile2DSourceLayer<DataT = any> extends CompositeLayer<Tile2DSourceLayerProps<DataT>> {
175
  /** deck.gl layer name used in debugging output. */
UNCOV
176
  static layerName = 'Tile2DSourceLayer';
6✔
177

178
  /** Default props shared by the vector and raster rendering paths. */
UNCOV
179
  static defaultProps = {
6✔
180
    ...TileLayer.defaultProps,
181
    tileSize: 256,
182
    minZoom: null,
183
    maxZoom: null,
184
    maxCacheSize: null,
185
    maxCacheByteSize: null,
186
    maxRequests: 6,
187
    debounceTime: 0,
188
    zoomOffset: 0,
189
    showTileBorders: true,
190
    renderSubLayers: defaultRenderSubLayers
191
  };
192

193
  /** Viewports tracked by id so shared tileset views stay stable across renders. */
UNCOV
194
  private _knownViewports: Map<string, any> = new Map();
7✔
195
  /** Typed deck.gl state for source resolution, traversal views, and tile sublayers. */
UNCOV
196
  state = null as unknown as Tile2DSourceLayerState<DataT>;
7✔
197

198
  /** Initializes local state before props are first rendered. */
199
  initializeState(): void {
200
    this._knownViewports.clear();
×
201
    if (this.context.viewport) {
×
202
      this._knownViewports.set(this._getViewportKey(), this.context.viewport);
×
203
    }
204
    this.state = {
×
205
      resolvedData: null,
206
      tileset: null,
207
      tilesetViews: new Map(),
208
      isLoaded: false,
209
      frameNumbers: new Map(),
210
      tileLayers: new Map(),
211
      unsubscribeTilesetEvents: null
212
    };
213
  }
214

215
  /** Finalizes owned resources and detaches from the shared tileset. */
216
  finalizeState(): void {
217
    this._releaseTileset();
×
218
  }
219

220
  /** Returns whether all visible sub-layers for all tracked views are loaded. */
221
  get isLoaded(): boolean {
222
    const {tilesetViews, tileLayers} = this.state;
×
223
    if (!tilesetViews.size) {
×
224
      return false;
×
225
    }
226
    return Boolean(
×
227
      Array.from(tilesetViews.values()).every(tilesetView =>
228
        tilesetView.selectedTiles?.every(tile => {
×
229
          const cachedLayers = tileLayers.get(tile.id);
×
230
          return (
×
231
            tile.isLoaded &&
×
232
            (!tile.content || !cachedLayers || cachedLayers.every(layer => layer.isLoaded))
233
          );
234
        })
235
      )
236
    );
237
  }
238

239
  /** Triggers updates whenever props, data, or update triggers change. */
240
  shouldUpdateState({changeFlags}: any): boolean {
241
    return changeFlags.somethingChanged;
×
242
  }
243

244
  /** Resolves sources and keeps the shared tileset in sync with current props. */
245
  updateState({props, oldProps, changeFlags}: any): void {
246
    if (this.context.viewport) {
×
247
      this._knownViewports.set(this._getViewportKey(), this.context.viewport);
×
248
    }
249

250
    const previousResolvedData = this.state.resolvedData;
×
251
    let resolvedData = previousResolvedData;
×
252
    const dataChanged =
253
      changeFlags.dataChanged ||
×
254
      props.sources !== oldProps.sources ||
255
      props.sourceOptions !== oldProps.sourceOptions;
256

257
    if (dataChanged) {
×
258
      resolvedData = this._resolveData(props);
×
259
      this.setState({resolvedData});
×
260
    }
261

262
    if (!resolvedData || this.sourceSupportsMVTLayer(resolvedData)) {
×
263
      this._releaseTileset();
×
264
      return;
×
265
    }
266

267
    const resolvedDataChanged = resolvedData !== previousResolvedData;
×
268
    const tileset = this._getOrCreateTileset(resolvedData, resolvedDataChanged);
×
269
    if (tileset !== this.state.tileset) {
×
270
      this.setState({tileset});
×
271
    } else {
272
      tileset.setOptions(this._getTilesetOptions(resolvedData));
×
273
      if (dataChanged) {
×
274
        tileset.reloadAll();
×
275
      } else if (changeFlags.propsOrDataChanged || changeFlags.updateTriggersChanged) {
×
276
        this.state.tileLayers.clear();
×
277
      }
278
    }
279

280
    this._updateTileset();
×
281
  }
282

283
  /** Adds tile references to picking info returned from sub-layers. */
284
  getPickingInfo(params: GetPickingInfoParams): Tile2DSourceLayerPickingInfo<DataT> {
285
    const {sourceLayer} = params;
×
286
    if (!sourceLayer) {
×
287
      throw new Error('Tile2DSourceLayer picking info requires a source layer.');
×
288
    }
289
    const sourceTile: SharedTile2DHeader<DataT> = (sourceLayer.props as any).tile;
×
290
    const info = params.info as Tile2DSourceLayerPickingInfo<DataT>;
×
291
    if (info.picked) {
×
292
      info.tile = sourceTile;
×
293
    }
294
    info.sourceTile = sourceTile;
×
295
    info.sourceTileSubLayer = sourceLayer;
×
296
    return info;
×
297
  }
298

299
  /** Forwards auto-highlight updates to the picked sub-layer. */
300
  protected _updateAutoHighlight(info: Tile2DSourceLayerPickingInfo<DataT>): void {
301
    info.sourceTileSubLayer.updateAutoHighlight(info);
×
302
  }
303

304
  /** Registers additional viewports in multi-view rendering scenarios. */
305
  activateViewport(viewport: any): void {
306
    const viewportKey = viewport.id || 'default';
×
307
    const previousViewport = this._knownViewports.get(viewportKey);
×
308
    this._knownViewports.set(viewportKey, viewport);
×
309
    if (!previousViewport || !viewport.equals(previousViewport)) {
×
310
      this.setNeedsUpdate();
×
311
    }
312
    super.activateViewport(viewport);
×
313
  }
314

315
  /** Filters tile sub-layers based on the active view-specific visibility state. */
316
  filterSubLayer({layer, cullRect}: any) {
317
    if (!this.state.tileset) {
×
318
      return true;
×
319
    }
320
    const {tile} = (layer as Layer<{tile: SharedTile2DHeader<DataT>}>).props;
×
321
    const tilesetView = this._getOrCreateTilesetView(this._getViewportKey());
×
322
    return tilesetView.isTileVisible(
×
323
      tile,
324
      cullRect,
325
      this.props.modelMatrix ? new Matrix4(this.props.modelMatrix) : null
×
326
    );
327
  }
328

329
  /** Render either the local-coordinate MVT path or the shared raster/vector path. */
330
  renderLayers(): Layer | null | LayersList {
331
    const {resolvedData, tileset, tileLayers} = this.state;
×
332
    if (!resolvedData) {
×
333
      return null;
×
334
    }
335

336
    if (this.sourceSupportsMVTLayer(resolvedData)) {
×
337
      resolvedData.options.table = resolvedData.options.table || {};
×
338
      resolvedData.options.table.coordinates = 'local';
×
339
      return this.renderMVTLayer(resolvedData);
×
340
    }
341

342
    if (!tileset) {
×
343
      return null;
×
344
    }
345

346
    resolvedData.options.table = resolvedData.options.table || {};
×
347
    resolvedData.options.table.coordinates = 'wgs84';
×
348

349
    return tileset.tiles.map(tile => {
×
350
      let layers = tileLayers.get(tile.id);
×
351
      if (!tile.isLoaded && !tile.content) {
×
352
        return layers;
×
353
      }
354
      if (!layers) {
×
355
        const rendered = this.props.renderSubLayers
×
356
          ? this.props.renderSubLayers({
357
              ...this.props,
358
              ...this.getSubLayerProps({
359
                id: tile.id,
360
                updateTriggers: this.props.updateTriggers
361
              }),
362
              data: tile.content as DataT,
363
              _offset: 0,
364
              tile,
365
              tileSource: resolvedData
366
            } as any)
367
          : null;
368
        layers = this._flattenTileLayers(rendered);
×
369
        tileLayers.set(tile.id, layers);
×
370
      }
371
      return layers;
×
372
    });
373
  }
374

375
  /** Check if the current source supports MVT layer rendering with local coordinates. */
376
  sourceSupportsMVTLayer(tileSource: TileSourceRuntime): boolean {
UNCOV
377
    return (
2✔
378
      tileSource.mimeType === 'application/vnd.mapbox-vector-tile' &&
3✔
379
      Boolean(tileSource.localCoordinates)
380
    );
381
  }
382

383
  /** Render vector tiles through `MVTLayer` when local coordinate support is required. */
384
  renderMVTLayer(tileSource: TileSourceRuntime) {
385
    const {showTileBorders, metadata, onTilesLoad, onTileError} = this.props;
×
386
    const minZoom = metadata?.minZoom || 0;
×
387
    const maxZoom = metadata?.maxZoom || 30;
×
388
    const devicePixelRatio = this.context.device.getCanvasContext().getDevicePixelRatio();
×
389

390
    return [
×
391
      new MVTSourceLoaderLayer({
392
        id: `${this.props.id}-mvt`,
393
        data: tileSource as any,
394
        getLineColor: [0, 0, 0],
395
        getLineWidth: 1,
396
        getFillColor: [100, 120, 140],
397
        lineWidthUnits: 'pixels',
398
        pickable: true,
399
        autoHighlight: true,
400
        onViewportLoad: onTilesLoad,
401
        onTileError,
402
        minZoom,
403
        maxZoom,
404
        tileSize: 256,
405
        zoomOffset: devicePixelRatio === 1 ? -1 : 0,
×
406
        showTileBorders
407
      } as any)
408
    ];
409
  }
410

411
  /** Resolves the shared tileset configuration from current layer props. */
412
  private _getTilesetOptions(
413
    tileSource: TileSourceRuntime
414
  ): Omit<Tileset2DProps<DataT, any>, 'tileSource'> & {tileSource: TileSourceRuntime} {
415
    const {
416
      tileSize,
417
      maxCacheSize,
418
      maxCacheByteSize,
419
      extent,
420
      maxZoom,
421
      minZoom,
422
      maxRequests,
423
      debounceTime,
424
      zoomOffset
425
    } = this.props;
×
426
    const devicePixelRatio = this.context.device.getCanvasContext().getDevicePixelRatio();
×
427
    const effectiveZoomOffset = this._isDefaultOptionValue(
×
428
      zoomOffset,
429
      TILE2D_LAYER_DEFAULT_OPTION_VALUES.zoomOffset
430
    )
431
      ? devicePixelRatio === 1
×
432
        ? -1
433
        : 0
434
      : zoomOffset;
435

436
    const options = {
×
437
      tileSource,
438
      adapter: sharedTile2DDeckAdapter,
439
      tileSize,
440
      zoomOffset: effectiveZoomOffset,
441
      onTileLoad: () => {},
442
      onTileError: () => {},
443
      onTileUnload: () => {}
444
    } as Omit<Tileset2DProps<DataT, any>, 'getTileData'>;
445

446
    this._assignTilesetOptionIfExplicit(
×
447
      options,
448
      'maxCacheSize',
449
      maxCacheSize,
450
      TILE2D_LAYER_DEFAULT_OPTION_VALUES.maxCacheSize
451
    );
452
    this._assignTilesetOptionIfExplicit(
×
453
      options,
454
      'maxCacheByteSize',
455
      maxCacheByteSize,
456
      TILE2D_LAYER_DEFAULT_OPTION_VALUES.maxCacheByteSize
457
    );
458
    this._assignTilesetOptionIfExplicit(
×
459
      options,
460
      'maxZoom',
461
      maxZoom,
462
      TILE2D_LAYER_DEFAULT_OPTION_VALUES.maxZoom
463
    );
464
    this._assignTilesetOptionIfExplicit(
×
465
      options,
466
      'minZoom',
467
      minZoom,
468
      TILE2D_LAYER_DEFAULT_OPTION_VALUES.minZoom
469
    );
470
    this._assignTilesetOptionIfExplicit(
×
471
      options,
472
      'extent',
473
      extent,
474
      TILE2D_LAYER_DEFAULT_OPTION_VALUES.extent
475
    );
476
    this._assignTilesetOptionIfExplicit(
×
477
      options,
478
      'maxRequests',
479
      maxRequests,
480
      TILE2D_LAYER_DEFAULT_OPTION_VALUES.maxRequests
481
    );
482
    this._assignTilesetOptionIfExplicit(
×
483
      options,
484
      'debounceTime',
485
      debounceTime,
486
      TILE2D_LAYER_DEFAULT_OPTION_VALUES.debounceTime
487
    );
488
    return options as Omit<Tileset2DProps<DataT, any>, 'getTileData'> & {
×
489
      tileSource: TileSourceRuntime;
490
    };
491
  }
492

493
  /** Creates or reuses the internal shared tileset for the current source. */
494
  private _getOrCreateTileset(
495
    tileSource: TileSourceRuntime,
496
    resolvedDataChanged: boolean
497
  ): Tileset2D<DataT, any> {
498
    if (!this.state.tileset || resolvedDataChanged) {
×
499
      this._releaseTileset();
×
500
      const tileset = Tileset2D.fromTileSource<DataT>(
×
501
        tileSource,
502
        this._getTilesetOptions(tileSource)
503
      );
504
      this.setState({
×
505
        tileset,
506
        tilesetViews: new Map(),
507
        tileLayers: new Map(),
508
        frameNumbers: new Map(),
509
        unsubscribeTilesetEvents: tileset.subscribe({
510
          onTileLoad: this._onTileLoad.bind(this),
511
          onTileError: this._onTileError.bind(this),
512
          onTileUnload: this._onTileUnload.bind(this),
513
          onUpdate: () => this.setNeedsUpdate(),
×
514
          onError: error => this.raiseError(error, 'loading TileSource metadata')
×
515
        })
516
      });
517
      return tileset;
×
518
    }
519

520
    return this.state.tileset;
×
521
  }
522

523
  /** Tears down subscriptions and per-view state for the outgoing tileset. */
524
  private _releaseTileset(): void {
525
    this.state?.unsubscribeTilesetEvents?.();
×
526
    for (const tilesetView of this.state?.tilesetViews?.values?.() || []) {
×
527
      tilesetView.finalize();
×
528
    }
529
    this.setState?.({
×
530
      tileset: null,
531
      tilesetViews: new Map(),
532
      tileLayers: new Map(),
533
      frameNumbers: new Map(),
534
      unsubscribeTilesetEvents: null
535
    });
536
  }
537

538
  /** Updates per-view traversal state for all known viewports. */
539
  private _updateTileset(): void {
540
    const {tileset} = this.state;
×
541
    if (!tileset) {
×
542
      return;
×
543
    }
544

545
    const {zRange = null, modelMatrix = null} = this.props;
×
546
    let anyTilesetChanged = false;
×
547

548
    for (const [viewportKey, viewport] of this._knownViewports) {
×
549
      const tilesetView = this._getOrCreateTilesetView(viewportKey);
×
550
      const frameNumber = tilesetView.update(viewport, {zRange, modelMatrix});
×
551
      const previousFrameNumber = this.state.frameNumbers.get(viewportKey);
×
552
      const tilesetChanged = previousFrameNumber !== frameNumber;
×
553
      anyTilesetChanged ||= tilesetChanged;
×
554

555
      if (tilesetView.isLoaded && tilesetChanged) {
×
556
        this._onViewportLoad(tilesetView);
×
557
      }
558
      if (tilesetChanged) {
×
559
        this.state.frameNumbers.set(viewportKey, frameNumber);
×
560
      }
561
    }
562

563
    const nextIsLoaded = this.isLoaded;
×
564
    const loadingStateChanged = this.state.isLoaded !== nextIsLoaded;
×
565
    if (loadingStateChanged) {
×
566
      for (const tilesetView of this.state.tilesetViews.values()) {
×
567
        if (tilesetView.isLoaded) {
×
568
          this._onViewportLoad(tilesetView);
×
569
        }
570
      }
571
    }
572

573
    if (anyTilesetChanged) {
×
574
      this.setState({frameNumbers: new Map(this.state.frameNumbers)});
×
575
    }
576
    this.state.isLoaded = nextIsLoaded;
×
577
  }
578

579
  /** Emits the viewport-load callback for one view. */
580
  private _onViewportLoad(tilesetView: SharedTile2DView<DataT>): void {
581
    if (tilesetView.selectedTiles) {
×
582
      this.props.onTilesLoad?.(tilesetView.selectedTiles);
×
583
    }
584
  }
585

586
  /** Clears cached sub-layers when a tile loads. */
587
  private _onTileLoad(tile: SharedTile2DHeader<DataT>): void {
588
    this.state.tileLayers.delete(tile.id);
×
589
    this.setNeedsUpdate();
×
590
  }
591

592
  /** Clears cached sub-layers when a tile errors. */
593
  private _onTileError(error: any, tile: SharedTile2DHeader<DataT>): void {
594
    this.state.tileLayers.delete(tile.id);
×
595
    this.props.onTileError?.(error, tile);
×
596
    this.setNeedsUpdate();
×
597
  }
598

599
  /** Removes cached sub-layers when a tile is evicted. */
600
  private _onTileUnload(tile: SharedTile2DHeader<DataT>): void {
601
    this.state.tileLayers.delete(tile.id);
×
602
  }
603

604
  /** Returns the active viewport key used to isolate per-view traversal state. */
605
  private _getViewportKey(): string {
606
    return this.context.viewport?.id || 'default';
×
607
  }
608

609
  /** Returns the per-viewport traversal state, creating it on demand. */
610
  private _getOrCreateTilesetView(viewportKey: string): SharedTile2DView<DataT> {
611
    let tilesetView = this.state.tilesetViews.get(viewportKey);
×
612
    if (!tilesetView) {
×
613
      const tileset = this.state.tileset;
×
614
      if (!tileset) {
×
615
        throw new Error('Tile2DSourceLayer tileset was not initialized.');
×
616
      }
617
      tilesetView = new SharedTile2DView(tileset);
×
618
      this.state.tilesetViews.set(viewportKey, tilesetView);
×
619
    }
620
    return tilesetView;
×
621
  }
622

623
  /** Resolves the `data` prop to a concrete loaders.gl tile source. */
624
  private _resolveData(props: Tile2DSourceLayerProps<DataT>): TileSourceRuntime | null {
UNCOV
625
    const {data, sources, sourceOptions = {}} = props;
2✔
UNCOV
626
    if (isTileSourceRuntime(data)) {
2✔
UNCOV
627
      return data;
1✔
628
    }
UNCOV
629
    if ((typeof data === 'string' || data instanceof Blob) && sources?.length) {
1!
UNCOV
630
      return createDataSource(data, sources, sourceOptions) as unknown as TileSourceRuntime;
1✔
631
    }
632
    throw new Error('Tile2DSourceLayer requires `sources` for URL/blob inputs.');
×
633
  }
634

635
  /** Copies a tileset option only when the layer prop was explicitly set. */
636
  private _assignTilesetOptionIfExplicit(
637
    options: Record<string, unknown>,
638
    key: string,
639
    value: unknown,
640
    defaultValue: unknown
641
  ): void {
642
    if (!this._isDefaultOptionValue(value, defaultValue)) {
×
643
      options[key] = value;
×
644
    }
645
  }
646

647
  /** Tests whether a layer prop still has its default value. */
648
  private _isDefaultOptionValue(value: unknown, defaultValue: unknown): boolean {
649
    if (Array.isArray(value) || Array.isArray(defaultValue)) {
×
650
      return (
×
651
        Array.isArray(value) &&
×
652
        Array.isArray(defaultValue) &&
653
        value.length === defaultValue.length &&
654
        value.every((entry, index) => entry === defaultValue[index])
×
655
      );
656
    }
657
    return value === defaultValue;
×
658
  }
659

660
  /** Normalizes nested render output into a flat tile sub-layer array. */
661
  private _flattenTileLayers(rendered: Layer | null | LayersList): any[] {
662
    return flatten(rendered as any, Boolean) as any[];
×
663
  }
664
}
665

666
/**
667
 * Default sublayer render callback for the shared raster/vector tile path.
668
 *
669
 * Renders vector tiles with `GeoJsonLayer`, image tiles with `BitmapLayer`, and
670
 * optional debug tile borders with `PathLayer`.
671
 */
672
function defaultRenderSubLayers<DataT>(
673
  props: Tile2DSourceLayerProps<DataT> & {
674
    id: string;
675
    data: DataT;
676
    _offset: number;
677
    tile: SharedTile2DHeader<DataT>;
678
    tileSource: TileSourceRuntime;
679
  }
680
) {
UNCOV
681
  const {tileSource, showTileBorders, minZoom, maxZoom, tile} = props;
2✔
682
  const {
683
    index: {z: zoom}
UNCOV
684
  } = tile;
2✔
685

UNCOV
686
  const layers: any[] = [];
2✔
UNCOV
687
  const resolvedMinZoom = minZoom ?? 0;
2✔
UNCOV
688
  const resolvedMaxZoom = maxZoom ?? 30;
2✔
689
  const borderColor =
UNCOV
690
    zoom <= resolvedMinZoom || zoom >= resolvedMaxZoom ? [255, 0, 0, 255] : [0, 0, 255, 255];
2!
691

UNCOV
692
  switch (tileSource.mimeType) {
2!
693
    case 'application/vnd.mapbox-vector-tile':
694
    case 'application/vnd.maplibre-tile':
UNCOV
695
      layers.push(
1✔
696
        new GeoJsonLayer(
697
          props as any,
698
          {
699
            id: `${props.id}-geojson`,
700
            data: props.data as any,
701
            pickable: true,
702
            autoHighlight: true,
703
            lineWidthScale: 500,
704
            lineWidthMinPixels: 0.5,
705
            getFillColor: [100, 120, 140, 255],
706
            highlightColor: [0, 0, 200, 255]
707
          } as any
708
        )
709
      );
UNCOV
710
      break;
1✔
711

712
    case 'image/png':
713
    case 'image/jpeg':
714
    case 'image/webp':
715
    case 'image/avif':
UNCOV
716
      if ('west' in tile.bbox) {
1!
UNCOV
717
        layers.push(
1✔
718
          new BitmapLayer(
719
            props as any,
720
            {
721
              data: null as any,
722
              image: props.data,
723
              bounds: [tile.bbox.west, tile.bbox.south, tile.bbox.east, tile.bbox.north],
724
              pickable: true
725
            } as any
726
          )
727
        );
728
      }
UNCOV
729
      break;
1✔
730

731
    default:
732
      break;
×
733
  }
734

UNCOV
735
  if (showTileBorders && 'west' in tile.bbox) {
2!
736
    layers.push(
×
737
      new PathLayer(
738
        props as any,
739
        {
740
          id: `${props.id}-border`,
741
          data: [
742
            {
743
              path: [
744
                [tile.bbox.west, tile.bbox.south],
745
                [tile.bbox.west, tile.bbox.north],
746
                [tile.bbox.east, tile.bbox.north],
747
                [tile.bbox.east, tile.bbox.south],
748
                [tile.bbox.west, tile.bbox.south]
749
              ]
750
            }
751
          ],
752
          getPath: (d: any) => d.path,
×
753
          getColor: borderColor as any,
754
          getWidth: 1,
755
          widthMinPixels: 1
756
        } as any
757
      )
758
    );
759
  }
760

UNCOV
761
  return layers;
2✔
762
}
763

764
function isTileSourceRuntime(value: unknown): value is TileSourceRuntime {
UNCOV
765
  return Boolean(
2✔
766
    value &&
7✔
767
      typeof value === 'object' &&
768
      'getTileData' in value &&
769
      'getMetadata' in value &&
770
      !('initialize' in value)
771
  );
772
}
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