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, }; }