Cesium3DTilesVoxelProvider.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. import Cartesian3 from "../Core/Cartesian3.js";
  2. import Check from "../Core/Check.js";
  3. import defaultValue from "../Core/defaultValue.js";
  4. import defined from "../Core/defined.js";
  5. import deprecationWarning from "../Core/deprecationWarning.js";
  6. import Ellipsoid from "../Core/Ellipsoid.js";
  7. import Matrix4 from "../Core/Matrix4.js";
  8. import OrientedBoundingBox from "../Core/OrientedBoundingBox.js";
  9. import Resource from "../Core/Resource.js";
  10. import RuntimeError from "../Core/RuntimeError.js";
  11. import Cesium3DTilesetMetadata from "./Cesium3DTilesetMetadata.js";
  12. import hasExtension from "./hasExtension.js";
  13. import ImplicitSubtree from "./ImplicitSubtree.js";
  14. import ImplicitSubtreeCache from "./ImplicitSubtreeCache.js";
  15. import ImplicitTileCoordinates from "./ImplicitTileCoordinates.js";
  16. import ImplicitTileset from "./ImplicitTileset.js";
  17. import MetadataSemantic from "./MetadataSemantic.js";
  18. import MetadataType from "./MetadataType.js";
  19. import preprocess3DTileContent from "./preprocess3DTileContent.js";
  20. import ResourceCache from "./ResourceCache.js";
  21. import VoxelBoxShape from "./VoxelBoxShape.js";
  22. import VoxelContent from "./VoxelContent.js";
  23. import VoxelCylinderShape from "./VoxelCylinderShape.js";
  24. import VoxelShapeType from "./VoxelShapeType.js";
  25. /**
  26. * A {@link VoxelProvider} that fetches voxel data from a 3D Tiles tileset.
  27. * <p>
  28. * Implements the {@link VoxelProvider} interface.
  29. * </p>
  30. * <div class="notice">
  31. * This object is normally not instantiated directly, use {@link Cesium3DTilesVoxelProvider.fromUrl}.
  32. * </div>
  33. *
  34. * @alias Cesium3DTilesVoxelProvider
  35. * @constructor
  36. * @augments VoxelProvider
  37. *
  38. * @param {object} options Object with the following properties:
  39. * @param {Resource|string|Promise<Resource>|Promise<string>} [options.url] The URL to a tileset JSON file. Deprecated.
  40. *
  41. * @see Cesium3DTilesVoxelProvider.fromUrl
  42. * @see VoxelProvider
  43. * @see VoxelPrimitive
  44. * @see VoxelShapeType
  45. *
  46. * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
  47. */
  48. function Cesium3DTilesVoxelProvider(options) {
  49. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  50. this._ready = false;
  51. /** @inheritdoc */
  52. this.shapeTransform = undefined;
  53. /** @inheritdoc */
  54. this.globalTransform = undefined;
  55. /** @inheritdoc */
  56. this.shape = undefined;
  57. /** @inheritdoc */
  58. this.minBounds = undefined;
  59. /** @inheritdoc */
  60. this.maxBounds = undefined;
  61. /** @inheritdoc */
  62. this.dimensions = undefined;
  63. /** @inheritdoc */
  64. this.paddingBefore = undefined;
  65. /** @inheritdoc */
  66. this.paddingAfter = undefined;
  67. /** @inheritdoc */
  68. this.names = undefined;
  69. /** @inheritdoc */
  70. this.types = undefined;
  71. /** @inheritdoc */
  72. this.componentTypes = undefined;
  73. /** @inheritdoc */
  74. this.minimumValues = undefined;
  75. /** @inheritdoc */
  76. this.maximumValues = undefined;
  77. /** @inheritdoc */
  78. this.maximumTileCount = undefined;
  79. this._implicitTileset = undefined;
  80. this._subtreeCache = new ImplicitSubtreeCache();
  81. const that = this;
  82. let tilesetJson;
  83. if (defined(options.url)) {
  84. deprecationWarning(
  85. "Cesium3DTilesVoxelProvider options.url",
  86. "Cesium3DTilesVoxelProvider constructor parameter options.url was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use Cesium3DTilesVoxelProvider.fromUrl instead."
  87. );
  88. this._readyPromise = Promise.resolve(options.url).then(function (url) {
  89. const resource = Resource.createIfNeeded(url);
  90. return resource
  91. .fetchJson()
  92. .then(function (tileset) {
  93. tilesetJson = tileset;
  94. validate(tilesetJson);
  95. const schemaLoader = getMetadataSchemaLoader(tilesetJson, resource);
  96. return schemaLoader.load();
  97. })
  98. .then(function (schemaLoader) {
  99. const root = tilesetJson.root;
  100. const voxel = root.content.extensions["3DTILES_content_voxels"];
  101. const className = voxel.class;
  102. const metadataJson = hasExtension(tilesetJson, "3DTILES_metadata")
  103. ? tilesetJson.extensions["3DTILES_metadata"]
  104. : tilesetJson;
  105. const metadataSchema = schemaLoader.schema;
  106. const metadata = new Cesium3DTilesetMetadata({
  107. metadataJson: metadataJson,
  108. schema: metadataSchema,
  109. });
  110. addAttributeInfo(that, metadata, className);
  111. const implicitTileset = new ImplicitTileset(
  112. resource,
  113. root,
  114. metadataSchema
  115. );
  116. const {
  117. shape,
  118. minBounds,
  119. maxBounds,
  120. shapeTransform,
  121. globalTransform,
  122. } = getShape(root);
  123. that.shape = shape;
  124. that.minBounds = minBounds;
  125. that.maxBounds = maxBounds;
  126. that.dimensions = Cartesian3.unpack(voxel.dimensions);
  127. that.shapeTransform = shapeTransform;
  128. that.globalTransform = globalTransform;
  129. that.maximumTileCount = getTileCount(metadata);
  130. let paddingBefore;
  131. let paddingAfter;
  132. if (defined(voxel.padding)) {
  133. paddingBefore = Cartesian3.unpack(voxel.padding.before);
  134. paddingAfter = Cartesian3.unpack(voxel.padding.after);
  135. }
  136. that.paddingBefore = paddingBefore;
  137. that.paddingAfter = paddingAfter;
  138. that._implicitTileset = implicitTileset;
  139. ResourceCache.unload(schemaLoader);
  140. that._ready = true;
  141. return that;
  142. });
  143. });
  144. }
  145. }
  146. Object.defineProperties(Cesium3DTilesVoxelProvider.prototype, {
  147. /**
  148. * Gets the promise that will be resolved when the provider is ready for use.
  149. *
  150. * @memberof Cesium3DTilesVoxelProvider.prototype
  151. * @type {Promise<Cesium3DTilesVoxelProvider>}
  152. * @readonly
  153. * @deprecated
  154. */
  155. readyPromise: {
  156. get: function () {
  157. deprecationWarning(
  158. "Cesium3DTilesVoxelProvider.readyPromise",
  159. "Cesium3DTilesVoxelProvider.readyPromise was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use Cesium3DTilesVoxelProvider.fromUrl instead."
  160. );
  161. return this._readyPromise;
  162. },
  163. },
  164. /**
  165. * Gets a value indicating whether or not the provider is ready for use.
  166. *
  167. * @memberof Cesium3DTilesVoxelProvider.prototype
  168. * @type {boolean}
  169. * @readonly
  170. * @deprecated
  171. */
  172. ready: {
  173. get: function () {
  174. deprecationWarning(
  175. "Cesium3DTilesVoxelProvider.ready",
  176. "Cesium3DTilesVoxelProvider.ready was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use Cesium3DTilesVoxelProvider.fromUrl instead."
  177. );
  178. return this._ready;
  179. },
  180. },
  181. });
  182. /**
  183. * Creates a {@link VoxelProvider} that fetches voxel data from a 3D Tiles tileset.
  184. *
  185. * @param {Resource|string} url The URL to a tileset JSON file
  186. * @returns {Promise<Cesium3DTilesVoxelProvider>} The created provider
  187. *
  188. * @exception {RuntimeException} Root must have content
  189. * @exception {RuntimeException} Root tile content must have 3DTILES_content_voxels extension
  190. * @exception {RuntimeException} Root tile must have implicit tiling
  191. * @exception {RuntimeException} Tileset must have a metadata schema
  192. * @exception {RuntimeException} Only box, region and 3DTILES_bounding_volume_cylinder are supported in Cesium3DTilesVoxelProvider
  193. */
  194. Cesium3DTilesVoxelProvider.fromUrl = async function (url) {
  195. //>>includeStart('debug', pragmas.debug);
  196. Check.defined("url", url);
  197. //>>includeEnd('debug');
  198. const resource = Resource.createIfNeeded(url);
  199. const tilesetJson = await resource.fetchJson();
  200. validate(tilesetJson);
  201. const schemaLoader = getMetadataSchemaLoader(tilesetJson, resource);
  202. await schemaLoader.load();
  203. const root = tilesetJson.root;
  204. const voxel = root.content.extensions["3DTILES_content_voxels"];
  205. const className = voxel.class;
  206. const metadataJson = hasExtension(tilesetJson, "3DTILES_metadata")
  207. ? tilesetJson.extensions["3DTILES_metadata"]
  208. : tilesetJson;
  209. const metadataSchema = schemaLoader.schema;
  210. const metadata = new Cesium3DTilesetMetadata({
  211. metadataJson: metadataJson,
  212. schema: metadataSchema,
  213. });
  214. const provider = new Cesium3DTilesVoxelProvider();
  215. addAttributeInfo(provider, metadata, className);
  216. const implicitTileset = new ImplicitTileset(resource, root, metadataSchema);
  217. const {
  218. shape,
  219. minBounds,
  220. maxBounds,
  221. shapeTransform,
  222. globalTransform,
  223. } = getShape(root);
  224. provider.shape = shape;
  225. provider.minBounds = minBounds;
  226. provider.maxBounds = maxBounds;
  227. provider.dimensions = Cartesian3.unpack(voxel.dimensions);
  228. provider.shapeTransform = shapeTransform;
  229. provider.globalTransform = globalTransform;
  230. provider.maximumTileCount = getTileCount(metadata);
  231. let paddingBefore;
  232. let paddingAfter;
  233. if (defined(voxel.padding)) {
  234. paddingBefore = Cartesian3.unpack(voxel.padding.before);
  235. paddingAfter = Cartesian3.unpack(voxel.padding.after);
  236. }
  237. provider.paddingBefore = paddingBefore;
  238. provider.paddingAfter = paddingAfter;
  239. provider._implicitTileset = implicitTileset;
  240. ResourceCache.unload(schemaLoader);
  241. provider._ready = true;
  242. provider._readyPromise = Promise.resolve(provider);
  243. return provider;
  244. };
  245. function getTileCount(metadata) {
  246. if (!defined(metadata.tileset)) {
  247. return undefined;
  248. }
  249. return metadata.tileset.getPropertyBySemantic(
  250. MetadataSemantic.TILESET_TILE_COUNT
  251. );
  252. }
  253. function validate(tileset) {
  254. const root = tileset.root;
  255. if (!defined(root.content)) {
  256. throw new RuntimeError("Root must have content");
  257. }
  258. if (!hasExtension(root.content, "3DTILES_content_voxels")) {
  259. throw new RuntimeError(
  260. "Root tile content must have 3DTILES_content_voxels extension"
  261. );
  262. }
  263. if (
  264. !hasExtension(root, "3DTILES_implicit_tiling") &&
  265. !defined(root.implicitTiling)
  266. ) {
  267. throw new RuntimeError("Root tile must have implicit tiling");
  268. }
  269. if (
  270. !defined(tileset.schema) &&
  271. !defined(tileset.schemaUri) &&
  272. !hasExtension(tileset, "3DTILES_metadata")
  273. ) {
  274. throw new RuntimeError("Tileset must have a metadata schema");
  275. }
  276. }
  277. function getShape(tile) {
  278. const boundingVolume = tile.boundingVolume;
  279. let tileTransform;
  280. if (defined(tile.transform)) {
  281. tileTransform = Matrix4.unpack(tile.transform);
  282. } else {
  283. tileTransform = Matrix4.clone(Matrix4.IDENTITY);
  284. }
  285. if (defined(boundingVolume.box)) {
  286. return getBoxShape(boundingVolume.box, tileTransform);
  287. } else if (defined(boundingVolume.region)) {
  288. return getEllipsoidShape(boundingVolume.region);
  289. } else if (hasExtension(boundingVolume, "3DTILES_bounding_volume_cylinder")) {
  290. return getCylinderShape(
  291. boundingVolume.extensions["3DTILES_bounding_volume_cylinder"].cylinder,
  292. tileTransform
  293. );
  294. }
  295. throw new RuntimeError(
  296. "Only box, region and 3DTILES_bounding_volume_cylinder are supported in Cesium3DTilesVoxelProvider"
  297. );
  298. }
  299. function getEllipsoidShape(region) {
  300. const west = region[0];
  301. const south = region[1];
  302. const east = region[2];
  303. const north = region[3];
  304. const minHeight = region[4];
  305. const maxHeight = region[5];
  306. const shapeTransform = Matrix4.fromScale(Ellipsoid.WGS84.radii);
  307. const minBoundsX = west;
  308. const maxBoundsX = east;
  309. const minBoundsY = south;
  310. const maxBoundsY = north;
  311. const minBoundsZ = minHeight;
  312. const maxBoundsZ = maxHeight;
  313. const minBounds = new Cartesian3(minBoundsX, minBoundsY, minBoundsZ);
  314. const maxBounds = new Cartesian3(maxBoundsX, maxBoundsY, maxBoundsZ);
  315. return {
  316. shape: VoxelShapeType.ELLIPSOID,
  317. minBounds: minBounds,
  318. maxBounds: maxBounds,
  319. shapeTransform: shapeTransform,
  320. globalTransform: Matrix4.clone(Matrix4.IDENTITY),
  321. };
  322. }
  323. function getBoxShape(box, tileTransform) {
  324. const obb = OrientedBoundingBox.unpack(box);
  325. const shapeTransform = Matrix4.fromRotationTranslation(
  326. obb.halfAxes,
  327. obb.center
  328. );
  329. return {
  330. shape: VoxelShapeType.BOX,
  331. minBounds: Cartesian3.clone(VoxelBoxShape.DefaultMinBounds),
  332. maxBounds: Cartesian3.clone(VoxelBoxShape.DefaultMaxBounds),
  333. shapeTransform: shapeTransform,
  334. globalTransform: tileTransform,
  335. };
  336. }
  337. function getCylinderShape(cylinder, tileTransform) {
  338. const obb = OrientedBoundingBox.unpack(cylinder);
  339. const shapeTransform = Matrix4.fromRotationTranslation(
  340. obb.halfAxes,
  341. obb.center
  342. );
  343. return {
  344. shape: VoxelShapeType.CYLINDER,
  345. minBounds: Cartesian3.clone(VoxelCylinderShape.DefaultMinBounds),
  346. maxBounds: Cartesian3.clone(VoxelCylinderShape.DefaultMaxBounds),
  347. shapeTransform: shapeTransform,
  348. globalTransform: tileTransform,
  349. };
  350. }
  351. function getMetadataSchemaLoader(tilesetJson, resource) {
  352. const { schemaUri, schema } = tilesetJson;
  353. if (!defined(schemaUri)) {
  354. return ResourceCache.getSchemaLoader({ schema });
  355. }
  356. return ResourceCache.getSchemaLoader({
  357. resource: resource.getDerivedResource({
  358. url: schemaUri,
  359. }),
  360. });
  361. }
  362. function addAttributeInfo(provider, metadata, className) {
  363. const { schema, statistics } = metadata;
  364. const classStatistics = statistics?.classes[className];
  365. const properties = schema.classes[className].properties;
  366. const propertyInfo = Object.entries(properties).map(([id, property]) => {
  367. const { type, componentType } = property;
  368. const min = classStatistics?.properties[id].min;
  369. const max = classStatistics?.properties[id].max;
  370. const componentCount = MetadataType.getComponentCount(type);
  371. const minValue = copyArray(min, componentCount);
  372. const maxValue = copyArray(max, componentCount);
  373. return { id, type, componentType, minValue, maxValue };
  374. });
  375. provider.names = propertyInfo.map((info) => info.id);
  376. provider.types = propertyInfo.map((info) => info.type);
  377. provider.componentTypes = propertyInfo.map((info) => info.componentType);
  378. const minimumValues = propertyInfo.map((info) => info.minValue);
  379. const maximumValues = propertyInfo.map((info) => info.maxValue);
  380. const hasMinimumValues = minimumValues.some(defined);
  381. provider.minimumValues = hasMinimumValues ? minimumValues : undefined;
  382. provider.maximumValues = hasMinimumValues ? maximumValues : undefined;
  383. }
  384. function copyArray(values, length) {
  385. // Copy input values into a new array of a specified length.
  386. // If the input is not an array, its value will be copied into the first element
  387. // of the returned array. If the input is an array shorter than the returned
  388. // array, the extra elements in the returned array will be undefined. If the
  389. // input is undefined, the return will be undefined.
  390. if (!defined(values)) {
  391. return;
  392. }
  393. const valuesArray = Array.isArray(values) ? values : [values];
  394. return Array.from({ length }, (v, i) => valuesArray[i]);
  395. }
  396. async function getVoxelContent(implicitTileset, tileCoordinates) {
  397. const voxelRelative = implicitTileset.contentUriTemplates[0].getDerivedResource(
  398. {
  399. templateValues: tileCoordinates.getTemplateValues(),
  400. }
  401. );
  402. const voxelResource = implicitTileset.baseResource.getDerivedResource({
  403. url: voxelRelative.url,
  404. });
  405. const arrayBuffer = await voxelResource.fetchArrayBuffer();
  406. const preprocessed = preprocess3DTileContent(arrayBuffer);
  407. const voxelContent = await VoxelContent.fromJson(
  408. voxelResource,
  409. preprocessed.jsonPayload,
  410. preprocessed.binaryPayload,
  411. implicitTileset.metadataSchema
  412. );
  413. return voxelContent;
  414. }
  415. async function getSubtreePromise(provider, subtreeCoord) {
  416. const implicitTileset = provider._implicitTileset;
  417. const subtreeCache = provider._subtreeCache;
  418. // First load the subtree to check if the tile is available.
  419. // If the subtree has been requested previously it might still be in the cache
  420. let subtree = subtreeCache.find(subtreeCoord);
  421. if (defined(subtree)) {
  422. return subtree;
  423. }
  424. const subtreeRelative = implicitTileset.subtreeUriTemplate.getDerivedResource(
  425. {
  426. templateValues: subtreeCoord.getTemplateValues(),
  427. }
  428. );
  429. const subtreeResource = implicitTileset.baseResource.getDerivedResource({
  430. url: subtreeRelative.url,
  431. });
  432. const arrayBuffer = await subtreeResource.fetchArrayBuffer();
  433. // Check one more time if the subtree is in the cache.
  434. // This could happen if there are two in-flight tile requests from the same
  435. // subtree and one finishes before the other.
  436. subtree = subtreeCache.find(subtreeCoord);
  437. if (defined(subtree)) {
  438. return subtree;
  439. }
  440. const preprocessed = preprocess3DTileContent(arrayBuffer);
  441. subtree = await ImplicitSubtree.fromSubtreeJson(
  442. subtreeResource,
  443. preprocessed.jsonPayload,
  444. preprocessed.binaryPayload,
  445. implicitTileset,
  446. subtreeCoord
  447. );
  448. subtreeCache.addSubtree(subtree);
  449. return subtree;
  450. }
  451. /** @inheritdoc */
  452. Cesium3DTilesVoxelProvider.prototype.requestData = function (options) {
  453. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  454. const tileLevel = defaultValue(options.tileLevel, 0);
  455. const tileX = defaultValue(options.tileX, 0);
  456. const tileY = defaultValue(options.tileY, 0);
  457. const tileZ = defaultValue(options.tileZ, 0);
  458. const keyframe = defaultValue(options.keyframe, 0);
  459. // 3D Tiles currently doesn't support time-dynamic data.
  460. if (keyframe !== 0) {
  461. return undefined;
  462. }
  463. // 1. Load the subtree that the tile belongs to (possibly from the subtree cache)
  464. // 2. Load the voxel content if available
  465. const implicitTileset = this._implicitTileset;
  466. const names = this.names;
  467. // Can't use a scratch variable here because the object is used inside the promise chain.
  468. const tileCoordinates = new ImplicitTileCoordinates({
  469. subdivisionScheme: implicitTileset.subdivisionScheme,
  470. subtreeLevels: implicitTileset.subtreeLevels,
  471. level: tileLevel,
  472. x: tileX,
  473. y: tileY,
  474. z: tileZ,
  475. });
  476. // Find the coordinates of the parent subtree containing tileCoordinates
  477. // If tileCoordinates is a subtree child, use that subtree
  478. // If tileCoordinates is a subtree root, use its parent subtree
  479. const isSubtreeRoot =
  480. tileCoordinates.isSubtreeRoot() && tileCoordinates.level > 0;
  481. const subtreeCoord = isSubtreeRoot
  482. ? tileCoordinates.getParentSubtreeCoordinates()
  483. : tileCoordinates.getSubtreeCoordinates();
  484. const that = this;
  485. return getSubtreePromise(that, subtreeCoord)
  486. .then(function (subtree) {
  487. const available = isSubtreeRoot
  488. ? subtree.childSubtreeIsAvailableAtCoordinates(tileCoordinates)
  489. : subtree.tileIsAvailableAtCoordinates(tileCoordinates);
  490. if (!available) {
  491. return Promise.reject("Tile is not available");
  492. }
  493. return getVoxelContent(implicitTileset, tileCoordinates);
  494. })
  495. .then(function (voxelContent) {
  496. return names.map(function (name) {
  497. return voxelContent.metadataTable.getPropertyTypedArray(name);
  498. });
  499. });
  500. };
  501. export default Cesium3DTilesVoxelProvider;