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