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 }