import CesiumMath from "../Core/Math.js"; import Check from "../Core/Check.js"; import DeveloperError from "../Core/DeveloperError.js"; import MortonOrder from "../Core/MortonOrder.js"; import ImplicitSubdivisionScheme from "./ImplicitSubdivisionScheme.js"; /** * The coordinates for a tile in an implicit tileset. The coordinates * are (level, x, y) for quadtrees or (level, x, y, z) for octrees. *
* Level numbers are 0-indexed and typically start at the root of the implicit
* tileset (the tile with either implicitTiling in its JSON (3D Tiles 1.1) or
* the 3DTILES_implicit_tiling
extension).
* This object can also represent the relative offset from one set of coordinates
* to another. See {@link ImplicitTileCoordinates#getOffsetCoordinates}. The term
* local coordinates refers to coordinates that are relative to the root of a
* subtree and the term global coordinates refers to coordinates relative to the
* root of an implicit tileset.
*
* For box bounding volumes, x, y, z increase along the +x, +y, and +z * directions defined by the half axes. *
** For region bounding volumes, x increases in the +longitude direction, y * increases in the +latitude direction, and z increases in the +height * direction. *
** Care must be taken when converting between implicit coordinates and Morton * indices because there is a 16-bit limit on {@link MortonOrder#encode2D} and * a 10-bit limit on {@link MortonOrder#encode3D}. Typically these conversions * should be done on local coordinates, not global coordinates, and the maximum * number of levels in the subtree should be 15 for quadtree and 9 for octree (to * account for the extra level needed by child subtree coordinates). *
* * @alias ImplicitTileCoordinates * @constructor * * @param {object} options An object with the following properties: * @param {ImplicitSubdivisionScheme} options.subdivisionScheme Whether the coordinates are for a quadtree or octree * @param {number} options.subtreeLevels The number of distinct levels within the coordinate's subtree * @param {number} options.level The level of a tile relative to the tile with the extension * @param {number} options.x The x coordinate of the tile * @param {number} options.y The y coordinate of the tile * @param {number} [options.z] The z coordinate of the tile. Only required when options.subdivisionScheme is ImplicitSubdivisionScheme.OCTREE * @private * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy. */ function ImplicitTileCoordinates(options) { //>>includeStart('debug', pragmas.debug); Check.typeOf.string("options.subdivisionScheme", options.subdivisionScheme); Check.typeOf.number("options.subtreeLevels", options.subtreeLevels); Check.typeOf.number("options.level", options.level); Check.typeOf.number("options.x", options.x); Check.typeOf.number("options.y", options.y); if (options.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) { Check.typeOf.number("options.z", options.z); } // Check for values that are negative if (options.level < 0) { throw new DeveloperError("level must be non-negative"); } if (options.x < 0) { throw new DeveloperError("x must be non-negative"); } if (options.y < 0) { throw new DeveloperError("y must be non-negative"); } if (options.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) { if (options.z < 0) { throw new DeveloperError("z must be non-negative"); } } // Check for values that are too large const dimensionAtLevel = 1 << options.level; if (options.x >= dimensionAtLevel) { throw new DeveloperError("x is out of range"); } if (options.y >= dimensionAtLevel) { throw new DeveloperError("y is out of range"); } if (options.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) { if (options.z >= dimensionAtLevel) { throw new DeveloperError("z is out of range"); } } //>>includeEnd('debug'); /** * Whether the tileset is a quadtree or octree * * @type {ImplicitSubdivisionScheme} * @readonly * @private */ this.subdivisionScheme = options.subdivisionScheme; /** * The number of distinct levels within the coordinate's subtree * * @type {number} * @readonly * @private */ this.subtreeLevels = options.subtreeLevels; /** * Level of this tile, relative to the tile with implicit tiling in its JSON * (3D Tiles 1.1) or the3DTILES_implicit_tiling
extension.
* Level numbers start at 0.
*
* @type {number}
* @readonly
* @private
*/
this.level = options.level;
/**
* X coordinate of this tile
*
* @type {number}
* @readonly
* @private
*/
this.x = options.x;
/**
* Y coordinate of this tile
*
* @type {number}
* @readonly
* @private
*/
this.y = options.y;
/**
* Z coordinate of this tile. Only defined for octrees.
*
* @type {number|undefined}
* @readonly
* @private
*/
this.z = undefined;
if (options.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) {
this.z = options.z;
}
}
Object.defineProperties(ImplicitTileCoordinates.prototype, {
/**
* An index in the range of [0, branchingFactor) that indicates
* which child of the parent cell these coordinates correspond to.
* This can be viewed as a morton index within the parent tile.
* * This is the last 3 bits of the morton index of the tile, but it can * be computed more directly by concatenating the bits [z0] y0 x0 *
* * @type {number} * @readonly * @private */ childIndex: { get: function () { let childIndex = 0; childIndex |= this.x & 1; childIndex |= (this.y & 1) << 1; if (this.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) { childIndex |= (this.z & 1) << 2; } return childIndex; }, }, /** * Get the Morton index for this tile within the current level by interleaving * the bits of the x, y and z coordinates. * * @type {number} * @readonly * @private */ mortonIndex: { get: function () { if (this.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) { return MortonOrder.encode3D(this.x, this.y, this.z); } return MortonOrder.encode2D(this.x, this.y); }, }, /** * Get the tile index by adding the Morton index to the level offset * * @type {number} * @readonly * @private */ tileIndex: { get: function () { const levelOffset = this.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE ? // (8^N - 1) / (8-1) ((1 << (3 * this.level)) - 1) / 7 : // (4^N - 1) / (4-1) ((1 << (2 * this.level)) - 1) / 3; const mortonIndex = this.mortonIndex; return levelOffset + mortonIndex; }, }, }); /** * Check that the two coordinates are compatible * @param {ImplicitTileCoordinates} a * @param {ImplicitTileCoordinates} b * @private */ function checkMatchingSubtreeShape(a, b) { if (a.subdivisionScheme !== b.subdivisionScheme) { throw new DeveloperError("coordinates must have same subdivisionScheme"); } if (a.subtreeLevels !== b.subtreeLevels) { throw new DeveloperError("coordinates must have same subtreeLevels"); } } /** * Compute the coordinates of a tile deeper in the tree with a (level, x, y, [z]) relative offset. * * @param {ImplicitTileCoordinates} offsetCoordinates The offset from the ancestor * @returns {ImplicitTileCoordinates} The coordinates of the descendant * @private */ ImplicitTileCoordinates.prototype.getDescendantCoordinates = function ( offsetCoordinates ) { //>>includeStart('debug', pragmas.debug); Check.typeOf.object("offsetCoordinates", offsetCoordinates); checkMatchingSubtreeShape(this, offsetCoordinates); //>>includeEnd('debug'); const descendantLevel = this.level + offsetCoordinates.level; const descendantX = (this.x << offsetCoordinates.level) + offsetCoordinates.x; const descendantY = (this.y << offsetCoordinates.level) + offsetCoordinates.y; if (this.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) { const descendantZ = (this.z << offsetCoordinates.level) + offsetCoordinates.z; return new ImplicitTileCoordinates({ subdivisionScheme: this.subdivisionScheme, subtreeLevels: this.subtreeLevels, level: descendantLevel, x: descendantX, y: descendantY, z: descendantZ, }); } // Quadtree return new ImplicitTileCoordinates({ subdivisionScheme: this.subdivisionScheme, subtreeLevels: this.subtreeLevels, level: descendantLevel, x: descendantX, y: descendantY, }); }; /** * Compute the coordinates of a tile higher up in the tree by going up a number of levels. * * @param {number} offsetLevels The number of levels to go up in the tree * @returns {ImplicitTileCoordinates} The coordinates of the ancestor * @private */ ImplicitTileCoordinates.prototype.getAncestorCoordinates = function ( offsetLevels ) { //>>includeStart('debug', pragmas.debug); Check.typeOf.number("offsetLevels", offsetLevels); if (offsetLevels < 0) { throw new DeveloperError("offsetLevels must be non-negative"); } if (offsetLevels > this.level) { throw new DeveloperError("ancestor cannot be above the tileset root"); } //>>includeEnd('debug'); const divisor = 1 << offsetLevels; const ancestorLevel = this.level - offsetLevels; const ancestorX = Math.floor(this.x / divisor); const ancestorY = Math.floor(this.y / divisor); if (this.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) { const ancestorZ = Math.floor(this.z / divisor); return new ImplicitTileCoordinates({ subdivisionScheme: this.subdivisionScheme, subtreeLevels: this.subtreeLevels, level: ancestorLevel, x: ancestorX, y: ancestorY, z: ancestorZ, }); } // Quadtree return new ImplicitTileCoordinates({ subdivisionScheme: this.subdivisionScheme, subtreeLevels: this.subtreeLevels, level: ancestorLevel, x: ancestorX, y: ancestorY, }); }; /** * Compute the (level, x, y, [z]) offset to a descendant * * @param {ImplicitTileCoordinates} descendantCoordinates The descendant coordinates * @returns {ImplicitTileCoordinates} The offset between the ancestor and the descendant */ ImplicitTileCoordinates.prototype.getOffsetCoordinates = function ( descendantCoordinates ) { //>>includeStart('debug', pragmas.debug); Check.typeOf.object("descendantCoordinates", descendantCoordinates); if ( !this.isEqual(descendantCoordinates) && !this.isAncestor(descendantCoordinates) ) { throw new DeveloperError("this is not an ancestor of descendant"); } checkMatchingSubtreeShape(this, descendantCoordinates); //>>includeEnd('debug'); const offsetLevel = descendantCoordinates.level - this.level; const dimensionAtOffsetLevel = 1 << offsetLevel; const offsetX = descendantCoordinates.x % dimensionAtOffsetLevel; const offsetY = descendantCoordinates.y % dimensionAtOffsetLevel; if (this.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) { const offsetZ = descendantCoordinates.z % dimensionAtOffsetLevel; return new ImplicitTileCoordinates({ subdivisionScheme: this.subdivisionScheme, subtreeLevels: this.subtreeLevels, level: offsetLevel, x: offsetX, y: offsetY, z: offsetZ, }); } // Quadtree return new ImplicitTileCoordinates({ subdivisionScheme: this.subdivisionScheme, subtreeLevels: this.subtreeLevels, level: offsetLevel, x: offsetX, y: offsetY, }); }; /** * Given the morton index of the child, compute the coordinates of the child. * This is a special case of {@link ImplicitTileCoordinates#getDescendantCoordinates}. * * @param {number} childIndex The morton index of the child tile relative to its parent * @returns {ImplicitTileCoordinates} The tile coordinates of the child * @private */ ImplicitTileCoordinates.prototype.getChildCoordinates = function (childIndex) { //>>includeStart('debug', pragmas.debug); Check.typeOf.number("childIndex", childIndex); const branchingFactor = ImplicitSubdivisionScheme.getBranchingFactor( this.subdivisionScheme ); if (childIndex < 0 || branchingFactor <= childIndex) { throw new DeveloperError( `childIndex must be at least 0 and less than ${branchingFactor}` ); } //>>includeEnd('debug'); const level = this.level + 1; const x = 2 * this.x + (childIndex % 2); const y = 2 * this.y + (Math.floor(childIndex / 2) % 2); if (this.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) { const z = 2 * this.z + (Math.floor(childIndex / 4) % 2); return new ImplicitTileCoordinates({ subdivisionScheme: this.subdivisionScheme, subtreeLevels: this.subtreeLevels, level: level, x: x, y: y, z: z, }); } // Quadtree return new ImplicitTileCoordinates({ subdivisionScheme: this.subdivisionScheme, subtreeLevels: this.subtreeLevels, level: level, x: x, y: y, }); }; /** * Get the coordinates of the subtree that contains this tile. If the tile is * the root of the subtree, the root of the subtree is returned. * * @returns {ImplicitTileCoordinates} The subtree that contains this tile * @private */ ImplicitTileCoordinates.prototype.getSubtreeCoordinates = function () { return this.getAncestorCoordinates(this.level % this.subtreeLevels); }; /** * Get the coordinates of the parent subtree that contains this tile * * @returns {ImplicitTileCoordinates} The parent subtree that contains this tile * @private */ ImplicitTileCoordinates.prototype.getParentSubtreeCoordinates = function () { return this.getAncestorCoordinates( (this.level % this.subtreeLevels) + this.subtreeLevels ); }; /** * Returns whether this tile is an ancestor of another tile * * @param {ImplicitTileCoordinates} descendantCoordinates the descendant coordinates * @returns {boolean}true
if this tile is an ancestor of the other tile
* @private
*/
ImplicitTileCoordinates.prototype.isAncestor = function (
descendantCoordinates
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("descendantCoordinates", descendantCoordinates);
checkMatchingSubtreeShape(this, descendantCoordinates);
//>>includeEnd('debug');
const levelDifference = descendantCoordinates.level - this.level;
if (levelDifference <= 0) {
return false;
}
const ancestorX = descendantCoordinates.x >> levelDifference;
const ancestorY = descendantCoordinates.y >> levelDifference;
const isAncestorX = this.x === ancestorX;
const isAncestorY = this.y === ancestorY;
if (this.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) {
const ancestorZ = descendantCoordinates.z >> levelDifference;
const isAncestorZ = this.z === ancestorZ;
return isAncestorX && isAncestorY && isAncestorZ;
}
// Quadtree
return isAncestorX && isAncestorY;
};
/**
* Returns whether the provided coordinates are equal to this coordinate
*
* @param {ImplicitTileCoordinates} otherCoordinates the other coordinates
* @returns {boolean} true
if the coordinates are equal
* @private
*/
ImplicitTileCoordinates.prototype.isEqual = function (otherCoordinates) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("otherCoordinates", otherCoordinates);
//>>includeEnd('debug');
return (
this.subdivisionScheme === otherCoordinates.subdivisionScheme &&
this.subtreeLevels === otherCoordinates.subtreeLevels &&
this.level === otherCoordinates.level &&
this.x === otherCoordinates.x &&
this.y === otherCoordinates.y &&
(this.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE
? this.z === otherCoordinates.z
: true)
);
};
/**
* Returns whether this tile is the root of the implicit tileset
*
* @returns {boolean} true
if this tile is the root
* @private
*/
ImplicitTileCoordinates.prototype.isImplicitTilesetRoot = function () {
return this.level === 0;
};
/**
* Returns whether this tile is the root of the subtree
*
* @returns {boolean} true
if this tile is the root of the subtree
* @private
*/
ImplicitTileCoordinates.prototype.isSubtreeRoot = function () {
return this.level % this.subtreeLevels === 0;
};
/**
* Returns whether this tile is on the last row of tiles in the subtree
*
* @returns {boolean} true
if this tile is on the last row of tiles in the subtree
* @private
*/
ImplicitTileCoordinates.prototype.isBottomOfSubtree = function () {
return this.level % this.subtreeLevels === this.subtreeLevels - 1;
};
/**
* Get a dictionary of values for templating into an implicit template URI.
*
* @returns {object} An object suitable for use with {@link Resource#getDerivedResource}
* @private
*/
ImplicitTileCoordinates.prototype.getTemplateValues = function () {
const values = {
level: this.level,
x: this.x,
y: this.y,
};
if (this.subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) {
values.z = this.z;
}
return values;
};
const scratchCoordinatesArray = [0, 0, 0];
/**
* Given a level number, morton index, and whether the tileset is an
* octree/quadtree, compute the (level, x, y, [z]) coordinates
*
* @param {ImplicitSubdivisionScheme} subdivisionScheme Whether the coordinates are for a quadtree or octree
* @param {number} subtreeLevels The number of distinct levels within the coordinate's subtree
* @param {number} level The level of the tree
* @param {number} mortonIndex The morton index of the tile.
* @returns {ImplicitTileCoordinates} The coordinates of the tile with the given Morton index
* @private
*/
ImplicitTileCoordinates.fromMortonIndex = function (
subdivisionScheme,
subtreeLevels,
level,
mortonIndex
) {
let coordinatesArray;
if (subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) {
coordinatesArray = MortonOrder.decode3D(
mortonIndex,
scratchCoordinatesArray
);
return new ImplicitTileCoordinates({
subdivisionScheme: subdivisionScheme,
subtreeLevels: subtreeLevels,
level: level,
x: coordinatesArray[0],
y: coordinatesArray[1],
z: coordinatesArray[2],
});
}
coordinatesArray = MortonOrder.decode2D(mortonIndex, scratchCoordinatesArray);
return new ImplicitTileCoordinates({
subdivisionScheme: subdivisionScheme,
subtreeLevels: subtreeLevels,
level: level,
x: coordinatesArray[0],
y: coordinatesArray[1],
});
};
/**
* Given a tile index and whether the tileset is an octree/quadtree, compute
* the (level, x, y, [z]) coordinates
*
* @param {ImplicitSubdivisionScheme} subdivisionScheme Whether the coordinates are for a quadtree or octree
* @param {number} subtreeLevels The number of distinct levels within the coordinate's subtree
* @param {number} tileIndex The tile's index
* @returns {ImplicitTileCoordinates} The coordinates of the tile with the given tile index
* @private
*/
ImplicitTileCoordinates.fromTileIndex = function (
subdivisionScheme,
subtreeLevels,
tileIndex
) {
let level;
let levelOffset;
let mortonIndex;
if (subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) {
// Node count up to octree level: (8^L - 1) / (8-1)
// (8^L - 1) / (8-1) <= X < (8^(L+1) - 1) / (8-1)
// 8^L <= (7x + 1) < 8^(L+1)
// L <= log8(7x + 1) < L + 1
// L = floor(log8(7x + 1))
// L = floor(log2(7x + 1) / log2(8))
// L = floor(log2(7x + 1) / 3)
level = Math.floor(CesiumMath.log2(7 * tileIndex + 1) / 3);
levelOffset = ((1 << (3 * level)) - 1) / 7;
mortonIndex = tileIndex - levelOffset;
} else {
// Node count up to quadtree level: (4^L - 1) / (4-1)
// (4^L - 1) / (4-1) <= X < (4^(L+1) - 1) / (4-1)
// 4^L <= (3x + 1) < 4^(L+1)
// L <= log4(3x + 1) < L + 1
// L = floor(log4(3x + 1))
// L = floor(log2(3x + 1) / log2(4))
// L = floor(log2(3x + 1) / 2)
level = Math.floor(CesiumMath.log2(3 * tileIndex + 1) / 2);
levelOffset = ((1 << (2 * level)) - 1) / 3;
mortonIndex = tileIndex - levelOffset;
}
return ImplicitTileCoordinates.fromMortonIndex(
subdivisionScheme,
subtreeLevels,
level,
mortonIndex
);
};
export default ImplicitTileCoordinates;