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.text.MessageFormat;
    20  import java.util.Arrays;
    21  import java.util.Collections;
    22  import java.util.List;
    23  import java.util.function.BiFunction;
    24
    25  import org.apache.commons.geometry.core.RegionLocation;
    26  import org.apache.commons.geometry.core.Transform;
    27  import org.apache.commons.geometry.core.partitioning.Hyperplane;
    28  import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
    29  import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
    30  import org.apache.commons.geometry.core.partitioning.Split;
    31  import org.apache.commons.numbers.angle.Angle;
    32  import org.apache.commons.numbers.core.Precision;
    33
    34  /** Class representing an angular interval of size greater than zero to {@code 2pi}. The interval is
    35   * defined by two azimuth angles: a min and a max. The interval starts at the min azimuth angle and
    36   * contains all points in the direction of increasing azimuth angles up to max.
    37   *
    38   * <p>Instances of this class are guaranteed to be immutable.</p>
    39   */
    40  public class AngularInterval implements HyperplaneBoundedRegion<Point1S> {
    41      /** The minimum boundary of the interval. */
    42      private final CutAngle minBoundary;
    43
    44      /** The maximum boundary of the interval. */
    45      private final CutAngle maxBoundary;
    46
    47      /** Point halfway between the min and max boundaries. */
    48      private final Point1S midpoint;
    49
    50      /** Construct a new instance representing the angular region between the given
    51       * min and max azimuth boundaries. The arguments must be either all finite or all
    52       * null (to indicate the full space). If the boundaries are finite, then the min
    53       * boundary azimuth value must be numerically less than the max boundary. Callers are
    54       * responsible for enforcing these constraints. No validation is performed.
    55       * @param minBoundary minimum boundary for the interval
    56       * @param maxBoundary maximum boundary for the interval
    57       */
    58      private AngularInterval(final CutAngle minBoundary, final CutAngle maxBoundary) {
    59
    60          this.minBoundary = minBoundary;
    61          this.maxBoundary = maxBoundary;
    62          this.midpoint = (minBoundary != null && maxBoundary != null) ?
    63                  Point1S.of(0.5 * (minBoundary.getAzimuth() + maxBoundary.getAzimuth())) :
    64                  null;
    65      }
    66
    67      /** Get the minimum azimuth angle for the interval, or {@code 0}
    68       * if the interval is full.
    69       * @return the minimum azimuth angle for the interval or {@code 0}
    70       *      if the interval represents the full space.
    71       */
    72      public double getMin() {
    73          return (minBoundary != null) ?
    74                  minBoundary.getAzimuth() :
    75                  0.0;
    76      }
    77
    78      /** Get the minimum boundary for the interval, or null if the
    79       * interval represents the full space.
    80       * @return the minimum point for the interval or null if
    81       *      the interval represents the full space
    82       */
    83      public CutAngle getMinBoundary() {
    84          return minBoundary;
    85      }
    86
    87      /** Get the maximum azimuth angle for the interval, or {@code 2pi} if
    88       * the interval represents the full space.
    89       * @return the maximum azimuth angle for the interval or {@code 2pi} if
    90       *      the interval represents the full space.
    91       */
    92      public double getMax() {
    93          return (maxBoundary != null) ?
    94                  maxBoundary.getAzimuth() :
    95                  Angle.TWO_PI;
    96      }
    97
    98      /** Get the maximum point for the interval. This will be null if the
    99       * interval represents the full space.
    100      * @return the maximum point for the interval or null if
    101      *      the interval represents the full space
    102      */
    103     public CutAngle getMaxBoundary() {
    104         return maxBoundary;
    105     }
    106
    107     /** Get the midpoint of the interval or null if the interval represents
    108      *  the full space.
    109      * @return the midpoint of the interval or null if the interval represents
    110      *      the full space
    111      * @see #getCentroid()
    112      */
    113     public Point1S getMidPoint() {
    114         return midpoint;
    115     }
    116
    117     /** {@inheritDoc} */
    118     @Override
    119     public boolean isFull() {
    120         // minBoundary and maxBoundary are either both null or both not null
    121         return minBoundary == null;
    122     }
    123
    124     /** {@inheritDoc}
    125      *
    126      * <p>This method always returns false.</p>
    127      */
    128     @Override
    129     public boolean isEmpty() {
    130         return false;
    131     }
    132
    133     /** {@inheritDoc} */
    134     @Override
    135     public double getSize() {
    136         return getMax() - getMin();
    137     }
    138
    139     /** {@inheritDoc}
    140      *
    141      * <p>This method simply returns 0 because boundaries in one dimension do not
    142      *  have any size.</p>
    143      */
    144     @Override
    145     public double getBoundarySize() {
    146         return 0;
    147     }
    148
    149     /** {@inheritDoc}
    150      *
    151      * <p>This method is an alias for {@link #getMidPoint()}.</p>
    152      * @see #getMidPoint()
    153      */
    154     @Override
    155     public Point1S getCentroid() {
    156         return getMidPoint();
    157     }
    158
    159     /** {@inheritDoc} */
    160     @Override
    161     public RegionLocation classify(final Point1S pt) {
    162         if (!isFull()) {
    163             final HyperplaneLocation minLoc = minBoundary.classify(pt);
    164             final HyperplaneLocation maxLoc = maxBoundary.classify(pt);
    165
    166             final boolean wraps = wrapsZero();
    167
    168             if ((!wraps && (minLoc == HyperplaneLocation.PLUS || maxLoc == HyperplaneLocation.PLUS)) ||
    169                     (wraps && minLoc == HyperplaneLocation.PLUS && maxLoc == HyperplaneLocation.PLUS)) {
    170                 return RegionLocation.OUTSIDE;
    171             } else if (minLoc == HyperplaneLocation.ON || maxLoc == HyperplaneLocation.ON) {
    172                 return RegionLocation.BOUNDARY;
    173             }
    174         }
    175         return RegionLocation.INSIDE;
    176     }
    177
    178     /** {@inheritDoc} */
    179     @Override
    180     public Point1S project(final Point1S pt) {
    181         if (!isFull()) {
    182             final double minDist = minBoundary.getPoint().distance(pt);
    183             final double maxDist = maxBoundary.getPoint().distance(pt);
    184
    185             return (minDist <= maxDist) ?
    186                     minBoundary.getPoint() :
    187                     maxBoundary.getPoint();
    188         }
    189         return null;
    190     }
    191
    192     /** Return true if the interval wraps around the zero/{@code 2pi} point. In this
    193      * case, the max boundary azimuth is less than that of the min boundary when both
    194      * values are normalized to the range {@code [0, 2pi)}.
    195      * @return true if the interval wraps around the zero/{@code 2pi} point
    196      */
    197     public boolean wrapsZero() {
    198         if (!isFull()) {
    199             final double minNormAz = minBoundary.getPoint().getNormalizedAzimuth();
    200             final double maxNormAz = maxBoundary.getPoint().getNormalizedAzimuth();
    201
    202             return maxNormAz < minNormAz;
    203         }
    204         return false;
    205     }
    206
    207     /** Return a new instance transformed by the argument. If the transformed size
    208      * of the interval is greater than or equal to 2pi, then an interval representing
    209      * the full space is returned.
    210      * @param transform transform to apply
    211      * @return a new instance transformed by the argument
    212      */
    213     public AngularInterval transform(final Transform<Point1S> transform) {
    214         return AngularInterval.transform(this, transform, AngularInterval::of);
    215     }
    216
    217     /** {@inheritDoc}
    218     *
    219     * <p>This method returns instances of {@link RegionBSPTree1S} instead of
    220     * {@link AngularInterval} since it is possible for a convex angular interval
    221     * to be split into disjoint regions by a single hyperplane. These disjoint
    222     * regions cannot be represented by this class and require the use of a BSP
    223     * tree.</p>
    224     *
    225     * @see RegionBSPTree1S#split(Hyperplane)
    226     */
    227     @Override
    228     public Split<RegionBSPTree1S> split(final Hyperplane<Point1S> splitter) {
    229         return toTree().split(splitter);
    230     }
    231
    232     /** Return a {@link RegionBSPTree1S} instance representing the same region
    233      * as this instance.
    234      * @return a BSP tree representing the same region as this instance
    235      */
    236     public RegionBSPTree1S toTree() {
    237         return RegionBSPTree1S.fromInterval(this);
    238     }
    239
    240     /** Return a list of convex intervals comprising this region.
    241      * @return a list of convex intervals comprising this region
    242      * @see Convex
    243      */
    244     public List<AngularInterval.Convex> toConvex() {
    245         if (isConvex(minBoundary, maxBoundary)) {
    246             return Collections.singletonList(new Convex(minBoundary, maxBoundary));
    247         }
    248
    249         final CutAngle midPos = CutAngles.createPositiveFacing(midpoint, minBoundary.getPrecision());
    250         final CutAngle midNeg = CutAngles.createNegativeFacing(midpoint, maxBoundary.getPrecision());
    251
    252         return Arrays.asList(
    253                     new Convex(minBoundary, midPos),
    254                     new Convex(midNeg, maxBoundary)
    255                 );
    256     }
    257
    258     /** {@inheritDoc} */
    259     @Override
    260     public String toString() {
    261         final StringBuilder sb = new StringBuilder();
    262         sb.append(this.getClass().getSimpleName())
    263             .append("[min= ")
    264             .append(getMin())
    265             .append(", max= ")
    266             .append(getMax())
    267             .append(']');
    268
    269         return sb.toString();
    270     }
    271
    272     /** Return an instance representing the full space. The returned instance contains all
    273      * possible azimuth angles.
    274      * @return an interval representing the full space
    275      */
    276     public static AngularInterval.Convex full() {
    277         return Convex.FULL;
    278     }
    279
    280     /** Return an instance representing the angular interval between the given min and max azimuth
    281      * values. The max value is adjusted to be numerically above the min value, even if the resulting
    282      * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
    283      * is returned if either point is infinite or min and max are equivalent as evaluated by the
    284      * given precision context.
    285      * @param min min azimuth value
    286      * @param max max azimuth value
    287      * @param precision precision precision context used to compare floating point values
    288      * @return a new instance resulting the angular region between the given min and max azimuths
    289      * @throws IllegalArgumentException if either azimuth is infinite or NaN
    290      */
    291     public static AngularInterval of(final double min, final double max,
    292             final Precision.DoubleEquivalence precision) {
    293         return of(Point1S.of(min), Point1S.of(max), precision);
    294     }
    295
    296     /** Return an instance representing the angular interval between the given min and max azimuth
    297      * points. The max point is adjusted to be numerically above the min point, even if the resulting
    298      * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
    299      * is returned if either point is infinite or min and max are equivalent as evaluated by the
    300      * given precision context.
    301      * @param min min azimuth value
    302      * @param max max azimuth value
    303      * @param precision precision precision context used to compare floating point values
    304      * @return a new instance resulting the angular region between the given min and max points
    305      * @throws IllegalArgumentException if either azimuth is infinite or NaN
    306      */
    307     public static AngularInterval of(final Point1S min, final Point1S max,
    308             final Precision.DoubleEquivalence precision) {
    309         return createInterval(min, max, precision, AngularInterval::new, Convex.FULL);
    310     }
    311
    312     /** Return an instance representing the angular interval between the given oriented points.
    313      * The negative-facing point is used as the minimum boundary and the positive-facing point is
    314      * adjusted to be above the minimum. The arguments can be given in any order. The full space
    315      * is returned if the points are equivalent or are oriented in the same direction.
    316      * @param a first oriented point
    317      * @param b second oriented point
    318      * @return an instance representing the angular interval between the given oriented points
    319      * @throws IllegalArgumentException if either argument is infinite or NaN
    320      */
    321     public static AngularInterval of(final CutAngle a, final CutAngle b) {
    322         return createInterval(a, b, AngularInterval::new, Convex.FULL);
    323     }
    324
    325     /** Internal method to create an interval between the given min and max points. The max point
    326      * is adjusted to be numerically above the min point, even if the resulting
    327      * azimuth value is greater than or equal to {@code 2pi}. The full instance argument
    328      * is returned if either point is infinite or min and max are equivalent as evaluated by the
    329      * given precision context.
    330      * @param min min azimuth value
    331      * @param max max azimuth value
    332      * @param precision precision precision context used to compare floating point values
    333      * @param factory factory object used to create new instances; this object is passed the validated
    334      *      min (negative-facing) cut and the max (positive-facing) cut, in that order
    335      * @param <T> Angular interval implementation type
    336      * @param fullSpace instance returned if the interval should represent the full space
    337      * @return a new instance resulting the angular region between the given min and max points
    338      * @throws IllegalArgumentException if either azimuth is infinite or NaN
    339      */
    340     private static <T extends AngularInterval> T createInterval(final Point1S min, final Point1S max,
    341             final Precision.DoubleEquivalence precision,
    342             final BiFunction<? super CutAngle, ? super CutAngle, T> factory, final T fullSpace) {
    343
    344         validateIntervalValues(min, max);
    345
    346         // return the full space if either point is infinite or the points are equivalent
    347         if (min.eq(max, precision)) {
    348             return fullSpace;
    349         }
    350
    351         final Point1S adjustedMax = max.above(min);
    352
    353         return factory.apply(
    354                     CutAngles.createNegativeFacing(min, precision),
    355                     CutAngles.createPositiveFacing(adjustedMax, precision)
    356                 );
    357     }
    358
    359     /** Internal method to create a new interval instance from the given cut angles.
    360      * The negative-facing point is used as the minimum boundary and the positive-facing point is
    361      * adjusted to be above the minimum. The arguments can be given in any order. The full space
    362      * argument is returned if the points are equivalent or are oriented in the same direction.
    363      * @param a first cut point
    364      * @param b second cut point
    365      * @param factory factory object used to create new instances; this object is passed the validated
    366      *      min (negative-facing) cut and the max (positive-facing) cut, in that order
    367      * @param fullSpace instance returned if the interval should represent the full space
    368      * @param <T> Angular interval implementation type
    369      * @return a new interval instance created from the given cut angles
    370      * @throws IllegalArgumentException if either argument is infinite or NaN
    371      */
    372     private static <T extends AngularInterval> T createInterval(final CutAngle a, final CutAngle b,
    373             final BiFunction<? super CutAngle, ? super CutAngle, T> factory, final T fullSpace) {
    374
    375         final Point1S aPoint = a.getPoint();
    376         final Point1S bPoint = b.getPoint();
    377
    378         validateIntervalValues(aPoint, bPoint);
    379
    380         if (a.isPositiveFacing() == b.isPositiveFacing() ||
    381                 aPoint.eq(bPoint, a.getPrecision()) ||
    382                 bPoint.eq(aPoint, b.getPrecision())) {
    383             // points are equivalent or facing in the same direction
    384             return fullSpace;
    385         }
    386
    387         final CutAngle min = a.isPositiveFacing() ? b : a;
    388         final CutAngle max = a.isPositiveFacing() ? a : b;
    389         final CutAngle adjustedMax = CutAngles.createPositiveFacing(
    390                 max.getPoint().above(min.getPoint()),
    391                 max.getPrecision());
    392
    393         return factory.apply(min, adjustedMax);
    394     }
    395
    396     /** Validate that the given points can be used to specify an angular interval.
    397      * @param a first point
    398      * @param b second point
    399      * @throws IllegalArgumentException if either point is infinite NaN
    400      */
    401     private static void validateIntervalValues(final Point1S a, final Point1S b) {
    402         if (!a.isFinite() || !b.isFinite()) {
    403             throw new IllegalArgumentException(MessageFormat.format("Invalid angular interval: [{0}, {1}]",
    404                     a.getAzimuth(), b.getAzimuth()));
    405         }
    406     }
    407
    408     /** Return true if the given cut angles define a convex region. By convention, the
    409      * precision context from the min cut is used for the floating point comparison.
    410      * @param min min (negative-facing) cut angle
    411      * @param max max (positive-facing) cut angle
    412      * @return true if the given cut angles define a convex region
    413      */
    414     private static boolean isConvex(final CutAngle min, final CutAngle max) {
    415         if (min != null && max != null) {
    416             final double dist = max.getAzimuth() - min.getAzimuth();
    417             final Precision.DoubleEquivalence precision = min.getPrecision();
    418             return precision.lte(dist, Math.PI);
    419         }
    420
    421         return true;
    422     }
    423
    424     /** Internal transform method that transforms the given instance, using the factory
    425      * method to create a new instance if needed.
    426      * @param interval interval to transform
    427      * @param transform transform to apply
    428      * @param factory object used to create new instances
    429      * @param <T> Angular interval implementation type
    430      * @return a transformed instance
    431      */
    432     private static <T extends AngularInterval> T transform(final T interval,
    433             final Transform<Point1S> transform,
    434             final BiFunction<? super CutAngle, ? super CutAngle, T> factory) {
    435
    436         if (!interval.isFull()) {
    437             final CutAngle tMin = interval.getMinBoundary().transform(transform);
    438             final CutAngle tMax = interval.getMaxBoundary().transform(transform);
    439
    440             return factory.apply(tMin, tMax);
    441         }
    442
    443         return interval;
    444     }
    445
    446     /** Class representing an angular interval with the additional property that the
    447      * region is convex. By convex, it is meant that the shortest path between any
    448      * two points in the region is also contained entirely in the region. If there is
    449      * a tie for shortest path, then it is sufficient that at least one lie entirely
    450      * within the region. For spherical 1D space, this means that the angular interval
    451      * is either completely full or has a length less than or equal to {@code pi}.
    452      */
    453     public static final class Convex extends AngularInterval {
    454         /** Interval instance representing the full space. */
    455         private static final Convex FULL = new Convex(null, null);
    456
    457         /** Construct a new convex instance from its boundaries and midpoint. No validation
    458          * of the argument is performed. Callers are responsible for ensuring that the size
    459          * of interval is less than or equal to {@code pi}.
    460          * @param minBoundary minimum boundary for the interval
    461          * @param maxBoundary maximum boundary for the interval
    462          * @throws IllegalArgumentException if the interval is not convex
    463          */
    464         private Convex(final CutAngle minBoundary, final CutAngle maxBoundary) {
    465             super(minBoundary, maxBoundary);
    466
    467             if (!isConvex(minBoundary, maxBoundary)) {
    468                 throw new IllegalArgumentException(MessageFormat.format("Interval is not convex: [{0}, {1}]",
    469                         minBoundary.getAzimuth(), maxBoundary.getAzimuth()));
    470             }
    471         }
    472
    473         /** {@inheritDoc} */
    474         @Override
    475         public List<AngularInterval.Convex> toConvex() {
    476             return Collections.singletonList(this);
    477         }
    478
    479         /** {@inheritDoc} */
    480         @Override
    481         public Convex transform(final Transform<Point1S> transform) {
    482             return AngularInterval.transform(this, transform, Convex::of);
    483         }
    484
    485         /** Split the instance along a circle diameter.The diameter is defined by the given split point and
    486          * its reversed antipodal point.
    487          * @param splitter split point defining one side of the split diameter
    488          * @return result of the split operation
    489          */
    490         public Split<Convex> splitDiameter(final CutAngle splitter) {
    491
    492             final CutAngle opposite = CutAngles.fromPointAndDirection(
    493                     splitter.getPoint().antipodal(),
    494                     !splitter.isPositiveFacing(),
    495                     splitter.getPrecision());
    496
    497             if (isFull()) {
    498                 final Convex minus = Convex.of(splitter, opposite);
    499                 final Convex plus = Convex.of(splitter.reverse(), opposite.reverse());
    500
    501                 return new Split<>(minus, plus);
    502             }
    503
    504             final CutAngle minBoundary = getMinBoundary();
    505             final CutAngle maxBoundary = getMaxBoundary();
    506
    507             final Point1S posPole = Point1S.of(splitter.getPoint().getAzimuth() + Angle.PI_OVER_TWO);
    508
    509             final int minLoc = minBoundary.getPrecision().compare(Angle.PI_OVER_TWO,
    510                     posPole.distance(minBoundary.getPoint()));
    511             final int maxLoc = maxBoundary.getPrecision().compare(Angle.PI_OVER_TWO,
    512                     posPole.distance(maxBoundary.getPoint()));
    513
    514             final boolean positiveFacingSplit = splitter.isPositiveFacing();
    515
    516             // assume a positive orientation of the splitter for region location
    517             // purposes and adjust later
    518             Convex pos = null;
    519             Convex neg = null;
    520
    521             if (minLoc > 0) {
    522                 // min is on the pos side
    523
    524                 if (maxLoc >= 0) {
    525                     // max is directly on the splitter or on the pos side
    526                     pos = this;
    527                 } else {
    528                     // min is on the pos side and max is on the neg side
    529                     final CutAngle posCut = positiveFacingSplit ?
    530                             opposite.reverse() :
    531                             opposite;
    532                     pos = Convex.of(minBoundary, posCut);
    533
    534                     final CutAngle negCut = positiveFacingSplit ?
    535                             opposite :
    536                             opposite.reverse();
    537                     neg = Convex.of(negCut, maxBoundary);
    538                 }
    539             } else if (minLoc < 0) {
    540                 // min is on the neg side
    541
    542                 if (maxLoc <= 0) {
    543                     // max is directly on the splitter or on the neg side
    544                     neg = this;
    545                 } else {
    546                     // min is on the neg side and max is on the pos side
    547                     final CutAngle posCut = positiveFacingSplit ?
    548                             splitter.reverse() :
    549                             splitter;
    550                     pos = Convex.of(maxBoundary, posCut);
    551
    552                     final CutAngle negCut = positiveFacingSplit ?
    553                             splitter :
    554                             splitter.reverse();
    555                     neg = Convex.of(negCut, minBoundary);
    556                 }
    557             } else {
    558                 // min is directly on the splitter; determine whether it was on the main split
    559                 // point or its antipodal point
    560                 if (splitter.getPoint().distance(minBoundary.getPoint()) < Angle.PI_OVER_TWO) {
    561                     // on main splitter; interval will be located on pos side of split
    562                     pos = this;
    563                 } else {
    564                     // on antipodal point; interval will be located on neg side of split
    565                     neg = this;
    566                 }
    567             }
    568
    569             // adjust for the actual orientation of the splitter
    570             final Convex minus = positiveFacingSplit ? neg : pos;
    571             final Convex plus = positiveFacingSplit ? pos : neg;
    572
    573             return new Split<>(minus, plus);
    574         }
    575
    576         /** Return an instance representing the convex angular interval between the given min and max azimuth
    577          * values. The max value is adjusted to be numerically above the min value, even if the resulting
    578          * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
    579          * is returned if either point is infinite or min and max are equivalent as evaluated by the
    580          * given precision context.
    581          * @param min min azimuth value
    582          * @param max max azimuth value
    583          * @param precision precision precision context used to compare floating point values
    584          * @return a new instance resulting the angular region between the given min and max azimuths
    585          * @throws IllegalArgumentException if either azimuth is infinite or NaN, or the given angular
    586          *      interval is not convex (meaning it has a size of greater than {@code pi})
    587          */
    588         public static Convex of(final double min, final double max, final Precision.DoubleEquivalence precision) {
    589             return of(Point1S.of(min), Point1S.of(max), precision);
    590         }
    591
    592         /** Return an instance representing the convex angular interval between the given min and max azimuth
    593          * points. The max point is adjusted to be numerically above the min point, even if the resulting
    594          * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
    595          * is returned if either point is infinite or min and max are equivalent as evaluated by the
    596          * given precision context.
    597          * @param min min azimuth value
    598          * @param max max azimuth value
    599          * @param precision precision precision context used to compare floating point values
    600          * @return a new instance resulting the angular region between the given min and max points
    601          * @throws IllegalArgumentException if either azimuth is infinite or NaN, or the given angular
    602          *      interval is not convex (meaning it has a size of greater than {@code pi})
    603          */
    604         public static Convex of(final Point1S min, final Point1S max, final Precision.DoubleEquivalence precision) {
    605             return createInterval(min, max, precision, Convex::new, Convex.FULL);
    606         }
    607
    608         /** Return an instance representing the convex angular interval between the given oriented points.
    609          * The negative-facing point is used as the minimum boundary and the positive-facing point is
    610          * adjusted to be above the minimum. The arguments can be given in any order. The full space
    611          * is returned if the points are equivalent or are oriented in the same direction.
    612          * @param a first oriented point
    613          * @param b second oriented point
    614          * @return an instance representing the angular interval between the given oriented points
    615          * @throws IllegalArgumentException if either azimuth is infinite or NaN, or the given angular
    616          *      interval is not convex (meaning it has a size of greater than {@code pi})
    617          */
    618         public static Convex of(final CutAngle a, final CutAngle b) {
    619             return createInterval(a, b, Convex::new, Convex.FULL);
    620         }
    621     }
    622 }