"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Geometry
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.CartographicRange = exports.Cartographic = void 0;
const core_geometry_1 = require("@itwin/core-geometry");
const core_bentley_1 = require("@itwin/core-bentley");
/** A position on the earth defined by longitude, latitude, and height above the [WGS84](https://en.wikipedia.org/wiki/World_Geodetic_System) ellipsoid.
 * @public
 */
class Cartographic {
    /**
     * @param longitude longitude, in radians.
     * @param latitude latitude, in radians.
     * @param height The height, in meters, above the ellipsoid.
     */
    constructor(longitude = 0, latitude = 0, height = 0) {
        this.longitude = longitude;
        this.latitude = latitude;
        this.height = height;
    }
    /** Create a Cartographic object with longitude, latitude, and height of zero. */
    static createZero() {
        return new Cartographic(0, 0, 0);
    }
    /** Create a new Cartographic from longitude and latitude specified in radians.
     * @param args an object containing a longitude, latitude, and an optional height property. The longitude and latitude properties are numbers specified in radians. The height property, if specified, is a number which contains the height in meters above the ellipsoid; if undefined, this height will default to zero.
     * @param result The object onto which to store the result.
     */
    static fromRadians(args, result) {
        if (!result)
            return new Cartographic(args.longitude, args.latitude, args.height);
        result.longitude = args.longitude;
        result.latitude = args.latitude;
        result.height = args.height !== undefined ? args.height : 0;
        return result;
    }
    /** Create a JSON representation of a Cartographic object. */
    toJSON() {
        return {
            latitude: this.latitude,
            longitude: this.longitude,
            height: this.height,
        };
    }
    /** Freeze this Cartographic */
    freeze() {
        return Object.freeze(this);
    }
    /** longitude, in degrees */
    get longitudeDegrees() {
        return core_geometry_1.Angle.radiansToDegrees(this.longitude);
    }
    /** latitude, in degrees */
    get latitudeDegrees() {
        return core_geometry_1.Angle.radiansToDegrees(this.latitude);
    }
    /** return the geocentric latitude angle for the input geodetic latitude angle (both in radians).
     * @param geodeticLatitude geodetic latitude angle in radians
     */
    static geocentricLatitudeFromGeodeticLatitude(geodeticLatitude) {
        return Math.atan(Cartographic._oneMinusF * Cartographic._oneMinusF * Math.tan(geodeticLatitude));
    }
    /** return the parametric latitude angle for the input geodetic latitude angle (both in radians).  The parametric latitude
     * is appropriate for input to the Ellipsoid methods.
     * @param geodeticLatitude geodetic latitude angle in radians
     */
    static parametricLatitudeFromGeodeticLatitude(geodeticLatitude) {
        return Math.atan(Cartographic._oneMinusF * Cartographic._oneMinusF * Cartographic._equatorOverPolar * Math.tan(geodeticLatitude));
    }
    /** Create a new Cartographic from longitude and latitude specified in degrees. The values in the resulting object will be in radians.
     * @param args an object containing a longitude, latitude, and an optional height property. The longitude and latitude properties are numbers specified in degrees. The height property, if specified, is a number which contains the height in meters above the ellipsoid; if undefined, this height will default to zero.
     * @param result The object onto which to store the result.
     */
    static fromDegrees(args, result) {
        return Cartographic.fromRadians({ longitude: core_geometry_1.Angle.degreesToRadians(args.longitude), latitude: core_geometry_1.Angle.degreesToRadians(args.latitude), height: args.height }, result);
    }
    /** Create a new Cartographic from longitude and latitude in [Angle]($geometry)s. The values in the resulting object will be in radians.
     * @param args an object containing a longitude, latitude, and an optional height property. The longitude and latitude properties are Angle objects. The height property, if specified, is a number which contains the height in meters above the ellipsoid; if undefined, this height will default to zero.
     * @param result The object into which to store the result (optional)
     */
    static fromAngles(args, result) {
        return Cartographic.fromRadians({ longitude: args.longitude.radians, latitude: args.latitude.radians, height: args.height }, result);
    }
    /** Creates a new Cartographic from an [ECEF](https://en.wikipedia.org/wiki/ECEF) position.
     * @param cartesian The position, in ECEF, to convert to cartographic representation.
     * @param [result] The object onto which to store the result.
     * @returns The modified result parameter, new Cartographic instance if none was provided, or undefined if the cartesian is at the center of the ellipsoid.
     */
    static fromEcef(cartesian, result) {
        const oneOverRadiiSquared = Cartographic._wgs84OneOverRadiiSquared;
        const p = Cartographic.scalePointToGeodeticSurface(cartesian, Cartographic._cartesianToCartographicP);
        if (!p)
            return undefined;
        const n = Cartographic._cartesianToCartographicN;
        Cartographic.multiplyComponents(p, oneOverRadiiSquared, n);
        Cartographic.normalize(n, n);
        const h = p.vectorTo(cartesian, Cartographic._cartesianToCartographicH);
        const longitude = Math.atan2(n.y, n.x);
        const latitude = Math.asin(n.z);
        const height = Math.sign(h.dotProduct(cartesian)) * h.magnitude();
        if (!result)
            return new Cartographic(longitude, latitude, height);
        result.longitude = longitude;
        result.latitude = latitude;
        result.height = height;
        return result;
    }
    /** Scale point to geodetic surface
     * @param point in ECEF to scale to the surface
     * @param [result] The object onto which to store the result.
     * @returns a point on the geodetic surface
     */
    static scalePointToGeodeticSurface(point, result) {
        const oneOverRadii = Cartographic._wgs84OneOverRadii;
        const oneOverRadiiSquared = Cartographic._wgs84OneOverRadiiSquared;
        const centerToleranceSquared = Cartographic._wgs84CenterToleranceSquared;
        return Cartographic._scaleToGeodeticSurface(point, oneOverRadii, oneOverRadiiSquared, centerToleranceSquared, result);
    }
    /** Duplicates a Cartographic. */
    clone(result) {
        if (!result)
            return new Cartographic(this.longitude, this.latitude, this.height);
        result.longitude = this.longitude;
        result.latitude = this.latitude;
        result.height = this.height;
        return result;
    }
    /** Return true if this Cartographic is the same as right */
    equals(right) {
        return (this === right) ||
            ((this.longitude === right.longitude) &&
                (this.latitude === right.latitude) &&
                (this.height === right.height));
    }
    /** Compares this Cartographic component-wise and returns true if they are within the provided epsilon, */
    equalsEpsilon(right, epsilon) {
        return (this === right) ||
            ((Math.abs(this.longitude - right.longitude) <= epsilon) &&
                (Math.abs(this.latitude - right.latitude) <= epsilon) &&
                (Math.abs(this.height - right.height) <= epsilon));
    }
    static normalize(cartesian, result) {
        const magnitude = cartesian.magnitude();
        result.x = cartesian.x / magnitude;
        result.y = cartesian.y / magnitude;
        result.z = cartesian.z / magnitude;
    }
    static multiplyComponents(left, right, result) {
        result.x = left.x * right.x;
        result.y = left.y * right.y;
        result.z = left.z * right.z;
    }
    static scalePoint(cartesian, scalar, result) {
        result.x = cartesian.x * scalar;
        result.y = cartesian.y * scalar;
        result.z = cartesian.z * scalar;
    }
    static addPoints(left, right, result) {
        result.x = left.x + right.x;
        result.y = left.y + right.y;
        result.z = left.z + right.z;
    }
    /** Create a string representing this cartographic in the format '(longitude, latitude, height)'. */
    toString() { return `(${this.longitude}, ${this.latitude}, ${this.height})`; }
    static _scaleToGeodeticSurface(cartesian, oneOverRadii, oneOverRadiiSquared, centerToleranceSquared, result) {
        const positionX = cartesian.x;
        const positionY = cartesian.y;
        const positionZ = cartesian.z;
        const oneOverRadiiX = oneOverRadii.x;
        const oneOverRadiiY = oneOverRadii.y;
        const oneOverRadiiZ = oneOverRadii.z;
        const x2 = positionX * positionX * oneOverRadiiX * oneOverRadiiX;
        const y2 = positionY * positionY * oneOverRadiiY * oneOverRadiiY;
        const z2 = positionZ * positionZ * oneOverRadiiZ * oneOverRadiiZ;
        // Compute the squared ellipsoid norm.
        const squaredNorm = x2 + y2 + z2;
        const ratio = Math.sqrt(1.0 / squaredNorm);
        // As an initial approximation, assume that the radial intersection is the projection point.
        const intersection = Cartographic._scaleToGeodeticSurfaceIntersection;
        Cartographic.scalePoint(cartesian, ratio, intersection);
        // If the position is near the center, the iteration will not converge.
        if (squaredNorm < centerToleranceSquared) {
            return !isFinite(ratio) ? undefined : core_geometry_1.Point3d.createFrom(intersection, result);
        }
        const oneOverRadiiSquaredX = oneOverRadiiSquared.x;
        const oneOverRadiiSquaredY = oneOverRadiiSquared.y;
        const oneOverRadiiSquaredZ = oneOverRadiiSquared.z;
        // Use the gradient at the intersection point in place of the true unit normal.
        // The difference in magnitude will be absorbed in the multiplier.
        const gradient = Cartographic._scaleToGeodeticSurfaceGradient;
        gradient.x = intersection.x * oneOverRadiiSquaredX * 2.0;
        gradient.y = intersection.y * oneOverRadiiSquaredY * 2.0;
        gradient.z = intersection.z * oneOverRadiiSquaredZ * 2.0;
        // Compute the initial guess at the normal vector multiplier, lambda.
        let lambda = (1.0 - ratio) * cartesian.magnitude() / (0.5 * gradient.magnitude());
        let correction = 0.0;
        let func;
        let denominator;
        let xMultiplier;
        let yMultiplier;
        let zMultiplier;
        let xMultiplier2;
        let yMultiplier2;
        let zMultiplier2;
        let xMultiplier3;
        let yMultiplier3;
        let zMultiplier3;
        do {
            lambda -= correction;
            xMultiplier = 1.0 / (1.0 + lambda * oneOverRadiiSquaredX);
            yMultiplier = 1.0 / (1.0 + lambda * oneOverRadiiSquaredY);
            zMultiplier = 1.0 / (1.0 + lambda * oneOverRadiiSquaredZ);
            xMultiplier2 = xMultiplier * xMultiplier;
            yMultiplier2 = yMultiplier * yMultiplier;
            zMultiplier2 = zMultiplier * zMultiplier;
            xMultiplier3 = xMultiplier2 * xMultiplier;
            yMultiplier3 = yMultiplier2 * yMultiplier;
            zMultiplier3 = zMultiplier2 * zMultiplier;
            func = x2 * xMultiplier2 + y2 * yMultiplier2 + z2 * zMultiplier2 - 1.0;
            // "denominator" here refers to the use of this expression in the velocity and acceleration
            // computations in the sections to follow.
            denominator = x2 * xMultiplier3 * oneOverRadiiSquaredX + y2 * yMultiplier3 * oneOverRadiiSquaredY + z2 * zMultiplier3 * oneOverRadiiSquaredZ;
            const derivative = -2.0 * denominator;
            correction = func / derivative;
        } while (Math.abs(func) > 0.01);
        if (!result)
            return new core_geometry_1.Point3d(positionX * xMultiplier, positionY * yMultiplier, positionZ * zMultiplier);
        result.x = positionX * xMultiplier;
        result.y = positionY * yMultiplier;
        result.z = positionZ * zMultiplier;
        return result;
    }
    /** Return an ECEF point from a Cartographic point */
    toEcef(result) {
        const cosLatitude = Math.cos(this.latitude);
        const scratchN = Cartographic._scratchN;
        const scratchK = Cartographic._scratchK;
        scratchN.x = cosLatitude * Math.cos(this.longitude);
        scratchN.y = cosLatitude * Math.sin(this.longitude);
        scratchN.z = Math.sin(this.latitude);
        Cartographic.normalize(scratchN, scratchN);
        Cartographic.multiplyComponents(Cartographic._wgs84RadiiSquared, scratchN, scratchK);
        const gamma = Math.sqrt(scratchN.dotProduct(scratchK));
        Cartographic.scalePoint(scratchK, 1.0 / gamma, scratchK);
        Cartographic.scalePoint(scratchN, this.height, scratchN);
        result = result ? result : new core_geometry_1.Point3d();
        Cartographic.addPoints(scratchK, scratchN, result);
        return result;
    }
}
exports.Cartographic = Cartographic;
Cartographic._oneMinusF = 1 - (core_geometry_1.Constant.earthRadiusWGS84.equator - core_geometry_1.Constant.earthRadiusWGS84.polar) / core_geometry_1.Constant.earthRadiusWGS84.equator;
Cartographic._equatorOverPolar = core_geometry_1.Constant.earthRadiusWGS84.equator / core_geometry_1.Constant.earthRadiusWGS84.polar;
Cartographic._cartesianToCartographicN = new core_geometry_1.Point3d();
Cartographic._cartesianToCartographicP = new core_geometry_1.Point3d();
Cartographic._cartesianToCartographicH = new core_geometry_1.Vector3d();
Cartographic._wgs84OneOverRadii = new core_geometry_1.Point3d(1.0 / 6378137.0, 1.0 / 6378137.0, 1.0 / 6356752.3142451793);
Cartographic._wgs84OneOverRadiiSquared = new core_geometry_1.Point3d(1.0 / (6378137.0 * 6378137.0), 1.0 / (6378137.0 * 6378137.0), 1.0 / (6356752.3142451793 * 6356752.3142451793));
Cartographic._wgs84RadiiSquared = new core_geometry_1.Point3d(6378137.0 * 6378137.0, 6378137.0 * 6378137.0, 6356752.3142451793 * 6356752.3142451793);
Cartographic._wgs84CenterToleranceSquared = 0.1;
Cartographic._scratchN = new core_geometry_1.Vector3d();
Cartographic._scratchK = new core_geometry_1.Vector3d();
Cartographic._scaleToGeodeticSurfaceIntersection = new core_geometry_1.Point3d();
Cartographic._scaleToGeodeticSurfaceGradient = new core_geometry_1.Point3d();
/** A cartographic range representing a rectangular region if low longitude/latitude > high then area crossing seam is indicated.
 * @public
 */
