diff --git a/docs/api-reference/geo-layers/tile-layer.md b/docs/api-reference/geo-layers/tile-layer.md index f0326a28cfe..0997d4ef04e 100644 --- a/docs/api-reference/geo-layers/tile-layer.md +++ b/docs/api-reference/geo-layers/tile-layer.md @@ -473,6 +473,28 @@ const quadkeyTileLayer = new TileLayer({ }); ``` +## Methods + +### `getTileLoadingState()` {#gettileloadingstate} + +Returns granular loading counts for the tiles in the current viewport. Unlike `layer.isLoaded`, which is `true` once all tile requests settle (including failures), this method distinguishes between tiles that loaded successfully, failed, and are still pending. + +Returns a `TileLoadingState` object: + +| Field | Type | Description | +| --- | --- | --- | +| `total` | `number` | Total tiles in the current viewport | +| `loaded` | `number` | Tiles that loaded successfully (have content) | +| `failed` | `number` | Tiles that settled with no content (404, error, etc.) | +| `pending` | `number` | Tiles still in flight | + +```ts +import type {TileLoadingState} from '@deck.gl/geo-layers'; + +const state: TileLoadingState = tileLayer.getTileLoadingState(); +console.log(`${state.loaded}/${state.total} tiles loaded, ${state.failed} failed`); +``` + ## Source [modules/geo-layers/src/tile-layer](https://github.com/visgl/deck.gl/tree/master/modules/geo-layers/src/tile-layer) diff --git a/modules/geo-layers/src/index.ts b/modules/geo-layers/src/index.ts index e9b8fcaba2d..2dbe29d6d60 100644 --- a/modules/geo-layers/src/index.ts +++ b/modules/geo-layers/src/index.ts @@ -27,7 +27,7 @@ export type {H3ClusterLayerProps} from './h3-layers/h3-cluster-layer'; export type {H3HexagonLayerProps} from './h3-layers/h3-hexagon-layer'; export type {GreatCircleLayerProps} from './great-circle-layer/great-circle-layer'; export type {S2LayerProps} from './s2-layer/s2-layer'; -export type {TileLayerProps, TileLayerPickingInfo} from './tile-layer/tile-layer'; +export type {TileLayerProps, TileLayerPickingInfo, TileLoadingState} from './tile-layer/tile-layer'; export type {TripsLayerProps} from './trips-layer/trips-layer'; export type {QuadkeyLayerProps} from './quadkey-layer/quadkey-layer'; export type {TerrainLayerProps} from './terrain-layer/terrain-layer'; diff --git a/modules/geo-layers/src/tile-layer/tile-layer.ts b/modules/geo-layers/src/tile-layer/tile-layer.ts index de5e6b3a8fb..ea9b34025ad 100644 --- a/modules/geo-layers/src/tile-layer/tile-layer.ts +++ b/modules/geo-layers/src/tile-layer/tile-layer.ts @@ -18,6 +18,13 @@ import {GeoJsonLayer} from '@deck.gl/layers'; import {LayersList} from '@deck.gl/core'; import type {TileLoadProps, ZRange} from '../tileset-2d/index'; + +export type TileLoadingState = { + total: number; + loaded: number; + failed: number; + pending: number; +}; import { Tileset2D, Tile2DHeader, @@ -222,6 +229,19 @@ export default class TileLayer extends ); } + getTileLoadingState(): TileLoadingState { + const selectedTiles: Tile2DHeader[] = this.state?.tileset?.selectedTiles ?? []; + let loaded = 0; + let failed = 0; + let pending = 0; + for (const tile of selectedTiles) { + if (!tile.isLoaded) pending++; + else if (tile.content === null) failed++; + else loaded++; + } + return {total: selectedTiles.length, loaded, failed, pending}; + } + shouldUpdateState({changeFlags}): boolean { return changeFlags.somethingChanged; } diff --git a/test/modules/geo-layers/get-tile-loading-state.spec.ts b/test/modules/geo-layers/get-tile-loading-state.spec.ts new file mode 100644 index 00000000000..fd505029acc --- /dev/null +++ b/test/modules/geo-layers/get-tile-loading-state.spec.ts @@ -0,0 +1,126 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {test, expect} from 'vitest'; +import {TileLayer} from '@deck.gl/geo-layers'; + +const TEST_DATA = [{position: [0, 0]}, {position: [1, 1]}]; + +test('TileLayer#getTileLoadingState - empty layer', () => { + const layer = new TileLayer({ + data: 'https://example.com/tiles/{z}/{x}/{y}', + getTileData: () => null + }); + + const state = layer.getTileLoadingState(); + + expect(state.total).toBe(0); + expect(state.loaded).toBe(0); + expect(state.failed).toBe(0); + expect(state.pending).toBe(0); +}); + +test('TileLayer#getTileLoadingState - all tiles loaded successfully', () => { + const layer = new TileLayer({ + id: 'test-layer', + data: 'https://example.com/tiles/{z}/{x}/{y}', + getTileData: async () => TEST_DATA + }); + + layer.state = { + tileset: { + selectedTiles: [ + {isLoaded: true, content: TEST_DATA}, + {isLoaded: true, content: TEST_DATA}, + {isLoaded: true, content: TEST_DATA} + ] + } + } as any; + + const state = layer.getTileLoadingState(); + + expect(state.total).toBe(3); + expect(state.loaded).toBe(3); + expect(state.failed).toBe(0); + expect(state.pending).toBe(0); +}); + +test('TileLayer#getTileLoadingState - some tiles failed', () => { + const layer = new TileLayer({ + id: 'test-layer', + data: 'https://example.com/tiles/{z}/{x}/{y}', + getTileData: async () => TEST_DATA + }); + + layer.state = { + tileset: { + selectedTiles: [ + {isLoaded: true, content: TEST_DATA}, + {isLoaded: true, content: null}, + {isLoaded: true, content: TEST_DATA}, + {isLoaded: true, content: null} + ] + } + } as any; + + const state = layer.getTileLoadingState(); + + expect(state.total).toBe(4); + expect(state.loaded).toBe(2); + expect(state.failed).toBe(2); + expect(state.pending).toBe(0); +}); + +test('TileLayer#getTileLoadingState - tiles still loading', () => { + const layer = new TileLayer({ + id: 'test-layer', + data: 'https://example.com/tiles/{z}/{x}/{y}', + getTileData: async () => TEST_DATA + }); + + layer.state = { + tileset: { + selectedTiles: [ + {isLoaded: true, content: TEST_DATA}, + {isLoaded: false, content: null}, + {isLoaded: false, content: null}, + {isLoaded: true, content: null} + ] + } + } as any; + + const state = layer.getTileLoadingState(); + + expect(state.total).toBe(4); + expect(state.loaded).toBe(1); + expect(state.failed).toBe(1); + expect(state.pending).toBe(2); +}); + +test('TileLayer#getTileLoadingState - all tiles failed', () => { + const layer = new TileLayer({ + id: 'test-layer', + data: 'https://example.com/tiles/{z}/{x}/{y}', + getTileData: async () => { + throw new Error('Network error'); + } + }); + + layer.state = { + tileset: { + selectedTiles: [ + {isLoaded: true, content: null}, + {isLoaded: true, content: null}, + {isLoaded: true, content: null} + ] + } + } as any; + + const state = layer.getTileLoadingState(); + + expect(state.total).toBe(3); + expect(state.loaded).toBe(0); + expect(state.failed).toBe(3); + expect(state.pending).toBe(0); +}); diff --git a/test/modules/geo-layers/index.ts b/test/modules/geo-layers/index.ts index 5521444b3ba..abf473bd0c3 100644 --- a/test/modules/geo-layers/index.ts +++ b/test/modules/geo-layers/index.ts @@ -44,5 +44,6 @@ import './tile-3d-layer'; import './terrain-layer.spec'; import './mvt-layer.spec'; import './geohash-layer.spec'; +import './get-tile-loading-state.spec'; import './tileset-2d';