AngularInterval.java

  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. import java.text.MessageFormat;
  19. import java.util.Arrays;
  20. import java.util.Collections;
  21. import java.util.List;
  22. import java.util.function.BiFunction;

  23. import org.apache.commons.geometry.core.RegionLocation;
  24. import org.apache.commons.geometry.core.Transform;
  25. import org.apache.commons.geometry.core.partitioning.Hyperplane;
  26. import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
  27. import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
  28. import org.apache.commons.geometry.core.partitioning.Split;
  29. import org.apache.commons.numbers.angle.Angle;
  30. import org.apache.commons.numbers.core.Precision;

  31. /** Class representing an angular interval of size greater than zero to {@code 2pi}. The interval is
  32.  * defined by two azimuth angles: a min and a max. The interval starts at the min azimuth angle and
  33.  * contains all points in the direction of increasing azimuth angles up to max.
  34.  *
  35.  * <p>Instances of this class are guaranteed to be immutable.</p>
  36.  */
  37. public class AngularInterval implements HyperplaneBoundedRegion<Point1S> {
  38.     /** The minimum boundary of the interval. */
  39.     private final CutAngle minBoundary;

  40.     /** The maximum boundary of the interval. */
  41.     private final CutAngle maxBoundary;

  42.     /** Point halfway between the min and max boundaries. */
  43.     private final Point1S midpoint;

  44.     /** Construct a new instance representing the angular region between the given
  45.      * min and max azimuth boundaries. The arguments must be either all finite or all
  46.      * null (to indicate the full space). If the boundaries are finite, then the min
  47.      * boundary azimuth value must be numerically less than the max boundary. Callers are
  48.      * responsible for enforcing these constraints. No validation is performed.
  49.      * @param minBoundary minimum boundary for the interval
  50.      * @param maxBoundary maximum boundary for the interval
  51.      */
  52.     private AngularInterval(final CutAngle minBoundary, final CutAngle maxBoundary) {

  53.         this.minBoundary = minBoundary;
  54.         this.maxBoundary = maxBoundary;
  55.         this.midpoint = (minBoundary != null && maxBoundary != null) ?
  56.                 Point1S.of(0.5 * (minBoundary.getAzimuth() + maxBoundary.getAzimuth())) :
  57.                 null;
  58.     }

  59.     /** Get the minimum azimuth angle for the interval, or {@code 0}
  60.      * if the interval is full.
  61.      * @return the minimum azimuth angle for the interval or {@code 0}
  62.      *      if the interval represents the full space.
  63.      */
  64.     public double getMin() {
  65.         return (minBoundary != null) ?
  66.                 minBoundary.getAzimuth() :
  67.                 0.0;
  68.     }

  69.     /** Get the minimum boundary for the interval, or null if the
  70.      * interval represents the full space.
  71.      * @return the minimum point for the interval or null if
  72.      *      the interval represents the full space
  73.      */
  74.     public CutAngle getMinBoundary() {
  75.         return minBoundary;
  76.     }

  77.     /** Get the maximum azimuth angle for the interval, or {@code 2pi} if
  78.      * the interval represents the full space.
  79.      * @return the maximum azimuth angle for the interval or {@code 2pi} if
  80.      *      the interval represents the full space.
  81.      */
  82.     public double getMax() {
  83.         return (maxBoundary != null) ?
  84.                 maxBoundary.getAzimuth() :
  85.                 Angle.TWO_PI;
  86.     }

  87.     /** Get the maximum point for the interval. This will be null if the
  88.      * interval represents the full space.
  89.      * @return the maximum point for the interval or null if
  90.      *      the interval represents the full space
  91.      */
  92.     public CutAngle getMaxBoundary() {
  93.         return maxBoundary;
  94.     }

  95.     /** Get the midpoint of the interval or null if the interval represents
  96.      *  the full space.
  97.      * @return the midpoint of the interval or null if the interval represents
  98.      *      the full space
  99.      * @see #getCentroid()
  100.      */
  101.     public Point1S getMidPoint() {
  102.         return midpoint;
  103.     }

  104.     /** {@inheritDoc} */
  105.     @Override
  106.     public boolean isFull() {
  107.         // minBoundary and maxBoundary are either both null or both not null
  108.         return minBoundary == null;
  109.     }

  110.     /** {@inheritDoc}
  111.      *
  112.      * <p>This method always returns false.</p>
  113.      */
  114.     @Override
  115.     public boolean isEmpty() {
  116.         return false;
  117.     }

  118.     /** {@inheritDoc} */
  119.     @Override
  120.     public double getSize() {
  121.         return getMax() - getMin();
  122.     }

  123.     /** {@inheritDoc}
  124.      *
  125.      * <p>This method simply returns 0 because boundaries in one dimension do not
  126.      *  have any size.</p>
  127.      */
  128.     @Override
  129.     public double getBoundarySize() {
  130.         return 0;
  131.     }

  132.     /** {@inheritDoc}
  133.      *
  134.      * <p>This method is an alias for {@link #getMidPoint()}.</p>
  135.      * @see #getMidPoint()
  136.      */
  137.     @Override
  138.     public Point1S getCentroid() {
  139.         return getMidPoint();
  140.     }

  141.     /** {@inheritDoc} */
  142.     @Override
  143.     public RegionLocation classify(final Point1S pt) {
  144.         if (!isFull()) {
  145.             final HyperplaneLocation minLoc = minBoundary.classify(pt);
  146.             final HyperplaneLocation maxLoc = maxBoundary.classify(pt);

  147.             final boolean wraps = wrapsZero();

  148.             if ((!wraps && (minLoc == HyperplaneLocation.PLUS || maxLoc == HyperplaneLocation.PLUS)) ||
  149.                     (wraps && minLoc == HyperplaneLocation.PLUS && maxLoc == HyperplaneLocation.PLUS)) {
  150.                 return RegionLocation.OUTSIDE;
  151.             } else if (minLoc == HyperplaneLocation.ON || maxLoc == HyperplaneLocation.ON) {
  152.                 return RegionLocation.BOUNDARY;
  153.             }
  154.         }
  155.         return RegionLocation.INSIDE;
  156.     }

  157.     /** {@inheritDoc} */
  158.     @Override
  159.     public Point1S project(final Point1S pt) {
  160.         if (!isFull()) {
  161.             final double minDist = minBoundary.getPoint().distance(pt);
  162.             final double maxDist = maxBoundary.getPoint().distance(pt);

  163.             return (minDist <= maxDist) ?
  164.                     minBoundary.getPoint() :
  165.                     maxBoundary.getPoint();
  166.         }
  167.         return null;
  168.     }

  169.     /** Return true if the interval wraps around the zero/{@code 2pi} point. In this
  170.      * case, the max boundary azimuth is less than that of the min boundary when both
  171.      * values are normalized to the range {@code [0, 2pi)}.
  172.      * @return true if the interval wraps around the zero/{@code 2pi} point
  173.      */
  174.     public boolean wrapsZero() {
  175.         if (!isFull()) {
  176.             final double minNormAz = minBoundary.getPoint().getNormalizedAzimuth();
  177.             final double maxNormAz = maxBoundary.getPoint().getNormalizedAzimuth();

  178.             return maxNormAz < minNormAz;
  179.         }
  180.         return false;
  181.     }

  182.     /** Return a new instance transformed by the argument. If the transformed size
  183.      * of the interval is greater than or equal to 2pi, then an interval representing
  184.      * the full space is returned.
  185.      * @param transform transform to apply
  186.      * @return a new instance transformed by the argument
  187.      */
  188.     public AngularInterval transform(final Transform<Point1S> transform) {
  189.         return AngularInterval.transform(this, transform, AngularInterval::of);
  190.     }

  191.     /** {@inheritDoc}
  192.     *
  193.     * <p>This method returns instances of {@link RegionBSPTree1S} instead of
  194.     * {@link AngularInterval} since it is possible for a convex angular interval
  195.     * to be split into disjoint regions by a single hyperplane. These disjoint
  196.     * regions cannot be represented by this class and require the use of a BSP
  197.     * tree.</p>
  198.     *
  199.     * @see RegionBSPTree1S#split(Hyperplane)
  200.     */
  201.     @Override
  202.     public Split<RegionBSPTree1S> split(final Hyperplane<Point1S> splitter) {
  203.         return toTree().split(splitter);
  204.     }

  205.     /** Return a {@link RegionBSPTree1S} instance representing the same region
  206.      * as this instance.
  207.      * @return a BSP tree representing the same region as this instance
  208.      */
  209.     public RegionBSPTree1S toTree() {
  210.         return RegionBSPTree1S.fromInterval(this);
  211.     }

  212.     /** Return a list of convex intervals comprising this region.
  213.      * @return a list of convex intervals comprising this region
  214.      * @see Convex
  215.      */
  216.     public List<AngularInterval.Convex> toConvex() {
  217.         if (isConvex(minBoundary, maxBoundary)) {
  218.             return Collections.singletonList(new Convex(minBoundary, maxBoundary));
  219.         }

  220.         final CutAngle midPos = CutAngles.createPositiveFacing(midpoint, minBoundary.getPrecision());
  221.         final CutAngle midNeg = CutAngles.createNegativeFacing(midpoint, maxBoundary.getPrecision());

  222.         return Arrays.asList(
  223.                     new Convex(minBoundary, midPos),
  224.                     new Convex(midNeg, maxBoundary)
  225.                 );
  226.     }

  227.     /** {@inheritDoc} */
  228.     @Override
  229.     public String toString() {
  230.         final StringBuilder sb = new StringBuilder();
  231.         sb.append(this.getClass().getSimpleName())
  232.             .append("[min= ")
  233.             .append(getMin())
  234.             .append(", max= ")
  235.             .append(getMax())
  236.             .append(']');

  237.         return sb.toString();
  238.     }

  239.     /** Return an instance representing the full space. The returned instance contains all
  240.      * possible azimuth angles.
  241.      * @return an interval representing the full space
  242.      */
  243.     public static AngularInterval.Convex full() {
  244.         return Convex.FULL;
  245.     }

  246.     /** Return an instance representing the angular interval between the given min and max azimuth
  247.      * values. The max value is adjusted to be numerically above the min value, even if the resulting
  248.      * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
  249.      * is returned if either point is infinite or min and max are equivalent as evaluated by the
  250.      * given precision context.
  251.      * @param min min azimuth value
  252.      * @param max max azimuth value
  253.      * @param precision precision precision context used to compare floating point values
  254.      * @return a new instance resulting the angular region between the given min and max azimuths
  255.      * @throws IllegalArgumentException if either azimuth is infinite or NaN
  256.      */
  257.     public static AngularInterval of(final double min, final double max,
  258.             final Precision.DoubleEquivalence precision) {
  259.         return of(Point1S.of(min), Point1S.of(max), precision);
  260.     }

  261.     /** Return an instance representing the angular interval between the given min and max azimuth
  262.      * points. The max point is adjusted to be numerically above the min point, even if the resulting
  263.      * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
  264.      * is returned if either point is infinite or min and max are equivalent as evaluated by the
  265.      * given precision context.
  266.      * @param min min azimuth value
  267.      * @param max max azimuth value
  268.      * @param precision precision precision context used to compare floating point values
  269.      * @return a new instance resulting the angular region between the given min and max points
  270.      * @throws IllegalArgumentException if either azimuth is infinite or NaN
  271.      */
  272.     public static AngularInterval of(final Point1S min, final Point1S max,
  273.             final Precision.DoubleEquivalence precision) {
  274.         return createInterval(min, max, precision, AngularInterval::new, Convex.FULL);
  275.     }

  276.     /** Return an instance representing the angular interval between the given oriented points.
  277.      * The negative-facing point is used as the minimum boundary and the positive-facing point is
  278.      * adjusted to be above the minimum. The arguments can be given in any order. The full space
  279.      * is returned if the points are equivalent or are oriented in the same direction.
  280.      * @param a first oriented point
  281.      * @param b second oriented point
  282.      * @return an instance representing the angular interval between the given oriented points
  283.      * @throws IllegalArgumentException if either argument is infinite or NaN
  284.      */
  285.     public static AngularInterval of(final CutAngle a, final CutAngle b) {
  286.         return createInterval(a, b, AngularInterval::new, Convex.FULL);
  287.     }

  288.     /** Internal method to create an interval between the given min and max points. The max point
  289.      * is adjusted to be numerically above the min point, even if the resulting
  290.      * azimuth value is greater than or equal to {@code 2pi}. The full instance argument
  291.      * is returned if either point is infinite or min and max are equivalent as evaluated by the
  292.      * given precision context.
  293.      * @param min min azimuth value
  294.      * @param max max azimuth value
  295.      * @param precision precision precision context used to compare floating point values
  296.      * @param factory factory object used to create new instances; this object is passed the validated
  297.      *      min (negative-facing) cut and the max (positive-facing) cut, in that order
  298.      * @param <T> Angular interval implementation type
  299.      * @param fullSpace instance returned if the interval should represent the full space
  300.      * @return a new instance resulting the angular region between the given min and max points
  301.      * @throws IllegalArgumentException if either azimuth is infinite or NaN
  302.      */
  303.     private static <T extends AngularInterval> T createInterval(final Point1S min, final Point1S max,
  304.             final Precision.DoubleEquivalence precision,
  305.             final BiFunction<? super CutAngle, ? super CutAngle, T> factory, final T fullSpace) {

  306.         validateIntervalValues(min, max);

  307.         // return the full space if either point is infinite or the points are equivalent
  308.         if (min.eq(max, precision)) {
  309.             return fullSpace;
  310.         }

  311.         final Point1S adjustedMax = max.above(min);

  312.         return factory.apply(
  313.                     CutAngles.createNegativeFacing(min, precision),
  314.                     CutAngles.createPositiveFacing(adjustedMax, precision)
  315.                 );
  316.     }

  317.     /** Internal method to create a new interval instance from the given cut angles.
  318.      * The negative-facing point is used as the minimum boundary and the positive-facing point is
  319.      * adjusted to be above the minimum. The arguments can be given in any order. The full space
  320.      * argument is returned if the points are equivalent or are oriented in the same direction.
  321.      * @param a first cut point
  322.      * @param b second cut point
  323.      * @param factory factory object used to create new instances; this object is passed the validated
  324.      *      min (negative-facing) cut and the max (positive-facing) cut, in that order
  325.      * @param fullSpace instance returned if the interval should represent the full space
  326.      * @param <T> Angular interval implementation type
  327.      * @return a new interval instance created from the given cut angles
  328.      * @throws IllegalArgumentException if either argument is infinite or NaN
  329.      */
  330.     private static <T extends AngularInterval> T createInterval(final CutAngle a, final CutAngle b,
  331.             final BiFunction<? super CutAngle, ? super CutAngle, T> factory, final T fullSpace) {

  332.         final Point1S aPoint = a.getPoint();
  333.         final Point1S bPoint = b.getPoint();

  334.         validateIntervalValues(aPoint, bPoint);

  335.         if (a.isPositiveFacing() == b.isPositiveFacing() ||
  336.                 aPoint.eq(bPoint, a.getPrecision()) ||
  337.                 bPoint.eq(aPoint, b.getPrecision())) {
  338.             // points are equivalent or facing in the same direction
  339.             return fullSpace;
  340.         }

  341.         final CutAngle min = a.isPositiveFacing() ? b : a;
  342.         final CutAngle max = a.isPositiveFacing() ? a : b;
  343.         final CutAngle adjustedMax = CutAngles.createPositiveFacing(
  344.                 max.getPoint().above(min.getPoint()),
  345.                 max.getPrecision());

  346.         return factory.apply(min, adjustedMax);
  347.     }

  348.     /** Validate that the given points can be used to specify an angular interval.
  349.      * @param a first point
  350.      * @param b second point
  351.      * @throws IllegalArgumentException if either point is infinite NaN
  352.      */
  353.     private static void validateIntervalValues(final Point1S a, final Point1S b) {
  354.         if (!a.isFinite() || !b.isFinite()) {
  355.             throw new IllegalArgumentException(MessageFormat.format("Invalid angular interval: [{0}, {1}]",
  356.                     a.getAzimuth(), b.getAzimuth()));
  357.         }
  358.     }

  359.     /** Return true if the given cut angles define a convex region. By convention, the
  360.      * precision context from the min cut is used for the floating point comparison.
  361.      * @param min min (negative-facing) cut angle
  362.      * @param max max (positive-facing) cut angle
  363.      * @return true if the given cut angles define a convex region
  364.      */
  365.     private static boolean isConvex(final CutAngle min, final CutAngle max) {
  366.         if (min != null && max != null) {
  367.             final double dist = max.getAzimuth() - min.getAzimuth();
  368.             final Precision.DoubleEquivalence precision = min.getPrecision();
  369.             return precision.lte(dist, Math.PI);
  370.         }

  371.         return true;
  372.     }

  373.     /** Internal transform method that transforms the given instance, using the factory
  374.      * method to create a new instance if needed.
  375.      * @param interval interval to transform
  376.      * @param transform transform to apply
  377.      * @param factory object used to create new instances
  378.      * @param <T> Angular interval implementation type
  379.      * @return a transformed instance
  380.      */
  381.     private static <T extends AngularInterval> T transform(final T interval,
  382.             final Transform<Point1S> transform,
  383.             final BiFunction<? super CutAngle, ? super CutAngle, T> factory) {

  384.         if (!interval.isFull()) {
  385.             final CutAngle tMin = interval.getMinBoundary().transform(transform);
  386.             final CutAngle tMax = interval.getMaxBoundary().transform(transform);

  387.             return factory.apply(tMin, tMax);
  388.         }

  389.         return interval;
  390.     }

  391.     /** Class representing an angular interval with the additional property that the
  392.      * region is convex. By convex, it is meant that the shortest path between any
  393.      * two points in the region is also contained entirely in the region. If there is
  394.      * a tie for shortest path, then it is sufficient that at least one lie entirely
  395.      * within the region. For spherical 1D space, this means that the angular interval
  396.      * is either completely full or has a length less than or equal to {@code pi}.
  397.      */
  398.     public static final class Convex extends AngularInterval {
  399.         /** Interval instance representing the full space. */
  400.         private static final Convex FULL = new Convex(null, null);

  401.         /** Construct a new convex instance from its boundaries and midpoint. No validation
  402.          * of the argument is performed. Callers are responsible for ensuring that the size
  403.          * of interval is less than or equal to {@code pi}.
  404.          * @param minBoundary minimum boundary for the interval
  405.          * @param maxBoundary maximum boundary for the interval
  406.          * @throws IllegalArgumentException if the interval is not convex
  407.          */
  408.         private Convex(final CutAngle minBoundary, final CutAngle maxBoundary) {
  409.             super(minBoundary, maxBoundary);

  410.             if (!isConvex(minBoundary, maxBoundary)) {
  411.                 throw new IllegalArgumentException(MessageFormat.format("Interval is not convex: [{0}, {1}]",
  412.                         minBoundary.getAzimuth(), maxBoundary.getAzimuth()));
  413.             }
  414.         }

  415.         /** {@inheritDoc} */
  416.         @Override
  417.         public List<AngularInterval.Convex> toConvex() {
  418.             return Collections.singletonList(this);
  419.         }

  420.         /** {@inheritDoc} */
  421.         @Override
  422.         public Convex transform(final Transform<Point1S> transform) {
  423.             return AngularInterval.transform(this, transform, Convex::of);
  424.         }

  425.         /** Split the instance along a circle diameter.The diameter is defined by the given split point and
  426.          * its reversed antipodal point.
  427.          * @param splitter split point defining one side of the split diameter
  428.          * @return result of the split operation
  429.          */
  430.         public Split<Convex> splitDiameter(final CutAngle splitter) {

  431.             final CutAngle opposite = CutAngles.fromPointAndDirection(
  432.                     splitter.getPoint().antipodal(),
  433.                     !splitter.isPositiveFacing(),
  434.                     splitter.getPrecision());

  435.             if (isFull()) {
  436.                 final Convex minus = Convex.of(splitter, opposite);
  437.                 final Convex plus = Convex.of(splitter.reverse(), opposite.reverse());

  438.                 return new Split<>(minus, plus);
  439.             }

  440.             final CutAngle minBoundary = getMinBoundary();
  441.             final CutAngle maxBoundary = getMaxBoundary();

  442.             final Point1S posPole = Point1S.of(splitter.getPoint().getAzimuth() + Angle.PI_OVER_TWO);

  443.             final int minLoc = minBoundary.getPrecision().compare(Angle.PI_OVER_TWO,
  444.                     posPole.distance(minBoundary.getPoint()));
  445.             final int maxLoc = maxBoundary.getPrecision().compare(Angle.PI_OVER_TWO,
  446.                     posPole.distance(maxBoundary.getPoint()));

  447.             final boolean positiveFacingSplit = splitter.isPositiveFacing();

  448.             // assume a positive orientation of the splitter for region location
  449.             // purposes and adjust later
  450.             Convex pos = null;
  451.             Convex neg = null;

  452.             if (minLoc > 0) {
  453.                 // min is on the pos side

  454.                 if (maxLoc >= 0) {
  455.                     // max is directly on the splitter or on the pos side
  456.                     pos = this;
  457.                 } else {
  458.                     // min is on the pos side and max is on the neg side
  459.                     final CutAngle posCut = positiveFacingSplit ?
  460.                             opposite.reverse() :
  461.                             opposite;
  462.                     pos = Convex.of(minBoundary, posCut);

  463.                     final CutAngle negCut = positiveFacingSplit ?
  464.                             opposite :
  465.                             opposite.reverse();
  466.                     neg = Convex.of(negCut, maxBoundary);
  467.                 }
  468.             } else if (minLoc < 0) {
  469.                 // min is on the neg side

  470.                 if (maxLoc <= 0) {
  471.                     // max is directly on the splitter or on the neg side
  472.                     neg = this;
  473.                 } else {
  474.                     // min is on the neg side and max is on the pos side
  475.                     final CutAngle posCut = positiveFacingSplit ?
  476.                             splitter.reverse() :
  477.                             splitter;
  478.                     pos = Convex.of(maxBoundary, posCut);

  479.                     final CutAngle negCut = positiveFacingSplit ?
  480.                             splitter :
  481.                             splitter.reverse();
  482.                     neg = Convex.of(negCut, minBoundary);
  483.                 }
  484.             } else {
  485.                 // min is directly on the splitter; determine whether it was on the main split
  486.                 // point or its antipodal point
  487.                 if (splitter.getPoint().distance(minBoundary.getPoint()) < Angle.PI_OVER_TWO) {
  488.                     // on main splitter; interval will be located on pos side of split
  489.                     pos = this;
  490.                 } else {
  491.                     // on antipodal point; interval will be located on neg side of split
  492.                     neg = this;
  493.                 }
  494.             }

  495.             // adjust for the actual orientation of the splitter
  496.             final Convex minus = positiveFacingSplit ? neg : pos;
  497.             final Convex plus = positiveFacingSplit ? pos : neg;

  498.             return new Split<>(minus, plus);
  499.         }

  500.         /** Return an instance representing the convex angular interval between the given min and max azimuth
  501.          * values. The max value is adjusted to be numerically above the min value, even if the resulting
  502.          * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
  503.          * is returned if either point is infinite or min and max are equivalent as evaluated by the
  504.          * given precision context.
  505.          * @param min min azimuth value
  506.          * @param max max azimuth value
  507.          * @param precision precision precision context used to compare floating point values
  508.          * @return a new instance resulting the angular region between the given min and max azimuths
  509.          * @throws IllegalArgumentException if either azimuth is infinite or NaN, or the given angular
  510.          *      interval is not convex (meaning it has a size of greater than {@code pi})
  511.          */
  512.         public static Convex of(final double min, final double max, final Precision.DoubleEquivalence precision) {
  513.             return of(Point1S.of(min), Point1S.of(max), precision);
  514.         }

  515.         /** Return an instance representing the convex angular interval between the given min and max azimuth
  516.          * points. The max point is adjusted to be numerically above the min point, even if the resulting
  517.          * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space
  518.          * is returned if either point is infinite or min and max are equivalent as evaluated by the
  519.          * given precision context.
  520.          * @param min min azimuth value
  521.          * @param max max azimuth value
  522.          * @param precision precision precision context used to compare floating point values
  523.          * @return a new instance resulting the angular region between the given min and max points
  524.          * @throws IllegalArgumentException if either azimuth is infinite or NaN, or the given angular
  525.          *      interval is not convex (meaning it has a size of greater than {@code pi})
  526.          */
  527.         public static Convex of(final Point1S min, final Point1S max, final Precision.DoubleEquivalence precision) {
  528.             return createInterval(min, max, precision, Convex::new, Convex.FULL);
  529.         }

  530.         /** Return an instance representing the convex angular interval between the given oriented points.
  531.          * The negative-facing point is used as the minimum boundary and the positive-facing point is
  532.          * adjusted to be above the minimum. The arguments can be given in any order. The full space
  533.          * is returned if the points are equivalent or are oriented in the same direction.
  534.          * @param a first oriented point
  535.          * @param b second oriented point
  536.          * @return an instance representing the angular interval between the given oriented points
  537.          * @throws IllegalArgumentException if either azimuth is infinite or NaN, or the given angular
  538.          *      interval is not convex (meaning it has a size of greater than {@code pi})
  539.          */
  540.         public static Convex of(final CutAngle a, final CutAngle b) {
  541.             return createInterval(a, b, Convex::new, Convex.FULL);
  542.         }
  543.     }
  544. }