"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 CartesianGeometry
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Angle = void 0;
const Geometry_1 = require("../Geometry");
/**
 * An `Angle` carries the numeric value of an angle, with methods to allow (require!) callers to
 * be clear about whether their angle is degrees or radians.
 * * After the Angle object is created, the callers should not know or care whether it is stored in
 * `degrees` or `radians` because both are available if requested by caller.
 * * The various access method are named so that callers can specify whether untyped numbers passed in or
 * out are degrees or radians.
 * * Visualization can be found at https://www.itwinjs.org/sandbox/SaeedTorabi/AngleSweep
 * @public
 */
class Angle {
    /** maximal accuracy value of pi/12 (15 degrees), in radians */
    static piOver12Radians = 0.26179938779914946;
    /** maximal accuracy value of pi/4 (45 degrees), in radians */
    static piOver4Radians = 7.853981633974483e-001;
    /** maximal accuracy value of pi/2 (90 degrees), in radians */
    static piOver2Radians = 1.5707963267948966e+000;
    /** maximal accuracy value of pi (180 degrees), in radians */
    static piRadians = 3.141592653589793e+000;
    /** maximal accuracy value of 3*pi/2 (270 degrees), in radians */
    static pi3Over2Radians = 4.71238898038469e+000;
    /** maximal accuracy value of 2*pi (360 degrees), in radians */
    static pi2Radians = 6.283185307179586e+000;
    /** scale factor for converting radians to degrees */
    static degreesPerRadian = (45.0 / Angle.piOver4Radians);
    /** scale factor for converting degrees to radians */
    static radiansPerDegree = (Angle.piOver4Radians / 45.0);
    _radians;
    _degrees;
    constructor(radians = 0, degrees) {
        this._radians = radians;
        this._degrees = degrees;
    }
    /** Return a new angle with the same content. */
    clone() {
        return new Angle(this._radians, this._degrees);
    }
    /** Freeze this instance so it is read-only */
    freeze() {
        return Object.freeze(this);
    }
    /**
     * Return a new Angle object for angle given in degrees.
     * @param degrees angle in degrees
     */
    static createDegrees(degrees) {
        return new Angle(Angle.degreesToRadians(degrees), degrees);
    }
    /**
     * Return a (new) Angle object for a value given in radians.
     * @param radians angle in radians
     */
    static createRadians(radians) {
        return new Angle(radians);
    }
    /**
     * Return a new `Angle` object with the default "small" angle measurement specified by [[Geometry.smallAngleRadians]].
     */
    static createSmallAngle() {
        return new Angle(Geometry_1.Geometry.smallAngleRadians);
    }
    /**
     * Return a (new) Angle object that is interpolated between two inputs (based on a fraction)
     * @param angle0 first angle in radians
     * @param fraction the interpolation fraction
     * @param angle1 second angle in radians
     */
    static createInterpolate(angle0, fraction, angle1) {
        return new Angle(Geometry_1.Geometry.interpolate(angle0.radians, fraction, angle1.radians));
    }
    /**
     * Return a (new) Angle object, with angle scaled from existing angle.
     * @param scale scale factor to apply to angle.
     */
    cloneScaled(scale) {
        return new Angle(this.radians * scale);
    }
    /**
     * Set this angle to a value given in radians.
     * @param radians angle given in radians
     */
    setRadians(radians) {
        this._radians = radians;
        this._degrees = undefined;
    }
    /**
     * Set this angle to a value given in degrees.
     * @param degrees angle given in degrees.
     */
    setDegrees(degrees) {
        this._radians = Angle.degreesToRadians(degrees);
        this._degrees = degrees;
    }
    /** Create an angle for a full circle. */
    static create360() {
        return new Angle(Math.PI * 2.0, 360.0);
    }
    /**
     * Create a (strongly typed) Angle whose tangent is `numerator/denominator`, using the signs of both in
     * determining the (otherwise ambiguous) quadrant.
     * @param numerator numerator for tangent
     * @param denominator denominator for tangent
     */
    static createAtan2(numerator, denominator) {
        return new Angle(Math.atan2(numerator, denominator));
    }
    /**
     * Copy all contents of `other` to this Angle.
     * @param other source data
     */
    setFrom(other) {
        this._radians = other._radians;
        this._degrees = other._degrees;
    }
    /**
     * Set an Angle from a JSON object
     * * A simple number is considered as degrees.
     * * specified `json.degrees` or `json._degrees` is degree value.
     * * specified `son.radians` or `json._radians` is radians value.
     * @param json object from JSON.parse. If a number, value is in *DEGREES*
     * @param defaultValRadians if json is undefined, default value in radians.
     */
    setFromJSON(json, defaultValRadians) {
        this._radians = defaultValRadians ? defaultValRadians : 0;
        if (!json)
            return;
        if (typeof json === "number") {
            this.setDegrees(json);
        }
        else if (typeof json.degrees === "number") {
            this.setDegrees(json.degrees);
        }
        else if (typeof json._degrees === "number") {
            this.setDegrees(json._degrees);
        }
        else if (typeof json.radians === "number") {
            this.setRadians(json.radians);
        }
        else if (typeof json._radians === "number") {
            this.setRadians(json._radians);
        }
    }
    /**
     * Create an Angle from a JSON object
     * @param json object from JSON.parse. If a number, value is in *DEGREES*
     * @param defaultValRadians if json is undefined, default value in radians.
     * @return a new Angle
     */
    static fromJSON(json, defaultValRadians) {
        const val = new Angle();
        val.setFromJSON(json, defaultValRadians);
        return val;
    }
    /** Convert an Angle to a JSON object as a number in degrees */
    toJSON() {
        return this.degrees;
    }
    /** Return a json object with radians keyword, e.g. `{ radians: 0.10}` */
    toJSONRadians() {
        return { radians: this.radians };
    }
    /** Return the angle measured in radians. */
    get radians() {
        return this._radians;
    }
    /** Return the angle measured in degrees. */
    get degrees() {
        return this._degrees !== undefined ? this._degrees : Angle.radiansToDegrees(this._radians);
    }
    /**
     * Convert an angle in degrees to radians.
     * @param degrees angle in degrees
     */
    static degreesToRadians(degrees) {
        return degrees * Math.PI / 180;
    }
    /**
     * Convert an angle in radians to degrees.
     * @param degrees angle in radians
     */
    static radiansToDegrees(radians) {
        if (radians < 0)
            return -Angle.radiansToDegrees(-radians);
        // Now radians is positive ...
        const pi = Math.PI;
        const factor = 180.0 / pi;
        /* the following if statements are for round-off reasons. The problem is that no IEEE number is
          * an exact hit for any primary multiple of pi (90, 180, etc). The following is supposed to have
          * a better chance that if the input was computed by direct assignment from 90, 180, etc degrees
          * it will return exactly 90,180 etc.
          */
        if (radians <= 0.25 * pi)
            return factor * radians;
        if (radians < 0.75 * pi)
            return 90.0 + 180 * ((radians - 0.5 * pi) / pi);
        if (radians <= 1.25 * pi)
            return 180.0 + 180 * ((radians - pi) / pi);
        if (radians <= 1.75 * pi)
            return 270.0 + 180 * ((radians - 1.5 * pi) / pi);
        // all larger radians reference from 360 degrees (2PI)
        return 360.0 + 180 * ((radians - 2.0 * pi) / pi);
    }
    /** Return the cosine of this Angle object's angle */
    cos() {
        return Math.cos(this._radians);
    }
    /** Return the sine of this Angle object's angle */
    sin() {
        return Math.sin(this._radians);
    }
    /** Return the tangent of this Angle object's angle */
    tan() {
        return Math.tan(this._radians);
    }
    /** Test if a radians (absolute) value is nearly 2PI or larger. */
    static isFullCircleRadians(radians) {
        return Math.abs(radians) >= Geometry_1.Geometry.fullCircleRadiansMinusSmallAngle;
    }
    /** Test if the radians value is a half circle */
    static isHalfCircleRadians(radians) {
        return Math.abs(Math.abs(radians) - Math.PI) <= Geometry_1.Geometry.smallAngleRadians;
    }
    /** Test if the angle is a full circle */
    get isFullCircle() {
        return Angle.isFullCircleRadians(this._radians);
    }
    /** Test if the angle is a half circle (in either direction) */
    get isHalfCircle() {
        return Angle.isHalfCircleRadians(this._radians);
    }
    /**
     * Adjust a degrees value so it is in [0, 360].
     * * Positive multiples of 360 return 0; negative multiples return 360.
     */
    static adjustDegrees0To360(degrees) {
        if (degrees >= 0) {
            const period = 360.0;
            if (degrees < period)
                return degrees;
            const numPeriods = Math.floor(degrees / period);
            return degrees - numPeriods * period;
        }
        else if (degrees < 0) {
            // negative angle ...
            return 360.0 - Angle.adjustDegrees0To360(-degrees);
        }
        // fall through for Nan (disaster) !!!
        return 0;
    }
    /** Adjust a degrees value so it is in [-180, 180]. */
    static adjustDegreesSigned180(degrees) {
        if (Math.abs(degrees) <= 180.0)
            return degrees;
        if (degrees >= 0) {
            const period = 360.0;
            const numPeriods = 1 + Math.floor((degrees - 180.0) / period);
            return degrees - numPeriods * period;
        }
        else if (degrees < 0) {
            // negative angle ...
            return -Angle.adjustDegreesSigned180(-degrees);
        }
        // fall through for NaN disaster.
        return 0;
    }
    /**
     * Adjust a radians value so it is in [0, 2pi].
     * * Nonnegative multiples of 2pi return 0; negative multiples return 2pi.
     */
    static adjustRadians0To2Pi(radians) {
        if (radians >= 0) {
            const period = Math.PI * 2.0;
            if (radians < period)
                return radians;
            const numPeriods = Math.floor(radians / period);
            return radians - numPeriods * period;
        }
        else if (radians < 0) {
            // negative angle ...
            return Math.PI * 2.0 - Angle.adjustRadians0To2Pi(-radians);
        }
        // fall through for NaN disaster.
        return 0;
    }
    /**
     * Adjust a radians value so it is in [0, 2pi).
     * * All multiples of 2pi (within `Geometry.smallAngleRadians`) return 0.
     */
    static adjustRadians0ToLessThan2Pi(radians) {
        if (Angle.isAlmostEqualRadiansAllowPeriodShift(radians, 0))
            radians = 0;
        return this.adjustRadians0To2Pi(radians);
    }
    /** Adjust a radians value so it is in [-pi, pi] */
    static adjustRadiansMinusPiPlusPi(radians) {
        if (Math.abs(radians) <= Math.PI)
            return radians;
        if (radians >= 0) {
            const period = Math.PI * 2.0;
            const numPeriods = 1 + Math.floor((radians - Math.PI) / period);
            return radians - numPeriods * period;
        }
        else if (radians < 0) {
            // negative angle ...
            return -Angle.adjustRadiansMinusPiPlusPi(-radians);
        }
        // fall through for NaN disaster.
        return 0;
    }
    /** Return a (newly allocated) Angle object with value 0 radians */
    static zero() {
        return new Angle(0);
    }
    /** Test if the angle is exactly zero. */
    get isExactZero() {
        return this.radians === 0;
    }
    /** Test if the angle is almost zero (within tolerance `Geometry.smallAngleRadians`) */
    get isAlmostZero() {
        return Math.abs(this.radians) < Geometry_1.Geometry.smallAngleRadians;
    }
    /** Test if the angle is almost a north or south pole (within tolerance `Geometry.smallAngleRadians`) */
    get isAlmostNorthOrSouthPole() {
        return Angle.isHalfCircleRadians(this.radians * 2.0);
    }
    /** Create an angle object with degrees adjusted into 0..360. */
    static createDegreesAdjustPositive(degrees) {
        return Angle.createDegrees(Angle.adjustDegrees0To360(degrees));
    }
    /** Create an angle object with degrees adjusted into -180..180. */
    static createDegreesAdjustSigned180(degrees) {
        return Angle.createDegrees(Angle.adjustDegreesSigned180(degrees));
    }
    /**
     * Test if two radian values are equivalent, allowing shift by full circle (i.e. by a multiple of `2*PI`)
     * @param radiansA first radians value
     * @param radiansB second radians value
     * @param radianTol radian tolerance with default value of Geometry.smallAngleRadians
     */
    static isAlmostEqualRadiansAllowPeriodShift(radiansA, radiansB, radianTol = Geometry_1.Geometry.smallAngleRadians) {
        const delta = Math.abs(radiansA - radiansB);
        if (delta <= radianTol)
            return true;
        const period = Math.PI * 2.0;
        if (Math.abs(delta - period) <= radianTol)
            return true;
        const numPeriod = Math.round(delta / period);
        const delta1 = delta - numPeriod * period;
        return Math.abs(delta1) <= radianTol;
    }
    /**
   * Test if this angle has magnitude no greater than that of `other`.
   * @param other the other angle
   */
    isMagnitudeLessThanOrEqual(other) {
        return Math.abs(this.radians) <= Math.abs(other.radians);
    }
    /**
     * Test if this angle and `other` are equivalent, allowing shift by full circle (i.e., multiples of `2 * PI`).
     * @param other the other angle
     * @param radianTol radian tolerance with default value of Geometry.smallAngleRadians
     */
    isAlmostEqualAllowPeriodShift(other, radianTol = Geometry_1.Geometry.smallAngleRadians) {
        return Angle.isAlmostEqualRadiansAllowPeriodShift(this._radians, other._radians, radianTol);
    }
    /**
     * Test if two angles (in radians) are almost equal, NOT allowing shift by full circle (i.e., multiples of `2 * PI`).
     * @param radiansA first radians value
     * @param radiansB second radians value
     * @param radianTol radian tolerance with default value of Geometry.smallAngleRadians
     */
    static isAlmostEqualRadiansNoPeriodShift(radiansA, radiansB, radianTol = Geometry_1.Geometry.smallAngleRadians) {
        return Math.abs(radiansA - radiansB) < radianTol;
    }
    /**
     * Test if this angle and `other` are almost equal, NOT allowing shift by full circle (i.e., multiples of `2 * PI`).
     * @param other the other angle
     * @param radianTol radian tolerance with default value of Geometry.smallAngleRadians
     */
    isAlmostEqualNoPeriodShift(other, radianTol = Geometry_1.Geometry.smallAngleRadians) {
        return Angle.isAlmostEqualRadiansNoPeriodShift(this._radians, other._radians, radianTol);
    }
    /**
     * Test if this angle and `other` are almost equal, NOT allowing shift by full circle (i.e., multiples of `2 * PI`).
     * * This function is same as isAlmostEqualRadiansNoPeriodShift. Please use isAlmostEqualRadiansNoPeriodShift.
     * @param other the other angle
     * @param radianTol radian tolerance with default value of Geometry.smallAngleRadians
     */
    isAlmostEqual(other, radianTol = Geometry_1.Geometry.smallAngleRadians) {
        return this.isAlmostEqualNoPeriodShift(other, radianTol);
    }
    /**
     * Test if dot product values indicate non-zero length perpendicular vectors.
     * @param dotUU dot product of vectorU with itself
     * @param dotVV dot product of vectorV with itself
     * @param dotUV dot product of vectorU with vectorV
     */
    static isPerpendicularDotSet(dotUU, dotVV, dotUV) {
        return dotUU > Geometry_1.Geometry.smallMetricDistanceSquared
            && dotVV > Geometry_1.Geometry.smallMetricDistanceSquared
            && dotUV * dotUV <= Geometry_1.Geometry.smallAngleRadiansSquared * dotUU * dotVV;
    }
    /**
     * Compute the angle A given r*cos(2A) and r*sin(2A) for some nonnegative scalar r.
     * * This function assumes the input arguments are related to an angle between -PI and PI.
     * * This function returns an angle between -PI and PI.
     * @param rCos2A scaled cosine value of twice the angle A.
     * @param rSin2A scaled sine value of twice the angle A.
     * @return cos(A), sin(A) and A in radians
     */
    static trigValuesToHalfAngleTrigValues(rCos2A, rSin2A) {
        const r = Geometry_1.Geometry.hypotenuseXY(rCos2A, rSin2A);
        if (r < Geometry_1.Geometry.smallMetricDistance) {
            return { c: 1.0, s: 0.0, radians: 0.0 }; // angle = 0
        }
        else {
            /* If the caller really gave you sine and cosine values, r should be 1.  However,
             * to allow scaled values -- e.g. the x and y components of any vector -- we normalize
             * right here. This adds an extra sqrt and two divisions, but improves
             * both the usefulness and robustness of the computation.
             */
            let cosA;
            let sinA = 0.0;
            const cos2A = rCos2A / r;
            const sin2A = rSin2A / r;
            if (cos2A >= 0.0) { // 2A is in NE and SE quadrants, A in same quadrant
                cosA = Math.sqrt(0.5 * (1.0 + cos2A)); // half angle formula. Use +root since cosA >= 0
                sinA = sin2A / (2.0 * cosA); // double angle formula
            }
            else {
                if (sin2A > 0.0) { // 2A in NW quadrant. A in NE quadrant
                    sinA = Math.sqrt(0.5 * (1.0 - cos2A)); // half angle formula. Use +root since sinA > 0
                }
                else { // 2A in SW quadrant. A in SE quadrant
                    sinA = -Math.sqrt(0.5 * (1.0 - cos2A)); // half angle formula. Use -root since sinA <= 0
                }
                cosA = sin2A / (2.0 * sinA); // double angle formula
            }
            return { c: cosA, s: sinA, radians: Math.atan2(sinA, cosA) };
        }
    }
    /** If value is close to -1, -0.5, 0, 0.5, 1, adjust it to the exact value. */
    static cleanupTrigValue(value, tolerance = Geometry_1.Geometry.smallFloatingPoint) {
        const absValue = Math.abs(value);
        if (absValue <= tolerance)
            return 0;
        let a = Math.abs(absValue - 0.5);
        if (a <= tolerance)
            return value < 0.0 ? -0.5 : 0.5;
        a = Math.abs(absValue - 1.0);
        if (a <= tolerance)
            return value < 0.0 ? -1.0 : 1.0;
        return value;
    }
    /**
     * Return the half angle cosine, sine, and radians for the given vector dot products.
     * * These values arise e.g. in the computation performed in `Arc3d.toScaledMatrix3d`.
     * * Let vectors U and V define the ellipse x(t) = c + U cos(t) + V sin(t). We seek an angle t0
     * such that the radial vector W(t0) := x(t0) - c is perpendicular to the ellipse.
     * * Then 0 = W(t0).x'(t0) = (U cos(t0) + V sin(t0)).(V cos(t0) - U sin(t0)) = U.V cos(2t0) + 0.5 (V.V - U.U) sin(2t0)
     * implies tan(2t0) = sin(2t0) / cos(2t0) = 2 U.V / (U.U - V.V), i.e., t0 can be computed given the input dot products.
     * Math details can be found at docs/learning/geometry/Angle.md
     * @param dotUU dot product of vectorU with itself
     * @param dotVV dot product of vectorV with itself
     * @param dotUV dot product of vectorU with vectorV
     * @param favorZero whether to allow a tight tolerance for returning t0 = 0 (default true).
     * When dotUV is near zero, U and V are nearly perpendicular, and the returned angle is near zero.
     * @return the angle t0 and its cosine and sine.
     */
    static dotProductsToHalfAngleTrigValues(dotUU, dotVV, dotUV, favorZero = true) {
        const cos2t0 = dotUU - dotVV;
        const sin2t0 = 2.0 * dotUV;
        if (favorZero && Math.abs(sin2t0) < Geometry_1.Geometry.smallAngleRadians * (Math.abs(dotUU) + Math.abs(dotVV)))
            return { c: 1.0, s: 0.0, radians: 0.0 };
        return Angle.trigValuesToHalfAngleTrigValues(cos2t0, sin2t0);
    }
    /**
     * Returns the angle between two vectors, with the vectors given as xyz components
     * * The returned angle is between 0 and PI
     * @param ux x component of vector u
     * @param uy y component of vector u
     * @param uz z component of vector u
     * @param vx x component of vector v
     * @param vy y component of vector v
     * @param vz z component of vector v
     */
    static radiansBetweenVectorsXYZ(ux, uy, uz, vx, vy, vz) {
        const uDotV = ux * vx + uy * vy + uz * vz;
        return Math.atan2(Geometry_1.Geometry.crossProductMagnitude(ux, uy, uz, vx, vy, vz), uDotV);
    }
    /**
     * Returns the angle between two vectors, with the vectors given as xyz components, and an up vector to resolve
     * angle to a full 2PI range.
     * * The returned angle is (-PI < radians <= PI) or (0 <= radians < 2 * PI)
     * * The angle is in the plane of the U and V vectors.
     * * The upVector determines a positive side of the plane but need not be strictly perpendicular to the plane.
     * @param ux x component of vector u
     * @param uy y component of vector u
     * @param uz z component of vector u
     * @param vx x component of vector v
     * @param vy y component of vector v
     * @param vz z component of vector v
     * @param upVectorX x component of vector to positive side of plane.
     * @param upVectorY y component of vector to positive side of plane.
     * @param upVectorZ z component of vector to positive side of plane.
     * @param adjustToAllPositive if true, return strictly non-negative sweep (0 <= radians < 2*PI). If false, return
     * signed (-PI < radians <= PI)
     */
    static orientedRadiansBetweenVectorsXYZ(ux, uy, uz, vx, vy, vz, upVectorX, upVectorY, upVectorZ, adjustToPositive = false) {
        const uDotV = ux * vx + uy * vy + uz * vz;
        const wx = uy * vz - uz * vy;
        const wy = uz * vx - ux * vz;
        const wz = ux * vy - uy * vx;
        const upDotW = upVectorX * wx + upVectorY * wy + upVectorZ * wz;
        const crossMagnitude = Geometry_1.Geometry.hypotenuseXYZ(wx, wy, wz);
        if (upDotW < 0.0) {
            if (adjustToPositive) {
                // The turn is greater than 180 degrees.  Take a peculiarly oriented atan2 to get the excess-180 part as
                // addition to PI. This gives the smoothest numerical transition passing PI.
                return Math.PI + Math.atan2(crossMagnitude, -uDotV);
            }
            else {
                return -Math.atan2(crossMagnitude, uDotV);
            }
        }
        else {
            return Math.atan2(crossMagnitude, uDotV);
        }
    }
    /**
     * Add a multiple of a full circle angle (360 degrees, 2PI) in place.
     * @param multiple multiplier factor
     */
    addMultipleOf2PiInPlace(multiple) {
        if (this._degrees !== undefined) {
            this._degrees += multiple * 360.0;
            this._radians = Angle.degreesToRadians(this._degrees);
        }
        else {
            this._radians += multiple * Angle.pi2Radians;
        }
    }
}
exports.Angle = Angle;
//# sourceMappingURL=Angle.js.map