import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import deprecationWarning from "../Core/deprecationWarning.js";
import Ellipsoid from "../Core/Ellipsoid.js";
import Matrix4 from "../Core/Matrix4.js";
import OrientedBoundingBox from "../Core/OrientedBoundingBox.js";
import Resource from "../Core/Resource.js";
import RuntimeError from "../Core/RuntimeError.js";
import Cesium3DTilesetMetadata from "./Cesium3DTilesetMetadata.js";
import hasExtension from "./hasExtension.js";
import ImplicitSubtree from "./ImplicitSubtree.js";
import ImplicitSubtreeCache from "./ImplicitSubtreeCache.js";
import ImplicitTileCoordinates from "./ImplicitTileCoordinates.js";
import ImplicitTileset from "./ImplicitTileset.js";
import MetadataSemantic from "./MetadataSemantic.js";
import MetadataType from "./MetadataType.js";
import preprocess3DTileContent from "./preprocess3DTileContent.js";
import ResourceCache from "./ResourceCache.js";
import VoxelBoxShape from "./VoxelBoxShape.js";
import VoxelContent from "./VoxelContent.js";
import VoxelCylinderShape from "./VoxelCylinderShape.js";
import VoxelShapeType from "./VoxelShapeType.js";
/**
* A {@link VoxelProvider} that fetches voxel data from a 3D Tiles tileset.
*
* Implements the {@link VoxelProvider} interface.
*
*
* This object is normally not instantiated directly, use {@link Cesium3DTilesVoxelProvider.fromUrl}.
*
*
* @alias Cesium3DTilesVoxelProvider
* @constructor
* @augments VoxelProvider
*
* @param {object} options Object with the following properties:
* @param {Resource|string|Promise|Promise} [options.url] The URL to a tileset JSON file. Deprecated.
*
* @see Cesium3DTilesVoxelProvider.fromUrl
* @see VoxelProvider
* @see VoxelPrimitive
* @see VoxelShapeType
*
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*/
function Cesium3DTilesVoxelProvider(options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
this._ready = false;
/** @inheritdoc */
this.shapeTransform = undefined;
/** @inheritdoc */
this.globalTransform = undefined;
/** @inheritdoc */
this.shape = undefined;
/** @inheritdoc */
this.minBounds = undefined;
/** @inheritdoc */
this.maxBounds = undefined;
/** @inheritdoc */
this.dimensions = undefined;
/** @inheritdoc */
this.paddingBefore = undefined;
/** @inheritdoc */
this.paddingAfter = undefined;
/** @inheritdoc */
this.names = undefined;
/** @inheritdoc */
this.types = undefined;
/** @inheritdoc */
this.componentTypes = undefined;
/** @inheritdoc */
this.minimumValues = undefined;
/** @inheritdoc */
this.maximumValues = undefined;
/** @inheritdoc */
this.maximumTileCount = undefined;
this._implicitTileset = undefined;
this._subtreeCache = new ImplicitSubtreeCache();
const that = this;
let tilesetJson;
if (defined(options.url)) {
deprecationWarning(
"Cesium3DTilesVoxelProvider options.url",
"Cesium3DTilesVoxelProvider constructor parameter options.url was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use Cesium3DTilesVoxelProvider.fromUrl instead."
);
this._readyPromise = Promise.resolve(options.url).then(function (url) {
const resource = Resource.createIfNeeded(url);
return resource
.fetchJson()
.then(function (tileset) {
tilesetJson = tileset;
validate(tilesetJson);
const schemaLoader = getMetadataSchemaLoader(tilesetJson, resource);
return schemaLoader.load();
})
.then(function (schemaLoader) {
const root = tilesetJson.root;
const voxel = root.content.extensions["3DTILES_content_voxels"];
const className = voxel.class;
const metadataJson = hasExtension(tilesetJson, "3DTILES_metadata")
? tilesetJson.extensions["3DTILES_metadata"]
: tilesetJson;
const metadataSchema = schemaLoader.schema;
const metadata = new Cesium3DTilesetMetadata({
metadataJson: metadataJson,
schema: metadataSchema,
});
addAttributeInfo(that, metadata, className);
const implicitTileset = new ImplicitTileset(
resource,
root,
metadataSchema
);
const {
shape,
minBounds,
maxBounds,
shapeTransform,
globalTransform,
} = getShape(root);
that.shape = shape;
that.minBounds = minBounds;
that.maxBounds = maxBounds;
that.dimensions = Cartesian3.unpack(voxel.dimensions);
that.shapeTransform = shapeTransform;
that.globalTransform = globalTransform;
that.maximumTileCount = getTileCount(metadata);
let paddingBefore;
let paddingAfter;
if (defined(voxel.padding)) {
paddingBefore = Cartesian3.unpack(voxel.padding.before);
paddingAfter = Cartesian3.unpack(voxel.padding.after);
}
that.paddingBefore = paddingBefore;
that.paddingAfter = paddingAfter;
that._implicitTileset = implicitTileset;
ResourceCache.unload(schemaLoader);
that._ready = true;
return that;
});
});
}
}
Object.defineProperties(Cesium3DTilesVoxelProvider.prototype, {
/**
* Gets the promise that will be resolved when the provider is ready for use.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {Promise}
* @readonly
* @deprecated
*/
readyPromise: {
get: function () {
deprecationWarning(
"Cesium3DTilesVoxelProvider.readyPromise",
"Cesium3DTilesVoxelProvider.readyPromise was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use Cesium3DTilesVoxelProvider.fromUrl instead."
);
return this._readyPromise;
},
},
/**
* Gets a value indicating whether or not the provider is ready for use.
*
* @memberof Cesium3DTilesVoxelProvider.prototype
* @type {boolean}
* @readonly
* @deprecated
*/
ready: {
get: function () {
deprecationWarning(
"Cesium3DTilesVoxelProvider.ready",
"Cesium3DTilesVoxelProvider.ready was deprecated in CesiumJS 1.104. It will be removed in 1.107. Use Cesium3DTilesVoxelProvider.fromUrl instead."
);
return this._ready;
},
},
});
/**
* Creates a {@link VoxelProvider} that fetches voxel data from a 3D Tiles tileset.
*
* @param {Resource|string} url The URL to a tileset JSON file
* @returns {Promise} The created provider
*
* @exception {RuntimeException} Root must have content
* @exception {RuntimeException} Root tile content must have 3DTILES_content_voxels extension
* @exception {RuntimeException} Root tile must have implicit tiling
* @exception {RuntimeException} Tileset must have a metadata schema
* @exception {RuntimeException} Only box, region and 3DTILES_bounding_volume_cylinder are supported in Cesium3DTilesVoxelProvider
*/
Cesium3DTilesVoxelProvider.fromUrl = async function (url) {
//>>includeStart('debug', pragmas.debug);
Check.defined("url", url);
//>>includeEnd('debug');
const resource = Resource.createIfNeeded(url);
const tilesetJson = await resource.fetchJson();
validate(tilesetJson);
const schemaLoader = getMetadataSchemaLoader(tilesetJson, resource);
await schemaLoader.load();
const root = tilesetJson.root;
const voxel = root.content.extensions["3DTILES_content_voxels"];
const className = voxel.class;
const metadataJson = hasExtension(tilesetJson, "3DTILES_metadata")
? tilesetJson.extensions["3DTILES_metadata"]
: tilesetJson;
const metadataSchema = schemaLoader.schema;
const metadata = new Cesium3DTilesetMetadata({
metadataJson: metadataJson,
schema: metadataSchema,
});
const provider = new Cesium3DTilesVoxelProvider();
addAttributeInfo(provider, metadata, className);
const implicitTileset = new ImplicitTileset(resource, root, metadataSchema);
const {
shape,
minBounds,
maxBounds,
shapeTransform,
globalTransform,
} = getShape(root);
provider.shape = shape;
provider.minBounds = minBounds;
provider.maxBounds = maxBounds;
provider.dimensions = Cartesian3.unpack(voxel.dimensions);
provider.shapeTransform = shapeTransform;
provider.globalTransform = globalTransform;
provider.maximumTileCount = getTileCount(metadata);
let paddingBefore;
let paddingAfter;
if (defined(voxel.padding)) {
paddingBefore = Cartesian3.unpack(voxel.padding.before);
paddingAfter = Cartesian3.unpack(voxel.padding.after);
}
provider.paddingBefore = paddingBefore;
provider.paddingAfter = paddingAfter;
provider._implicitTileset = implicitTileset;
ResourceCache.unload(schemaLoader);
provider._ready = true;
provider._readyPromise = Promise.resolve(provider);
return provider;
};
function getTileCount(metadata) {
if (!defined(metadata.tileset)) {
return undefined;
}
return metadata.tileset.getPropertyBySemantic(
MetadataSemantic.TILESET_TILE_COUNT
);
}
function validate(tileset) {
const root = tileset.root;
if (!defined(root.content)) {
throw new RuntimeError("Root must have content");
}
if (!hasExtension(root.content, "3DTILES_content_voxels")) {
throw new RuntimeError(
"Root tile content must have 3DTILES_content_voxels extension"
);
}
if (
!hasExtension(root, "3DTILES_implicit_tiling") &&
!defined(root.implicitTiling)
) {
throw new RuntimeError("Root tile must have implicit tiling");
}
if (
!defined(tileset.schema) &&
!defined(tileset.schemaUri) &&
!hasExtension(tileset, "3DTILES_metadata")
) {
throw new RuntimeError("Tileset must have a metadata schema");
}
}
function getShape(tile) {
const boundingVolume = tile.boundingVolume;
let tileTransform;
if (defined(tile.transform)) {
tileTransform = Matrix4.unpack(tile.transform);
} else {
tileTransform = Matrix4.clone(Matrix4.IDENTITY);
}
if (defined(boundingVolume.box)) {
return getBoxShape(boundingVolume.box, tileTransform);
} else if (defined(boundingVolume.region)) {
return getEllipsoidShape(boundingVolume.region);
} else if (hasExtension(boundingVolume, "3DTILES_bounding_volume_cylinder")) {
return getCylinderShape(
boundingVolume.extensions["3DTILES_bounding_volume_cylinder"].cylinder,
tileTransform
);
}
throw new RuntimeError(
"Only box, region and 3DTILES_bounding_volume_cylinder are supported in Cesium3DTilesVoxelProvider"
);
}
function getEllipsoidShape(region) {
const west = region[0];
const south = region[1];
const east = region[2];
const north = region[3];
const minHeight = region[4];
const maxHeight = region[5];
const shapeTransform = Matrix4.fromScale(Ellipsoid.WGS84.radii);
const minBoundsX = west;
const maxBoundsX = east;
const minBoundsY = south;
const maxBoundsY = north;
const minBoundsZ = minHeight;
const maxBoundsZ = maxHeight;
const minBounds = new Cartesian3(minBoundsX, minBoundsY, minBoundsZ);
const maxBounds = new Cartesian3(maxBoundsX, maxBoundsY, maxBoundsZ);
return {
shape: VoxelShapeType.ELLIPSOID,
minBounds: minBounds,
maxBounds: maxBounds,
shapeTransform: shapeTransform,
globalTransform: Matrix4.clone(Matrix4.IDENTITY),
};
}
function getBoxShape(box, tileTransform) {
const obb = OrientedBoundingBox.unpack(box);
const shapeTransform = Matrix4.fromRotationTranslation(
obb.halfAxes,
obb.center
);
return {
shape: VoxelShapeType.BOX,
minBounds: Cartesian3.clone(VoxelBoxShape.DefaultMinBounds),
maxBounds: Cartesian3.clone(VoxelBoxShape.DefaultMaxBounds),
shapeTransform: shapeTransform,
globalTransform: tileTransform,
};
}
function getCylinderShape(cylinder, tileTransform) {
const obb = OrientedBoundingBox.unpack(cylinder);
const shapeTransform = Matrix4.fromRotationTranslation(
obb.halfAxes,
obb.center
);
return {
shape: VoxelShapeType.CYLINDER,
minBounds: Cartesian3.clone(VoxelCylinderShape.DefaultMinBounds),
maxBounds: Cartesian3.clone(VoxelCylinderShape.DefaultMaxBounds),
shapeTransform: shapeTransform,
globalTransform: tileTransform,
};
}
function getMetadataSchemaLoader(tilesetJson, resource) {
const { schemaUri, schema } = tilesetJson;
if (!defined(schemaUri)) {
return ResourceCache.getSchemaLoader({ schema });
}
return ResourceCache.getSchemaLoader({
resource: resource.getDerivedResource({
url: schemaUri,
}),
});
}
function addAttributeInfo(provider, metadata, className) {
const { schema, statistics } = metadata;
const classStatistics = statistics?.classes[className];
const properties = schema.classes[className].properties;
const propertyInfo = Object.entries(properties).map(([id, property]) => {
const { type, componentType } = property;
const min = classStatistics?.properties[id].min;
const max = classStatistics?.properties[id].max;
const componentCount = MetadataType.getComponentCount(type);
const minValue = copyArray(min, componentCount);
const maxValue = copyArray(max, componentCount);
return { id, type, componentType, minValue, maxValue };
});
provider.names = propertyInfo.map((info) => info.id);
provider.types = propertyInfo.map((info) => info.type);
provider.componentTypes = propertyInfo.map((info) => info.componentType);
const minimumValues = propertyInfo.map((info) => info.minValue);
const maximumValues = propertyInfo.map((info) => info.maxValue);
const hasMinimumValues = minimumValues.some(defined);
provider.minimumValues = hasMinimumValues ? minimumValues : undefined;
provider.maximumValues = hasMinimumValues ? maximumValues : undefined;
}
function copyArray(values, length) {
// Copy input values into a new array of a specified length.
// If the input is not an array, its value will be copied into the first element
// of the returned array. If the input is an array shorter than the returned
// array, the extra elements in the returned array will be undefined. If the
// input is undefined, the return will be undefined.
if (!defined(values)) {
return;
}
const valuesArray = Array.isArray(values) ? values : [values];
return Array.from({ length }, (v, i) => valuesArray[i]);
}
async function getVoxelContent(implicitTileset, tileCoordinates) {
const voxelRelative = implicitTileset.contentUriTemplates[0].getDerivedResource(
{
templateValues: tileCoordinates.getTemplateValues(),
}
);
const voxelResource = implicitTileset.baseResource.getDerivedResource({
url: voxelRelative.url,
});
const arrayBuffer = await voxelResource.fetchArrayBuffer();
const preprocessed = preprocess3DTileContent(arrayBuffer);
const voxelContent = await VoxelContent.fromJson(
voxelResource,
preprocessed.jsonPayload,
preprocessed.binaryPayload,
implicitTileset.metadataSchema
);
return voxelContent;
}
async function getSubtreePromise(provider, subtreeCoord) {
const implicitTileset = provider._implicitTileset;
const subtreeCache = provider._subtreeCache;
// First load the subtree to check if the tile is available.
// If the subtree has been requested previously it might still be in the cache
let subtree = subtreeCache.find(subtreeCoord);
if (defined(subtree)) {
return subtree;
}
const subtreeRelative = implicitTileset.subtreeUriTemplate.getDerivedResource(
{
templateValues: subtreeCoord.getTemplateValues(),
}
);
const subtreeResource = implicitTileset.baseResource.getDerivedResource({
url: subtreeRelative.url,
});
const arrayBuffer = await subtreeResource.fetchArrayBuffer();
// Check one more time if the subtree is in the cache.
// This could happen if there are two in-flight tile requests from the same
// subtree and one finishes before the other.
subtree = subtreeCache.find(subtreeCoord);
if (defined(subtree)) {
return subtree;
}
const preprocessed = preprocess3DTileContent(arrayBuffer);
subtree = await ImplicitSubtree.fromSubtreeJson(
subtreeResource,
preprocessed.jsonPayload,
preprocessed.binaryPayload,
implicitTileset,
subtreeCoord
);
subtreeCache.addSubtree(subtree);
return subtree;
}
/** @inheritdoc */
Cesium3DTilesVoxelProvider.prototype.requestData = function (options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
const tileLevel = defaultValue(options.tileLevel, 0);
const tileX = defaultValue(options.tileX, 0);
const tileY = defaultValue(options.tileY, 0);
const tileZ = defaultValue(options.tileZ, 0);
const keyframe = defaultValue(options.keyframe, 0);
// 3D Tiles currently doesn't support time-dynamic data.
if (keyframe !== 0) {
return undefined;
}
// 1. Load the subtree that the tile belongs to (possibly from the subtree cache)
// 2. Load the voxel content if available
const implicitTileset = this._implicitTileset;
const names = this.names;
// Can't use a scratch variable here because the object is used inside the promise chain.
const tileCoordinates = new ImplicitTileCoordinates({
subdivisionScheme: implicitTileset.subdivisionScheme,
subtreeLevels: implicitTileset.subtreeLevels,
level: tileLevel,
x: tileX,
y: tileY,
z: tileZ,
});
// Find the coordinates of the parent subtree containing tileCoordinates
// If tileCoordinates is a subtree child, use that subtree
// If tileCoordinates is a subtree root, use its parent subtree
const isSubtreeRoot =
tileCoordinates.isSubtreeRoot() && tileCoordinates.level > 0;
const subtreeCoord = isSubtreeRoot
? tileCoordinates.getParentSubtreeCoordinates()
: tileCoordinates.getSubtreeCoordinates();
const that = this;
return getSubtreePromise(that, subtreeCoord)
.then(function (subtree) {
const available = isSubtreeRoot
? subtree.childSubtreeIsAvailableAtCoordinates(tileCoordinates)
: subtree.tileIsAvailableAtCoordinates(tileCoordinates);
if (!available) {
return Promise.reject("Tile is not available");
}
return getVoxelContent(implicitTileset, tileCoordinates);
})
.then(function (voxelContent) {
return names.map(function (name) {
return voxelContent.metadataTable.getPropertyTypedArray(name);
});
});
};
export default Cesium3DTilesVoxelProvider;