import Check from "../Core/Check.js";
import defaultValue from "../Core/defaultValue.js";
import DeveloperError from "../Core/DeveloperError.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import getJsonFromTypedArray from "../Core/getJsonFromTypedArray.js";
import RuntimeError from "../Core/RuntimeError.js";
import hasExtension from "./hasExtension.js";
import ImplicitAvailabilityBitstream from "./ImplicitAvailabilityBitstream.js";
import ImplicitMetadataView from "./ImplicitMetadataView.js";
import ImplicitSubdivisionScheme from "./ImplicitSubdivisionScheme.js";
import ImplicitSubtreeMetadata from "./ImplicitSubtreeMetadata.js";
import MetadataTable from "./MetadataTable.js";
import ResourceCache from "./ResourceCache.js";
/**
* An object representing a single subtree in an implicit tileset
* including availability.
*
* Subtrees handle tile metadata, defined in the subtree JSON in either
* tileMetadata (3D Tiles 1.1) or the 3DTILES_metadata extension.
* Subtrees also handle content metadata and metadata about the subtree itself.
*
*
* This object is normally not instantiated directly, use {@link ImplicitSubtree.fromSubtreeJson}.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_metadata#implicit-tile-properties|Implicit Tile Properties in the 3DTILES_metadata specification}
* @see ImplicitSubtree.fromSubtreeJson
*
* @alias ImplicitSubtree
* @constructor
*
* @param {Resource} resource The resource for this subtree. This is used for fetching external buffers as needed.
* @param {ImplicitTileset} implicitTileset The implicit tileset. This includes information about the size of subtrees
* @param {ImplicitTileCoordinates} implicitCoordinates The coordinates of the subtree's root tile.
*
* @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 ImplicitSubtree(resource, implicitTileset, implicitCoordinates) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("resource", resource);
Check.typeOf.object("implicitTileset", implicitTileset);
Check.typeOf.object("implicitCoordinates", implicitCoordinates);
//>>includeEnd('debug');
this._resource = resource;
this._subtreeJson = undefined;
this._bufferLoader = undefined;
this._tileAvailability = undefined;
this._contentAvailabilityBitstreams = [];
this._childSubtreeAvailability = undefined;
this._implicitCoordinates = implicitCoordinates;
this._subtreeLevels = implicitTileset.subtreeLevels;
this._subdivisionScheme = implicitTileset.subdivisionScheme;
this._branchingFactor = implicitTileset.branchingFactor;
// properties for metadata
this._metadata = undefined;
this._tileMetadataTable = undefined;
this._tilePropertyTableJson = undefined;
this._contentMetadataTables = [];
this._contentPropertyTableJsons = [];
// Jump buffers are maps of availability bit index to entity ID
this._tileJumpBuffer = undefined;
this._contentJumpBuffers = [];
this._ready = false;
}
Object.defineProperties(ImplicitSubtree.prototype, {
/**
* Returns true once all necessary availability buffers
* are loaded.
*
* @type {boolean}
* @readonly
* @private
*/
ready: {
get: function () {
return this._ready;
},
},
/**
* When subtree metadata is present (3D Tiles 1.1), this property stores an {@link ImplicitSubtreeMetadata} instance
*
* @type {ImplicitSubtreeMetadata}
* @readonly
* @private
*/
metadata: {
get: function () {
return this._metadata;
},
},
/**
* When tile metadata is present (3D Tiles 1.1) or the 3DTILES_metadata extension is used,
* this property stores a {@link MetadataTable} instance for the tiles in the subtree.
*
* @type {MetadataTable}
* @readonly
* @private
*/
tileMetadataTable: {
get: function () {
return this._tileMetadataTable;
},
},
/**
* When tile metadata is present (3D Tiles 1.1) or the 3DTILES_metadata extension is used,
* this property stores the JSON from the extension. This is used by {@link TileMetadata}
* to get the extras and extensions for the tiles in the subtree.
*
* @type {object}
* @readonly
* @private
*/
tilePropertyTableJson: {
get: function () {
return this._tilePropertyTableJson;
},
},
/**
* When content metadata is present (3D Tiles 1.1), this property stores
* an array of {@link MetadataTable} instances for the contents in the subtree.
*
* @type {Array}
* @readonly
* @private
*/
contentMetadataTables: {
get: function () {
return this._contentMetadataTables;
},
},
/**
* When content metadata is present (3D Tiles 1.1), this property
* an array of the JSONs from the extension. This is used to get the extras
* and extensions for the contents in the subtree.
*
* @type {Array}
* @readonly
* @private
*/
contentPropertyTableJsons: {
get: function () {
return this._contentPropertyTableJsons;
},
},
/**
* Gets the implicit tile coordinates for the root of the subtree.
*
* @type {ImplicitTileCoordinates}
* @readonly
* @private
*/
implicitCoordinates: {
get: function () {
return this._implicitCoordinates;
},
},
});
/**
* Check if a specific tile is available at an index of the tile availability bitstream
*
* @param {number} index The index of the desired tile
* @returns {boolean} The value of the i-th bit
* @private
*/
ImplicitSubtree.prototype.tileIsAvailableAtIndex = function (index) {
return this._tileAvailability.getBit(index);
};
/**
* Check if a specific tile is available at an implicit tile coordinate
*
* @param {ImplicitTileCoordinates} implicitCoordinates The global coordinates of a tile
* @returns {boolean} The value of the i-th bit
* @private
*/
ImplicitSubtree.prototype.tileIsAvailableAtCoordinates = function (
implicitCoordinates
) {
const index = this.getTileIndex(implicitCoordinates);
return this.tileIsAvailableAtIndex(index);
};
/**
* Check if a specific tile's content is available at an index of the content availability bitstream
*
* @param {number} index The index of the desired tile
* @param {number} [contentIndex=0] The index of the desired content when multiple contents are used.
* @returns {boolean} The value of the i-th bit
* @private
*/
ImplicitSubtree.prototype.contentIsAvailableAtIndex = function (
index,
contentIndex
) {
contentIndex = defaultValue(contentIndex, 0);
//>>includeStart('debug', pragmas.debug);
if (
contentIndex < 0 ||
contentIndex >= this._contentAvailabilityBitstreams.length
) {
throw new DeveloperError("contentIndex out of bounds.");
}
//>>includeEnd('debug');
return this._contentAvailabilityBitstreams[contentIndex].getBit(index);
};
/**
* Check if a specific tile's content is available at an implicit tile coordinate
*
* @param {ImplicitTileCoordinates} implicitCoordinates The global coordinates of a tile
* @param {number} [contentIndex=0] The index of the desired content when the 3DTILES_multiple_contents extension is used.
* @returns {boolean} The value of the i-th bit
* @private
*/
ImplicitSubtree.prototype.contentIsAvailableAtCoordinates = function (
implicitCoordinates,
contentIndex
) {
const index = this.getTileIndex(implicitCoordinates);
return this.contentIsAvailableAtIndex(index, contentIndex);
};
/**
* Check if a child subtree is available at an index of the child subtree availability bitstream
*
* @param {number} index The index of the desired child subtree
* @returns {boolean} The value of the i-th bit
* @private
*/
ImplicitSubtree.prototype.childSubtreeIsAvailableAtIndex = function (index) {
return this._childSubtreeAvailability.getBit(index);
};
/**
* Check if a specific child subtree is available at an implicit tile coordinate
*
* @param {ImplicitTileCoordinates} implicitCoordinates The global coordinates of a child subtree
* @returns {boolean} The value of the i-th bit
* @private
*/
ImplicitSubtree.prototype.childSubtreeIsAvailableAtCoordinates = function (
implicitCoordinates
) {
const index = this.getChildSubtreeIndex(implicitCoordinates);
return this.childSubtreeIsAvailableAtIndex(index);
};
/**
* Get the index of the first node at the given level within this subtree.
* e.g. for a quadtree:
*
*
Level 0 starts at index 0
*
Level 1 starts at index 1
*
Level 2 starts at index 5
*
*
* @param {number} level The 0-indexed level number relative to the root of the subtree
* @returns {number} The first index at the desired level
* @private
*/
ImplicitSubtree.prototype.getLevelOffset = function (level) {
const branchingFactor = this._branchingFactor;
return (Math.pow(branchingFactor, level) - 1) / (branchingFactor - 1);
};
/**
* Get the morton index of a tile's parent. This is equivalent to
* chopping off the last 2 (quadtree) or 3 (octree) bits of the morton
* index.
*
* @param {number} childIndex The morton index of the child tile relative to its parent
* @returns {number} The index of the child's parent node
* @private
*/
ImplicitSubtree.prototype.getParentMortonIndex = function (mortonIndex) {
let bitsPerLevel = 2;
if (this._subdivisionScheme === ImplicitSubdivisionScheme.OCTREE) {
bitsPerLevel = 3;
}
return mortonIndex >> bitsPerLevel;
};
/**
* Parse all relevant information out of the subtree. This fetches any
* external buffers that are used by the implicit tileset.
*
* @param {Resource} resource The resource for this subtree. This is used for fetching external buffers as needed.
* @param {object} [json] The JSON object for this subtree. If parsing from a binary subtree file, this will be undefined.
* @param {Uint8Array} [subtreeView] The contents of the subtree binary
* @param {ImplicitTileset} implicitTileset The implicit tileset this subtree belongs to.
* @param {ImplicitTileCoordinates} implicitCoordinates The coordinates of the subtree's root tile.
* @return {Promise} The created subtree
* @private
*
* @exception {DeveloperError} One of json and subtreeView must be defined.
*/
ImplicitSubtree.fromSubtreeJson = async function (
resource,
json,
subtreeView,
implicitTileset,
implicitCoordinates
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("resource", resource);
if (defined(json) === defined(subtreeView)) {
throw new DeveloperError("One of json and subtreeView must be defined.");
}
Check.typeOf.object("implicitTileset", implicitTileset);
Check.typeOf.object("implicitCoordinates", implicitCoordinates);
//>>includeEnd('debug');
const subtree = new ImplicitSubtree(
resource,
implicitTileset,
implicitCoordinates
);
let chunks;
if (defined(json)) {
chunks = {
json: json,
binary: undefined,
};
} else {
chunks = parseSubtreeChunks(subtreeView);
}
const subtreeJson = chunks.json;
subtree._subtreeJson = subtreeJson;
let tilePropertyTableJson;
if (hasExtension(subtreeJson, "3DTILES_metadata")) {
tilePropertyTableJson = subtreeJson.extensions["3DTILES_metadata"];
} else if (defined(subtreeJson.tileMetadata)) {
const propertyTableIndex = subtreeJson.tileMetadata;
tilePropertyTableJson = subtreeJson.propertyTables[propertyTableIndex];
}
const contentPropertyTableJsons = [];
if (defined(subtreeJson.contentMetadata)) {
const length = subtreeJson.contentMetadata.length;
for (let i = 0; i < length; i++) {
const propertyTableIndex = subtreeJson.contentMetadata[i];
contentPropertyTableJsons.push(
subtreeJson.propertyTables[propertyTableIndex]
);
}
}
let metadata;
const schema = implicitTileset.metadataSchema;
const subtreeMetadata = subtreeJson.subtreeMetadata;
if (defined(subtreeMetadata)) {
const metadataClass = subtreeMetadata.class;
const subtreeMetadataClass = schema.classes[metadataClass];
metadata = new ImplicitSubtreeMetadata({
subtreeMetadata: subtreeMetadata,
class: subtreeMetadataClass,
});
}
subtree._metadata = metadata;
subtree._tilePropertyTableJson = tilePropertyTableJson;
subtree._contentPropertyTableJsons = contentPropertyTableJsons;
// if no contentAvailability is specified, no tile in the subtree has
// content
const defaultContentAvailability = {
constant: 0,
};
// In 3D Tiles 1.1, content availability is provided in an array in the subtree JSON
// regardless of whether or not it contains multiple contents. This differs from previous
// schemas, where content availability is either a single object in the subtree JSON or
// as an array in the 3DTILES_multiple_contents extension.
//
// After identifying how availability is stored, put the results in this new array for consistent processing later
subtreeJson.contentAvailabilityHeaders = [];
if (hasExtension(subtreeJson, "3DTILES_multiple_contents")) {
subtreeJson.contentAvailabilityHeaders =
subtreeJson.extensions["3DTILES_multiple_contents"].contentAvailability;
} else if (Array.isArray(subtreeJson.contentAvailability)) {
subtreeJson.contentAvailabilityHeaders = subtreeJson.contentAvailability;
} else {
subtreeJson.contentAvailabilityHeaders.push(
defaultValue(subtreeJson.contentAvailability, defaultContentAvailability)
);
}
const bufferHeaders = preprocessBuffers(subtreeJson.buffers);
const bufferViewHeaders = preprocessBufferViews(
subtreeJson.bufferViews,
bufferHeaders
);
// Buffers and buffer views are inactive until explicitly marked active.
// This way we can avoid fetching buffers that will not be used.
markActiveBufferViews(subtreeJson, bufferViewHeaders);
if (defined(tilePropertyTableJson)) {
markActiveMetadataBufferViews(tilePropertyTableJson, bufferViewHeaders);
}
for (let i = 0; i < contentPropertyTableJsons.length; i++) {
const contentPropertyTableJson = contentPropertyTableJsons[i];
markActiveMetadataBufferViews(contentPropertyTableJson, bufferViewHeaders);
}
const buffersU8 = await requestActiveBuffers(
subtree,
bufferHeaders,
chunks.binary
);
const bufferViewsU8 = parseActiveBufferViews(bufferViewHeaders, buffersU8);
parseAvailability(subtree, subtreeJson, implicitTileset, bufferViewsU8);
if (defined(tilePropertyTableJson)) {
parseTileMetadataTable(subtree, implicitTileset, bufferViewsU8);
makeTileJumpBuffer(subtree);
}
parseContentMetadataTables(subtree, implicitTileset, bufferViewsU8);
makeContentJumpBuffers(subtree);
subtree._ready = true;
return subtree;
};
/**
* A helper object for storing the two parts of the subtree binary
*
* @typedef {object} SubtreeChunks
* @property {object} json The json chunk of the subtree
* @property {Uint8Array} binary The binary chunk of the subtree. This represents the internal buffer.
* @private
*/
/**
* Given the binary contents of a subtree, split into JSON and binary chunks
*
* @param {Uint8Array} subtreeView The subtree binary
* @returns {SubtreeChunks} An object containing the JSON and binary chunks.
* @private
*/
function parseSubtreeChunks(subtreeView) {
// Parse the header
const littleEndian = true;
const subtreeReader = new DataView(
subtreeView.buffer,
subtreeView.byteOffset
);
// Skip to the chunk lengths
let byteOffset = 8;
// Read the bottom 32 bits of the 64-bit byte length. This is ok for now because:
// 1) not all browsers have native 64-bit operations
// 2) the data is well under 4GB
const jsonByteLength = subtreeReader.getUint32(byteOffset, littleEndian);
byteOffset += 8;
const binaryByteLength = subtreeReader.getUint32(byteOffset, littleEndian);
byteOffset += 8;
const subtreeJson = getJsonFromTypedArray(
subtreeView,
byteOffset,
jsonByteLength
);
byteOffset += jsonByteLength;
const subtreeBinary = subtreeView.subarray(
byteOffset,
byteOffset + binaryByteLength
);
return {
json: subtreeJson,
binary: subtreeBinary,
};
}
/**
* A buffer header is the JSON header from the subtree JSON chunk plus
* a couple extra boolean flags for easy reference.
*
* Buffers are assumed inactive until explicitly marked active. This is used
* to avoid fetching unneeded buffers.
*
* @typedef {object} BufferHeader
* @property {boolean} isExternal True if this is an external buffer
* @property {boolean} isActive Whether this buffer is currently used.
* @property {string} [uri] The URI of the buffer (external buffers only)
* @property {number} byteLength The byte length of the buffer, including any padding contained within.
* @private
*/
/**
* Iterate over the list of buffers from the subtree JSON and add the
* isExternal and isActive fields for easier parsing later. This modifies
* the objects in place.
*
* @param {Object[]} [bufferHeaders=[]] The JSON from subtreeJson.buffers.
* @returns {BufferHeader[]} The same array of headers with additional fields.
* @private
*/
function preprocessBuffers(bufferHeaders) {
bufferHeaders = defined(bufferHeaders) ? bufferHeaders : [];
for (let i = 0; i < bufferHeaders.length; i++) {
const bufferHeader = bufferHeaders[i];
bufferHeader.isExternal = defined(bufferHeader.uri);
bufferHeader.isActive = false;
}
return bufferHeaders;
}
/**
* A buffer header is the JSON header from the subtree JSON chunk plus
* the isActive flag and a reference to the header for the underlying buffer
*
* @typedef {object} BufferViewHeader
* @property {BufferHeader} bufferHeader A reference to the header for the underlying buffer
* @property {boolean} isActive Whether this bufferView is currently used.
* @property {number} buffer The index of the underlying buffer.
* @property {number} byteOffset The start byte of the bufferView within the buffer.
* @property {number} byteLength The length of the bufferView. No padding is included in this length.
* @private
*/
/**
* Iterate the list of buffer views from the subtree JSON and add the
* isActive flag. Also save a reference to the bufferHeader
*
* @param {Object[]} [bufferViewHeaders=[]] The JSON from subtree.bufferViews
* @param {BufferHeader[]} bufferHeaders The preprocessed buffer headers
* @returns {BufferViewHeader[]} The same array of bufferView headers with additional fields
* @private
*/
function preprocessBufferViews(bufferViewHeaders, bufferHeaders) {
bufferViewHeaders = defined(bufferViewHeaders) ? bufferViewHeaders : [];
for (let i = 0; i < bufferViewHeaders.length; i++) {
const bufferViewHeader = bufferViewHeaders[i];
const bufferHeader = bufferHeaders[bufferViewHeader.buffer];
bufferViewHeader.bufferHeader = bufferHeader;
bufferViewHeader.isActive = false;
}
return bufferViewHeaders;
}
/**
* Determine which buffer views need to be loaded into memory. This includes:
*
*
*
The tile availability bitstream (if a bitstream is defined)
*
The content availability bitstream(s) (if a bitstream is defined)
*
The child subtree availability bitstream (if a bitstream is defined)
*
*
*
* This function modifies the buffer view headers' isActive flags in place.
*
*
* @param {Object[]} subtreeJson The JSON chunk from the subtree
* @param {BufferViewHeader[]} bufferViewHeaders The preprocessed buffer view headers
* @private
*/
function markActiveBufferViews(subtreeJson, bufferViewHeaders) {
let header;
const tileAvailabilityHeader = subtreeJson.tileAvailability;
// Check for bitstream first, which is part of the current schema.
// bufferView is the name of the bitstream from an older schema.
if (defined(tileAvailabilityHeader.bitstream)) {
header = bufferViewHeaders[tileAvailabilityHeader.bitstream];
} else if (defined(tileAvailabilityHeader.bufferView)) {
header = bufferViewHeaders[tileAvailabilityHeader.bufferView];
}
if (defined(header)) {
header.isActive = true;
header.bufferHeader.isActive = true;
}
const contentAvailabilityHeaders = subtreeJson.contentAvailabilityHeaders;
for (let i = 0; i < contentAvailabilityHeaders.length; i++) {
header = undefined;
if (defined(contentAvailabilityHeaders[i].bitstream)) {
header = bufferViewHeaders[contentAvailabilityHeaders[i].bitstream];
} else if (defined(contentAvailabilityHeaders[i].bufferView)) {
header = bufferViewHeaders[contentAvailabilityHeaders[i].bufferView];
}
if (defined(header)) {
header.isActive = true;
header.bufferHeader.isActive = true;
}
}
header = undefined;
const childSubtreeAvailabilityHeader = subtreeJson.childSubtreeAvailability;
if (defined(childSubtreeAvailabilityHeader.bitstream)) {
header = bufferViewHeaders[childSubtreeAvailabilityHeader.bitstream];
} else if (defined(childSubtreeAvailabilityHeader.bufferView)) {
header = bufferViewHeaders[childSubtreeAvailabilityHeader.bufferView];
}
if (defined(header)) {
header.isActive = true;
header.bufferHeader.isActive = true;
}
}
/**
* For handling metadata, look over the tile and content metadata buffers
*
* This always loads all of the metadata immediately. Future iterations may
* allow filtering this to avoid downloading unneeded buffers.
*
*
* @param {object} propertyTableJson The property table JSON for either a tile or some content
* @param {BufferViewHeader[]} bufferViewHeaders The preprocessed buffer view headers
* @private
*/
function markActiveMetadataBufferViews(propertyTableJson, bufferViewHeaders) {
const properties = propertyTableJson.properties;
let header;
for (const key in properties) {
if (properties.hasOwnProperty(key)) {
const metadataHeader = properties[key];
// An older spec used bufferView
const valuesBufferView = defaultValue(
metadataHeader.values,
metadataHeader.bufferView
);
header = bufferViewHeaders[valuesBufferView];
header.isActive = true;
header.bufferHeader.isActive = true;
// An older spec used stringOffsetBufferView
const stringOffsetBufferView = defaultValue(
metadataHeader.stringOffsets,
metadataHeader.stringOffsetBufferView
);
if (defined(stringOffsetBufferView)) {
header = bufferViewHeaders[stringOffsetBufferView];
header.isActive = true;
header.bufferHeader.isActive = true;
}
// an older spec used arrayOffsetBufferView
const arrayOffsetBufferView = defaultValue(
metadataHeader.arrayOffsets,
metadataHeader.arrayOffsetBufferView
);
if (defined(arrayOffsetBufferView)) {
header = bufferViewHeaders[arrayOffsetBufferView];
header.isActive = true;
header.bufferHeader.isActive = true;
}
}
}
}
/**
* Go through the list of buffers and gather all the active ones into a
* a dictionary. Since external buffers are allowed, this sometimes involves
* fetching separate binary files. Consequently, this method returns a promise.
*
* The results are put into a dictionary object. The keys are indices of
* buffers, and the values are Uint8Arrays of the contents. Only buffers
* marked with the isActive flag are fetched.
*
*
* The internal buffer (the subtree's binary chunk) is also stored in this
* dictionary if it is marked active.
*
* @param {ImplicitSubtree} subtree The subtree
* @param {BufferHeader[]} bufferHeaders The preprocessed buffer headers
* @param {Uint8Array} internalBuffer The binary chunk of the subtree file
* @returns {Promise