UNPKG

@loaders.gl/3d-tiles

Version:

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

463 lines (412 loc) 16.2 kB
// loaders.gl // SPDX-License-Identifier: MIT // Copyright vis.gl contributors import type {Availability, Tile3DBoundingVolume, Subtree} from '../../../types'; import {Tile3DSubtreeLoader} from '../../../tile-3d-subtree-loader'; import {load} from '@loaders.gl/core'; import {default as log} from '@probe.gl/log'; import {getS2CellIdFromToken, getS2ChildCellId, getS2TokenFromCellId} from '../../utils/s2/index'; import type {S2VolumeInfo} from '../../utils/obb/s2-corners-to-obb'; import {convertS2BoundingVolumetoOBB} from '../../utils/obb/s2-corners-to-obb'; import Long from 'long'; import {Tiles3DLoaderOptions} from '../../../tiles-3d-loader'; import {ImplicitOptions} from '../parse-3d-tile-header'; const QUADTREE_DIVISION_COUNT = 4; const OCTREE_DIVISION_COUNT = 8; const SUBDIVISION_COUNT_MAP = { QUADTREE: QUADTREE_DIVISION_COUNT, OCTREE: OCTREE_DIVISION_COUNT }; /** * S2VolumeBox is an extention of BoundingVolume of type "box" */ export type S2VolumeBox = { /** BoundingVolume of type "box" has the "box" field. S2VolumeBox contains it as well. */ box: number[]; /** s2VolumeInfo provides additional info about the box - specifically the token, min and max height */ s2VolumeInfo: S2VolumeInfo; }; function getChildS2VolumeBox( s2VolumeBox: S2VolumeBox | undefined, index: number, subdivisionScheme: string ): S2VolumeBox | undefined { if (s2VolumeBox?.box) { // Check if the BoundingVolume is of type "box" const cellId: Long = 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: S2VolumeInfo = {...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: S2VolumeInfo = s2VolumeBox.s2VolumeInfo; const delta = s2VolumeInfo.maximumHeight - s2VolumeInfo.minimumHeight; const sizeZ: number = delta / 2.0; // It's a next level (a child) const midZ: number = s2VolumeInfo.minimumHeight + delta / 2.0; s2VolumeInfo.minimumHeight = midZ - sizeZ; s2VolumeInfo.maximumHeight = midZ + sizeZ; break; default: break; } const box = convertS2BoundingVolumetoOBB(s2ChildVolumeInfo); const childS2VolumeBox: S2VolumeBox = { 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: { subtree: Subtree; subtreeData?: {level: number; x: number; y: number; z: number}; parentData?: { mortonIndex: number; localLevel: number; localX: number; localY: number; localZ: number; }; childIndex?: number; implicitOptions: ImplicitOptions; loaderOptions: Tiles3DLoaderOptions; s2VolumeBox?: S2VolumeBox; }) { 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: S2VolumeBox | undefined = 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: Availability | Availability[], index: number ): boolean { let availabilityObject: Availability; 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: {level: number; x: number; y: number; z: number}, options: ImplicitOptions, s2VolumeBox?: 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: Tile3DBoundingVolume = 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: Tile3DBoundingVolume, coordinates: {level: number; x: number; y: number; z: number}, subdivisionScheme: string ): Tile3DBoundingVolume { 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: number; let childMaximumHeight: number; 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: number, lower: number, shift: number): number { 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: string, level: number, x: number, y: number, z: number ): string { 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: {[key: string]: number}): {[key: string]: string} { 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: number, availabilityBuffer: Uint8Array ): boolean { const byteIndex = Math.floor(availabilityIndex / 8); const bitIndex = availabilityIndex % 8; const bitValue = (availabilityBuffer[byteIndex] >> bitIndex) & 1; return bitValue === 1; }