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