This is an automated email from the ASF dual-hosted git repository.

mattjuntunen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git


The following commit(s) were added to refs/heads/master by this push:
     new e13f152  GEOMETRY-143: defining strict rules in CutAngle for 
classification of points near zero; initial fix provided by Ron Goldman
e13f152 is described below

commit e13f15213d9dc46c96759d87660f8dbb05155811
Author: Matt Juntunen <mattjuntu...@apache.org>
AuthorDate: Sun Jan 23 00:15:25 2022 -0500

    GEOMETRY-143: defining strict rules in CutAngle for classification of 
points near zero; initial fix provided by Ron Goldman
---
 .../geometry/spherical/oned/AngularInterval.java   | 38 ++++++----
 .../commons/geometry/spherical/oned/CutAngle.java  | 88 +++++++++++++++++-----
 .../commons/geometry/spherical/oned/Point1S.java   | 16 ++++
 .../spherical/oned/AngularIntervalTest.java        | 31 ++++++++
 .../geometry/spherical/oned/CutAngleTest.java      | 23 ++++++
 .../geometry/spherical/oned/Point1STest.java       | 45 +++++++++++
 6 files changed, 206 insertions(+), 35 deletions(-)

diff --git 
a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/AngularInterval.java
 
b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/AngularInterval.java
index 906ac58..ef7e457 100644
--- 
a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/AngularInterval.java
+++ 
b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/AngularInterval.java
@@ -47,6 +47,9 @@ public class AngularInterval implements 
HyperplaneBoundedRegion<Point1S> {
     /** Point halfway between the min and max boundaries. */
     private final Point1S midpoint;
 
+    /** Flag set to true if the interval wraps around the {@code 0/2pi} point. 
*/
+    private final boolean wraps;
+
     /** Construct a new instance representing the angular region between the 
given
      * min and max azimuth boundaries. The arguments must be either all finite 
or all
      * null (to indicate the full space). If the boundaries are finite, then 
the min
@@ -59,9 +62,22 @@ public class AngularInterval implements 
HyperplaneBoundedRegion<Point1S> {
 
         this.minBoundary = minBoundary;
         this.maxBoundary = maxBoundary;
-        this.midpoint = (minBoundary != null && maxBoundary != null) ?
-                Point1S.of(0.5 * (minBoundary.getAzimuth() + 
maxBoundary.getAzimuth())) :
-                null;
+
+        Point1S midpointVal = null;
+        boolean wrapsVal = false;
+
+        if (minBoundary != null && maxBoundary != null) {
+            midpointVal = Point1S.of(0.5 * (minBoundary.getAzimuth() + 
maxBoundary.getAzimuth()));
+
+            // The interval wraps zero if the max boundary lies on the other 
side of zero than
+            // the min. This is a more reliable way to compute the wrapping 
flag than direct
+            // comparison of the normalized azimuths since this approach takes 
into account
+            // azimuths that are equivalent to zero.
+            wrapsVal = minBoundary.classify(maxBoundary.getPoint()) == 
HyperplaneLocation.PLUS;
+        }
+
+        this.midpoint =  midpointVal;
+        this.wraps = wrapsVal;
     }
 
     /** Get the minimum azimuth angle for the interval, or {@code 0}
@@ -163,13 +179,11 @@ public class AngularInterval implements 
HyperplaneBoundedRegion<Point1S> {
             final HyperplaneLocation minLoc = minBoundary.classify(pt);
             final HyperplaneLocation maxLoc = maxBoundary.classify(pt);
 
-            final boolean wraps = wrapsZero();
-
-            if ((!wraps && (minLoc == HyperplaneLocation.PLUS || maxLoc == 
HyperplaneLocation.PLUS)) ||
+            if (minLoc == HyperplaneLocation.ON || maxLoc == 
HyperplaneLocation.ON) {
+                return RegionLocation.BOUNDARY;
+            } else if ((!wraps && (minLoc == HyperplaneLocation.PLUS || maxLoc 
== HyperplaneLocation.PLUS)) ||
                     (wraps && minLoc == HyperplaneLocation.PLUS && maxLoc == 
HyperplaneLocation.PLUS)) {
                 return RegionLocation.OUTSIDE;
-            } else if (minLoc == HyperplaneLocation.ON || maxLoc == 
HyperplaneLocation.ON) {
-                return RegionLocation.BOUNDARY;
             }
         }
         return RegionLocation.INSIDE;
@@ -195,13 +209,7 @@ public class AngularInterval implements 
HyperplaneBoundedRegion<Point1S> {
      * @return true if the interval wraps around the zero/{@code 2pi} point
      */
     public boolean wrapsZero() {
-        if (!isFull()) {
-            final double minNormAz = 
minBoundary.getPoint().getNormalizedAzimuth();
-            final double maxNormAz = 
maxBoundary.getPoint().getNormalizedAzimuth();
-
-            return maxNormAz < minNormAz;
-        }
-        return false;
+        return wraps;
     }
 
     /** Return a new instance transformed by the argument. If the transformed 
size
diff --git 
a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java
 
b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java
index ad760a4..dcb993f 100644
--- 
a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java
+++ 
b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java
@@ -33,24 +33,46 @@ import org.apache.commons.numbers.core.Precision;
  * meaning an azimuth angle and a direction (increasing or decreasing angles)
  * along the circle.
  *
+ * <p><strong>Point Classification</strong></p>
  * <p>Hyperplanes split the spaces they are embedded in into three distinct 
parts:
  * the hyperplane itself, a plus side and a minus side. However, since 
spherical
  * space wraps around, a single oriented point is not sufficient to partition 
the space;
  * any point could be classified as being on the plus or minus side of a 
hyperplane
  * depending on the direction that the circle is traversed. The approach taken 
in this
- * class to address this issue is to (1) define a second, implicit cut point 
at {@code 0pi} and
+ * class to address this issue is to (1) define a second, implicit cut point 
at \(0\pi\) and
  * (2) define the domain of hyperplane points (for partitioning purposes) to 
be the
- * range {@code [0, 2pi)}. Each hyperplane then splits the space into the 
intervals
- * {@code [0, x]} and {@code [x, 2pi)}, where {@code x} is the location of the 
hyperplane.
+ * range \([0, 2\pi)\). Each hyperplane then splits the space into the 
intervals
+ * \([0, x]\) and \([x, 2\pi)\), where \(x\) is the location of the hyperplane.
  * One way to visualize this is to picture the circle as a cake that has 
already been
- * cut at {@code 0pi}. Each hyperplane then specifies the location of the 
second
+ * cut at \(0\pi\). Each hyperplane then specifies the location of the second
  * cut of the cake, with the plus and minus sides being the pieces thus cut.
  * </p>
  *
- * <p>Note that with the hyperplane partitioning rules described above, the 
hyperplane
- * at {@code 0pi} is unique in that it has the entire space on one side (minus 
the hyperplane
- * itself) and no points whatsoever on the other. This is very different from 
hyperplanes in
- * Euclidean space, which always have infinitely many points on both sides.</p>
+ * <p>Note that with the hyperplane partitioning approach described above, the 
hyperplane
+ * at \(0\pi\) is unique in that it has the entire space on one side (except 
for the
+ * hyperplane itself) and no points whatsoever on the other. This is very 
different from
+ * hyperplanes in Euclidean space, which always have infinitely many points on 
both sides.</p>
+ *
+ * <p>Due to the unique status of the \(0\pi\) point, special care must be 
given to azimuths very
+ * close to this value. The rules below define exactly how points are 
classified when the hyperplane
+ * point, the test point, or both lie very close to \(0\pi\). In what follows, 
\(H\) represents the
+ * hyperplane point, \(P\) the point being classified, and the symbol 
\(\approx\) means equivalent as
+ * evaluated by the instance's {@link #getPrecision() precision context}. Note 
that points are considered
+ * equivalent to \(0\pi\) if their normalized azimuths are close to either 
\(0\pi\) or \(2\pi\).
+ * <ul>
+ *  <li>\(H \approx P\) \(\implies\) \(P\) is classified as {@link 
HyperplaneLocation#ON ON}.</li>
+ *  <li>\(H \approx 0\) and \(P \approx 0\) \(\implies\) \(P\) is classified as
+ *      {@link HyperplaneLocation#ON ON}.</li>
+ *  <li>\(H \approx 0\) and \(P \neq 0\) \(\implies\) \(P\) is classified as 
{@link HyperplaneLocation#PLUS PLUS}
+ *      if the cut is positive facing and {@link HyperplaneLocation#MINUS 
MINUS} if negative facing.</li>
+ *  <li>\(H \neq 0\) and \(P \approx 0\) \(\implies\) \(P\) is classified as 
{@link HyperplaneLocation#MINUS MINUS}
+ *      if the cut is positive facing and {@link HyperplaneLocation#PLUS PLUS} 
if negative facing.</li>
+ *  <li>\(H \neq 0\) and \(P \neq 0\) \(\implies\) The normalized azimuths of 
\(H\) and \(P\) are compared and the
+ *      standard rules applied. If \(P \gt H\), then \(P\) is classified as 
{@link HyperplaneLocation#PLUS PLUS}
+ *      if the cut is positive facing and {@link HyperplaneLocation#MINUS 
MINUS} if negative facing. If
+ *      \(P \lt H\), then \(P\) is classified as {@link 
HyperplaneLocation#MINUS MINUS} if the cut is
+ *      positive facing and {@link HyperplaneLocation#PLUS PLUS} if negative 
facing.</li>
+ * </ul>
  *
  * <p>Instances of this class are guaranteed to be immutable.</p>
  * @see CutAngles
@@ -140,25 +162,51 @@ public final class CutAngle extends 
AbstractHyperplane<Point1S> {
         return positiveFacing ? +dist : -dist;
     }
 
-    /** {@inheritDoc} */
+    /** {@inheritDoc}
+     *
+     * <p>Special classification rules are applied in this method due to the 
unique nature
+     * of spherical space. See the class level documentation for details.</p>
+     */
     @Override
     public HyperplaneLocation classify(final Point1S pt) {
+        int cmp = classifyPositiveFacing(pt);
+        if (!positiveFacing) {
+            cmp = -cmp;
+        }
+
+        if (cmp < 0) {
+            return HyperplaneLocation.MINUS;
+        } else if (cmp > 0) {
+            return HyperplaneLocation.PLUS;
+        }
+        return HyperplaneLocation.ON;
+    }
+
+    /** Classify the given point assuming a positive facing cut.
+     * @param pt point to classify
+     * @return int value indicating the classification region of {@code pt}
+     */
+    private int classifyPositiveFacing(final Point1S pt) {
         final Precision.DoubleEquivalence precision = getPrecision();
 
-        final Point1S compPt = Point1S.ZERO.eq(pt, precision) ?
-                Point1S.ZERO :
-                pt;
+        final double az = pt.getNormalizedAzimuth();
+        final double base = this.point.getNormalizedAzimuth();
 
-        final double offsetValue = offset(compPt);
-        final double cmp = precision.signum(offsetValue);
+        final int cmp = precision.compare(az, base);
 
-        if (cmp > 0) {
-            return HyperplaneLocation.PLUS;
-        } else if (cmp < 0) {
-            return HyperplaneLocation.MINUS;
-        }
+        if (cmp != 0) {
+            final boolean azIsZero = pt.eqZero(precision);
+            final boolean baseIsZero = this.point.eqZero(precision);
 
-        return HyperplaneLocation.ON;
+            if (baseIsZero) {
+                return azIsZero ?
+                        0 :
+                        +1;
+            } else if (azIsZero) {
+                return -1;
+            }
+        }
+        return cmp;
     }
 
     /** {@inheritDoc} */
diff --git 
a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/Point1S.java
 
b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/Point1S.java
index b5a1935..916d21b 100644
--- 
a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/Point1S.java
+++ 
b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/Point1S.java
@@ -210,6 +210,22 @@ public final class Point1S implements Point<Point1S> {
         return precision.eqZero(dist);
     }
 
+    /** Return true if this instance is equivalent to the {@link Point1S#ZERO 
zero point},
+     * meaning that the shortest angular distance of this point from the zero 
point is equal
+     * to zero as evaluated by the given precision context. This method is 
functionally equivalent
+     * to {@code pt.eq(Point1S.ZERO, precision)} but with simplified logic due 
to the comparison
+     * point being zero.
+     * @param precision precision context used for floating point comparisons
+     * @return true if this instance is equivalent to the zero point
+     * @see #eq(Point1S, 
org.apache.commons.numbers.core.Precision.DoubleEquivalence)
+     */
+    public boolean eqZero(final Precision.DoubleEquivalence precision) {
+        final double closest = normalizedAzimuth > Math.PI ?
+                Angle.TWO_PI :
+                0d;
+        return precision.eq(normalizedAzimuth, closest);
+    }
+
     /**
      * Get a hashCode for the point. Points normally must have exactly the
      * same azimuth angles in order to have the same hash code. Points
diff --git 
a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/AngularIntervalTest.java
 
b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/AngularIntervalTest.java
index 28361af..f245abd 100644
--- 
a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/AngularIntervalTest.java
+++ 
b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/AngularIntervalTest.java
@@ -49,6 +49,35 @@ class AngularIntervalTest {
     }
 
     @Test
+    void testOf_endPointsCloseToZero() {
+        // arrange
+        final double pi = Math.PI;
+
+        final double belowZero = -5e-11;
+        final double aboveZero = 5e-11;
+
+        final double belowTwoPi = Angle.TWO_PI - 5e-11;
+        final double aboveTwoPi = Angle.TWO_PI + 5e-11;
+
+        // act/assert
+        checkInterval(AngularInterval.of(belowZero, pi, TEST_PRECISION), 
belowZero, pi);
+        checkInterval(AngularInterval.of(aboveZero, pi, TEST_PRECISION), 
aboveZero, pi);
+
+        checkInterval(AngularInterval.of(belowTwoPi, pi, TEST_PRECISION), 
belowTwoPi, pi + Angle.TWO_PI);
+        checkInterval(AngularInterval.of(aboveTwoPi, pi, TEST_PRECISION), 
aboveTwoPi, pi + Angle.TWO_PI);
+
+        checkInterval(AngularInterval.of(pi, belowZero, TEST_PRECISION), pi, 
belowZero + Angle.TWO_PI);
+        checkInterval(AngularInterval.of(pi, aboveZero, TEST_PRECISION), pi, 
aboveZero + Angle.TWO_PI);
+
+        checkInterval(AngularInterval.of(pi, belowTwoPi, TEST_PRECISION), pi, 
belowTwoPi);
+        checkInterval(AngularInterval.of(pi, aboveTwoPi, TEST_PRECISION), pi, 
aboveTwoPi);
+
+        // from GEOMETRY-143
+        checkInterval(AngularInterval.of(6, 
Double.parseDouble("0x1.921fb54442c8ep2"), TEST_PRECISION),
+                6, Double.parseDouble("0x1.921fb54442c8ep2"));
+    }
+
+    @Test
     void testOf_doubles_invalidArgs() {
         // act/assert
         Assertions.assertThrows(IllegalArgumentException.class, () -> 
AngularInterval.of(Double.NEGATIVE_INFINITY, 0, TEST_PRECISION));
@@ -802,6 +831,8 @@ class AngularIntervalTest {
         Assertions.assertEquals(0, interval.getBoundarySize(), TEST_EPS);
         Assertions.assertEquals(max - min, interval.getSize(), TEST_EPS);
 
+        checkClassify(interval, RegionLocation.BOUNDARY, 
interval.getMinBoundary().getPoint());
+
         checkClassify(interval, RegionLocation.INSIDE, interval.getMidPoint());
         checkClassify(interval, RegionLocation.BOUNDARY,
                 interval.getMinBoundary().getPoint(), 
interval.getMaxBoundary().getPoint());
diff --git 
a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/CutAngleTest.java
 
b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/CutAngleTest.java
index 95a0ad2..48fc2fb 100644
--- 
a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/CutAngleTest.java
+++ 
b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/CutAngleTest.java
@@ -168,6 +168,29 @@ class CutAngleTest {
     }
 
     @Test
+    void testClassify_azimuthsCloseToZero() {
+        // arrange
+        final CutAngle belowZeroPos = CutAngles.createPositiveFacing(-5e-11, 
TEST_PRECISION);
+        final CutAngle belowZeroNeg = CutAngles.createNegativeFacing(-5e-11, 
TEST_PRECISION);
+
+        final CutAngle aboveZeroPos = CutAngles.createPositiveFacing(5e-11, 
TEST_PRECISION);
+        final CutAngle aboveZeroNeg = CutAngles.createNegativeFacing(5e-11, 
TEST_PRECISION);
+
+        // act/assert
+        checkClassify(belowZeroPos, HyperplaneLocation.PLUS, -1.6e-10, 
1.2e-10, 1.6e-10);
+        checkClassify(belowZeroPos, HyperplaneLocation.ON, -1.2e-10, -8e-11, 
-4e-11, 0, 4e-11, 8e-11);
+
+        checkClassify(belowZeroNeg, HyperplaneLocation.MINUS, -1.6e-10, 
1.2e-10, 1.6e-10);
+        checkClassify(belowZeroNeg, HyperplaneLocation.ON, -1.2e-10, -8e-11, 
-4e-11, 0, 4e-11, 8e-11);
+
+        checkClassify(aboveZeroPos, HyperplaneLocation.PLUS, -1.6e-10, 
-1.2e-10, 1.6e-10);
+        checkClassify(aboveZeroPos, HyperplaneLocation.ON, -8e-11, -4e-11, 0, 
4e-11, 8e-11, 1.2e-10);
+
+        checkClassify(aboveZeroNeg, HyperplaneLocation.MINUS, -1.6e-10, 
-1.2e-10, 1.6e-10);
+        checkClassify(aboveZeroNeg, HyperplaneLocation.ON, -8e-11, -4e-11, 0, 
4e-11, 8e-11, 1.2e-10);
+    }
+
+    @Test
     void testContains() {
         // arrange
         final CutAngle pt = CutAngles.createNegativeFacing(Angle.PI_OVER_TWO, 
TEST_PRECISION);
diff --git 
a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/Point1STest.java
 
b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/Point1STest.java
index 54609b7..7105019 100644
--- 
a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/Point1STest.java
+++ 
b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/Point1STest.java
@@ -267,6 +267,51 @@ class Point1STest {
     }
 
     @Test
+    void testEqZero() {
+        // arrange
+        final double eps = 1e-8;
+        final double delta = 1e-3 * eps;
+        final Precision.DoubleEquivalence precision = 
Precision.doubleEquivalenceOfEpsilon(eps);
+
+        // act/assert
+        checkEqZero(0, precision);
+        checkEqZero(Angle.TWO_PI, precision);
+        checkEqZero(-Angle.TWO_PI, precision);
+
+        checkNotEqZero(Math.PI, precision);
+        checkNotEqZero(-Math.PI, precision);
+
+        checkNotEqZero(Angle.PI_OVER_TWO, precision);
+        checkNotEqZero(-Angle.PI_OVER_TWO, precision);
+
+        checkNotEqZero(-eps - delta, precision);
+        checkNotEqZero(-eps - delta - Angle.TWO_PI, precision);
+
+        for (double a = -eps + delta; a < eps; a += delta) {
+            checkEqZero(a, precision);
+            checkEqZero(a - Angle.TWO_PI, precision);
+            checkEqZero(a + Angle.TWO_PI, precision);
+        }
+
+        checkNotEqZero(eps + delta, precision);
+        checkNotEqZero(eps + delta + Angle.TWO_PI, precision);
+    }
+
+    private void checkEqZero(final double az, final 
Precision.DoubleEquivalence precision) {
+        final Point1S pt = Point1S.of(az);
+
+        Assertions.assertTrue(pt.eqZero(precision));
+        Assertions.assertTrue(Point1S.ZERO.eq(pt, precision));
+    }
+
+    private void checkNotEqZero(final double az, final 
Precision.DoubleEquivalence precision) {
+        final Point1S pt = Point1S.of(az);
+
+        Assertions.assertFalse(pt.eqZero(precision));
+        Assertions.assertFalse(Point1S.ZERO.eq(pt, precision));
+    }
+
+    @Test
     void testDistance() {
         // arrange
         final Point1S a = Point1S.of(0.0);

Reply via email to