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 }