"use strict";
/*---------------------------------------------------------------------------------------------
* 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
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.RealityTile = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const core_geometry_1 = require("@itwin/core-geometry");
const core_common_1 = require("@itwin/core-common");
const IModelApp_1 = require("../IModelApp");
const GraphicBranch_1 = require("../render/GraphicBranch");
const internal_1 = require("./internal");
const scratchLoadedChildren = new Array();
const scratchCorners = [core_geometry_1.Point3d.createZero(), core_geometry_1.Point3d.createZero(), core_geometry_1.Point3d.createZero(), core_geometry_1.Point3d.createZero(), core_geometry_1.Point3d.createZero(), core_geometry_1.Point3d.createZero(), core_geometry_1.Point3d.createZero(), core_geometry_1.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 core_common_1.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
 */
class RealityTile extends internal_1.Tile {
    /** Transform to go from tile's local coordinate system to the root tile's corodinate system.
     * @see [[RealityModelTileLoader.findTileInJson]] to see how the transformToRoot is calculated.
     * @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 === internal_1.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 core_geometry_1.ClipVector.createCapture([core_geometry_1.ClipShape.createBlock(this.contentRange, core_geometry_1.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 !== internal_1.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 === internal_1.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 < (0, core_bentley_1.expectDefined)(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
            core_common_1.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 === internal_1.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 internal_1.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_1.GraphicBranch(false);
            branch.add(this._graphic);
            this._reprojectedGraphic = IModelApp_1.IModelApp.renderSystem.createGraphicBranch(branch, this._reprojectionTransform);
        }
        return this._reprojectedGraphic;
    }
    /** @internal */
    get unprojectedGraphic() {
        return this._graphic;
    }
    /** @internal */
    disposeContents() {
        super.disposeContents();
        this._reprojectedGraphic = (0, core_bentley_1.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 (internal_1.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;
        }
    }
}
exports.RealityTile = RealityTile;
/** 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_1.GraphicBranch(false);
            branch.add(parentGraphics);
            const renderSystem = IModelApp_1.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(core_geometry_1.ClipVector.create([(0, core_bentley_1.expectDefined)(core_geometry_1.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