All Downloads are FREE. Search and download functionalities are using the official Maven repository.

pl.poznan.put.circular.Angle Maven / Gradle / Ivy

package pl.poznan.put.circular;

import org.apache.commons.lang3.Validate;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
import org.apache.commons.math3.util.FastMath;
import org.apache.commons.math3.util.MathUtils;
import org.apache.commons.math3.util.Precision;
import org.immutables.value.Value;
import pl.poznan.put.circular.exception.InvalidCircularValueException;
import pl.poznan.put.circular.exception.InvalidVectorFormatException;

import javax.annotation.Nullable;
import java.util.Locale;
import java.util.regex.Pattern;

/** A measurement for which one can distinguish a direction (i.e. [0..360) degrees) */
@Value.Immutable
public abstract class Angle implements Comparable {
  private static final Pattern DOT = Pattern.compile("[.]");
  private static final int MINUTES_IN_DAY = 24 * 60;

  /**
   * Calculates angle ABC.
   *
   * @param coordA Coordinate of point A.
   * @param coordB Coordinate of point B.
   * @param coordC Coordinate of point C.
   * @return An angle between points A, B and C.
   */
  public static Angle betweenPoints(
      final Vector3D coordA, final Vector3D coordB, final Vector3D coordC) {
    final Vector3D vectorAB = coordB.subtract(coordA);
    final Vector3D vectorCB = coordB.subtract(coordC);
    return ImmutableAngle.of(Vector3D.angle(vectorAB, vectorCB));
  }

  /**
   * Calculates torsion angle given four points. Uses cos^-1 and a check for pseudovector.
   *
   * @param a1 Atom 1.
   * @param a2 Atom 2.
   * @param a3 Atom 3.
   * @param a4 Atom 4.
   * @return A torsion angle (rotation around vector B-C).
   */
  public static Angle torsionAngleByAcos(
      final Vector3D a1, final Vector3D a2, final Vector3D a3, final Vector3D a4) {
    final Vector3D d1 = a1.subtract(a2);
    final Vector3D d2 = a2.subtract(a3);
    final Vector3D d3 = a3.subtract(a4);

    final Vector3D u1 = d1.crossProduct(d2);
    final Vector3D u2 = d2.crossProduct(d3);

    final double ctor = u1.dotProduct(u2) / FastMath.sqrt(u1.dotProduct(u1) * u2.dotProduct(u2));
    final double torp = FastMath.acos(ctor < -1.0 ? -1.0 : Math.min(ctor, 1.0));
    return ImmutableAngle.of(u1.dotProduct(u2.crossProduct(d2)) >= 0 ? torp : -torp);
  }

  /**
   * Calculates torsion angle given four points. Uses atan2 method.
   *
   * @param coordA Coordinate of point A.
   * @param coordB Coordinate of point B.
   * @param coordC Coordinate of point C.
   * @param coordD Coordinate of point D.
   * @return A torsion angle (rotation around vector B-C).
   */
  public static Angle torsionAngle(
      final Vector3D coordA, final Vector3D coordB, final Vector3D coordC, final Vector3D coordD) {
    final Vector3D v1 = coordB.subtract(coordA);
    final Vector3D v2 = coordC.subtract(coordB);
    final Vector3D v3 = coordD.subtract(coordC);

    final Vector3D tmp1 = v1.crossProduct(v2);
    final Vector3D tmp2 = v2.crossProduct(v3);
    final Vector3D tmp3 = v1.scalarMultiply(v2.getNorm());
    return ImmutableAngle.of(FastMath.atan2(tmp3.dotProduct(tmp2), tmp1.dotProduct(tmp2)));
  }

  /**
   * Parses a string in format HH.MM as a vector on a circular clock. For 'm' minutes after
   * midnight, the vector has value of '360 * m / (24 * 60)'.
   *
   * @param hourMinute String in format HH.MM.
   * @return A vector representation of time on a circular clock.
   * @throws InvalidVectorFormatException If the input string has an invalid format.
   * @throws InvalidCircularValueException If the input string is parsed to a value outside the
   *     range [0..360)
   */
  public static Angle fromHourMinuteString(final String hourMinute) {
    final String[] split = Angle.DOT.split(hourMinute);

    if (split.length != 2) {
      throw new InvalidVectorFormatException(
          "Required format is HH.MM eg. 02.40. The input given was: " + hourMinute);
    }

    try {
      final int hours = Integer.parseInt(split[0]);
      int minutes = Integer.parseInt(split[1]);
      minutes += hours * 60;
      return ImmutableAngle.of(
          (MathUtils.TWO_PI * (double) minutes) / (double) Angle.MINUTES_IN_DAY);
    } catch (final NumberFormatException e) {
      throw new InvalidVectorFormatException(
          "Required format is HH.MM eg. 02.40. The input given was: " + hourMinute, e);
    }
  }

