import arrayFill from "../Core/arrayFill.js";
import AttributeType from "./AttributeType.js";
import Check from "../Core/Check.js";
import clone from "../Core/clone.js";
import combine from "../Core/combine.js";
import ComponentDatatype from "../Core/ComponentDatatype.js";
import defined from "../Core/defined.js";
import defaultValue from "../Core/defaultValue.js";
import DeveloperError from "../Core/DeveloperError.js";
import getBinaryAccessor from "./getBinaryAccessor.js";
import RuntimeError from "../Core/RuntimeError.js";
/**
* Object for handling the 3DTILES_batch_table_hierarchy
extension
*
* @param {Object} options Object with the following properties:
* @param {Object} options.extension The 3DTILES_batch_table_hierarchy
extension object.
* @param {Uint8Array} [options.binaryBody] The binary body of the batch table
*
* @alias BatchTableHierarchy
* @constructor
*
* @private
*/
export default function BatchTableHierarchy(options) {
this._classes = undefined;
this._classIds = undefined;
this._classIndexes = undefined;
this._parentCounts = undefined;
this._parentIndexes = undefined;
this._parentIds = undefined;
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.extension", options.extension);
//>>includeEnd('debug');
initialize(this, options.extension, options.binaryBody);
//>>includeStart('debug', pragmas.debug);
validateHierarchy(this);
//>>includeEnd('debug');
}
/**
* Parse the batch table hierarchy from the
* 3DTILES_batch_table_hierarchy
extension.
*
* @param {BatchTableHierarchy} hierarchy The hierarchy instance
* @param {Object} hierarchyJson The JSON of the extension
* @param {Uint8Array} [binaryBody] The binary body of the batch table for accessing binary properties
* @private
*/
function initialize(hierarchy, hierarchyJson, binaryBody) {
let i;
let classId;
let binaryAccessor;
const instancesLength = hierarchyJson.instancesLength;
const classes = hierarchyJson.classes;
let classIds = hierarchyJson.classIds;
let parentCounts = hierarchyJson.parentCounts;
let parentIds = hierarchyJson.parentIds;
let parentIdsLength = instancesLength;
if (defined(classIds.byteOffset)) {
classIds.componentType = defaultValue(
classIds.componentType,
ComponentDatatype.UNSIGNED_SHORT
);
classIds.type = AttributeType.SCALAR;
binaryAccessor = getBinaryAccessor(classIds);
classIds = binaryAccessor.createArrayBufferView(
binaryBody.buffer,
binaryBody.byteOffset + classIds.byteOffset,
instancesLength
);
}
let parentIndexes;
if (defined(parentCounts)) {
if (defined(parentCounts.byteOffset)) {
parentCounts.componentType = defaultValue(
parentCounts.componentType,
ComponentDatatype.UNSIGNED_SHORT
);
parentCounts.type = AttributeType.SCALAR;
binaryAccessor = getBinaryAccessor(parentCounts);
parentCounts = binaryAccessor.createArrayBufferView(
binaryBody.buffer,
binaryBody.byteOffset + parentCounts.byteOffset,
instancesLength
);
}
parentIndexes = new Uint16Array(instancesLength);
parentIdsLength = 0;
for (i = 0; i < instancesLength; ++i) {
parentIndexes[i] = parentIdsLength;
parentIdsLength += parentCounts[i];
}
}
if (defined(parentIds) && defined(parentIds.byteOffset)) {
parentIds.componentType = defaultValue(
parentIds.componentType,
ComponentDatatype.UNSIGNED_SHORT
);
parentIds.type = AttributeType.SCALAR;
binaryAccessor = getBinaryAccessor(parentIds);
parentIds = binaryAccessor.createArrayBufferView(
binaryBody.buffer,
binaryBody.byteOffset + parentIds.byteOffset,
parentIdsLength
);
}
const classesLength = classes.length;
for (i = 0; i < classesLength; ++i) {
const classInstancesLength = classes[i].length;
const properties = classes[i].instances;
const binaryProperties = getBinaryProperties(
classInstancesLength,
properties,
binaryBody
);
classes[i].instances = combine(binaryProperties, properties);
}
const classCounts = arrayFill(new Array(classesLength), 0);
const classIndexes = new Uint16Array(instancesLength);
for (i = 0; i < instancesLength; ++i) {
classId = classIds[i];
classIndexes[i] = classCounts[classId];
++classCounts[classId];
}
hierarchy._classes = classes;
hierarchy._classIds = classIds;
hierarchy._classIndexes = classIndexes;
hierarchy._parentCounts = parentCounts;
hierarchy._parentIndexes = parentIndexes;
hierarchy._parentIds = parentIds;
}
function getBinaryProperties(featuresLength, properties, binaryBody) {
let binaryProperties;
for (const name in properties) {
if (properties.hasOwnProperty(name)) {
const property = properties[name];
const byteOffset = property.byteOffset;
if (defined(byteOffset)) {
// This is a binary property
const componentType = property.componentType;
const type = property.type;
if (!defined(componentType)) {
throw new RuntimeError("componentType is required.");
}
if (!defined(type)) {
throw new RuntimeError("type is required.");
}
if (!defined(binaryBody)) {
throw new RuntimeError(
`Property ${name} requires a batch table binary.`
);
}
const binaryAccessor = getBinaryAccessor(property);
const componentCount = binaryAccessor.componentsPerAttribute;
const classType = binaryAccessor.classType;
const typedArray = binaryAccessor.createArrayBufferView(
binaryBody.buffer,
binaryBody.byteOffset + byteOffset,
featuresLength
);
if (!defined(binaryProperties)) {
binaryProperties = {};
}
// Store any information needed to access the binary data, including the typed array,
// componentCount (e.g. a VEC4 would be 4), and the type used to pack and unpack (e.g. Cartesian4).
binaryProperties[name] = {
typedArray: typedArray,
componentCount: componentCount,
type: classType,
};
}
}
}
return binaryProperties;
}
//>>includeStart('debug', pragmas.debug);
const scratchValidateStack = [];
function validateHierarchy(hierarchy) {
const stack = scratchValidateStack;
stack.length = 0;
const classIds = hierarchy._classIds;
const instancesLength = classIds.length;
for (let i = 0; i < instancesLength; ++i) {
validateInstance(hierarchy, i, stack);
}
}
function validateInstance(hierarchy, instanceIndex, stack) {
const parentCounts = hierarchy._parentCounts;
const parentIds = hierarchy._parentIds;
const parentIndexes = hierarchy._parentIndexes;
const classIds = hierarchy._classIds;
const instancesLength = classIds.length;
if (!defined(parentIds)) {
// No need to validate if there are no parents
return;
}
if (instanceIndex >= instancesLength) {
throw new DeveloperError(
`Parent index ${instanceIndex} exceeds the total number of instances: ${instancesLength}`
);
}
if (stack.indexOf(instanceIndex) > -1) {
throw new DeveloperError(
"Circular dependency detected in the batch table hierarchy."
);
}
stack.push(instanceIndex);
const parentCount = defined(parentCounts) ? parentCounts[instanceIndex] : 1;
const parentIndex = defined(parentCounts)
? parentIndexes[instanceIndex]
: instanceIndex;
for (let i = 0; i < parentCount; ++i) {
const parentId = parentIds[parentIndex + i];
// Stop the traversal when the instance has no parent (its parentId equals itself), else continue the traversal.
if (parentId !== instanceIndex) {
validateInstance(hierarchy, parentId, stack);
}
}
stack.pop(instanceIndex);
}
//>>includeEnd('debug');
// The size of this array equals the maximum instance count among all loaded tiles, which has the potential to be large.
const scratchVisited = [];
const scratchStack = [];
let marker = 0;
function traverseHierarchyMultipleParents(
hierarchy,
instanceIndex,
endConditionCallback
) {
const classIds = hierarchy._classIds;
const parentCounts = hierarchy._parentCounts;
const parentIds = hierarchy._parentIds;
const parentIndexes = hierarchy._parentIndexes;
const instancesLength = classIds.length;
// Ignore instances that have already been visited. This occurs in diamond inheritance situations.
// Use a marker value to indicate that an instance has been visited, which increments with each run.
// This is more efficient than clearing the visited array every time.
const visited = scratchVisited;
visited.length = Math.max(visited.length, instancesLength);
const visitedMarker = ++marker;
const stack = scratchStack;
stack.length = 0;
stack.push(instanceIndex);
while (stack.length > 0) {
instanceIndex = stack.pop();
if (visited[instanceIndex] === visitedMarker) {
// This instance has already been visited, stop traversal
continue;
}
visited[instanceIndex] = visitedMarker;
const result = endConditionCallback(hierarchy, instanceIndex);
if (defined(result)) {
// The end condition was met, stop the traversal and return the result
return result;
}
const parentCount = parentCounts[instanceIndex];
const parentIndex = parentIndexes[instanceIndex];
for (let i = 0; i < parentCount; ++i) {
const parentId = parentIds[parentIndex + i];
// Stop the traversal when the instance has no parent (its parentId equals itself)
// else add the parent to the stack to continue the traversal.
if (parentId !== instanceIndex) {
stack.push(parentId);
}
}
}
}
function traverseHierarchySingleParent(
hierarchy,
instanceIndex,
endConditionCallback
) {
let hasParent = true;
while (hasParent) {
const result = endConditionCallback(hierarchy, instanceIndex);
if (defined(result)) {
// The end condition was met, stop the traversal and return the result
return result;
}
const parentId = hierarchy._parentIds[instanceIndex];
hasParent = parentId !== instanceIndex;
instanceIndex = parentId;
}
}
function traverseHierarchy(hierarchy, instanceIndex, endConditionCallback) {
// Traverse over the hierarchy and process each instance with the endConditionCallback.
// When the endConditionCallback returns a value, the traversal stops and that value is returned.
const parentCounts = hierarchy._parentCounts;
const parentIds = hierarchy._parentIds;
if (!defined(parentIds)) {
return endConditionCallback(hierarchy, instanceIndex);
} else if (defined(parentCounts)) {
return traverseHierarchyMultipleParents(
hierarchy,
instanceIndex,
endConditionCallback
);
}
return traverseHierarchySingleParent(
hierarchy,
instanceIndex,
endConditionCallback
);
}
/**
* Returns whether the feature has this property.
*
* @param {Number} batchId the batch ID of the feature
* @param {String} propertyId The case-sensitive ID of the property.
* @returns {Boolean} Whether the feature has this property.
* @private
*/
BatchTableHierarchy.prototype.hasProperty = function (batchId, propertyId) {
const result = traverseHierarchy(this, batchId, function (
hierarchy,
instanceIndex
) {
const classId = hierarchy._classIds[instanceIndex];
const instances = hierarchy._classes[classId].instances;
if (defined(instances[propertyId])) {
return true;
}
});
return defined(result);
};
/**
* Returns whether any feature has this property.
*
* @param {String} propertyId The case-sensitive ID of the property.
* @returns {Boolean} Whether any feature has this property.
* @private
*/
BatchTableHierarchy.prototype.propertyExists = function (propertyId) {
const classes = this._classes;
const classesLength = classes.length;
for (let i = 0; i < classesLength; ++i) {
const instances = classes[i].instances;
if (defined(instances[propertyId])) {
return true;
}
}
return false;
};
/**
* Returns an array of property IDs.
*
* @param {Number} batchId the batch ID of the feature
* @param {Number} index The index of the entity.
* @param {String[]} [results] An array into which to store the results.
* @returns {String[]} The property IDs.
* @private
*/
BatchTableHierarchy.prototype.getPropertyIds = function (batchId, results) {
results = defined(results) ? results : [];
results.length = 0;
traverseHierarchy(this, batchId, function (hierarchy, instanceIndex) {
const classId = hierarchy._classIds[instanceIndex];
const instances = hierarchy._classes[classId].instances;
for (const name in instances) {
if (instances.hasOwnProperty(name)) {
if (results.indexOf(name) === -1) {
results.push(name);
}
}
}
});
return results;
};
/**
* Returns a copy of the value of the property with the given ID.
*
* @param {Number} batchId the batch ID of the feature
* @param {String} propertyId The case-sensitive ID of the property.
* @returns {*} The value of the property or undefined
if the feature does not have this property.
* @private
*/
BatchTableHierarchy.prototype.getProperty = function (batchId, propertyId) {
return traverseHierarchy(this, batchId, function (hierarchy, instanceIndex) {
const classId = hierarchy._classIds[instanceIndex];
const instanceClass = hierarchy._classes[classId];
const indexInClass = hierarchy._classIndexes[instanceIndex];
const propertyValues = instanceClass.instances[propertyId];
if (defined(propertyValues)) {
if (defined(propertyValues.typedArray)) {
return getBinaryProperty(propertyValues, indexInClass);
}
return clone(propertyValues[indexInClass], true);
}
});
};
function getBinaryProperty(binaryProperty, index) {
const typedArray = binaryProperty.typedArray;
const componentCount = binaryProperty.componentCount;
if (componentCount === 1) {
return typedArray[index];
}
return binaryProperty.type.unpack(typedArray, index * componentCount);
}
/**
* Sets the value of the property with the given ID. Only properties of the
* instance may be set; parent properties may not be set.
*
* @param {Number} batchId The batchId of the feature
* @param {String} propertyId The case-sensitive ID of the property.
* @param {*} value The value of the property that will be copied.
* @returns {Boolean} true
if the property was set, false
otherwise.
*
* @exception {DeveloperError} when setting an inherited property
* @private
*/
BatchTableHierarchy.prototype.setProperty = function (
batchId,
propertyId,
value
) {
const result = traverseHierarchy(this, batchId, function (
hierarchy,
instanceIndex
) {
const classId = hierarchy._classIds[instanceIndex];
const instanceClass = hierarchy._classes[classId];
const indexInClass = hierarchy._classIndexes[instanceIndex];
const propertyValues = instanceClass.instances[propertyId];
if (defined(propertyValues)) {
//>>includeStart('debug', pragmas.debug);
if (instanceIndex !== batchId) {
throw new DeveloperError(
`Inherited property "${propertyId}" is read-only.`
);
}
//>>includeEnd('debug');
if (defined(propertyValues.typedArray)) {
setBinaryProperty(propertyValues, indexInClass, value);
} else {
propertyValues[indexInClass] = clone(value, true);
}
return true;
}
});
return defined(result);
};
function setBinaryProperty(binaryProperty, index, value) {
const typedArray = binaryProperty.typedArray;
const componentCount = binaryProperty.componentCount;
if (componentCount === 1) {
typedArray[index] = value;
} else {
binaryProperty.type.pack(value, typedArray, index * componentCount);
}
}
/**
* Check if a feature belongs to a class with the given name
*
* @param {Number} batchId The batch ID of the feature
* @param {String} className The name of the class
* @return {Boolean} true
if the feature belongs to the class given by className, or false
otherwise
* @private
*/
BatchTableHierarchy.prototype.isClass = function (batchId, className) {
// PERFORMANCE_IDEA : cache results in the ancestor classes to speed up this check if this area becomes a hotspot
// PERFORMANCE_IDEA : treat class names as integers for faster comparisons
const result = traverseHierarchy(this, batchId, function (
hierarchy,
instanceIndex
) {
const classId = hierarchy._classIds[instanceIndex];
const instanceClass = hierarchy._classes[classId];
if (instanceClass.name === className) {
return true;
}
});
return defined(result);
};
/**
* Get the name of the class a given feature belongs to
*
* @param {Number} batchId The batch ID of the feature
* @return {String} The name of the class this feature belongs to
*/
BatchTableHierarchy.prototype.getClassName = function (batchId) {
const classId = this._classIds[batchId];
const instanceClass = this._classes[classId];
return instanceClass.name;
};