View Javadoc
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 }