import defined from "../Core/defined.js"; import deprecationWarning from "../Core/deprecationWarning.js"; import destroyObject from "../Core/destroyObject.js"; import DeveloperError from "../Core/DeveloperError.js"; import Request from "../Core/Request.js"; import RequestScheduler from "../Core/RequestScheduler.js"; import RequestState from "../Core/RequestState.js"; import RequestType from "../Core/RequestType.js"; import RuntimeError from "../Core/RuntimeError.js"; import Cesium3DContentGroup from "./Cesium3DContentGroup.js"; import Cesium3DTileContentType from "./Cesium3DTileContentType.js"; import Cesium3DTileContentFactory from "./Cesium3DTileContentFactory.js"; import findContentMetadata from "./findContentMetadata.js"; import findGroupMetadata from "./findGroupMetadata.js"; import preprocess3DTileContent from "./preprocess3DTileContent.js"; /** * A collection of contents for tiles that have multiple contents, either via the tile JSON (3D Tiles 1.1) or the 3DTILES_multiple_contents extension. *

* Implements the {@link Cesium3DTileContent} interface. *

* * @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_multiple_contents|3DTILES_multiple_contents extension} * * @alias Multiple3DTileContent * @constructor * * @param {Cesium3DTileset} tileset The tileset this content belongs to * @param {Cesium3DTile} tile The content this content belongs to * @param {Resource} tilesetResource The resource that points to the tileset. This will be used to derive each inner content's resource. * @param {object} contentsJson Either the tile JSON containing the contents array (3D Tiles 1.1), or 3DTILES_multiple_contents extension JSON * * @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 Multiple3DTileContent(tileset, tile, tilesetResource, contentsJson) { this._tileset = tileset; this._tile = tile; this._tilesetResource = tilesetResource; this._contents = []; this._contentsCreated = false; // An older version of 3DTILES_multiple_contents used "content" instead of "contents" const contentHeaders = defined(contentsJson.contents) ? contentsJson.contents : contentsJson.content; this._innerContentHeaders = contentHeaders; this._requestsInFlight = 0; // How many times cancelPendingRequests() has been called. This is // used to help short-circuit computations after a tile was canceled. this._cancelCount = 0; const contentCount = this._innerContentHeaders.length; this._arrayFetchPromises = new Array(contentCount); this._requests = new Array(contentCount); this._ready = false; this._resolveContent = undefined; this._readyPromise = new Promise((resolve) => { this._resolveContent = resolve; }); this._innerContentResources = new Array(contentCount); this._serverKeys = new Array(contentCount); for (let i = 0; i < contentCount; i++) { const contentResource = tilesetResource.getDerivedResource({ url: contentHeaders[i].uri, }); const serverKey = RequestScheduler.getServerKey( contentResource.getUrlComponent() ); this._innerContentResources[i] = contentResource; this._serverKeys[i] = serverKey; } } Object.defineProperties(Multiple3DTileContent.prototype, { /** * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent checks if any of the inner contents have dirty featurePropertiesDirty. * @memberof Multiple3DTileContent.prototype * * @type {boolean} * * @private */ featurePropertiesDirty: { get: function () { const contents = this._contents; const length = contents.length; for (let i = 0; i < length; ++i) { if (contents[i].featurePropertiesDirty) { return true; } } return false; }, set: function (value) { const contents = this._contents; const length = contents.length; for (let i = 0; i < length; ++i) { contents[i].featurePropertiesDirty = value; } }, }, /** * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent * always returns 0. Instead call featuresLength for a specific inner content. * * @memberof Multiple3DTileContent.prototype * * @type {number} * @readonly * * @private */ featuresLength: { get: function () { return 0; }, }, /** * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent * always returns 0. Instead, call pointsLength for a specific inner content. * * @memberof Multiple3DTileContent.prototype * * @type {number} * @readonly * * @private */ pointsLength: { get: function () { return 0; }, }, /** * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent * always returns 0. Instead call trianglesLength for a specific inner content. * * @memberof Multiple3DTileContent.prototype * * @type {number} * @readonly * * @private */ trianglesLength: { get: function () { return 0; }, }, /** * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent * always returns 0. Instead call geometryByteLength for a specific inner content. * * @memberof Multiple3DTileContent.prototype * * @type {number} * @readonly * * @private */ geometryByteLength: { get: function () { return 0; }, }, /** * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent * always returns 0. Instead call texturesByteLength for a specific inner content. * * @memberof Multiple3DTileContent.prototype * * @type {number} * @readonly * * @private */ texturesByteLength: { get: function () { return 0; }, }, /** * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent * always returns 0. Instead call batchTableByteLength for a specific inner content. * * @memberof Multiple3DTileContent.prototype * * @type {number} * @readonly * * @private */ batchTableByteLength: { get: function () { return 0; }, }, innerContents: { get: function () { return this._contents; }, }, /** * Returns true when the tile's content is ready to render; otherwise false * * @memberof Multiple3DTileContent.prototype * * @type {boolean} * @readonly * @private */ ready: { get: function () { if (!this._contentsCreated) { return false; } return this._ready; }, }, /** * Gets the promise that will be resolved when the tile's content is ready to render. * * @memberof Multiple3DTileContent.prototype * * @type {Promise} * @readonly * @deprecated * @private */ readyPromise: { get: function () { deprecationWarning( "Multiple3DTileContent.readyPromise", "Multiple3DTileContent.readyPromise was deprecated in CesiumJS 1.104. It will be removed in 1.107. Wait for Multiple3DTileContent.ready to return true instead." ); return this._readyPromise; }, }, tileset: { get: function () { return this._tileset; }, }, tile: { get: function () { return this._tile; }, }, /** * Part of the {@link Cesium3DTileContent} interface. * Unlike other content types, Multiple3DTileContent does not * have a single URL, so this returns undefined. * @memberof Multiple3DTileContent.prototype * * @type {string} * @readonly * @private */ url: { get: function () { return undefined; }, }, /** * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent * always returns undefined. Instead call metadata for a specific inner content. * @memberof Multiple3DTileContent.prototype * @private */ metadata: { get: function () { return undefined; }, set: function () { //>>includeStart('debug', pragmas.debug); throw new DeveloperError("Multiple3DTileContent cannot have metadata"); //>>includeEnd('debug'); }, }, /** * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent * always returns undefined. Instead call batchTable for a specific inner content. * @memberof Multiple3DTileContent.prototype * @private */ batchTable: { get: function () { return undefined; }, }, /** * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent * always returns undefined. Instead call group for a specific inner content. * @memberof Multiple3DTileContent.prototype * @private */ group: { get: function () { return undefined; }, set: function () { //>>includeStart('debug', pragmas.debug); throw new DeveloperError( "Multiple3DTileContent cannot have group metadata" ); //>>includeEnd('debug'); }, }, /** * Get an array of the inner content URLs, regardless of whether they've * been fetched or not. This is intended for use with * {@link Cesium3DTileset#debugShowUrl}. * @memberof Multiple3DTileContent.prototype * * @type {string[]} * @readonly * @private */ innerContentUrls: { get: function () { return this._innerContentHeaders.map(function (contentHeader) { return contentHeader.uri; }); }, }, }); function updatePendingRequests(multipleContents, deltaRequestCount) { multipleContents._requestsInFlight += deltaRequestCount; multipleContents.tileset.statistics.numberOfPendingRequests += deltaRequestCount; } function cancelPendingRequests(multipleContents, originalContentState) { multipleContents._cancelCount++; // reset the tile's content state to try again later. multipleContents._tile._contentState = originalContentState; const statistics = multipleContents.tileset.statistics; statistics.numberOfPendingRequests -= multipleContents._requestsInFlight; statistics.numberOfAttemptedRequests += multipleContents._requestsInFlight; multipleContents._requestsInFlight = 0; // Discard the request promises. const contentCount = multipleContents._innerContentHeaders.length; multipleContents._arrayFetchPromises = new Array(contentCount); } /** * Request the inner contents of this Multiple3DTileContent. This must be called once a frame until * {@link Multiple3DTileContent#contentsFetchedPromise} is defined. This promise * becomes available as soon as all requests are scheduled. *

* This method also updates the tile statistics' pending request count if the * requests are successfully scheduled. *

* * @return {Promise|undefined} A promise that resolves when the request completes, or undefined if there is no request needed, or the request cannot be scheduled. * @private */ Multiple3DTileContent.prototype.requestInnerContents = function () { // It's possible for these promises to leak content array buffers if the // camera moves before they all are scheduled. To prevent this leak, check // if we can schedule all the requests at once. If not, no requests are // scheduled if (!canScheduleAllRequests(this._serverKeys)) { this.tileset.statistics.numberOfAttemptedRequests += this._serverKeys.length; return; } const contentHeaders = this._innerContentHeaders; updatePendingRequests(this, contentHeaders.length); const originalCancelCount = this._cancelCount; for (let i = 0; i < contentHeaders.length; i++) { // The cancel count is needed to avoid a race condition where a content // is canceled multiple times. this._arrayFetchPromises[i] = requestInnerContent( this, i, originalCancelCount, this._tile._contentState ); } return createInnerContents(this); }; /** * Check if all requests for inner contents can be scheduled at once. This is slower, but it avoids a potential memory leak. * @param {string[]} serverKeys The server keys for all of the inner contents * @return {boolean} True if the request scheduler has enough open slots for all inner contents * @private */ function canScheduleAllRequests(serverKeys) { const requestCountsByServer = {}; for (let i = 0; i < serverKeys.length; i++) { const serverKey = serverKeys[i]; if (defined(requestCountsByServer[serverKey])) { requestCountsByServer[serverKey]++; } else { requestCountsByServer[serverKey] = 1; } } for (const key in requestCountsByServer) { if ( requestCountsByServer.hasOwnProperty(key) && !RequestScheduler.serverHasOpenSlots(key, requestCountsByServer[key]) ) { return false; } } return RequestScheduler.heapHasOpenSlots(serverKeys.length); } function requestInnerContent( multipleContents, index, originalCancelCount, originalContentState ) { // it is important to clone here. The fetchArrayBuffer() below here uses // throttling, but other uses of the resources do not. const contentResource = multipleContents._innerContentResources[ index ].clone(); const tile = multipleContents.tile; // Always create a new request. If the tile gets canceled, this // avoids getting stuck in the canceled state. const priorityFunction = function () { return tile._priority; }; const serverKey = multipleContents._serverKeys[index]; const request = new Request({ throttle: true, throttleByServer: true, type: RequestType.TILES3D, priorityFunction: priorityFunction, serverKey: serverKey, }); contentResource.request = request; multipleContents._requests[index] = request; const promise = contentResource.fetchArrayBuffer(); if (!defined(promise)) { return; } return promise .then(function (arrayBuffer) { // Pending requests have already been canceled. if (originalCancelCount < multipleContents._cancelCount) { return; } if ( contentResource.request.cancelled || contentResource.request.state === RequestState.CANCELLED ) { cancelPendingRequests(multipleContents, originalContentState); return; } updatePendingRequests(multipleContents, -1); return arrayBuffer; }) .catch(function (error) { // Pending requests have already been canceled. if (originalCancelCount < multipleContents._cancelCount) { return; } if ( contentResource.request.cancelled || contentResource.request.state === RequestState.CANCELLED ) { cancelPendingRequests(multipleContents, originalContentState); return; } updatePendingRequests(multipleContents, -1); handleInnerContentFailed(multipleContents, index, error); }); } async function createInnerContents(multipleContents) { const originalCancelCount = multipleContents._cancelCount; const arrayBuffers = await Promise.all(multipleContents._arrayFetchPromises); // Request have been cancelled if (originalCancelCount < multipleContents._cancelCount) { return; } const promises = arrayBuffers.map((arrayBuffer, i) => createInnerContent(multipleContents, arrayBuffer, i) ); // Even if we had a partial success (in which case the inner promise will be handled, but the content will not be returned), mark that we finished creating // contents const contents = await Promise.all(promises); multipleContents._contentsCreated = true; multipleContents._contents = contents.filter(defined); return contents; } async function createInnerContent(multipleContents, arrayBuffer, index) { if (!defined(arrayBuffer)) { // Content was not fetched. The error was handled in // the fetch promise. Return undefined to indicate partial failure. return; } try { const preprocessed = preprocess3DTileContent(arrayBuffer); if (preprocessed.contentType === Cesium3DTileContentType.EXTERNAL_TILESET) { throw new RuntimeError( "External tilesets are disallowed inside multiple contents" ); } multipleContents._disableSkipLevelOfDetail = multipleContents._disableSkipLevelOfDetail || preprocessed.contentType === Cesium3DTileContentType.GEOMETRY || preprocessed.contentType === Cesium3DTileContentType.VECTOR; const tileset = multipleContents._tileset; const resource = multipleContents._innerContentResources[index]; const tile = multipleContents._tile; let content; const contentFactory = Cesium3DTileContentFactory[preprocessed.contentType]; if (defined(preprocessed.binaryPayload)) { content = await Promise.resolve( contentFactory( tileset, tile, resource, preprocessed.binaryPayload.buffer, 0 ) ); } else { // JSON formats content = await Promise.resolve( contentFactory(tileset, tile, resource, preprocessed.jsonPayload) ); } const contentHeader = multipleContents._innerContentHeaders[index]; if (tile.hasImplicitContentMetadata) { const subtree = tile.implicitSubtree; const coordinates = tile.implicitCoordinates; content.metadata = subtree.getContentMetadataView(coordinates, index); } else if (!tile.hasImplicitContent) { content.metadata = findContentMetadata(tileset, contentHeader); } const groupMetadata = findGroupMetadata(tileset, contentHeader); if (defined(groupMetadata)) { content.group = new Cesium3DContentGroup({ metadata: groupMetadata, }); } return content; } catch (error) { handleInnerContentFailed(multipleContents, index, error); } } function handleInnerContentFailed(multipleContents, index, error) { const tileset = multipleContents._tileset; const url = multipleContents._innerContentResources[index].url; const message = defined(error.message) ? error.message : error.toString(); if (tileset.tileFailed.numberOfListeners > 0) { tileset.tileFailed.raiseEvent({ url: url, message: message, }); } else { console.log(`A content failed to load: ${url}`); console.log(`Error: ${message}`); } } /** * Cancel all requests for inner contents. This is called by the tile * when a tile goes out of view. * * @private */ Multiple3DTileContent.prototype.cancelRequests = function () { for (let i = 0; i < this._requests.length; i++) { const request = this._requests[i]; if (defined(request)) { request.cancel(); } } }; /** * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent * always returns false. Instead call hasProperty for a specific inner content * @private */ Multiple3DTileContent.prototype.hasProperty = function (batchId, name) { return false; }; /** * Part of the {@link Cesium3DTileContent} interface. Multiple3DTileContent * always returns undefined. Instead call getFeature for a specific inner content * @private */ Multiple3DTileContent.prototype.getFeature = function (batchId) { return undefined; }; Multiple3DTileContent.prototype.applyDebugSettings = function (enabled, color) { const contents = this._contents; const length = contents.length; for (let i = 0; i < length; ++i) { contents[i].applyDebugSettings(enabled, color); } }; Multiple3DTileContent.prototype.applyStyle = function (style) { const contents = this._contents; const length = contents.length; for (let i = 0; i < length; ++i) { contents[i].applyStyle(style); } }; Multiple3DTileContent.prototype.update = function (tileset, frameState) { const contents = this._contents; const length = contents.length; let ready = true; for (let i = 0; i < length; ++i) { contents[i].update(tileset, frameState); ready = ready && contents[i].ready; } if (!this._ready && ready) { this._ready = true; this._resolveContent(this); } }; Multiple3DTileContent.prototype.isDestroyed = function () { return false; }; Multiple3DTileContent.prototype.destroy = function () { const contents = this._contents; const length = contents.length; for (let i = 0; i < length; ++i) { contents[i].destroy(); } return destroyObject(this); }; export default Multiple3DTileContent;