UNPKG

@itwin/core-frontend

Version:
561 lines • 26.3 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Tiles */ import { dispose } from "@itwin/core-bentley"; import { ClipMaskXYZRangePlanes, ClipShape, ClipVector, Point3d } from "@itwin/core-geometry"; import { Frustum } from "@itwin/core-common"; import { IModelApp } from "../IModelApp"; import { GraphicBranch } from "../render/GraphicBranch"; import { RealityTileRegion, Tile, TileLoadStatus, TileTreeLoadStatus, } from "./internal"; const scratchLoadedChildren = new Array(); const scratchCorners = [Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero()]; const additiveRefinementThreshold = 10000; // Additive tiles (Cesium OSM tileset) are subdivided until their range diagonal falls below this threshold to ensure accurate reprojection. const additiveRefinementDepthLimit = 20; const scratchFrustum = new Frustum(); /** A [[Tile]] within a [[RealityTileTree]], representing part of a reality model (e.g., a point cloud or photogrammetry mesh) or 3d terrain with map imagery. * @public */ export class RealityTile extends Tile { /** @internal */ transformToRoot; /** @internal */ additiveRefinement; /** @internal */ noContentButTerminateOnSelection; /** @internal */ rangeCorners; /** @internal */ region; /** @internal */ _geometry; _everDisplayed = false; /** @internal */ _reprojectionTransform; _reprojectedGraphic; _geometricError; /** @internal */ _copyright; /** @internal */ tree; /** @internal */ get reprojectionTransform() { return this._reprojectionTransform; } /** @internal */ constructor(props, tree) { super(props, tree); this.transformToRoot = props.transformToRoot; this.additiveRefinement = props.additiveRefinement ?? this.realityParent?.additiveRefinement; this.noContentButTerminateOnSelection = props.noContentButTerminateOnSelection; this.rangeCorners = props.rangeCorners; this.region = props.region; this._geometricError = props.geometricError; this.tree = tree; if (undefined === this.transformToRoot) return; // Can transform be non-rigid?? -- if so would have to handle (readonly) radius. this.boundingSphere.transformBy(this.transformToRoot, this.boundingSphere); this.transformToRoot.multiplyRange(this.range, this.range); if (this.rangeCorners) this.transformToRoot.multiplyPoint3dArrayInPlace(this.rangeCorners); if (undefined !== this._contentRange) this.transformToRoot.multiplyRange(this._contentRange, this._contentRange); } /** @internal */ setContent(content) { super.setContent(content); this._geometry = content.geometry; this._copyright = content.copyright; } /** @internal */ freeMemory() { // Prevent freeing if AdditiveRefinementStepChildren are present, since they depend on the parent tile to draw. // This assumes at least one of the step children is currently selected, which is not necessarily the case. Eventually the // normal periodic pruning of expired tiles will clean up that case, but it could be held them in memory longer than necessary. if (!this.realityChildren?.some((child) => child.isStepChild)) super.freeMemory(); } /** @internal */ get realityChildren() { return this.children; } /** @internal */ get realityParent() { return this.parent; } /** @internal */ get realityRoot() { return this.tree; } /** @internal */ get graphicType() { return undefined; } // If undefined, use tree type. /** @internal */ get maxDepth() { return this.realityRoot.loader.maxDepth; } /** @internal */ get isPointCloud() { return this.realityRoot.loader.containsPointClouds; } /** @internal */ get isLoaded() { return this.loadStatus === TileLoadStatus.Ready; } // Reality tiles may depend on secondary tiles (maps) so can ge loaded but not ready. /** A representation of the tile's geometry. * This property is only available when using [[TileGeometryCollector]]. */ get geometry() { return this._geometry; } /** @internal */ get copyright() { return this._copyright; } /** @internal */ get isDisplayable() { if (this.noContentButTerminateOnSelection) return false; else return super.isDisplayable; } /** @internal */ markUsed(args) { args.markUsed(this); } /** @internal */ markDisplayed() { this._everDisplayed = true; } /** @internal */ isOccluded(_viewingSpace) { return false; } /** @internal */ get channel() { return this.realityRoot.loader.getRequestChannel(this); } /** @internal */ async requestContent(isCanceled) { return this.realityRoot.loader.requestTileContent(this, isCanceled); } /** @internal */ useAdditiveRefinementStepchildren() { // Create additive stepchildren only if we are this tile is additive and we are re-projecting and the radius exceeds the additiveRefinementThreshold. // This criteria is currently only met by the Cesium OSM tileset. const rangeDiagonal = this.rangeCorners ? this.rangeCorners[0].distance(this.rangeCorners[3]) : 0; return this.additiveRefinement && this.isDisplayable && rangeDiagonal > additiveRefinementThreshold && this.depth < additiveRefinementDepthLimit && this.realityRoot.doReprojectChildren(this); } /** @internal */ _loadChildren(resolve, reject) { this.realityRoot.loader.loadChildren(this).then((children) => { /* If this is a large tile is to be included additively, but we are re-projecting (Cesium OSM) then we must add step-children to display the geometry as an overly large tile cannot be reprojected accurately. */ if (this.useAdditiveRefinementStepchildren()) this.loadAdditiveRefinementChildren((stepChildren) => { children = children ? children?.concat(stepChildren) : stepChildren; }); if (children) this.realityRoot.reprojectAndResolveChildren(this, children, resolve); /* Potentially reproject and resolve these children */ }).catch((err) => { reject(err); }); } /** @internal */ async readContent(data, system, isCanceled) { return this.realityRoot.loader.loadTileContent(this, data, system, isCanceled); } /** @internal */ computeLoadPriority(viewports, users) { return this.realityRoot.loader.computeTilePriority(this, viewports, users); } /** @internal */ getContentClip() { return ClipVector.createCapture([ClipShape.createBlock(this.contentRange, ClipMaskXYZRangePlanes.All)]); } /** Allow tile to select additional tiles (Terrain Imagery...) * @internal */ selectSecondaryTiles(_args, _context) { } /** An upsampled tile is not loadable - will override to return loadable parent. * @internal */ get loadableTile() { return this; } /** @internal */ preloadRealityTilesAtDepth(depth, context, args) { if (this.depth === depth) { context.preload(this, args); return; } this.loadChildren(); if (undefined !== this.realityChildren) { for (const child of this.realityChildren) child.preloadRealityTilesAtDepth(depth, context, args); } } // Preload tiles that are protected: // * used tiles (where "used" may mean: selected/preloaded for display or content requested); // * parents and siblings of other protected tiles. /** @internal */ preloadProtectedTiles(args, context) { const children = this.realityChildren; let hasProtectedChildren = false; if (children && !this.additiveRefinement) { for (const child of children) { hasProtectedChildren = child.preloadProtectedTiles(args, context) || hasProtectedChildren; } } if (children && hasProtectedChildren) { for (const child of children) { if (child.isDisplayable && !child.isLoaded) context.preload(child, args); } return true; // Parents of protected tiles are protected } // Special case of the root tile if (this === this.realityRoot.rootTile) { context.preload(this, args); return true; } return context.selected.find((tile) => tile === this) !== undefined; } /** @internal */ addBoundingGraphic(builder, color) { builder.setSymbology(color, color, 3); let corners = this.rangeCorners ? this.rangeCorners : this.range.corners(); if (this._reprojectionTransform) corners = this._reprojectionTransform.multiplyPoint3dArray(corners); builder.addRangeBoxFromCorners(corners); } /** @internal */ reproject(rootReprojection) { this._reprojectionTransform = rootReprojection; rootReprojection.multiplyRange(this.range, this.range); this.boundingSphere.transformBy(rootReprojection, this.boundingSphere); if (this.contentRange) rootReprojection.multiplyRange(this.contentRange, this.contentRange); if (this.rangeCorners) rootReprojection.multiplyPoint3dArrayInPlace(this.rangeCorners); } /** @internal */ allChildrenIncluded(tiles) { if (this.children === undefined || tiles.length !== this.children.length) return false; for (const tile of tiles) if (tile.parent !== this) return false; return true; } /** @internal */ getLoadedRealityChildren(args) { if (this._childrenLoadStatus !== TileTreeLoadStatus.Loaded || this.realityChildren === undefined) return false; for (const child of this.realityChildren) { if (child.isReady && child.computeVisibilityFactor(args) > 0) { scratchLoadedChildren.push(child); } else if (!child.getLoadedRealityChildren(args)) { return false; } } return true; } /** @internal */ forceSelectRealityTile() { return false; } /** @internal */ minimumVisibleFactor() { if (this.additiveRefinement) return 0.25; else return 0; } /** @internal */ selectRealityTiles(context, args, traversalDetails) { const visibility = this.computeVisibilityFactor(args); const isNotVisible = visibility < 0; if (isNotVisible) return; // Force loading if loader requires this tile. (cesium terrain visibility). if (this.realityRoot.loader.forceTileLoad(this) && !this.isReady) { context.selectOrQueue(this, args, traversalDetails); return; } // Force to return early without selecting if (visibility >= 1 && this.noContentButTerminateOnSelection) return; const shouldSelectThisTile = visibility >= 1 || this._anyChildNotFound || this.forceSelectRealityTile() || context.selectionCountExceeded; if (shouldSelectThisTile && this.isDisplayable) { // Select this tile // Return early if tile is totally occluded if (this.isOccluded(args.viewingSpace)) return; // Attempt to select this tile. If not ready, queue it context.selectOrQueue(this, args, traversalDetails); // This tile is visible but not loaded - Use higher resolution children if present if (!this.isReady) this.selectRealityChildrenAsFallback(context, args, traversalDetails); } else { // Select children instead of this tile // With additive refinement it is necessary to display this tile along with any displayed children if (this.additiveRefinement && this.isDisplayable && !this.useAdditiveRefinementStepchildren()) context.selectOrQueue(this, args, traversalDetails); this.selectRealityChildren(context, args, traversalDetails); // Children are not ready: use this tile to avoid leaving a hole traversalDetails.shouldSelectParent = traversalDetails.shouldSelectParent || traversalDetails.queuedChildren.length !== 0; if (traversalDetails.shouldSelectParent) { // If the tile has not yet been displayed in this viewport -- display only if it is visible enough. Avoid overly tiles popping into view unexpectedly (terrain) if (visibility > this.minimumVisibleFactor() || this._everDisplayed) { context.selectOrQueue(this, args, traversalDetails); } } } } // Attempt to select the children of a tile in case they could be displayed while this tile is loading. This does not take into account visibility. /** @internal */ selectRealityChildrenAsFallback(context, args, traversalDetails) { const childrenReady = this.getLoadedRealityChildren(args); if (childrenReady) { context.select(scratchLoadedChildren, args); traversalDetails.shouldSelectParent = false; } scratchLoadedChildren.length = 0; } // Recurse through children to select them normally /** @internal */ selectRealityChildren(context, args, traversalDetails) { // Load children if not yet requested const childrenLoadStatus = this.loadChildren(); // NB: asynchronous // Children are not ready yet if (childrenLoadStatus === TileTreeLoadStatus.Loading) { args.markChildrenLoading(); traversalDetails.shouldSelectParent = true; return; } if (this.realityChildren !== undefined) { // Attempt to select the children const traversalChildren = this.realityRoot.getTraversalChildren(this.depth); traversalChildren.initialize(); for (let i = 0; i < this.children.length; i++) this.realityChildren[i].selectRealityTiles(context, args, traversalChildren.getChildDetail(i)); traversalChildren.combine(traversalDetails); } } /** @internal */ purgeContents(olderThan, useProtectedTiles) { const tilesToPurge = new Set(); // Get the list of tiles to purge if (useProtectedTiles && !this.additiveRefinement) this.getTilesToPurge(olderThan, tilesToPurge); else this.getTilesToPurgeWithoutProtection(olderThan, tilesToPurge); // Discard contents of tiles that have been marked. // Note we do not discard the child Tile objects themselves. for (const tile of tilesToPurge) tile.disposeContents(); } // Populate a set with tiles that should be disposed. Prevent some tiles to be disposed to avoid holes when moving. // Return true if the current tile is "protected". getTilesToPurge(olderThan, tilesToPurge) { const children = this.realityChildren; // Protected tiles cannot be purged. They are: // * used tiles (where "used" may mean: selected/preloaded for display or content requested); // * parents and siblings of other protected tiles. let hasProtectedChildren = false; if (children) { for (const child of children) { hasProtectedChildren = child.getTilesToPurge(olderThan, tilesToPurge) || hasProtectedChildren; } if (hasProtectedChildren) { // Siblings of protected tiles are protected too. We need to remove them from it for (const child of children) { // Because the current tile can be invisible, relying on its children to display geometry, // we have to recurse in order to remove the first children that has geometry, otherwise, // some holes might appear child.removeFirstDisplayableChildrenFromSet(tilesToPurge); } return true; // Parents of protected tiles are protected } } const isInUse = this.usageMarker.getIsTileInUse(); if (!isInUse && this.usageMarker.isTimestampExpired(olderThan)) { tilesToPurge.add(this); } return isInUse; } // Populate a set with tiles that should be disposed. Does not prevent some tiles to be disposed to avoid holes when moving. // This method is simpler and more fitting for devices that has a bigger memory constraint, such as mobiles. // However, it causes the apparition of holes by letting important tiles to be purged. getTilesToPurgeWithoutProtection(olderThan, tilesToPurge) { const children = this.realityChildren; if (children) { for (const child of children) { child.getTilesToPurgeWithoutProtection(olderThan, tilesToPurge); } } if (this.usageMarker.isExpired(olderThan)) tilesToPurge.add(this); } removeFirstDisplayableChildrenFromSet(set) { if (set.size === 0) return; if (this.isDisplayable) { set.delete(this); return; } if (this.realityChildren !== undefined) { for (const child of this.realityChildren) child.removeFirstDisplayableChildrenFromSet(set); } } /** @internal */ computeVisibilityFactor(args) { if (this.isEmpty) return -1; if (this.rangeCorners) scratchFrustum.setFromCorners(this.rangeCorners); else Frustum.fromRange(this.range, scratchFrustum); if (this.isFrustumCulled(scratchFrustum, args, true, this.boundingSphere)) return -1; // some nodes are merely for structure and don't have any geometry if (0 === this.maximumSize) return 0; if (this.isLeaf) return this.hasContentRange && this.isContentCulled(args) ? -1 : 1; if (undefined !== this._geometricError) { const radius = args.getTileRadius(this); const center = args.getTileCenter(this); const pixelSize = args.computePixelSizeInMetersAtClosestPoint(center, radius); const sse = this._geometricError / pixelSize; return args.maximumScreenSpaceError / sse; } return this.maximumSize / args.getPixelSize(this); } /** @internal */ get _anyChildNotFound() { if (undefined !== this.children) for (const child of this.children) if (child.isNotFound) return true; return this._childrenLoadStatus === TileTreeLoadStatus.NotFound; } /** @internal */ getSizeProjectionCorners() { if (!this.tree.isContentUnbounded) return undefined; // For a non-global tree use the standard size algorithm. // For global tiles (as in OSM buildings) return the range corners or X-Y corners only if bounded by region- this allows an algorithm that uses the area of the projected corners to attenuate horizon tiles. if (!this.rangeCorners) return this.range.corners(scratchCorners); return this.region ? this.rangeCorners.slice(4) : this.rangeCorners; } /** @internal */ get isStepChild() { return false; } /** @internal */ loadAdditiveRefinementChildren(resolve) { const region = this.region; const corners = this.rangeCorners; if (!region || !corners) return; const maximumSize = this.maximumSize; const rangeDiagonal = corners[0].distance(corners[3]); const isLeaf = rangeDiagonal < additiveRefinementThreshold || this.depth > additiveRefinementDepthLimit; const stepChildren = new Array(); const latitudeDelta = (region.maxLatitude - region.minLatitude) / 2; const longitudeDelta = (region.maxLongitude - region.minLongitude) / 2; const minHeight = region.minHeight; const maxHeight = region.maxHeight; for (let i = 0, minLongitude = region.minLongitude, step = 0; i < 2; i++, minLongitude += longitudeDelta, step++) { for (let j = 0, minLatitude = region.minLatitude; j < 2; j++, minLatitude += latitudeDelta) { const childRegion = new RealityTileRegion({ minLatitude, maxLatitude: minLatitude + latitudeDelta, minLongitude, maxLongitude: minLongitude + longitudeDelta, minHeight, maxHeight }); const childRange = childRegion.getRange(); const contentId = `${this.contentId}_S${step++}`; const childParams = { rangeCorners: childRange.corners, contentId, range: childRange.range, maximumSize, parent: this, additiveRefinement: false, isLeaf, region: childRegion }; stepChildren.push(new AdditiveRefinementStepChild(childParams, this.realityRoot)); } } resolve(stepChildren); } /** @internal */ produceGraphics() { if (undefined === this._reprojectionTransform) return super.produceGraphics(); if (undefined === this._reprojectedGraphic && undefined !== this._graphic) { const branch = new GraphicBranch(false); branch.add(this._graphic); this._reprojectedGraphic = IModelApp.renderSystem.createGraphicBranch(branch, this._reprojectionTransform); } return this._reprojectedGraphic; } /** @internal */ get unprojectedGraphic() { return this._graphic; } /** @internal */ disposeContents() { super.disposeContents(); this._reprojectedGraphic = dispose(this._reprojectedGraphic); } /** @internal */ collectTileGeometry(collector) { const status = collector.collectTile(this); switch (status) { case "reject": return; case "continue": if (!this.isLeaf && !this._anyChildNotFound) { const childrenLoadStatus = this.loadChildren(); if (TileTreeLoadStatus.Loading === childrenLoadStatus) { collector.markLoading(); } else if (undefined !== this.realityChildren && !this._anyChildNotFound) { for (const child of this.realityChildren) child.collectTileGeometry(collector); } break; } // else fall through to "accept" // eslint-disable-next-line no-fallthrough case "accept": if (!this.isReady) collector.addMissingTile(this.loadableTile); else if (this.geometry?.polyfaces) collector.polyfaces.push(...this.geometry.polyfaces); break; } } } /** When additive refinement is used (as in the Cesium OSM tileset) it is not possible to accurately reproject very large, low level tiles * In this case we create additional "step" children (grandchildren etc. ) that will clipped portions display the their ancestor's additive geometry. * These step children are subdivided until they are small enough to be accurately reprojected - this is controlled by the additiveRefinementThreshold (currently 2KM). * The stepchildren do not contain any tile graphics - they just create a branch with clipping and reprojection to display their additive refinement ancestor graphics. */ class AdditiveRefinementStepChild extends RealityTile { get isStepChild() { return true; } _loadableTile; constructor(props, tree) { super(props, tree); this._loadableTile = this.realityParent; for (; this._loadableTile && this._loadableTile.isStepChild; this._loadableTile = this._loadableTile.realityParent) ; } get loadableTile() { return this._loadableTile; } get isLoading() { return this._loadableTile.isLoading; } get isQueued() { return this._loadableTile.isQueued; } get isNotFound() { return this._loadableTile.isNotFound; } get isReady() { return this._loadableTile.isReady; } get isLoaded() { return this._loadableTile.isLoaded; } get isEmpty() { return false; } produceGraphics() { if (undefined === this._graphic) { const parentGraphics = this._loadableTile.unprojectedGraphic; if (!parentGraphics || !this._reprojectionTransform) return undefined; const branch = new GraphicBranch(false); branch.add(parentGraphics); const renderSystem = IModelApp.renderSystem; const branchOptions = {}; if (this.rangeCorners) { const clipPolygon = [this.rangeCorners[0], this.rangeCorners[1], this.rangeCorners[3], this.rangeCorners[2]]; branchOptions.clipVolume = renderSystem.createClipVolume(ClipVector.create([ClipShape.createShape(clipPolygon, undefined, undefined, this.tree.iModelTransform)])); } this._graphic = renderSystem.createGraphicBranch(branch, this._reprojectionTransform, branchOptions); } return this._graphic; } markUsed(args) { args.markUsed(this); args.markUsed(this._loadableTile); } _loadChildren(resolve, _reject) { this.loadAdditiveRefinementChildren((stepChildren) => { if (stepChildren) this.realityRoot.reprojectAndResolveChildren(this, stepChildren, resolve); }); } } //# sourceMappingURL=RealityTile.js.map