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.euclidean.threed;
    18
    19  import org.apache.commons.geometry.core.Spatial;
    20  import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
    21  import org.apache.commons.geometry.euclidean.internal.Vectors;
    22  import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
    23  import org.apache.commons.numbers.angle.PlaneAngleRadians;
    24
    25  /** Class representing <a href="https://en.wikipedia.org/wiki/Spherical_coordinate_system">spherical coordinates</a>
    26   * in 3 dimensional Euclidean space.
    27   *
    28   * <p>Spherical coordinates for a point are defined by three values:
    29   * <ol>
    30   *  <li><em>Radius</em> - The distance from the point to a fixed referenced point.</li>
    31   *  <li><em>Azimuth angle</em> - The angle measured from a fixed reference direction in a plane to
    32   * the orthogonal projection of the point on that plane.</li>
    33   *  <li><em>Polar angle</em> - The angle measured from a fixed zenith direction to the point. The zenith
    34   *direction must be orthogonal to the reference plane.</li>
    35   * </ol>
    36   * This class follows the convention of using the origin as the reference point; the positive x-axis as the
    37   * reference direction for the azimuth angle, measured in the x-y plane with positive angles moving counter-clockwise
    38   * toward the positive y-axis; and the positive z-axis as the zenith direction. Spherical coordinates are
    39   * related to Cartesian coordinates as follows:
    40   * <pre>
    41   * x = r cos(&theta;) sin(&Phi;)
    42   * y = r sin(&theta;) sin(&Phi;)
    43   * z = r cos(&Phi;)
    44   *
    45   * r = &radic;(x^2 + y^2 + z^2)
    46   * &theta; = atan2(y, x)
    47   * &Phi; = acos(z/r)
    48   * </pre>
    49   * where <em>r</em> is the radius, <em>&theta;</em> is the azimuth angle, and <em>&Phi;</em> is the polar angle
    50   * of the spherical coordinates.
    51   *
    52   * <p>There are numerous, competing conventions for the symbols used to represent spherical coordinate values. For
    53   * example, the mathematical convention is to use <em>(r, &theta;, &Phi;)</em> to represent radius, azimuth angle, and
    54   * polar angle, whereas the physics convention flips the angle values and uses <em>(r, &Phi;, &theta;)</em>. As such,
    55   * this class avoids the use of these symbols altogether in favor of the less ambiguous formal names of the values,
    56   * e.g. {@code radius}, {@code azimuth}, and {@code polar}.</p>
    57   *
    58   * <p>In order to ensure the uniqueness of coordinate sets, coordinate values
    59   * are normalized so that {@code radius} is in the range {@code [0, +Infinity)},
    60   * {@code azimuth} is in the range {@code [0, 2pi)}, and {@code polar} is in the
    61   * range {@code [0, pi]}.</p>
    62   *
    63   * @see <a href="https://en.wikipedia.org/wiki/Spherical_coordinate_system">Spherical Coordinate System</a>
    64   */
    65  public final class SphericalCoordinates implements Spatial {
    66      /** Radius value. */
    67      private final double radius;
    68
    69      /** Azimuth angle in radians. */
    70      private final double azimuth;
    71
    72      /** Polar angle in radians. */
    73      private final double polar;
    74
    75      /** Simple constructor. The given inputs are normalized.
    76       * @param radius Radius value.
    77       * @param azimuth Azimuth angle in radians.
    78       * @param polar Polar angle in radians.
    79       */
    80      private SphericalCoordinates(double radius, double azimuth, double polar) {
    81          if (radius < 0) {
    82              // negative radius; flip the angles
    83              radius = Math.abs(radius);
    84              azimuth += PlaneAngleRadians.PI;
    85              polar += PlaneAngleRadians.PI;
    86          }
    87
    88          this.radius = radius;
    89          this.azimuth = normalizeAzimuth(azimuth);
    90          this.polar = normalizePolar(polar);
    91      }
    92
    93      /** Return the radius value. The value is in the range {@code [0, +Infinity)}.
    94       * @return the radius value
    95       */
    96      public double getRadius() {
    97          return radius;
    98      }
    99
    100     /** Return the azimuth angle in radians. This is the angle in the x-y plane measured counter-clockwise from
    101      * the positive x axis. The angle is in the range {@code [0, 2pi)}.
    102      * @return the azimuth angle in radians
    103      */
    104     public double getAzimuth() {
    105         return azimuth;
    106     }
    107
    108     /** Return the polar angle in radians. This is the angle the coordinate ray makes with the positive z axis.
    109      * The angle is in the range {@code [0, pi]}.
    110      * @return the polar angle in radians
    111      */
    112     public double getPolar() {
    113         return polar;
    114     }
    115
    116     /** {@inheritDoc} */
    117     @Override
    118     public int getDimension() {
    119         return 3;
    120     }
    121
    122     /** {@inheritDoc} */
    123     @Override
    124     public boolean isNaN() {
    125         return Double.isNaN(radius) || Double.isNaN(azimuth) || Double.isNaN(polar);
    126     }
    127
    128     /** {@inheritDoc} */
    129     @Override
    130     public boolean isInfinite() {
    131         return !isNaN() && (Double.isInfinite(radius) || Double.isInfinite(azimuth) || Double.isInfinite(polar));
    132     }
    133
    134     /** {@inheritDoc} */
    135     @Override
    136     public boolean isFinite() {
    137         return Double.isFinite(radius) && Double.isFinite(azimuth) && Double.isFinite(polar);
    138     }
    139
    140     /** Convert this set of spherical coordinates to a Cartesian form.
    141      * @return A 3-dimensional vector with an equivalent set of
    142      *      Cartesian coordinates.
    143      */
    144     public Vector3D toVector() {
    145         return toCartesian(radius, azimuth, polar);
    146     }
    147
    148     /** Get a hashCode for this set of spherical coordinates.
    149      * <p>All NaN values have the same hash code.</p>
    150      *
    151      * @return a hash code value for this object
    152      */
    153     @Override
    154     public int hashCode() {
    155         if (isNaN()) {
    156             return 127;
    157         }
    158         return 449 * (79 * Double.hashCode(radius) + Double.hashCode(azimuth) + Double.hashCode(polar));
    159     }
    160
    161     /** Test for the equality of two sets of spherical coordinates.
    162      * <p>
    163      * If all values of two sets of coordinates are exactly the same, and none are
    164      * <code>Double.NaN</code>, the two sets are considered to be equal.
    165      * </p>
    166      * <p>
    167      * <code>NaN</code> values are considered to globally affect the coordinates
    168      * and be equal to each other - i.e, if any (or all) values of the
    169      * coordinate set are equal to <code>Double.NaN</code>, the set as a whole
    170      * is considered to equal NaN.
    171      * </p>
    172      *
    173      * @param other Object to test for equality to this
    174      * @return true if two SphericalCoordinates objects are equal, false if
    175      *         object is null, not an instance of SphericalCoordinates, or
    176      *         not equal to this SphericalCoordinates instance
    177      *
    178      */
    179     @Override
    180     public boolean equals(final Object other) {
    181         if (this == other) {
    182             return true;
    183         }
    184         if (other instanceof SphericalCoordinates) {
    185             final SphericalCoordinates rhs = (SphericalCoordinates) other;
    186             if (rhs.isNaN()) {
    187                 return this.isNaN();
    188             }
    189
    190             return Double.compare(radius, rhs.radius) == 0 &&
    191                     Double.compare(azimuth, rhs.azimuth) == 0 &&
    192                     Double.compare(polar, rhs.polar) == 0;
    193         }
    194         return false;
    195     }
    196
    197     /** {@inheritDoc} */
    198     @Override
    199     public String toString() {
    200         return SimpleTupleFormat.getDefault().format(radius, azimuth, polar);
    201     }
    202
    203     /** Return a new instance with the given spherical coordinate values. The values are normalized
    204      * so that {@code radius} lies in the range {@code [0, +Infinity)}, {@code azimuth} lies in the range
    205      * {@code [0, 2pi)}, and {@code polar} lies in the range {@code [0, +pi]}.
    206      * @param radius the length of the line segment from the origin to the coordinate point.
    207      * @param azimuth the angle in the x-y plane, measured in radians counter-clockwise
    208      *      from the positive x-axis.
    209      * @param polar the angle in radians between the positive z-axis and the ray from the origin
    210      *      to the coordinate point.
    211      * @return a new {@link SphericalCoordinates} instance representing the same point as the given set of
    212      *      spherical coordinates.
    213      */
    214     public static SphericalCoordinates of(final double radius, final double azimuth, final double polar) {
    215         return new SphericalCoordinates(radius, azimuth, polar);
    216     }
    217
    218     /** Convert the given set of Cartesian coordinates to spherical coordinates.
    219      * @param x X coordinate value
    220      * @param y Y coordinate value
    221      * @param z Z coordinate value
    222      * @return a set of spherical coordinates equivalent to the given Cartesian coordinates
    223      */
    224     public static SphericalCoordinates fromCartesian(final double x, final double y, final double z) {
    225         final double radius = Vectors.norm(x, y, z);
    226         final double azimuth = Math.atan2(y, x);
    227
    228         // default the polar angle to 0 when the radius is 0
    229         final double polar = (radius > 0.0) ? Math.acos(z / radius) : 0.0;
    230
    231         return new SphericalCoordinates(radius, azimuth, polar);
    232     }
    233
    234     /** Convert the given set of Cartesian coordinates to spherical coordinates.
    235      * @param vec vector containing Cartesian coordinates to convert
    236      * @return a set of spherical coordinates equivalent to the given Cartesian coordinates
    237      */
    238     public static SphericalCoordinates fromCartesian(final Vector3D vec) {
    239         return fromCartesian(vec.getX(), vec.getY(), vec.getZ());
    240     }
    241
    242     /** Convert the given set of spherical coordinates to Cartesian coordinates.
    243      * @param radius The spherical radius value.
    244      * @param azimuth The spherical azimuth angle in radians.
    245      * @param polar The spherical polar angle in radians.
    246      * @return A 3-dimensional vector with an equivalent set of
    247      *      Cartesian coordinates.
    248      */
    249     public static Vector3D toCartesian(final double radius, final double azimuth, final double polar) {
    250         final double xyLength = radius * Math.sin(polar);
    251
    252         final double x = xyLength * Math.cos(azimuth);
    253         final double y = xyLength * Math.sin(azimuth);
    254         final double z = radius * Math.cos(polar);
    255
    256         return Vector3D.of(x, y, z);
    257     }
    258
    259     /** Parse the given string and return a new {@link SphericalCoordinates} instance. The parsed
    260      * coordinate values are normalized as in the {@link #of(double, double, double)} method.
    261      * The expected string format is the same as that returned by {@link #toString()}.
    262      * @param input the string to parse
    263      * @return new {@link SphericalCoordinates} instance
    264      * @throws IllegalArgumentException if the string format is invalid.
    265      */
    266     public static SphericalCoordinates parse(final String input) {
    267         return SimpleTupleFormat.getDefault().parse(input, SphericalCoordinates::new);
    268     }
    269
    270     /** Normalize an azimuth value to be within the range {@code [0, 2pi)}. This
    271      * is exactly equivalent to {@link PolarCoordinates#normalizeAzimuth(double)}.
    272      * @param azimuth azimuth value in radians
    273      * @return equivalent azimuth value in the range {@code [0, 2pi)}.
    274      * @see PolarCoordinates#normalizeAzimuth(double)
    275      */
    276     public static double normalizeAzimuth(final double azimuth) {
    277         return PolarCoordinates.normalizeAzimuth(azimuth);
    278     }
    279
    280     /** Normalize a polar value to be within the range {@code [0, +pi]}. Since the
    281      * polar angle is the angle between two vectors (the zenith direction and the
    282      * point vector), the sign of the angle is not significant as in the azimuth angle.
    283      * For example, a polar angle of {@code -pi/2} and one of {@code +pi/2} will both
    284      * normalize to {@code pi/2}.
    285      * @param polar polar value in radians
    286      * @return equivalent polar value in the range {@code [0, +pi]}
    287      */
    288     public static double normalizePolar(double polar) {
    289         // normalize the polar angle; this is the angle between the polar vector and the point ray
    290         // so it is unsigned (unlike the azimuth) and should be in the range [0, pi]
    291         if (Double.isFinite(polar)) {
    292             polar = Math.abs(PlaneAngleRadians.normalizeBetweenMinusPiAndPi(polar));
    293         }
    294
    295         return polar;
    296     }
    297 }