SphericalCoordinates.java

  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. import org.apache.commons.geometry.core.Spatial;
  19. import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
  20. import org.apache.commons.geometry.euclidean.internal.Vectors;
  21. import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
  22. import org.apache.commons.numbers.angle.Angle;

  23. /** Class representing <a href="https://en.wikipedia.org/wiki/Spherical_coordinate_system">spherical coordinates</a>
  24.  * in 3 dimensional Euclidean space.
  25.  *
  26.  * <p>Spherical coordinates for a point are defined by three values:
  27.  * <ol>
  28.  *  <li><em>Radius</em> - The distance from the point to a fixed referenced point.</li>
  29.  *  <li><em>Azimuth angle</em> - The angle measured from a fixed reference direction in a plane to
  30.  * the orthogonal projection of the point on that plane.</li>
  31.  *  <li><em>Polar angle</em> - The angle measured from a fixed zenith direction to the point. The zenith
  32.  *direction must be orthogonal to the reference plane.</li>
  33.  * </ol>
  34.  * This class follows the convention of using the origin as the reference point; the positive x-axis as the
  35.  * reference direction for the azimuth angle, measured in the x-y plane with positive angles moving counter-clockwise
  36.  * toward the positive y-axis; and the positive z-axis as the zenith direction. Spherical coordinates are
  37.  * related to Cartesian coordinates as follows:
  38.  * <pre>
  39.  * x = r cos(&theta;) sin(&Phi;)
  40.  * y = r sin(&theta;) sin(&Phi;)
  41.  * z = r cos(&Phi;)
  42.  *
  43.  * r = &radic;(x^2 + y^2 + z^2)
  44.  * &theta; = atan2(y, x)
  45.  * &Phi; = acos(z/r)
  46.  * </pre>
  47.  * where <em>r</em> is the radius, <em>&theta;</em> is the azimuth angle, and <em>&Phi;</em> is the polar angle
  48.  * of the spherical coordinates.
  49.  *
  50.  * <p>There are numerous, competing conventions for the symbols used to represent spherical coordinate values. For
  51.  * example, the mathematical convention is to use <em>(r, &theta;, &Phi;)</em> to represent radius, azimuth angle, and
  52.  * polar angle, whereas the physics convention flips the angle values and uses <em>(r, &Phi;, &theta;)</em>. As such,
  53.  * this class avoids the use of these symbols altogether in favor of the less ambiguous formal names of the values,
  54.  * e.g. {@code radius}, {@code azimuth}, and {@code polar}.</p>
  55.  *
  56.  * <p>In order to ensure the uniqueness of coordinate sets, coordinate values
  57.  * are normalized so that {@code radius} is in the range {@code [0, +Infinity)},
  58.  * {@code azimuth} is in the range {@code [0, 2pi)}, and {@code polar} is in the
  59.  * range {@code [0, pi]}.</p>
  60.  *
  61.  * @see <a href="https://en.wikipedia.org/wiki/Spherical_coordinate_system">Spherical Coordinate System</a>
  62.  */
  63. public final class SphericalCoordinates implements Spatial {
  64.     /** Radius value. */
  65.     private final double radius;

  66.     /** Azimuth angle in radians. */
  67.     private final double azimuth;

  68.     /** Polar angle in radians. */
  69.     private final double polar;

  70.     /** Simple constructor. The given inputs are normalized.
  71.      * @param radius Radius value.
  72.      * @param azimuth Azimuth angle in radians.
  73.      * @param polar Polar angle in radians.
  74.      */
  75.     private SphericalCoordinates(final double radius, final double azimuth, final double polar) {
  76.         double rad = radius;
  77.         double az = azimuth;
  78.         double pol = polar;

  79.         if (rad < 0) {
  80.             // negative radius; flip the angles
  81.             rad = Math.abs(rad);
  82.             az += Math.PI;
  83.             pol += Math.PI;
  84.         }

  85.         this.radius = rad;
  86.         this.azimuth = normalizeAzimuth(az);
  87.         this.polar = normalizePolar(pol);
  88.     }

  89.     /** Return the radius value. The value is in the range {@code [0, +Infinity)}.
  90.      * @return the radius value
  91.      */
  92.     public double getRadius() {
  93.         return radius;
  94.     }

  95.     /** Return the azimuth angle in radians. This is the angle in the x-y plane measured counter-clockwise from
  96.      * the positive x axis. The angle is in the range {@code [0, 2pi)}.
  97.      * @return the azimuth angle in radians
  98.      */
  99.     public double getAzimuth() {
  100.         return azimuth;
  101.     }

  102.     /** Return the polar angle in radians. This is the angle the coordinate ray makes with the positive z axis.
  103.      * The angle is in the range {@code [0, pi]}.
  104.      * @return the polar angle in radians
  105.      */
  106.     public double getPolar() {
  107.         return polar;
  108.     }

  109.     /** {@inheritDoc} */
  110.     @Override
  111.     public int getDimension() {
  112.         return 3;
  113.     }

  114.     /** {@inheritDoc} */
  115.     @Override
  116.     public boolean isNaN() {
  117.         return Double.isNaN(radius) || Double.isNaN(azimuth) || Double.isNaN(polar);
  118.     }

  119.     /** {@inheritDoc} */
  120.     @Override
  121.     public boolean isInfinite() {
  122.         return !isNaN() && (Double.isInfinite(radius) || Double.isInfinite(azimuth) || Double.isInfinite(polar));
  123.     }

  124.     /** {@inheritDoc} */
  125.     @Override
  126.     public boolean isFinite() {
  127.         return Double.isFinite(radius) && Double.isFinite(azimuth) && Double.isFinite(polar);
  128.     }

  129.     /** Convert this set of spherical coordinates to a Cartesian form.
  130.      * @return A 3-dimensional vector with an equivalent set of
  131.      *      Cartesian coordinates.
  132.      */
  133.     public Vector3D toVector() {
  134.         return toCartesian(radius, azimuth, polar);
  135.     }

  136.     /** Get a hashCode for this set of spherical coordinates.
  137.      * <p>All NaN values have the same hash code.</p>
  138.      *
  139.      * @return a hash code value for this object
  140.      */
  141.     @Override
  142.     public int hashCode() {
  143.         if (isNaN()) {
  144.             return 127;
  145.         }
  146.         return (Double.hashCode(radius) >> 17) ^
  147.                 (Double.hashCode(azimuth) >> 5) ^
  148.                 Double.hashCode(polar);
  149.     }

  150.     /** Test for the equality of two sets of spherical coordinates.
  151.      * <p>
  152.      * If all values of two sets of coordinates are exactly the same, and none are
  153.      * <code>Double.NaN</code>, the two sets are considered to be equal.
  154.      * </p>
  155.      * <p>
  156.      * <code>NaN</code> values are considered to globally affect the coordinates
  157.      * and be equal to each other - i.e, if any (or all) values of the
  158.      * coordinate set are equal to <code>Double.NaN</code>, the set as a whole
  159.      * is considered to equal NaN.
  160.      * </p>
  161.      *
  162.      * @param other Object to test for equality to this
  163.      * @return true if two SphericalCoordinates objects are equal, false if
  164.      *         object is null, not an instance of SphericalCoordinates, or
  165.      *         not equal to this SphericalCoordinates instance
  166.      *
  167.      */
  168.     @Override
  169.     public boolean equals(final Object other) {
  170.         if (this == other) {
  171.             return true;
  172.         }
  173.         if (other instanceof SphericalCoordinates) {
  174.             final SphericalCoordinates rhs = (SphericalCoordinates) other;
  175.             if (rhs.isNaN()) {
  176.                 return this.isNaN();
  177.             }

  178.             return Double.compare(radius, rhs.radius) == 0 &&
  179.                     Double.compare(azimuth, rhs.azimuth) == 0 &&
  180.                     Double.compare(polar, rhs.polar) == 0;
  181.         }
  182.         return false;
  183.     }

  184.     /** {@inheritDoc} */
  185.     @Override
  186.     public String toString() {
  187.         return SimpleTupleFormat.getDefault().format(radius, azimuth, polar);
  188.     }

  189.     /** Return a new instance with the given spherical coordinate values. The values are normalized
  190.      * so that {@code radius} lies in the range {@code [0, +Infinity)}, {@code azimuth} lies in the range
  191.      * {@code [0, 2pi)}, and {@code polar} lies in the range {@code [0, +pi]}.
  192.      * @param radius the length of the line segment from the origin to the coordinate point.
  193.      * @param azimuth the angle in the x-y plane, measured in radians counter-clockwise
  194.      *      from the positive x-axis.
  195.      * @param polar the angle in radians between the positive z-axis and the ray from the origin
  196.      *      to the coordinate point.
  197.      * @return a new {@link SphericalCoordinates} instance representing the same point as the given set of
  198.      *      spherical coordinates.
  199.      */
  200.     public static SphericalCoordinates of(final double radius, final double azimuth, final double polar) {
  201.         return new SphericalCoordinates(radius, azimuth, polar);
  202.     }

  203.     /** Convert the given set of Cartesian coordinates to spherical coordinates.
  204.      * @param x X coordinate value
  205.      * @param y Y coordinate value
  206.      * @param z Z coordinate value
  207.      * @return a set of spherical coordinates equivalent to the given Cartesian coordinates
  208.      */
  209.     public static SphericalCoordinates fromCartesian(final double x, final double y, final double z) {
  210.         final double radius = Vectors.norm(x, y, z);
  211.         final double azimuth = Math.atan2(y, x);

  212.         // default the polar angle to 0 when the radius is 0
  213.         final double polar = (radius > 0.0) ? Math.acos(z / radius) : 0.0;

  214.         return new SphericalCoordinates(radius, azimuth, polar);
  215.     }

  216.     /** Convert the given set of Cartesian coordinates to spherical coordinates.
  217.      * @param vec vector containing Cartesian coordinates to convert
  218.      * @return a set of spherical coordinates equivalent to the given Cartesian coordinates
  219.      */
  220.     public static SphericalCoordinates fromCartesian(final Vector3D vec) {
  221.         return fromCartesian(vec.getX(), vec.getY(), vec.getZ());
  222.     }

  223.     /** Convert the given set of spherical coordinates to Cartesian coordinates.
  224.      * @param radius The spherical radius value.
  225.      * @param azimuth The spherical azimuth angle in radians.
  226.      * @param polar The spherical polar angle in radians.
  227.      * @return A 3-dimensional vector with an equivalent set of
  228.      *      Cartesian coordinates.
  229.      */
  230.     public static Vector3D toCartesian(final double radius, final double azimuth, final double polar) {
  231.         final double xyLength = radius * Math.sin(polar);

  232.         final double x = xyLength * Math.cos(azimuth);
  233.         final double y = xyLength * Math.sin(azimuth);
  234.         final double z = radius * Math.cos(polar);

  235.         return Vector3D.of(x, y, z);
  236.     }

  237.     /** Parse the given string and return a new {@link SphericalCoordinates} instance. The parsed
  238.      * coordinate values are normalized as in the {@link #of(double, double, double)} method.
  239.      * The expected string format is the same as that returned by {@link #toString()}.
  240.      * @param input the string to parse
  241.      * @return new {@link SphericalCoordinates} instance
  242.      * @throws IllegalArgumentException if the string format is invalid.
  243.      */
  244.     public static SphericalCoordinates parse(final String input) {
  245.         return SimpleTupleFormat.getDefault().parse(input, SphericalCoordinates::new);
  246.     }

  247.     /** Normalize an azimuth value to be within the range {@code [0, 2pi)}. This
  248.      * is exactly equivalent to {@link PolarCoordinates#normalizeAzimuth(double)}.
  249.      * @param azimuth azimuth value in radians
  250.      * @return equivalent azimuth value in the range {@code [0, 2pi)}.
  251.      * @see PolarCoordinates#normalizeAzimuth(double)
  252.      */
  253.     public static double normalizeAzimuth(final double azimuth) {
  254.         return PolarCoordinates.normalizeAzimuth(azimuth);
  255.     }

  256.     /** Normalize a polar value to be within the range {@code [0, +pi]}. Since the
  257.      * polar angle is the angle between two vectors (the zenith direction and the
  258.      * point vector), the sign of the angle is not significant as in the azimuth angle.
  259.      * For example, a polar angle of {@code -pi/2} and one of {@code +pi/2} will both
  260.      * normalize to {@code pi/2}.
  261.      * @param polar polar value in radians
  262.      * @return equivalent polar value in the range {@code [0, +pi]}
  263.      */
  264.     public static double normalizePolar(final double polar) {
  265.         // normalize the polar angle; this is the angle between the polar vector and the point ray
  266.         // so it is unsigned (unlike the azimuth) and should be in the range [0, pi]
  267.         if (Double.isFinite(polar)) {
  268.             return Math.abs(Angle.Rad.WITHIN_MINUS_PI_AND_PI.applyAsDouble(polar));
  269.         }

  270.         return polar;
  271.     }
  272. }