Skip to content

Commit 41a8aff

Browse files
Add tile loading placeholders
1 parent 95cbde6 commit 41a8aff

10 files changed

Lines changed: 786 additions & 146 deletions

File tree

docs/api-reference/geo-layers/terrain-layer.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,10 @@ new deck.TerrainLayer({});
145145

146146
## Properties
147147

148-
When in Tiled Mode, inherits from all [TileLayer](./tile-layer.md) properties. Forwards `wireframe` property to [SimpleMeshLayer](../mesh-layers/simple-mesh-layer.md).
148+
When in Tiled Mode, inherits from all [TileLayer](./tile-layer.md) properties. Tiled terrain
149+
forwards `renderPlaceholder` to its internal `TileLayer`; this can be used to render loading
150+
footprints before terrain meshes are available. Forwards `wireframe` property to
151+
[SimpleMeshLayer](../mesh-layers/simple-mesh-layer.md).
149152

150153

151154

docs/api-reference/geo-layers/tile-layer.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,10 @@ If not supplied, the `maxCacheByteSize` is set to `Infinity`.
302302

303303
How the tile layer refines the visibility of tiles. When zooming in and out, if the layer only shows tiles from the current zoom level, then the user may observe undesirable flashing while new data is loading. By setting `refinementStrategy` the layer can attempt to maintain visual continuity by displaying cached data from a different zoom level before data is available.
304304

305+
`refinementStrategy` only reuses loaded tile content that is already in the cache. To render
306+
synthetic content while a selected tile has no loaded content and no cached ancestor or child is
307+
visible, use [`renderPlaceholder`](#renderplaceholder).
308+
305309
This prop accepts one of the following:
306310

307311
* `'best-available'`: If a tile in the current viewport is waiting for its data to load, use cached content from the closest zoom level to fill the empty space. This approach minimizes the visual flashing due to missing content.
@@ -371,6 +375,44 @@ Note that the following sub layer props are overridden by `TileLayer` internally
371375
- `visible` (toggled based on tile visibility)
372376
- `highlightedObjectIndex` (set based on the parent layer's highlight state)
373377

378+
#### `renderPlaceholder` (Function, optional) {#renderplaceholder}
379+
380+
Renders one or an array of Layer instances for a selected tile while its data is loading.
381+
382+
This prop is disabled by default. When supplied, it is called for selected tiles that have no loaded
383+
content and no generated sublayers. With the default `refinementStrategy: 'best-available'`, cached
384+
ancestor or child content still takes priority, so placeholders only fill cold-start or cache-miss
385+
gaps. With `refinementStrategy: 'no-overlap'`, placeholders are shown instead of cached refinement
386+
content for selected loading tiles.
387+
388+
The callback receives all the `TileLayer` props and the following props:
389+
390+
* `id` (string): A unique id for this placeholder sublayer
391+
* `data` (null): Placeholder tiles do not have loaded tile data
392+
* `bounds` (number[4]): Bounds of the tile in `[left, bottom, right, top]` order
393+
* `tile` ([Tile](#tile))
394+
395+
- Default: `null`
396+
397+
For raster tiles, return a `BitmapLayer` that spans `props.bounds`. Set `pickable: false` if
398+
placeholder layers should not participate in picking.
399+
400+
```ts
401+
renderPlaceholder: props => {
402+
const {data, bounds, ...otherProps} = props;
403+
404+
return new BitmapLayer(otherProps, {
405+
image: 'data:image/png;base64,...',
406+
bounds,
407+
pickable: false,
408+
opacity: 0.35
409+
});
410+
}
411+
```
412+
413+
Placeholder sublayers do not make the tile or layer loaded. `isLoaded`, `onViewportLoad`, tile cache
414+
behavior, and tile error handling continue to depend on the real tile request.
415+
374416
#### `zRange` (number[2], optional) {#zrange}
375417

376418
An array representing the height range of the content in the tiles, as `[minZ, maxZ]`. This is designed to support tiles with 2.5D content, such as buildings or terrains. At high pitch angles, such a tile may "extrude into" the viewport even if its 2D bounding box is out of view. Therefore, it is necessary to provide additional information for the layer to behave correctly. The value of this prop is used for two purposes: 1) to determine the necessary tiles to load and/or render; 2) to determine the possible intersecting tiles during picking.

examples/website/image-tile/app.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: MIT
33
// Copyright (c) vis.gl contributors
44

5-
/* global fetch, DOMParser */
5+
/* global fetch, DOMParser, setTimeout */
66
import React, {useState, useEffect} from 'react';
77
import {createRoot} from 'react-dom/client';
88

@@ -24,6 +24,11 @@ const INITIAL_VIEW_STATE: OrthographicViewState = {
2424

2525
const ROOT_URL =
2626
'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/image-tiles/moon.image';
27+
const PLACEHOLDER_IMAGE =
28+
'data:image/svg+xml;charset=utf-8,' +
29+
'%3Csvg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8"%3E' +
30+
'%3Crect width="8" height="8" fill="%23242a2e"/%3E' +
31+
'%3Cpath d="M0 8 8 0" stroke="%23404a50" stroke-width="1"/%3E%3C/svg%3E';
2732

2833
function getTooltip({tile, bitmap}: TileLayerPickingInfo<ImageBitmap, BitmapLayerPickingInfo>) {
2934
if (tile && bitmap) {
@@ -35,11 +40,19 @@ function getTooltip({tile, bitmap}: TileLayerPickingInfo<ImageBitmap, BitmapLaye
3540
return null;
3641
}
3742

43+
function sleep(ms: number): Promise<void> {
44+
return new Promise(resolve => setTimeout(resolve, ms));
45+
}
46+
3847
export default function App({
3948
autoHighlight = true,
49+
showPlaceholders = true,
50+
loadDelay = 0,
4051
onTilesLoad
4152
}: {
4253
autoHighlight?: boolean;
54+
showPlaceholders?: boolean;
55+
loadDelay?: number;
4356
onTilesLoad?: () => void;
4457
}) {
4558
const [dimensions, setDimensions] = useState<{width: number; height: number; tileSize: number}>();
@@ -85,14 +98,34 @@ export default function App({
8598
minZoom: -7,
8699
maxZoom: 0,
87100
extent: [0, 0, dimensions.width, dimensions.height],
88-
getTileData: ({index}) => {
101+
getTileData: async ({index}) => {
89102
const {x, y, z} = index;
103+
if (loadDelay > 0) {
104+
await sleep(loadDelay);
105+
}
90106
return load(
91107
`${ROOT_URL}/moon.image_files/${15 + z}/${x}_${y}.jpeg`
92108
) as Promise<ImageBitmap>;
93109
},
94110
onViewportLoad: onTilesLoad,
95111

112+
renderPlaceholder: showPlaceholders
113+
? props => {
114+
const {width, height} = dimensions;
115+
const {data: _data, bounds, ...otherProps} = props;
116+
return new BitmapLayer(otherProps, {
117+
image: PLACEHOLDER_IMAGE,
118+
bounds: [
119+
clamp(bounds[0], 0, width),
120+
clamp(bounds[1], 0, height),
121+
clamp(bounds[2], 0, width),
122+
clamp(bounds[3], 0, height)
123+
],
124+
pickable: false,
125+
opacity: 0.5
126+
});
127+
}
128+
: undefined,
96129
renderSubLayers: props => {
97130
const [[left, bottom], [right, top]] = props.tile.boundingBox;
98131
const {width, height} = dimensions;

0 commit comments

Comments
 (0)