import { defined, defaultValue } from "../Source/Cesium.js";
import concatTypedArrays from "./concatTypedArrays.js";
import MetadataTester from "./MetadataTester.js";
/**
* Class to generate implicit subtrees for implicit tiling unit tests
* @private
*/
export default function ImplicitTilingTester() {}
/**
* Description of a single availability bitstream
* @typedef {Object} AvailabilityDescription
* @property {String|Number} descriptor Either a string of 0s and 1s representing the bitstream values, or an integer 0 or 1 to indicate a constant value.
* @property {Number} lengthBits How many bits are in the bitstream. This must be specified, even if descriptor is a string of 0s and 1s
* @property {Boolean} isInternal true if an internal bufferView should be created. false indicates the bufferview is stored in an external buffer instead.
* @property {Boolean} [shareBuffer=false] This is only used for content availability. If true, then the content availability will share the same buffer as the tile availaibility, as this is a common optimization
* @property {Boolean} [includeAvailableCount=false] If true, set availableCount
*/
/**
* A description of 3DTILES_metadata properties stored in the subtree.
* @typedef {Object} MetadataDescription
* @property {Boolean} isInternal True if the metadata should be stored in the subtree file, false if the metadata should be stored in an external buffer.
* @property {Object} propertyTables Options to pass into {@link MetadataTester.createPropertyTables} to create the feature table buffer views.
* @private
*/
/**
* A JSON description of a subtree file for easier generation
* @typedef {Object} SubtreeDescription
* @property {AvailabilityDescription} tileAvailability A description of the tile availability bitstream to generate
* @property {AvailabilityDescription} contentAvailability A description of the content availability bitstream to generate
* @property {AvailabilityDescription} childSubtreeAvailability A description of the child subtree availability bitstream to generate
* @property {AvailabilityDescription} other A description of another bitstream. This is not used for availability, but rather to simulate extra buffer views.
* @property {MetadataDescription} [metadata] For testing 3DTILES_metadata, additional options can be passed in here.
* @private
*/
/**
* Results of procedurally generating a subtree.
* @typedef {Object} GeneratedSubtree
* @property {Uint8Array} subtreeBuffer A typed array storing the contents of the subtree file (including the internal buffer)
* @property {Uint8Array} externalBuffer A typed array representing an external .bin file. This is always returned, but it may be an empty typed array.
*/
/**
* Generate a subtree buffer
* @param {SubtreeDescription} subtreeDescription A JSON description of the subtree's structure and values
* @param {Boolean} constantOnly true if all the bitstreams are constant, i.e. no buffers/bufferViews are needed.
* @return {GeneratedSubtree} The procedurally generated subtree and an external buffer.
*
* @example
* var subtreeDescription = {
* tileAvailability: {
* descriptor: 1,
* lengthBits: 5,
* isInternal: true,
* },
* contentAvailability: {
* descriptor: "11000",
* lengthBits: 5,
* isInternal: true,
* },
* childSubtreeAvailability: {
* descriptor: "1111000010100000",
* lengthBits: 16,
* isInternal: true,
* },
* other: {
* descriptor: "101010",
* lengthBits: 6,
* isInternal: false,
* },
* };
* var results = ImplicitTilingTester.generateSubtreeBuffers(
* subtreeDescription
* );
*
* @private
*/
ImplicitTilingTester.generateSubtreeBuffers = function (
subtreeDescription,
constantOnly
) {
constantOnly = defaultValue(constantOnly, false);
// This will be populated by makeBufferViews() and makeBuffers()
var subtreeJson = {};
if (!constantOnly) {
subtreeJson = {
buffers: [],
bufferViews: [],
};
}
var bufferViewsU8 = makeBufferViews(subtreeDescription, subtreeJson);
var buffersU8 = makeBuffers(bufferViewsU8, subtreeJson);
var jsonChunk = makeJsonChunk(subtreeJson);
var binaryChunk = buffersU8.internal;
var header = makeSubtreeHeader(jsonChunk.length, binaryChunk.length);
var subtreeBuffer = concatTypedArrays([header, jsonChunk, binaryChunk]);
return {
subtreeBuffer: subtreeBuffer,
externalBuffer: buffersU8.external,
};
};
function makeBufferViews(subtreeDescription, subtreeJson) {
// Content availability is optional.
var hasContent = defined(subtreeDescription.contentAvailability);
// pass 1: parse availability -------------------------------------------
var parsedAvailability = {
tileAvailability: parseAvailability(subtreeDescription.tileAvailability),
childSubtreeAvailability: parseAvailability(
subtreeDescription.childSubtreeAvailability
),
};
if (hasContent) {
parsedAvailability.contentAvailability = subtreeDescription.contentAvailability.map(
parseAvailability
);
}
// to simulate additional buffer views for metadata or other purposes.
if (defined(subtreeDescription.other)) {
parsedAvailability.other = parseAvailability(subtreeDescription.other);
}
// pass 2: create buffer view JSON and gather typed arrays ------------------
var bufferViewsU8 = {
internal: [],
external: [],
count: 0,
};
var bufferViewJsonArray = [];
gatherBufferViews(
bufferViewsU8,
bufferViewJsonArray,
parsedAvailability.tileAvailability
);
if (hasContent) {
parsedAvailability.contentAvailability.forEach(function (
contentAvailability
) {
gatherBufferViews(
bufferViewsU8,
bufferViewJsonArray,
contentAvailability
);
});
}
gatherBufferViews(
bufferViewsU8,
bufferViewJsonArray,
parsedAvailability.childSubtreeAvailability
);
// to simulate additional buffer views for metadata or other purposes.
if (defined(parsedAvailability.other)) {
gatherBufferViews(
bufferViewsU8,
bufferViewJsonArray,
parsedAvailability.other
);
}
if (bufferViewJsonArray.length > 0) {
subtreeJson.bufferViews = bufferViewJsonArray;
}
// pass 3: update the subtree availability JSON -----------------------------
subtreeJson.tileAvailability =
parsedAvailability.tileAvailability.availabilityJson;
subtreeJson.childSubtreeAvailability =
parsedAvailability.childSubtreeAvailability.availabilityJson;
if (hasContent) {
var contentAvailabilityArray = parsedAvailability.contentAvailability;
if (contentAvailabilityArray.length > 1) {
subtreeJson.extensions = {
"3DTILES_multiple_contents": {
contentAvailability: contentAvailabilityArray.map(function (x) {
return x.availabilityJson;
}),
},
};
} else {
subtreeJson.contentAvailability =
contentAvailabilityArray[0].availabilityJson;
}
}
// pass 4: add metadata buffer views --------------------------------------
if (defined(subtreeDescription.metadata)) {
addMetadata(bufferViewsU8, subtreeJson, subtreeDescription.metadata);
}
// wrap up ----------------------------------------------------------------
return bufferViewsU8;
}
function gatherBufferViews(
bufferViewsU8,
bufferViewJsonArray,
parsedBitstream
) {
if (defined(parsedBitstream.constant)) {
parsedBitstream.availabilityJson = {
constant: parsedBitstream.constant,
availableCount: parsedBitstream.availableCount,
};
} else if (defined(parsedBitstream.shareBuffer)) {
// simplifying assumptions:
// 1. shareBuffer is only used for content availability
// 2. tileAvailability is stored in the first bufferView so it has index 0
parsedBitstream.availabilityJson = {
bufferView: 0,
availableCount: parsedBitstream.availableCount,
};
} else {
var bufferViewId = bufferViewsU8.count;
bufferViewsU8.count++;
var bufferViewJson = {
buffer: undefined,
byteOffset: undefined,
byteLength: parsedBitstream.byteLength,
};
bufferViewJsonArray.push(bufferViewJson);
parsedBitstream.availabilityJson = {
bufferView: bufferViewId,
availableCount: parsedBitstream.availableCount,
};
var bufferView = {
bufferView: parsedBitstream.bitstream,
// save a reference to the object so we can update the offsets and
// lengths later.
json: bufferViewJson,
};
if (parsedBitstream.isInternal) {
bufferViewsU8.internal.push(bufferView);
} else {
bufferViewsU8.external.push(bufferView);
}
}
}
function makeSubtreeHeader(jsonByteLength, binaryByteLength) {
var buffer = new ArrayBuffer(24);
var dataView = new DataView(buffer);
// ASCII 'subt' as a little-endian uint32_t
var MAGIC = 0x74627573;
var littleEndian = true;
var VERSION = 1;
dataView.setUint32(0, MAGIC, littleEndian);
dataView.setUint32(4, VERSION, littleEndian);
// The test data is small, so only the low 32-bits are needed.
dataView.setUint32(8, jsonByteLength, littleEndian);
dataView.setUint32(16, binaryByteLength, littleEndian);
return new Uint8Array(buffer);
}
function makeJsonChunk(json) {
var jsonString = JSON.stringify(json);
// To keep unit tests simple, this assumes ASCII characters. However, UTF-8
// characters are allowed in general.
var jsonByteLength = jsonString.length;
var paddedLength = jsonByteLength;
if (paddedLength % 8 !== 0) {
paddedLength += 8 - (paddedLength % 8);
}
var i;
var buffer = new Uint8Array(paddedLength);
for (i = 0; i < jsonByteLength; i++) {
buffer[i] = jsonString.charCodeAt(i);
}
for (i = jsonByteLength; i < paddedLength; i++) {
buffer[i] = " ".charCodeAt(0);
}
return buffer;
}
function parseAvailability(availability) {
var parsed = parseAvailabilityDescriptor(availability.descriptor);
parsed.isInternal = availability.isInternal;
parsed.shareBuffer = availability.shareBuffer;
if (defined(parsed.constant) && availability.includeAvailableCount) {
// Only set available count to the number of bits if the constant is 1
parsed.availableCount = parsed.constant * availability.lengthBits;
}
// this will be populated by gatherBufferViews()
parsed.availabilityJson = undefined;
return parsed;
}
function parseAvailabilityDescriptor(descriptor, includeAvailableCount) {
if (typeof descriptor === "number") {
return {
constant: descriptor,
};
}
var bits = descriptor.split("").map(function (x) {
return Number(x);
});
var byteLength = Math.ceil(bits.length / 8);
var byteLengthWithPadding = byteLength;
// Add padding if needed
if (byteLengthWithPadding % 8 !== 0) {
byteLengthWithPadding += 8 - (byteLengthWithPadding % 8);
}
var availableCount = 0;
var bitstream = new Uint8Array(byteLength);
for (var i = 0; i < bits.length; i++) {
var bit = bits[i];
availableCount += bit;
var byte = i >> 3;
var bitIndex = i % 8;
bitstream[byte] |= bit << bitIndex;
}
if (!includeAvailableCount) {
availableCount = undefined;
}
return {
byteLength: byteLength,
byteLengthWithPadding: byteLengthWithPadding,
bitstream: bitstream,
availableCount: availableCount,
};
}
function addMetadata(bufferViewsU8, subtreeJson, metadataOptions) {
var propertyTableResults = MetadataTester.createPropertyTables(
metadataOptions.propertyTables
);
// Add bufferViews to the list -----------------------------------
if (!defined(subtreeJson.bufferViews)) {
subtreeJson.bufferViews = [];
}
var bufferViewJsonArray = subtreeJson.bufferViews;
var bufferViewArray = metadataOptions.isInternal
? bufferViewsU8.internal
: bufferViewsU8.external;
var metadataBufferViewsU8 = propertyTableResults.bufferViews;
var metadataBufferViewCount = Object.keys(metadataBufferViewsU8).length;
for (var i = 0; i < metadataBufferViewCount; i++) {
var bufferViewU8 = metadataBufferViewsU8[i];
var bufferViewJson = {
buffer: undefined,
byteOffset: undefined,
byteLength: bufferViewU8.byteLength,
};
bufferViewJsonArray.push(bufferViewJson);
var paddedBufferView = padUint8Array(bufferViewU8);
var bufferView = {
bufferView: paddedBufferView,
// save a reference to the object so we can update the offsets and
// lengths later.
json: bufferViewJson,
};
bufferViewArray.push(bufferView);
}
// Renumber buffer views ----------------------------------------------
// This tester assumes there's a single property table for the tile metadata
var tileTable = propertyTableResults.propertyTables[0];
var tileTableProperties = tileTable.properties;
var firstMetadataIndex = bufferViewsU8.count;
var properties = {};
for (var key in tileTableProperties) {
if (tileTableProperties.hasOwnProperty(key)) {
var property = tileTableProperties[key];
var propertyJson = {
bufferView: property.bufferView + firstMetadataIndex,
};
if (defined(property.stringOffsetBufferView)) {
propertyJson.stringOffsetBufferView =
property.stringOffsetBufferView + firstMetadataIndex;
}
if (defined(property.arrayOffsetBufferView)) {
propertyJson.arrayOffsetBufferView =
property.arrayOffsetBufferView + firstMetadataIndex;
}
properties[key] = propertyJson;
}
}
// Store results in subtree JSON -----------------------------------------
if (!defined(subtreeJson.extensions)) {
subtreeJson.extensions = {};
}
subtreeJson.extensions["3DTILES_metadata"] = {
class: tileTable.class,
properties: properties,
};
}
function padUint8Array(array) {
// if already aligned to 8 bytes, we're done.
if (array.length % 8 === 0) {
return array;
}
var paddingLength = 8 - (array.length % 8);
var padding = new Uint8Array(paddingLength);
return concatTypedArrays([array, padding]);
}
function makeBuffers(bufferViewsU8, subtreeJson) {
var bufferCount = 0;
var byteLength = 0;
var typedArrays = [];
var i;
var bufferView;
for (i = 0; i < bufferViewsU8.internal.length; i++) {
bufferView = bufferViewsU8.internal[i];
typedArrays.push(bufferView.bufferView);
bufferView.json.buffer = bufferCount;
bufferView.json.byteOffset = byteLength;
byteLength += bufferView.bufferView.length;
}
// An internal buffer typed array will always be returned, even if length
// 0. However, don't add json for an unused buffer.
var internalBufferU8 = concatTypedArrays(typedArrays);
if (typedArrays.length > 0) {
subtreeJson.buffers.push({
byteLength: byteLength,
});
bufferCount += 1;
}
// Reset counts
byteLength = 0;
typedArrays = [];
for (i = 0; i < bufferViewsU8.external.length; i++) {
bufferView = bufferViewsU8.external[i];
typedArrays.push(bufferView.bufferView);
bufferView.json.buffer = bufferCount;
bufferView.json.byteOffset = byteLength;
byteLength += bufferView.bufferView.length;
}
var externalBufferU8 = concatTypedArrays(typedArrays);
if (typedArrays.length > 0) {
subtreeJson.buffers.push({
// dummy URI, unit tests should mock any requests.
uri: "external.bin",
byteLength: byteLength,
});
}
return {
internal: internalBufferU8,
external: externalBufferU8,
};
}