modules/geo-layers/src/tile-layer/tileset-2d.js (249 lines of code) (raw):

import Tile2DHeader from './tile-2d-header'; import {getTileIndices, tileToBoundingBox} from './utils'; import {RequestScheduler} from '@loaders.gl/loader-utils'; const TILE_STATE_UNKNOWN = 0; const TILE_STATE_VISIBLE = 1; /* show cached parent tile if children are loading +-----------+ +-----+ +-----+-----+ | | | | | | | | | | | | | | | | --> +-----+-----+ -> +-----+-----+ | | | | | | | | | | | | | | +-----------+ +-----+ +-----+-----+ show cached children tiles when parent is loading +-------+---- +------------ | | | | | | | | | +-------+---- --> | | | | */ const TILE_STATE_PLACEHOLDER = 3; const TILE_STATE_HIDDEN = 4; // tiles that should be displayed in the current viewport const TILE_STATE_SELECTED = 5; export const STRATEGY_NEVER = 'never'; export const STRATEGY_REPLACE = 'no-overlap'; export const STRATEGY_DEFAULT = 'best-available'; const DEFAULT_CACHE_SCALE = 5; /** * 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._getTileData = opts.getTileData; this.onTileError = opts.onTileError; this.onTileLoad = tile => { 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.setOptions(opts); } /* Public API */ get tiles() { return this._tiles; } get selectedTiles() { return this._selectedTiles; } get isLoaded() { return this._selectedTiles.every(tile => tile.isLoaded); } 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); } } /** * Update the cache with the given viewport and triggers callback onUpdate. * @param {*} viewport * @param {*} onUpdate */ update(viewport, {zRange} = {}) { if (!viewport.equals(this._viewport)) { this._viewport = viewport; const tileIndices = this.getTileIndices({ viewport, maxZoom: this._maxZoom, minZoom: this._minZoom, zRange }); this._selectedTiles = tileIndices.map(index => this._getTile(index, true)); if (this._dirty) { // Some new tiles are added this._rebuildTree(); } } // Update tile states const changed = this.updateTileStates(); 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}) { const {tileSize, extent} = this.opts; return getTileIndices({viewport, maxZoom, minZoom, zRange, tileSize, extent}); } // Add custom metadata to tiles getTileMetadata({x, y, z}) { return {bbox: tileToBoundingBox(this._viewport, x, y, z)}; } // 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() { this._updateTileStates(this.selectedTiles); const {maxRequests} = this.opts; const abortCandidates = []; let ongoingRequestCount = 0; let changed = false; for (const tile of this._cache.values()) { const isVisible = Boolean(tile.state & TILE_STATE_VISIBLE); if (tile.isVisible !== isVisible) { changed = true; tile.isVisible = isVisible; } // isSelected used in request scheduler tile.isSelected = tile.state === TILE_STATE_SELECTED; // Keep track of all the ongoing requests if (tile.isLoading) { ongoingRequestCount++; if (!tile.isSelected) { abortCandidates.push(tile); } } } if (maxRequests > 0) { while (ongoingRequestCount > maxRequests && abortCandidates.length > 0) { // There are too many ongoing requests, so abort some that are unselected const tile = abortCandidates.shift(); tile.abort(); ongoingRequestCount--; } } return changed; } /* Private methods */ // 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); } } } // A selected tile is always visible. // Never show two overlapping tiles. // If a selected tile is loading, try showing a cached ancester with the closest z // If a selected tile is loading, and no ancester is shown - try showing cached // descendants with the closest z _updateTileStates(selectedTiles) { const {_cache} = this; const refinementStrategy = this.opts.refinementStrategy || STRATEGY_DEFAULT; // Reset states for (const tile of _cache.values()) { tile.state = TILE_STATE_UNKNOWN; } // For all the selected && pending tiles: // - pick the closest ancestor as placeholder // - if no ancestor is visible, pick the closest children as placeholder for (const tile of selectedTiles) { tile.state = TILE_STATE_SELECTED; } if (refinementStrategy === STRATEGY_NEVER) { return; } for (const tile of selectedTiles) { getPlaceholderInAncestors(tile, refinementStrategy); } for (const tile of selectedTiles) { if (needsPlaceholder(tile)) { getPlaceholderInChildren(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); } 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); if (!tile && create) { tile = new Tile2DHeader({ x, y, z, onTileLoad: this.onTileLoad, onTileError: this.onTileError }); Object.assign(tile, this.getTileMetadata(tile)); tile.loadData(this._getTileData, this._requestScheduler); this._cache.set(tileId, tile); this._dirty = true; } else if (tile && tile.isCancelled && !tile.isLoading) { tile.loadData(this._getTileData, this._requestScheduler); } 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; } } // A selected tile needs placeholder from its children if // - it is not loaded // - none of its ancestors is visible and loaded function needsPlaceholder(tile) { let t = tile; while (t) { if (t.state & (TILE_STATE_VISIBLE === 0)) { return true; } if (t.isLoaded) { return false; } t = t.parent; } return true; } function getPlaceholderInAncestors(tile, refinementStrategy) { let parent; let state = TILE_STATE_PLACEHOLDER; while ((parent = tile.parent)) { if (tile.isLoaded) { // If a tile is loaded, mark all its ancestors as hidden state = TILE_STATE_HIDDEN; if (refinementStrategy === STRATEGY_DEFAULT) { return; } } parent.state = Math.max(parent.state, state); tile = parent; } } // Recursively set children as placeholder function getPlaceholderInChildren(tile) { for (const child of tile.children) { child.state = Math.max(child.state, TILE_STATE_PLACEHOLDER); if (!child.isLoaded) { getPlaceholderInChildren(child); } } }