UNPKG

@here/harp-mapview

Version:

Functionality needed to render a map.

1,032 lines 47.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VisibleTileSet = exports.ResourceComputationType = void 0; const harp_geoutils_1 = require("@here/harp-geoutils"); const harp_lrucache_1 = require("@here/harp-lrucache"); const harp_utils_1 = require("@here/harp-utils"); const THREE = require("three"); const BackgroundDataSource_1 = require("./BackgroundDataSource"); const FrustumIntersection_1 = require("./FrustumIntersection"); const MapView_1 = require("./MapView"); /** * Way the memory consumption of a tile is computed. Either in number of tiles, or in MegaBytes. If * it is in MB, an estimation is used. */ var ResourceComputationType; (function (ResourceComputationType) { ResourceComputationType[ResourceComputationType["EstimationInMb"] = 0] = "EstimationInMb"; ResourceComputationType[ResourceComputationType["NumberOfTiles"] = 1] = "NumberOfTiles"; })(ResourceComputationType = exports.ResourceComputationType || (exports.ResourceComputationType = {})); // Direction in quad tree to search: up -> shallower levels, down -> deeper levels. var SearchDirection; (function (SearchDirection) { SearchDirection[SearchDirection["NONE"] = 0] = "NONE"; SearchDirection[SearchDirection["UP"] = 1] = "UP"; SearchDirection[SearchDirection["DOWN"] = 2] = "DOWN"; SearchDirection[SearchDirection["BOTH"] = 3] = "BOTH"; })(SearchDirection || (SearchDirection = {})); const MB_FACTOR = 1.0 / (1024.0 * 1024.0); /** * Wrapper for LRU cache that encapsulates tiles caching for any {@link DataSource} used. * * Provides LRU based caching mechanism where each tile is identified by its tile key * (morton code) and data source name. * Tiles are kept in the cache based on last recently used policy, cached tile may be evicted * only when cache reaches full saturation and tile is no longer visible. * @note Currently cached entries (tiles) are identified by unique tile code (morton code) and * data source name, thus it is required that each {@link DataSource} used should have unique * name, but implementation could be improved to omit this limitation. */ class DataSourceCache { constructor(cacheSize, rct = ResourceComputationType.EstimationInMb) { this.m_disposedTiles = []; this.m_resourceComputationType = rct; this.m_tileCache = new harp_lrucache_1.LRUCache(cacheSize, (tile) => { if (this.m_resourceComputationType === ResourceComputationType.EstimationInMb) { // Default is size in MB. return tile.memoryUsage * MB_FACTOR; } else { return 1; } }); this.m_tileCache.evictionCallback = (_, tile) => { if (tile.tileLoader !== undefined) { // Cancel downloads as early as possible. tile.tileLoader.cancel(); } this.m_disposedTiles.push(tile); }; this.m_tileCache.canEvict = (_, tile) => { // Tiles can be evicted that weren't requested in the last frame. return !tile.isVisible; }; } /** * Creates unique tile key for caching based on morton code, tile offset and its data source. * * @param mortonCode - The tile morton code. * @param offset - The tile offset. * @param dataSource - The {@link DataSource} from which tile was loaded. */ static getKey(mortonCode, offset, dataSource) { return `${dataSource.name}_${mortonCode}_${offset}`; } /** * Create unique tile identifier for caching, based on tile object passed in. * * @param tile - The tile for which key is generated. */ static getKeyForTile(tile) { return DataSourceCache.getKey(tile.tileKey.mortonCode(), tile.offset, tile.dataSource); } /** * Get information how cached tiles affects cache space available. * * The way how cache evaluates the __resources size__ have a big influence on entire * caching mechanism, if [[resourceComputationType]] is set to: * [[ResourceComputationType.EstimationInMb]] then each tiles contributes to cache size * differently depending on the memory consumed, on other side * [[ResourceComputationType.NumberOfTiles]] says each tile occupies single slot in cache, * so its real memory consumed does not matter affect caching behavior. Of course in * the second scenario cache may grow significantly in terms of memory usage and thus it * is out of control. * * @return [[ResourceComputationType]] enum that describes if resources are counted by * space occupied in memory or just by number of them. */ get resourceComputationType() { return this.m_resourceComputationType; } /** * Get the cache capacity measured as number if megabytes or number of entries. * * The total cached tiles size determines cache saturation, if it reaches the capacity value * then the resources becomes evicted (released) starting from the oldest (the latest used). * * @see size. * @see resourceComputationType. */ get capacity() { return this.m_tileCache.capacity; } /** * Get total cache size described as number of megabytes consumed or number of tiles stored. * * @see capacity. * @see resourceComputationType. */ get size() { return this.m_tileCache.size; } /** * Set cache capacity and the algorithm used for cache size calculation. * * @see capacity. * @see resourceComputationType. * @param size - The new capacity declared in megabytes or number of entires. * @param rct - The enum value that determines how size and capacity are evaluated. */ setCapacity(size, rct) { this.m_resourceComputationType = rct; this.m_tileCache.setCapacityAndMeasure(size, (tile) => { if (this.m_resourceComputationType === ResourceComputationType.EstimationInMb) { // Default is size in MB. return tile.memoryUsage * MB_FACTOR; } else { return 1; } }); } /** * Get tile cached or __undefined__ if tile is not yet in cache. * * @param mortonCode - An unique tile morton code. * @param offset - Tile offset. * @param dataSource - A {@link DataSource} the tile comes from. */ get(mortonCode, offset, dataSource) { return this.m_tileCache.get(DataSourceCache.getKey(mortonCode, offset, dataSource)); } /** * Add new tile to the cache. * * @param mortonCode - En unique tile code (morton code). * @param offset - The tile offset. * @param dataSource - A {@link DataSource} the tile comes from. * @param tile - The tile reference. */ set(mortonCode, offset, dataSource, tile) { this.m_tileCache.set(DataSourceCache.getKey(mortonCode, offset, dataSource), tile); } /** * Delete tile from cache. * * @note This method will not call eviction callback. * @param tile - The tile reference to be removed from cache. */ delete(tile) { const tileKey = DataSourceCache.getKeyForTile(tile); this.deleteByKey(tileKey); } /** * Delete tile using its unique identifier. * * @note Tile identifier its constructed using information about tile code (morton code) and its * {@link DataSource}. * @note This is explicit removal thus eviction callback will not be processed. * @see DataSourceCache.getKey. * @param tileKey - The unique tile identifier. */ deleteByKey(tileKey) { this.m_tileCache.delete(tileKey); } /** * Dispose all tiles releasing their internal data. */ disposeTiles() { this.m_disposedTiles.forEach(tile => { tile.dispose(); }); this.m_disposedTiles.length = 0; } /** * Shrink cache to its allowed capacity. * * This method should be called each time after operations are performed on the cache entries, * in order to keep cache size consistent. It informs caching mechanism to invalidate memory * consumed by its entries and check if cache is overgrown, is such case some tiles will be * evicted. */ shrinkToCapacity() { this.m_tileCache.shrinkToCapacity(); } /** * Evict all cached tiles implicitly even without checking if still in use. */ evictAll() { this.m_tileCache.evictAll(); } /** * Evict selected tiles implicitly. * * @param selector - The callback used to determine if tile should be evicted. */ evictSelected(selector) { this.m_tileCache.evictSelected(selector); } /** * Call functor (callback) on each tile store in cache. * * Optionally you may specify from which {@link DataSource} tiles should be processed. * This limits the tiles visited to a sub-set originating from single {@link DataSource}. * @param callback - The function to be called for each visited tile. * @param inDataSource - The optional {@link DataSource} to which tiles should belong. */ forEach(callback, inDataSource) { this.m_tileCache.forEach((entry, key) => { if (inDataSource === undefined || entry.dataSource === inDataSource) { callback(entry, key); } }); } } // Sort by distance to camera, now the tiles that are further away are at the end // of the list. // // Sort is unstable if distance is equal, which happens a lot when looking top-down. // Unstable sorting makes label placement unstable at tile borders, leading to // flickering. const compareDistances = (a, b) => { const distanceDiff = a.distance - b.distance; // Take care or numerical precision issues const minDiff = (a.distance + b.distance) * 0.000001; return Math.abs(distanceDiff) < minDiff ? a.tileKey.mortonCode() - b.tileKey.mortonCode() : distanceDiff; }; /** * Manages visible {@link Tile}s for {@link MapView}. * * Responsible for election of rendered tiles: * - quad-tree traversal * - frustum culling * - sorting tiles by relevance (visible area) to prioritize load * - limiting number of visible tiles * - caching tiles * - searching cache to replace visible but yet empty tiles with already loaded siblings in nearby * zoom levels */ class VisibleTileSet { constructor(m_frustumIntersection, m_tileGeometryManager, options, m_taskQueue) { var _a; this.m_frustumIntersection = m_frustumIntersection; this.m_tileGeometryManager = m_tileGeometryManager; this.options = options; this.m_taskQueue = m_taskQueue; this.dataSourceTileList = []; this.allVisibleTilesLoaded = false; this.m_cameraOverride = new THREE.PerspectiveCamera(); this.m_viewRange = { near: 0.1, far: Infinity, minimum: 0.1, maximum: Infinity }; // Maps morton codes to a given Tile, used to find overlapping Tiles. We only need to have this // for a single TilingScheme, i.e. that of the BackgroundDataSource. this.m_coveringMap = new Map(); this.m_resourceComputationType = ResourceComputationType.EstimationInMb; this.options = options; this.options.maxTilesPerFrame = Math.floor((_a = this.options.maxTilesPerFrame) !== null && _a !== void 0 ? _a : 0); this.m_resourceComputationType = options.resourceComputationType === undefined ? ResourceComputationType.EstimationInMb : options.resourceComputationType; this.m_dataSourceCache = new DataSourceCache(this.options.tileCacheSize, this.m_resourceComputationType); } /** * Returns cache size. */ getDataSourceCacheSize() { return this.options.tileCacheSize; } /** * Sets cache size. * * @param size - cache size * @param computationType - Optional value specifying the way a {@link Tile}s cache usage is * computed, either based on size in MB (mega bytes) or in number of tiles. Defaults to * `ResourceComputationType.EstimationInMb`. */ setDataSourceCacheSize(size, computationType = ResourceComputationType.EstimationInMb) { this.options.tileCacheSize = size; // This effectively invalidates DataSourceCache this.resourceComputationType = computationType; } /** * Retrieves maximum number of visible tiles. */ getNumberOfVisibleTiles() { return this.options.maxVisibleDataSourceTiles; } /** * Sets maximum number of visible tiles. * * @param size - size of visible tiles array */ setNumberOfVisibleTiles(size) { this.options.maxVisibleDataSourceTiles = size; } /** * Gets the maximum number of tiles that can be added to the scene per frame * @beta * @internal */ get maxTilesPerFrame() { return this.options.maxTilesPerFrame; } /** * Gets the maximum number of tiles that can be added to the scene per frame * @beta * @internal * @param value */ set maxTilesPerFrame(value) { if (value < 0) { throw new Error("Invalid value, this will result in no tiles ever showing"); } this.options.maxTilesPerFrame = Math.floor(value); } /** * The way the cache usage is computed, either based on size in MB (mega bytes) or in number of * tiles. */ get resourceComputationType() { return this.m_resourceComputationType; } /** * Sets the way tile cache is managing its elements. * * Cache may be either keeping number of elements stored or the memory consumed by them. * * @param computationType - Type of algorith used in cache for checking full saturation, * may be counting number of elements or memory consumed by them. */ set resourceComputationType(computationType) { this.m_resourceComputationType = computationType; this.m_dataSourceCache.setCapacity(this.options.tileCacheSize, computationType); } /** * Evaluate frustum near/far clip planes and visibility ranges. */ updateClipPlanes(maxElevation, minElevation) { if (maxElevation !== undefined) { this.options.clipPlanesEvaluator.maxElevation = maxElevation; } if (minElevation !== undefined) { this.options.clipPlanesEvaluator.minElevation = minElevation; } const { camera, projection, elevationProvider } = this.m_frustumIntersection.mapView; this.m_viewRange = this.options.clipPlanesEvaluator.evaluateClipPlanes(camera, projection, elevationProvider); return this.m_viewRange; } /** * Calculates a new set of visible tiles. * @param storageLevel - The camera storage level, see {@link MapView.storageLevel}. * @param zoomLevel - The camera zoom level. * @param dataSources - The data sources for which the visible tiles will be calculated. * @param elevationRangeSource - Source of elevation range data if any. * @returns view ranges and their status since last update (changed or not). */ updateRenderList(storageLevel, zoomLevel, dataSources, frameNumber, elevationRangeSource) { let allVisibleTilesLoaded = true; // This isn't really const, because we pass by ref to the methods below. const newTilesPerFrame = 0; const visibleTileKeysResult = this.getVisibleTileKeysForDataSources(zoomLevel, dataSources, elevationRangeSource); this.dataSourceTileList = []; this.m_coveringMap.clear(); for (const { dataSource, visibleTileKeys } of visibleTileKeysResult.tileKeys) { visibleTileKeys.sort(compareDistances); // Create actual tiles only for the allowed number of visible tiles const dataZoomLevel = dataSource.getDataZoomLevel(zoomLevel); const visibleResult = this.processVisibleTiles(visibleTileKeys, dataSource, frameNumber, { newTilesPerFrame }, true); const dependentResult = this.processVisibleTiles(visibleResult.dependentTiles, dataSource, frameNumber, { newTilesPerFrame }, false); // creates geometry if not yet available this.m_tileGeometryManager.updateTiles(visibleResult.visibleTiles); this.m_tileGeometryManager.updateTiles(dependentResult.visibleTiles); // used to actually render the tiles or find alternatives for incomplete tiles this.dataSourceTileList.push({ dataSource, storageLevel, zoomLevel: dataZoomLevel, allVisibleTileLoaded: visibleResult.allDataSourceTilesLoaded && dependentResult.allDataSourceTilesLoaded, numTilesLoading: visibleResult.numTilesLoading + dependentResult.numTilesLoading, visibleTiles: [...visibleResult.visibleTiles, ...dependentResult.visibleTiles], renderedTiles: new Map() }); allVisibleTilesLoaded = allVisibleTilesLoaded && visibleResult.allDataSourceTilesLoaded; } this.allVisibleTilesLoaded = allVisibleTilesLoaded && visibleTileKeysResult.allBoundingBoxesFinal; this.populateRenderedTiles(); this.forEachCachedTile(tile => { // Remove all tiles that are still being loaded, but are no longer visible. They have to // be reloaded when they become visible again. Hopefully, they are still in the browser // cache by then. if (!tile.isVisible && !tile.allGeometryLoaded) { // The internal TileLoader is cancelled automatically when the Tile is disposed. this.disposeTile(tile); } }); this.m_dataSourceCache.shrinkToCapacity(); let minElevation; let maxElevation; this.dataSourceTileList.forEach(renderListEntry => { // Calculate min/max elevation from every data source tiles, // data sources without elevationRangeSource will contribute to // values with zero levels for both elevations. const tiles = renderListEntry.renderedTiles; tiles.forEach(tile => { tile.update(renderListEntry.zoomLevel); minElevation = harp_utils_1.MathUtils.min2(minElevation, tile.geoBox.minAltitude); maxElevation = harp_utils_1.MathUtils.max2(maxElevation, tile.geoBox.maxAltitude); }); }); if (minElevation === undefined) { minElevation = 0; } if (maxElevation === undefined) { maxElevation = 0; } // If clip planes evaluator depends on the tiles elevation re-calculate // frustum planes and update the camera near/far plane distances. let viewRangesChanged = false; const oldViewRanges = this.m_viewRange; const newViewRanges = this.updateClipPlanes(maxElevation, minElevation); viewRangesChanged = viewRangesEqual(newViewRanges, oldViewRanges) === false; return { viewRanges: newViewRanges, viewRangesChanged }; } /** * Gets the tile corresponding to the given data source, key and offset, creating it if * necessary. * * @param dataSource - The data source the tile belongs to. * @param tileKey - The key identifying the tile. * @param offset - Tile offset. * @param frameNumber - Frame in which the tile was requested * @return The tile if it was found or created, undefined otherwise. */ getTile(dataSource, tileKey, offset, frameNumber) { const cacheOnly = false; return this.getTileImpl(dataSource, tileKey, offset, cacheOnly, frameNumber); } /** * Gets the tile corresponding to the given data source, key and offset from the cache. * * @param dataSource - The data source the tile belongs to. * @param tileKey - The key identifying the tile. * @param offset - Tile offset. * @param frameNumber - Frame in which the tile was requested * @return The tile if found in cache, undefined otherwise. */ getCachedTile(dataSource, tileKey, offset, frameNumber) { harp_utils_1.assert(dataSource.cacheable); const cacheOnly = true; return this.getTileImpl(dataSource, tileKey, offset, cacheOnly, frameNumber); } /** * Gets the tile corresponding to the given data source, key and offset from the rendered tiles. * * @param dataSource - The data source the tile belongs to. * @param tileKey - The key identifying the tile. * @param offset - Tile offset. * @return The tile if found among the rendered tiles, undefined otherwise. */ getRenderedTile(dataSource, tileKey, offset = 0) { const dataSourceVisibleTileList = this.dataSourceTileList.find(list => { return list.dataSource === dataSource; }); if (dataSourceVisibleTileList === undefined) { return undefined; } return dataSourceVisibleTileList.renderedTiles.get(harp_geoutils_1.TileKeyUtils.getKeyForTileKeyAndOffset(tileKey, offset)); } /** * Gets the tile corresponding to the given data source and location from the rendered tiles. * * @param dataSource - The data source the tile belongs to. * @param geoPoint - The geolocation included within the tile. * @return The tile if found among the rendered tiles, undefined otherwise. */ getRenderedTileAtLocation(dataSource, geoPoint, offset = 0) { const dataSourceVisibleTileList = this.dataSourceTileList.find(list => { return list.dataSource === dataSource; }); if (dataSourceVisibleTileList === undefined) { return undefined; } const tilingScheme = dataSource.getTilingScheme(); const visibleLevel = dataSourceVisibleTileList.zoomLevel; const visibleTileKey = tilingScheme.getTileKey(geoPoint, visibleLevel); if (!visibleTileKey) { return undefined; } let tile = dataSourceVisibleTileList.renderedTiles.get(harp_geoutils_1.TileKeyUtils.getKeyForTileKeyAndOffset(visibleTileKey, offset)); if (tile !== undefined) { return tile; } const { searchLevelsUp, searchLevelsDown } = this.getSearchDirection(dataSource, visibleLevel); let parentTileKey = visibleTileKey; for (let levelOffset = 1; levelOffset <= searchLevelsUp; ++levelOffset) { parentTileKey = parentTileKey.parent(); tile = dataSourceVisibleTileList.renderedTiles.get(harp_geoutils_1.TileKeyUtils.getKeyForTileKeyAndOffset(parentTileKey, offset)); if (tile !== undefined) { return tile; } } const worldPoint = tilingScheme.projection.projectPoint(geoPoint); for (let levelOffset = 1; levelOffset <= searchLevelsDown; ++levelOffset) { const childLevel = visibleLevel + levelOffset; const childTileKey = harp_geoutils_1.TileKeyUtils.worldCoordinatesToTileKey(tilingScheme, worldPoint, childLevel); if (childTileKey) { tile = dataSourceVisibleTileList.renderedTiles.get(harp_geoutils_1.TileKeyUtils.getKeyForTileKeyAndOffset(childTileKey, offset)); if (tile !== undefined) { return tile; } } } return undefined; } /** * Removes all internal bookkeeping entries and cache related to specified datasource. * * Called by {@link MapView} when {@link DataSource} has been removed from {@link MapView}. */ removeDataSource(dataSource) { this.clearTileCache(dataSource); this.dataSourceTileList = this.dataSourceTileList.filter(tileList => tileList.dataSource !== dataSource); } /** * Clear the tile cache. * * Remove the {@link Tile} objects created by cacheable {@link DataSource}. * If a {@link DataSource} name is * provided, this method restricts the eviction * the {@link DataSource} with the given name. * * @param dataSourceName - The name of the {@link DataSource}. * @param filter Optional tile filter */ clearTileCache(dataSource, filter) { if (dataSource !== undefined) { this.m_dataSourceCache.evictSelected((tile, _) => { return (tile.dataSource === dataSource && (filter !== undefined ? filter(tile) : true)); }); } else if (filter !== undefined) { this.m_dataSourceCache.evictSelected(filter); } else { this.m_dataSourceCache.evictAll(); } } /** * Visit each tile in visible, rendered, and cached sets. * * * Visible and temporarily rendered tiles will be marked for update and retained. * * Cached but not rendered/visible will be evicted. * * @param dataSource - If passed, only the tiles from this {@link DataSource} instance * are processed. If `undefined`, tiles from all {@link DataSource}s are processed. * @param filter Optional tile filter */ markTilesDirty(dataSource, filter) { if (dataSource === undefined) { this.dataSourceTileList.forEach(renderListEntry => { this.markDataSourceTilesDirty(renderListEntry, filter); }); } else { const renderListEntry = this.dataSourceTileList.find(e => e.dataSource === dataSource); if (renderListEntry === undefined) { return; } this.markDataSourceTilesDirty(renderListEntry, filter); } } /** * Dispose tiles that are marked for removal by {@link @here/harp-lrucache#LRUCache} algorithm. */ disposePendingTiles() { this.m_dataSourceCache.disposeTiles(); } /** * Process callback function [[fun]] with each visible tile in set. * * @param fun - The callback function to be called. */ forEachVisibleTile(fun) { for (const listEntry of this.dataSourceTileList) { listEntry.renderedTiles.forEach(fun); } } /** * Process callback function [[fun]] with each tile in the cache. * * Optional [[dataSource]] parameter limits processing to the tiles that belongs to * DataSource passed in. * * @param fun - The callback function to be called. * @param dataSource - The optional DataSource reference for tiles selection. */ forEachCachedTile(fun, dataSource) { this.m_dataSourceCache.forEach((tile, _) => fun(tile), dataSource); } /** * Dispose a `Tile` from cache, 'dispose()' is also called on the tile to free its resources. */ disposeTile(tile) { // TODO: Consider using evict here! this.m_dataSourceCache.delete(tile); tile.dispose(); } // Requests the tiles using the tilekeys from the DataSource and returns them, including whether // all tiles were loaded and how many are loading. processVisibleTiles(visibleTileKeys, dataSource, frameNumber, // Must be passed by reference refs, processDependentTiles) { var _a; let allDataSourceTilesLoaded = true; let numTilesLoading = 0; const visibleTiles = []; const dependentTiles = []; for (let i = 0; i < visibleTileKeys.length && visibleTiles.length < this.options.maxVisibleDataSourceTiles; i++) { const tileEntry = visibleTileKeys[i]; const tile = this.getTile(dataSource, tileEntry.tileKey, tileEntry.offset, frameNumber); if (tile === undefined) { continue; } visibleTiles.push(tile); allDataSourceTilesLoaded = allDataSourceTilesLoaded && tile.allGeometryLoaded; if (!tile.allGeometryLoaded) { numTilesLoading++; } else { // If this tile's data source is "covering" then other tiles beneath it have // their rendering skipped, see [[Tile.willRender]]. this.skipOverlappedTiles(dataSource, tile); if (this.processDelayTileRendering(tile, refs.newTilesPerFrame, frameNumber)) { refs.newTilesPerFrame++; } } // Update the visible area of the tile. This is used for those tiles that are // currently loaded and are waiting to be decoded to sort the jobs by area. tile.visibleArea = tileEntry.area; tile.elevationRange = (_a = tileEntry.elevationRange) !== null && _a !== void 0 ? _a : { minElevation: 0, maxElevation: 0 }; if (processDependentTiles) { // Add any dependent tileKeys if not already visible. Consider to optimize with a // Set if this proves to be a bottleneck (because of O(n^2) search). Given the fact // that dependencies are rare and used for non tiled data, this shouldn't be a // problem. for (const tileKey of tile.dependencies) { if (visibleTileKeys.find(tileKeyEntry => tileKeyEntry.tileKey.mortonCode() === tileKey.mortonCode()) === undefined && // Check that we haven't already added this TileKey dependentTiles.find(tileKeyEntry => tileKeyEntry.tileKey.mortonCode() === tileKey.mortonCode()) === undefined) { dependentTiles.push(new FrustumIntersection_1.TileKeyEntry(tileKey, 0)); } } } } return { allDataSourceTilesLoaded, numTilesLoading, visibleTiles, dependentTiles }; } // Processes if the tile should delay its rendering, returns if the tile is new, which is needed // to count how many tiles are generated per frame. processDelayTileRendering(tile, newTilesPerFrame, frameNumber) { let isNewTile = false; if ( // if set to 0, it will ignore the limit and upload all available this.options.maxTilesPerFrame !== 0 && newTilesPerFrame > this.options.maxTilesPerFrame && //if the tile was already visible last frame dont delay it !(tile.frameNumLastVisible === frameNumber - 1)) { tile.delayRendering = true; tile.mapView.update(); } else { if (tile.frameNumVisible < 0) { // Store the fist frame the tile became visible. tile.frameNumVisible = frameNumber; isNewTile = true; } tile.numFramesVisible++; tile.delayRendering = false; } return isNewTile; } /** * Skips rendering of tiles that are overlapped. The overlapping {@link Tile} comes from a * {@link DataSource} which is fully covering, i.e. there it is fully opaque. **/ skipOverlappedTiles(dataSource, tile) { if (this.options.projection.type === harp_geoutils_1.ProjectionType.Spherical) { // HARP-7899, currently the globe has no background planes in the tiles (it relies on // the BackgroundDataSource), because the LOD mismatches, hence disabling for globe. return; } if (dataSource.isFullyCovering()) { const key = tile.uniqueKey; const entry = this.m_coveringMap.get(key); if (entry === undefined) { // We need to reset the flag so that if the covering datasource is disabled, that // the tiles beneath then start to render. tile.skipRendering = false; this.m_coveringMap.set(key, tile); } else { // Skip the Tile if either the stored entry or the tile to consider is from the // [[BackgroundDataSource]] if (entry.dataSource instanceof BackgroundDataSource_1.BackgroundDataSource) { entry.skipRendering = true; } else if (dataSource instanceof BackgroundDataSource_1.BackgroundDataSource) { tile.skipRendering = true; } } } } // Returns the search direction and the number of levels up / down that can be searched. getSearchDirection(dataSource, visibleLevel) { const searchLevelsUp = Math.min(this.options.quadTreeSearchDistanceUp, Math.max(0, visibleLevel - dataSource.minDataLevel)); const searchLevelsDown = Math.min(this.options.quadTreeSearchDistanceDown, Math.max(0, dataSource.maxDataLevel - visibleLevel)); const searchDirection = searchLevelsDown > 0 && searchLevelsUp > 0 ? SearchDirection.BOTH : searchLevelsDown > 0 ? SearchDirection.DOWN : searchLevelsUp > 0 ? SearchDirection.UP : SearchDirection.NONE; return { searchDirection, searchLevelsUp, searchLevelsDown }; } /** * Populates the list of tiles to render, see "renderedTiles". Tiles that are loaded and which * are an exact match are added straight to the list, tiles that are still loading are replaced * with tiles in the cache that are either a parent or child of the requested tile. This helps * to prevent flickering when zooming in / out. The distance to search is based on the options * [[quadTreeSearchDistanceDown]] and [[quadTreeSearchDistanceUp]]. * * Each {@link DataSource} can also switch this behaviour on / off using the * [[allowOverlappingTiles]] flag. * */ populateRenderedTiles() { this.dataSourceTileList.forEach(renderListEntry => { const renderedTiles = renderListEntry.renderedTiles; // Tiles for which we need to fall(back/forward) to. const incompleteTiles = []; // Populate the list of tiles which can be shown ("renderedTiles"), and the list of // tiles that are incomplete, and for which we search for an alternative // ("incompleteTiles"). renderListEntry.visibleTiles.forEach(tile => { tile.levelOffset = 0; if (tile.hasGeometry && !tile.delayRendering) { renderedTiles.set(tile.uniqueKey, tile); } else { // if dataSource supports cache and it was existing before this render // then enable searching for loaded tiles in cache incompleteTiles.push(tile.uniqueKey); } }); const dataSource = renderListEntry.dataSource; if (incompleteTiles.length === 0 || dataSource.allowOverlappingTiles === false) { // Either all tiles are loaded or the datasource doesn't support using cached tiles // from other levels. return; } const dataZoomLevel = renderListEntry.zoomLevel; const { searchDirection } = this.getSearchDirection(dataSource, dataZoomLevel); // Minor optimization for the fallback search, only check parent tiles once, otherwise // the recursive algorithm checks all parent tiles multiple times, the key is the code // of the tile that is checked and the value is whether a parent was found or not. const checkedTiles = new Map(); // Iterate over incomplete (not loaded tiles) and find their parents or children that // are in cache that can be rendered temporarily until tile is loaded. Note, we favour // falling back to parent tiles rather than children. for (const tileKeyCode of incompleteTiles) { if (searchDirection === SearchDirection.BOTH || searchDirection === SearchDirection.UP) { if (this.findUp(tileKeyCode, dataZoomLevel, renderedTiles, checkedTiles, dataSource)) { // Continue to next entry so we don't search down. continue; } } if (searchDirection === SearchDirection.BOTH || searchDirection === SearchDirection.DOWN) { this.findDown(tileKeyCode, dataZoomLevel, renderedTiles, dataSource); } } }); } findDown(tileKeyCode, dataZoomLevel, renderedTiles, dataSource) { const { offset, mortonCode } = harp_geoutils_1.TileKeyUtils.extractOffsetAndMortonKeyFromKey(tileKeyCode); const tileKey = harp_geoutils_1.TileKey.fromMortonCode(mortonCode); const tilingScheme = dataSource.getTilingScheme(); for (const childTileKey of tilingScheme.getSubTileKeys(tileKey)) { const childTileCode = harp_geoutils_1.TileKeyUtils.getKeyForTileKeyAndOffset(childTileKey, offset); const childTile = this.m_dataSourceCache.get(childTileKey.mortonCode(), offset, dataSource); const nextLevelDiff = Math.abs(childTileKey.level - dataZoomLevel); if (childTile !== undefined && childTile.hasGeometry && !childTile.delayRendering) { //childTile has geometry and was/can be uploaded to the GPU, //so we can use it as fallback renderedTiles.set(childTileCode, childTile); childTile.levelOffset = nextLevelDiff; continue; } // Recurse down until the max distance is reached. if (nextLevelDiff < this.options.quadTreeSearchDistanceDown) { this.findDown(childTileCode, dataZoomLevel, renderedTiles, dataSource); } } } /** * Returns true if a tile was found in the cache which is a parent * @param tileKeyCode - Morton code of the current tile that should be searched for. * @param dataZoomLevel - The current data zoom level of tiles that are to be displayed. * @param renderedTiles - The list of tiles that are shown to the user. * @param checkedTiles - Used to map a given code to a boolean which tells us if an ancestor is * displayed or not. * @param dataSource - The provider of tiles. * @returns Whether a parent tile exists. */ findUp(tileKeyCode, dataZoomLevel, renderedTiles, checkedTiles, dataSource) { const parentCode = harp_geoutils_1.TileKeyUtils.getParentKeyFromKey(tileKeyCode); // Check if another sibling has already added the parent. if (renderedTiles.get(parentCode) !== undefined) { return true; } const exists = checkedTiles.get(parentCode); if (exists !== undefined) { return exists; } const { offset, mortonCode } = harp_geoutils_1.TileKeyUtils.extractOffsetAndMortonKeyFromKey(parentCode); const parentTile = this.m_dataSourceCache.get(mortonCode, offset, dataSource); const parentTileKey = parentTile ? parentTile.tileKey : harp_geoutils_1.TileKey.fromMortonCode(mortonCode); const nextLevelDiff = Math.abs(dataZoomLevel - parentTileKey.level); if (parentTile !== undefined && parentTile.hasGeometry && !parentTile.delayRendering) { checkedTiles.set(parentCode, true); // parentTile has geometry, so can be reused as fallback renderedTiles.set(parentCode, parentTile); // We want to have parent tiles as -ve, hence the minus. parentTile.levelOffset = -nextLevelDiff; return true; } else { checkedTiles.set(parentCode, false); } // Recurse up until the max distance is reached or we go to the parent of all parents. if (nextLevelDiff < this.options.quadTreeSearchDistanceUp && parentTileKey.level !== 0) { const foundUp = this.findUp(parentCode, dataZoomLevel, renderedTiles, checkedTiles, dataSource); // If there was a tile upstream found, then add it to the list, so we can // early skip checkedTiles. checkedTiles.set(parentCode, foundUp); if (foundUp) { return true; } } return false; } getTileImpl(dataSource, tileKey, offset, cacheOnly, frameNumber) { function touchTile(tileToUpdate) { // Keep the tile from being removed from the cache. tileToUpdate.frameNumLastRequested = frameNumber; } if (!dataSource.cacheable && !cacheOnly) { const resultTile = dataSource.getTile(tileKey, true); if (resultTile !== undefined) { this.addToTaskQueue(resultTile); touchTile(resultTile); } return resultTile; } const tileCache = this.m_dataSourceCache; let tile = tileCache.get(tileKey.mortonCode(), offset, dataSource); if (tile !== undefined && tile.offset === offset) { touchTile(tile); return tile; } if (cacheOnly) { return undefined; } tile = dataSource.getTile(tileKey, true); // TODO: Update all tile information including area, min/max elevation from TileKeyEntry if (tile !== undefined) { this.addToTaskQueue(tile); tile.offset = offset; touchTile(tile); tileCache.set(tileKey.mortonCode(), offset, dataSource, tile); } return tile; } addToTaskQueue(tile) { this.m_taskQueue.add({ execute: tile.load.bind(tile), group: MapView_1.TileTaskGroups.FETCH_AND_DECODE, getPriority: () => { var _a, _b; return (_b = (_a = tile === null || tile === void 0 ? void 0 : tile.tileLoader) === null || _a === void 0 ? void 0 : _a.priority) !== null && _b !== void 0 ? _b : 0; }, isExpired: () => { return !(tile === null || tile === void 0 ? void 0 : tile.isVisible); }, estimatedProcessTime: () => { return 1; } }); } markDataSourceTilesDirty(renderListEntry, filter) { const dataSourceCache = this.m_dataSourceCache; const retainedTiles = new Set(); const markTileDirty = (tile) => { var _a; const tileKey = DataSourceCache.getKeyForTile(tile); if (!retainedTiles.has(tileKey)) { retainedTiles.add(tileKey); // We need to cancel the loader first because if we don't then the call to // tileLoader.loadAndDecode() inside Tile::load will return the existing promise (if // the tile is still loading) and not re-request the tile data from the provider as // required. (_a = tile.tileLoader) === null || _a === void 0 ? void 0 : _a.cancel(); this.addToTaskQueue(tile); } }; renderListEntry.visibleTiles.forEach(tile => { if (filter === undefined || filter(tile)) { markTileDirty(tile); } }); renderListEntry.renderedTiles.forEach(tile => { if (filter === undefined || filter(tile)) { markTileDirty(tile); } }); dataSourceCache.forEach((tile, key) => { if ((filter === undefined || filter(tile)) && !retainedTiles.has(key)) { dataSourceCache.deleteByKey(key); tile.dispose(); } }, renderListEntry.dataSource); } // Computes the visible tile keys for each supplied data source. getVisibleTileKeysForDataSources(zoomLevel, dataSources, elevationRangeSource) { const tileKeys = Array(); let allBoundingBoxesFinal = true; if (dataSources.length === 0) { return { tileKeys, allBoundingBoxesFinal }; } const dataSourceBuckets = new Map(); dataSources.forEach(dataSource => { const tilingScheme = dataSource.getTilingScheme(); const bucket = dataSourceBuckets.get(tilingScheme); if (bucket === undefined) { dataSourceBuckets.set(tilingScheme, [dataSource]); } else { bucket.push(dataSource); } }); // If elevation is to be taken into account extend view frustum: // (near ~0, far: maxVisibilityRange) that allows to consider tiles that // are far below ground plane and high enough to intersect the frustum. if (elevationRangeSource !== undefined) { this.m_cameraOverride.copy(this.m_frustumIntersection.camera); this.m_cameraOverride.near = Math.min(this.m_cameraOverride.near, this.m_viewRange.minimum); this.m_cameraOverride.far = Math.max(this.m_cameraOverride.far, this.m_viewRange.maximum); this.m_cameraOverride.updateProjectionMatrix(); this.m_frustumIntersection.updateFrustum(this.m_cameraOverride.projectionMatrix); } else { this.m_frustumIntersection.updateFrustum(); } // For each bucket of data sources with same tiling scheme, calculate frustum intersection // once using the maximum display level. for (const [tilingScheme, bucket] of dataSourceBuckets) { const zoomLevels = bucket.map(dataSource => dataSource.getDataZoomLevel(zoomLevel)); const result = this.m_frustumIntersection.compute(tilingScheme, elevationRangeSource, zoomLevels, bucket); allBoundingBoxesFinal = allBoundingBoxesFinal && result.calculationFinal; for (const dataSource of bucket) { // For each data source check what tiles from the intersection should be rendered // at this zoom level. const visibleTileKeys = []; const dataZoomLevel = dataSource.getDataZoomLevel(zoomLevel); for (const tileKeyEntry of result.tileKeyEntries.get(dataZoomLevel).values()) { if (dataSource.canGetTile(dataZoomLevel, tileKeyEntry.tileKey)) { visibleTileKeys.push(tileKeyEntry); } } tileKeys.push({ dataSource, visibleTileKeys }); } } return { tileKeys, allBoundingBoxesFinal }; } } exports.VisibleTileSet = VisibleTileSet; function viewRangesEqual(a, b) { return (a.far === b.far && a.maximum === b.maximum && a.minimum === b.minimum && a.near === b.near); } //# sourceMappingURL=VisibleTileSet.js.map