SphericalCoordinates.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.geometry.euclidean.threed;
import org.apache.commons.geometry.core.Spatial;
import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
import org.apache.commons.geometry.euclidean.internal.Vectors;
import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
import org.apache.commons.numbers.angle.Angle;
/** Class representing <a href="https://en.wikipedia.org/wiki/Spherical_coordinate_system">spherical coordinates</a>
* in 3 dimensional Euclidean space.
*
* <p>Spherical coordinates for a point are defined by three values:
* <ol>
* <li><em>Radius</em> - The distance from the point to a fixed referenced point.</li>
* <li><em>Azimuth angle</em> - The angle measured from a fixed reference direction in a plane to
* the orthogonal projection of the point on that plane.</li>
* <li><em>Polar angle</em> - The angle measured from a fixed zenith direction to the point. The zenith
*direction must be orthogonal to the reference plane.</li>
* </ol>
* This class follows the convention of using the origin as the reference point; the positive x-axis as the
* reference direction for the azimuth angle, measured in the x-y plane with positive angles moving counter-clockwise
* toward the positive y-axis; and the positive z-axis as the zenith direction. Spherical coordinates are
* related to Cartesian coordinates as follows:
* <pre>
* x = r cos(θ) sin(Φ)
* y = r sin(θ) sin(Φ)
* z = r cos(Φ)
*
* r = √(x^2 + y^2 + z^2)
* θ = atan2(y, x)
* Φ = acos(z/r)
* </pre>
* where <em>r</em> is the radius, <em>θ</em> is the azimuth angle, and <em>Φ</em> is the polar angle
* of the spherical coordinates.
*
* <p>There are numerous, competing conventions for the symbols used to represent spherical coordinate values. For
* example, the mathematical convention is to use <em>(r, θ, Φ)</em> to represent radius, azimuth angle, and
* polar angle, whereas the physics convention flips the angle values and uses <em>(r, Φ, θ)</em>. As such,
* this class avoids the use of these symbols altogether in favor of the less ambiguous formal names of the values,
* e.g. {@code radius}, {@code azimuth}, and {@code polar}.</p>
*
* <p>In order to ensure the uniqueness of coordinate sets, coordinate values
* are normalized so that {@code radius} is in the range {@code [0, +Infinity)},
* {@code azimuth} is in the range {@code [0, 2pi)}, and {@code polar} is in the
* range {@code [0, pi]}.</p>
*
* @see <a href="https://en.wikipedia.org/wiki/Spherical_coordinate_system">Spherical Coordinate System</a>
*/
public final class SphericalCoordinates implements Spatial {
/** Radius value. */
private final double radius;
/** Azimuth angle in radians. */
private final double azimuth;
/** Polar angle in radians. */
private final double polar;
/** Simple constructor. The given inputs are normalized.
* @param radius Radius value.
* @param azimuth Azimuth angle in radians.
* @param polar Polar angle in radians.
*/
private SphericalCoordinates(final double radius, final double azimuth, final double polar) {
double rad = radius;
double az = azimuth;
double pol = polar;
if (rad < 0) {
// negative radius; flip the angles
rad = Math.abs(rad);
az += Math.PI;
pol += Math.PI;
}
this.radius = rad;
this.azimuth = normalizeAzimuth(az);
this.polar = normalizePolar(pol);
}
/** Return the radius value. The value is in the range {@code [0, +Infinity)}.
* @return the radius value
*/
public double getRadius() {
return radius;
}
/** Return the azimuth angle in radians. This is the angle in the x-y plane measured counter-clockwise from
* the positive x axis. The angle is in the range {@code [0, 2pi)}.
* @return the azimuth angle in radians
*/
public double getAzimuth() {
return azimuth;
}
/** Return the polar angle in radians. This is the angle the coordinate ray makes with the positive z axis.
* The angle is in the range {@code [0, pi]}.
* @return the polar angle in radians
*/
public double getPolar() {
return polar;
}
/** {@inheritDoc} */
@Override
public int getDimension() {
return 3;
}
/** {@inheritDoc} */
@Override
public boolean isNaN() {
return Double.isNaN(radius) || Double.isNaN(azimuth) || Double.isNaN(polar);
}
/** {@inheritDoc} */
@Override
public boolean isInfinite() {
return !isNaN() && (Double.isInfinite(radius) || Double.isInfinite(azimuth) || Double.isInfinite(polar));
}
/** {@inheritDoc} */
@Override
public boolean isFinite() {
return Double.isFinite(radius) && Double.isFinite(azimuth) && Double.isFinite(polar);
}
/** Convert this set of spherical coordinates to a Cartesian form.
* @return A 3-dimensional vector with an equivalent set of
* Cartesian coordinates.
*/
public Vector3D toVector() {
return toCartesian(radius, azimuth, polar);
}
/** Get a hashCode for this set of spherical coordinates.
* <p>All NaN values have the same hash code.</p>
*
* @return a hash code value for this object
*/
@Override
public int hashCode() {
if (isNaN()) {
return 127;
}
return (Double.hashCode(radius) >> 17) ^
(Double.hashCode(azimuth) >> 5) ^
Double.hashCode(polar);
}
/** Test for the equality of two sets of spherical coordinates.
* <p>
* If all values of two sets of coordinates are exactly the same, and none are
* <code>Double.NaN</code>, the two sets are considered to be equal.
* </p>
* <p>
* <code>NaN</code> values are considered to globally affect the coordinates
* and be equal to each other - i.e, if any (or all) values of the
* coordinate set are equal to <code>Double.NaN</code>, the set as a whole
* is considered to equal NaN.
* </p>
*
* @param other Object to test for equality to this
* @return true if two SphericalCoordinates objects are equal, false if
* object is null, not an instance of SphericalCoordinates, or
* not equal to this SphericalCoordinates instance
*
*/
@Override
public boolean equals(final Object other) {
if (this == other) {
return true;
}
if (other instanceof SphericalCoordinates) {
final SphericalCoordinates rhs = (SphericalCoordinates) other;
if (rhs.isNaN()) {
return this.isNaN();
}
return Double.compare(radius, rhs.radius) == 0 &&
Double.compare(azimuth, rhs.azimuth) == 0 &&
Double.compare(polar, rhs.polar) == 0;
}
return false;
}
/** {@inheritDoc} */
@Override
public String toString() {
return SimpleTupleFormat.getDefault().format(radius, azimuth, polar);
}
/** Return a new instance with the given spherical coordinate values. The values are normalized
* so that {@code radius} lies in the range {@code [0, +Infinity)}, {@code azimuth} lies in the range
* {@code [0, 2pi)}, and {@code polar} lies in the range {@code [0, +pi]}.
* @param radius the length of the line segment from the origin to the coordinate point.
* @param azimuth the angle in the x-y plane, measured in radians counter-clockwise
* from the positive x-axis.
* @param polar the angle in radians between the positive z-axis and the ray from the origin
* to the coordinate point.
* @return a new {@link SphericalCoordinates} instance representing the same point as the given set of
* spherical coordinates.
*/
public static SphericalCoordinates of(final double radius, final double azimuth, final double polar) {
return new SphericalCoordinates(radius, azimuth, polar);
}
/** Convert the given set of Cartesian coordinates to spherical coordinates.
* @param x X coordinate value
* @param y Y coordinate value
* @param z Z coordinate value
* @return a set of spherical coordinates equivalent to the given Cartesian coordinates
*/
public static SphericalCoordinates fromCartesian(final double x, final double y, final double z) {
final double radius = Vectors.norm(x, y, z);
final double azimuth = Math.atan2(y, x);
// default the polar angle to 0 when the radius is 0
final double polar = (radius > 0.0) ? Math.acos(z / radius) : 0.0;
return new SphericalCoordinates(radius, azimuth, polar);
}
/** Convert the given set of Cartesian coordinates to spherical coordinates.
* @param vec vector containing Cartesian coordinates to convert
* @return a set of spherical coordinates equivalent to the given Cartesian coordinates
*/
public static SphericalCoordinates fromCartesian(final Vector3D vec) {
return fromCartesian(vec.getX(), vec.getY(), vec.getZ());
}
/** Convert the given set of spherical coordinates to Cartesian coordinates.
* @param radius The spherical radius value.
* @param azimuth The spherical azimuth angle in radians.
* @param polar The spherical polar angle in radians.
* @return A 3-dimensional vector with an equivalent set of
* Cartesian coordinates.
*/
public static Vector3D toCartesian(final double radius, final double azimuth, final double polar) {
final double xyLength = radius * Math.sin(polar);
final double x = xyLength * Math.cos(azimuth);
final double y = xyLength * Math.sin(azimuth);
final double z = radius * Math.cos(polar);
return Vector3D.of(x, y, z);
}
/** Parse the given string and return a new {@link SphericalCoordinates} instance. The parsed
* coordinate values are normalized as in the {@link #of(double, double, double)} method.
* The expected string format is the same as that returned by {@link #toString()}.
* @param input the string to parse
* @return new {@link SphericalCoordinates} instance
* @throws IllegalArgumentException if the string format is invalid.
*/
public static SphericalCoordinates parse(final String input) {
return SimpleTupleFormat.getDefault().parse(input, SphericalCoordinates::new);
}
/** Normalize an azimuth value to be within the range {@code [0, 2pi)}. This
* is exactly equivalent to {@link PolarCoordinates#normalizeAzimuth(double)}.
* @param azimuth azimuth value in radians
* @return equivalent azimuth value in the range {@code [0, 2pi)}.
* @see PolarCoordinates#normalizeAzimuth(double)
*/
public static double normalizeAzimuth(final double azimuth) {
return PolarCoordinates.normalizeAzimuth(azimuth);
}
/** Normalize a polar value to be within the range {@code [0, +pi]}. Since the
* polar angle is the angle between two vectors (the zenith direction and the
* point vector), the sign of the angle is not significant as in the azimuth angle.
* For example, a polar angle of {@code -pi/2} and one of {@code +pi/2} will both
* normalize to {@code pi/2}.
* @param polar polar value in radians
* @return equivalent polar value in the range {@code [0, +pi]}
*/
public static double normalizePolar(final double polar) {
// normalize the polar angle; this is the angle between the polar vector and the point ray
// so it is unsigned (unlike the azimuth) and should be in the range [0, pi]
if (Double.isFinite(polar)) {
return Math.abs(Angle.Rad.WITHIN_MINUS_PI_AND_PI.applyAsDouble(polar));
}
return polar;
}
}