123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660 |
- import defer from "../Core/defer.js";
- import defined from "../Core/defined.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 <code>3DTILES_multiple_contents</code> extension.
- * <p>
- * Implements the {@link Cesium3DTileContent} interface.
- * </p>
- *
- * @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 <code>3DTILES_multiple_contents</code> 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.
- */
- export default function Multiple3DTileContent(
- tileset,
- tile,
- tilesetResource,
- contentsJson
- ) {
- this._tileset = tileset;
- this._tile = tile;
- this._tilesetResource = tilesetResource;
- this._contents = [];
- // 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._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;
- }
- // undefined until the first time requests are scheduled
- this._contentsFetchedPromise = undefined;
- this._readyPromise = defer();
- }
- Object.defineProperties(Multiple3DTileContent.prototype, {
- /**
- * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code> 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. <code>Multiple3DTileContent</code>
- * always returns <code>0</code>. Instead call <code>featuresLength</code> for a specific inner content.
- * @memberof Multiple3DTileContent.prototype
- * @private
- */
- featuresLength: {
- get: function () {
- return 0;
- },
- },
- /**
- * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
- * always returns <code>0</code>. Instead, call <code>pointsLength</code> for a specific inner content.
- * @memberof Multiple3DTileContent.prototype
- * @private
- */
- pointsLength: {
- get: function () {
- return 0;
- },
- },
- /**
- * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
- * always returns <code>0</code>. Instead call <code>trianglesLength</code> for a specific inner content.
- * @memberof Multiple3DTileContent.prototype
- * @private
- */
- trianglesLength: {
- get: function () {
- return 0;
- },
- },
- /**
- * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
- * always returns <code>0</code>. Instead call <code>geometryByteLength</code> for a specific inner content.
- * @memberof Multiple3DTileContent.prototype
- * @private
- */
- geometryByteLength: {
- get: function () {
- return 0;
- },
- },
- /**
- * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
- * always returns <code>0</code>. Instead call <code>texturesByteLength</code> for a specific inner content.
- * @memberof Multiple3DTileContent.prototype
- * @private
- */
- texturesByteLength: {
- get: function () {
- return 0;
- },
- },
- /**
- * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
- * always returns <code>0</code>. Instead call <code>batchTableByteLength</code> for a specific inner content.
- * @memberof Multiple3DTileContent.prototype
- * @private
- */
- batchTableByteLength: {
- get: function () {
- return 0;
- },
- },
- innerContents: {
- get: function () {
- return this._contents;
- },
- },
- readyPromise: {
- get: function () {
- return this._readyPromise.promise;
- },
- },
- tileset: {
- get: function () {
- return this._tileset;
- },
- },
- tile: {
- get: function () {
- return this._tile;
- },
- },
- /**
- * Part of the {@link Cesium3DTileContent} interface.
- * Unlike other content types, <code>Multiple3DTileContent</code> 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. <code>Multiple3DTileContent</code>
- * always returns <code>undefined</code>. Instead call <code>metadata</code> 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. <code>Multiple3DTileContent</code>
- * always returns <code>undefined</code>. Instead call <code>batchTable</code> for a specific inner content.
- * @memberof Multiple3DTileContent.prototype
- * @private
- */
- batchTable: {
- get: function () {
- return undefined;
- },
- },
- /**
- * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
- * always returns <code>undefined</code>. Instead call <code>group</code> 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;
- });
- },
- },
- /**
- * A promise that resolves when all of the inner contents have been fetched.
- * This promise is undefined until the first frame where all array buffer
- * requests have been scheduled.
- * @memberof Multiple3DTileContent.prototype
- *
- * @type {Promise}
- * @private
- */
- contentsFetchedPromise: {
- get: function () {
- if (defined(this._contentsFetchedPromise)) {
- return this._contentsFetchedPromise.promise;
- }
- return undefined;
- },
- },
- });
- 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;
- multipleContents.tileset.statistics.numberOfPendingRequests -=
- 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 <code>Multiple3DTileContent</code>. This must be called once a frame until
- * {@link Multiple3DTileContent#contentsFetchedPromise} is defined. This promise
- * becomes available as soon as all requests are scheduled.
- * <p>
- * This method also updates the tile statistics' pending request count if the
- * requests are successfully scheduled.
- * </p>
- *
- * @return {Number} The number of attempted requests that were unable to 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)) {
- return this._serverKeys.length;
- }
- const contentHeaders = this._innerContentHeaders;
- updatePendingRequests(this, contentHeaders.length);
- 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,
- this._cancelCount,
- this._tile._contentState
- );
- }
- // set up the deferred promise the first time requestInnerContent()
- // is called.
- if (!defined(this._contentsFetchedPromise)) {
- this._contentsFetchedPromise = defer();
- }
- createInnerContents(this);
- return 0;
- };
- /**
- * 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;
- return contentResource
- .fetchArrayBuffer()
- .then(function (arrayBuffer) {
- // Short circuit if another inner content was canceled.
- if (originalCancelCount < multipleContents._cancelCount) {
- return undefined;
- }
- updatePendingRequests(multipleContents, -1);
- return arrayBuffer;
- })
- .catch(function (error) {
- // Short circuit if another inner content was canceled.
- if (originalCancelCount < multipleContents._cancelCount) {
- return undefined;
- }
- if (contentResource.request.state === RequestState.CANCELLED) {
- cancelPendingRequests(multipleContents, originalContentState);
- return undefined;
- }
- updatePendingRequests(multipleContents, -1);
- handleInnerContentFailed(multipleContents, index, error);
- return undefined;
- });
- }
- function createInnerContents(multipleContents) {
- const originalCancelCount = multipleContents._cancelCount;
- Promise.all(multipleContents._arrayFetchPromises)
- .then(function (arrayBuffers) {
- if (originalCancelCount < multipleContents._cancelCount) {
- return undefined;
- }
- return arrayBuffers.map(function (arrayBuffer, i) {
- if (!defined(arrayBuffer)) {
- // Content was not fetched. The error was handled in
- // the fetch promise
- return undefined;
- }
- try {
- return createInnerContent(multipleContents, arrayBuffer, i);
- } catch (error) {
- handleInnerContentFailed(multipleContents, i, error);
- return undefined;
- }
- });
- })
- .then(function (contents) {
- if (!defined(contents)) {
- // request was canceled. resolve the promise (Cesium3DTile will
- // detect that the the content was canceled), then discard the promise
- // so a new one can be created
- if (defined(multipleContents._contentsFetchedPromise)) {
- multipleContents._contentsFetchedPromise.resolve();
- multipleContents._contentsFetchedPromise = undefined;
- }
- return;
- }
- multipleContents._contents = contents.filter(defined);
- awaitReadyPromises(multipleContents);
- if (defined(multipleContents._contentsFetchedPromise)) {
- multipleContents._contentsFetchedPromise.resolve();
- }
- })
- .catch(function (error) {
- if (defined(multipleContents._contentsFetchedPromise)) {
- multipleContents._contentsFetchedPromise.reject(error);
- }
- });
- }
- function createInnerContent(multipleContents, arrayBuffer, index) {
- 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 = contentFactory(
- tileset,
- tile,
- resource,
- preprocessed.binaryPayload.buffer,
- 0
- );
- } else {
- // JSON formats
- content = 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;
- }
- function awaitReadyPromises(multipleContents) {
- const readyPromises = multipleContents._contents.map(function (content) {
- return content.readyPromise;
- });
- Promise.all(readyPromises)
- .then(function () {
- multipleContents._readyPromise.resolve(multipleContents);
- })
- .catch(function (error) {
- multipleContents._readyPromise.reject(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. <code>Multiple3DTileContent</code>
- * always returns <code>false</code>. Instead call <code>hasProperty</code> for a specific inner content
- * @private
- */
- Multiple3DTileContent.prototype.hasProperty = function (batchId, name) {
- return false;
- };
- /**
- * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
- * always returns <code>undefined</code>. Instead call <code>getFeature</code> 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;
- for (let i = 0; i < length; ++i) {
- contents[i].update(tileset, frameState);
- }
- };
- 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);
- };
|