/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module iModels
 */
import { assert, BeEvent, expectDefined, GeoServiceStatus, Id64, IModelStatus, OpenMode } from "@itwin/core-bentley";
import { AxisIndex, AxisOrder, Constant, Geometry, Matrix3d, Point3d, Range3d, Transform, Vector3d, YawPitchRollAngles, } from "@itwin/core-geometry";
import { Cartographic } from "./geometry/Cartographic";
import { GeographicCRS } from "./geometry/CoordinateReferenceSystem";
import { IModelError } from "./IModelError";
/** The position and orientation of an iModel on the earth in [ECEF](https://en.wikipedia.org/wiki/ECEF) (Earth Centered Earth Fixed) coordinates
 * @note This is an immutable type - all of its properties are frozen.
 * @see [GeoLocation of iModels]($docs/learning/GeoLocation.md)
 * @public
 */
export class EcefLocation {
    /** The origin of the ECEF transform. */
    origin;
    /** The orientation of the ECEF transform */
    orientation;
    /** Optional position on the earth used to establish the ECEF origin and orientation. */
    cartographicOrigin;
    /** Optional X column vector used with [[yVector]] to calculate potentially non-rigid transform if a projection is present. */
    xVector;
    /** Optional Y column vector used with [[xVector]] to calculate potentially non-rigid transform if a projection is present. */
    yVector;
    /** The transform from iModel Spatial coordinates to ECEF from this EcefLocation */
    _transform = Transform.createIdentity();
    /** Get the transform from iModel Spatial coordinates to ECEF from this EcefLocation */
    getTransform() { return this._transform; }
    /** Construct a new EcefLocation. Once constructed, it is frozen and cannot be modified. */
    constructor(props) {
        this.origin = Point3d.fromJSON(props.origin).freeze();
        this.orientation = YawPitchRollAngles.fromJSON(props.orientation).freeze();
        if (props.cartographicOrigin)
            this.cartographicOrigin = Cartographic.fromRadians({ longitude: props.cartographicOrigin.longitude, latitude: props.cartographicOrigin.latitude, height: props.cartographicOrigin.height }).freeze();
        if (props.xVector && props.yVector) {
            this.xVector = Vector3d.fromJSON(props.xVector).freeze();
            this.yVector = Vector3d.fromJSON(props.yVector).freeze();
        }
        if (props.transform) {
            this._transform.setFromJSON(props.transform);
            this._transform.freeze();
        }
        else {
            let matrix;
            if (this.xVector && this.yVector) {
                const zVector = this.xVector.crossProduct(this.yVector);
                if (zVector.normalizeInPlace())
                    matrix = Matrix3d.createColumns(this.xVector, this.yVector, zVector);
            }
            if (!matrix)
                matrix = this.orientation.toMatrix3d();
            this._transform = Transform.createOriginAndMatrix(this.origin, matrix);
            this._transform.freeze();
        }
    }
    /** Returns true if this EcefLocation is not located at the center of the Earth.
     * @alpha are locations very close to the center considered valid? What are the specific criteria?
     */
    get isValid() {
        return !this.origin.isZero;
    }
    /** Construct ECEF Location from cartographic origin with optional known point and angle.   */
    static createFromCartographicOrigin(origin, point, angle) {
        const ecefOrigin = origin.toEcef();
        const deltaRadians = 10 / Constant.earthRadiusWGS84.polar;
        const northCarto = Cartographic.fromRadians({ longitude: origin.longitude, latitude: origin.latitude + deltaRadians, height: origin.height });
        const eastCarto = Cartographic.fromRadians({ longitude: origin.longitude + deltaRadians, latitude: origin.latitude, height: origin.height });
        const ecefNorth = northCarto.toEcef();
        const ecefEast = eastCarto.toEcef();
        const xVector = expectDefined(Vector3d.createStartEnd(ecefOrigin, ecefEast).normalize());
        const yVector = expectDefined(Vector3d.createStartEnd(ecefOrigin, ecefNorth).normalize());
        const matrix = expectDefined(Matrix3d.createRigidFromColumns(xVector, yVector, AxisOrder.XYZ));
        if (angle !== undefined) {
            const north = Matrix3d.createRotationAroundAxisIndex(AxisIndex.Z, angle);
            matrix.multiplyMatrixMatrix(north, matrix);
        }
        if (point !== undefined) {
            const delta = matrix.multiplyVector(Vector3d.create(-point.x, -point.y, -point.z));
            ecefOrigin.addInPlace(delta);
        }
        return new EcefLocation({ origin: ecefOrigin, orientation: expectDefined(YawPitchRollAngles.createFromMatrix3d(matrix)), cartographicOrigin: origin });
    }
    /** Construct ECEF Location from transform with optional position on the earth used to establish the ECEF origin and orientation. */
    static createFromTransform(transform) {
        const ecefOrigin = transform.getOrigin();
        const angleFromInput = YawPitchRollAngles.createDegrees(0, 0, 0);
        const locationOrientationFromInputT = YawPitchRollAngles.createFromMatrix3d(transform.getMatrix(), angleFromInput);
        const transformProps = transform.toJSON();
        return new EcefLocation({ origin: ecefOrigin, orientation: locationOrientationFromInputT ?? angleFromInput, transform: transformProps });
    }
    /** Get the location center of the earth in the iModel coordinate system. */
    get earthCenter() {
        const matrix = this.orientation.toMatrix3d();
        return Point3d.createFrom(matrix.multiplyTransposeXYZ(-this.origin.x, -this.origin.y, -this.origin.z));
    }
    /** Return true if this location is equivalent to another location within a small tolerance. */
    isAlmostEqual(other) {
        if (!this.origin.isAlmostEqual(other.origin) || !this.orientation.isAlmostEqual(other.orientation))
            return false;
        if ((this.xVector === undefined) !== (other.xVector === undefined) || (this.yVector === undefined) !== (other.yVector === undefined))
            return false;
        if (this.xVector !== undefined && other.xVector !== undefined && !this.xVector.isAlmostEqual(other.xVector))
            return false;
        if (this.yVector !== undefined && other.yVector !== undefined && !this.yVector.isAlmostEqual(other.yVector))
            return false;
        if (!this.getTransform().isAlmostEqual(other.getTransform()))
            return false;
        const thisCarto = this.cartographicOrigin;
        const otherCarto = other.cartographicOrigin;
        if (undefined === thisCarto || undefined === otherCarto)
            return undefined === thisCarto && undefined === otherCarto;
        return thisCarto.equalsEpsilon(otherCarto, Geometry.smallMetricDistance);
    }
    toJSON() {
        const props = {
            origin: this.origin.toJSON(),
            orientation: this.orientation.toJSON(),
            transform: this.getTransform().toJSON(),
        };
        if (this.cartographicOrigin)
            props.cartographicOrigin = this.cartographicOrigin.toJSON();
        if (this.xVector)
            props.xVector = this.xVector.toJSON();
        if (this.yVector)
            props.yVector = this.yVector.toJSON();
        return props;
    }
}
/** Represents an iModel in JavaScript.
 * @see [GeoLocation of iModels]($docs/learning/GeoLocation.md)
 * @public
 */
