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}