Skip to content

Commit

Permalink
TileLayer improvements (#4139)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress committed Feb 4, 2020
1 parent 48923e7 commit ff6f9ff
Show file tree
Hide file tree
Showing 11 changed files with 654 additions and 371 deletions.
10 changes: 9 additions & 1 deletion docs/layers/base-tile-layer.md
Expand Up @@ -31,7 +31,6 @@ To use pre-bundled scripts:
<!-- or -->
<script src="https://unpkg.com/@deck.gl/core@^8.0.0/dist.min.js"></script>
<script src="https://unpkg.com/@deck.gl/layers@^8.0.0/dist.min.js"></script>
<script src="https://unpkg.com/@deck.gl/geo-layers@^8.0.0/dist.min.js"></script>
```

```js
Expand Down Expand Up @@ -81,6 +80,15 @@ The maximum cache size for a tile layer. If not defined, it is calculated using

- Default: `null`

##### `strategy` (Enum, optional)

How the tile layer determines the visibility of tiles. One of the following:

* `'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.
* `'no-overlap'`: Avoid showing overlapping tiles when backfilling with cached content. This is usually favorable when tiles do not have opaque backgrounds.

- Default: `'best-available'`

### Render Options

##### `onViewportLoad` (Function, optional)
Expand Down
4 changes: 2 additions & 2 deletions modules/geo-layers/src/tile-layer/utils/viewport-util.js
Expand Up @@ -55,7 +55,6 @@ export function getTileIndices(viewport, maxZoom, minZoom) {
expected 2 3 0 1 2 3
*/
let [minX, minY] = getTileIndex([bbox[0], bbox[3]], scale);
// eslint-disable-next-line prefer-const
let [maxX, maxY] = getTileIndex([bbox[2], bbox[1]], scale);
const indices = [];

Expand All @@ -65,7 +64,8 @@ export function getTileIndices(viewport, maxZoom, minZoom) {
*/
minX = Math.floor(minX);
maxX = Math.min(minX + scale, maxX); // Avoid creating duplicates
minY = Math.floor(minY);
minY = Math.max(0, Math.floor(minY));
maxY = Math.min(scale, maxY);
for (let x = minX; x < maxX; x++) {
for (let y = minY; y < maxY; y++) {
// Cast to valid x between [0, scale]
Expand Down
88 changes: 41 additions & 47 deletions modules/layers/src/base-tile-layer/base-tile-layer.js
@@ -1,20 +1,21 @@
import {log} from '@deck.gl/core';
import {CompositeLayer} from '@deck.gl/core';
import GeoJsonLayer from '../geojson-layer/geojson-layer';
import TileCache from './utils/tile-cache';
import Tileset2D, {STRATEGY_DEFAULT} from './utils/tileset-2d';

const defaultProps = {
renderSubLayers: {type: 'function', value: props => new GeoJsonLayer(props), compare: false},
getTileData: {type: 'function', value: ({x, y, z}) => Promise.resolve(null), compare: false},
tileToBoundingBox: {type: 'function', value: () => null, compare: false},
getTileIndices: {type: 'function', value: () => null, compare: false},
tileToBoundingBox: {type: 'function', value: (x, y, z) => null, compare: false},
getTileIndices: {type: 'function', value: (viewport, maxZoom, minZoom) => [], compare: false},
// TODO - change to onViewportLoad to align with Tile3DLayer
onViewportLoad: {type: 'function', optional: true, value: null, compare: false},
// eslint-disable-next-line
onTileError: {type: 'function', value: err => console.error(err), compare: false},
maxZoom: null,
minZoom: 0,
maxCacheSize: null
maxCacheSize: null,
strategy: STRATEGY_DEFAULT
};

export default class BaseTileLayer extends CompositeLayer {
Expand All @@ -34,68 +35,74 @@ export default class BaseTileLayer extends CompositeLayer {
}

updateState({props, oldProps, context, changeFlags}) {
let {tileCache} = this.state;
if (
!tileCache ||
let {tileset} = this.state;
const createTileCache =
!tileset ||
(changeFlags.updateTriggersChanged &&
(changeFlags.updateTriggersChanged.all || changeFlags.updateTriggersChanged.getTileData))
) {
(changeFlags.updateTriggersChanged.all || changeFlags.updateTriggersChanged.getTileData));

if (createTileCache) {
const {
getTileData,
maxZoom,
minZoom,
maxCacheSize,
strategy,
getTileIndices,
tileToBoundingBox
} = props;
if (tileCache) {
tileCache.finalize();
if (tileset) {
tileset.finalize();
}
tileCache = new TileCache({
tileset = new Tileset2D({
getTileData,
getTileIndices,
tileToBoundingBox,
maxSize: maxCacheSize,
maxZoom,
minZoom,
onTileLoad: this._onTileLoad.bind(this),
strategy,
onTileLoad: this._updateTileset.bind(this),
onTileError: this._onTileError.bind(this)
});
this.setState({tileCache});
this.setState({tileset});
} else if (changeFlags.propsChanged) {
// if any props changed, delete the cached layers
this.state.tileCache.tiles.forEach(tile => {
this.state.tileset.tiles.forEach(tile => {
tile.layer = null;
});
}

const {viewport} = context;
if (changeFlags.viewportChanged && viewport.id !== 'DEFAULT-INITIAL-VIEWPORT') {
const z = this.getLayerZoomLevel();
tileCache.update(viewport);
// The tiles that should be displayed at this zoom level
const currTiles = tileCache.tiles.filter(tile => tile.z === z);
this.setState({isLoaded: false, tiles: currTiles});
this._onTileLoad();
if (createTileCache || changeFlags.viewportChanged) {
this._updateTileset();
}
}

_onTileLoad() {
_updateTileset() {
const {tileset} = this.state;
const {onViewportLoad} = this.props;
const currTiles = this.state.tiles;
const allCurrTilesLoaded = currTiles.every(tile => tile.isLoaded);
if (this.state.isLoaded !== allCurrTilesLoaded) {
this.setState({isLoaded: allCurrTilesLoaded});
if (allCurrTilesLoaded && onViewportLoad) {
onViewportLoad(currTiles.filter(tile => tile._data).map(tile => tile._data));
}
const frameNumber = tileset.update(this.context.viewport);
const {isLoaded} = tileset;

const loadingStateChanged = this.state.isLoaded !== isLoaded;
const tilesetChanged = this.state.frameNumber !== frameNumber;

if (isLoaded && onViewportLoad && (loadingStateChanged || tilesetChanged)) {
onViewportLoad(tileset.selectedTiles.map(tile => tile.data));
}

if (tilesetChanged) {
// Save the tileset frame number - trigger a rerender
this.setState({frameNumber});
}
// Save the loaded state - should not trigger a rerender
this.state.isLoaded = isLoaded;
}

_onTileError(error) {
this.props.onTileError(error);
// errorred tiles should not block rendering, are considered "loaded" with empty data
this._onTileLoad();
this._updateTileset();
}

getPickingInfo({info, sourceLayer}) {
Expand All @@ -104,26 +111,13 @@ export default class BaseTileLayer extends CompositeLayer {
return info;
}

getLayerZoomLevel() {
const z = Math.ceil(this.context.viewport.zoom);
const {maxZoom, minZoom} = this.props;
if (Number.isFinite(maxZoom) && z > maxZoom) {
return Math.floor(maxZoom);
} else if (Number.isFinite(minZoom) && z < minZoom) {
return Math.ceil(minZoom);
}
return z;
}

renderLayers() {
const {renderSubLayers, visible} = this.props;
const z = this.getLayerZoomLevel();
return this.state.tileCache.tiles.map(tile => {
return this.state.tileset.tiles.map(tile => {
// For a tile to be visible:
// - parent layer must be visible
// - tile must be visible in the current viewport
// - if all tiles are loaded, only display the tiles from the current z level
const isVisible = visible && tile.isVisible && (!this.state.isLoaded || tile.z === z);
const isVisible = visible && tile.isVisible;
// cache the rendered layer in the tile
if (!tile.layer) {
tile.layer = renderSubLayers(
Expand Down
@@ -1,35 +1,38 @@
export default class Tile {
export default class Tile2DHeader {
constructor({getTileData, x, y, z, onTileLoad, onTileError, tileToBoundingBox}) {
this.x = x;
this.y = y;
this.z = z;
this.bbox = tileToBoundingBox(this.x, this.y, this.z);
this.isVisible = true;
this.getTileData = getTileData;
this._data = null;
this.selected = true;
this.parent = null;
this.children = [];

this.content = null;
this._isLoaded = false;
this._loader = this._loadData();
this._loader = this._loadData(getTileData);

this.onTileLoad = onTileLoad;
this.onTileError = onTileError;
}

get data() {
return this._data || this._loader;
return this.content || this._loader;
}

get isLoaded() {
return this._isLoaded;
}

_loadData() {
_loadData(getTileData) {
const {x, y, z, bbox} = this;
if (!this.getTileData) {
if (!getTileData) {
return null;
}

return Promise.resolve(this.getTileData({x, y, z, bbox}))
return Promise.resolve(getTileData({x, y, z, bbox}))
.then(buffers => {
this._data = buffers;
this.content = buffers;
this._isLoaded = true;
this.onTileLoad(this);
return buffers;
Expand All @@ -39,10 +42,4 @@ export default class Tile {
this.onTileError(err);
});
}

isOverlapped(tile) {
const {x, y, z} = this;
const m = Math.pow(2, tile.z - z);
return Math.floor(tile.x / m) === x && Math.floor(tile.y / m) === y;
}
}

0 comments on commit ff6f9ff

Please sign in to comment.