  /**
   * Calculates angles' difference using formula min(|left - right|, 360 - |left - right|).
   *
   * @param left Minuend in radians.
   * @param right Subtrahend in radians.
   * @return The result of minuend - subtrahend in angular space.
   */
  public static double subtractByMinimum(final double left, final double right) {
    final double d = FastMath.abs(left - right);
    return FastMath.min(d, MathUtils.TWO_PI - d);
  }

  /**
   * Calculates angles' difference using formula acos(dot(left, right)).
   *
   * @param left Minuend in radians.
   * @param right Subtrahend in radians.
   * @return The result of minuend - subtrahend in angular space.
   */
  public static double subtractAsVectors(final double left, final double right) {
    double v = FastMath.sin(left) * FastMath.sin(right);
    v += FastMath.cos(left) * FastMath.cos(right);
    v = FastMath.min(1.0, v);
    v = FastMath.max(-1.0, v);
    return FastMath.acos(v);
  }

  /**
   * Calculates angles' difference using formula: pi - |pi - |left - right||.
   *
   * @param left Minuend in radians.
   * @param right Subtrahend in radians.
   * @return The result of minuend - subtrahend in angular space.
   */
  public static double subtractByAbsolutes(final double left, final double right) {
    return FastMath.PI - FastMath.abs(FastMath.PI - FastMath.abs(left - right));
  }

  /** @return Value in radians in range (-pi; pi]. */
  @Value.Parameter(order = 1)
  public abstract double radians();

  /** @return Value in degrees in range (-180; 180]. */
  public final double degrees() {
    return FastMath.toDegrees(radians());
  }

  /** @return Value in degrees in range [0; 360). */
  public final double degrees360() {
    return FastMath.toDegrees(radians2PI());
  }

  /** @return Value in radians in range [0; 2pi). */
  public final double radians2PI() {
    return (radians() < 0.0) ? (radians() + MathUtils.TWO_PI) : radians();
  }

  /** @return True if value is not NaN. */
  public final boolean isValid() {
    return !Double.isNaN(radians());
  }

  /**
   * Return strue if this instance is in range [begin; end). For example 45 degrees is between 30
   * degrees and 60 degrees. Also, 15 degrees is between -30 and 30 degrees.
   *
   * @param begin Beginning of the range of values.
   * @param end Ending of the range of values.
   * @return true if object is between [begin; end)
   */
  public final boolean isBetween(final Angle begin, final Angle end) {
    final double radians2PI = radians2PI();
    final double begin2PI = begin.radians2PI();
    final double end2PI = end.radians2PI();

    return (begin2PI < end2PI)
        ? ((radians2PI >= begin2PI) && (radians2PI < end2PI))
        : ((radians2PI >= begin2PI) || (radians2PI < end2PI));
  }

  /**
   * Multiplies the angular value by a constant.
   *
   * @param v Multiplier.
   * @return Angular value multiplied.
   */
  public final Angle multiply(final double v) {
    return ImmutableAngle.of((radians() * v));
  }

  /**
   * Subtracts another angular value from this.
   *
   * @param other Another angular value.
   * @return Result of this - other in angular space.
   */
  public final Angle subtract(final Angle other) {
    return ImmutableAngle.of(Angle.subtractByAbsolutes(radians(), other.radians()));
  }

  /**
   * Returns an ordered difference between angles. It describes a rotation from one angle to another
   * one and is therefore in range [-180; 180) degrees.
   *
   * @param other The other angle which value should be subtracted from this one.
   * @return An ordered difference from first to second angle in range [-180; 180) degrees.
   */
  public final Angle orderedSubtract(final Angle other) {
    double d = radians() - other.radians();
    while (Precision.compareTo(d, -FastMath.PI, 1.0e-3) < 0) {
      d += MathUtils.TWO_PI;
    }
    while (Precision.compareTo(d, FastMath.PI, 1.0e-3) > 0) {
      d -= MathUtils.TWO_PI;
    }
    return ImmutableAngle.of(d);
  }

  /**
   * Computes a useful distance in range [0; 2] between two angular values.
   *
   * @param other The other angle.
   * @return Value in range [0; 2] denoting distance between two angles.
   */
  public final double distance(final Angle other) {
    return 1 - FastMath.cos(radians() - other.radians());
  }

  @Override
  public final int compareTo(final Angle t) {
    return Double.compare(radians(), t.radians());
  }

  @Override
  public final boolean equals(@Nullable final Object obj) {
    if (this == obj) return true;
    return obj instanceof Angle && Precision.equals(radians(), ((Angle) obj).radians(), 1.0e-3);
  }

  @Override
  public final String toString() {
    return String.format(Locale.US, "Angle{degrees=%.2f}", degrees());
  }

  @Value.Check
  protected Angle normalize() {
    double value = radians();

    if (Double.isNaN(value)) {
      return this;
    }

    Validate.finite(value);

    if ((value > -FastMath.PI && value <= FastMath.PI)) {
      return this;
    }

    while (value <= -FastMath.PI) {
      value += MathUtils.TWO_PI;
    }
    while (value > FastMath.PI) {
      value -= MathUtils.TWO_PI;
    }
    return ImmutableAngle.of(value);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy