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.geometry.core.precision.DoublePrecisionContext;
    32  import org.apache.commons.numbers.angle.PlaneAngleRadians;
    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                  PlaneAngleRadians.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, final DoublePrecisionContext precision) {
    292         return of(Point1S.of(min), Point1S.of(max), precision);
    293     }
    294
    295     /** Return an instance representing the angular interval between the given min and max azimuth
    296      * points. The max point is adjusted to be numerically above the min point, even if the resulting
    297      * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
    298      * is returned if either point is infinite or min and max are equivalent as evaluated by the
    299      * given precision context.
    300      * @param min min azimuth value
    301      * @param max max azimuth value
    302      * @param precision precision precision context used to compare floating point values
    303      * @return a new instance resulting the angular region between the given min and max points
    304      * @throws IllegalArgumentException if either azimuth is infinite or NaN
    305      */
    306     public static AngularInterval of(final Point1S min, final Point1S max, final DoublePrecisionContext precision) {
    307         return createInterval(min, max, precision, AngularInterval::new, Convex.FULL);
    308     }
    309
    310     /** Return an instance representing the angular interval between the given oriented points.
    311      * The negative-facing point is used as the minimum boundary and the positive-facing point is
    312      * adjusted to be above the minimum. The arguments can be given in any order. The full space
    313      * is returned if the points are equivalent or are oriented in the same direction.
    314      * @param a first oriented point
    315      * @param b second oriented point
    316      * @return an instance representing the angular interval between the given oriented points
    317      * @throws IllegalArgumentException if either argument is infinite or NaN
    318      */
    319     public static AngularInterval of(final CutAngle a, final CutAngle b) {
    320         return createInterval(a, b, AngularInterval::new, Convex.FULL);
    321     }
    322
    323     /** Internal method to create an interval between the given min and max points. The max point
    324      * is adjusted to be numerically above the min point, even if the resulting
    325      * azimuth value is greater than or equal to {@code 2pi}. The full instance argument
    326      * is returned if either point is infinite or min and max are equivalent as evaluated by the
    327      * given precision context.
    328      * @param min min azimuth value
    329      * @param max max azimuth value
    330      * @param precision precision precision context used to compare floating point values
    331      * @param factory factory object used to create new instances; this object is passed the validated
    332      *      min (negative-facing) cut and the max (positive-facing) cut, in that order
    333      * @param <T> Angular interval implementation type
    334      * @param fullSpace instance returned if the interval should represent the full space
    335      * @return a new instance resulting the angular region between the given min and max points
    336      * @throws IllegalArgumentException if either azimuth is infinite or NaN
    337      */
    338     private static <T extends AngularInterval> T createInterval(final Point1S min, final Point1S max,
    339             final DoublePrecisionContext precision, final BiFunction<CutAngle, CutAngle, T> factory,
    340             final T fullSpace) {
    341
    342         validateIntervalValues(min, max);
    343
    344         // return the full space if either point is infinite or the points are equivalent
    345         if (min.eq(max, precision)) {
    346             return fullSpace;
    347         }
    348
    349         final Point1S adjustedMax = max.above(min);
    350
    351         return factory.apply(
    352                     CutAngles.createNegativeFacing(min, precision),
    353                     CutAngles.createPositiveFacing(adjustedMax, precision)
    354                 );
    355     }
    356
    357     /** Internal method to create a new interval instance from the given cut angles.
    358      * The negative-facing point is used as the minimum boundary and the positive-facing point is
    359      * adjusted to be above the minimum. The arguments can be given in any order. The full space
    360      * argument is returned if the points are equivalent or are oriented in the same direction.
    361      * @param a first cut point
    362      * @param b second cut point
    363      * @param factory factory object used to create new instances; this object is passed the validated
    364      *      min (negative-facing) cut and the max (positive-facing) cut, in that order
    365      * @param fullSpace instance returned if the interval should represent the full space
    366      * @param <T> Angular interval implementation type
    367      * @return a new interval instance created from the given cut angles
    368      * @throws IllegalArgumentException if either argument is infinite or NaN
    369      */
    370     private static <T extends AngularInterval> T createInterval(final CutAngle a, final CutAngle b,
    371             final BiFunction<CutAngle, CutAngle, T> factory, final T fullSpace) {
    372
    373         final Point1S aPoint = a.getPoint();
    374         final Point1S bPoint = b.getPoint();
    375
    376         validateIntervalValues(aPoint, bPoint);
    377
    378         if (a.isPositiveFacing() == b.isPositiveFacing() ||
    379                 aPoint.eq(bPoint, a.getPrecision()) ||
    380                 bPoint.eq(aPoint, b.getPrecision())) {
    381             // points are equivalent or facing in the same direction
    382             return fullSpace;
    383         }
    384
    385         final CutAngle min = a.isPositiveFacing() ? b : a;
    386         final CutAngle max = a.isPositiveFacing() ? a : b;
    387         final CutAngle adjustedMax = CutAngles.createPositiveFacing(
    388                 max.getPoint().above(min.getPoint()),
    389                 max.getPrecision());
    390
    391         return factory.apply(min, adjustedMax);
    392     }
    393
    394     /** Validate that the given points can be used to specify an angular interval.
    395      * @param a first point
    396      * @param b second point
    397      * @throws IllegalArgumentException if either point is infinite NaN
    398      */
    399     private static void validateIntervalValues(final Point1S a, final Point1S b) {
    400         if (!a.isFinite() || !b.isFinite()) {
    401             throw new IllegalArgumentException(MessageFormat.format("Invalid angular interval: [{0}, {1}]",
    402                     a.getAzimuth(), b.getAzimuth()));
    403         }
    404     }
    405
    406     /** Return true if the given cut angles define a convex region. By convention, the
    407      * precision context from the min cut is used for the floating point comparison.
    408      * @param min min (negative-facing) cut angle
    409      * @param max max (positive-facing) cut angle
    410      * @return true if the given cut angles define a convex region
    411      */
    412     private static boolean isConvex(final CutAngle min, final CutAngle max) {
    413         if (min != null && max != null) {
    414             final double dist = max.getAzimuth() - min.getAzimuth();
    415             final DoublePrecisionContext precision = min.getPrecision();
    416             return precision.lte(dist, PlaneAngleRadians.PI);
    417         }
    418
    419         return true;
    420     }
    421
    422     /** Internal transform method that transforms the given instance, using the factory
    423      * method to create a new instance if needed.
    424      * @param interval interval to transform
    425      * @param transform transform to apply
    426      * @param factory object used to create new instances
    427      * @param <T> Angular interval implementation type
    428      * @return a transformed instance
    429      */
    430     private static <T extends AngularInterval> T transform(final T interval,
    431             final Transform<Point1S> transform,
    432             final BiFunction<CutAngle, CutAngle, T> factory) {
    433
    434         if (!interval.isFull()) {
    435             final CutAngle tMin = interval.getMinBoundary().transform(transform);
    436             final CutAngle tMax = interval.getMaxBoundary().transform(transform);
    437
    438             return factory.apply(tMin, tMax);
    439         }
    440
    441         return interval;
    442     }
    443
    444     /** Class representing an angular interval with the additional property that the
    445      * region is convex. By convex, it is meant that the shortest path between any
    446      * two points in the region is also contained entirely in the region. If there is
    447      * a tie for shortest path, then it is sufficient that at least one lie entirely
    448      * within the region. For spherical 1D space, this means that the angular interval
    449      * is either completely full or has a length less than or equal to {@code pi}.
    450      */
    451     public static final class Convex extends AngularInterval {
    452         /** Interval instance representing the full space. */
    453         private static final Convex FULL = new Convex(null, null);
    454
    455         /** Construct a new convex instance from its boundaries and midpoint. No validation
    456          * of the argument is performed. Callers are responsible for ensuring that the size
    457          * of interval is less than or equal to {@code pi}.
    458          * @param minBoundary minimum boundary for the interval
    459          * @param maxBoundary maximum boundary for the interval
    460          * @throws IllegalArgumentException if the interval is not convex
    461          */
    462         private Convex(final CutAngle minBoundary, final CutAngle maxBoundary) {
    463             super(minBoundary, maxBoundary);
    464
    465             if (!isConvex(minBoundary, maxBoundary)) {
    466                 throw new IllegalArgumentException(MessageFormat.format("Interval is not convex: [{0}, {1}]",
    467                         minBoundary.getAzimuth(), maxBoundary.getAzimuth()));
    468             }
    469         }
    470
    471         /** {@inheritDoc} */
    472         @Override
    473         public List<AngularInterval.Convex> toConvex() {
    474             return Collections.singletonList(this);
    475         }
    476
    477         /** {@inheritDoc} */
    478         @Override
    479         public Convex transform(final Transform<Point1S> transform) {
    480             return AngularInterval.transform(this, transform, Convex::of);
    481         }
    482
    483         /** Split the instance along a circle diameter.The diameter is defined by the given split point and
    484          * its reversed antipodal point.
    485          * @param splitter split point defining one side of the split diameter
    486          * @return result of the split operation
    487          */
    488         public Split<Convex> splitDiameter(final CutAngle splitter) {
    489
    490             final CutAngle opposite = CutAngles.fromPointAndDirection(
    491                     splitter.getPoint().antipodal(),
    492                     !splitter.isPositiveFacing(),
    493                     splitter.getPrecision());
    494
    495             if (isFull()) {
    496                 final Convex minus = Convex.of(splitter, opposite);
    497                 final Convex plus = Convex.of(splitter.reverse(), opposite.reverse());
    498
    499                 return new Split<>(minus, plus);
    500             }
    501
    502             final CutAngle minBoundary = getMinBoundary();
    503             final CutAngle maxBoundary = getMaxBoundary();
    504
    505             final Point1S posPole = Point1S.of(splitter.getPoint().getAzimuth() + PlaneAngleRadians.PI_OVER_TWO);
    506
    507             final int minLoc = minBoundary.getPrecision().compare(PlaneAngleRadians.PI_OVER_TWO,
    508                     posPole.distance(minBoundary.getPoint()));
    509             final int maxLoc = maxBoundary.getPrecision().compare(PlaneAngleRadians.PI_OVER_TWO,
    510                     posPole.distance(maxBoundary.getPoint()));
    511
    512             final boolean positiveFacingSplit = splitter.isPositiveFacing();
    513
    514             // assume a positive orientation of the splitter for region location
    515             // purposes and adjust later
    516             Convex pos = null;
    517             Convex neg = null;
    518
    519             if (minLoc > 0) {
    520                 // min is on the pos side
    521
    522                 if (maxLoc >= 0) {
    523                     // max is directly on the splitter or on the pos side
    524                     pos = this;
    525                 } else {
    526                     // min is on the pos side and max is on the neg side
    527                     final CutAngle posCut = positiveFacingSplit ?
    528                             opposite.reverse() :
    529                             opposite;
    530                     pos = Convex.of(minBoundary, posCut);
    531
    532                     final CutAngle negCut = positiveFacingSplit ?
    533                             opposite :
    534                             opposite.reverse();
    535                     neg = Convex.of(negCut, maxBoundary);
    536                 }
    537             } else if (minLoc < 0) {
    538                 // min is on the neg side
    539
    540                 if (maxLoc <= 0) {
    541                     // max is directly on the splitter or on the neg side
    542                     neg = this;
    543                 } else {
    544                     // min is on the neg side and max is on the pos side
    545                     final CutAngle posCut = positiveFacingSplit ?
    546                             splitter.reverse() :
    547                             splitter;
    548                     pos = Convex.of(maxBoundary, posCut);
    549
    550                     final CutAngle negCut = positiveFacingSplit ?
    551                             splitter :
    552                             splitter.reverse();
    553                     neg = Convex.of(negCut, minBoundary);
    554                 }
    555             } else {
    556                 // min is directly on the splitter; determine whether it was on the main split
    557                 // point or its antipodal point
    558                 if (splitter.getPoint().distance(minBoundary.getPoint()) < PlaneAngleRadians.PI_OVER_TWO) {
    559                     // on main splitter; interval will be located on pos side of split
    560                     pos = this;
    561                 } else {
    562                     // on antipodal point; interval will be located on neg side of split
    563                     neg = this;
    564                 }
    565             }
    566
    567             // adjust for the actual orientation of the splitter
    568             final Convex minus = positiveFacingSplit ? neg : pos;
    569             final Convex plus = positiveFacingSplit ? pos : neg;
    570
    571             return new Split<>(minus, plus);
    572         }
    573
    574         /** Return an instance representing the convex angular interval between the given min and max azimuth
    575          * values. The max value is adjusted to be numerically above the min value, even if the resulting
    576          * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
    577          * is returned if either point is infinite or min and max are equivalent as evaluated by the
    578          * given precision context.
    579          * @param min min azimuth value
    580          * @param max max azimuth value
    581          * @param precision precision precision context used to compare floating point values
    582          * @return a new instance resulting the angular region between the given min and max azimuths
    583          * @throws IllegalArgumentException if either azimuth is infinite or NaN, or the given angular
    584          *      interval is not convex (meaning it has a size of greater than {@code pi})
    585          */
    586         public static Convex of(final double min, final double max, final DoublePrecisionContext precision) {
    587             return of(Point1S.of(min), Point1S.of(max), precision);
    588         }
    589
    590         /** Return an instance representing the convex angular interval between the given min and max azimuth
    591          * points. The max point is adjusted to be numerically above the min point, even if the resulting
    592          * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
    593          * is returned if either point is infinite or min and max are equivalent as evaluated by the
    594          * given precision context.
    595          * @param min min azimuth value
    596          * @param max max azimuth value
    597          * @param precision precision precision context used to compare floating point values
    598          * @return a new instance resulting the angular region between the given min and max points
    599          * @throws IllegalArgumentException if either azimuth is infinite or NaN, or the given angular
    600          *      interval is not convex (meaning it has a size of greater than {@code pi})
    601          */
    602         public static Convex of(final Point1S min, final Point1S max, final DoublePrecisionContext precision) {
    603             return createInterval(min, max, precision, Convex::new, Convex.FULL);
    604         }
    605
    606         /** Return an instance representing the convex angular interval between the given oriented points.
    607          * The negative-facing point is used as the minimum boundary and the positive-facing point is
    608          * adjusted to be above the minimum. The arguments can be given in any order. The full space
    609          * is returned if the points are equivalent or are oriented in the same direction.
    610          * @param a first oriented point
    611          * @param b second oriented point
    612          * @return an instance representing the angular interval between the given oriented points
    613          * @throws IllegalArgumentException if either azimuth is infinite or NaN, or the given angular
    614          *      interval is not convex (meaning it has a size of greater than {@code pi})
    615          */
    616         public static Convex of(final CutAngle a, final CutAngle b) {
    617             return createInterval(a, b, Convex::new, Convex.FULL);
    618         }
    619     }
    620 }