blob: 630d6efcdcf3e31f12fe50251a8a923548e2ba16 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.geometry.spherical.oned;
import java.util.Comparator;
import java.util.Objects;
import org.apache.commons.geometry.core.Geometry;
import org.apache.commons.geometry.core.Point;
import org.apache.commons.geometry.core.exception.GeometryValueException;
import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
import org.apache.commons.numbers.angle.PlaneAngle;
import org.apache.commons.numbers.angle.PlaneAngleRadians;
/** This class represents a point on the 1-sphere, or in other words, an
* azimuth angle on a circle. The value of the azimuth angle is not normalized
* by default, meaning that instances can be constructed representing negative
* values or values greater than {@code 2pi}. However, instances separated by a
* multiple of {@code 2pi} are considered equivalent for most methods, with the
* exceptions being {@link #equals(Object)} and {@link #hashCode()}, where the
* azimuth values must match exactly in order for instances to be considered
* equal.
*
* <p>Instances of this class are guaranteed to be immutable.</p>
*/
public final class Point1S implements Point<Point1S> {
/** A point with coordinates set to {@code 0*pi}. */
public static final Point1S ZERO = Point1S.of(Geometry.ZERO_PI);
/** A point with coordinates set to {@code pi}. */
public static final Point1S PI = Point1S.of(Geometry.PI);
// CHECKSTYLE: stop ConstantName
/** A point with all coordinates set to NaN. */
public static final Point1S NaN = Point1S.of(Double.NaN);
// CHECKSTYLE: resume ConstantName
/** Comparator that sorts points by normalized azimuth in ascending order.
* Points are only considered equal if their normalized azimuths match exactly.
* Null arguments are evaluated as being greater than non-null arguments.
* @see #getNormalizedAzimuth()
*/
public static final Comparator<Point1S> NORMALIZED_AZIMUTH_ASCENDING_ORDER = (a, b) -> {
int cmp = 0;
if (a != null && b != null) {
cmp = Double.compare(a.getNormalizedAzimuth(), b.getNormalizedAzimuth());
} else if (a != null) {
cmp = -1;
} else if (b != null) {
cmp = 1;
}
return cmp;
};
/** Azimuthal angle in radians. */
private final double azimuth;
/** Normalized azimuth value in the range {@code [0, 2pi)}. */
private final double normalizedAzimuth;
/** Build a point from its internal components.
* @param azimuth azimuth angle
* @param normalizedAzimuth azimuth angle normalized to the range {@code [0, 2pi)}
*/
private Point1S(final double azimuth, final double normalizedAzimuth) {
this.azimuth = azimuth;
this.normalizedAzimuth = normalizedAzimuth;
}
/** Get the azimuth angle in radians. This value is not normalized and
* can be any floating point number.
* @return azimuth angle
* @see Point1S#of(double)
*/
public double getAzimuth() {
return azimuth;
}
/** Get the azimuth angle normalized to the range {@code [0, 2pi)}.
* @return the azimuth angle normalized to the range {@code [0, 2pi)}.
*/
public double getNormalizedAzimuth() {
return normalizedAzimuth;
}
/** Get the normalized vector corresponding to this azimuth angle in 2D Euclidean space.
* @return normalized vector
*/
public Vector2D getVector() {
if (isFinite()) {
return PolarCoordinates.toCartesian(1, azimuth);
}
return null;
}
/** {@inheritDoc} */
@Override
public int getDimension() {
return 1;
}
/** {@inheritDoc} */
@Override
public boolean isNaN() {
return Double.isNaN(azimuth);
}
/** {@inheritDoc} */
@Override
public boolean isInfinite() {
return !isNaN() && Double.isInfinite(azimuth);
}
/** {@inheritDoc} */
@Override
public boolean isFinite() {
return Double.isFinite(azimuth);
}
/** {@inheritDoc}
*
* <p>The returned value is the shortest angular distance between
* the two points, in the range {@code [0, pi]}.</p>
*/
@Override
public double distance(final Point1S point) {
return distance(this, point);
}
/** Return the signed distance (angular separation) between this instance and the
* given point in the range {@code [-pi, pi)}. If {@code p1} is the current instance,
* {@code p2} the given point, and {@code d} the signed distance, then
* {@code p1.getAzimuth() + d} is an angle equivalent to {@code p2.getAzimuth()}.
* @param point point to compute the signed distance to
* @return the signed distance between this instance and the given point in the range
* {@code [-pi, pi)}
*/
public double signedDistance(final Point1S point) {
return signedDistance(this, point);
}
/** Return an equivalent point with an azimuth value at or above the given base.
* The returned point has an azimuth value in the range {@code [base, base + 2pi)}.
* @param base point to place this instance's azimuth value above
* @return a point equivalent to the current instance but with an azimuth
* value in the range {@code [base, base + 2pi)}
* @throws GeometryValueException if the azimuth value is NaN or infinite and
* cannot be normalized
*/
public Point1S above(final Point1S base) {
return normalize(base.getAzimuth() + Geometry.PI);
}
/** Return an equivalent point with an azimuth value strictly below the given base.
* The returned point has an azimuth value in the range {@code [base - 2pi, base)}.
* @param base point to place this instance's azimuth value below
* @return a point equivalent to the current instance but with an azimuth
* value in the range {@code [base - 2pi, base)}
* @throws GeometryValueException if the azimuth value is NaN or infinite and
* cannot be normalized
*/
public Point1S below(final Point1S base) {
return normalize(base.getAzimuth() - Geometry.PI);
}
/** Normalize this point around the given center point. The azimuth value of
* the returned point is in the range {@code [center - pi, center + pi)}.
* @param center point to center this instance around
* @return a point equivalent to this instance but with an azimuth value
* in the range {@code [center - pi, center + pi)}.
* @throws GeometryValueException if the azimuth value is NaN or infinite and
* cannot be normalized
*/
public Point1S normalize(final Point1S center) {
return normalize(center.getAzimuth());
}
/** Return an equivalent point with an azimuth value normalized around the given center
* angle. The azimuth value of the returned point is in the range
* {@code [center - pi, center + pi)}.
* @param center angle to center this instance around
* @return a point equivalent to this instance but with an azimuth value
* in the range {@code [center - pi, center + pi)}.
* @throws GeometryValueException if the azimuth value is NaN or infinite and
* cannot be normalized
*/
public Point1S normalize(final double center) {
if (isFinite()) {
final double az = PlaneAngleRadians.normalize(azimuth, center);
return new Point1S(az, normalizedAzimuth);
}
throw new GeometryValueException("Cannot normalize azimuth value: " + azimuth);
}
/** Get the point exactly opposite this point on the circle, {@code pi} distance away.
* The azimuth of the antipodal point is in the range {@code [0, 2pi)}.
* @return the point exactly opposite this point on the circle
*/
public Point1S antipodal() {
double az = normalizedAzimuth + Geometry.PI;
if (az >= Geometry.TWO_PI) {
az -= Geometry.TWO_PI;
}
return Point1S.of(az);
}
/** Return true if this instance is equivalent to the argument. The points are
* considered equivalent if the shortest angular distance between them is equal to
* zero as evaluated by the given precision context. This means that points that differ
* in azimuth by multiples of {@code 2pi} are considered equivalent.
* @param other point to compare with
* @param precision precision context used for floating point comparisons
* @return true if this instance is equivalent to the argument
*/
public boolean eq(final Point1S other, final DoublePrecisionContext precision) {
final double dist = signedDistance(other);
return precision.eqZero(dist);
}
/**
* Get a hashCode for the point. Points normally must have exactly the
* same azimuth angles in order to have the same hash code. Points
* will angles that differ by multiples of {@code 2pi} will not
* necessarily have the same hash code.
*
* <p>All NaN values have the same hash code.</p>
*
* @return a hash code value for this object
*/
@Override
public int hashCode() {
if (isNaN()) {
return 542;
}
return 1759 * Objects.hash(azimuth, normalizedAzimuth);
}
/** Test for the exact equality of two points on the 1-sphere.
*
* <p>If all coordinates of the given points are exactly the same, and none are
* <code>Double.NaN</code>, the points are considered to be equal. Points with
* azimuth values separated by multiples of {@code 2pi} are <em>not</em> considered
* equal.</p>
*
* <p><code>NaN</code> coordinates are considered to affect globally the vector
* and be equals to each other - i.e, if either (or all) coordinates of the
* point are equal to <code>Double.NaN</code>, the point is equal to
* {@link #NaN}.</p>
*
* @param other Object to test for equality to this
* @return true if two points on the 1-sphere objects are exactly equal, false if
* object is null, not an instance of Point1S, or
* not equal to this Point1S instance
*
*/
@Override
public boolean equals(final Object other) {
if (this == other) {
return true;
}
if (other instanceof Point1S) {
final Point1S rhs = (Point1S) other;
if (rhs.isNaN()) {
return this.isNaN();
}
return Double.compare(azimuth, rhs.azimuth) == 0 &&
Double.compare(normalizedAzimuth, rhs.normalizedAzimuth) == 0;
}
return false;
}
/** {@inheritDoc} */
@Override
public String toString() {
return SimpleTupleFormat.getDefault().format(getAzimuth());
}
/** Create a new point instance from the given azimuth angle.
* @param azimuth azimuth angle in radians
* @return point instance with the given azimuth angle
* @see #getAzimuth()
*/
public static Point1S of(final double azimuth) {
final double normalizedAzimuth = PolarCoordinates.normalizeAzimuth(azimuth);
return new Point1S(azimuth, normalizedAzimuth);
}
/** Create a new point instance from the given azimuth angle.
* @param azimuth azimuth azimuth angle in radians
* @return point instance with the given azimuth angle
* @see #getAzimuth()
*/
public static Point1S of(final PlaneAngle azimuth) {
return of(azimuth.toRadians());
}
/** Create a new point instance from the given Euclidean 2D vector. The returned point
* will have an azimuth value equal to the angle between the positive x-axis and the
* given vector, measured in a counter-clockwise direction.
* @param vector 3D vector to create the point from
* @return a new point instance with an azimuth value equal to the angle between the given
* vector and the positive x-axis, measured in a counter-clockwise direction
*/
public static Point1S from(final Vector2D vector) {
final PolarCoordinates polar = PolarCoordinates.fromCartesian(vector);
final double az = polar.getAzimuth();
return new Point1S(az, az);
}
/** Create a new point instance containing an azimuth value equal to that of the
* given set of polar coordinates.
* @param polar polar coordinates to convert to a point
* @return a new point instance containing an azimuth value equal to that of
* the given set of polar coordinates.
*/
public static Point1S from(final PolarCoordinates polar) {
final double az = polar.getAzimuth();
return new Point1S(az, az);
}
/** Parse the given string and returns a new point instance. The expected string
* format is the same as that returned by {@link #toString()}.
* @param str the string to parse
* @return point instance represented by the string
* @throws IllegalArgumentException if the given string has an invalid format
*/
public static Point1S parse(final String str) {
return SimpleTupleFormat.getDefault().parse(str, az -> Point1S.of(az));
}
/** Compute the signed shortest distance (angular separation) between two points. The return
* value is in the range {@code [-pi, pi)} and is such that {@code p1.getAzimuth() + d}
* (where {@code d} is the signed distance) is an angle equivalent to {@code p2.getAzimuth()}.
* @param p1 first point
* @param p2 second point
* @return the signed angular separation between p1 and p2, in the range {@code [-pi, pi)}.
*/
public static double signedDistance(final Point1S p1, final Point1S p2) {
double dist = p2.normalizedAzimuth - p1.normalizedAzimuth;
if (dist < -Geometry.PI) {
dist += Geometry.TWO_PI;
}
if (dist >= Geometry.PI) {
dist -= Geometry.TWO_PI;
}
return dist;
}
/** Compute the shortest distance (angular separation) between two points. The returned
* value is in the range {@code [0, pi]}. This method is equal to the absolute value of
* the {@link #signedDistance(Point1S, Point1S) signed distance}.
* @param p1 first point
* @param p2 second point
* @return the angular separation between p1 and p2, in the range {@code [0, pi]}.
* @see #signedDistance(Point1S, Point1S)
*/
public static double distance(final Point1S p1, final Point1S p2) {
return Math.abs(signedDistance(p1, p2));
}
}