@itwin/core-frontend
Version:
iTwin.js frontend components
427 lines • 21.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 { assert, BeTimePoint, ProcessDetector } from "@itwin/core-bentley";
import { Matrix3d, Point3d, Range3d, Transform, Vector3d, } from "@itwin/core-geometry";
import { Cartographic, ColorDef, GeoCoordStatus } from "@itwin/core-common";
import { BackgroundMapGeometry } from "../BackgroundMapGeometry";
import { IModelApp } from "../IModelApp";
import { GraphicBranch } from "../render/GraphicBranch";
import { MapTile, RealityTile, TileTree, } from "./internal";
/** @internal */
export class TraversalDetails {
queuedChildren = new Array();
childrenSelected = false;
shouldSelectParent = false;
initialize() {
this.queuedChildren.length = 0;
this.childrenSelected = false;
this.shouldSelectParent = false;
}
}
/** @internal */
export class TraversalChildrenDetails {
_childDetails = [];
initialize() {
for (const child of this._childDetails)
child.initialize();
}
getChildDetail(index) {
while (this._childDetails.length <= index)
this._childDetails.push(new TraversalDetails());
return this._childDetails[index];
}
combine(parentDetails) {
parentDetails.queuedChildren.length = 0;
parentDetails.childrenSelected = false;
parentDetails.shouldSelectParent = false;
for (const child of this._childDetails) {
parentDetails.childrenSelected = parentDetails.childrenSelected || child.childrenSelected;
parentDetails.shouldSelectParent = parentDetails.shouldSelectParent || child.shouldSelectParent;
for (const queuedChild of child.queuedChildren)
parentDetails.queuedChildren.push(queuedChild);
}
}
}
/** @internal */
export class TraversalSelectionContext {
selected;
displayedDescendants;
preloadDebugBuilder;
_maxSelectionCount;
preloaded = new Set();
missing = new Array();
get selectionCountExceeded() { return this._maxSelectionCount === undefined ? false : (this.missing.length + this.selected.length) > this._maxSelectionCount; } // Avoid selecting excessive number of tiles.
constructor(selected, displayedDescendants, preloadDebugBuilder, _maxSelectionCount) {
this.selected = selected;
this.displayedDescendants = displayedDescendants;
this.preloadDebugBuilder = preloadDebugBuilder;
this._maxSelectionCount = _maxSelectionCount;
}
selectOrQueue(tile, args, traversalDetails) {
tile.selectSecondaryTiles(args, this);
tile.markUsed(args);
traversalDetails.shouldSelectParent = true;
if (tile.isReady) {
args.markReady(tile);
this.selected.push(tile);
tile.markDisplayed();
this.displayedDescendants.push((traversalDetails.childrenSelected) ? traversalDetails.queuedChildren.slice() : []);
traversalDetails.queuedChildren.length = 0;
traversalDetails.childrenSelected = true;
traversalDetails.shouldSelectParent = false;
}
else if (!tile.isNotFound) {
traversalDetails.queuedChildren.push(tile);
if (!tile.isLoaded)
this.missing.push(tile);
}
}
preload(tile, args) {
if (!this.preloaded.has(tile)) {
if (this.preloadDebugBuilder)
tile.addBoundingGraphic(this.preloadDebugBuilder, ColorDef.red);
tile.markUsed(args);
tile.selectSecondaryTiles(args, this);
this.preloaded.add(tile);
if (!tile.isNotFound && !tile.isLoaded)
this.missing.push(tile);
}
}
select(tiles, args) {
for (const tile of tiles) {
tile.markUsed(args);
this.selected.push(tile);
this.displayedDescendants.push([]);
}
}
}
const scratchCarto = Cartographic.createZero();
const scratchPoint = Point3d.createZero(), scratchOrigin = Point3d.createZero();
const scratchRange = Range3d.createNull();
const scratchX = Vector3d.createZero(), scratchY = Vector3d.createZero(), scratchZ = Vector3d.createZero();
const scratchMatrix = Matrix3d.createZero(), scratchTransform = Transform.createZero();
/** Base class for a [[TileTree]] representing a reality model (e.g., a point cloud or photogrammetry mesh) or 3d terrain with map imagery.
* The tiles within the tree are instances of [[RealityTile]]s.
* @public
*/
export class RealityTileTree extends TileTree {
/** @internal */
traversalChildrenByDepth = [];
/** @internal */
loader;
/** @internal */
yAxisUp;
/** @internal */
cartesianRange;
/** @internal */
cartesianTransitionDistance;
/** @internal */
_gcsConverter;
/** @internal */
_rootTile;
/** @internal */
_rootToEcef;
/** @internal */
_ecefToDb;
/** @internal */
baseUrl;
/** If set to true, tile geometry will be reprojected using the tile's reprojection transform when geometry is collected.
* @internal */
reprojectGeometry;
/** @internal */
constructor(params) {
super(params);
this.loader = params.loader;
this.yAxisUp = true === params.yAxisUp;
this._rootTile = this.createTile(params.rootTile);
this.cartesianRange = BackgroundMapGeometry.getCartesianRange(this.iModel);
this.cartesianTransitionDistance = this.cartesianRange.diagonal().magnitudeXY() * .25; // Transition distance from elliptical to cartesian.
this._gcsConverter = params.gcsConverterAvailable ? params.iModel.geoServices.getConverter("WGS84") : undefined;
if (params.rootToEcef) {
this._rootToEcef = params.rootToEcef;
const dbToRoot = this.iModelTransform.inverse();
if (dbToRoot) {
const dbToEcef = this._rootToEcef.multiplyTransformTransform(dbToRoot);
this._ecefToDb = dbToEcef.inverse();
}
}
this.baseUrl = params.baseUrl;
this.reprojectGeometry = params.reprojectGeometry;
}
/** The mapping of per-feature JSON properties from this tile tree's batch table, if one is defined.
* @beta
*/
get batchTableProperties() {
return this.loader.getBatchIdMap();
}
/** @internal */
get rootTile() { return this._rootTile; }
/** @internal */
get is3d() { return true; }
/** @internal */
get maxDepth() { return this.loader.maxDepth; }
/** @internal */
get minDepth() { return this.loader.minDepth; }
/** @internal */
get isContentUnbounded() { return this.loader.isContentUnbounded; }
/** @internal */
get isTransparent() { return false; }
/** @internal */
_selectTiles(args) { return this.selectRealityTiles(args, []); }
/** @internal */
get viewFlagOverrides() { return this.loader.viewFlagOverrides; }
/** @internal */
get parentsAndChildrenExclusive() { return this.loader.parentsAndChildrenExclusive; }
/** @internal */
createTile(props) { return new RealityTile(props, this); }
/** Collect tiles from this tile tree based on the criteria implemented by `collector`.
* @internal
*/
collectTileGeometry(collector) {
this.rootTile.collectTileGeometry(collector);
}
/** @internal */
prune() {
const olderThan = BeTimePoint.now().minus(this.expirationTime);
this.rootTile.purgeContents(olderThan, !ProcessDetector.isMobileBrowser);
}
/** @internal */
draw(args) {
const displayedTileDescendants = new Array();
const debugControl = args.context.target.debugControl;
const selectBuilder = (debugControl && debugControl.displayRealityTileRanges) ? args.context.createSceneGraphicBuilder() : undefined;
const preloadDebugBuilder = (debugControl && debugControl.displayRealityTilePreload) ? args.context.createSceneGraphicBuilder() : undefined;
const graphicTypeBranches = new Map();
const selectedTiles = this.selectRealityTiles(args, displayedTileDescendants, preloadDebugBuilder);
args.processSelectedTiles(selectedTiles);
let sortIndices;
if (!this.parentsAndChildrenExclusive) {
sortIndices = selectedTiles.map((_x, i) => i);
sortIndices.sort((a, b) => selectedTiles[a].depth - selectedTiles[b].depth);
}
if (args.shouldCollectClassifierGraphics)
this.collectClassifierGraphics(args, selectedTiles);
assert(selectedTiles.length === displayedTileDescendants.length);
for (let i = 0; i < selectedTiles.length; i++) {
const index = sortIndices ? sortIndices[i] : i;
const selectedTile = selectedTiles[index];
const graphics = args.getTileGraphics(selectedTile);
const tileGraphicType = selectedTile.graphicType;
let targetBranch;
if (undefined !== tileGraphicType && tileGraphicType !== args.context.graphicType) {
if (!(targetBranch = graphicTypeBranches.get(tileGraphicType))) {
graphicTypeBranches.set(tileGraphicType, targetBranch = new GraphicBranch(false));
targetBranch.setViewFlagOverrides(args.graphics.viewFlagOverrides);
targetBranch.symbologyOverrides = args.graphics.symbologyOverrides;
}
}
if (!targetBranch)
targetBranch = args.graphics;
if (undefined !== graphics) {
const displayedDescendants = displayedTileDescendants[index];
if (0 === displayedDescendants.length || !this.loader.parentsAndChildrenExclusive || selectedTile.allChildrenIncluded(displayedDescendants)) {
targetBranch.add(graphics);
if (selectBuilder)
selectedTile.addBoundingGraphic(selectBuilder, ColorDef.green);
}
else {
if (selectBuilder)
selectedTile.addBoundingGraphic(selectBuilder, ColorDef.red);
for (const displayedDescendant of displayedDescendants) {
const clipVector = displayedDescendant.getContentClip();
if (selectBuilder)
displayedDescendant.addBoundingGraphic(selectBuilder, ColorDef.blue);
if (undefined === clipVector) {
targetBranch.add(graphics);
}
else {
clipVector.transformInPlace(args.location);
if (!this.isTransparent)
for (const primitive of clipVector.clips)
for (const clipPlanes of primitive.fetchClipPlanesRef().convexSets)
for (const plane of clipPlanes.planes)
plane.offsetDistance(-displayedDescendant.radius * .05); // Overlap with existing (high resolution) tile slightly to avoid cracks.
const branch = new GraphicBranch(false);
branch.add(graphics);
const clipVolume = args.context.target.renderSystem.createClipVolume(clipVector);
targetBranch.add(args.context.createGraphicBranch(branch, Transform.createIdentity(), { clipVolume }));
}
}
}
if (preloadDebugBuilder)
targetBranch.add(preloadDebugBuilder.finish());
if (selectBuilder)
targetBranch.add(selectBuilder.finish());
const rangeGraphic = selectedTile.getRangeGraphic(args.context);
if (undefined !== rangeGraphic)
targetBranch.add(rangeGraphic);
}
}
args.drawGraphics();
for (const graphicTypeBranch of graphicTypeBranches) {
args.drawGraphicsWithType(graphicTypeBranch[0], graphicTypeBranch[1]);
}
}
/** @internal */
collectClassifierGraphics(args, selectedTiles) {
const classifier = args.context.planarClassifiers.get(this.modelId);
if (classifier)
classifier.collectGraphics(args.context, { modelId: this.modelId, tiles: selectedTiles, location: args.location, isPointCloud: this.isPointCloud });
}
/** @internal */
getTraversalChildren(depth) {
while (this.traversalChildrenByDepth.length <= depth)
this.traversalChildrenByDepth.push(new TraversalChildrenDetails());
return this.traversalChildrenByDepth[depth];
}
/** @internal */
doReprojectChildren(tile) {
if (!(tile instanceof RealityTile) || this._gcsConverter === undefined || this._rootToEcef === undefined || undefined === this._ecefToDb)
return false;
const tileRange = this.iModelTransform.isIdentity ? tile.range : this.iModelTransform.multiplyRange(tile.range, scratchRange);
return this.cartesianRange.intersectsRange(tileRange);
}
/** @internal */
reprojectAndResolveChildren(parent, children, resolve) {
if (!this.doReprojectChildren(parent)) {
resolve(children);
return;
}
const ecefToDb = this._ecefToDb; // Tested for undefined in doReprojectChildren
const rootToDb = this.iModelTransform;
const dbToEcef = ecefToDb.inverse();
const reprojectChildren = new Array();
for (const child of children) {
const realityChild = child;
const childRange = rootToDb.multiplyRange(realityChild.contentRange, scratchRange);
const dbCenter = childRange.center;
const ecefCenter = dbToEcef.multiplyPoint3d(dbCenter);
const dbPoints = [dbCenter, dbCenter.plusXYZ(1), dbCenter.plusXYZ(0, 1), dbCenter.plusXYZ(0, 0, 1)];
reprojectChildren.push({ child: realityChild, ecefCenter, dbPoints });
}
if (reprojectChildren.length === 0)
resolve(children);
else {
const requestProps = new Array();
for (const reprojection of reprojectChildren) {
for (const dbPoint of reprojection.dbPoints) {
const ecefPoint = dbToEcef.multiplyPoint3d(dbPoint);
const carto = Cartographic.fromEcef(ecefPoint, scratchCarto);
if (carto)
requestProps.push({ x: carto.longitudeDegrees, y: carto.latitudeDegrees, z: carto.height });
}
}
if (requestProps.length !== 4 * reprojectChildren.length)
resolve(children);
else {
this._gcsConverter.getIModelCoordinatesFromGeoCoordinates(requestProps).then((response) => {
const reprojectedCoords = response.iModelCoords;
const dbToRoot = rootToDb.inverse();
const getReprojectedPoint = (original, reprojectedXYZ) => {
scratchPoint.setFromJSON(reprojectedXYZ);
const cartesianDistance = this.cartesianRange.distanceToPoint(scratchPoint);
if (cartesianDistance < this.cartesianTransitionDistance)
return scratchPoint.interpolate(cartesianDistance / this.cartesianTransitionDistance, original, scratchPoint);
else
return original;
};
let responseIndex = 0;
for (const reprojection of reprojectChildren) {
if (reprojectedCoords.every((coord) => coord.s === GeoCoordStatus.Success)) {
const reprojectedOrigin = getReprojectedPoint(reprojection.dbPoints[0], reprojectedCoords[responseIndex++].p).clone(scratchOrigin);
const xVector = Vector3d.createStartEnd(reprojectedOrigin, getReprojectedPoint(reprojection.dbPoints[1], reprojectedCoords[responseIndex++].p), scratchX);
const yVector = Vector3d.createStartEnd(reprojectedOrigin, getReprojectedPoint(reprojection.dbPoints[2], reprojectedCoords[responseIndex++].p), scratchY);
const zVector = Vector3d.createStartEnd(reprojectedOrigin, getReprojectedPoint(reprojection.dbPoints[3], reprojectedCoords[responseIndex++].p), scratchZ);
const matrix = Matrix3d.createColumns(xVector, yVector, zVector, scratchMatrix);
if (matrix !== undefined) {
const dbReprojection = Transform.createMatrixPickupPutdown(matrix, reprojection.dbPoints[0], reprojectedOrigin, scratchTransform);
if (dbReprojection) {
const rootReprojection = dbToRoot.multiplyTransformTransform(dbReprojection).multiplyTransformTransform(rootToDb);
reprojection.child.reproject(rootReprojection);
}
}
}
}
resolve(children);
}).catch(() => {
resolve(children); // Error occured in reprojection - just resolve with unprojected corners.
});
}
}
}
/** @internal */
getBaseRealityDepth(_sceneContext) { return -1; }
/** Scan the list of currently selected reality tiles, and fire the viewport's 'onMapLayerScaleRangeVisibilityChanged ' event
* if any scale range visibility change is detected for one more map-layer definition.
* @internal
*/
reportTileVisibility(_args, _selected) { }
/** @internal */
selectRealityTiles(args, displayedDescendants, preloadDebugBuilder) {
this._lastSelected = BeTimePoint.now();
const selected = [];
const context = new TraversalSelectionContext(selected, displayedDescendants, preloadDebugBuilder, args.maxRealityTreeSelectionCount);
const rootTile = this.rootTile;
const debugControl = args.context.target.debugControl;
const freezeTiles = debugControl && debugControl.freezeRealityTiles;
rootTile.selectRealityTiles(context, args, new TraversalDetails());
const baseDepth = this.getBaseRealityDepth(args.context);
if (IModelApp.tileAdmin.isPreloadingAllowed && 0 === context.missing.length) {
if (baseDepth > 0) // Maps may force loading of low level globe tiles.
rootTile.preloadRealityTilesAtDepth(baseDepth, context, args);
if (!freezeTiles)
rootTile.preloadProtectedTiles(args, context);
}
if (!freezeTiles)
for (const tile of context.missing) {
const loadableTile = tile.loadableTile;
loadableTile.markUsed(args);
args.insertMissing(loadableTile);
}
if (debugControl && debugControl.logRealityTiles) {
this.logTiles("Selected: ", selected.values());
const preloaded = [];
for (const tile of context.preloaded)
preloaded.push(tile);
this.logTiles("Preloaded: ", preloaded.values());
this.logTiles("Missing: ", context.missing.values());
const imageryTiles = [];
for (const selectedTile of selected) {
if (selectedTile instanceof MapTile) {
const selectedImageryTiles = (selectedTile).imageryTiles;
if (selectedImageryTiles)
selectedImageryTiles.forEach((tile) => imageryTiles.push(tile));
}
}
if (imageryTiles.length)
this.logTiles("Imagery:", imageryTiles.values());
}
this.reportTileVisibility(args, selected);
IModelApp.tileAdmin.addTilesForUser(args.context.viewport, selected, args.readyTiles, args.touchedTiles);
return selected;
}
/** @internal */
logTiles(label, tiles) {
let depthString = "";
let min = 10000, max = -10000;
let count = 0;
const depthMap = new Map();
for (const tile of tiles) {
count++;
const depth = tile.depth;
min = Math.min(min, tile.depth);
max = Math.max(max, tile.depth);
const found = depthMap.get(depth);
depthMap.set(depth, found === undefined ? 1 : found + 1);
}
depthMap.forEach((value, key) => depthString += `${key}(x${value}), `);
// eslint-disable-next-line no-console
console.log(`${label}: ${count} Min: ${min} Max: ${max} Depths: ${depthString}`);
}
}
//# sourceMappingURL=RealityTileTree.js.map