@itwin/core-frontend
Version:
iTwin.js frontend components
561 lines • 26.3 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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