/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Views
 */
import { assert, BeEvent, dispose, Id64, JsonUtils } from "@itwin/core-bentley";
import { Angle, AxisOrder, Constant, Geometry, Map4d, Matrix3d, Plane3dByOriginAndUnitNormal, Point2d, Point3d, Range2d, Range3d, Ray3d, Transform, Vector2d, Vector3d, YawPitchRollAngles, } from "@itwin/core-geometry";
import { Camera, Cartographic, Frustum, GlobeMode, GridOrientationType, IModelReadRpcInterface, Npc, ViewDetails, ViewDetails3d, } from "@itwin/core-common";
import { AuxCoordSystem2dState, AuxCoordSystem3dState, AuxCoordSystemState } from "./AuxCoordSys";
import { DisplayStyle3dState } from "./DisplayStyleState";
import { ElementState } from "./EntityState";
import { Frustum2d } from "./Frustum2d";
import { IModelApp } from "./IModelApp";
import { GeometricModel2dState } from "./ModelState";
import { NotifyMessageDetails, OutputMessagePriority } from "./NotificationManager";
import { StandardView } from "./StandardView";
import { DisclosedTileTreeSet } from "./tile/internal";
import { areaToEyeHeight, areaToEyeHeightFromGcs } from "./ViewGlobalLocation";
import { ViewingSpace } from "./ViewingSpace";
import { ViewPose2d, ViewPose3d } from "./ViewPose";
import { ViewStatus } from "./ViewStatus";
import { EnvironmentDecorations } from "./EnvironmentDecorations";
/** Decorates the viewport with the view's grid. Graphics are cached as long as scene remains valid. */
class GridDecorator {
    constructor(_view) {
        this._view = _view;
        this.useCachedDecorations = true;
    }
    decorate(context) {
        const vp = context.viewport;
        if (!vp.isGridOn)
            return;
        const orientation = this._view.getGridOrientation();
        if (GridOrientationType.AuxCoord < orientation) {
            return; // NEEDSWORK...
        }
        if (GridOrientationType.AuxCoord === orientation) {
            this._view.auxiliaryCoordinateSystem.drawGrid(context);
            return;
        }
        const isoGrid = false;
        const gridsPerRef = this._view.getGridsPerRef();
        const spacing = Point2d.createFrom(this._view.getGridSpacing());
        const origin = Point3d.create();
        const matrix = Matrix3d.createIdentity();
        const fixedRepsAuto = Point2d.create();
        this._view.getGridSettings(vp, origin, matrix, orientation);
        context.drawStandardGrid(origin, matrix, spacing, gridsPerRef, isoGrid, orientation !== GridOrientationType.View ? fixedRepsAuto : undefined);
    }
}
const scratchCorners = Range3d.createNull().corners();
const scratchRay = Ray3d.createZero();
const unitRange2d = Range2d.createXYXY(0, 0, 1, 1);
const scratchRange2d = Range2d.createNull();
const scratchRange2dIntersect = Range2d.createNull();
/** The front-end state of a [[ViewDefinition]] element.
 * A ViewState is typically associated with a [[Viewport]] to display the contents of the view on the screen. A ViewState being displayed by a Viewport is considered to be
 * "attached" to that viewport; a "detached" viewport is not being displayed by any viewport. Because the Viewport modifies the state of its attached ViewState, a ViewState
 * can only be attached to one Viewport at a time. Technically, two Viewports can display two different ViewStates that both use the same [[DisplayStyleState]], but this is
 * discouraged - changes made to the style by one Viewport will affect the contents of the other Viewport.
 * * @see [Views]($docs/learning/frontend/Views.md)
 * @public
 * @extensions
 */
