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(θ) 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(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 }