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.Angle;
    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(final double radius, final double azimuth, final double polar) {
    81          double rad = radius;
    82          double az = azimuth;
    83          double pol = polar;
    84
    85          if (rad < 0) {
    86              // negative radius; flip the angles
    87              rad = Math.abs(rad);
    88              az += Math.PI;
    89              pol += Math.PI;
    90          }
    91
    92          this.radius = rad;
    93          this.azimuth = normalizeAzimuth(az);
    94          this.polar = normalizePolar(pol);
    95      }
    96
    97      /** Return the radius value. The value is in the range {@code [0, +Infinity)}.
    98       * @return the radius value
    99       */
    100     public double getRadius() {
    101         return radius;
    102     }
    103
    104     /** Return the azimuth angle in radians. This is the angle in the x-y plane measured counter-clockwise from
    105      * the positive x axis. The angle is in the range {@code [0, 2pi)}.
    106      * @return the azimuth angle in radians
    107      */
    108     public double getAzimuth() {
    109         return azimuth;
    110     }
    111
    112     /** Return the polar angle in radians. This is the angle the coordinate ray makes with the positive z axis.
    113      * The angle is in the range {@code [0, pi]}.
    114      * @return the polar angle in radians
    115      */
    116     public double getPolar() {
    117         return polar;
    118     }
    119
    120     /** {@inheritDoc} */
    121     @Override
    122     public int getDimension() {
    123         return 3;
    124     }
    125
    126     /** {@inheritDoc} */
    127     @Override
    128     public boolean isNaN() {
    129         return Double.isNaN(radius) || Double.isNaN(azimuth) || Double.isNaN(polar);
    130     }
    131
    132     /** {@inheritDoc} */
    133     @Override
    134     public boolean isInfinite() {
    135         return !isNaN() && (Double.isInfinite(radius) || Double.isInfinite(azimuth) || Double.isInfinite(polar));
    136     }
    137
    138     /** {@inheritDoc} */
    139     @Override
    140     public boolean isFinite() {
    141         return Double.isFinite(radius) && Double.isFinite(azimuth) && Double.isFinite(polar);
    142     }
    143
    144     /** Convert this set of spherical coordinates to a Cartesian form.
    145      * @return A 3-dimensional vector with an equivalent set of
    146      *      Cartesian coordinates.
    147      */
    148     public Vector3D toVector() {
    149         return toCartesian(radius, azimuth, polar);
    150     }
    151
    152     /** Get a hashCode for this set of spherical coordinates.
    153      * <p>All NaN values have the same hash code.</p>
    154      *
    155      * @return a hash code value for this object
    156      */
    157     @Override
    158     public int hashCode() {
    159         if (isNaN()) {
    160             return 127;
    161         }
    162         return (Double.hashCode(radius) >> 17) ^
    163                 (Double.hashCode(azimuth) >> 5) ^
    164                 Double.hashCode(polar);
    165     }
    166
    167     /** Test for the equality of two sets of spherical coordinates.
    168      * <p>
    169      * If all values of two sets of coordinates are exactly the same, and none are
    170      * <code>Double.NaN</code>, the two sets are considered to be equal.
    171      * </p>
    172      * <p>
    173      * <code>NaN</code> values are considered to globally affect the coordinates
    174      * and be equal to each other - i.e, if any (or all) values of the
    175      * coordinate set are equal to <code>Double.NaN</code>, the set as a whole
    176      * is considered to equal NaN.
    177      * </p>
    178      *
    179      * @param other Object to test for equality to this
    180      * @return true if two SphericalCoordinates objects are equal, false if
    181      *         object is null, not an instance of SphericalCoordinates, or
    182      *         not equal to this SphericalCoordinates instance
    183      *
    184      */
    185     @Override
    186     public boolean equals(final Object other) {
    187         if (this == other) {
    188             return true;
    189         }
    190         if (other instanceof SphericalCoordinates) {
    191             final SphericalCoordinates rhs = (SphericalCoordinates) other;
    192             if (rhs.isNaN()) {
    193                 return this.isNaN();
    194             }
    195
    196             return Double.compare(radius, rhs.radius) == 0 &&
    197                     Double.compare(azimuth, rhs.azimuth) == 0 &&
    198                     Double.compare(polar, rhs.polar) == 0;
    199         }
    200         return false;
    201     }
    202
    203     /** {@inheritDoc} */
    204     @Override
    205     public String toString() {
    206         return SimpleTupleFormat.getDefault().format(radius, azimuth, polar);
    207     }
    208
    209     /** Return a new instance with the given spherical coordinate values. The values are normalized
    210      * so that {@code radius} lies in the range {@code [0, +Infinity)}, {@code azimuth} lies in the range
    211      * {@code [0, 2pi)}, and {@code polar} lies in the range {@code [0, +pi]}.
    212      * @param radius the length of the line segment from the origin to the coordinate point.
    213      * @param azimuth the angle in the x-y plane, measured in radians counter-clockwise
    214      *      from the positive x-axis.
    215      * @param polar the angle in radians between the positive z-axis and the ray from the origin
    216      *      to the coordinate point.
    217      * @return a new {@link SphericalCoordinates} instance representing the same point as the given set of
    218      *      spherical coordinates.
    219      */
    220     public static SphericalCoordinates of(final double radius, final double azimuth, final double polar) {
    221         return new SphericalCoordinates(radius, azimuth, polar);
    222     }
    223
    224     /** Convert the given set of Cartesian coordinates to spherical coordinates.
    225      * @param x X coordinate value
    226      * @param y Y coordinate value
    227      * @param z Z coordinate value
    228      * @return a set of spherical coordinates equivalent to the given Cartesian coordinates
    229      */
    230     public static SphericalCoordinates fromCartesian(final double x, final double y, final double z) {
    231         final double radius = Vectors.norm(x, y, z);
    232         final double azimuth = Math.atan2(y, x);
    233
    234         // default the polar angle to 0 when the radius is 0
    235         final double polar = (radius > 0.0) ? Math.acos(z / radius) : 0.0;
    236
    237         return new SphericalCoordinates(radius, azimuth, polar);
    238     }
    239
    240     /** Convert the given set of Cartesian coordinates to spherical coordinates.
    241      * @param vec vector containing Cartesian coordinates to convert
    242      * @return a set of spherical coordinates equivalent to the given Cartesian coordinates
    243      */
    244     public static SphericalCoordinates fromCartesian(final Vector3D vec) {
    245         return fromCartesian(vec.getX(), vec.getY(), vec.getZ());
    246     }
    247
    248     /** Convert the given set of spherical coordinates to Cartesian coordinates.
    249      * @param radius The spherical radius value.
    250      * @param azimuth The spherical azimuth angle in radians.
    251      * @param polar The spherical polar angle in radians.
    252      * @return A 3-dimensional vector with an equivalent set of
    253      *      Cartesian coordinates.
    254      */
    255     public static Vector3D toCartesian(final double radius, final double azimuth, final double polar) {
    256         final double xyLength = radius * Math.sin(polar);
    257
    258         final double x = xyLength * Math.cos(azimuth);
    259         final double y = xyLength * Math.sin(azimuth);
    260         final double z = radius * Math.cos(polar);
    261
    262         return Vector3D.of(x, y, z);
    263     }
    264
    265     /** Parse the given string and return a new {@link SphericalCoordinates} instance. The parsed
    266      * coordinate values are normalized as in the {@link #of(double, double, double)} method.
    267      * The expected string format is the same as that returned by {@link #toString()}.
    268      * @param input the string to parse
    269      * @return new {@link SphericalCoordinates} instance
    270      * @throws IllegalArgumentException if the string format is invalid.
    271      */
    272     public static SphericalCoordinates parse(final String input) {
    273         return SimpleTupleFormat.getDefault().parse(input, SphericalCoordinates::new);
    274     }
    275
    276     /** Normalize an azimuth value to be within the range {@code [0, 2pi)}. This
    277      * is exactly equivalent to {@link PolarCoordinates#normalizeAzimuth(double)}.
    278      * @param azimuth azimuth value in radians
    279      * @return equivalent azimuth value in the range {@code [0, 2pi)}.
    280      * @see PolarCoordinates#normalizeAzimuth(double)
    281      */
    282     public static double normalizeAzimuth(final double azimuth) {
    283         return PolarCoordinates.normalizeAzimuth(azimuth);
    284     }
    285
    286     /** Normalize a polar value to be within the range {@code [0, +pi]}. Since the
    287      * polar angle is the angle between two vectors (the zenith direction and the
    288      * point vector), the sign of the angle is not significant as in the azimuth angle.
    289      * For example, a polar angle of {@code -pi/2} and one of {@code +pi/2} will both
    290      * normalize to {@code pi/2}.
    291      * @param polar polar value in radians
    292      * @return equivalent polar value in the range {@code [0, +pi]}
    293      */
    294     public static double normalizePolar(final double polar) {
    295         // normalize the polar angle; this is the angle between the polar vector and the point ray
    296         // so it is unsigned (unlike the azimuth) and should be in the range [0, pi]
    297         if (Double.isFinite(polar)) {
    298             return Math.abs(Angle.Rad.WITHIN_MINUS_PI_AND_PI.applyAsDouble(polar));
    299         }
    300
    301         return polar;
    302     }
    303 }