export class ViewState extends ElementState {
    static get className() { return "ViewDefinition"; }
    /** Selects the categories that are display by this ViewState. */
    get categorySelector() {
        return this._categorySelector;
    }
    set categorySelector(selector) {
        if (selector === this._categorySelector)
            return;
        const isAttached = this.isAttachedToViewport;
        this.unregisterCategorySelectorListeners();
        this._categorySelector = selector;
        if (isAttached) {
            this.registerCategorySelectorListeners();
            this.onViewedCategoriesChanged.raiseEvent();
        }
    }
    /** The style that controls how the contents of the view are displayed. */
    get displayStyle() {
        return this._displayStyle;
    }
    set displayStyle(style) {
        if (style === this.displayStyle)
            return;
        if (this.isAttachedToViewport)
            this.onDisplayStyleChanged.raiseEvent(style);
        this._displayStyle = style;
    }
    /** @internal */
    constructor(props, iModel, categoryOrClone, displayStyle) {
        super(props, iModel);
        this._unregisterCategorySelectorListeners = [];
        /** An event raised when the set of categories viewed by this view changes, *only* if the view is attached to a [[Viewport]]. */
        this.onViewedCategoriesChanged = new BeEvent();
        /** An event raised just before assignment to the [[displayStyle]] property, *only* if the view is attached to a [[Viewport]].
         * @see [[DisplayStyleSettings]] for events raised when properties of the display style change.
         */
        this.onDisplayStyleChanged = new BeEvent();
        /** Event raised just before assignment to the [[modelDisplayTransformProvider]] property, *only* if the view is attached to a [[Viewport]].
         * @beta
         */
        this.onModelDisplayTransformProviderChanged = new BeEvent();
        this._maxGlobalScopeFactor = 0;
        this.description = props.description;
        this.isPrivate = props.isPrivate;
        this._displayStyle = displayStyle;
        this._categorySelector = categoryOrClone;
        this._gridDecorator = new GridDecorator(this);
        if (!(categoryOrClone instanceof ViewState)) // is this from the clone method?
            return; // not from clone
        // from clone, 3rd argument is source ViewState
        const source = categoryOrClone;
        this._categorySelector = source.categorySelector.clone();
        this._displayStyle = source.displayStyle.clone();
        this._extentLimits = source._extentLimits;
        this._auxCoordSystem = source._auxCoordSystem;
        this._modelDisplayTransformProvider = source._modelDisplayTransformProvider;
    }
    /** Create a new ViewState object from a set of properties. Generally this is called internally by [[IModelConnection.Views.load]] after the properties
     * have been read from an iModel. But, it can also be used to create a ViewState in memory, from scratch or from properties stored elsewhere.
     */
    static createFromProps(_props, _iModel) { return undefined; }
    /** Serialize this ViewState as a set of properties that can be used to recreate it via [[ViewState.createFromProps]]. */
    toProps() {
        return {
            viewDefinitionProps: this.toJSON(),
            categorySelectorProps: this.categorySelector.toJSON(),
            displayStyleProps: this.displayStyle.toJSON(),
        };
    }
    /** Flags controlling various aspects of this view's [[DisplayStyleState]].
     * @see [DisplayStyleSettings.viewFlags]($common)
     */
    get viewFlags() {
        return this.displayStyle.viewFlags;
    }
    set viewFlags(flags) {
        this.displayStyle.viewFlags = flags;
    }
    /** See [DisplayStyleSettings.analysisStyle]($common). */
    get analysisStyle() {
        return this.displayStyle.settings.analysisStyle;
    }
    /** The [RenderSchedule.Script]($common) that animates the contents of the view, if any.
     * @see [[DisplayStyleState.scheduleScript]].
     */
    get scheduleScript() {
        return this.displayStyle.scheduleScript;
    }
    /** @internal */
    get scheduleScriptReference() {
        return this.displayStyle.scheduleScriptReference; // eslint-disable-line @typescript-eslint/no-deprecated
    }
    /** Get the globe projection mode.
     * @internal
     */
    get globeMode() { return this.displayStyle.globeMode; }
    /** Determine whether this ViewState exactly matches another. */
    equals(other) { return super.equals(other) && this.categorySelector.equals(other.categorySelector) && this.displayStyle.equals(other.displayStyle); }
    /** Convert to JSON representation. */
    toJSON() {
        const json = super.toJSON();
        json.categorySelectorId = this.categorySelector.id;
        json.displayStyleId = this.displayStyle.id;
        json.isPrivate = this.isPrivate;
        json.description = this.description;
        return json;
    }
    /**
     * Populates the hydrateRequest object stored on the ViewState with:
     *  not loaded categoryIds based off of the ViewStates categorySelector.
     *  Auxiliary coordinate system id if valid.
     */
    preload(hydrateRequest) {
        const acsId = this.getAuxiliaryCoordinateSystemId();
        if (Id64.isValid(acsId))
            hydrateRequest.acsId = acsId;
    }
    /** Asynchronously load any required data for this ViewState from the backend.
     * FINAL, No subclass should override load. If additional load behavior is needed, see preload and postload.
     * @note callers should await the Promise returned by this method before using this ViewState.
     * @see [Views]($docs/learning/frontend/Views.md)
     */
    async load() {
        // If the iModel associated with the viewState is a blankConnection,
        // then no data can be retrieved from the backend.
        if (this.iModel.isBlank)
            return;
        const hydrateRequest = {};
        this.preload(hydrateRequest);
        const promises = [
            IModelReadRpcInterface.getClientForRouting(this.iModel.routingContext.token).hydrateViewState(this.iModel.getRpcProps(), hydrateRequest).
                then(async (hydrateResponse) => this.postload(hydrateResponse)),
            this.displayStyle.load(),
        ];
        const subcategories = this.iModel.subcategories.load(this.categorySelector.categories);
        if (undefined !== subcategories)
            promises.push(subcategories.promise.then((_) => { }));
        await Promise.all(promises);
    }
    async postload(hydrateResponse) {
        if (hydrateResponse.acsElementProps)
            this._auxCoordSystem = AuxCoordSystemState.fromProps(hydrateResponse.acsElementProps, this.iModel);
    }
    /** Returns true if all [[TileTree]]s required by this view have been loaded.
     * Note that the map tile trees associated to the viewport rather than the view, to check the
     * map tiles as well call [[Viewport.areAreAllTileTreesLoaded]].
     */
    get areAllTileTreesLoaded() {
        let allLoaded = true;
        this.forEachTileTreeRef((ref) => {
            allLoaded = allLoaded && ref.isLoadingComplete;
        });
        return allLoaded;
    }
    /** Get the name of the [[ViewDefinition]] from which this ViewState originated. */
    get name() {
        return this.code.value;
    }
    /** Get this view's background color. */
    get backgroundColor() {
        return this.displayStyle.backgroundColor;
    }
    /** Query the symbology overrides applied to geometry belonging to a specific subcategory when rendered using this ViewState.
     * @param id The Id of the subcategory.
     * @return The symbology overrides applied to all geometry belonging to the specified subcategory, or undefined if no such overrides exist.
     */
    getSubCategoryOverride(id) {
        return this.displayStyle.getSubCategoryOverride(id);
    }
    /** Query the symbology overrides applied to a model when rendered using this ViewState.
     * @param id The Id of the model.
     * @return The symbology overrides applied to the model, or undefined if no such overrides exist.
     */
    getModelAppearanceOverride(id) {
        return this.displayStyle.settings.getModelAppearanceOverride(id);
    }
    /** @internal */
    isSubCategoryVisible(id) {
        const app = this.iModel.subcategories.getSubCategoryAppearance(id);
        if (undefined === app)
            return false;
        const ovr = this.getSubCategoryOverride(id);
        if (undefined === ovr || undefined === ovr.invisible)
            return !app.invisible;
        return !ovr.invisible;
    }
    /** Returns true if this ViewState is-a [[ViewState2d]] */
    is2d() { return !this.is3d(); }
    /** Returns true if this ViewState is-a [[SheetViewState]] */
    isSheetView() { return false; }
    /** set the center of this view to a new position. */
    setCenter(center) {
        const diff = center.minus(this.getCenter());
        this.setOrigin(this.getOrigin().plus(diff));
    }
    /** Execute a function against each [[TileTreeReference]] associated with this view.
     * @note This may include tile trees not associated with any [[GeometricModelState]] - e.g., context reality data.
     */
    forEachTileTreeRef(func) {
        this.forEachModelTreeRef(func);
        this.displayStyle.forEachTileTreeRef(func);
    }
    /** Disclose *all* TileTrees currently in use by this view. This set may include trees not reported by [[forEachTileTreeRef]] - e.g., those used by view attachments, map-draped terrain, etc.
     * @internal
     */
    discloseTileTrees(trees) {
        this.forEachTileTreeRef((ref) => trees.disclose(ref));
    }
    /** Discloses graphics memory consumed by viewed tile trees and other consumers like view attachments.
     * @internal
     */
    collectStatistics(stats) {
        const trees = new DisclosedTileTreeSet();
        this.discloseTileTrees(trees);
        for (const tree of trees)
            tree.collectStatistics(stats);
        this.collectNonTileTreeStatistics(stats);
    }
    /** Discloses graphics memory consumed by any consumers *other* than viewed tile trees, like view attachments.
     * @internal
     */
    collectNonTileTreeStatistics(_stats) {
        //
    }
    /** @internal */
    createScene(context) {
        this.forEachTileTreeRef((ref) => ref.addToScene(context));
    }
    /** Add view-specific decorations. The base implementation draws the grid. Subclasses must invoke super.decorate()
     * @internal
     */
    decorate(context) {
        this.drawGrid(context);
    }
    /** @internal */
    static getStandardViewMatrix(id) {
        return StandardView.getStandardRotation(id);
    }
    /** Orient this view to one of the [[StandardView]] rotations. */
    setStandardRotation(id) { this.setRotation(ViewState.getStandardViewMatrix(id)); }
    /** Orient this view to one of the [[StandardView]] rotations, if the the view is not viewing the project then the
     * standard rotation is relative to the global position rather than the project.
     */
    setStandardGlobalRotation(id) {
        const worldToView = ViewState.getStandardViewMatrix(id);
        const globeToWorld = this.getGlobeRotation();
        if (globeToWorld)
            return this.setRotation(worldToView.multiplyMatrixMatrix(globeToWorld));
        else
            this.setRotation(worldToView);
    }
    /** Get the target point of the view. If there is no camera, center is returned. */
    getTargetPoint(result) { return this.getCenter(result); }
    /**  Get the point at the geometric center of the view. */
    getCenter(result) {
        const delta = this.getRotation().multiplyTransposeVector(this.getExtents());
        return this.getOrigin().plusScaled(delta, 0.5, result);
    }
    /** @internal */
    drawGrid(context) {
        context.addFromDecorator(this._gridDecorator);
    }
    /** @internal */
    computeWorldToNpc(viewRot, inOrigin, delta, enforceFrontToBackRatio = true) {
        if (viewRot === undefined)
            viewRot = this.getRotation();
        const xVector = viewRot.rowX();
        const yVector = viewRot.rowY();
        const zVector = viewRot.rowZ();
        if (delta === undefined)
            delta = this.getExtents();
        if (inOrigin === undefined)
            inOrigin = this.getOrigin();
        let frustFraction = 1.0;
        let xExtent;
        let yExtent;
        let zExtent;
        let origin;
        // Compute root vectors along edges of view frustum.
        if (this.is3d() && this.isCameraOn) {
            const camera = this.camera;
            const eyeToOrigin = Vector3d.createStartEnd(camera.eye, inOrigin); // vector from origin on backplane to eye
            viewRot.multiplyVectorInPlace(eyeToOrigin); // align with view coordinates.
            const focusDistance = camera.focusDist;
            let zDelta = delta.z;
            let zBack = eyeToOrigin.z; // Distance from eye to backplane.
            let zFront = zBack + zDelta; // Distance from eye to frontplane.
            const nearScale = IModelApp.renderSystem.supportsLogZBuffer ? ViewingSpace.nearScaleLog24 : ViewingSpace.nearScaleNonLog24;
            if (enforceFrontToBackRatio && zFront / zBack < nearScale) {
                // In this case we are running up against the zBuffer resolution limitation (currently 24 bits).
                // Set back clipping plane at 10 kilometer which gives us a front clipping plane about 3 meters.
                // Decreasing the maximumBackClip (MicroStation uses 1 kilometer) will reduce the minimum front
                // clip, but also reduce the back clip (so far geometry may not be visible).
                const maximumBackClip = 10 * Constant.oneKilometer;
                if (-zBack > maximumBackClip) {
                    zBack = -maximumBackClip;
                    eyeToOrigin.z = zBack;
                }
                zFront = zBack * nearScale;
                zDelta = zFront - eyeToOrigin.z;
            }
            // z out back of eye ===> origin z coordinates are negative.  (Back plane more negative than front plane)
            const backFraction = -zBack / focusDistance; // Perspective fraction at back clip plane.
            const frontFraction = -zFront / focusDistance; // Perspective fraction at front clip plane.
            frustFraction = frontFraction / backFraction;
            // delta.x,delta.y are view rectangle sizes at focus distance.  Scale to back plane:
            xExtent = xVector.scale(delta.x * backFraction); // xExtent at back == delta.x * backFraction.
            yExtent = yVector.scale(delta.y * backFraction); // yExtent at back == delta.y * backFraction.
            // Calculate the zExtent in the View coordinate system.
            zExtent = new Vector3d(eyeToOrigin.x * (frontFraction - backFraction), eyeToOrigin.y * (frontFraction - backFraction), zDelta);
            viewRot.multiplyTransposeVectorInPlace(zExtent); // rotate back to root coordinates.
            origin = new Point3d(eyeToOrigin.x * backFraction, // Calculate origin in eye coordinates
            eyeToOrigin.y * backFraction, eyeToOrigin.z);
            viewRot.multiplyTransposeVectorInPlace(origin); // Rotate back to root coordinates
            origin.plus(camera.eye, origin); // Add the eye point.
        }
        else {
            origin = inOrigin;
            xExtent = xVector.scale(delta.x);
            yExtent = yVector.scale(delta.y);
            zExtent = zVector.scale(delta.z ? delta.z : 1.0);
        }
        // calculate the root-to-npc mapping (using expanded frustum)
        return { map: Map4d.createVectorFrustum(origin, xExtent, yExtent, zExtent, frustFraction), frustFraction };
    }
    /** Calculate the world coordinate Frustum from the parameters of this ViewState.
     * @param result Optional Frustum to hold result. If undefined a new Frustum is created.
     * @returns The 8-point Frustum with the corners of this ViewState, or undefined if the parameters are invalid.
     */
    calculateFrustum(result) {
        const val = this.computeWorldToNpc();
        if (undefined === val.map)
            return undefined;
        const box = result ? result.initNpc() : new Frustum();
        val.map.transform1.multiplyPoint3dArrayQuietNormalize(box.points);
        return box;
    }
    calculateFocusCorners() {
        const map = this.computeWorldToNpc().map;
        const focusNpcZ = Geometry.clamp(map.transform0.multiplyPoint3dQuietNormalize(this.getTargetPoint()).z, 0, 1.0);
        const pts = [new Point3d(0.0, 0.0, focusNpcZ), new Point3d(1.0, 0.0, focusNpcZ), new Point3d(0.0, 1.0, focusNpcZ), new Point3d(1.0, 1.0, focusNpcZ)];
        map.transform1.multiplyPoint3dArrayQuietNormalize(pts);
        return pts;
    }
    /** Initialize the origin, extents, and rotation from an existing Frustum
     * This function is commonly used in the implementation of [[ViewTool]]s as follows:
     *  1. Obtain the ViewState's initial frustum.
     *  2. Modify the frustum based on user input.
     *  3. Update the ViewState to match the modified frustum.
     * @param frustum the input Frustum.
     * @param opts for providing onExtentsError
     * @return Success if the frustum was successfully updated, or an appropriate error code.
     */
    setupFromFrustum(inFrustum, opts) {
        const frustum = inFrustum.clone(); // make sure we don't modify input frustum
        frustum.fixPointOrder();
        const frustPts = frustum.points;
        const viewOrg = frustPts[Npc.LeftBottomRear];
        // frustumX, frustumY, frustumZ are vectors along edges of the frustum. They are NOT unit vectors.
        // X and Y should be perpendicular, and Z should be right handed.
        const frustumX = Vector3d.createFrom(frustPts[Npc.RightBottomRear].minus(viewOrg));
        const frustumY = Vector3d.createFrom(frustPts[Npc.LeftTopRear].minus(viewOrg));
        const frustumZ = Vector3d.createFrom(frustPts[Npc.LeftBottomFront].minus(viewOrg));
        const frustMatrix = Matrix3d.createRigidFromColumns(frustumX, frustumY, AxisOrder.XYZ);
        if (!frustMatrix)
            return ViewStatus.InvalidWindow;
        // if we're close to one of the standard views, adjust to it to remove any "fuzz"
        StandardView.adjustToStandardRotation(frustMatrix);
        const xDir = frustMatrix.getColumn(0);
        const yDir = frustMatrix.getColumn(1);
        const zDir = frustMatrix.getColumn(2);
        // set up view Rotation matrix as rows of frustum matrix.
        const viewRot = frustMatrix.inverse();
        if (!viewRot)
            return ViewStatus.InvalidWindow;
        // Left handed frustum?
        const zSize = zDir.dotProduct(frustumZ);
        if (zSize < 0.0)
            return ViewStatus.InvalidWindow;
        const viewDiagRoot = new Vector3d();
        viewDiagRoot.plus2Scaled(xDir, xDir.dotProduct(frustumX), yDir, yDir.dotProduct(frustumY), viewDiagRoot); // vectors on the back plane
        viewDiagRoot.plusScaled(zDir, zSize, viewDiagRoot); // add in z vector perpendicular to x,y
        // use center of frustum and view diagonal for origin. Original frustum may not have been orthogonal
        frustum.getCenter().plusScaled(viewDiagRoot, -0.5, viewOrg);
        // delta is in view coordinates
        const viewDelta = viewRot.multiplyVector(viewDiagRoot);
        const status = this.adjustViewDelta(viewDelta, viewOrg, viewRot, undefined, opts);
        if (ViewStatus.Success !== status)
            return status;
        this.setOrigin(viewOrg);
        this.setExtents(viewDelta);
        this.setRotation(viewRot);
        this._updateMaxGlobalScopeFactor();
        return ViewStatus.Success;
    }
    /** Get or set the largest and smallest values allowed for the extents for this ViewState
     * The default limits vary based on the type of view:
     *   - Spatial and drawing view extents cannot exceed the diameter of the earth.
     *   - Sheet view extents cannot exceed ten times the paper size of the sheet.
     * Explicitly setting the extent limits overrides the default limits.
     * @see [[resetExtentLimits]] to restore the default limits.
     */
    get extentLimits() { return undefined !== this._extentLimits ? this._extentLimits : this.defaultExtentLimits; }
    set extentLimits(limits) { this._extentLimits = limits; }
    /** Resets the largest and smallest values allowed for the extents of this ViewState to their default values.
     * @see [[extentLimits]].
     */
    resetExtentLimits() { this._extentLimits = undefined; }
    setDisplayStyle(style) { this.displayStyle = style; }
    /** Adjust the y dimension of this ViewState so that its aspect ratio matches the supplied value.
     * @internal
     */
    fixAspectRatio(windowAspect) {
        const origExtents = this.getExtents();
        const extents = origExtents.clone();
        extents.y = extents.x / (windowAspect * this.getAspectRatioSkew());
        if (extents.isAlmostEqual(origExtents))
            return;
        // adjust origin by half of the distance we modified extents to keep centered
        const origin = this.getOrigin().clone();
        origin.addScaledInPlace(this.getRotation().multiplyTransposeVector(extents.vectorTo(origExtents, origExtents)), .5);
        this.setOrigin(origin);
        this.setExtents(extents);
    }
    /** @internal */
    outputStatusMessage(status) {
        IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, IModelApp.localization.getLocalizedString(`iModelJs:Viewing.${ViewStatus[status]}`)));
        return status;
    }
    /** @internal */
    adjustViewDelta(delta, origin, rot, aspect, opts) {
        const origDelta = delta.clone();
        let status = ViewStatus.Success;
        const limit = this.extentLimits;
        const limitDelta = (val) => {
            if (val < limit.min) {
                val = limit.min;
                status = ViewStatus.MinWindow;
            }
            else if (val > limit.max) {
                val = limit.max;
                status = ViewStatus.MaxWindow;
            }
            return val;
        };
        delta.x = limitDelta(delta.x);
        delta.y = limitDelta(delta.y);
        if (aspect) { // skip if either undefined or 0
            aspect *= this.getAspectRatioSkew();
            if (delta.x > (aspect * delta.y))
                delta.y = delta.x / aspect;
            else
                delta.x = delta.y * aspect;
        }
        if (!delta.isAlmostEqual(origDelta))
            origin.addScaledInPlace(rot.multiplyTransposeVector(delta.vectorTo(origDelta, origDelta)), .5);
        return (status !== ViewStatus.Success && opts?.onExtentsError) ? opts.onExtentsError(status) : status;
    }
    /** Adjust the aspect ratio of this ViewState so it matches the supplied value. The adjustment is accomplished by increasing one dimension
     * and leaving the other unchanged, depending on the ratio of this ViewState's current aspect ratio to the supplied one. This means the result
     * always shows everything in the current volume, plus potentially more.
     * @note The *automatic* adjustment that happens when ViewStates are used in Viewports **always** adjusts the Y axis (making
     * it potentially smaller). That's so that process can be reversible if the view's aspect ratio changes repeatedly (as happens when panels slide in/out, etc.)
     */
    adjustAspectRatio(aspect) {
        const extents = this.getExtents();
        const origin = this.getOrigin();
        this.adjustViewDelta(extents, origin, this.getRotation(), aspect);
        this.setExtents(extents);
        this.setOrigin(origin);
    }
    /** Set the CategorySelector for this view. */
    setCategorySelector(categories) { this.categorySelector = categories; }
    /** get the auxiliary coordinate system state object for this ViewState. */
    get auxiliaryCoordinateSystem() {
        if (!this._auxCoordSystem)
            this._auxCoordSystem = this.createAuxCoordSystem("");
        return this._auxCoordSystem;
    }
    /** Get the Id of the auxiliary coordinate system for this ViewState */
    getAuxiliaryCoordinateSystemId() {
        return this.details.auxiliaryCoordinateSystemId;
    }
    /** Set or clear the AuxiliaryCoordinateSystem for this view.
     * @param acs the new AuxiliaryCoordinateSystem for this view. If undefined, no AuxiliaryCoordinateSystem will be used.
     */
    setAuxiliaryCoordinateSystem(acs) {
        this._auxCoordSystem = acs;
        this.details.auxiliaryCoordinateSystemId = undefined !== acs ? acs.id : Id64.invalid;
    }
    /** Determine whether the specified Category is displayed in this view */
    viewsCategory(id) {
        return this.categorySelector.isCategoryViewed(id);
    }
    /**  Get the aspect ratio (width/height) of this view */
    getAspectRatio() {
        const extents = this.getExtents();
        return extents.x / extents.y;
    }
    /** Get the aspect ratio skew (x/y, usually 1.0) that is used to exaggerate the y axis of the view. */
    getAspectRatioSkew() {
        return this.details.aspectRatioSkew;
    }
    /** Set the aspect ratio skew (x/y) for this view. To remove aspect ratio skew, pass 1.0 for val. */
    setAspectRatioSkew(val) {
        this.details.aspectRatioSkew = val;
    }
    /** Get the unit vector that points in the view X (left-to-right) direction.
     * @param result optional Vector3d to be used for output. If undefined, a new object is created.
     */
    getXVector(result) { return this.getRotation().getRow(0, result); }
    /** Get the unit vector that points in the view Y (bottom-to-top) direction.
     * @param result optional Vector3d to be used for output. If undefined, a new object is created.
     */
    getYVector(result) { return this.getRotation().getRow(1, result); }
    /** Get the unit vector that points in the view Z (front-to-back) direction.
     * @param result optional Vector3d to be used for output. If undefined, a new object is created.
     */
    getZVector(result) { return this.getRotation().getRow(2, result); }
    /** Set or clear the clipping volume for this view.
     * @param clip the new clipping volume. If undefined, clipping is removed from view.
     * @note The ViewState takes ownership of the supplied ClipVector - it should not be modified after passing it to this function.
     */
    setViewClip(clip) {
        this.details.clipVector = clip;
    }
    /** Get the clipping volume for this view, if defined
     * @note Do *not* modify the returned ClipVector. If you wish to change the ClipVector, clone the returned ClipVector, modify it as desired, and pass the clone to [[setViewClip]].
     */
    getViewClip() {
        return this.details.clipVector;
    }
    /** Set the grid settings for this view */
    setGridSettings(orientation, spacing, gridsPerRef) {
        switch (orientation) {
            case GridOrientationType.WorldYZ:
            case GridOrientationType.WorldXZ:
                if (!this.is3d())
                    return;
                break;
        }
        this.details.gridOrientation = orientation;
        this.details.gridsPerRef = gridsPerRef;
        this.details.gridSpacing = spacing;
    }
    /** Populate the given origin and rotation with information from the grid settings from the grid orientation. */
    getGridSettings(vp, origin, rMatrix, orientation) {
        // start with global origin (for spatial views) and identity matrix
        rMatrix.setIdentity();
        origin.setFrom(vp.view.isSpatialView() ? vp.view.iModel.globalOrigin : Point3d.create());
        switch (orientation) {
            case GridOrientationType.View: {
                const centerWorld = Point3d.create(0.5, 0.5, 0.5);
                vp.npcToWorld(centerWorld, centerWorld);
                rMatrix.setFrom(vp.rotation);
                rMatrix.multiplyXYZtoXYZ(origin, origin);
                origin.z = centerWorld.z;
                rMatrix.multiplyTransposeVectorInPlace(origin);
                break;
            }
            case GridOrientationType.WorldXY:
                break;
            case GridOrientationType.WorldYZ: {
                const rowX = rMatrix.getRow(0);
                const rowY = rMatrix.getRow(1);
                const rowZ = rMatrix.getRow(2);
                rMatrix.setRow(0, rowY);
                rMatrix.setRow(1, rowZ);
                rMatrix.setRow(2, rowX);
                break;
            }
            case GridOrientationType.WorldXZ: {
                const rowX = rMatrix.getRow(0);
                const rowY = rMatrix.getRow(1);
                const rowZ = rMatrix.getRow(2);
                rMatrix.setRow(0, rowX);
                rMatrix.setRow(1, rowZ);
                rMatrix.setRow(2, rowY);
                break;
            }
        }
    }
    /** Get the grid settings for this view */
    getGridOrientation() {
        return this.details.gridOrientation;
    }
    getGridsPerRef() {
        return this.details.gridsPerRef;
    }
    getGridSpacing() {
        return this.details.gridSpacing;
    }
    /** Change the volume that this view displays, keeping its current rotation.
     * @param volume The new volume, in world-coordinates, for the view. The resulting view will show all of worldVolume, by fitting a
     * view-axis-aligned bounding box around it. For views that are not aligned with the world coordinate system, this will sometimes
     * result in a much larger volume than worldVolume.
     * @param aspect The X/Y aspect ratio of the view into which the result will be displayed. If the aspect ratio of the volume does not
     * match aspect, the shorter axis is lengthened and the volume is centered. If aspect is undefined, no adjustment is made.
     * @param options for providing MarginPercent and onExtentsError
     * @note for 2d views, only the X and Y values of volume are used.
     */
    lookAtVolume(volume, aspect, options) {
        const rangeBox = Frustum.fromRange(volume).points;
        this.getRotation().multiplyVectorArrayInPlace(rangeBox);
        return this.lookAtViewAlignedVolume(Range3d.createArray(rangeBox), aspect, options);
    }
    /** Look at a volume of space defined by a range in view local coordinates, keeping its current rotation.
     * @param volume The new volume, in view-aligned coordinates. The resulting view will show all of the volume.
     * @param aspect The X/Y aspect ratio of the view into which the result will be displayed. If the aspect ratio of the volume does not
     * match aspect, the shorter axis is lengthened and the volume is centered. If aspect is undefined, no adjustment is made.
     * @param options for providing MarginPercent and onExtentsError
     * @see lookAtVolume
     */
    lookAtViewAlignedVolume(volume, aspect, options) {
        if (volume.isNull) // make sure volume is valid
            return;
        const viewRot = this.getRotation();
        const newOrigin = volume.low.clone();
        const newDelta = volume.diagonal();
        const minimumDepth = Constant.oneMillimeter;
        if (newDelta.z < minimumDepth) {
            newOrigin.z -= (minimumDepth - newDelta.z) / 2.0;
            newDelta.z = minimumDepth;
        }
        if (this.is3d() && this.isCameraOn) {
            // If the camera is on, the only way to guarantee we can see the entire volume is to set delta at the front plane, not focus plane.
            // That generally causes the view to be too large (objects in it are too small), since we can't tell whether the objects are at
            // the front or back of the view. For this reason, don't attempt to add any "margin" to camera views.
        }
        else if (undefined !== options?.paddingPercent) {
            let left, right, top, bottom;
            const padding = options.paddingPercent;
            if (typeof padding === "number") {
                left = right = top = bottom = padding;
            }
            else {
                left = padding.left ?? 0;
                right = padding.right ?? 0;
                top = padding.top ?? 0;
                bottom = padding.bottom ?? 0;
            }
            const width = newDelta.x;
            const height = newDelta.y;
            newOrigin.x -= left * width;
            newDelta.x += (right + left) * width;
            newOrigin.y -= bottom * height;
            newDelta.y += (top + bottom) * height;
        }
        else if (options?.marginPercent) {
            // compute how much space we'll need for both of X and Y margins in root coordinates
            const margin = options.marginPercent;
            const wPercent = margin.left + margin.right;
            const hPercent = margin.top + margin.bottom;
            const marginHorizontal = wPercent / (1 - wPercent) * newDelta.x;
            const marginVert = hPercent / (1 - hPercent) * newDelta.y;
            // compute left and bottom margins in root coordinates
            const marginLeft = margin.left / (1 - wPercent) * newDelta.x;
            const marginBottom = margin.bottom / (1 - hPercent) * newDelta.y;
            // add the margins to the range
            newOrigin.x -= marginLeft;
            newOrigin.y -= marginBottom;
            newDelta.x += marginHorizontal;
            newDelta.y += marginVert;
        }
        else {
            const origDelta = newDelta.clone();
            newDelta.scale(1.04, newDelta); // default "dilation"
            newOrigin.addScaledInPlace(origDelta.minus(newDelta, origDelta), .5);
        }
        viewRot.multiplyTransposeVectorInPlace(newOrigin);
        if (ViewStatus.Success !== this.adjustViewDelta(newDelta, newOrigin, viewRot, aspect, options))
            return;
        this.setExtents(newDelta);
        this.setOrigin(newOrigin);
        if (!this.is3d())
            return;
        const cameraDef = this.camera;
        cameraDef.validateLens();
        // move the camera back so the entire x,y range is visible at front plane
        const frontDist = newDelta.x / (2.0 * Math.tan(cameraDef.getLensAngle().radians / 2.0));
        const backDist = frontDist + newDelta.z;
        cameraDef.setFocusDistance(frontDist); // do this even if the camera isn't currently on.
        this.centerEyePoint(backDist); // do this even if the camera isn't currently on.
        this.verifyFocusPlane(); // changes delta/origin
    }
    /** Set the rotation of this ViewState to the supplied rotation, by rotating it about a point.
     * @param rotation The new rotation matrix for this ViewState.
     * @param point The point to rotate about. If undefined, use the [[getTargetPoint]].
     */
    setRotationAboutPoint(rotation, point) {
        if (undefined === point)
            point = this.getTargetPoint();
        const inverse = rotation.clone().inverse();
        if (undefined === inverse)
            return;
        const targetMatrix = inverse.multiplyMatrixMatrix(this.getRotation());
        const worldTransform = Transform.createFixedPointAndMatrix(point, targetMatrix);
        const frustum = this.calculateFrustum();
        if (undefined !== frustum) {
            frustum.multiply(worldTransform);
            this.setupFromFrustum(frustum);
        }
    }
    /** Intended strictly as a temporary solution for interactive editing applications, until official support for such apps is implemented.
     * Invalidates tile trees for all specified models (or all viewed models, if none specified), causing subsequent requests for tiles to make new requests to back-end for updated tiles.
     * Returns true if any tile tree was invalidated.
     * @internal
     */
    refreshForModifiedModels(modelIds) {
        let refreshed = false;
        this.forEachModelTreeRef((ref) => {
            const tree = ref.treeOwner.tileTree;
            if (undefined !== tree && (undefined === modelIds || Id64.has(modelIds, tree.modelId))) {
                ref.treeOwner.dispose();
                refreshed = true;
            }
        });
        return refreshed;
    }
    /** Determine whether this ViewState has the same coordinate system as another one.
     * They must be from the same iModel, and view a model in common.
     */
    hasSameCoordinates(other) {
        if (this.iModel !== other.iModel)
            return false;
        // Spatial views view any number of spatial models all sharing one coordinate system.
        if (this.isSpatialView() && other.isSpatialView())
            return true;
        // People sometimes mistakenly stick 2d models into spatial views' model selectors.
        if (this.isSpatialView() || other.isSpatialView())
            return false;
        // Non-spatial views view exactly one model. If they view the same model, they share a coordinate system.
        let allowView = false;
        this.forEachModel((model) => {
            allowView ||= other.viewsModel(model.id);
        });
        return allowView;
    }
    /** Compute the vector in the "up" direction of a specific point in world space.
     * This is typically a unit Z vector. However, if the point is outside of the iModel's project extents and using ellipsoid [[globeMode]], an up-vector
     * will be computed relative to the surface of the ellipsoid at that point.
     */
    getUpVector(point) {
        if (!this.iModel.isGeoLocated || this.globeMode !== GlobeMode.Ellipsoid || this.iModel.projectExtents.containsPoint(point))
            return Vector3d.unitZ();
        const earthCenter = this.iModel.getMapEcefToDb(0).origin;
        const normal = Vector3d.createStartEnd(earthCenter, point);
        normal.normalizeInPlace();
        return normal;
    }
    /** Return true if the view is looking at the current iModel project extents or
     * false if the viewed area do does not include more than one percent of the project.
     */
    getIsViewingProject() {
        if (!this.isSpatialView())
            return false;
        const worldToNpc = this.computeWorldToNpc();
        if (!worldToNpc || !worldToNpc.map)
            return false;
        const expandedRange = this.iModel.projectExtents.clone();
        expandedRange.expandInPlace(10E3);
        const corners = expandedRange.corners(scratchCorners);
        worldToNpc.map.transform0.multiplyPoint3dArrayQuietNormalize(corners);
        scratchRange2d.setNull();
        corners.forEach((corner) => scratchRange2d.extendXY(corner.x, corner.y));
        const intersection = scratchRange2d.intersect(unitRange2d, scratchRange2dIntersect);
        if (!intersection || intersection.isNull)
            return false;
        const area = (intersection.high.x - intersection.low.x) * (intersection.high.y - intersection.low.y);
        return area > 1.0E-2;
    }
    /** If the view is not of the project as determined by [[getIsViewingProject]] then return
     * the rotation from a global reference frame to world coordinates.  The global reference frame includes
     * Y vector towards true north, X parallel to the equator and Z perpendicular to the ellipsoid surface
     */
    getGlobeRotation() {
        if (!this.iModel.isGeoLocated || this.globeMode !== GlobeMode.Ellipsoid || this.getIsViewingProject())
            return undefined;
        const backgroundMapGeometry = this.displayStyle.getBackgroundMapGeometry();
        if (!backgroundMapGeometry)
            return undefined;
        const targetRay = Ray3d.create(this.getCenter(), this.getRotation().rowZ().negate(), scratchRay);
        const earthEllipsoid = backgroundMapGeometry.getEarthEllipsoid();
        const intersectFractions = new Array(), intersectAngles = new Array();
        if (earthEllipsoid.intersectRay(targetRay, intersectFractions, undefined, intersectAngles) < 2)
            return undefined;
        let minIndex = 0, minFraction = -1.0E10;
        for (let i = 0; i < intersectFractions.length; i++) {
            const fraction = intersectFractions[i];
            if (fraction < minFraction) {
                minFraction = fraction;
                minIndex = i;
            }
        }
        const angles = intersectAngles[minIndex];
        const pointAndDeriv = earthEllipsoid.radiansToPointAndDerivatives(angles.longitudeRadians, angles.latitudeRadians, false);
        return Matrix3d.createRigidFromColumns(pointAndDeriv.vectorU, pointAndDeriv.vectorV, AxisOrder.XYZ)?.transpose();
    }
    /** A value that represents the global scope of the view -- a value greater than one indicates that the scope of this view is global (viewing most of Earth). */
    get globalScopeFactor() {
        return this.getExtents().magnitudeXY() / Constant.earthRadiusWGS84.equator;
    }
    /** The maximum global scope is not persistent, but maintained as highest global scope factor. This can be used to determine
     * if the view is of a limited area or if it has ever viewed the entire globe and therefore may be assumed to view it again
     * and therefore may warrant resources for displaying the globe, such as an expanded viewing frustum and preloading globe map tiles.
     * A value greater than one indicates that the viewport has been used to view globally at least once.
     * @internal
     */
    get maxGlobalScopeFactor() { return this._maxGlobalScopeFactor; }
    _updateMaxGlobalScopeFactor() { this._maxGlobalScopeFactor = Math.max(this._maxGlobalScopeFactor, this.globalScopeFactor); }
    /** Return elevation applied to model when displayed. This is strictly relevant to plan projection models.
     * @internal
     */
    getModelElevation(_modelId) { return 0; }
    /** An object that can provide per-model transforms to be applied at display time.
     * @note The transform is used for display purposes only. Operations upon geometry within the model may not take the display transform into account.
     * @see [[computeDisplayTransform]] to compute a full display transform for a model or an element within it, which may include a transform supplied by this provider.
     * @beta
     */
    get modelDisplayTransformProvider() {
        return this._modelDisplayTransformProvider;
    }
    set modelDisplayTransformProvider(provider) {
        if (provider === this.modelDisplayTransformProvider)
            return;
        if (this.isAttachedToViewport)
            this.onModelDisplayTransformProviderChanged.raiseEvent(provider);
        this._modelDisplayTransformProvider = provider;
    }
    /** Compute the transform applied to a model or element at display time, if any.
     * The display transform may be constructed from any combination of the following:
     *  - [PlanProjectionSettings.elevation]($common) applied to plan projection models by [DisplayStyle3dSettings.planProjectionSettings]($common);
     *  - A per-model transform supplied by this view's [[modelDisplayTransformProvider]]; and/or
     *  - A transform applied to an element by an [RenderSchedule.ElementTimeline]($common) defined by this view's [[scheduleScript]].
     *  - A transform from the coordinate space of a [ViewAttachment]($backend)'s view to that of the [[SheetViewState]] in which it is displayed.
     *  - A transform from the coordinate space of a [[SpatialViewState]] to that of the [[DrawingViewState]] in which it is displayed, where the spatial view is attached via a [SectionDrawing]($backend).
     * @param args A description of how to compute the transform.
     * @returns The computed transform, or `undefined` if no display transform is to be applied.
     * @beta
     */
    computeDisplayTransform(args) {
        const elevation = this.getModelElevation(args.modelId);
        const modelTransform = this.modelDisplayTransformProvider?.getModelDisplayTransform(args.modelId);
        // NB: A ModelTimeline can apply a transform to all elements in the model, but no code exists which actually applies that at display time.
        // So for now we continue to only consider the ElementTimeline transform.
        let scriptTransform;
        if (this.scheduleScript && args.elementId) {
            const idPair = Id64.getUint32Pair(args.elementId);
            const modelTimeline = this.scheduleScript.find(args.modelId);
            const elementTimeline = modelTimeline?.getTimelineForElement(idPair.lower, idPair.upper);
            scriptTransform = elementTimeline?.getAnimationTransform(args.timePoint ?? this.displayStyle.settings.timePoint ?? 0);
        }
        if (0 === elevation && !modelTransform && !scriptTransform)
            return undefined;
        const transform = Transform.createTranslationXYZ(0, 0, elevation);
        if (modelTransform?.premultiply)
            modelTransform.transform.multiplyTransformTransform(transform, transform);
        if (modelTransform && !modelTransform.premultiply)
            transform.multiplyTransformTransform(modelTransform.transform, transform);
        if (scriptTransform)
            transform.multiplyTransformTransform(scriptTransform, transform);
        return transform;
    }
    /** Invoked when this view becomes the view displayed by the specified [[Viewport]].
     * A ViewState can be attached to at most **one** Viewport at any given time.
     * This method is invoked automatically by the viewport - there is generally no reason for applications to invoke it directly.
     * @note If you override this method you **must** call `super.attachToViewport`.
     * @throws Error if the view is already attached to any Viewport.
     * @see [[detachFromViewport]] from the inverse operation.
     */
    attachToViewport(_args) {
        if (this.isAttachedToViewport)
            throw new Error("Attempting to attach a ViewState that is already attached to a Viewport");
        this.registerCategorySelectorListeners();
    }
    registerCategorySelectorListeners() {
        const cats = this.categorySelector.observableCategories;
        const event = () => this.onViewedCategoriesChanged.raiseEvent();
        this._unregisterCategorySelectorListeners.push(cats.onAdded.addListener(event));
        this._unregisterCategorySelectorListeners.push(cats.onDeleted.addListener(event));
        this._unregisterCategorySelectorListeners.push(cats.onCleared.addListener(event));
    }
    /** Invoked when this view, previously attached to the specified [[Viewport]] via [[attachToViewport]], is no longer the view displayed by that Viewport.
     * This method is invoked automatically by the viewport - there is generally no reason for applications to invoke it directly.
     * @note If you override this method you **must** call `super.detachFromViewport`.
     * @throws Error if the view is not attached to any Viewport.
     */
    detachFromViewport() {
        if (!this.isAttachedToViewport)
            throw new Error("Attempting to detach a ViewState from a Viewport to which it is not attached.");
        this.unregisterCategorySelectorListeners();
    }
    unregisterCategorySelectorListeners() {
        this._unregisterCategorySelectorListeners.forEach((f) => f());
        this._unregisterCategorySelectorListeners.length = 0;
    }
    /** Returns whether this view is currently being displayed by a [[Viewport]].
     * @public
     */
    get isAttachedToViewport() {
        // In attachToViewport, we register event listeners on the category selector. We remove them in detachFromViewport.
        // So a non-empty list of event listener removal functions indicates we are currently attached to a viewport.
        return this._unregisterCategorySelectorListeners.length > 0;
    }
    /** Returns an iterator over additional Viewports used to construct this view's scene. e.g., those used for ViewAttachments and section drawings.
     * This exists chiefly for display-performance-test-app to determine when all tiles required for the view have been loaded.
     * @internal
     */
    get secondaryViewports() {
        return [];
    }
    /** Find the viewport that renders the contents of the view attachment with the specified element Id into this view.
     * @internal
     */
    getAttachmentViewport(_args) {
        return undefined;
    }
}
/** Defines the state of a view of 3d models.
 * @see [ViewState Parameters]($docs/learning/frontend/views#viewstate-parameters)
 * @public
 * @extensions
 */
