001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.euclidean.threed;
018
019import org.apache.commons.geometry.core.Spatial;
020import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
021import org.apache.commons.geometry.euclidean.internal.Vectors;
022import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
023import org.apache.commons.numbers.angle.Angle;
024
025/** Class representing <a href="https://en.wikipedia.org/wiki/Spherical_coordinate_system">spherical coordinates</a>
026 * in 3 dimensional Euclidean space.
027 *
028 * <p>Spherical coordinates for a point are defined by three values:
029 * <ol>
030 *  <li><em>Radius</em> - The distance from the point to a fixed referenced point.</li>
031 *  <li><em>Azimuth angle</em> - The angle measured from a fixed reference direction in a plane to
032 * the orthogonal projection of the point on that plane.</li>
033 *  <li><em>Polar angle</em> - The angle measured from a fixed zenith direction to the point. The zenith
034 *direction must be orthogonal to the reference plane.</li>
035 * </ol>
036 * This class follows the convention of using the origin as the reference point; the positive x-axis as the
037 * reference direction for the azimuth angle, measured in the x-y plane with positive angles moving counter-clockwise
038 * toward the positive y-axis; and the positive z-axis as the zenith direction. Spherical coordinates are
039 * related to Cartesian coordinates as follows:
040 * <pre>
041 * x = r cos(&theta;) sin(&Phi;)
042 * y = r sin(&theta;) sin(&Phi;)
043 * z = r cos(&Phi;)
044 *
045 * r = &radic;(x^2 + y^2 + z^2)
046 * &theta; = atan2(y, x)
047 * &Phi; = acos(z/r)
048 * </pre>
049 * where <em>r</em> is the radius, <em>&theta;</em> is the azimuth angle, and <em>&Phi;</em> is the polar angle
050 * of the spherical coordinates.
051 *
052 * <p>There are numerous, competing conventions for the symbols used to represent spherical coordinate values. For
053 * example, the mathematical convention is to use <em>(r, &theta;, &Phi;)</em> to represent radius, azimuth angle, and
054 * polar angle, whereas the physics convention flips the angle values and uses <em>(r, &Phi;, &theta;)</em>. As such,
055 * this class avoids the use of these symbols altogether in favor of the less ambiguous formal names of the values,
056 * e.g. {@code radius}, {@code azimuth}, and {@code polar}.</p>
057 *
058 * <p>In order to ensure the uniqueness of coordinate sets, coordinate values
059 * are normalized so that {@code radius} is in the range {@code [0, +Infinity)},
060 * {@code azimuth} is in the range {@code [0, 2pi)}, and {@code polar} is in the
061 * range {@code [0, pi]}.</p>
062 *
063 * @see <a href="https://en.wikipedia.org/wiki/Spherical_coordinate_system">Spherical Coordinate System</a>
064 */
065public final class SphericalCoordinates implements Spatial {
066    /** Radius value. */
067    private final double radius;
068
069    /** Azimuth angle in radians. */
070    private final double azimuth;
071
072    /** Polar angle in radians. */
073    private final double polar;
074
075    /** Simple constructor. The given inputs are normalized.
076     * @param radius Radius value.
077     * @param azimuth Azimuth angle in radians.
078     * @param polar Polar angle in radians.
079     */
080    private SphericalCoordinates(final double radius, final double azimuth, final double polar) {
081        double rad = radius;
082        double az = azimuth;
083        double pol = polar;
084
085        if (rad < 0) {
086            // negative radius; flip the angles
087            rad = Math.abs(rad);
088            az += Math.PI;
089            pol += Math.PI;
090        }
091
092        this.radius = rad;
093        this.azimuth = normalizeAzimuth(az);
094        this.polar = normalizePolar(pol);
095    }
096
097    /** Return the radius value. The value is in the range {@code [0, +Infinity)}.
098     * @return the radius value
099     */
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}