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.spherical.oned;
18
19 import java.util.Comparator;
20
21 import org.apache.commons.geometry.core.Point;
22 import org.apache.commons.geometry.core.internal.DoubleFunction1N;
23 import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
24 import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
25 import org.apache.commons.geometry.euclidean.twod.Vector2D;
26 import org.apache.commons.numbers.angle.Angle;
27 import org.apache.commons.numbers.core.Precision;
28
29 /** This class represents a point on the 1-sphere, or in other words, an
30 * azimuth angle on a circle. The value of the azimuth angle is not normalized
31 * by default, meaning that instances can be constructed representing negative
32 * values or values greater than {@code 2pi}. However, instances separated by a
33 * multiple of {@code 2pi} are considered equivalent for most methods, with the
34 * exceptions being {@link #equals(Object)} and {@link #hashCode()}, where the
35 * azimuth values must match exactly in order for instances to be considered
36 * equal.
37 *
38 * <p>Instances of this class are guaranteed to be immutable.</p>
39 */
40 public final class Point1S implements Point<Point1S> {
41
42 /** A point with coordinates set to {@code 0*pi}. */
43 public static final Point1S ZERO = Point1S.of(0.0);
44
45 /** A point with coordinates set to {@code pi}. */
46 public static final Point1S PI = Point1S.of(Math.PI);
47
48 /** A point with all coordinates set to NaN. */
49 public static final Point1S NaN = Point1S.of(Double.NaN);
50
51 /** Comparator that sorts points by normalized azimuth in ascending order.
52 * Points are only considered equal if their normalized azimuths match exactly.
53 * Null arguments are evaluated as being greater than non-null arguments.
54 * @see #getNormalizedAzimuth()
55 */
56 public static final Comparator<Point1S> NORMALIZED_AZIMUTH_ASCENDING_ORDER = (a, b) -> {
57 int cmp = 0;
58
59 if (a != null && b != null) {
60 cmp = Double.compare(a.getNormalizedAzimuth(), b.getNormalizedAzimuth());
61 } else if (a != null) {
62 cmp = -1;
63 } else if (b != null) {
64 cmp = 1;
65 }
66
67 return cmp;
68 };
69
70 /** Azimuthal angle in radians. */
71 private final double azimuth;
72
73 /** Normalized azimuth value in the range {@code [0, 2pi)}. */
74 private final double normalizedAzimuth;
75
76 /** Build a point from its internal components.
77 * @param azimuth azimuth angle
78 * @param normalizedAzimuth azimuth angle normalized to the range {@code [0, 2pi)}
79 */
80 private Point1S(final double azimuth, final double normalizedAzimuth) {
81 this.azimuth = azimuth;
82 this.normalizedAzimuth = normalizedAzimuth;
83 }
84
85 /** Get the azimuth angle in radians. This value is not normalized and
86 * can be any floating point number.
87 * @return azimuth angle
88 * @see Point1S#of(double)
89 */
90 public double getAzimuth() {
91 return azimuth;
92 }
93
94 /** Get the azimuth angle normalized to the range {@code [0, 2pi)}.
95 * @return the azimuth angle normalized to the range {@code [0, 2pi)}.
96 */
97 public double getNormalizedAzimuth() {
98 return normalizedAzimuth;
99 }
100
101 /** Get the normalized vector corresponding to this azimuth angle in 2D Euclidean space.
102 * @return normalized vector
103 */
104 public Vector2D getVector() {
105 if (isFinite()) {
106 return PolarCoordinates.toCartesian(1, azimuth);
107 }
108
109 return null;
110 }
111
112 /** {@inheritDoc} */
113 @Override
114 public int getDimension() {
115 return 1;
116 }
117
118 /** {@inheritDoc} */
119 @Override
120 public boolean isNaN() {
121 return Double.isNaN(azimuth);
122 }
123
124 /** {@inheritDoc} */
125 @Override
126 public boolean isInfinite() {
127 return !isNaN() && Double.isInfinite(azimuth);
128 }
129
130 /** {@inheritDoc} */
131 @Override
132 public boolean isFinite() {
133 return Double.isFinite(azimuth);
134 }
135
136 /** {@inheritDoc}
137 *
138 * <p>The returned value is the shortest angular distance between
139 * the two points, in the range {@code [0, pi]}.</p>
140 */
141 @Override
142 public double distance(final Point1S point) {
143 return distance(this, point);
144 }
145
146 /** Return the signed distance (angular separation) between this instance and the
147 * given point in the range {@code [-pi, pi)}. If {@code p1} is the current instance,
148 * {@code p2} the given point, and {@code d} the signed distance, then
149 * {@code p1.getAzimuth() + d} is an angle equivalent to {@code p2.getAzimuth()}.
150 * @param point point to compute the signed distance to
151 * @return the signed distance between this instance and the given point in the range
152 * {@code [-pi, pi)}
153 */
154 public double signedDistance(final Point1S point) {
155 return signedDistance(this, point);
156 }
157
158 /** Return an equivalent point with an azimuth value at or above the given base
159 * value in radians. The returned point has an azimuth value in the range
160 * {@code [base, base + 2pi)}.
161 * @param base base azimuth to place this instance's azimuth value above
162 * @return a point equivalent to the current instance but with an azimuth
163 * value in the range {@code [base, base + 2pi)}
164 * @throws IllegalArgumentException if the azimuth value is NaN or infinite and
165 * cannot be normalized
166 */
167 public Point1S above(final double base) {
168 if (isFinite()) {
169 final double az = Angle.Rad.normalizer(base).applyAsDouble(azimuth);
170 return new Point1S(az, normalizedAzimuth);
171 }
172 throw new IllegalArgumentException("Cannot normalize azimuth value: " + azimuth);
173 }
174
175 /** Return an equivalent point with an azimuth value at or above the given base.
176 * The returned point has an azimuth value in the range {@code [base, base + 2pi)}.
177 * @param base point to place this instance's azimuth value above
178 * @return a point equivalent to the current instance but with an azimuth
179 * value in the range {@code [base, base + 2pi)}
180 * @throws IllegalArgumentException if the azimuth value is NaN or infinite and
181 * cannot be normalized
182 */
183 public Point1S above(final Point1S base) {
184 return above(base.getAzimuth());
185 }
186
187 /** Get the point exactly opposite this point on the circle, {@code pi} distance away.
188 * The azimuth of the antipodal point is in the range {@code [0, 2pi)}.
189 * @return the point exactly opposite this point on the circle
190 */
191 public Point1S antipodal() {
192 double az = normalizedAzimuth + Math.PI;
193 if (az >= Angle.TWO_PI) {
194 az -= Angle.TWO_PI;
195 }
196
197 return Point1S.of(az);
198 }
199
200 /** Return true if this instance is equivalent to the argument. The points are
201 * considered equivalent if the shortest angular distance between them is equal to
202 * zero as evaluated by the given precision context. This means that points that differ
203 * in azimuth by multiples of {@code 2pi} are considered equivalent.
204 * @param other point to compare with
205 * @param precision precision context used for floating point comparisons
206 * @return true if this instance is equivalent to the argument
207 */
208 public boolean eq(final Point1S other, final Precision.DoubleEquivalence precision) {
209 final double dist = signedDistance(other);
210 return precision.eqZero(dist);
211 }
212
213 /**
214 * Get a hashCode for the point. Points normally must have exactly the
215 * same azimuth angles in order to have the same hash code. Points
216 * will angles that differ by multiples of {@code 2pi} will not
217 * necessarily have the same hash code.
218 *
219 * <p>All NaN values have the same hash code.</p>
220 *
221 * @return a hash code value for this object
222 */
223 @Override
224 public int hashCode() {
225 if (isNaN()) {
226 return 542;
227 }
228 return (Double.hashCode(azimuth) >> 17) ^
229 Double.hashCode(normalizedAzimuth);
230 }
231
232 /** Test for the exact equality of two points on the 1-sphere.
233 *
234 * <p>If all coordinates of the given points are exactly the same, and none are
235 * <code>Double.NaN</code>, the points are considered to be equal. Points with
236 * azimuth values separated by multiples of {@code 2pi} are <em>not</em> considered
237 * equal.</p>
238 *
239 * <p><code>NaN</code> coordinates are considered to affect globally the vector
240 * and be equals to each other - i.e, if either (or all) coordinates of the
241 * point are equal to <code>Double.NaN</code>, the point is equal to
242 * {@link #NaN}.</p>
243 *
244 * @param other Object to test for equality to this
245 * @return true if two points on the 1-sphere objects are exactly equal, false if
246 * object is null, not an instance of Point1S, or
247 * not equal to this Point1S instance
248 *
249 */
250 @Override
251 public boolean equals(final Object other) {
252 if (this == other) {
253 return true;
254 }
255
256 if (other instanceof Point1S) {
257 final Point1S rhs = (Point1S) other;
258
259 if (rhs.isNaN()) {
260 return this.isNaN();
261 }
262
263 return Double.compare(azimuth, rhs.azimuth) == 0 &&
264 Double.compare(normalizedAzimuth, rhs.normalizedAzimuth) == 0;
265 }
266
267 return false;
268 }
269
270 /** {@inheritDoc} */
271 @Override
272 public String toString() {
273 return SimpleTupleFormat.getDefault().format(getAzimuth());
274 }
275
276 /** Create a new point instance from the given azimuth angle.
277 * @param azimuth azimuth angle in radians
278 * @return point instance with the given azimuth angle
279 * @see #getAzimuth()
280 */
281 public static Point1S of(final double azimuth) {
282 final double normalizedAzimuth = PolarCoordinates.normalizeAzimuth(azimuth);
283
284 return new Point1S(azimuth, normalizedAzimuth);
285 }
286
287 /** Create a new point instance from the given azimuth angle.
288 * @param azimuth azimuth azimuth angle
289 * @return point instance with the given azimuth angle
290 * @see #getAzimuth()
291 */
292 public static Point1S of(final Angle azimuth) {
293 return of(azimuth.toRad().getAsDouble());
294 }
295
296 /** Create a new point instance from the given Euclidean 2D vector. The returned point
297 * will have an azimuth value equal to the angle between the positive x-axis and the
298 * given vector, measured in a counter-clockwise direction.
299 * @param vector 3D vector to create the point from
300 * @return a new point instance with an azimuth value equal to the angle between the given
301 * vector and the positive x-axis, measured in a counter-clockwise direction
302 */
303 public static Point1S from(final Vector2D vector) {
304 final PolarCoordinates polar = PolarCoordinates.fromCartesian(vector);
305 final double az = polar.getAzimuth();
306
307 return new Point1S(az, az);
308 }
309
310 /** Create a new point instance containing an azimuth value equal to that of the
311 * given set of polar coordinates.
312 * @param polar polar coordinates to convert to a point
313 * @return a new point instance containing an azimuth value equal to that of
314 * the given set of polar coordinates.
315 */
316 public static Point1S from(final PolarCoordinates polar) {
317 final double az = polar.getAzimuth();
318
319 return new Point1S(az, az);
320 }
321
322 /** Parse the given string and returns a new point instance. The expected string
323 * format is the same as that returned by {@link #toString()}.
324 * @param str the string to parse
325 * @return point instance represented by the string
326 * @throws IllegalArgumentException if the given string has an invalid format
327 */
328 public static Point1S parse(final String str) {
329 return SimpleTupleFormat.getDefault().parse(str, (DoubleFunction1N<Point1S>) Point1S::of);
330 }
331
332 /** Compute the signed shortest distance (angular separation) between two points. The return
333 * value is in the range {@code [-pi, pi)} and is such that {@code p1.getAzimuth() + d}
334 * (where {@code d} is the signed distance) is an angle equivalent to {@code p2.getAzimuth()}.
335 * @param p1 first point
336 * @param p2 second point
337 * @return the signed angular separation between p1 and p2, in the range {@code [-pi, pi)}.
338 */
339 public static double signedDistance(final Point1S p1, final Point1S p2) {
340 double dist = p2.normalizedAzimuth - p1.normalizedAzimuth;
341 if (dist < -Math.PI) {
342 dist += Angle.TWO_PI;
343 }
344 if (dist >= Math.PI) {
345 dist -= Angle.TWO_PI;
346 }
347 return dist;
348 }
349
350 /** Compute the shortest distance (angular separation) between two points. The returned
351 * value is in the range {@code [0, pi]}. This method is equal to the absolute value of
352 * the {@link #signedDistance(Point1S, Point1S) signed distance}.
353 * @param p1 first point
354 * @param p2 second point
355 * @return the angular separation between p1 and p2, in the range {@code [0, pi]}.
356 * @see #signedDistance(Point1S, Point1S)
357 */
358 public static double distance(final Point1S p1, final Point1S p2) {
359 return Math.abs(signedDistance(p1, p2));
360 }
361 }