UNPKG

@itwin/core-frontend

Version:
427 lines • 21.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 { 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