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.twod;
18
19 import org.apache.commons.geometry.core.Spatial;
20 import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
21 import org.apache.commons.numbers.angle.Angle;
22
23 /** Class representing <a href="https://en.wikipedia.org/wiki/Polar_coordinate_system">polar coordinates</a>
24 * in 2 dimensional Euclidean space.
25 *
26 * <p>Polar coordinates are defined by a distance from a reference point
27 * and an angle from a reference direction. The distance value is called
28 * the radial coordinate, or <em>radius</em>, and the angle is called the angular coordinate,
29 * or <em>azimuth</em>. This class follows the standard
30 * mathematical convention of using the positive x-axis as the reference
31 * direction and measuring positive angles counter-clockwise, toward the
32 * positive y-axis. The origin is used as the reference point. Polar coordinate
33 * are related to Cartesian coordinates as follows:
34 * <pre>
35 * x = r * cos(θ)
36 * y = r * sin(θ)
37 *
38 * r = √(x^2 + y^2)
39 * θ = atan2(y, x)
40 * </pre>
41 * where <em>r</em> is the radius and <em>θ</em> is the azimuth of the polar coordinates.
42 *
43 * <p>In order to ensure the uniqueness of coordinate sets, coordinate values
44 * are normalized so that {@code radius} is in the range {@code [0, +Infinity)}
45 * and {@code azimuth} is in the range {@code [0, 2pi)}.</p>
46 *
47 * @see <a href="https://en.wikipedia.org/wiki/Polar_coordinate_system">Polar Coordinate System</a>
48 */
49 public final class PolarCoordinates implements Spatial {
50 /** Radius value. */
51 private final double radius;
52
53 /** Azimuth angle in radians. */
54 private final double azimuth;
55
56 /** Simple constructor. Input values are normalized.
57 * @param radius Radius value.
58 * @param azimuth Azimuth angle in radians.
59 */
60 private PolarCoordinates(final double radius, final double azimuth) {
61 double rad = radius;
62 double az = azimuth;
63
64 if (rad < 0) {
65 // negative radius; flip the angles
66 rad = Math.abs(radius);
67 az += Math.PI;
68 }
69
70 this.radius = rad;
71 this.azimuth = normalizeAzimuth(az);
72 }
73
74 /** Return the radius value. The value will be greater than or equal to 0.
75 * @return radius value
76 */
77 public double getRadius() {
78 return radius;
79 }
80
81 /** Return the azimuth angle in radians. The value will be
82 * in the range {@code [0, 2pi)}.
83 * @return azimuth value in radians.
84 */
85 public double getAzimuth() {
86 return azimuth;
87 }
88
89 /** {@inheritDoc} */
90 @Override
91 public int getDimension() {
92 return 2;
93 }
94
95 /** {@inheritDoc} */
96 @Override
97 public boolean isNaN() {
98 return Double.isNaN(radius) || Double.isNaN(azimuth);
99 }
100
101 /** {@inheritDoc} */
102 @Override
103 public boolean isInfinite() {
104 return !isNaN() && (Double.isInfinite(radius) || Double.isInfinite(azimuth));
105 }
106
107 /** {@inheritDoc} */
108 @Override
109 public boolean isFinite() {
110 return Double.isFinite(radius) && Double.isFinite(azimuth);
111 }
112
113 /** Convert this set of polar coordinates to Cartesian coordinates.
114 * @return A 2-dimensional vector with an equivalent set of
115 * coordinates in Cartesian form
116 */
117 public Vector2D toCartesian() {
118 return toCartesian(radius, azimuth);
119 }
120
121 /** Get a hashCode for this set of polar coordinates.
122 * <p>All NaN values have the same hash code.</p>
123 *
124 * @return a hash code value for this object
125 */
126 @Override
127 public int hashCode() {
128 if (isNaN()) {
129 return 191;
130 }
131 return 449 * (76 * Double.hashCode(radius) + Double.hashCode(azimuth));
132 }
133
134 /** Test for the equality of two sets of polar coordinates.
135 * <p>
136 * If all values of two sets of coordinates are exactly the same, and none are
137 * <code>Double.NaN</code>, the two sets are considered to be equal.
138 * </p>
139 * <p>
140 * <code>NaN</code> values are considered to globally affect the coordinates
141 * and be equal to each other - i.e, if either (or all) values of the
142 * coordinate set are equal to <code>Double.NaN</code>, the set as a whole is
143 * considered to equal <code>NaN</code>.
144 * </p>
145 *
146 * @param other Object to test for equality to this
147 * @return true if two PolarCoordinates objects are equal, false if
148 * object is null, not an instance of PolarCoordinates, or
149 * not equal to this PolarCoordinates instance
150 *
151 */
152 @Override
153 public boolean equals(final Object other) {
154 if (this == other) {
155 return true;
156 }
157 if (other instanceof PolarCoordinates) {
158 final PolarCoordinates rhs = (PolarCoordinates) other;
159 if (rhs.isNaN()) {
160 return this.isNaN();
161 }
162
163 return Double.compare(radius, rhs.radius) == 0 &&
164 Double.compare(azimuth, rhs.azimuth) == 0;
165 }
166 return false;
167 }
168
169 /** {@inheritDoc} */
170 @Override
171 public String toString() {
172 return SimpleTupleFormat.getDefault().format(radius, azimuth);
173 }
174
175 /** Return a new instance with the given polar coordinate values.
176 * The values are normalized so that {@code radius} lies in the range {@code [0, +Infinity)}
177 * and {@code azimuth} in the range {@code [0, 2pi)}.
178 * @param radius Radius value.
179 * @param azimuth Azimuth angle in radians.
180 * @return new {@link PolarCoordinates} instance
181 */
182 public static PolarCoordinates of(final double radius, final double azimuth) {
183 return new PolarCoordinates(radius, azimuth);
184 }
185
186 /** Convert the given Cartesian coordinates to polar form.
187 * @param x X coordinate value
188 * @param y Y coordinate value
189 * @return polar coordinates equivalent to the given Cartesian coordinates
190 */
191 public static PolarCoordinates fromCartesian(final double x, final double y) {
192 final double azimuth = Math.atan2(y, x);
193 final double radius = Math.hypot(x, y);
194
195 return new PolarCoordinates(radius, azimuth);
196 }
197
198 /** Convert the given Cartesian coordinates to polar form.
199 * @param vec vector containing Cartesian coordinates
200 * @return polar coordinates equivalent to the given Cartesian coordinates
201 */
202 public static PolarCoordinates fromCartesian(final Vector2D vec) {
203 return fromCartesian(vec.getX(), vec.getY());
204 }
205
206 /** Convert the given polar coordinates to Cartesian form.
207 * @param radius Radius value.
208 * @param azimuth Azimuth angle in radians.
209 * @return A 2-dimensional vector with an equivalent set of
210 * coordinates in Cartesian form
211 */
212 public static Vector2D toCartesian(final double radius, final double azimuth) {
213 final double x = radius * Math.cos(azimuth);
214 final double y = radius * Math.sin(azimuth);
215
216 return Vector2D.of(x, y);
217 }
218
219 /** Parse the given string and return a new polar coordinates instance. The parsed
220 * coordinates are normalized as in the {@link #of(double, double)} method. The expected string
221 * format is the same as that returned by {@link #toString()}.
222 * @param input the string to parse
223 * @return new {@link PolarCoordinates} instance
224 * @throws IllegalArgumentException if the string format is invalid.
225 */
226 public static PolarCoordinates parse(final String input) {
227 return SimpleTupleFormat.getDefault().parse(input, PolarCoordinates::new);
228 }
229
230 /** Normalize an azimuth value to be within the range {@code [0, 2pi)}.
231 * @param azimuth azimuth value in radians
232 * @return equivalent azimuth value in the range {@code [0, 2pi)}.
233 */
234 public static double normalizeAzimuth(final double azimuth) {
235 if (Double.isFinite(azimuth)) {
236 return Angle.Rad.WITHIN_0_AND_2PI.applyAsDouble(azimuth);
237 }
238
239 return azimuth;
240 }
241 }