import decodeGoogleEarthEnterpriseData from "../Core/decodeGoogleEarthEnterpriseData.js"; import GoogleEarthEnterpriseTileInformation from "../Core/GoogleEarthEnterpriseTileInformation.js"; import RuntimeError from "../Core/RuntimeError.js"; import pako from "../ThirdParty/pako.js"; import createTaskProcessorWorker from "./createTaskProcessorWorker.js"; // Datatype sizes const sizeOfUint16 = Uint16Array.BYTES_PER_ELEMENT; const sizeOfInt32 = Int32Array.BYTES_PER_ELEMENT; const sizeOfUint32 = Uint32Array.BYTES_PER_ELEMENT; const Types = { METADATA: 0, TERRAIN: 1, DBROOT: 2, }; Types.fromString = function (s) { if (s === "Metadata") { return Types.METADATA; } else if (s === "Terrain") { return Types.TERRAIN; } else if (s === "DbRoot") { return Types.DBROOT; } }; function decodeGoogleEarthEnterprisePacket(parameters, transferableObjects) { const type = Types.fromString(parameters.type); let buffer = parameters.buffer; decodeGoogleEarthEnterpriseData(parameters.key, buffer); const uncompressedTerrain = uncompressPacket(buffer); buffer = uncompressedTerrain.buffer; const length = uncompressedTerrain.length; switch (type) { case Types.METADATA: return processMetadata(buffer, length, parameters.quadKey); case Types.TERRAIN: return processTerrain(buffer, length, transferableObjects); case Types.DBROOT: transferableObjects.push(buffer); return { buffer: buffer, }; } } const qtMagic = 32301; function processMetadata(buffer, totalSize, quadKey) { const dv = new DataView(buffer); let offset = 0; const magic = dv.getUint32(offset, true); offset += sizeOfUint32; if (magic !== qtMagic) { throw new RuntimeError("Invalid magic"); } const dataTypeId = dv.getUint32(offset, true); offset += sizeOfUint32; if (dataTypeId !== 1) { throw new RuntimeError("Invalid data type. Must be 1 for QuadTreePacket"); } // Tile format version const quadVersion = dv.getUint32(offset, true); offset += sizeOfUint32; if (quadVersion !== 2) { throw new RuntimeError( "Invalid QuadTreePacket version. Only version 2 is supported." ); } const numInstances = dv.getInt32(offset, true); offset += sizeOfInt32; const dataInstanceSize = dv.getInt32(offset, true); offset += sizeOfInt32; if (dataInstanceSize !== 32) { throw new RuntimeError("Invalid instance size."); } const dataBufferOffset = dv.getInt32(offset, true); offset += sizeOfInt32; const dataBufferSize = dv.getInt32(offset, true); offset += sizeOfInt32; const metaBufferSize = dv.getInt32(offset, true); offset += sizeOfInt32; // Offset from beginning of packet (instances + current offset) if (dataBufferOffset !== numInstances * dataInstanceSize + offset) { throw new RuntimeError("Invalid dataBufferOffset"); } // Verify the packets is all there header + instances + dataBuffer + metaBuffer if (dataBufferOffset + dataBufferSize + metaBufferSize !== totalSize) { throw new RuntimeError("Invalid packet offsets"); } // Read all the instances const instances = []; for (let i = 0; i < numInstances; ++i) { const bitfield = dv.getUint8(offset); ++offset; ++offset; // 2 byte align const cnodeVersion = dv.getUint16(offset, true); offset += sizeOfUint16; const imageVersion = dv.getUint16(offset, true); offset += sizeOfUint16; const terrainVersion = dv.getUint16(offset, true); offset += sizeOfUint16; // Number of channels stored in the dataBuffer offset += sizeOfUint16; offset += sizeOfUint16; // 4 byte align // Channel type offset into dataBuffer offset += sizeOfInt32; // Channel version offset into dataBuffer offset += sizeOfInt32; offset += 8; // Ignore image neighbors for now // Data providers const imageProvider = dv.getUint8(offset++); const terrainProvider = dv.getUint8(offset++); offset += sizeOfUint16; // 4 byte align instances.push( new GoogleEarthEnterpriseTileInformation( bitfield, cnodeVersion, imageVersion, terrainVersion, imageProvider, terrainProvider ) ); } const tileInfo = []; let index = 0; function populateTiles(parentKey, parent, level) { let isLeaf = false; if (level === 4) { if (parent.hasSubtree()) { return; // We have a subtree, so just return } isLeaf = true; // No subtree, so set all children to null } for (let i = 0; i < 4; ++i) { const childKey = parentKey + i.toString(); if (isLeaf) { // No subtree so set all children to null tileInfo[childKey] = null; } else if (level < 4) { // We are still in the middle of the subtree, so add child // only if their bits are set, otherwise set child to null. if (!parent.hasChild(i)) { tileInfo[childKey] = null; } else { if (index === numInstances) { console.log("Incorrect number of instances"); return; } const instance = instances[index++]; tileInfo[childKey] = instance; populateTiles(childKey, instance, level + 1); } } } } let level = 0; const root = instances[index++]; if (quadKey === "") { // Root tile has data at its root and one less level ++level; } else { tileInfo[quadKey] = root; // This will only contain the child bitmask } populateTiles(quadKey, root, level); return tileInfo; } const numMeshesPerPacket = 5; const numSubMeshesPerMesh = 4; // Each terrain packet will have 5 meshes - each containg 4 sub-meshes: // 1 even level mesh and its 4 odd level children. // Any remaining bytes after the 20 sub-meshes contains water surface meshes, // which are ignored. function processTerrain(buffer, totalSize, transferableObjects) { const dv = new DataView(buffer); // Find the sub-meshes. const advanceMesh = function (pos) { for (let sub = 0; sub < numSubMeshesPerMesh; ++sub) { const size = dv.getUint32(pos, true); pos += sizeOfUint32; pos += size; if (pos > totalSize) { throw new RuntimeError("Malformed terrain packet found."); } } return pos; }; let offset = 0; const terrainMeshes = []; while (terrainMeshes.length < numMeshesPerPacket) { const start = offset; offset = advanceMesh(offset); const mesh = buffer.slice(start, offset); transferableObjects.push(mesh); terrainMeshes.push(mesh); } return terrainMeshes; } const compressedMagic = 0x7468dead; const compressedMagicSwap = 0xadde6874; function uncompressPacket(data) { // The layout of this decoded data is // Magic Uint32 // Size Uint32 // [GZipped chunk of Size bytes] // Pullout magic and verify we have the correct data const dv = new DataView(data); let offset = 0; const magic = dv.getUint32(offset, true); offset += sizeOfUint32; if (magic !== compressedMagic && magic !== compressedMagicSwap) { throw new RuntimeError("Invalid magic"); } // Get the size of the compressed buffer - the endianness depends on which magic was used const size = dv.getUint32(offset, magic === compressedMagic); offset += sizeOfUint32; const compressedPacket = new Uint8Array(data, offset); const uncompressedPacket = pako.inflate(compressedPacket); if (uncompressedPacket.length !== size) { throw new RuntimeError("Size of packet doesn't match header"); } return uncompressedPacket; } export default createTaskProcessorWorker(decodeGoogleEarthEnterprisePacket);