class CartographicRange {
    constructor(spatialRange, spatialToEcef) {
        this._ranges = [];
        // These following are used to preserve the min/max latitude and longitudes.
        // The longitudes are raw values and may cross over the -PI or 2PI boundaries.
        this._minLongitude = 0;
        this._maxLongitude = 0;
        this._minLatitude = 0;
        this._maxLatitude = 0;
        // Compute 8 corners in spatial coordinate system before converting to ECEF
        // We want a box oriented in the spatial coordinate system and not in the ECEF coordinate system
        const spatialCorners = spatialRange.corners();
        const ecefCorners = spatialToEcef.multiplyPoint3dArray(spatialCorners);
        let low, high;
        for (const ecefCorner of ecefCorners) {
            const geoPt = Cartographic.fromEcef(ecefCorner);
            if (!geoPt)
                continue;
            if (undefined === low || undefined === high) {
                low = geoPt;
                high = geoPt.clone();
                continue;
            }
            low.latitude = Math.min(low.latitude, geoPt.latitude);
            low.longitude = Math.min(low.longitude, geoPt.longitude);
            high.latitude = Math.max(high.latitude, geoPt.latitude);
            high.longitude = Math.max(high.longitude, geoPt.longitude);
        }
        if (!low || !high) {
            (0, core_bentley_1.assert)(false);
            return;
        }
        const longitudeRanges = [];
        this._minLongitude = Math.min(low.longitude, high.longitude), this._maxLongitude = Math.max(low.longitude, high.longitude);
        if (this._maxLongitude - this._minLongitude > core_geometry_1.Angle.piRadians) {
            longitudeRanges.push(core_geometry_1.Range1d.createXX(0.0, this._minLongitude));
            longitudeRanges.push(core_geometry_1.Range1d.createXX(this._maxLongitude, core_geometry_1.Angle.pi2Radians));
        }
        else {
            longitudeRanges.push(core_geometry_1.Range1d.createXX(this._minLongitude, this._maxLongitude));
        }
        for (const longitudeRange of longitudeRanges) {
            this._minLatitude = Math.min(low.latitude, high.latitude), this._maxLatitude = Math.max(low.latitude, high.latitude);
            if (this._maxLatitude - this._minLatitude > core_geometry_1.Angle.piOver2Radians) {
                this._ranges.push(core_geometry_1.Range2d.createXYXY(longitudeRange.low, 0.0, longitudeRange.high, this._minLatitude));
                this._ranges.push(core_geometry_1.Range2d.createXYXY(longitudeRange.low, this._maxLatitude, longitudeRange.high, core_geometry_1.Angle.piRadians));
            }
            else {
                this._ranges.push(core_geometry_1.Range2d.createXYXY(longitudeRange.low, this._minLatitude, longitudeRange.high, this._maxLatitude));
            }
        }
    }
    intersectsRange(other) {
        for (const range of this._ranges)
            for (const otherRange of other._ranges)
                if (range.intersectsRange(otherRange))
                    return true;
        return false;
    }
    /** This method returns the raw latitude / longitude for the range in a Range2d object.
     * The X value represents the longitude and the Y value the latitudes.
     * Y values are kept between -PI and +PI while
     * longitude values can be expressed in any range between -2PI to +2PI
     * given the minimum longitude is always smaller numerically than the maximum longitude.
     * Note that usually the longitudes are usually by convention in the range of -PI to PI except
     * for ranges that overlap the -PI/+PI frontier in which case either representation is acceptable.
     */
    getLongitudeLatitudeBoundingBox() {
        return core_geometry_1.Range2d.createXYXY(this._minLongitude, this._minLatitude, this._maxLongitude, this._maxLatitude);
    }
}
exports.CartographicRange = CartographicRange;
//# sourceMappingURL=Cartographic.js.map