export class IModel {
    _projectExtents;
    _name;
    _rootSubject;
    _globalOrigin;
    _ecefLocation;
    _geographicCoordinateSystem;
    _iModelId;
    _changeset;
    /** The Id of the repository model. */
    static repositoryModelId = "0x1";
    /** The Id of the root subject element. */
    static rootSubjectId = "0x1";
    /** The Id of the dictionary model. */
    static dictionaryId = "0x10";
    /** Event raised after [[name]] changes. */
    onNameChanged = new BeEvent();
    /** Event raised after [[rootSubject]] changes. */
    onRootSubjectChanged = new BeEvent();
    /** Event raised after [[projectExtents]] changes. */
    onProjectExtentsChanged = new BeEvent();
    /** Event raised after [[globalOrigin]] changes. */
    onGlobalOriginChanged = new BeEvent();
    /** Event raised after [[ecefLocation]] changes. */
    onEcefLocationChanged = new BeEvent();
    /** Event raised after [[geographicCoordinateSystem]] changes. */
    onGeographicCoordinateSystemChanged = new BeEvent();
    /** Event raised after [[changeset]] changes. */
    onChangesetChanged = new BeEvent();
    /** Name of the iModel */
    get name() {
        assert(this._name !== undefined);
        return this._name;
    }
    set name(name) {
        if (name !== this._name) {
            const old = this._name;
            this._name = name;
            if (undefined !== old)
                this.onNameChanged.raiseEvent(old);
        }
    }
    /** The name and description of the root subject of this iModel */
    get rootSubject() {
        assert(this._rootSubject !== undefined);
        return this._rootSubject;
    }
    set rootSubject(subject) {
        if (undefined === this._rootSubject || this._rootSubject.name !== subject.name || this._rootSubject.description !== subject.description) {
            const old = this._rootSubject;
            this._rootSubject = subject;
            if (old)
                this.onRootSubjectChanged.raiseEvent(old);
        }
    }
    /**
     * The volume, in spatial coordinates, inside which the entire project is contained.
     * @note The object returned from this method is frozen. You *must* make a copy before you do anything that might attempt to modify it.
     */
    get projectExtents() {
        assert(undefined !== this._projectExtents);
        return this._projectExtents;
    }
    set projectExtents(extents) {
        // Don't allow any axis of the project extents to be less than 1 meter.
        const projectExtents = extents.clone();
        projectExtents.ensureMinLengths(1.0);
        if (!this._projectExtents || !this._projectExtents.isAlmostEqual(projectExtents)) {
            const old = this._projectExtents;
            projectExtents.freeze();
            this._projectExtents = projectExtents;
            if (old)
                this.onProjectExtentsChanged.raiseEvent(old);
        }
    }
    /** An offset to be applied to all spatial coordinates. */
    get globalOrigin() {
        assert(this._globalOrigin !== undefined);
        return this._globalOrigin;
    }
    set globalOrigin(org) {
        if (!this._globalOrigin || !this._globalOrigin.isAlmostEqual(org)) {
            const old = this._globalOrigin;
            org.freeze();
            this._globalOrigin = org;
            if (old)
                this.onGlobalOriginChanged.raiseEvent(old);
        }
    }
    /** The [EcefLocation]($docs/learning/glossary#ecefLocation) of the iModel in Earth Centered Earth Fixed coordinates.
     * If the iModel property geographicCoordinateSystem is not defined then the ecefLocation provides a geolocation by defining a
     * 3D coordinate system relative to the Earth model WGS84. Refer to additional documentation for details. If the geographicCoordinateSystem
     * property is defined then the ecefLocation must be used with care. When the geographicCoordinateSystem is defined it indicates the
     * iModel cartesian space is the result of a cartographic projection. This implies a flattening of the Earth surface process that
     * results in scale, angular or area distortion. The ecefLocation is then an approximation calculated at the center of the project extent.
     * If the project is more than 2 kilometer in size, the ecefLocation may represent a poor approximation of the effective
     * cartographic projection used and a linear transformation should then be calculated at the exact origin of the data
     * it must position.
     * @see [GeoLocation of iModels]($docs/learning/GeoLocation.md)
    */
    get ecefLocation() {
        return this._ecefLocation;
    }
    set ecefLocation(ecefLocation) {
        const old = this._ecefLocation;
        if (!old && !ecefLocation)
            return;
        else if (old && ecefLocation && old.isAlmostEqual(ecefLocation))
            return;
        this._ecefLocation = ecefLocation;
        this.onEcefLocationChanged.raiseEvent(old);
    }
    /** Set the [EcefLocation]($docs/learning/glossary#ecefLocation) for this iModel. */
    setEcefLocation(ecef) {
        this.ecefLocation = new EcefLocation(ecef);
    }
    /** The geographic coordinate reference system of the iModel. */
    get geographicCoordinateSystem() {
        return this._geographicCoordinateSystem;
    }
    set geographicCoordinateSystem(geoCRS) {
        const old = this._geographicCoordinateSystem;
        if (!old && !geoCRS)
            return;
        else if (old && geoCRS && old.equals(geoCRS))
            return;
        this._geographicCoordinateSystem = geoCRS;
        this.onGeographicCoordinateSystemChanged.raiseEvent(old);
    }
    /** Sets the geographic coordinate reference system from GeographicCRSProps. */
    setGeographicCoordinateSystem(geoCRS) {
        this.geographicCoordinateSystem = new GeographicCRS(geoCRS);
    }
    /** @internal */
    getConnectionProps() {
        return {
            name: this.name,
            rootSubject: this.rootSubject,
            projectExtents: this.projectExtents.toJSON(),
            globalOrigin: this.globalOrigin.toJSON(),
            ecefLocation: this.ecefLocation,
            geographicCoordinateSystem: this.geographicCoordinateSystem?.toJSON(),
            ...this._getRpcProps(),
        };
    }
    /** Convert this iModel to a JSON representation. */
    toJSON() {
        return this.getConnectionProps();
    }
    /** A key used to identify this iModel in RPC calls from frontend to backend.
     * @internal
     */
    _fileKey;
    /** Get the key that was used to open this iModel. This is the value used for Rpc and Ipc communications. */
    get key() { return this._fileKey; }
    /** @internal */
    _iTwinId;
    /** The Guid that identifies the iTwin that owns this iModel. */
    get iTwinId() { return this._iTwinId; }
    /** The Guid that identifies this iModel. */
    get iModelId() { return this._iModelId; }
    /** @public */
    get changeset() {
        return { ...this._changeset };
    }
    set changeset(changeset) {
        const prev = this._changeset;
        if (prev.id !== changeset.id || prev.index !== changeset.index) {
            this._changeset = { id: changeset.id, index: changeset.index };
            this.onChangesetChanged.raiseEvent(prev);
        }
    }
    _openMode = OpenMode.Readonly;
    /** The [[OpenMode]] used for this IModel. */
    get openMode() { return this._openMode; }
    /** Return a token for RPC operations.
     * @throws IModelError if the iModel is not open.
     */
    getRpcProps() {
        if (!this.isOpen)
            throw new IModelError(IModelStatus.BadRequest, "IModel is not open for rpc");
        return this._getRpcProps();
    }
    /** Returns the iModel's RPC properties.
     * @note It is an error to attempt to use these properties as a token for RPC operations if the iModel is not open.
     * @internal
     */
    _getRpcProps() {
        return {
            key: this._fileKey,
            iTwinId: this.iTwinId,
            iModelId: this.iModelId,
            changeset: this.changeset,
        };
    }
    /** @internal */
    constructor(tokenProps) {
        this._changeset = tokenProps?.changeset ?? { id: "", index: 0 };
        this._fileKey = "";
        if (tokenProps) {
            this._fileKey = tokenProps.key;
            this._iTwinId = tokenProps.iTwinId;
            this._iModelId = tokenProps.iModelId;
        }
    }
    /** @internal */
    initialize(name, props) {
        this.name = name;
        this.rootSubject = props.rootSubject;
        this.projectExtents = Range3d.fromJSON(props.projectExtents);
        this.globalOrigin = Point3d.fromJSON(props.globalOrigin);
        const ecefLocation = props.ecefLocation ? new EcefLocation(props.ecefLocation) : undefined;
        this.ecefLocation = ecefLocation?.isValid ? ecefLocation : undefined;
        this.geographicCoordinateSystem = props.geographicCoordinateSystem ? new GeographicCRS(props.geographicCoordinateSystem) : undefined;
    }
    /** Get the default subCategoryId for the supplied categoryId */
    static getDefaultSubCategoryId(categoryId) {
        return Id64.isValid(categoryId) ? Id64.fromLocalAndBriefcaseIds(Id64.getLocalId(categoryId) + 1, Id64.getBriefcaseId(categoryId)) : Id64.invalid;
    }
    /** True if this iModel has an [EcefLocation]($docs/learning/glossary#ecefLocation). */
    get isGeoLocated() { return undefined !== this._ecefLocation; }
    /** Get the Transform from this iModel's Spatial coordinates to ECEF coordinates using its [[IModel.ecefLocation]].
     * @throws IModelError if [[isGeoLocated]] is false.
     */
    getEcefTransform() {
        if (undefined === this._ecefLocation)
            throw new IModelError(GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
        return this._ecefLocation.getTransform();
    }
    /** Convert a point in this iModel's Spatial coordinates to an ECEF point using its [[IModel.ecefLocation]].
     * @param spatial A point in the iModel's spatial coordinates
     * @param result If defined, use this for output
     * @returns A Point3d in ECEF coordinates
     * @throws IModelError if [[isGeoLocated]] is false.
     */
    spatialToEcef(spatial, result) { return this.getEcefTransform().multiplyPoint3d(spatial, result); }
    /** Convert a point in ECEF coordinates to a point in this iModel's Spatial coordinates using its [[ecefLocation]].
     * @param ecef A point in ECEF coordinates
     * @param result If defined, use this for output
     * @returns A Point3d in this iModel's spatial coordinates
     * @throws IModelError if [[isGeoLocated]] is false.
     * @note The resultant point will only be meaningful if the ECEF coordinate is close on the earth to the iModel.
     */
    ecefToSpatial(ecef, result) { return expectDefined(this.getEcefTransform().multiplyInversePoint3d(ecef, result)); }
    /** Convert a point in this iModel's Spatial coordinates to a [[Cartographic]] using its [[IModel.ecefLocation]].
     * @param spatial A point in the iModel's spatial coordinates
     * @param result If defined, use this for output
     * @returns A Cartographic location
     * @throws IModelError if [[isGeoLocated]] is false.
     */
    spatialToCartographicFromEcef(spatial, result) { return expectDefined(Cartographic.fromEcef(this.spatialToEcef(spatial), result)); }
    /** Convert a [[Cartographic]] to a point in this iModel's Spatial coordinates using its [[IModel.ecefLocation]].
     * @param cartographic A cartographic location
     * @param result If defined, use this for output
     * @returns A point in this iModel's spatial coordinates
     * @throws IModelError if [[isGeoLocated]] is false.
     * @note The resultant point will only be meaningful if the ECEF coordinate is close on the earth to the iModel.
     */
    cartographicToSpatialFromEcef(cartographic, result) { return this.ecefToSpatial(cartographic.toEcef(result), result); }
}
//# sourceMappingURL=IModel.js.map