-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat(geo-layers): Add getTileLoadingState for granular tile loading info #10372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
akre54
wants to merge
4
commits into
visgl:master
Choose a base branch
from
akre54:fix/tile-isloaded-bug
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
67229a2
feat(geo-layers): Add getTileLoadingState utility for granular tile l…
bf1bf57
fix: Update tests to use vitest and fix file locations
df2931b
refactor(geo-layers): getTileLoadingState as TileLayer method
akre54 5a07c8a
fix(test): format get-tile-loading-state.spec.ts with prettier
akre54 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
185 changes: 185 additions & 0 deletions
185
docs/api-reference/geo-layers/get-tile-loading-state.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| # getTileLoadingState | ||
|
|
||
| Get detailed loading state for tiles in a TileLayer's viewport. | ||
|
|
||
| ```typescript | ||
| import {getTileLoadingState} from '@deck.gl/geo-layers'; | ||
| import type {TileLoadingState} from '@deck.gl/geo-layers'; | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| ```typescript | ||
| import {Deck} from '@deck.gl/core'; | ||
| import {TileLayer, getTileLoadingState} from '@deck.gl/geo-layers'; | ||
|
|
||
| const tileLayer = new TileLayer({ | ||
| data: 'https://tile.example.com/tiles/{z}/{x}/{y}.json', | ||
| getTileData: async ({index, id, bbox}) => { | ||
| const response = await fetch(getURLFromTemplate(id, index)); | ||
| if (!response.ok) throw new Error('Tile not found'); | ||
| return await response.json(); | ||
| }, | ||
| renderSubLayers: props => { | ||
| return new ScatterplotLayer(props); | ||
| } | ||
| }); | ||
|
|
||
| // Check detailed loading state | ||
| const state = getTileLoadingState(tileLayer); | ||
|
|
||
| console.log(`${state.loaded}/${state.total} tiles loaded`); | ||
| console.log(`${state.failed} tiles failed`); | ||
| console.log(`${state.percentLoaded}% complete`); | ||
|
|
||
| if (state.isComplete && !state.isSuccess) { | ||
| console.warn('Some tiles failed to load'); | ||
| } | ||
| ``` | ||
|
|
||
| ## Parameters | ||
|
|
||
| ### `layer` (TileLayer) | ||
|
|
||
| A TileLayer instance to inspect. | ||
|
|
||
| ## Returns | ||
|
|
||
| `TileLoadingState` object with the following properties: | ||
|
|
||
| - **`total`** (`number`): Total number of tiles in the current viewport | ||
| - **`loaded`** (`number`): Number of tiles that loaded successfully (with content) | ||
| - **`failed`** (`number`): Number of tiles that failed to load (404, network error, etc.) | ||
| - **`pending`** (`number`): Number of tiles still loading | ||
| - **`percentLoaded`** (`number`): Percentage of successfully loaded tiles (0-100) | ||
| - **`isComplete`** (`boolean`): True if all tile requests are settled (loaded or failed) | ||
| - **`isSuccess`** (`boolean`): True if all tiles loaded successfully (no failures) | ||
|
|
||
| ## Understanding layer.isLoaded vs getTileLoadingState | ||
|
|
||
| deck.gl's `layer.isLoaded` property returns `true` once all tile requests are "settled" (completed or failed). This is **intentional** to prevent waiting forever for tiles that will never load (404s, network errors, missing tiles, etc.). | ||
|
|
||
| From the source code comments: | ||
|
|
||
| > "Error / empty tiles resolve to `content === null`. Once Tile2DHeader marks those requests as loaded, do not wait for generated sublayers because there is nothing to render for that tile and `tile.layers` will remain null." | ||
|
|
||
| ### When to use each | ||
|
|
||
| **Use `layer.isLoaded`** when you want to know: | ||
| - Are all tile requests complete? (boolean) | ||
| - Is the layer ready to render? | ||
| - Should I wait or proceed with what's available? | ||
|
|
||
| **Use `getTileLoadingState()`** when you need: | ||
| - How many tiles loaded vs failed? (granular counts) | ||
| - Progress percentage for a loading indicator | ||
| - To distinguish between "loading", "loaded successfully", and "loaded with errors" | ||
| - To show error states in the UI | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Progress Bar | ||
|
|
||
| ```typescript | ||
| function ProgressBar({tileLayer}) { | ||
| const state = getTileLoadingState(tileLayer); | ||
|
|
||
| return ( | ||
| <div className="progress-bar"> | ||
| <div className="progress" style={{width: `${state.percentLoaded}%`}} /> | ||
| <span>{state.loaded}/{state.total} tiles loaded</span> | ||
| {state.failed > 0 && ( | ||
| <span className="error">{state.failed} failed</span> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ### Wait for Complete Load | ||
|
|
||
| ```typescript | ||
| // Wait for all tiles to load successfully | ||
| async function waitForTiles(layer: TileLayer): Promise<void> { | ||
| return new Promise((resolve, reject) => { | ||
| const check = () => { | ||
| const state = getTileLoadingState(layer); | ||
|
|
||
| if (!state.isComplete) { | ||
| // Still loading, check again | ||
| requestAnimationFrame(check); | ||
| return; | ||
| } | ||
|
|
||
| if (state.isSuccess) { | ||
| resolve(); | ||
| } else { | ||
| reject(new Error(`${state.failed} tiles failed to load`)); | ||
| } | ||
| }; | ||
|
|
||
| check(); | ||
| }); | ||
| } | ||
| ``` | ||
|
|
||
| ### Video Export | ||
|
|
||
| ```typescript | ||
| // For video export, you might want to proceed even if some tiles failed | ||
| async function captureFrame(deck: Deck): Promise<void> { | ||
| const tileLayers = deck.props.layers.filter(l => l instanceof TileLayer); | ||
|
|
||
| // Check if all tile requests are settled (loaded or failed) | ||
| const allSettled = tileLayers.every(layer => { | ||
| const state = getTileLoadingState(layer); | ||
| return state.isComplete; | ||
| }); | ||
|
|
||
| if (!allSettled) { | ||
| // Wait for tiles to finish loading or failing | ||
| await new Promise(resolve => setTimeout(resolve, 100)); | ||
| return captureFrame(deck); | ||
| } | ||
|
|
||
| // Proceed with frame capture (may have missing tiles) | ||
| const canvas = deck.canvas as HTMLCanvasElement; | ||
| const imageData = canvas.toDataURL('image/png'); | ||
|
|
||
| // Log any failures | ||
| tileLayers.forEach(layer => { | ||
| const state = getTileLoadingState(layer); | ||
| if (state.failed > 0) { | ||
| console.warn(`Layer ${layer.id}: ${state.failed} tiles failed`); | ||
| } | ||
| }); | ||
| } | ||
| ``` | ||
|
|
||
| ### Error Recovery | ||
|
|
||
| ```typescript | ||
| function TileLayerWithRetry({...props}) { | ||
| const [retryCount, setRetryCount] = useState(0); | ||
| const layerRef = useRef<TileLayer>(null); | ||
|
|
||
| useEffect(() => { | ||
| if (!layerRef.current) return; | ||
|
|
||
| const state = getTileLoadingState(layerRef.current); | ||
|
|
||
| if (state.isComplete && !state.isSuccess && retryCount < 3) { | ||
| console.log(`${state.failed} tiles failed, retrying...`); | ||
| setRetryCount(retryCount + 1); | ||
| // Trigger layer update to retry failed tiles | ||
| layerRef.current.setNeedsUpdate(); | ||
| } | ||
| }, [retryCount]); | ||
|
|
||
| return <TileLayer ref={layerRef} {...props} />; | ||
| } | ||
| ``` | ||
|
|
||
| ## Source | ||
|
|
||
| [modules/geo-layers/src/tile-layer/get-tile-loading-state.ts](https://github.com/visgl/deck.gl/tree/master/modules/geo-layers/src/tile-layer/get-tile-loading-state.ts) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
modules/geo-layers/src/tile-layer/get-tile-loading-state.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| // deck.gl | ||
| // SPDX-License-Identifier: MIT | ||
| // Copyright (c) vis.gl contributors | ||
|
|
||
| import type {TileLayer} from './tile-layer'; | ||
| import type {Tile2DHeader} from '../tileset-2d/tile-2d-header'; | ||
|
|
||
| export type TileLoadingState = { | ||
|
akre54 marked this conversation as resolved.
Outdated
|
||
| /** Total number of tiles in the current viewport */ | ||
| total: number; | ||
| /** Number of tiles that have loaded successfully (with content) */ | ||
| loaded: number; | ||
| /** Number of tiles that failed to load (isLoaded but content is null) */ | ||
| failed: number; | ||
| /** Number of tiles still loading */ | ||
| pending: number; | ||
| /** Percentage of tiles loaded (loaded / total) */ | ||
| percentLoaded: number; | ||
| /** True if all tiles are done loading (loaded or failed) */ | ||
| isComplete: boolean; | ||
| /** True if all tiles loaded successfully (no failures) */ | ||
| isSuccess: boolean; | ||
| }; | ||
|
|
||
| /** | ||
| * Get detailed loading state for tiles in a TileLayer's viewport. | ||
| * | ||
| * Unlike `layer.isLoaded` which only returns a boolean, this function provides | ||
| * granular information about tile loading progress, including: | ||
| * - How many tiles loaded successfully vs failed | ||
| * - Loading percentage for progress UI | ||
| * - Whether any tiles are still pending | ||
| * | ||
| * @param layer - A TileLayer instance | ||
| * @returns Detailed tile loading state | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const state = getTileLoadingState(tileLayer); | ||
| * console.log(`${state.loaded}/${state.total} tiles loaded`); | ||
| * console.log(`${state.failed} tiles failed`); | ||
| * console.log(`${state.percentLoaded}% complete`); | ||
| * | ||
| * if (state.isComplete && !state.isSuccess) { | ||
| * console.warn('Some tiles failed to load'); | ||
| * } | ||
| * ``` | ||
| * | ||
| * @note | ||
| * deck.gl's `layer.isLoaded` returns `true` once all tile requests are "settled" | ||
| * (completed or failed). This is intentional to prevent waiting forever for tiles | ||
| * that will never load (404s, network errors, etc.). Use this function to distinguish | ||
| * between successfully loaded tiles and failed tiles. | ||
| */ | ||
| export function getTileLoadingState<T>(layer: TileLayer<T>): TileLoadingState { | ||
|
akre54 marked this conversation as resolved.
Outdated
|
||
| const tileset = layer.state?.tileset; | ||
| const selectedTiles: Tile2DHeader<T>[] = tileset?.selectedTiles || []; | ||
|
|
||
| if (selectedTiles.length === 0) { | ||
| return { | ||
| total: 0, | ||
| loaded: 0, | ||
| failed: 0, | ||
| pending: 0, | ||
| percentLoaded: 100, | ||
| isComplete: true, | ||
| isSuccess: true | ||
| }; | ||
| } | ||
|
|
||
| let loaded = 0; | ||
| let failed = 0; | ||
| let pending = 0; | ||
|
|
||
| for (const tile of selectedTiles) { | ||
| if (!tile.isLoaded) { | ||
| pending++; | ||
| } else if (tile.content === null) { | ||
| // Tile request completed but returned no content (404, error, etc.) | ||
| failed++; | ||
| } else { | ||
| // Tile loaded successfully with content | ||
| loaded++; | ||
| } | ||
| } | ||
|
|
||
| const total = selectedTiles.length; | ||
| const percentLoaded = total > 0 ? Math.round((loaded / total) * 100) : 100; | ||
| const isComplete = pending === 0; | ||
| const isSuccess = isComplete && failed === 0; | ||
|
|
||
| return { | ||
| total, | ||
| loaded, | ||
| failed, | ||
| pending, | ||
| percentLoaded, | ||
| isComplete, | ||
| isSuccess | ||
| }; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.