modules/geo-layers/src/tile-layer/tile-layer.js (174 lines of code) (raw):
import {CompositeLayer, _flatten as flatten} from '@deck.gl/core';
import {GeoJsonLayer} from '@deck.gl/layers';
import {load} from '@loaders.gl/core';
import Tileset2D, {STRATEGY_DEFAULT} from './tileset-2d';
import {urlType, getURLFromTemplate} from './utils';
const defaultProps = {
data: [],
dataComparator: urlType.equals,
renderSubLayers: {type: 'function', value: props => new GeoJsonLayer(props), compare: false},
getTileData: {type: 'function', optional: true, value: null, compare: false},
// TODO - change to onViewportLoad to align with Tile3DLayer
onViewportLoad: {type: 'function', optional: true, value: null, compare: false},
onTileLoad: {type: 'function', value: tile => {}, compare: false},
// eslint-disable-next-line
onTileError: {type: 'function', value: err => console.error(err), compare: false},
extent: {type: 'array', optional: true, value: null, compare: true},
tileSize: 512,
maxZoom: null,
minZoom: 0,
maxCacheSize: null,
maxCacheByteSize: null,
refinementStrategy: STRATEGY_DEFAULT,
zRange: null,
// Use load directly so we don't use ResourceManager
fetch: {
type: 'function',
value: (url, {layer, signal}) => {
const loadOptions = {signal, ...(layer.getLoadOptions() || {})};
return load(url, loadOptions);
},
compare: false
},
maxRequests: 6
};
export default class TileLayer extends CompositeLayer {
initializeState() {
this.state = {
tiles: [],
isLoaded: false
};
}
get isLoaded() {
const {tileset} = this.state;
return tileset.selectedTiles.every(
tile => tile.layers && tile.layers.every(layer => layer.isLoaded)
);
}
shouldUpdateState({changeFlags}) {
return changeFlags.somethingChanged;
}
updateState({props, oldProps, context, changeFlags}) {
let {tileset} = this.state;
const createTileCache =
!tileset ||
changeFlags.dataChanged ||
(changeFlags.updateTriggersChanged &&
(changeFlags.updateTriggersChanged.all || changeFlags.updateTriggersChanged.getTileData));
if (createTileCache) {
const {
maxZoom,
minZoom,
tileSize,
maxCacheSize,
maxCacheByteSize,
refinementStrategy,
extent,
maxRequests
} = props;
tileset = new Tileset2D({
getTileData: this.getTileData.bind(this),
maxCacheSize,
maxCacheByteSize,
maxZoom,
minZoom,
tileSize,
refinementStrategy,
extent,
onTileLoad: this._onTileLoad.bind(this),
onTileError: this._onTileError.bind(this),
maxRequests
});
this.setState({tileset});
} else if (changeFlags.propsChanged || changeFlags.updateTriggersChanged) {
tileset.setOptions(props);
// if any props changed, delete the cached layers
this.state.tileset.tiles.forEach(tile => {
tile.layers = null;
});
}
this._updateTileset();
}
_updateTileset() {
const {tileset} = this.state;
const {onViewportLoad, zRange} = this.props;
const frameNumber = tileset.update(this.context.viewport, {zRange});
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;
}
_onTileLoad(tile) {
const layer = this.getCurrentLayer();
layer.props.onTileLoad(tile);
if (tile.isVisible) {
this.setNeedsUpdate();
}
}
_onTileError(error, tile) {
const layer = this.getCurrentLayer();
layer.props.onTileError(error);
// errorred tiles should not block rendering, are considered "loaded" with empty data
layer._updateTileset();
if (tile.isVisible) {
this.setNeedsUpdate();
}
}
// Methods for subclass to override
getTileData(tile) {
const {data} = this.props;
const {getTileData, fetch} = this.getCurrentLayer().props;
const {signal} = tile;
tile.url = getURLFromTemplate(data, tile);
if (getTileData) {
return getTileData(tile);
}
if (tile.url) {
return fetch(tile.url, {layer: this, signal});
}
return null;
}
renderSubLayers(props) {
return this.props.renderSubLayers(props);
}
getHighlightedObjectIndex() {
return -1;
}
getPickingInfo({info, sourceLayer}) {
info.sourceLayer = sourceLayer;
info.tile = sourceLayer.props.tile;
return info;
}
renderLayers() {
const {visible} = this.props;
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
const isVisible = visible && tile.isVisible;
const highlightedObjectIndex = this.getHighlightedObjectIndex(tile);
// cache the rendered layer in the tile
if (!tile.isLoaded) {
// no op
} else if (!tile.layers) {
const layers = this.renderSubLayers(
Object.assign({}, this.props, {
id: `${this.id}-${tile.x}-${tile.y}-${tile.z}`,
data: tile.data,
visible: isVisible,
_offset: 0,
tile,
highlightedObjectIndex
})
);
tile.layers = flatten(layers, Boolean);
} else if (
tile.layers[0] &&
(tile.layers[0].props.visible !== isVisible ||
tile.layers[0].props.highlightedObjectIndex !== highlightedObjectIndex)
) {
tile.layers = tile.layers.map(layer =>
layer.clone({visible: isVisible, highlightedObjectIndex})
);
}
return tile.layers;
});
}
}
TileLayer.layerName = 'TileLayer';
TileLayer.defaultProps = defaultProps;