Multiple3DTileContent.js 20 KB

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