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.spherical.oned;
018
019import java.util.Comparator;
020
021import org.apache.commons.geometry.core.Point;
022import org.apache.commons.geometry.core.internal.DoubleFunction1N;
023import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
024import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
025import org.apache.commons.geometry.euclidean.twod.Vector2D;
026import org.apache.commons.numbers.angle.Angle;
027import org.apache.commons.numbers.core.Precision;
028
029/** This class represents a point on the 1-sphere, or in other words, an
030 * azimuth angle on a circle. The value of the azimuth angle is not normalized
031 * by default, meaning that instances can be constructed representing negative
032 * values or values greater than {@code 2pi}. However, instances separated by a
033 * multiple of {@code 2pi} are considered equivalent for most methods, with the
034 * exceptions being {@link #equals(Object)} and {@link #hashCode()}, where the
035 * azimuth values must match exactly in order for instances to be considered
036 * equal.
037 *
038 * <p>Instances of this class are guaranteed to be immutable.</p>
039 */
040public final class Point1S implements Point<Point1S> {
041
042    /** A point with coordinates set to {@code 0*pi}. */
043    public static final Point1S ZERO = Point1S.of(0.0);
044
045    /** A point with coordinates set to {@code pi}. */
046    public static final Point1S PI = Point1S.of(Math.PI);
047
048    /** A point with all coordinates set to NaN. */
049    public static final Point1S NaN = Point1S.of(Double.NaN);
050
051    /** Comparator that sorts points by normalized azimuth in ascending order.
052     * Points are only considered equal if their normalized azimuths match exactly.
053     * Null arguments are evaluated as being greater than non-null arguments.
054     * @see #getNormalizedAzimuth()
055     */
056    public static final Comparator<Point1S> NORMALIZED_AZIMUTH_ASCENDING_ORDER = (a, b) -> {
057        int cmp = 0;
058
059        if (a != null && b != null) {
060            cmp = Double.compare(a.getNormalizedAzimuth(), b.getNormalizedAzimuth());
061        } else if (a != null) {
062            cmp = -1;
063        } else if (b != null) {
064            cmp = 1;
065        }
066
067        return cmp;
068    };
069
070    /** Azimuthal angle in radians. */
071    private final double azimuth;
072
073    /** Normalized azimuth value in the range {@code [0, 2pi)}. */
074    private final double normalizedAzimuth;
075
076    /** Build a point from its internal components.
077     * @param azimuth azimuth angle
078     * @param normalizedAzimuth azimuth angle normalized to the range {@code [0, 2pi)}
079     */
080    private Point1S(final double azimuth, final double normalizedAzimuth) {
081        this.azimuth  = azimuth;
082        this.normalizedAzimuth = normalizedAzimuth;
083    }
084
085    /** Get the azimuth angle in radians. This value is not normalized and
086     * can be any floating point number.
087     * @return azimuth angle
088     * @see Point1S#of(double)
089     */
090    public double getAzimuth() {
091        return azimuth;
092    }
093
094    /** Get the azimuth angle normalized to the range {@code [0, 2pi)}.
095     * @return the azimuth angle normalized to the range {@code [0, 2pi)}.
096     */
097    public double getNormalizedAzimuth() {
098        return normalizedAzimuth;
099    }
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}