View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  package org.apache.commons.geometry.spherical.oned;
    18
    19  import java.util.Comparator;
    20  import java.util.Objects;
    21
    22  import org.apache.commons.geometry.core.Point;
    23  import org.apache.commons.geometry.core.internal.DoubleFunction1N;
    24  import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
    25  import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
    26  import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
    27  import org.apache.commons.geometry.euclidean.twod.Vector2D;
    28  import org.apache.commons.numbers.angle.PlaneAngle;
    29  import org.apache.commons.numbers.angle.PlaneAngleRadians;
    30
    31  /** This class represents a point on the 1-sphere, or in other words, an
    32   * azimuth angle on a circle. The value of the azimuth angle is not normalized
    33   * by default, meaning that instances can be constructed representing negative
    34   * values or values greater than {@code 2pi}. However, instances separated by a
    35   * multiple of {@code 2pi} are considered equivalent for most methods, with the
    36   * exceptions being {@link #equals(Object)} and {@link #hashCode()}, where the
    37   * azimuth values must match exactly in order for instances to be considered
    38   * equal.
    39   *
    40   * <p>Instances of this class are guaranteed to be immutable.</p>
    41   */
    42  public final class Point1S implements Point<Point1S> {
    43
    44      /** A point with coordinates set to {@code 0*pi}. */
    45      public static final Point1S ZERO = Point1S.of(0.0);
    46
    47      /** A point with coordinates set to {@code pi}. */
    48      public static final Point1S PI = Point1S.of(PlaneAngleRadians.PI);
    49
    50      // CHECKSTYLE: stop ConstantName
    51      /** A point with all coordinates set to NaN. */
    52      public static final Point1S NaN = Point1S.of(Double.NaN);
    53      // CHECKSTYLE: resume ConstantName
    54
    55      /** Comparator that sorts points by normalized azimuth in ascending order.
    56       * Points are only considered equal if their normalized azimuths match exactly.
    57       * Null arguments are evaluated as being greater than non-null arguments.
    58       * @see #getNormalizedAzimuth()
    59       */
    60      public static final Comparator<Point1S> NORMALIZED_AZIMUTH_ASCENDING_ORDER = (a, b) -> {
    61          int cmp = 0;
    62
    63          if (a != null && b != null) {
    64              cmp = Double.compare(a.getNormalizedAzimuth(), b.getNormalizedAzimuth());
    65          } else if (a != null) {
    66              cmp = -1;
    67          } else if (b != null) {
    68              cmp = 1;
    69          }
    70
    71          return cmp;
    72      };
    73
    74      /** Azimuthal angle in radians. */
    75      private final double azimuth;
    76
    77      /** Normalized azimuth value in the range {@code [0, 2pi)}. */
    78      private final double normalizedAzimuth;
    79
    80      /** Build a point from its internal components.
    81       * @param azimuth azimuth angle
    82       * @param normalizedAzimuth azimuth angle normalized to the range {@code [0, 2pi)}
    83       */
    84      private Point1S(final double azimuth, final double normalizedAzimuth) {
    85          this.azimuth  = azimuth;
    86          this.normalizedAzimuth = normalizedAzimuth;
    87      }
    88
    89      /** Get the azimuth angle in radians. This value is not normalized and
    90       * can be any floating point number.
    91       * @return azimuth angle
    92       * @see Point1S#of(double)
    93       */
    94      public double getAzimuth() {
    95          return azimuth;
    96      }
    97
    98      /** Get the azimuth angle normalized to the range {@code [0, 2pi)}.
    99       * @return the azimuth angle normalized to the range {@code [0, 2pi)}.
    100      */
    101     public double getNormalizedAzimuth() {
    102         return normalizedAzimuth;
    103     }
    104
    105     /** Get the normalized vector corresponding to this azimuth angle in 2D Euclidean space.
    106      * @return normalized vector
    107      */
    108     public Vector2D getVector() {
    109         if (isFinite()) {
    110             return PolarCoordinates.toCartesian(1, azimuth);
    111         }
    112
    113         return null;
    114     }
    115
    116     /** {@inheritDoc} */
    117     @Override
    118     public int getDimension() {
    119         return 1;
    120     }
    121
    122     /** {@inheritDoc} */
    123     @Override
    124     public boolean isNaN() {
    125         return Double.isNaN(azimuth);
    126     }
    127
    128     /** {@inheritDoc} */
    129     @Override
    130     public boolean isInfinite() {
    131         return !isNaN() && Double.isInfinite(azimuth);
    132     }
    133
    134     /** {@inheritDoc} */
    135     @Override
    136     public boolean isFinite() {
    137         return Double.isFinite(azimuth);
    138     }
    139
    140     /** {@inheritDoc}
    141      *
    142      * <p>The returned value is the shortest angular distance between
    143      * the two points, in the range {@code [0, pi]}.</p>
    144      */
    145     @Override
    146     public double distance(final Point1S point) {
    147         return distance(this, point);
    148     }
    149
    150     /** Return the signed distance (angular separation) between this instance and the
    151      * given point in the range {@code [-pi, pi)}. If {@code p1} is the current instance,
    152      * {@code p2} the given point, and {@code d} the signed distance, then
    153      * {@code p1.getAzimuth() + d} is an angle equivalent to {@code p2.getAzimuth()}.
    154      * @param point point to compute the signed distance to
    155      * @return the signed distance between this instance and the given point in the range
    156      *      {@code [-pi, pi)}
    157      */
    158     public double signedDistance(final Point1S point) {
    159         return signedDistance(this, point);
    160     }
    161
    162     /** Return an equivalent point with an azimuth value at or above the given base.
    163      * The returned point has an azimuth value in the range {@code [base, base + 2pi)}.
    164      * @param base point to place this instance's azimuth value above
    165      * @return a point equivalent to the current instance but with an azimuth
    166      *      value in the range {@code [base, base + 2pi)}
    167      * @throws IllegalArgumentException if the azimuth value is NaN or infinite and
    168      *      cannot be normalized
    169      */
    170     public Point1S above(final Point1S base) {
    171         return normalize(base.getAzimuth() + PlaneAngleRadians.PI);
    172     }
    173
    174     /** Return an equivalent point with an azimuth value strictly below the given base.
    175      * The returned point has an azimuth value in the range {@code [base - 2pi, base)}.
    176      * @param base point to place this instance's azimuth value below
    177      * @return a point equivalent to the current instance but with an azimuth
    178      *      value in the range {@code [base - 2pi, base)}
    179      * @throws IllegalArgumentException if the azimuth value is NaN or infinite and
    180      *      cannot be normalized
    181      */
    182     public Point1S below(final Point1S base) {
    183         return normalize(base.getAzimuth() - PlaneAngleRadians.PI);
    184     }
    185
    186     /** Normalize this point around the given center point. The azimuth value of
    187      * the returned point is in the range {@code [center - pi, center + pi)}.
    188      * @param center point to center this instance around
    189      * @return a point equivalent to this instance but with an azimuth value
    190      *      in the range {@code [center - pi, center + pi)}.
    191      * @throws IllegalArgumentException if the azimuth value is NaN or infinite and
    192      *      cannot be normalized
    193      */
    194     public Point1S normalize(final Point1S center) {
    195         return normalize(center.getAzimuth());
    196     }
    197
    198     /** Return an equivalent point with an azimuth value normalized around the given center
    199      * angle. The azimuth value of the returned point is in the range
    200      * {@code [center - pi, center + pi)}.
    201      * @param center angle to center this instance around
    202      * @return a point equivalent to this instance but with an azimuth value
    203      *      in the range {@code [center - pi, center + pi)}.
    204      * @throws IllegalArgumentException if the azimuth value is NaN or infinite and
    205      *      cannot be normalized
    206      */
    207     public Point1S normalize(final double center) {
    208         if (isFinite()) {
    209             final double az = PlaneAngleRadians.normalize(azimuth, center);
    210             return new Point1S(az, normalizedAzimuth);
    211         }
    212         throw new IllegalArgumentException("Cannot normalize azimuth value: " + azimuth);
    213     }
    214
    215     /** Get the point exactly opposite this point on the circle, {@code pi} distance away.
    216      * The azimuth of the antipodal point is in the range {@code [0, 2pi)}.
    217      * @return the point exactly opposite this point on the circle
    218      */
    219     public Point1S antipodal() {
    220         double az = normalizedAzimuth + PlaneAngleRadians.PI;
    221         if (az >= PlaneAngleRadians.TWO_PI) {
    222             az -= PlaneAngleRadians.TWO_PI;
    223         }
    224
    225         return Point1S.of(az);
    226     }
    227
    228     /** Return true if this instance is equivalent to the argument. The points are
    229      * considered equivalent if the shortest angular distance between them is equal to
    230      * zero as evaluated by the given precision context. This means that points that differ
    231      * in azimuth by multiples of {@code 2pi} are considered equivalent.
    232      * @param other point to compare with
    233      * @param precision precision context used for floating point comparisons
    234      * @return true if this instance is equivalent to the argument
    235      */
    236     public boolean eq(final Point1S other, final DoublePrecisionContext precision) {
    237         final double dist = signedDistance(other);
    238         return precision.eqZero(dist);
    239     }
    240
    241     /**
    242      * Get a hashCode for the point. Points normally must have exactly the
    243      * same azimuth angles in order to have the same hash code. Points
    244      * will angles that differ by multiples of {@code 2pi} will not
    245      * necessarily have the same hash code.
    246      *
    247      * <p>All NaN values have the same hash code.</p>
    248      *
    249      * @return a hash code value for this object
    250      */
    251     @Override
    252     public int hashCode() {
    253         if (isNaN()) {
    254             return 542;
    255         }
    256         return 1759 * Objects.hash(azimuth, normalizedAzimuth);
    257     }
    258
    259     /** Test for the exact equality of two points on the 1-sphere.
    260      *
    261      * <p>If all coordinates of the given points are exactly the same, and none are
    262      * <code>Double.NaN</code>, the points are considered to be equal. Points with
    263      * azimuth values separated by multiples of {@code 2pi} are <em>not</em> considered
    264      * equal.</p>
    265      *
    266      * <p><code>NaN</code> coordinates are considered to affect globally the vector
    267      * and be equals to each other - i.e, if either (or all) coordinates of the
    268      * point are equal to <code>Double.NaN</code>, the point is equal to
    269      * {@link #NaN}.</p>
    270      *
    271      * @param other Object to test for equality to this
    272      * @return true if two points on the 1-sphere objects are exactly equal, false if
    273      *         object is null, not an instance of Point1S, or
    274      *         not equal to this Point1S instance
    275      *
    276      */
    277     @Override
    278     public boolean equals(final Object other) {
    279         if (this == other) {
    280             return true;
    281         }
    282
    283         if (other instanceof Point1S) {
    284             final Point1S rhs = (Point1S) other;
    285
    286             if (rhs.isNaN()) {
    287                 return this.isNaN();
    288             }
    289
    290             return Double.compare(azimuth, rhs.azimuth) == 0 &&
    291                     Double.compare(normalizedAzimuth, rhs.normalizedAzimuth) == 0;
    292         }
    293
    294         return false;
    295     }
    296
    297     /** {@inheritDoc} */
    298     @Override
    299     public String toString() {
    300         return SimpleTupleFormat.getDefault().format(getAzimuth());
    301     }
    302
    303     /** Create a new point instance from the given azimuth angle.
    304      * @param azimuth azimuth angle in radians
    305      * @return point instance with the given azimuth angle
    306      * @see #getAzimuth()
    307      */
    308     public static Point1S of(final double azimuth) {
    309         final double normalizedAzimuth = PolarCoordinates.normalizeAzimuth(azimuth);
    310
    311         return new Point1S(azimuth, normalizedAzimuth);
    312     }
    313
    314     /** Create a new point instance from the given azimuth angle.
    315      * @param azimuth azimuth azimuth angle in radians
    316      * @return point instance with the given azimuth angle
    317      * @see #getAzimuth()
    318      */
    319     public static Point1S of(final PlaneAngle azimuth) {
    320         return of(azimuth.toRadians());
    321     }
    322
    323     /** Create a new point instance from the given Euclidean 2D vector. The returned point
    324      * will have an azimuth value equal to the angle between the positive x-axis and the
    325      * given vector, measured in a counter-clockwise direction.
    326      * @param vector 3D vector to create the point from
    327      * @return a new point instance with an azimuth value equal to the angle between the given
    328      *      vector and the positive x-axis, measured in a counter-clockwise direction
    329      */
    330     public static Point1S from(final Vector2D vector) {
    331         final PolarCoordinates polar = PolarCoordinates.fromCartesian(vector);
    332         final double az = polar.getAzimuth();
    333
    334         return new Point1S(az, az);
    335     }
    336
    337     /** Create a new point instance containing an azimuth value equal to that of the
    338      * given set of polar coordinates.
    339      * @param polar polar coordinates to convert to a point
    340      * @return a new point instance containing an azimuth value equal to that of
    341      *      the given set of polar coordinates.
    342      */
    343     public static Point1S from(final PolarCoordinates polar) {
    344         final double az = polar.getAzimuth();
    345
    346         return new Point1S(az, az);
    347     }
    348
    349     /** Parse the given string and returns a new point instance. The expected string
    350      * format is the same as that returned by {@link #toString()}.
    351      * @param str the string to parse
    352      * @return point instance represented by the string
    353      * @throws IllegalArgumentException if the given string has an invalid format
    354      */
    355     public static Point1S parse(final String str) {
    356         return SimpleTupleFormat.getDefault().parse(str, (DoubleFunction1N<Point1S>) Point1S::of);
    357     }
    358
    359     /** Compute the signed shortest distance (angular separation) between two points. The return
    360      * value is in the range {@code [-pi, pi)} and is such that {@code p1.getAzimuth() + d}
    361      * (where {@code d} is the signed distance) is an angle equivalent to {@code p2.getAzimuth()}.
    362      * @param p1 first point
    363      * @param p2 second point
    364      * @return the signed angular separation between p1 and p2, in the range {@code [-pi, pi)}.
    365      */
    366     public static double signedDistance(final Point1S p1, final Point1S p2) {
    367         double dist = p2.normalizedAzimuth - p1.normalizedAzimuth;
    368         if (dist < -PlaneAngleRadians.PI) {
    369             dist += PlaneAngleRadians.TWO_PI;
    370         }
    371         if (dist >= PlaneAngleRadians.PI) {
    372             dist -= PlaneAngleRadians.TWO_PI;
    373         }
    374         return dist;
    375     }
    376
    377     /** Compute the shortest distance (angular separation) between two points. The returned
    378      * value is in the range {@code [0, pi]}. This method is equal to the absolute value of
    379      * the {@link #signedDistance(Point1S, Point1S) signed distance}.
    380      * @param p1 first point
    381      * @param p2 second point
    382      * @return the angular separation between p1 and p2, in the range {@code [0, pi]}.
    383      * @see #signedDistance(Point1S, Point1S)
    384      */
    385     public static double distance(final Point1S p1, final Point1S p2) {
    386         return Math.abs(signedDistance(p1, p2));
    387     }
    388 }