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 }