export class ViewState3d extends ViewState {
    static get className() { return "ViewDefinition3d"; }
    /** Provides access to optional detail settings for this view. */
    get details() {
        return this._details;
    }
    allow3dManipulations() {
        return this.details.allow3dManipulations;
    }
    /** Set whether [[ViewTool]]s are allowed to operate in 3 dimensions on this view. */
    setAllow3dManipulations(allow) {
        this.details.allow3dManipulations = allow;
    }
    constructor(props, iModel, categories, displayStyle) {
        super(props, iModel, categories, displayStyle);
        this._modelClips = [];
        /** Minimum distance for front plane */
        this.forceMinFrontDist = 0.0;
        this._cameraOn = JsonUtils.asBool(props.cameraOn);
        this.origin = Point3d.fromJSON(props.origin);
        this.extents = Vector3d.fromJSON(props.extents);
        this.rotation = YawPitchRollAngles.fromJSON(props.angles).toMatrix3d();
        assert(this.rotation.isRigid());
        this.camera = new Camera(props.camera);
        // if the camera is on, make sure the eyepoint is centered.
        if (this.is3d() && this.isCameraOn)
            this.centerEyePoint();
        this._details = new ViewDetails3d(this.jsonProperties);
        this._details.onModelClipGroupsChanged.addListener((newGroups) => this.updateModelClips(newGroups));
        this.updateModelClips(this._details.modelClipGroups);
        this._updateMaxGlobalScopeFactor();
    }
    updateModelClips(groups) {
        this._modelClips.length = 0;
        for (const group of groups.groups) {
            const clip = group.clip ? IModelApp.renderSystem.createClipVolume(group.clip) : undefined;
            this._modelClips.push(clip);
        }
    }
    /** @internal */
    getModelClip(modelId) {
        // ###TODO: ViewFlags.clipVolume is for the *view clip* only. Some tiles will want to ignore *all* clips (i.e., section-cut tiles).
        const index = this.details.modelClipGroups.findGroupIndex(modelId);
        return -1 !== index ? this._modelClips[index] : undefined;
    }
    /** Capture a copy of the viewed volume and camera parameters. */
    savePose() { return new ViewPose3d(this); }
    /** See [[ViewState.applyPose]]. */
    applyPose(val) {
        if (val instanceof ViewPose3d) {
            this._cameraOn = val.cameraOn;
            this.setOrigin(val.origin);
            this.setExtents(val.extents);
            this.rotation.setFrom(val.rotation);
            this.camera.setFrom(val.camera);
            this._updateMaxGlobalScopeFactor();
        }
        return this;
    }
    toJSON() {
        const val = super.toJSON();
        val.cameraOn = this._cameraOn;
        val.origin = this.origin;
        val.extents = this.extents;
        val.angles = YawPitchRollAngles.createFromMatrix3d(this.rotation).toJSON();
        val.camera = this.camera;
        return val;
    }
    /** See [[ViewState.is3d]]. */
    is3d() { return true; }
    /** @internal */
    isDrawingView() { return false; }
    get isCameraOn() { return this._cameraOn; }
    get isGlobalView() {
        if (undefined === this.iModel.ecefLocation)
            return false;
        return this.globalScopeFactor >= 1;
    }
    /** A value that represents the global scope of the view -- a value greater than one indicates that the scope of this view is global.
     * @see [[isGlobalView]].
     */
    get globalScopeFactor() {
        const eyeHeight = this.getEyeCartographicHeight();
        return (undefined === eyeHeight) ? (this.extents.magnitudeXY() / Constant.earthRadiusWGS84.equator) : (eyeHeight / ViewState3d._minGlobeEyeHeight);
    }
    /** A value representing the degree to which a view is viewing the globe as opposed to a specific location
     * a value of zero or less indicates that the view is not global, a value between zero and one represent a semi
     * global view.  Values greater than one indicate a global view.
     *
     * A Global view is arbitrarily designated as a camera view with the camera height greater than one fourth of the globe
     * radius or an orthographic view with view diagonal greater than one fourth of the globe radius.
     */
    globalViewTransition() {
        if (undefined === this.iModel.ecefLocation)
            return 0.0;
        let h = 0.0;
        if (this.isCameraOn) {
            const carto = this.rootToCartographic(this.getEyePoint(), ViewState3d._scratchGlobeCarto);
            h = (undefined === carto ? 0.0 : carto.height);
        }
        else
            h = this.extents.magnitudeXY();
        const startTransition = 0.33333 * ViewState3d._minGlobeEyeHeight;
        if (h > ViewState3d._minGlobeEyeHeight)
            return 1.0;
        else if (h < startTransition)
            return 0.0;
        else
            return (h - startTransition) / (ViewState3d._minGlobeEyeHeight - startTransition);
    }
    getCartographicHeight(point) {
        const ecefLocation = this.iModel.ecefLocation;
        if (undefined === ecefLocation)
            return undefined;
        const carto = this.rootToCartographic(point, ViewState3d._scratchGlobeCarto);
        return carto === undefined ? undefined : carto.height;
    }
    getEyeCartographicHeight() {
        return this.isCameraOn ? this.getCartographicHeight(this.getEyePoint()) : undefined;
    }
    isEyePointGlobalView(eyePoint) {
        const cartoHeight = this.getCartographicHeight(eyePoint);
        return undefined === cartoHeight ? false : cartoHeight > ViewState3d._minGlobeEyeHeight;
    }
    /** Look at a global location, placing the camera's eye at the specified eye height above a viewed location.
     *  If location is defined, its center position will be viewed using the specified eye height.
     *  If location also has an area specified, the eye height will be adjusted to view the specified location based on that area.
     *  Otherwise, this function views a point on the earth as if the current eye point was placed on the earth. If the eyePoint parameter is defined, instead this point will be placed on the earth and viewed.
     *  Specify pitchAngleRadians to tilt the final view; this defaults to 0.
     *  Returns the distance from original eye point to new eye point.
     *  @public
     */
    lookAtGlobalLocation(eyeHeight, pitchAngleRadians = 0, location, eyePoint) {
        if (!this.iModel.isGeoLocated)
            return 0;
        if (location !== undefined && location.area !== undefined)
            eyeHeight = areaToEyeHeight(this, location.area, location.center.height);
        const origEyePoint = eyePoint !== undefined ? eyePoint.clone() : this.getEyeOrOrthographicViewPoint().clone();
        let targetPoint = origEyePoint;
        const targetPointCartographic = location !== undefined ? location.center.clone() : this.rootToCartographic(targetPoint);
        targetPointCartographic.height = 0.0;
        targetPoint = this.cartographicToRoot(targetPointCartographic);
        targetPointCartographic.height = eyeHeight;
        const lEyePoint = this.cartographicToRoot(targetPointCartographic);
        return this.finishLookAtGlobalLocation(targetPointCartographic, origEyePoint, lEyePoint, targetPoint, pitchAngleRadians);
    }
    /** Look at a global location, placing the camera's eye at the specified eye height above a viewed location using the GCS.
     *  If location is defined, its center position will be viewed using the specified eye height.
     *  If location also has an area specified, the eye height will be adjusted to view the specified location based on that area.
     *  Otherwise, this function views a point on the earth as if the current eye point was placed on the earth. If the eyePoint parameter is defined, instead this point will be placed on the earth and viewed.
     *  Specify pitchAngleRadians to tilt the final view; this defaults to 0.
     *  Returns the distance from original eye point to new eye point.
     *  @public
     */
    async lookAtGlobalLocationFromGcs(eyeHeight, pitchAngleRadians = 0, location, eyePoint) {
        if (!this.iModel.isGeoLocated)
            return 0;
        if (location !== undefined && location.area !== undefined)
            eyeHeight = await areaToEyeHeightFromGcs(this, location.area, location.center.height);
        const origEyePoint = eyePoint !== undefined ? eyePoint.clone() : this.getEyeOrOrthographicViewPoint().clone();
        let targetPoint = origEyePoint;
        const targetPointCartographic = location !== undefined ? location.center.clone() : this.rootToCartographic(targetPoint);
        targetPointCartographic.height = 0.0;
        targetPoint = (await this.cartographicToRootFromGcs(targetPointCartographic));
        targetPointCartographic.height = eyeHeight;
        const lEyePoint = (await this.cartographicToRootFromGcs(targetPointCartographic));
        return this.finishLookAtGlobalLocation(targetPointCartographic, origEyePoint, lEyePoint, targetPoint, pitchAngleRadians);
    }
    finishLookAtGlobalLocation(targetPointCartographic, origEyePoint, eyePoint, targetPoint, pitchAngleRadians) {
        targetPointCartographic.latitude += .001;
        const northOfEyePoint = this.cartographicToRoot(targetPointCartographic);
        let upVector = targetPoint.unitVectorTo(northOfEyePoint);
        if (this.globeMode === GlobeMode.Plane)
            upVector = Vector3d.create(Math.abs(upVector.x), Math.abs(upVector.y), Math.abs(upVector.z));
        if (0 !== pitchAngleRadians) {
            const pitchAxis = upVector.unitCrossProduct(Vector3d.createStartEnd(targetPoint, eyePoint));
            if (undefined !== pitchAxis) {
                const pitchMatrix = Matrix3d.createRotationAroundVector(pitchAxis, Angle.createRadians(pitchAngleRadians));
                const pitchTransform = Transform.createFixedPointAndMatrix(targetPoint, pitchMatrix);
                eyePoint = pitchTransform.multiplyPoint3d(eyePoint);
                pitchMatrix.multiplyVector(upVector, upVector);
            }
        }
        const isCameraEnabled = this.isCameraOn;
        this.lookAt({ eyePoint, targetPoint, upVector, lensAngle: this.camera.getLensAngle() });
        if (!isCameraEnabled && this.isCameraOn)
            this.turnCameraOff();
        return eyePoint.distance(origEyePoint);
    }
    /** Convert a point in spatial coordinates to a cartographic coordinate. */
    rootToCartographic(root, result) {
        const backgroundMapGeometry = this.displayStyle.getBackgroundMapGeometry();
        return backgroundMapGeometry ? backgroundMapGeometry.dbToCartographic(root, result) : undefined;
    }
    /** Convert a cartographic coordinate to a point in spatial coordinates. */
    cartographicToRoot(cartographic, result) {
        const backgroundMapGeometry = this.displayStyle.getBackgroundMapGeometry();
        return backgroundMapGeometry ? backgroundMapGeometry.cartographicToDb(cartographic, result) : undefined;
    }
    /** Convert a point in spatial coordinates to a cartographic coordinate using the GCS reprojection.
     * @see [[rootToCartographicUsingGcs]] to convert multiple points at once.
     * @see [[cartographicToRootFromGcs]] for the inverse conversion.
     */
    async rootToCartographicFromGcs(root, result) {
        const backgroundMapGeometry = this.displayStyle.getBackgroundMapGeometry();
        if (!backgroundMapGeometry)
            return undefined;
        const carto = (await backgroundMapGeometry.dbToCartographicFromGcs([root]))[0];
        return carto.clone(result);
    }
    /** Convert spatial coordinates to cartographic coordinates using the GCS reprojection.
     * @param root Spatial coordinates to be converted
     * @returns the converted coordinates of the same length and order as `root`, or `undefined` if the conversion cannot be performed.
     * @see [[cartographicToRootUsingGcs]] for the inverse conversion.
     */
    async rootToCartographicUsingGcs(root) {
        const bgmap = this.displayStyle.getBackgroundMapGeometry();
        return bgmap?.dbToCartographicFromGcs(root);
    }
    /** Convert a cartographic coordinate to a point in spatial coordinates using the GCS reprojection.
     * @see [[cartographicToRootUsingGcs]] to convert multiple points at once.
     * @see [[rootToCartographicFromGcs]] for the inverse conversion.
     */
    async cartographicToRootFromGcs(cartographic, result) {
        const backgroundMapGeometry = this.displayStyle.getBackgroundMapGeometry();
        if (!backgroundMapGeometry)
            return undefined;
        const root = (await backgroundMapGeometry.cartographicToDbFromGcs([cartographic]))[0];
        return root.clone(result);
    }
    /** Convert cartographic coordinates to spatial coordinates using the GCS reprojection.
     * @param cartographic Cartographic coordinates to be converted
     * @returns the converted coordinates of the same length and order as `cartographic`, or `undefined` if the conversion cannot be performed.
     * @see [[rootToCartographicUsingGcs]] for the inverse conversion.
     */
    async cartographicToRootUsingGcs(cartographic) {
        const bgmap = this.displayStyle.getBackgroundMapGeometry();
        return bgmap?.cartographicToDbFromGcs(cartographic);
    }
    setupFromFrustum(frustum, opts) {
        const stat = super.setupFromFrustum(frustum, opts);
        if (ViewStatus.Success !== stat)
            return stat;
        this.turnCameraOff();
        const frustPts = frustum.points;
        // use comparison of back, front plane X sizes to indicate camera or flat view ...
        const xBack = frustPts[Npc.LeftBottomRear].distance(frustPts[Npc.RightBottomRear]);
        const xFront = frustPts[Npc.LeftBottomFront].distance(frustPts[Npc.RightBottomFront]);
        const flatViewFractionTolerance = 1.0e-6;
        if (xFront > xBack * (1.0 + flatViewFractionTolerance))
            return ViewStatus.InvalidWindow;
        // see if the frustum is tapered, and if so, set up camera eyepoint and adjust viewOrg and delta.
        const compression = xFront / xBack;
        if (compression >= (1.0 - flatViewFractionTolerance))
            return ViewStatus.Success;
        // the frustum has perspective, turn camera on
        let viewOrg = frustPts[Npc.LeftBottomRear];
        const viewDelta = this.getExtents().clone();
        const zDir = this.getZVector();
        const frustumZ = viewOrg.vectorTo(frustPts[Npc.LeftBottomFront]);
        const frustOrgToEye = frustumZ.scale(1.0 / (1.0 - compression));
        const eyePoint = viewOrg.plus(frustOrgToEye);
        const backDistance = frustOrgToEye.dotProduct(zDir); // distance from eye to back plane of frustum
        const focusDistance = this.camera.isFocusValid ? this.camera.focusDist : (backDistance - (viewDelta.z / 2.0));
        const focalFraction = focusDistance / backDistance; // ratio of focus plane distance to back plane distance
        viewOrg = eyePoint.plus2Scaled(frustOrgToEye, -focalFraction, zDir, focusDistance - backDistance); // now project that point onto back plane
        viewDelta.x *= focalFraction; // adjust view delta for x and y so they are also at focus plane
        viewDelta.y *= focalFraction;
        this.setEyePoint(eyePoint);
        this.setFocusDistance(focusDistance);
        this.setOrigin(viewOrg);
        this.setExtents(viewDelta);
        this.setLensAngle(this.calcLensAngle());
        this.enableCamera();
        this._updateMaxGlobalScopeFactor();
        return ViewStatus.Success;
    }
    static calculateMaxDepth(delta, zVec) {
        const depthRatioLimit = 1.0E8; // Limit for depth Ratio.
        const maxTransformRowRatio = 1.0E5;
        const minXYComponent = Math.min(Math.abs(zVec.x), Math.abs(zVec.y));
        const maxDepthRatio = (0.0 === minXYComponent) ? depthRatioLimit : Math.min((maxTransformRowRatio / minXYComponent), depthRatioLimit);
        return Math.max(delta.x, delta.y) * maxDepthRatio;
    }
    getOrigin() { return this.origin; }
    getExtents() { return this.extents; }
    getRotation() { return this.rotation; }
    setOrigin(origin) { this.origin.setFrom(origin); }
    setExtents(extents) { this.extents.setFrom(extents); }
    setRotation(rot) { this.rotation.setFrom(rot); }
    /** @internal */
    enableCamera() {
        if (this.supportsCamera())
            this._cameraOn = true;
    }
    supportsCamera() {
        return this.allow3dManipulations();
    }
    minimumFrontDistance() {
        return Math.max(15.2 * Constant.oneCentimeter, this.forceMinFrontDist);
    }
    isEyePointAbove(elevation) {
        return !this._cameraOn ? (this.getZVector().z > 0) : (this.getEyePoint().z > elevation);
    }
    /** The style that controls how the contents of the view are displayed. */
    get displayStyle() {
        return this.getDisplayStyle3d();
    }
    set displayStyle(style) {
        assert(style instanceof DisplayStyle3dState);
        super.displayStyle = style;
    }
    /** The style that controls how the contents of the view are displayed.
     * @see [[ViewState3d.displayStyle]].
     */
    getDisplayStyle3d() {
        return super.displayStyle;
    }
    /** Turn the camera off for this view. After this call, the camera parameters in this view definition are ignored and views that use it will
     * display with an orthographic (infinite focal length) projection of the view volume from the view direction.
     * @note To turn the camera back on, call #lookAt
     */
    turnCameraOff() { this._cameraOn = false; }
    /** Determine whether the camera is valid for this view */
    get isCameraValid() { return this.camera.isValid; }
    /** Calculate the lens angle formed by the current delta and focus distance */
    calcLensAngle() {
        return Angle.createRadians(2.0 * Math.atan2(this.extents.x * 0.5, this.camera.getFocusDistance()));
    }
    /** Get the target point of the view. If there is no camera, view center is returned. */
    getTargetPoint(result) {
        if (!this._cameraOn) {
            const earthFocalPoint = this.getEarthFocalPoint();
            return earthFocalPoint ? earthFocalPoint : super.getTargetPoint(result);
        }
        return this.getEyePoint().plusScaled(this.getZVector(), -1.0 * this.getFocusDistance(), result);
    }
    /** Setup view state for either perspective or orthographic view.
     * @returns A [[ViewStatus]] indicating whether the camera was successfully positioned.
     * @note If the aspect ratio of viewDelta does not match the aspect ratio of a Viewport into which this view is displayed, it will be
     * adjusted when the [[Viewport]] is synchronized from this view.
     */
    lookAt(args) {
        if (args.lensAngle) {
            const lensAngle = args.lensAngle;
            const eyePoint = Vector3d.createFrom(args.eyePoint);
            const focus = eyePoint.vectorTo(args.targetPoint).magnitude(); // Set focus at target point
            if (focus <= Constant.oneMillimeter) // eye and target are too close together
                return ViewStatus.InvalidTargetPoint;
            if (lensAngle.radians < .0001 || lensAngle.radians > Math.PI)
                return ViewStatus.InvalidLens;
            const width = 2.0 * Math.tan(lensAngle.radians / 2.0) * focus;
            const newExtents = Vector2d.createFrom(args.newExtents ?? this.extents);
            newExtents.scale(width / newExtents.x, newExtents);
            args = { ...args, newExtents };
        }
        const isPerspective = undefined !== args.targetPoint;
        if (isPerspective && !this.supportsCamera())
            return ViewStatus.NotCameraView;
        const eye = new Point3d(args.eyePoint.x, args.eyePoint.y, args.eyePoint.z);
        const yVec = args.upVector.normalize();
        if (!yVec) // up vector zero length?
            return ViewStatus.InvalidUpVector;
        const minFrontDist = this.minimumFrontDistance();
        let zVec;
        let focusDist;
        if (args.targetPoint) {
            zVec = Vector3d.createStartEnd(args.targetPoint, eye); // z defined by direction from eye to target
            focusDist = zVec.normalizeWithLength(zVec).mag; // set focus at target point
            if (focusDist <= minFrontDist) { // eye and target are too close together
                args.opts?.onExtentsError?.(ViewStatus.InvalidTargetPoint);
                return ViewStatus.InvalidTargetPoint;
            }
        }
        else {
            zVec = Vector3d.createFrom(args.viewDirection).negate();
            if (!zVec.normalizeInPlace())
                return ViewStatus.InvalidDirection;
            focusDist = this.getFocusDistance();
        }
        const xVec = new Vector3d();
        if (yVec.crossProduct(zVec).normalizeWithLength(xVec).mag < Geometry.smallMetricDistance)
            return ViewStatus.InvalidUpVector; // up is parallel to z
        if (zVec.crossProduct(xVec).normalizeWithLength(yVec).mag < Geometry.smallMetricDistance)
            return ViewStatus.InvalidUpVector;
        // we now have rows of the rotation matrix
        const rotation = Matrix3d.createRows(xVec, yVec, zVec);
        let backDist = args.backDistance ? args.backDistance : this.getBackDistance();
        let frontDist = args.frontDistance ? args.frontDistance : this.getFrontDistance();
        const delta = args.newExtents ? new Vector3d(Math.abs(args.newExtents.x), Math.abs(args.newExtents.y), this.extents.z) : this.extents.clone();
        // The front/back distance are relatively arbitrary -- the frustum will be adjusted to include geometry.
        // Set them here to reasonable in front of eye and just beyond target.
        frontDist = Math.min(frontDist, (.5 * Constant.oneMeter));
        backDist = Math.min(backDist, focusDist + (.5 * Constant.oneMeter));
        if (backDist < focusDist) // make sure focus distance is in front of back distance.
            backDist = focusDist + Constant.oneMillimeter;
        if (frontDist > focusDist)
            frontDist = focusDist - minFrontDist;
        if (frontDist < minFrontDist)
            frontDist = minFrontDist;
        delta.z = (backDist - frontDist);
        const stat = this.adjustViewDelta(delta, eye, rotation, undefined, args.opts);
        if (ViewStatus.Success !== stat)
            return stat;
        if (delta.z > ViewState3d.calculateMaxDepth(delta, zVec)) // make sure we're not zoomed out too far
            return ViewStatus.MaxDisplayDepth;
        // The origin is defined as the lower left of the view rectangle on the focus plane, projected to the back plane.
        // Start at eye point, and move to center of back plane, then move left half of width. and down half of height
        const origin = eye.plus3Scaled(zVec, -backDist, xVec, -0.5 * delta.x, yVec, -0.5 * delta.y);
        this.setEyePoint(args.eyePoint);
        this.setRotation(rotation);
        this.setFocusDistance(focusDist);
        this.setOrigin(origin);
        this.setExtents(delta);
        this.setLensAngle(this.calcLensAngle());
        if (isPerspective)
            this.enableCamera();
        else
            this.turnCameraOff();
        this._updateMaxGlobalScopeFactor();
        return ViewStatus.Success;
    }
    /** Change the focus distance for this ViewState3d. Preserves the content of the view.
     * @internal
     */
    changeFocusDistance(newDist) {
        if (newDist <= Constant.oneMillimeter)
            return ViewStatus.InvalidTargetPoint;
        const oldExtents = this.extents.clone(); // save current extents so we can keep frustum unchanged
        this.extents.x = 2.0 * Math.tan(this.camera.lens.radians / 2.0) * newDist; // new width based on focus distance and lens angle.
        this.extents.y = this.extents.y * (this.extents.x / oldExtents.x); // preserve aspect ratio
        this.origin.addScaledInPlace(this.rotation.multiplyTransposeVector(this.extents.vectorTo(oldExtents)), .5); // move origin by half the change in extents
        this.camera.focusDist = newDist; // save new focus distance
        return ViewStatus.Success;
    }
    /** Change the focus distance for this ViewState3d to be defined by the the supplied point, if it is in front of the camera.
     * Preserves the content of the view.
     * @internal
     */
    changeFocusFromPoint(pt) {
        return this.changeFocusDistance(this.getZVector().dotProduct(pt.vectorTo(this.camera.eye)));
    }
    /** Move the camera relative to its current location by a distance in camera coordinates.
     * @param distance to move camera. Length is in world units, direction relative to current camera orientation.
     * @returns Status indicating whether the camera was successfully positioned. See values at [[ViewStatus]] for possible errors.
     */
    moveCameraLocal(distance) {
        const distWorld = this.rotation.multiplyTransposeVector(distance);
        return this.moveCameraWorld(distWorld);
    }
    /** Move the camera relative to its current location by a distance in world coordinates.
     * @param distance in world units.
     * @returns Status indicating whether the camera was successfully positioned. See values at [[ViewStatus]] for possible errors.
     */
    moveCameraWorld(distance) {
        if (!this._cameraOn) {
            this.origin.plus(distance, this.origin);
            return ViewStatus.Success;
        }
        const targetPoint = this.getTargetPoint().plus(distance);
        const eyePoint = this.getEyePoint().plus(distance);
        return this.lookAt({ eyePoint, targetPoint, upVector: this.getYVector() });
    }
    /** Rotate the camera from its current location about an axis relative to its current orientation.
     * @param angle The angle to rotate the camera.
     * @param axis The axis about which to rotate the camera. The axis is a direction relative to the current camera orientation.
     * @param aboutPt The point, in world coordinates, about which the camera is rotated. If aboutPt is undefined, the camera rotates in place
     *  (i.e. about the current eyePoint).
     * @note Even though the axis is relative to the current camera orientation, the aboutPt is in world coordinates, \b not relative to the camera.
     * @returns Status indicating whether the camera was successfully positioned. See values at [[ViewStatus]] for possible errors.
     */
    rotateCameraLocal(angle, axis, aboutPt) {
        const axisWorld = this.getRotation().multiplyTransposeVector(axis);
        return this.rotateCameraWorld(angle, axisWorld, aboutPt);
    }
    /** Rotate the camera from its current location about an axis in world coordinates.
     * @param angle The angle to rotate the camera.
     * @param axis The world-based axis (direction) about which to rotate the camera.
     * @param aboutPt The point, in world coordinates, about which the camera is rotated. If aboutPt is undefined, the camera rotates in place
     *  (i.e. about the current eyePoint).
     * @returns Status indicating whether the camera was successfully positioned. See values at [[ViewStatus]] for possible errors.
     */
    rotateCameraWorld(angle, axis, aboutPt) {
        const about = aboutPt ? aboutPt : this.getEyePoint();
        const rotation = Matrix3d.createRotationAroundVector(axis, angle);
        if (!rotation)
            return ViewStatus.InvalidUpVector; // Invalid axis given
        const trans = Transform.createFixedPointAndMatrix(about, rotation);
        const targetPoint = trans.multiplyPoint3d(this.getTargetPoint());
        const upVector = rotation.multiplyVector(this.getYVector());
        return this.lookAt({ eyePoint: this.getEyePoint(), targetPoint, upVector });
    }
    /** Move camera about the global ellipsoid. This rotates the camera position about the center of the global ellipsoid maintaining the current height.
     * @param fromPoint Point to pan from.
     * @param point Point to point to.
     * @returns Status indicating whether the camera was successfully positioned. See values at [[ViewStatus]] for possible errors.
     */
    moveCameraGlobal(fromPoint, toPoint) {
        if (!this.iModel.ecefLocation)
            return ViewStatus.NotGeolocated;
        if (this.globeMode !== GlobeMode.Ellipsoid)
            return ViewStatus.NotEllipsoidGlobeMode;
        const earthCenter = this.iModel.ecefLocation?.earthCenter;
        const rMatrix = Matrix3d.createRotationVectorToVector(Vector3d.createStartEnd(earthCenter, toPoint), Vector3d.createStartEnd(earthCenter, fromPoint));
        if (!rMatrix)
            return ViewStatus.DegenerateGeometry;
        const rotationTransform = Transform.createFixedPointAndMatrix(earthCenter, rMatrix);
        const frustum = this.calculateFrustum();
        if (!frustum)
            return ViewStatus.DegenerateGeometry;
        frustum.multiply(rotationTransform);
        return this.setupFromFrustum(frustum);
    }
    /** Get the distance from the eyePoint to the front plane for this view. */
    getFrontDistance() { return this.getBackDistance() - this.extents.z; }
    /** Get the distance from the eyePoint to the back plane for this view. */
    getBackDistance() {
        // backDist is the z component of the vector from the origin to the eyePoint .
        const eyeOrg = this.origin.vectorTo(this.getEyePoint());
        this.getRotation().multiplyVector(eyeOrg, eyeOrg);
        return eyeOrg.z;
    }
    /** Place the eyepoint of the camera so it is aligned with the center of the view. This removes any 1-point perspective skewing that may be
     * present in the current view.
     * @param backDistance If defined, the new the distance from the eyepoint to the back plane. Otherwise the distance from the
     * current eyepoint is used.
     */
    centerEyePoint(backDistance) {
        const eyePoint = this.getExtents().scale(0.5);
        eyePoint.z = backDistance ? backDistance : this.getBackDistance();
        const eye = this.getOrigin().plus(this.getRotation().multiplyTransposeXYZ(eyePoint.x, eyePoint.y, eyePoint.z));
        this.camera.setEyePoint(eye);
    }
    /** Center the focus distance of the camera halfway between the front plane and the back plane, keeping the eyepoint,
     * lens angle, rotation, back distance, and front distance unchanged.
     * @note The focus distance, origin, and delta values are modified, but the view encloses the same volume and appears visually unchanged.
     */
    centerFocusDistance() {
        const backDistance = this.getBackDistance();
        const frontDistance = this.getFrontDistance();
        const eyePoint = this.getEyePoint();
        const targetPoint = eyePoint.plusScaled(this.getZVector(), frontDistance - backDistance);
        this.lookAt({ eyePoint, targetPoint, upVector: this.getYVector(), lensAngle: this.getLensAngle(), frontDistance, backDistance });
    }
    /** Ensure the focus plane lies between the front and back planes. If not, center it. */
    verifyFocusPlane() {
        if (!this._cameraOn)
            return;
        let backDist = this.getBackDistance();
        const frontDist = backDist - this.extents.z;
        const camera = this.camera;
        const extents = this.extents;
        const rot = this.rotation;
        if (backDist <= 0.0 || frontDist <= 0.0) {
            // the camera location is invalid. Set it based on the view range.
            const tanAngle = Math.tan(camera.lens.radians / 2.0);
            backDist = extents.z / tanAngle;
            camera.setFocusDistance(backDist / 2);
            this.centerEyePoint(backDist);
            return;
        }
        const focusDist = camera.focusDist;
        if (focusDist > frontDist && focusDist < backDist)
            return;
        // put it halfway between front and back planes
        camera.setFocusDistance((extents.z / 2.0) + frontDist);
        // moving the focus plane means we have to adjust the origin and delta too (they're on the focus plane, see diagram above)
        const ratio = camera.focusDist / focusDist;
        extents.x *= ratio;
        extents.y *= ratio;
        camera.eye.plus3Scaled(rot.rowZ(), -backDist, rot.rowX(), -0.5 * extents.x, rot.rowY(), -0.5 * extents.y, this.origin); // this centers the camera too
    }
    /** Get the current location of the eyePoint for camera in this view. */
    getEyePoint() { return this.camera.eye; }
    /** Get the lens angle for this view. */
    getLensAngle() { return this.camera.lens; }
    /** Set the lens angle for this view.
     *  @param angle The new lens angle in radians. Must be greater than 0 and less than pi.
     *  @note This does not change the view's current field-of-view. Instead, it changes the lens that will be used if the view
     *  is subsequently modified and the lens angle is used to position the eyepoint.
     *  @note To change the field-of-view (i.e. "zoom") of a view, pass a new viewDelta to #lookAt
     */
    setLensAngle(angle) { this.camera.setLensAngle(angle); }
    /** Change the location of the eyePoint for the camera in this view.
     * @param pt The new eyepoint.
     * @note This method is generally for internal use only. Moving the eyePoint arbitrarily can result in skewed or illegal perspectives.
     * The most common method for user-level camera positioning is #lookAt.
     */
    setEyePoint(pt) { this.camera.setEyePoint(pt); }
    /** Set the focus distance for this view.
     *  @note Changing the focus distance changes the plane on which the delta.x and delta.y values lie. So, changing focus distance
     *  without making corresponding changes to delta.x and delta.y essentially changes the lens angle, causing a "zoom" effect
     */
    setFocusDistance(dist) { this.camera.setFocusDistance(dist); }
    /**  Get the distance from the eyePoint to the focus plane for this view. */
    getFocusDistance() { return this.camera.focusDist; }
    /** Obtain an "eye" point for this view. If the camera is on, this simply returns [[Camera.getEyePoint]].
     * Otherwise, a pseudo-eye-point is computed from the view direction and a lens angle of PI/2.
     */
    getEyeOrOrthographicViewPoint() {
        if (this.isCameraOn)
            return this.camera.getEyePoint();
        this.camera.validateLens();
        const tanHalfAngle = Math.tan(this.camera.lens.radians / 2);
        const halfDelta = this.getExtents().magnitudeXY();
        const eyeDistance = tanHalfAngle ? (halfDelta / tanHalfAngle) : 0;
        const zVector = this.getRotation().getRow(2);
        return this.getCenter().plusScaled(zVector, -eyeDistance);
    }
    createAuxCoordSystem(acsName) { return AuxCoordSystem3dState.createNew(acsName, this.iModel); }
    decorate(context) {
        super.decorate(context);
        if (this._environmentDecorations)
            this._environmentDecorations.decorate(context);
    }
    /** Returns the ground elevation taken from the environment added with the global z position of this imodel. */
    getGroundElevation() {
        const env = this.getDisplayStyle3d().environment;
        return env.ground.elevation + this.iModel.globalOrigin.z;
    }
    /** Return the ground extents, which will originate either from the viewport frustum or the extents of the imodel. */
    getGroundExtents(vp) {
        const displayStyle = this.getDisplayStyle3d();
        const extents = new Range3d();
        if (!displayStyle.environment.displayGround)
            return extents; // Ground plane is not enabled
        const elevation = this.getGroundElevation();
        if (undefined !== vp) {
            const viewRay = Ray3d.create(Point3d.create(), vp.rotation.rowZ());
            const xyPlane = Plane3dByOriginAndUnitNormal.create(Point3d.create(0, 0, elevation), Vector3d.create(0, 0, 1));
            // first determine whether the ground plane is displayed in the view
            const worldFrust = vp.getFrustum();
            for (const point of worldFrust.points) {
                viewRay.origin = point; // We never modify the reference
                const xyzPoint = Point3d.create();
                const param = viewRay.intersectionWithPlane(xyPlane, xyzPoint);
                if (param === undefined)
                    return extents; // View does not show ground plane
            }
        }
        extents.setFrom(this.iModel.projectExtents);
        extents.low.z = extents.high.z = elevation;
        const center = extents.low.interpolate(.5, extents.high);
        const radius = extents.low.distance(extents.high);
        extents.setNull();
        extents.extendPoint(center); // Extents now contains single point
        extents.low.addScaledInPlace(Vector3d.create(-1, -1, -1), radius);
        extents.high.addScaledInPlace(Vector3d.create(1, 1, 1), radius);
        extents.low.z = extents.high.z = elevation;
        return extents;
    }
    /** @internal */
    getModelElevation(modelId) {
        const settings = this.getDisplayStyle3d().settings.getPlanProjectionSettings(modelId);
        return settings && settings.elevation ? settings.elevation : 0;
    }
    /** If the background map is displayed, return the point on the globe in directly front of the eye (or in center of view if camera off)
     *  This is generally a better target point for orthographic views than the view center which can be far from the area of interest.
     * @public
     */
    getEarthFocalPoint() {
        if (!this.iModel.ecefLocation || this.globeMode !== GlobeMode.Ellipsoid)
            return undefined;
        const backgroundMapGeometry = this.displayStyle.getBackgroundMapGeometry();
        if (undefined === backgroundMapGeometry)
            return undefined;
        const earthEllipsoid = backgroundMapGeometry.getEarthEllipsoid();
        const viewZ = this.getRotation().rowZ();
        const center = this.getCenter();
        const eye = this.isCameraOn ? this.camera.getEyePoint() : center.plusScaled(viewZ, Constant.diameterOfEarth);
        const eyeRay = Ray3d.create(eye, viewZ);
        const fractions = new Array(), points = new Array();
        if (earthEllipsoid.intersectRay(eyeRay, fractions, points, undefined)) {
            let fraction = -1.0E10, index = -1;
            for (let i = 0; i < fractions.length; i++)
                if (fractions[i] > fraction) {
                    fraction = fractions[i];
                    index = i;
                }
            return (index >= 0 && fraction < 0) ? points[index] : undefined;
        }
        else {
            return eyeRay.projectPointToRay(center);
        }
    }
    /**
     * For a geoLocated project, align the view with the global ellipsoid by rotating
     * around the supplied target point such that the view axis points toward the
     * globe center. If the viewing height is below the global transition threshold.
     * @param target The rotation target or pivot point.  This point will remain stationary in the view.
     * @param transition If this is defined and true then the rotation is scaled by the [[ViewState.globalViewTransition]]  This
     * will cause a smooth transition as a view is zoomed out from a specific location to a more global representation.
     * @public
     */
    alignToGlobe(target, transition) {
        if (!this.iModel.ecefLocation)
            return ViewStatus.NotGeolocated;
        if (this.globeMode !== GlobeMode.Ellipsoid)
            return ViewStatus.NotEllipsoidGlobeMode;
        const globalTransition = this.globalViewTransition();
        if (globalTransition <= 0)
            return ViewStatus.HeightBelowTransition;
        const earthCenter = this.iModel.ecefLocation?.earthCenter;
        const viewCenter = this.getCenter();
        const viewZ = this.getRotation().rowZ();
        const eye = this.isCameraOn ? this.camera.eye : viewCenter.plusScaled(viewZ, Constant.diameterOfEarth);
        const centerToEye = earthCenter.unitVectorTo(eye);
        if (!centerToEye)
            return ViewStatus.DegenerateGeometry;
        const axis = viewZ.unitCrossProduct(centerToEye);
        if (!axis)
            return ViewStatus.DegenerateGeometry;
        const theta = viewZ.angleTo(centerToEye);
        if (theta.radians > Angle.piOver2Radians)
            return ViewStatus.DegenerateGeometry;
        if (theta.isAlmostZero)
            return ViewStatus.NoTransitionRequired;
        const transitionRotation = Matrix3d.createRotationAroundVector(axis, transition ? theta.cloneScaled(globalTransition) : theta);
        if (!transitionRotation)
            return ViewStatus.DegenerateGeometry;
        const transitionTransform = Transform.createFixedPointAndMatrix(target, transitionRotation);
        const frustum = this.calculateFrustum();
        if (!frustum)
            return ViewStatus.DegenerateGeometry;
        frustum.multiply(transitionTransform);
        return this.setupFromFrustum(frustum);
    }
    /** See [[ViewState.attachToViewport]]. */
    attachToViewport(args) {
        super.attachToViewport(args);
        const removeListener = this.displayStyle.settings.onEnvironmentChanged.addListener((env) => {
            this._environmentDecorations?.setEnvironment(env);
        });
        this._environmentDecorations = new EnvironmentDecorations(this, () => args.invalidateDecorations(), () => removeListener());
    }
    /** See [[ViewState.detachFromViewport]]. */
    detachFromViewport() {
        super.detachFromViewport();
        this._environmentDecorations = dispose(this._environmentDecorations);
    }
}
ViewState3d._minGlobeEyeHeight = Constant.earthRadiusWGS84.equator / 4; // View as globe if more than a quarter of earth radius from surface.
ViewState3d._scratchGlobeCarto = Cartographic.createZero();
/** Defines the state of a view of a single 2d model.
 * @public
 * @extensions
 */
