| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735 |
- // @ts-check
-
- import defined from "../Core/defined.js";
- import DeveloperError from "../Core/DeveloperError.js";
- import Rectangle from "../Core/Rectangle.js";
- import Cartographic from "../Core/Cartographic.js";
- import QuadtreeTileLoadState from "./QuadtreeTileLoadState.js";
- import TileSelectionResult from "./TileSelectionResult.js";
-
- /** @import TilingScheme from "../Core/TilingScheme.js"; */
-
- /**
- * A simple Least Recently Used (LRU) cache implementation.
- *
- * @private
- */
- class LRUCache {
- /** @param {number} maxSize */
- constructor(maxSize) {
- this.maxSize = maxSize;
- this.cache = new Map();
- }
-
- /**
- * @param {unknown} key
- * @returns {unknown}
- */
- get(key) {
- if (!this.cache.has(key)) {
- return undefined;
- }
- // Move accessed item to the end (most recently used)
- const value = this.cache.get(key);
- this.cache.delete(key);
- this.cache.set(key, value);
- return value;
- }
-
- /**
- * @param {unknown} key
- * @param {unknown} value
- */
- set(key, value) {
- if (this.cache.has(key)) {
- this.cache.delete(key);
- } else if (this.cache.size >= this.maxSize) {
- // Remove the least recently used (first entry)
- const firstKey = this.cache.keys().next().value;
- this.cache.delete(firstKey);
- }
- this.cache.set(key, value);
- }
-
- /**
- * @type {number}
- * @readonly
- */
- get size() {
- return this.cache.size;
- }
-
- clear() {
- this.cache.clear();
- }
- }
-
- // Maximum cache entries per tile
- const MAX_CACHE_ENTRIES = 1000;
-
- /**
- * A single tile in a {@link QuadtreePrimitive}.
- *
- * @private
- */
- class QuadtreeTile {
- /**
- * @param {object} options
- * @param {number} options.level The level of the tile in the quadtree.
- * @param {number} options.x The X coordinate of the tile in the quadtree. 0 is the westernmost tile.
- * @param {number} options.y The Y coordinate of the tile in the quadtree. 0 is the northernmost tile.
- * @param {TilingScheme} options.tilingScheme The tiling scheme in which this tile exists.
- * @param {QuadtreeTile} [options.parent] This tile's parent, or undefined if this is a root tile.
- */
- constructor(options) {
- //>>includeStart('debug', pragmas.debug);
- if (!defined(options)) {
- throw new DeveloperError("options is required.");
- }
- if (!defined(options.x)) {
- throw new DeveloperError("options.x is required.");
- } else if (!defined(options.y)) {
- throw new DeveloperError("options.y is required.");
- } else if (options.x < 0 || options.y < 0) {
- throw new DeveloperError(
- "options.x and options.y must be greater than or equal to zero.",
- );
- }
- if (!defined(options.level)) {
- throw new DeveloperError(
- "options.level is required and must be greater than or equal to zero.",
- );
- }
- if (!defined(options.tilingScheme)) {
- throw new DeveloperError("options.tilingScheme is required.");
- }
- //>>includeEnd('debug');
-
- this._tilingScheme = options.tilingScheme;
- this._x = options.x;
- this._y = options.y;
- this._level = options.level;
- this._parent = options.parent;
-
- /** @type {Rectangle} */
- this._rectangle = this._tilingScheme.tileXYToRectangle(
- this._x,
- this._y,
- this._level,
- );
-
- this._southwestChild = undefined;
- this._southeastChild = undefined;
- this._northwestChild = undefined;
- this._northeastChild = undefined;
-
- // TileReplacementQueue gets/sets these private properties.
- this.replacementPrevious = undefined;
- this.replacementNext = undefined;
-
- // The distance from the camera to this tile, updated when the tile is selected
- // for rendering. We can get rid of this if we have a better way to sort by
- // distance - for example, by using the natural ordering of a quadtree.
- // QuadtreePrimitive gets/sets this private property.
- this._distance = 0.0;
- this._loadPriority = 0.0;
-
- this._customData = new Set();
- this._customDataIterator = undefined;
- /** @type {unknown[]} */
- this._addedCustomData = [];
- /** @type {unknown[]} */
- this._removedCustomData = [];
- this._lastSelectionResult = TileSelectionResult.NONE;
- this._lastSelectionResultFrame = undefined;
- this._loadedCallbacks = {};
-
- // Cache for storing computed position values per tile to avoid redundant calculations
- this._positionCache = new LRUCache(MAX_CACHE_ENTRIES);
-
- /**
- * Gets or sets the current state of the tile in the tile load pipeline.
- * @type {QuadtreeTileLoadState}
- * @default {@link QuadtreeTileLoadState.START}
- */
- this.state = QuadtreeTileLoadState.START;
-
- /**
- * Gets or sets a value indicating whether or not the tile is currently renderable.
- * @type {boolean}
- * @default false
- */
- this.renderable = false;
-
- /**
- * Gets or set a value indicating whether or not the tile was entirely upsampled from its
- * parent tile. If all four children of a parent tile were upsampled from the parent,
- * we will render the parent instead of the children even if the LOD indicates that
- * the children would be preferable.
- * @type {boolean}
- * @default false
- */
- this.upsampledFromParent = false;
-
- /**
- * Gets or sets the additional data associated with this tile. The exact content is specific to the
- * {@link QuadtreeTileProvider}.
- * @type {object}
- * @default undefined
- */
- this.data = undefined;
- }
-
- /**
- * Creates a rectangular set of tiles for level of detail zero, the coarsest, least detailed level.
- *
- *
- * @param {TilingScheme} tilingScheme The tiling scheme for which the tiles are to be created.
- * @returns {QuadtreeTile[]} An array containing the tiles at level of detail zero, starting with the
- * tile in the northwest corner and followed by the tile (if any) to its east.
- */
- static createLevelZeroTiles(tilingScheme) {
- //>>includeStart('debug', pragmas.debug);
- if (!defined(tilingScheme)) {
- throw new DeveloperError("tilingScheme is required.");
- }
- //>>includeEnd('debug');
-
- const numberOfLevelZeroTilesX = tilingScheme.getNumberOfXTilesAtLevel(0);
- const numberOfLevelZeroTilesY = tilingScheme.getNumberOfYTilesAtLevel(0);
-
- const result = new Array(numberOfLevelZeroTilesX * numberOfLevelZeroTilesY);
-
- let index = 0;
- for (let y = 0; y < numberOfLevelZeroTilesY; ++y) {
- for (let x = 0; x < numberOfLevelZeroTilesX; ++x) {
- result[index++] = new QuadtreeTile({
- tilingScheme: tilingScheme,
- x: x,
- y: y,
- level: 0,
- });
- }
- }
-
- return result;
- }
-
- /**
- * Generates a unique cache key for a given cartographic position.
- *
- * @param {Cartographic} cartographic The cartographic coordinates.
- * @param {number} maximumScreenSpaceError The maximum screen-space error, in pixels, that is allowed.
- * A higher maximum error will render fewer tiles and improve performance, while a lower
- * value will improve visual quality.
- * @returns {string} A string representing the spatial hash key.
- */
- _getCacheKey(cartographic, maximumScreenSpaceError) {
- return createSpatialHashKey(
- cartographic.longitude,
- cartographic.latitude,
- this._rectangle,
- maximumScreenSpaceError,
- );
- }
-
- /**
- * Retrieves a cached position for the specified cartographic position.
- *
- *
- * @param {Cartographic} cartographic - The cartographic coordinates.
- * @param {number} maximumScreenSpaceError The maximum screen-space error, in pixels, that is allowed.
- * A higher maximum error will render fewer tiles and improve performance, while a lower
- * value will improve visual quality.
- * @returns {object|undefined} The cached position data or undefined if not found.
- */
- getPositionCacheEntry(cartographic, maximumScreenSpaceError) {
- const result = this._positionCache.get(
- this._getCacheKey(cartographic, maximumScreenSpaceError),
- );
- return /** @type {object|undefined} */ (result);
- }
-
- /**
- * Sets a position on the cache for this tile.
- *
- *
- * @param {Cartographic} cartographic - The cartographic coordinates.
- * @param {number} maximumScreenSpaceError The maximum screen-space error, in pixels, that is allowed.
- * A higher maximum error will render fewer tiles and improve performance, while a lower
- * value will improve visual quality.
- * @param {object} value - The object to be cached.
- */
- setPositionCacheEntry(cartographic, maximumScreenSpaceError, value) {
- this._positionCache.set(
- this._getCacheKey(cartographic, maximumScreenSpaceError),
- value,
- );
- }
-
- /**
- * Clears the position cache for this tile.
- * This function removes all cached positions that were previously stored
- * to optimize height computations.
- *
- */
- clearPositionCache() {
- if (this._positionCache.size > 0) {
- this._positionCache.clear();
- }
- }
-
- updateCustomData() {
- const added = this._addedCustomData;
- const removed = this._removedCustomData;
- if (added.length === 0 && removed.length === 0) {
- return;
- }
-
- const customData = this.customData;
- for (let i = 0; i < added.length; ++i) {
- const data = /** @type {*} */ (added[i]);
- customData.add(data);
-
- const child = childTileAtPosition(this, data.positionCartographic);
- child._addedCustomData.push(data);
- }
- this._addedCustomData.length = 0;
-
- for (let i = 0; i < removed.length; ++i) {
- const data = /** @type {*} */ (removed[i]);
- if (customData.has(data)) {
- customData.delete(data);
- }
-
- const child = childTileAtPosition(this, data.positionCartographic);
- child._removedCustomData.push(data);
- }
- this._removedCustomData.length = 0;
- }
-
- /**
- * Gets the tiling scheme used to tile the surface.
- * @type {TilingScheme}
- */
- get tilingScheme() {
- return this._tilingScheme;
- }
-
- /**
- * Gets the tile X coordinate.
- * @type {number}
- */
- get x() {
- return this._x;
- }
-
- /**
- * Gets the tile Y coordinate.
- * @type {number}
- */
- get y() {
- return this._y;
- }
-
- /**
- * Gets the level-of-detail, where zero is the coarsest, least-detailed.
- * @type {number}
- */
- get level() {
- return this._level;
- }
-
- /**
- * Gets the parent tile of this tile.
- * @type {QuadtreeTile}
- */
- get parent() {
- return this._parent;
- }
-
- /**
- * Gets the cartographic rectangle of the tile, with north, south, east and
- * west properties in radians.
- * @type {Rectangle}
- */
- get rectangle() {
- return this._rectangle;
- }
-
- /**
- * An array of tiles that is at the next level of the tile tree.
- * @type {QuadtreeTile[]}
- */
- get children() {
- return [
- this.northwestChild,
- this.northeastChild,
- this.southwestChild,
- this.southeastChild,
- ];
- }
-
- /**
- * Gets the southwest child tile.
- * @type {QuadtreeTile}
- */
- get southwestChild() {
- if (!defined(this._southwestChild)) {
- this._southwestChild = new QuadtreeTile({
- tilingScheme: this.tilingScheme,
- x: this.x * 2,
- y: this.y * 2 + 1,
- level: this.level + 1,
- parent: this,
- });
- }
- return this._southwestChild;
- }
-
- /**
- * Gets the southeast child tile.
- * @type {QuadtreeTile}
- */
- get southeastChild() {
- if (!defined(this._southeastChild)) {
- this._southeastChild = new QuadtreeTile({
- tilingScheme: this.tilingScheme,
- x: this.x * 2 + 1,
- y: this.y * 2 + 1,
- level: this.level + 1,
- parent: this,
- });
- }
- return this._southeastChild;
- }
-
- /**
- * Gets the northwest child tile.
- * @type {QuadtreeTile}
- */
- get northwestChild() {
- if (!defined(this._northwestChild)) {
- this._northwestChild = new QuadtreeTile({
- tilingScheme: this.tilingScheme,
- x: this.x * 2,
- y: this.y * 2,
- level: this.level + 1,
- parent: this,
- });
- }
- return this._northwestChild;
- }
-
- /**
- * Gets the northeast child tile.
- * @type {QuadtreeTile}
- */
- get northeastChild() {
- if (!defined(this._northeastChild)) {
- this._northeastChild = new QuadtreeTile({
- tilingScheme: this.tilingScheme,
- x: this.x * 2 + 1,
- y: this.y * 2,
- level: this.level + 1,
- parent: this,
- });
- }
- return this._northeastChild;
- }
-
- /**
- * A set of objects associated with this tile.
- * @type {Set<*>}
- */
- get customData() {
- return this._customData;
- }
-
- /**
- * Gets a value indicating whether or not this tile needs further loading.
- * This property will return true if the {@link QuadtreeTile#state} is
- * <code>START</code> or <code>LOADING</code>.
- * @type {boolean}
- */
- get needsLoading() {
- return this.state < QuadtreeTileLoadState.DONE;
- }
-
- /**
- * Gets a value indicating whether or not this tile is eligible to be unloaded.
- * Typically, a tile is ineligible to be unloaded while an asynchronous operation,
- * such as a request for data, is in progress on it. A tile will never be
- * unloaded while it is needed for rendering, regardless of the value of this
- * property. If {@link QuadtreeTile#data} is defined and has an
- * <code>eligibleForUnloading</code> property, the value of that property is returned.
- * Otherwise, this property returns true.
- * @type {boolean}
- */
- get eligibleForUnloading() {
- let result = true;
-
- if (defined(this.data)) {
- result = /** @type {*} */ (this.data).eligibleForUnloading;
- if (!defined(result)) {
- result = true;
- }
- }
-
- return result;
- }
-
- /**
- * @param {QuadtreeTile[]} levelZeroTiles
- * @param {number} x
- * @param {number} y
- * @returns {QuadtreeTile}
- */
- findLevelZeroTile(levelZeroTiles, x, y) {
- const xTiles = this.tilingScheme.getNumberOfXTilesAtLevel(0);
- if (x < 0) {
- x += xTiles;
- } else if (x >= xTiles) {
- x -= xTiles;
- }
-
- if (y < 0 || y >= this.tilingScheme.getNumberOfYTilesAtLevel(0)) {
- return undefined;
- }
-
- return levelZeroTiles.filter(function (tile) {
- return tile.x === x && tile.y === y;
- })[0];
- }
-
- /**
- * @param {QuadtreeTile[]} levelZeroTiles
- * @returns {QuadtreeTile|undefined}
- */
- findTileToWest(levelZeroTiles) {
- const parent = this.parent;
- if (parent === undefined) {
- return this.findLevelZeroTile(levelZeroTiles, this.x - 1, this.y);
- }
-
- if (parent.southeastChild === this) {
- return parent.southwestChild;
- } else if (parent.northeastChild === this) {
- return parent.northwestChild;
- }
-
- const westOfParent = parent.findTileToWest(levelZeroTiles);
- if (westOfParent === undefined) {
- return undefined;
- } else if (parent.southwestChild === this) {
- return westOfParent.southeastChild;
- }
- return westOfParent.northeastChild;
- }
-
- /**
- * @param {QuadtreeTile[]} levelZeroTiles
- * @returns {QuadtreeTile|undefined}
- */
- findTileToEast(levelZeroTiles) {
- const parent = this.parent;
- if (parent === undefined) {
- return this.findLevelZeroTile(levelZeroTiles, this.x + 1, this.y);
- }
-
- if (parent.southwestChild === this) {
- return parent.southeastChild;
- } else if (parent.northwestChild === this) {
- return parent.northeastChild;
- }
-
- const eastOfParent = parent.findTileToEast(levelZeroTiles);
- if (eastOfParent === undefined) {
- return undefined;
- } else if (parent.southeastChild === this) {
- return eastOfParent.southwestChild;
- }
- return eastOfParent.northwestChild;
- }
-
- /**
- * @param {QuadtreeTile[]} levelZeroTiles
- * @returns {QuadtreeTile|undefined}
- */
- findTileToSouth(levelZeroTiles) {
- const parent = this.parent;
- if (parent === undefined) {
- return this.findLevelZeroTile(levelZeroTiles, this.x, this.y + 1);
- }
-
- if (parent.northwestChild === this) {
- return parent.southwestChild;
- } else if (parent.northeastChild === this) {
- return parent.southeastChild;
- }
-
- const southOfParent = parent.findTileToSouth(levelZeroTiles);
- if (southOfParent === undefined) {
- return undefined;
- } else if (parent.southwestChild === this) {
- return southOfParent.northwestChild;
- }
- return southOfParent.northeastChild;
- }
-
- /**
- * @param {QuadtreeTile[]} levelZeroTiles
- * @returns {QuadtreeTile|undefined}
- */
- findTileToNorth(levelZeroTiles) {
- const parent = this.parent;
- if (parent === undefined) {
- return this.findLevelZeroTile(levelZeroTiles, this.x, this.y - 1);
- }
-
- if (parent.southwestChild === this) {
- return parent.northwestChild;
- } else if (parent.southeastChild === this) {
- return parent.northeastChild;
- }
-
- const northOfParent = parent.findTileToNorth(levelZeroTiles);
- if (northOfParent === undefined) {
- return undefined;
- } else if (parent.northwestChild === this) {
- return northOfParent.southwestChild;
- }
- return northOfParent.southeastChild;
- }
-
- /**
- * Frees the resources associated with this tile and returns it to the <code>START</code>
- * {@link QuadtreeTileLoadState}. If the {@link QuadtreeTile#data} property is defined and it
- * has a <code>freeResources</code> method, the method will be invoked.
- *
- */
- freeResources() {
- // Clears cached heights when the tile is freed
- this.clearPositionCache();
- this.state = QuadtreeTileLoadState.START;
- this.renderable = false;
- this.upsampledFromParent = false;
-
- const data = /** @type {*} */ (this.data);
- if (defined(data) && defined(data.freeResources)) {
- data.freeResources();
- }
-
- freeTile(this._southwestChild);
- this._southwestChild = undefined;
- freeTile(this._southeastChild);
- this._southeastChild = undefined;
- freeTile(this._northwestChild);
- this._northwestChild = undefined;
- freeTile(this._northeastChild);
- this._northeastChild = undefined;
- }
- }
-
- /**
- * Creates a spatial hash key for the given longitude, latitude, and tile level.
- * The precision is adjusted based on the tile level and extent to achieve finer precision at higher levels.
- *
- * This function calculates the spatial hash key by first determining the precision at the given tile for the current maximum screenspace error (MAX_ERROR_PX),
- * and then rounding the longitude and latitude to that precision for consistency.
- *
- * The steps for computing the level precision are as follows:
- *
- * 1. Compute the resolution (meters per pixel) at the given level:
- * level_resolution_m = (2 * PI * RADIUS) / (2^level * TILE_SIZE)
- *
- * 2. Compute the target precision in meters:
- * level_precision_m = level_resolution_m * MAX_ERROR_PX
- *
- * 3. Compute the target precision to radians:
- * level_precision_rad = level_precision_m / BODY_RADIUS
- *
- * This simplifies to:
- * level_precision_rad = (2 * PI * MAX_ERROR_PX) / (2^level * TILE_SIZE)
- * which can also be written as:
- * level_precision_rad = (PI * MAX_ERROR_PX) / (2^(level-1) * TILE_SIZE)
- *
- * The computed level_precision_rad is then used to round the input longitude and latitude,
- * ensuring that positions that fall within the same spatial bin produce the same hash key.
- *
- * The constants below are computed once since they are fixed for the given configuration.
- *
- * @param {number} longitude - The longitude in radians.
- * @param {number} latitude - The latitude in radians.
- * @param {Rectangle} rectangle - The quadtree tile extents.
- * @returns {string} A string representing the spatial hash key.
- */
- const TILE_SIZE = 256;
-
- /**
- * @param {number} longitude
- * @param {number} latitude
- * @param {Rectangle} rectangle
- * @param {number} maximumScreenSpaceError
- * @returns {string}
- * @ignore
- */
- function createSpatialHashKey(
- longitude,
- latitude,
- rectangle,
- maximumScreenSpaceError,
- ) {
- // Adjust precision based on quadtree level - higher levels get finer precision
- const maxError = (rectangle.width / TILE_SIZE) * maximumScreenSpaceError;
- // Round to the grid precision
- const lonGrid = Math.floor(longitude / maxError) * maxError;
- const latGrid = Math.floor(latitude / maxError) * maxError;
- return `${lonGrid.toFixed(10)},${latGrid.toFixed(10)}`;
- }
-
- const splitPointScratch = new Cartographic();
-
- /**
- * Determines which child tile that contains the specified position. Assumes the position is within
- * the bounds of the parent tile.
- * @private
- * @param {QuadtreeTile} tile - The parent tile.
- * @param {Cartographic} positionCartographic - The cartographic position.
- * @returns {QuadtreeTile} The child tile that contains the position.
- */
- function childTileAtPosition(tile, positionCartographic) {
- // Can't assume that a given tiling scheme divides a parent into four tiles at its rectangle's center.
- // But we can safely take any child tile's rectangle and take its center-facing corner as the parent's split point.
- const nwChildRectangle = tile.northwestChild.rectangle;
- const tileSplitPoint = Rectangle.southeast(
- nwChildRectangle,
- splitPointScratch,
- );
-
- const x = positionCartographic.longitude >= tileSplitPoint.longitude ? 1 : 0;
- const y = positionCartographic.latitude < tileSplitPoint.latitude ? 1 : 0;
-
- switch (y * 2 + x) {
- case 0:
- return tile.northwestChild;
- case 1:
- return tile.northeastChild;
- case 2:
- return tile.southwestChild;
- default:
- return tile.southeastChild;
- }
- }
-
- /**
- * @param {QuadtreeTile} tile
- * @ignore
- */
- function freeTile(tile) {
- if (defined(tile)) {
- tile.freeResources();
- }
- }
-
- export default QuadtreeTile;
|