UNPKG

@deck.gl/geo-layers

Version:

deck.gl layers supporting geospatial use cases and GIS formats

445 lines (386 loc) 12.3 kB
import Tile2DHeader from './tile-2d-header'; import {getTileIndices, tileToBoundingBox} from './utils'; import {RequestScheduler} from '@loaders.gl/loader-utils'; import {Matrix4} from '@math.gl/core'; // bit masks const TILE_STATE_VISITED = 1; const TILE_STATE_VISIBLE = 2; /* show cached parent tile if children are loading +-----------+ +-----+ +-----+-----+ | | | | | | | | | | | | | | | | --> +-----+-----+ -> +-----+-----+ | | | | | | | | | | | | | | +-----------+ +-----+ +-----+-----+ show cached children tiles when parent is loading +-------+---- +------------ | | | | | | | | | +-------+---- --> | | | | */ export const STRATEGY_NEVER = 'never'; export const STRATEGY_REPLACE = 'no-overlap'; export const STRATEGY_DEFAULT = 'best-available'; const DEFAULT_CACHE_SCALE = 5; const STRATEGIES = { [STRATEGY_DEFAULT]: updateTileStateDefault, [STRATEGY_REPLACE]: updateTileStateReplace, [STRATEGY_NEVER]: () => {} }; /** * Manages loading and purging of tiles data. This class caches recently visited tiles * and only create new tiles if they are present. */ export default class Tileset2D { /** * Takes in a function that returns tile data, a cache size, and a max and a min zoom level. * Cache size defaults to 5 * number of tiles in the current viewport */ constructor(opts) { this.opts = opts; this.onTileLoad = tile => { this.opts.onTileLoad(tile); if (this.opts.maxCacheByteSize) { this._cacheByteSize += tile.byteLength; this._resizeCache(); } }; this._requestScheduler = new RequestScheduler({ maxRequests: opts.maxRequests, throttleRequests: opts.maxRequests > 0 }); // Maps tile id in string {z}-{x}-{y} to a Tile object this._cache = new Map(); this._tiles = []; this._dirty = false; this._cacheByteSize = 0; // Cache the last processed viewport this._viewport = null; this._selectedTiles = null; this._frameNumber = 0; this._modelMatrix = new Matrix4(); this._modelMatrixInverse = new Matrix4(); this.setOptions(opts); } /* Public API */ get tiles() { return this._tiles; } get selectedTiles() { return this._selectedTiles; } get isLoaded() { return this._selectedTiles.every(tile => tile.isLoaded); } get needsReload() { return this._selectedTiles.some(tile => tile.needsReload); } setOptions(opts) { Object.assign(this.opts, opts); if (Number.isFinite(opts.maxZoom)) { this._maxZoom = Math.floor(opts.maxZoom); } if (Number.isFinite(opts.minZoom)) { this._minZoom = Math.ceil(opts.minZoom); } } // Clean up any outstanding tile requests. finalize() { for (const tile of this._cache.values()) { if (tile.isLoading) { tile.abort(); } } this._cache.clear(); this._tiles = []; this._selectedTiles = null; } reloadAll() { for (const tileId of this._cache.keys()) { const tile = this._cache.get(tileId); if (!this._selectedTiles.includes(tile)) { this._cache.delete(tileId); } else { tile.setNeedsReload(); } } } /** * Update the cache with the given viewport and model matrix and triggers callback onUpdate. * @param {*} viewport * @param {*} onUpdate * @param {*} modelMatrix */ update(viewport, {zRange, modelMatrix} = {}) { const modelMatrixAsMatrix4 = new Matrix4(modelMatrix); const isModelMatrixNew = !modelMatrixAsMatrix4.equals(this._modelMatrix); if (!viewport.equals(this._viewport) || isModelMatrixNew) { if (isModelMatrixNew) { this._modelMatrixInverse = modelMatrixAsMatrix4.clone().invert(); this._modelMatrix = modelMatrixAsMatrix4; } this._viewport = viewport; const tileIndices = this.getTileIndices({ viewport, maxZoom: this._maxZoom, minZoom: this._minZoom, zRange, modelMatrix: this._modelMatrix, modelMatrixInverse: this._modelMatrixInverse }); this._selectedTiles = tileIndices.map(index => this._getTile(index, true)); if (this._dirty) { // Some new tiles are added this._rebuildTree(); } // Check for needed reloads explicitly even if the view/matrix has not changed. } else if (this.needsReload) { this._selectedTiles = this._selectedTiles.map(tile => this._getTile({x: tile.x, y: tile.y, z: tile.z}) ); } // Update tile states const changed = this.updateTileStates(); this._pruneRequests(); if (this._dirty) { // cache size is either the user defined maxSize or 5 * number of current tiles in the viewport. this._resizeCache(); } if (changed) { this._frameNumber++; } return this._frameNumber; } /* Public interface for subclassing */ // Returns array of {x, y, z} getTileIndices({viewport, maxZoom, minZoom, zRange, modelMatrix, modelMatrixInverse}) { const {tileSize, extent, zoomOffset} = this.opts; return getTileIndices({ viewport, maxZoom, minZoom, zRange, tileSize, extent, modelMatrix, modelMatrixInverse, zoomOffset }); } // Add custom metadata to tiles getTileMetadata({x, y, z}) { const {tileSize} = this.opts; return {bbox: tileToBoundingBox(this._viewport, x, y, z, tileSize)}; } // Returns {x, y, z} of the parent tile getParentIndex(tileIndex) { // Perf: mutate the input object to avoid GC tileIndex.x = Math.floor(tileIndex.x / 2); tileIndex.y = Math.floor(tileIndex.y / 2); tileIndex.z -= 1; return tileIndex; } // Returns true if any tile's visibility changed updateTileStates() { const refinementStrategy = this.opts.refinementStrategy || STRATEGY_DEFAULT; const visibilities = new Array(this._cache.size); let i = 0; // Reset state for (const tile of this._cache.values()) { // save previous state visibilities[i++] = tile.isVisible; tile.isSelected = false; tile.isVisible = false; } for (const tile of this._selectedTiles) { tile.isSelected = true; tile.isVisible = true; } // Strategy-specific state logic (typeof refinementStrategy === 'function' ? refinementStrategy : STRATEGIES[refinementStrategy])(Array.from(this._cache.values())); i = 0; // Check if any visibility has changed for (const tile of this._cache.values()) { if (visibilities[i++] !== tile.isVisible) { return true; } } return false; } /* Private methods */ _pruneRequests() { const {maxRequests} = this.opts; const abortCandidates = []; let ongoingRequestCount = 0; for (const tile of this._cache.values()) { // Keep track of all the ongoing requests if (tile.isLoading) { ongoingRequestCount++; if (!tile.isSelected && !tile.isVisible) { abortCandidates.push(tile); } } } while (maxRequests > 0 && ongoingRequestCount > maxRequests && abortCandidates.length > 0) { // There are too many ongoing requests, so abort some that are unselected const tile = abortCandidates.shift(); tile.abort(); ongoingRequestCount--; } } // This needs to be called every time some tiles have been added/removed from cache _rebuildTree() { const {_cache} = this; // Reset states for (const tile of _cache.values()) { tile.parent = null; tile.children.length = 0; } // Rebuild tree for (const tile of _cache.values()) { const parent = this._getNearestAncestor(tile.x, tile.y, tile.z); tile.parent = parent; if (parent) { parent.children.push(tile); } } } /** * Clear tiles that are not visible when the cache is full */ /* eslint-disable complexity */ _resizeCache() { const {_cache, opts} = this; const maxCacheSize = opts.maxCacheSize || (opts.maxCacheByteSize ? Infinity : DEFAULT_CACHE_SCALE * this.selectedTiles.length); const maxCacheByteSize = opts.maxCacheByteSize || Infinity; const overflown = _cache.size > maxCacheSize || this._cacheByteSize > maxCacheByteSize; if (overflown) { for (const [tileId, tile] of _cache) { if (!tile.isVisible) { // delete tile this._cacheByteSize -= opts.maxCacheByteSize ? tile.byteLength : 0; _cache.delete(tileId); this.opts.onTileUnload(tile); } if (_cache.size <= maxCacheSize && this._cacheByteSize <= maxCacheByteSize) { break; } } this._rebuildTree(); this._dirty = true; } if (this._dirty) { this._tiles = Array.from(this._cache.values()) // sort by zoom level so that smaller tiles are displayed on top .sort((t1, t2) => t1.z - t2.z); this._dirty = false; } } /* eslint-enable complexity */ _getTile({x, y, z}, create) { const tileId = `${x},${y},${z}`; let tile = this._cache.get(tileId); let needsReload = false; if (!tile && create) { tile = new Tile2DHeader({x, y, z}); Object.assign(tile, this.getTileMetadata(tile)); needsReload = true; this._cache.set(tileId, tile); this._dirty = true; } else if (tile && tile.needsReload) { needsReload = true; } if (needsReload) { tile.loadData({ getData: this.opts.getTileData, requestScheduler: this._requestScheduler, onLoad: this.onTileLoad, onError: this.opts.onTileError }); } return tile; } _getNearestAncestor(x, y, z) { const {_minZoom = 0} = this; let index = {x, y, z}; while (index.z > _minZoom) { index = this.getParentIndex(index); const parent = this._getTile(index); if (parent) { return parent; } } return null; } } /* -- Refinement strategies --*/ /* eslint-disable max-depth */ // For all the selected && pending tiles: // - pick the closest ancestor as placeholder // - if no ancestor is visible, pick the closest children as placeholder function updateTileStateDefault(allTiles) { for (const tile of allTiles) { tile.state = 0; } for (const tile of allTiles) { if (tile.isSelected && !getPlaceholderInAncestors(tile)) { getPlaceholderInChildren(tile); } } for (const tile of allTiles) { tile.isVisible = Boolean(tile.state & TILE_STATE_VISIBLE); } } // Until a selected tile and all its selected siblings are loaded, use the closest ancestor as placeholder function updateTileStateReplace(allTiles) { for (const tile of allTiles) { tile.state = 0; } for (const tile of allTiles) { if (tile.isSelected) { getPlaceholderInAncestors(tile); } } // Always process parents first const sortedTiles = Array.from(allTiles).sort((t1, t2) => t1.z - t2.z); for (const tile of sortedTiles) { tile.isVisible = Boolean(tile.state & TILE_STATE_VISIBLE); if (tile.isVisible || tile.state & TILE_STATE_VISITED) { // If the tile is rendered, or if the tile has been explicitly hidden, hide all of its children for (const child of tile.children) { child.state = TILE_STATE_VISITED; } } else if (tile.isSelected) { getPlaceholderInChildren(tile); } } } // Walk up the tree until we find one ancestor that is loaded. Returns true if successful. function getPlaceholderInAncestors(tile) { while (tile) { if (tile.isLoaded || tile.content) { tile.state |= TILE_STATE_VISIBLE; return true; } tile = tile.parent; } return false; } // Recursively set children as placeholder function getPlaceholderInChildren(tile) { for (const child of tile.children) { if (child.isLoaded || child.content) { child.state |= TILE_STATE_VISIBLE; } else { getPlaceholderInChildren(child); } } }