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(θ) sin(Φ)
42 * y = r sin(θ) sin(Φ)
43 * z = r cos(Φ)
44 *
45 * r = √(x^2 + y^2 + z^2)
46 * θ = atan2(y, x)
47 * Φ = acos(z/r)
48 * </pre>
49 * where <em>r</em> is the radius, <em>θ</em> is the azimuth angle, and <em>Φ</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, θ, Φ)</em> to represent radius, azimuth angle, and
54 * polar angle, whereas the physics convention flips the angle values and uses <em>(r, Φ, θ)</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 }