Multiple3DTileContent.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. import defined from "../Core/defined.js";
  2. import deprecationWarning from "../Core/deprecationWarning.js";
  3. import destroyObject from "../Core/destroyObject.js";
  4. import DeveloperError from "../Core/DeveloperError.js";
  5. import Request from "../Core/Request.js";
  6. import RequestScheduler from "../Core/RequestScheduler.js";
  7. import RequestState from "../Core/RequestState.js";
  8. import RequestType from "../Core/RequestType.js";
  9. import RuntimeError from "../Core/RuntimeError.js";
  10. import Cesium3DContentGroup from "./Cesium3DContentGroup.js";
  11. import Cesium3DTileContentType from "./Cesium3DTileContentType.js";
  12. import Cesium3DTileContentFactory from "./Cesium3DTileContentFactory.js";
  13. import findContentMetadata from "./findContentMetadata.js";
  14. import findGroupMetadata from "./findGroupMetadata.js";
  15. import preprocess3DTileContent from "./preprocess3DTileContent.js";
  16. /**
  17. * 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.
  18. * <p>
  19. * Implements the {@link Cesium3DTileContent} interface.
  20. * </p>
  21. *
  22. * @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_multiple_contents|3DTILES_multiple_contents extension}
  23. *
  24. * @alias Multiple3DTileContent
  25. * @constructor
  26. *
  27. * @param {Cesium3DTileset} tileset The tileset this content belongs to
  28. * @param {Cesium3DTile} tile The content this content belongs to
  29. * @param {Resource} tilesetResource The resource that points to the tileset. This will be used to derive each inner content's resource.
  30. * @param {object} contentsJson Either the tile JSON containing the contents array (3D Tiles 1.1), or <code>3DTILES_multiple_contents</code> extension JSON
  31. *
  32. * @private
  33. * @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.
  34. */
  35. function Multiple3DTileContent(tileset, tile, tilesetResource, contentsJson) {
  36. this._tileset = tileset;
  37. this._tile = tile;
  38. this._tilesetResource = tilesetResource;
  39. this._contents = [];
  40. this._contentsCreated = false;
  41. // An older version of 3DTILES_multiple_contents used "content" instead of "contents"
  42. const contentHeaders = defined(contentsJson.contents)
  43. ? contentsJson.contents
  44. : contentsJson.content;
  45. this._innerContentHeaders = contentHeaders;
  46. this._requestsInFlight = 0;
  47. // How many times cancelPendingRequests() has been called. This is
  48. // used to help short-circuit computations after a tile was canceled.
  49. this._cancelCount = 0;
  50. const contentCount = this._innerContentHeaders.length;
  51. this._arrayFetchPromises = new Array(contentCount);
  52. this._requests = new Array(contentCount);
  53. this._ready = false;
  54. this._resolveContent = undefined;
  55. this._readyPromise = new Promise((resolve) => {
  56. this._resolveContent = resolve;
  57. });
  58. this._innerContentResources = new Array(contentCount);
  59. this._serverKeys = new Array(contentCount);
  60. for (let i = 0; i < contentCount; i++) {
  61. const contentResource = tilesetResource.getDerivedResource({
  62. url: contentHeaders[i].uri,
  63. });
  64. const serverKey = RequestScheduler.getServerKey(
  65. contentResource.getUrlComponent()
  66. );
  67. this._innerContentResources[i] = contentResource;
  68. this._serverKeys[i] = serverKey;
  69. }
  70. }
  71. Object.defineProperties(Multiple3DTileContent.prototype, {
  72. /**
  73. * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code> checks if any of the inner contents have dirty featurePropertiesDirty.
  74. * @memberof Multiple3DTileContent.prototype
  75. *
  76. * @type {boolean}
  77. *
  78. * @private
  79. */
  80. featurePropertiesDirty: {
  81. get: function () {
  82. const contents = this._contents;
  83. const length = contents.length;
  84. for (let i = 0; i < length; ++i) {
  85. if (contents[i].featurePropertiesDirty) {
  86. return true;
  87. }
  88. }
  89. return false;
  90. },
  91. set: function (value) {
  92. const contents = this._contents;
  93. const length = contents.length;
  94. for (let i = 0; i < length; ++i) {
  95. contents[i].featurePropertiesDirty = value;
  96. }
  97. },
  98. },
  99. /**
  100. * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
  101. * always returns <code>0</code>. Instead call <code>featuresLength</code> for a specific inner content.
  102. *
  103. * @memberof Multiple3DTileContent.prototype
  104. *
  105. * @type {number}
  106. * @readonly
  107. *
  108. * @private
  109. */
  110. featuresLength: {
  111. get: function () {
  112. return 0;
  113. },
  114. },
  115. /**
  116. * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
  117. * always returns <code>0</code>. Instead, call <code>pointsLength</code> for a specific inner content.
  118. *
  119. * @memberof Multiple3DTileContent.prototype
  120. *
  121. * @type {number}
  122. * @readonly
  123. *
  124. * @private
  125. */
  126. pointsLength: {
  127. get: function () {
  128. return 0;
  129. },
  130. },
  131. /**
  132. * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
  133. * always returns <code>0</code>. Instead call <code>trianglesLength</code> for a specific inner content.
  134. *
  135. * @memberof Multiple3DTileContent.prototype
  136. *
  137. * @type {number}
  138. * @readonly
  139. *
  140. * @private
  141. */
  142. trianglesLength: {
  143. get: function () {
  144. return 0;
  145. },
  146. },
  147. /**
  148. * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
  149. * always returns <code>0</code>. Instead call <code>geometryByteLength</code> for a specific inner content.
  150. *
  151. * @memberof Multiple3DTileContent.prototype
  152. *
  153. * @type {number}
  154. * @readonly
  155. *
  156. * @private
  157. */
  158. geometryByteLength: {
  159. get: function () {
  160. return 0;
  161. },
  162. },
  163. /**
  164. * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
  165. * always returns <code>0</code>. Instead call <code>texturesByteLength</code> for a specific inner content.
  166. *
  167. * @memberof Multiple3DTileContent.prototype
  168. *
  169. * @type {number}
  170. * @readonly
  171. *
  172. * @private
  173. */
  174. texturesByteLength: {
  175. get: function () {
  176. return 0;
  177. },
  178. },
  179. /**
  180. * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
  181. * always returns <code>0</code>. Instead call <code>batchTableByteLength</code> for a specific inner content.
  182. *
  183. * @memberof Multiple3DTileContent.prototype
  184. *
  185. * @type {number}
  186. * @readonly
  187. *
  188. * @private
  189. */
  190. batchTableByteLength: {
  191. get: function () {
  192. return 0;
  193. },
  194. },
  195. innerContents: {
  196. get: function () {
  197. return this._contents;
  198. },
  199. },
  200. /**
  201. * Returns true when the tile's content is ready to render; otherwise false
  202. *
  203. * @memberof Multiple3DTileContent.prototype
  204. *
  205. * @type {boolean}
  206. * @readonly
  207. * @private
  208. */
  209. ready: {
  210. get: function () {
  211. if (!this._contentsCreated) {
  212. return false;
  213. }
  214. return this._ready;
  215. },
  216. },
  217. /**
  218. * Gets the promise that will be resolved when the tile's content is ready to render.
  219. *
  220. * @memberof Multiple3DTileContent.prototype
  221. *
  222. * @type {Promise<Multiple3DTileContent>}
  223. * @readonly
  224. * @deprecated
  225. * @private
  226. */
  227. readyPromise: {
  228. get: function () {
  229. deprecationWarning(
  230. "Multiple3DTileContent.readyPromise",
  231. "Multiple3DTileContent.readyPromise was deprecated in CesiumJS 1.104. It will be removed in 1.107. Wait for Multiple3DTileContent.ready to return true instead."
  232. );
  233. return this._readyPromise;
  234. },
  235. },
  236. tileset: {
  237. get: function () {
  238. return this._tileset;
  239. },
  240. },
  241. tile: {
  242. get: function () {
  243. return this._tile;
  244. },
  245. },
  246. /**
  247. * Part of the {@link Cesium3DTileContent} interface.
  248. * Unlike other content types, <code>Multiple3DTileContent</code> does not
  249. * have a single URL, so this returns undefined.
  250. * @memberof Multiple3DTileContent.prototype
  251. *
  252. * @type {string}
  253. * @readonly
  254. * @private
  255. */
  256. url: {
  257. get: function () {
  258. return undefined;
  259. },
  260. },
  261. /**
  262. * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
  263. * always returns <code>undefined</code>. Instead call <code>metadata</code> for a specific inner content.
  264. * @memberof Multiple3DTileContent.prototype
  265. * @private
  266. */
  267. metadata: {
  268. get: function () {
  269. return undefined;
  270. },
  271. set: function () {
  272. //>>includeStart('debug', pragmas.debug);
  273. throw new DeveloperError("Multiple3DTileContent cannot have metadata");
  274. //>>includeEnd('debug');
  275. },
  276. },
  277. /**
  278. * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
  279. * always returns <code>undefined</code>. Instead call <code>batchTable</code> for a specific inner content.
  280. * @memberof Multiple3DTileContent.prototype
  281. * @private
  282. */
  283. batchTable: {
  284. get: function () {
  285. return undefined;
  286. },
  287. },
  288. /**
  289. * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
  290. * always returns <code>undefined</code>. Instead call <code>group</code> for a specific inner content.
  291. * @memberof Multiple3DTileContent.prototype
  292. * @private
  293. */
  294. group: {
  295. get: function () {
  296. return undefined;
  297. },
  298. set: function () {
  299. //>>includeStart('debug', pragmas.debug);
  300. throw new DeveloperError(
  301. "Multiple3DTileContent cannot have group metadata"
  302. );
  303. //>>includeEnd('debug');
  304. },
  305. },
  306. /**
  307. * Get an array of the inner content URLs, regardless of whether they've
  308. * been fetched or not. This is intended for use with
  309. * {@link Cesium3DTileset#debugShowUrl}.
  310. * @memberof Multiple3DTileContent.prototype
  311. *
  312. * @type {string[]}
  313. * @readonly
  314. * @private
  315. */
  316. innerContentUrls: {
  317. get: function () {
  318. return this._innerContentHeaders.map(function (contentHeader) {
  319. return contentHeader.uri;
  320. });
  321. },
  322. },
  323. });
  324. function updatePendingRequests(multipleContents, deltaRequestCount) {
  325. multipleContents._requestsInFlight += deltaRequestCount;
  326. multipleContents.tileset.statistics.numberOfPendingRequests += deltaRequestCount;
  327. }
  328. function cancelPendingRequests(multipleContents, originalContentState) {
  329. multipleContents._cancelCount++;
  330. // reset the tile's content state to try again later.
  331. multipleContents._tile._contentState = originalContentState;
  332. const statistics = multipleContents.tileset.statistics;
  333. statistics.numberOfPendingRequests -= multipleContents._requestsInFlight;
  334. statistics.numberOfAttemptedRequests += multipleContents._requestsInFlight;
  335. multipleContents._requestsInFlight = 0;
  336. // Discard the request promises.
  337. const contentCount = multipleContents._innerContentHeaders.length;
  338. multipleContents._arrayFetchPromises = new Array(contentCount);
  339. }
  340. /**
  341. * Request the inner contents of this <code>Multiple3DTileContent</code>. This must be called once a frame until
  342. * {@link Multiple3DTileContent#contentsFetchedPromise} is defined. This promise
  343. * becomes available as soon as all requests are scheduled.
  344. * <p>
  345. * This method also updates the tile statistics' pending request count if the
  346. * requests are successfully scheduled.
  347. * </p>
  348. *
  349. * @return {Promise<void>|undefined} A promise that resolves when the request completes, or undefined if there is no request needed, or the request cannot be scheduled.
  350. * @private
  351. */
  352. Multiple3DTileContent.prototype.requestInnerContents = function () {
  353. // It's possible for these promises to leak content array buffers if the
  354. // camera moves before they all are scheduled. To prevent this leak, check
  355. // if we can schedule all the requests at once. If not, no requests are
  356. // scheduled
  357. if (!canScheduleAllRequests(this._serverKeys)) {
  358. this.tileset.statistics.numberOfAttemptedRequests += this._serverKeys.length;
  359. return;
  360. }
  361. const contentHeaders = this._innerContentHeaders;
  362. updatePendingRequests(this, contentHeaders.length);
  363. const originalCancelCount = this._cancelCount;
  364. for (let i = 0; i < contentHeaders.length; i++) {
  365. // The cancel count is needed to avoid a race condition where a content
  366. // is canceled multiple times.
  367. this._arrayFetchPromises[i] = requestInnerContent(
  368. this,
  369. i,
  370. originalCancelCount,
  371. this._tile._contentState
  372. );
  373. }
  374. return createInnerContents(this);
  375. };
  376. /**
  377. * Check if all requests for inner contents can be scheduled at once. This is slower, but it avoids a potential memory leak.
  378. * @param {string[]} serverKeys The server keys for all of the inner contents
  379. * @return {boolean} True if the request scheduler has enough open slots for all inner contents
  380. * @private
  381. */
  382. function canScheduleAllRequests(serverKeys) {
  383. const requestCountsByServer = {};
  384. for (let i = 0; i < serverKeys.length; i++) {
  385. const serverKey = serverKeys[i];
  386. if (defined(requestCountsByServer[serverKey])) {
  387. requestCountsByServer[serverKey]++;
  388. } else {
  389. requestCountsByServer[serverKey] = 1;
  390. }
  391. }
  392. for (const key in requestCountsByServer) {
  393. if (
  394. requestCountsByServer.hasOwnProperty(key) &&
  395. !RequestScheduler.serverHasOpenSlots(key, requestCountsByServer[key])
  396. ) {
  397. return false;
  398. }
  399. }
  400. return RequestScheduler.heapHasOpenSlots(serverKeys.length);
  401. }
  402. function requestInnerContent(
  403. multipleContents,
  404. index,
  405. originalCancelCount,
  406. originalContentState
  407. ) {
  408. // it is important to clone here. The fetchArrayBuffer() below here uses
  409. // throttling, but other uses of the resources do not.
  410. const contentResource = multipleContents._innerContentResources[
  411. index
  412. ].clone();
  413. const tile = multipleContents.tile;
  414. // Always create a new request. If the tile gets canceled, this
  415. // avoids getting stuck in the canceled state.
  416. const priorityFunction = function () {
  417. return tile._priority;
  418. };
  419. const serverKey = multipleContents._serverKeys[index];
  420. const request = new Request({
  421. throttle: true,
  422. throttleByServer: true,
  423. type: RequestType.TILES3D,
  424. priorityFunction: priorityFunction,
  425. serverKey: serverKey,
  426. });
  427. contentResource.request = request;
  428. multipleContents._requests[index] = request;
  429. const promise = contentResource.fetchArrayBuffer();
  430. if (!defined(promise)) {
  431. return;
  432. }
  433. return promise
  434. .then(function (arrayBuffer) {
  435. // Pending requests have already been canceled.
  436. if (originalCancelCount < multipleContents._cancelCount) {
  437. return;
  438. }
  439. if (
  440. contentResource.request.cancelled ||
  441. contentResource.request.state === RequestState.CANCELLED
  442. ) {
  443. cancelPendingRequests(multipleContents, originalContentState);
  444. return;
  445. }
  446. updatePendingRequests(multipleContents, -1);
  447. return arrayBuffer;
  448. })
  449. .catch(function (error) {
  450. // Pending requests have already been canceled.
  451. if (originalCancelCount < multipleContents._cancelCount) {
  452. return;
  453. }
  454. if (
  455. contentResource.request.cancelled ||
  456. contentResource.request.state === RequestState.CANCELLED
  457. ) {
  458. cancelPendingRequests(multipleContents, originalContentState);
  459. return;
  460. }
  461. updatePendingRequests(multipleContents, -1);
  462. handleInnerContentFailed(multipleContents, index, error);
  463. });
  464. }
  465. async function createInnerContents(multipleContents) {
  466. const originalCancelCount = multipleContents._cancelCount;
  467. const arrayBuffers = await Promise.all(multipleContents._arrayFetchPromises);
  468. // Request have been cancelled
  469. if (originalCancelCount < multipleContents._cancelCount) {
  470. return;
  471. }
  472. const promises = arrayBuffers.map((arrayBuffer, i) =>
  473. createInnerContent(multipleContents, arrayBuffer, i)
  474. );
  475. // 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
  476. // contents
  477. const contents = await Promise.all(promises);
  478. multipleContents._contentsCreated = true;
  479. multipleContents._contents = contents.filter(defined);
  480. return contents;
  481. }
  482. async function createInnerContent(multipleContents, arrayBuffer, index) {
  483. if (!defined(arrayBuffer)) {
  484. // Content was not fetched. The error was handled in
  485. // the fetch promise. Return undefined to indicate partial failure.
  486. return;
  487. }
  488. try {
  489. const preprocessed = preprocess3DTileContent(arrayBuffer);
  490. if (preprocessed.contentType === Cesium3DTileContentType.EXTERNAL_TILESET) {
  491. throw new RuntimeError(
  492. "External tilesets are disallowed inside multiple contents"
  493. );
  494. }
  495. multipleContents._disableSkipLevelOfDetail =
  496. multipleContents._disableSkipLevelOfDetail ||
  497. preprocessed.contentType === Cesium3DTileContentType.GEOMETRY ||
  498. preprocessed.contentType === Cesium3DTileContentType.VECTOR;
  499. const tileset = multipleContents._tileset;
  500. const resource = multipleContents._innerContentResources[index];
  501. const tile = multipleContents._tile;
  502. let content;
  503. const contentFactory = Cesium3DTileContentFactory[preprocessed.contentType];
  504. if (defined(preprocessed.binaryPayload)) {
  505. content = await Promise.resolve(
  506. contentFactory(
  507. tileset,
  508. tile,
  509. resource,
  510. preprocessed.binaryPayload.buffer,
  511. 0
  512. )
  513. );
  514. } else {
  515. // JSON formats
  516. content = await Promise.resolve(
  517. contentFactory(tileset, tile, resource, preprocessed.jsonPayload)
  518. );
  519. }
  520. const contentHeader = multipleContents._innerContentHeaders[index];
  521. if (tile.hasImplicitContentMetadata) {
  522. const subtree = tile.implicitSubtree;
  523. const coordinates = tile.implicitCoordinates;
  524. content.metadata = subtree.getContentMetadataView(coordinates, index);
  525. } else if (!tile.hasImplicitContent) {
  526. content.metadata = findContentMetadata(tileset, contentHeader);
  527. }
  528. const groupMetadata = findGroupMetadata(tileset, contentHeader);
  529. if (defined(groupMetadata)) {
  530. content.group = new Cesium3DContentGroup({
  531. metadata: groupMetadata,
  532. });
  533. }
  534. return content;
  535. } catch (error) {
  536. handleInnerContentFailed(multipleContents, index, error);
  537. }
  538. }
  539. function handleInnerContentFailed(multipleContents, index, error) {
  540. const tileset = multipleContents._tileset;
  541. const url = multipleContents._innerContentResources[index].url;
  542. const message = defined(error.message) ? error.message : error.toString();
  543. if (tileset.tileFailed.numberOfListeners > 0) {
  544. tileset.tileFailed.raiseEvent({
  545. url: url,
  546. message: message,
  547. });
  548. } else {
  549. console.log(`A content failed to load: ${url}`);
  550. console.log(`Error: ${message}`);
  551. }
  552. }
  553. /**
  554. * Cancel all requests for inner contents. This is called by the tile
  555. * when a tile goes out of view.
  556. *
  557. * @private
  558. */
  559. Multiple3DTileContent.prototype.cancelRequests = function () {
  560. for (let i = 0; i < this._requests.length; i++) {
  561. const request = this._requests[i];
  562. if (defined(request)) {
  563. request.cancel();
  564. }
  565. }
  566. };
  567. /**
  568. * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
  569. * always returns <code>false</code>. Instead call <code>hasProperty</code> for a specific inner content
  570. * @private
  571. */
  572. Multiple3DTileContent.prototype.hasProperty = function (batchId, name) {
  573. return false;
  574. };
  575. /**
  576. * Part of the {@link Cesium3DTileContent} interface. <code>Multiple3DTileContent</code>
  577. * always returns <code>undefined</code>. Instead call <code>getFeature</code> for a specific inner content
  578. * @private
  579. */
  580. Multiple3DTileContent.prototype.getFeature = function (batchId) {
  581. return undefined;
  582. };
  583. Multiple3DTileContent.prototype.applyDebugSettings = function (enabled, color) {
  584. const contents = this._contents;
  585. const length = contents.length;
  586. for (let i = 0; i < length; ++i) {
  587. contents[i].applyDebugSettings(enabled, color);
  588. }
  589. };
  590. Multiple3DTileContent.prototype.applyStyle = function (style) {
  591. const contents = this._contents;
  592. const length = contents.length;
  593. for (let i = 0; i < length; ++i) {
  594. contents[i].applyStyle(style);
  595. }
  596. };
  597. Multiple3DTileContent.prototype.update = function (tileset, frameState) {
  598. const contents = this._contents;
  599. const length = contents.length;
  600. let ready = true;
  601. for (let i = 0; i < length; ++i) {
  602. contents[i].update(tileset, frameState);
  603. ready = ready && contents[i].ready;
  604. }
  605. if (!this._ready && ready) {
  606. this._ready = true;
  607. this._resolveContent(this);
  608. }
  609. };
  610. Multiple3DTileContent.prototype.isDestroyed = function () {
  611. return false;
  612. };
  613. Multiple3DTileContent.prototype.destroy = function () {
  614. const contents = this._contents;
  615. const length = contents.length;
  616. for (let i = 0; i < length; ++i) {
  617. contents[i].destroy();
  618. }
  619. return destroyObject(this);
  620. };
  621. export default Multiple3DTileContent;