export class ViewState2d extends ViewState {
    static get className() { return "ViewDefinition2d"; }
    get baseModelId() { return this._baseModelId; }
    /** @internal */
    get _tileTreeRef() {
        if (undefined === this._treeRef) {
            const model = this.getViewedModel();
            if (undefined !== model)
                this._treeRef = model.createTileTreeReference(this);
        }
        return this._treeRef;
    }
    constructor(props, iModel, categories, displayStyle) {
        super(props, iModel, categories, displayStyle);
        this.origin = Point2d.fromJSON(props.origin);
        this.delta = Point2d.fromJSON(props.delta);
        this.angle = Angle.fromJSON(props.angle);
        this._baseModelId = Id64.fromJSON(props.baseModelId);
        this._details = new ViewDetails(this.jsonProperties);
    }
    toJSON() {
        const val = super.toJSON();
        val.origin = this.origin;
        val.delta = this.delta;
        val.angle = this.angle;
        val.baseModelId = this.baseModelId;
        return val;
    }
    /** See [[ViewState.is3d]]. */
    is3d() { return false; }
    /** @internal */
    isSpatialView() { return false; }
    /** Capture a copy of the viewed area. */
    savePose() { return new ViewPose2d(this); }
    /** See [[ViewState.applyPose]]. */
    applyPose(val) {
        if (val instanceof ViewPose2d) {
            this.setOrigin(val.origin);
            this.setExtents(val.delta);
            this.angle.setFrom(val.angle);
        }
        return this;
    }
    /** Return the model for this 2d view. */
    getViewedModel() {
        const model = this.iModel.models.getLoaded(this.baseModelId);
        if (model && !(model instanceof GeometricModel2dState))
            return undefined;
        return model;
    }
    /** Change the model viewed by this view.
     * @note The new model should be of the same type (drawing or sheet) as the current viewed model.
     * @throws Error if attempting to change the viewed model while the view is attached to a viewport.
     * @see [[Viewport.changeViewedModel2d]].
     * @public
     */
    async changeViewedModel(newViewedModelId) {
        if (this.isAttachedToViewport)
            throw new Error("Cannot change the viewed model of a view that is attached to a viewport.");
        this._baseModelId = newViewedModelId;
        this._treeRef = undefined;
        await this.load();
    }
    computeFitRange() {
        return this.getViewedExtents();
    }
    /** @internal */
    preload(hydrateRequest) {
        super.preload(hydrateRequest);
        if (this.iModel.models.getLoaded(this.baseModelId) === undefined)
            hydrateRequest.baseModelId = this.baseModelId;
    }
    /** @internal */
    async postload(hydrateResponse) {
        const promises = [];
        promises.push(super.postload(hydrateResponse));
        if (hydrateResponse.baseModelProps !== undefined)
            promises.push(this.iModel.models.updateLoadedWithModelProps([hydrateResponse.baseModelProps]));
        await Promise.all(promises);
    }
    /** Provides access to optional detail settings for this view. */
    get details() {
        return this._details;
    }
    allow3dManipulations() { return false; }
    getOrigin() { return new Point3d(this.origin.x, this.origin.y, Frustum2d.minimumZExtents.low); }
    getExtents() { return new Vector3d(this.delta.x, this.delta.y, Frustum2d.minimumZExtents.length()); }
    getRotation() { return Matrix3d.createRotationAroundVector(Vector3d.unitZ(), this.angle); }
    setExtents(delta) { this.delta.set(delta.x, delta.y); }
    setOrigin(origin) { this.origin.set(origin.x, origin.y); }
    setRotation(rot) {
        const xColumn = rot.getColumn(0);
        this.angle.setRadians(Math.atan2(xColumn.y, xColumn.x));
    }
    viewsModel(modelId) { return this.baseModelId === modelId; }
    forEachModel(func) {
        const model = this.iModel.models.getLoaded(this.baseModelId);
        if (undefined !== model && undefined !== model.asGeometricModel2d)
            func(model);
    }
    /** @internal */
    forEachModelTreeRef(func) {
        const ref = this._tileTreeRef;
        if (undefined !== ref)
            func(ref);
    }
    createAuxCoordSystem(acsName) { return AuxCoordSystem2dState.createNew(acsName, this.iModel); }
}
//# sourceMappingURL=ViewState.js.map