@deck.gl/geo-layers
Version:
deck.gl layers supporting geospatial use cases and GIS formats
237 lines (201 loc) • 6.18 kB
JavaScript
import {CompositeLayer, _flatten as flatten} from '@deck.gl/core';
import {GeoJsonLayer} from '@deck.gl/layers';
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},
onTileUnload: {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,
maxRequests: 6,
zoomOffset: 0
};
export default class TileLayer extends CompositeLayer {
initializeState() {
this.state = {
tileset: null,
isLoaded: false
};
}
finalizeState() {
this.state.tileset?.finalize();
}
get isLoaded() {
const {tileset} = this.state;
return tileset.selectedTiles.every(
tile => tile.isLoaded && tile.layers && tile.layers.every(layer => layer.isLoaded)
);
}
shouldUpdateState({changeFlags}) {
return changeFlags.somethingChanged;
}
updateState({props, changeFlags}) {
let {tileset} = this.state;
const propsChanged = changeFlags.propsOrDataChanged || changeFlags.updateTriggersChanged;
const dataChanged =
changeFlags.dataChanged ||
(changeFlags.updateTriggersChanged &&
(changeFlags.updateTriggersChanged.all || changeFlags.updateTriggersChanged.getTileData));
if (!tileset) {
tileset = new Tileset2D(this._getTilesetOptions(props));
this.setState({tileset});
} else if (propsChanged) {
tileset.setOptions(this._getTilesetOptions(props));
if (dataChanged) {
// reload all tiles
// use cached layers until new content is loaded
tileset.reloadAll();
} else {
// some render options changed, regenerate sub layers now
this.state.tileset.tiles.forEach(tile => {
tile.layers = null;
});
}
}
this._updateTileset();
}
_getTilesetOptions(props) {
const {
tileSize,
maxCacheSize,
maxCacheByteSize,
refinementStrategy,
extent,
maxZoom,
minZoom,
maxRequests,
zoomOffset
} = props;
return {
maxCacheSize,
maxCacheByteSize,
maxZoom,
minZoom,
tileSize,
refinementStrategy,
extent,
maxRequests,
zoomOffset,
getTileData: this.getTileData.bind(this),
onTileLoad: this._onTileLoad.bind(this),
onTileError: this._onTileError.bind(this),
onTileUnload: this._onTileUnload.bind(this)
};
}
_updateTileset() {
const {tileset} = this.state;
const {zRange, modelMatrix} = this.props;
const frameNumber = tileset.update(this.context.viewport, {zRange, modelMatrix});
const {isLoaded} = tileset;
const loadingStateChanged = this.state.isLoaded !== isLoaded;
const tilesetChanged = this.state.frameNumber !== frameNumber;
if (isLoaded && (loadingStateChanged || tilesetChanged)) {
this._onViewportLoad();
}
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;
}
_onViewportLoad() {
const {tileset} = this.state;
const {onViewportLoad} = this.props;
if (onViewportLoad) {
onViewportLoad(tileset.selectedTiles);
}
}
_onTileLoad(tile) {
this.props.onTileLoad(tile);
tile.layers = null;
this.setNeedsUpdate();
}
_onTileError(error, tile) {
this.props.onTileError(error);
tile.layers = null;
this.setNeedsUpdate();
}
_onTileUnload(tile) {
this.props.onTileUnload(tile);
}
// Methods for subclass to override
getTileData(tile) {
const {data, getTileData, fetch} = this.props;
const {signal} = tile;
tile.url = getURLFromTemplate(data, tile);
if (getTileData) {
return getTileData(tile);
}
if (tile.url) {
return fetch(tile.url, {propName: 'data', layer: this, signal});
}
return null;
}
renderSubLayers(props) {
return this.props.renderSubLayers(props);
}
getSubLayerPropsByTile(tile) {
return null;
}
getPickingInfo({info, sourceLayer}) {
info.tile = sourceLayer.props.tile;
return info;
}
_updateAutoHighlight(info) {
if (info.sourceLayer) {
info.sourceLayer.updateAutoHighlight(info);
}
}
renderLayers() {
return this.state.tileset.tiles.map(tile => {
const subLayerProps = this.getSubLayerPropsByTile(tile);
// cache the rendered layer in the tile
if (!tile.isLoaded && !tile.content) {
// nothing to show
} else if (!tile.layers) {
const layers = this.renderSubLayers({
...this.props,
id: `${this.id}-${tile.x}-${tile.y}-${tile.z}`,
data: tile.content,
_offset: 0,
tile
});
tile.layers = flatten(layers, Boolean).map(layer =>
layer.clone({
tile,
...subLayerProps
})
);
} else if (
subLayerProps &&
tile.layers[0] &&
Object.keys(subLayerProps).some(
propName => tile.layers[0].props[propName] !== subLayerProps[propName]
)
) {
tile.layers = tile.layers.map(layer => layer.clone(subLayerProps));
}
return tile.layers;
});
}
filterSubLayer({layer}) {
return layer.props.tile.isVisible;
}
}
TileLayer.layerName = 'TileLayer';
TileLayer.defaultProps = defaultProps;