UNPKG

@loaders.gl/3d-tiles

Version:

3D Tiles, an open standard for streaming massive heterogeneous 3D geospatial datasets.

323 lines 15.2 kB
// loaders.gl // SPDX-License-Identifier: MIT // Copyright vis.gl contributors import { Tile3DSubtreeLoader } from "../../../tile-3d-subtree-loader.js"; import { load } from '@loaders.gl/core'; import { default as log } from '@probe.gl/log'; import { getS2CellIdFromToken, getS2ChildCellId, getS2TokenFromCellId } from "../../utils/s2/index.js"; import { convertS2BoundingVolumetoOBB } from "../../utils/obb/s2-corners-to-obb.js"; const QUADTREE_DIVISION_COUNT = 4; const OCTREE_DIVISION_COUNT = 8; const SUBDIVISION_COUNT_MAP = { QUADTREE: QUADTREE_DIVISION_COUNT, OCTREE: OCTREE_DIVISION_COUNT }; function getChildS2VolumeBox(s2VolumeBox, index, subdivisionScheme) { if (s2VolumeBox?.box) { // Check if the BoundingVolume is of type "box" const cellId = getS2CellIdFromToken(s2VolumeBox.s2VolumeInfo.token); const childCellId = getS2ChildCellId(cellId, index); const childToken = getS2TokenFromCellId(childCellId); // Clone object. Note, s2VolumeInfo is a plain object that doesn't contain any nested object. // So, we can use the Spread Operator to make a shallow copy of the object. const s2ChildVolumeInfo = { ...s2VolumeBox.s2VolumeInfo }; s2ChildVolumeInfo.token = childToken; // replace the token with the child's one // In case of QUADTREE the sizeZ should NOT be changed! // https://portal.ogc.org/files/102132 // A quadtree divides space only on the x and y dimensions. // It divides each tile into 4 smaller tiles where the x and y dimensions are halved. // The quadtree z minimum and maximum remain unchanged. switch (subdivisionScheme) { case 'OCTREE': const s2VolumeInfo = s2VolumeBox.s2VolumeInfo; const delta = s2VolumeInfo.maximumHeight - s2VolumeInfo.minimumHeight; const sizeZ = delta / 2.0; // It's a next level (a child) const midZ = s2VolumeInfo.minimumHeight + delta / 2.0; s2VolumeInfo.minimumHeight = midZ - sizeZ; s2VolumeInfo.maximumHeight = midZ + sizeZ; break; default: break; } const box = convertS2BoundingVolumetoOBB(s2ChildVolumeInfo); const childS2VolumeBox = { box, s2VolumeInfo: s2ChildVolumeInfo }; return childS2VolumeBox; } return undefined; } /** * Recursively parse implicit tiles tree * Spec - https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_implicit_tiling * TODO Check out do we able to use Tile3D class as return type here. * * @param subtree - the current subtree. Subtrees contain availability data for <implicitOptions.subtreeLevels>. * Once we go deeper than that many levels, we will need load a child subtree to get further availability data. * @param subtreeData - the coordinates of the current subtree, relative to the root of this implicit tiles tree. * @param parentData - the coordinates of the parent tile, relative to the current subtree. * The overall coordinates of the current tile can be found by combining the coordinates of the current subtree, the parent tile, * and tje single-bit coordinates that can be calculated from the childIndex. * @param childIndex - which child the current tile is of its parent. In the range 0-7 for OCTREE, 0-3 for QUADTREE. * @param implicitOptions - options specified at the root of this implicit tile tree - numbers of levels, URL templates. * @param loaderOptions - see Tiles3DLoaderOptions. */ // eslint-disable-next-line max-statements, complexity export async function parseImplicitTiles(params) { const { subtree, subtreeData = { level: 0, x: 0, y: 0, z: 0 }, parentData = { mortonIndex: 0, localLevel: -1, localX: 0, localY: 0, localZ: 0 }, childIndex = 0, implicitOptions, loaderOptions, s2VolumeBox } = params; const { subdivisionScheme, subtreeLevels, maximumLevel, contentUrlTemplate, subtreesUriTemplate, basePath } = implicitOptions; const tile = { children: [], lodMetricValue: 0, contentUrl: '' }; if (!maximumLevel) { log.once(`Missing 'maximumLevel' or 'availableLevels' property. The subtree ${contentUrlTemplate} won't be loaded...`); return tile; } // Local tile level - relative to the current subtree. const localLevel = parentData.localLevel + 1; // Global tile level - relative to the root tile of this implicit subdivision scheme. const level = subtreeData.level + localLevel; if (level > maximumLevel) { return tile; } const childrenPerTile = SUBDIVISION_COUNT_MAP[subdivisionScheme]; const bitsPerTile = Math.log2(childrenPerTile); // childIndex is in range 0...3 for quadtrees and 0...7 for octrees const lastBitX = childIndex & 0b01; // Get first bit for X const lastBitY = (childIndex >> 1) & 0b01; // Get second bit for Y const lastBitZ = (childIndex >> 2) & 0b01; // Get third bit for Z // Local tile coordinates - relative to the current subtree root. const localX = concatBits(parentData.localX, lastBitX, 1); const localY = concatBits(parentData.localY, lastBitY, 1); const localZ = concatBits(parentData.localZ, lastBitZ, 1); // Global tile coordinates - relative to the implicit-tile-tree root. // Found by combining the local coordinates which are relative to the current subtree, with the subtree coordinates. const x = concatBits(subtreeData.x, localX, localLevel); const y = concatBits(subtreeData.y, localY, localLevel); const z = concatBits(subtreeData.z, localZ, localLevel); const mortonIndex = concatBits(parentData.mortonIndex, childIndex, bitsPerTile); const isChildSubtreeAvailable = localLevel === subtreeLevels && getAvailabilityResult(subtree.childSubtreeAvailability, mortonIndex); // Context to provide the next recursive call. // This context is set up differently depending on whether its time to start a new subtree or not. let nextSubtree; let nextSubtreeData; let nextParentData; let tileAvailabilityIndex; if (isChildSubtreeAvailable) { const subtreePath = `${basePath}/${subtreesUriTemplate}`; const childSubtreeUrl = replaceContentUrlTemplate(subtreePath, level, x, y, z); const childSubtree = await load(childSubtreeUrl, Tile3DSubtreeLoader, loaderOptions); // The next subtree is the newly-loaded child subtree. nextSubtree = childSubtree; // The current tile is actually the root tile in the next subtree, so it has a tileAvailabilityIndex of 0. tileAvailabilityIndex = 0; // The next subtree starts HERE - at the current tile. nextSubtreeData = { level, x, y, z }; // The next parent is also the current tile - so it has local coordinates of 0 relative to the next subtree. nextParentData = { mortonIndex: 0, localLevel: 0, localX: 0, localY: 0, localZ: 0 }; } else { // Continue on with the same subtree as we're using currently. nextSubtree = subtree; // Calculate a tileAvailabilityIndex for the current tile within the current subtree. const levelOffset = (childrenPerTile ** localLevel - 1) / (childrenPerTile - 1); tileAvailabilityIndex = levelOffset + mortonIndex; // The next subtree is the same as the current subtree. nextSubtreeData = subtreeData; // The next parent is the current tile: it has the local coordinates we already calculated. nextParentData = { mortonIndex, localLevel, localX, localY, localZ }; } const isTileAvailable = getAvailabilityResult(nextSubtree.tileAvailability, tileAvailabilityIndex); if (!isTileAvailable) { return tile; } const isContentAvailable = getAvailabilityResult(nextSubtree.contentAvailability, tileAvailabilityIndex); if (isContentAvailable) { tile.contentUrl = replaceContentUrlTemplate(contentUrlTemplate, level, x, y, z); } for (let index = 0; index < childrenPerTile; index++) { const childS2VolumeBox = getChildS2VolumeBox(s2VolumeBox, index, subdivisionScheme); // Recursive calling... const childTile = await parseImplicitTiles({ subtree: nextSubtree, subtreeData: nextSubtreeData, parentData: nextParentData, childIndex: index, implicitOptions, loaderOptions, s2VolumeBox: childS2VolumeBox }); if (childTile.contentUrl || childTile.children.length) { // @ts-ignore tile.children.push(childTile); } } if (tile.contentUrl || tile.children.length) { const coordinates = { level, x, y, z }; const formattedTile = formatTileData(tile, coordinates, implicitOptions, s2VolumeBox); return formattedTile; } return tile; } /** * Check tile availability in the bitstream array * @param availabilityData - tileAvailability / contentAvailability / childSubtreeAvailability object * @param index - index in the bitstream array * @returns */ function getAvailabilityResult(availabilityData, index) { let availabilityObject; if (Array.isArray(availabilityData)) { /** TODO: we don't support `3DTILES_multiple_contents` extension at the moment. * https://github.com/CesiumGS/3d-tiles/blob/main/extensions/3DTILES_implicit_tiling/README.md#multiple-contents * Take first item in the array */ availabilityObject = availabilityData[0]; if (availabilityData.length > 1) { // eslint-disable-next-line no-console log.once('Not supported extension "3DTILES_multiple_contents" has been detected'); } } else { availabilityObject = availabilityData; } if ('constant' in availabilityObject) { return Boolean(availabilityObject.constant); } if (availabilityObject.explicitBitstream) { return getBooleanValueFromBitstream(index, availabilityObject.explicitBitstream); } return false; } /** * Do formatting of implicit tile data. * TODO Check out do we able to use Tile3D class as type here. * * @param tile - tile data to format. * @param coordinates - global tile coordinates (relative to the root of the implicit tile tree). * @param options - options specified at the root of this implicit tile tree - numbers of levels, URL templates. * @param s2VolumeBox - the S2VolumeBox for this particular child, if available. * @returns */ function formatTileData(tile, coordinates, options, s2VolumeBox) { const { basePath, refine, getRefine, lodMetricType, getTileType, rootLodMetricValue, rootBoundingVolume } = options; const uri = tile.contentUrl && tile.contentUrl.replace(`${basePath}/`, ''); const lodMetricValue = rootLodMetricValue / 2 ** coordinates.level; const boundingVolume = s2VolumeBox?.box ? { box: s2VolumeBox.box } : rootBoundingVolume; const boundingVolumeForChildTile = calculateBoundingVolumeForChildTile(boundingVolume, coordinates, options.subdivisionScheme); return { children: tile.children, contentUrl: tile.contentUrl, content: { uri }, id: tile.contentUrl, refine: getRefine(refine), type: getTileType(tile), lodMetricType, lodMetricValue, geometricError: lodMetricValue, transform: tile.transform, boundingVolume: boundingVolumeForChildTile }; } /** * Calculate child bounding volume. * Spec - https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_implicit_tiling#subdivision-rules * @param rootBoundingVolume * @param coordinates * @param subdivisionScheme */ function calculateBoundingVolumeForChildTile(rootBoundingVolume, coordinates, subdivisionScheme) { if (rootBoundingVolume.region) { const { level, x, y, z } = coordinates; const [west, south, east, north, minimumHeight, maximumHeight] = rootBoundingVolume.region; const boundingVolumesCount = 2 ** level; const sizeX = (east - west) / boundingVolumesCount; const [childWest, childEast] = [west + sizeX * x, west + sizeX * (x + 1)]; const sizeY = (north - south) / boundingVolumesCount; const [childSouth, childNorth] = [south + sizeY * y, south + sizeY * (y + 1)]; // In case of QUADTREE the sizeZ should NOT be changed! // https://portal.ogc.org/files/102132 // A quadtree divides space only on the x and y dimensions. // It divides each tile into 4 smaller tiles where the x and y dimensions are halved. // The quadtree z minimum and maximum remain unchanged. let childMinimumHeight; let childMaximumHeight; if (subdivisionScheme === 'OCTREE') { const sizeZ = (maximumHeight - minimumHeight) / boundingVolumesCount; [childMinimumHeight, childMaximumHeight] = [ minimumHeight + sizeZ * z, minimumHeight + sizeZ * (z + 1) ]; } else { [childMinimumHeight, childMaximumHeight] = [minimumHeight, maximumHeight]; } return { region: [childWest, childSouth, childEast, childNorth, childMinimumHeight, childMaximumHeight] }; } if (rootBoundingVolume.box) { return rootBoundingVolume; } throw new Error(`Unsupported bounding volume type ${JSON.stringify(rootBoundingVolume)}`); } /** * Do binary concatenation * @param higher - number to put to higher part of result * @param lower - number to put to lower part of result * @param shift - number of bits to shift lower number */ function concatBits(higher, lower, shift) { return (higher << shift) + lower; } /** * Replace implicit tile content url with real coordinates. * @param templateUrl * @param level * @param x * @param y * @param z */ export function replaceContentUrlTemplate(templateUrl, level, x, y, z) { const mapUrl = generateMapUrl({ level, x, y, z }); return templateUrl.replace(/{level}|{x}|{y}|{z}/gi, (matched) => mapUrl[matched]); } /** * Get Map object for content url generation * @param items */ function generateMapUrl(items) { const mapUrl = {}; for (const key in items) { mapUrl[`{${key}}`] = items[key]; } return mapUrl; } /** * Get boolean value from bistream by index * A boolean value is encoded as a single bit, either 0 (false) or 1 (true). * Multiple boolean values are packed tightly in the same buffer. * These buffers of tightly-packed bits are sometimes referred to as bitstreams. * Spec - https://github.com/CesiumGS/3d-tiles/tree/implicit-revisions/specification/Metadata#booleans * @param availabilitiIndex */ function getBooleanValueFromBitstream(availabilityIndex, availabilityBuffer) { const byteIndex = Math.floor(availabilityIndex / 8); const bitIndex = availabilityIndex % 8; const bitValue = (availabilityBuffer[byteIndex] >> bitIndex) & 1; return bitValue === 1; } //# sourceMappingURL=parse-3d-implicit-tiles.js.map