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(θ) sin(Φ) 042 * y = r sin(θ) sin(Φ) 043 * z = r cos(Φ) 044 * 045 * r = √(x^2 + y^2 + z^2) 046 * θ = atan2(y, x) 047 * Φ = acos(z/r) 048 * </pre> 049 * where <em>r</em> is the radius, <em>θ</em> is the azimuth angle, and <em>Φ</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, θ, Φ)</em> to represent radius, azimuth angle, and 054 * polar angle, whereas the physics convention flips the angle values and uses <em>(r, Φ, θ)</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}