001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.geometry.euclidean.oned; 018 019import java.text.MessageFormat; 020 021import org.apache.commons.geometry.core.RegionLocation; 022import org.apache.commons.geometry.core.Transform; 023import org.apache.commons.geometry.core.partitioning.Hyperplane; 024import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion; 025import org.apache.commons.geometry.core.partitioning.HyperplaneLocation; 026import org.apache.commons.geometry.core.partitioning.Split; 027import org.apache.commons.numbers.core.Precision; 028 029/** Class representing an interval in one dimension. The interval is defined 030 * by minimum and maximum values. One or both of these values may be infinite 031 * although not with the same sign. 032 * 033 * <p>Instances of this class are guaranteed to be immutable.</p> 034 */ 035public final class Interval implements HyperplaneBoundedRegion<Vector1D> { 036 /** Interval instance representing the entire real number line. */ 037 private static final Interval FULL = new Interval(null, null); 038 039 /** {@link OrientedPoint} instance representing the min boundary of the interval, 040 * or null if no min boundary exists. If present, this instance will be negative-facing. 041 * Infinite values are allowed but not NaN. 042 */ 043 private final OrientedPoint minBoundary; 044 045 /** {@link OrientedPoint} instance representing the max boundary of the interval, 046 * or null if no max boundary exists. If present, this instance will be negative-facing. 047 * Infinite values are allowed but not NaN. 048 */ 049 private final OrientedPoint maxBoundary; 050 051 /** Create an instance from min and max bounding hyperplanes. No validation is performed. 052 * Callers are responsible for ensuring that the given hyperplanes represent a valid 053 * interval. 054 * @param minBoundary the min (negative-facing) hyperplane 055 * @param maxBoundary the max (positive-facing) hyperplane 056 */ 057 private Interval(final OrientedPoint minBoundary, final OrientedPoint maxBoundary) { 058 this.minBoundary = minBoundary; 059 this.maxBoundary = maxBoundary; 060 } 061 062 /** Get the minimum value for the interval or {@link Double#NEGATIVE_INFINITY} 063 * if no minimum value exists. 064 * @return the minimum value for the interval or {@link Double#NEGATIVE_INFINITY} 065 * if no minimum value exists. 066 */ 067 public double getMin() { 068 return (minBoundary != null) ? minBoundary.getLocation() : Double.NEGATIVE_INFINITY; 069 } 070 071 /** Get the maximum value for the interval or {@link Double#POSITIVE_INFINITY} 072 * if no maximum value exists. 073 * @return the maximum value for the interval or {@link Double#POSITIVE_INFINITY} 074 * if no maximum value exists. 075 */ 076 public double getMax() { 077 return (maxBoundary != null) ? maxBoundary.getLocation() : Double.POSITIVE_INFINITY; 078 } 079 080 /** 081 * Get the {@link OrientedPoint} forming the minimum bounding hyperplane 082 * of the interval, or null if none exists. If present, This hyperplane 083 * is oriented to point in the negative direction. 084 * @return the hyperplane forming the minimum boundary of the interval or 085 * null if no minimum boundary exists 086 */ 087 public OrientedPoint getMinBoundary() { 088 return minBoundary; 089 } 090 091 /** 092 * Get the {@link OrientedPoint} forming the maximum bounding hyperplane 093 * of the interval, or null if none exists. If present, this hyperplane 094 * is oriented to point in the positive direction. 095 * @return the hyperplane forming the maximum boundary of the interval or 096 * null if no maximum boundary exists 097 */ 098 public OrientedPoint getMaxBoundary() { 099 return maxBoundary; 100 } 101 102 /** Return true if the interval has a minimum (lower) boundary. 103 * @return true if the interval has minimum (lower) boundary 104 */ 105 public boolean hasMinBoundary() { 106 return minBoundary != null; 107 } 108 109 /** Return true if the interval has a maximum (upper) boundary. 110 * @return true if the interval has maximum (upper) boundary 111 */ 112 public boolean hasMaxBoundary() { 113 return maxBoundary != null; 114 } 115 116 /** True if the region is infinite, meaning that at least one of the boundaries 117 * does not exist. 118 * @return true if the region is infinite 119 */ 120 @Override 121 public boolean isInfinite() { 122 return minBoundary == null || maxBoundary == null; 123 } 124 125 /** True if the region is finite, meaning that both the minimum and maximum 126 * boundaries exist and the region size is finite. 127 * @return true if the region is finite 128 */ 129 @Override 130 public boolean isFinite() { 131 return !isInfinite(); 132 } 133 134 /** {@inheritDoc} */ 135 @Override 136 public RegionLocation classify(final Vector1D pt) { 137 return classify(pt.getX()); 138 } 139 140 /** Classify a point with respect to the interval. 141 * @param location the location to classify 142 * @return the classification of the point with respect to the interval 143 * @see #classify(Vector1D) 144 */ 145 public RegionLocation classify(final double location) { 146 final RegionLocation minLoc = classifyWithBoundary(location, minBoundary); 147 final RegionLocation maxLoc = classifyWithBoundary(location, maxBoundary); 148 149 if (minLoc == RegionLocation.BOUNDARY || maxLoc == RegionLocation.BOUNDARY) { 150 return RegionLocation.BOUNDARY; 151 } else if (minLoc == RegionLocation.INSIDE && maxLoc == RegionLocation.INSIDE) { 152 return RegionLocation.INSIDE; 153 } 154 return RegionLocation.OUTSIDE; 155 } 156 157 /** Classify the location using the given interval boundary, which may be null. 158 * @param location the location to classify 159 * @param boundary interval boundary to classify against 160 * @return the location of the point relative to the boundary 161 */ 162 private RegionLocation classifyWithBoundary(final double location, final OrientedPoint boundary) { 163 if (Double.isNaN(location)) { 164 return RegionLocation.OUTSIDE; 165 } else if (boundary == null) { 166 return RegionLocation.INSIDE; 167 } else { 168 final HyperplaneLocation hyperLoc = boundary.classify(location); 169 170 if (hyperLoc == HyperplaneLocation.ON) { 171 return RegionLocation.BOUNDARY; 172 } else if (hyperLoc == HyperplaneLocation.PLUS) { 173 return RegionLocation.OUTSIDE; 174 } 175 return RegionLocation.INSIDE; 176 } 177 } 178 179 /** Return true if the given point location is on the inside or boundary 180 * of the region. 181 * @param x the location to test 182 * @return true if the location is on the inside or boundary of the region 183 * @see #contains(org.apache.commons.geometry.core.Point) 184 */ 185 public boolean contains(final double x) { 186 return classify(x) != RegionLocation.OUTSIDE; 187 } 188 189 /** {@inheritDoc} 190 * 191 * <p>The point is projected onto the nearest interval boundary. When a point 192 * is on the inside of the interval and is equidistant from both boundaries, 193 * then the minimum boundary is selected. when a point is on the outside of the 194 * interval and is equidistant from both boundaries (as is the case for intervals 195 * representing a single point), then the boundary facing the point is returned, 196 * ensuring that the returned offset is positive. 197 * </p> 198 */ 199 @Override 200 public Vector1D project(final Vector1D pt) { 201 202 OrientedPoint boundary = null; 203 204 if (minBoundary != null && maxBoundary != null) { 205 // both boundaries are present; use the closest 206 final double minOffset = minBoundary.offset(pt.getX()); 207 final double maxOffset = maxBoundary.offset(pt.getX()); 208 209 final double minDist = Math.abs(minOffset); 210 final double maxDist = Math.abs(maxOffset); 211 212 // Project onto the max boundary if it's the closest or the point is on its plus side. 213 // Otherwise, project onto the min boundary. 214 if (maxDist < minDist || maxOffset > 0) { 215 boundary = maxBoundary; 216 } else { 217 boundary = minBoundary; 218 } 219 } else if (minBoundary != null) { 220 // only the min boundary is present 221 boundary = minBoundary; 222 } else if (maxBoundary != null) { 223 // only the max boundary is present 224 boundary = maxBoundary; 225 } 226 227 return (boundary != null) ? boundary.project(pt) : null; 228 } 229 230 /** Return a new instance transformed by the argument. 231 * @param transform transform to apply 232 * @return a new instance transformed by the argument 233 */ 234 public Interval transform(final Transform<Vector1D> transform) { 235 final OrientedPoint transformedMin = (minBoundary != null) ? 236 minBoundary.transform(transform) : 237 null; 238 final OrientedPoint transformedMax = (maxBoundary != null) ? 239 maxBoundary.transform(transform) : 240 null; 241 242 return of(transformedMin, transformedMax); 243 } 244 245 /** {@inheritDoc} 246 * 247 * <p>This method always returns false since there is always at least 248 * one point that can be classified as not being on the outside of 249 * the region.</p> 250 */ 251 @Override 252 public boolean isEmpty() { 253 return false; 254 } 255 256 /** {@inheritDoc} */ 257 @Override 258 public boolean isFull() { 259 return minBoundary == null && maxBoundary == null; 260 } 261 262 /** {@inheritDoc} */ 263 @Override 264 public double getSize() { 265 if (isInfinite()) { 266 return Double.POSITIVE_INFINITY; 267 } 268 269 return getMax() - getMin(); 270 } 271 272 /** {@inheritDoc} 273 * 274 * <p>This method simply returns 0 because boundaries in one dimension do not 275 * have any size.</p> 276 */ 277 @Override 278 public double getBoundarySize() { 279 return 0; 280 } 281 282 /** {@inheritDoc} */ 283 @Override 284 public Vector1D getCentroid() { 285 if (isInfinite()) { 286 return null; 287 } 288 289 final double min = getMin(); 290 final double max = getMax(); 291 292 return Vector1D.of((0.5 * (max - min)) + min); 293 } 294 295 /** {@inheritDoc} */ 296 @Override 297 public Split<Interval> split(final Hyperplane<Vector1D> splitter) { 298 final OrientedPoint splitOrientedPoint = (OrientedPoint) splitter; 299 final Vector1D splitPoint = splitOrientedPoint.getPoint(); 300 301 final HyperplaneLocation splitterMinLoc = (minBoundary != null) ? minBoundary.classify(splitPoint) : null; 302 final HyperplaneLocation splitterMaxLoc = (maxBoundary != null) ? maxBoundary.classify(splitPoint) : null; 303 304 Interval low = null; 305 Interval high = null; 306 307 if (splitterMinLoc != HyperplaneLocation.ON || splitterMaxLoc != HyperplaneLocation.ON) { 308 309 if (splitterMinLoc != null && splitterMinLoc != HyperplaneLocation.MINUS) { 310 // splitter is on or below min boundary 311 high = this; 312 } else if (splitterMaxLoc != null && splitterMaxLoc != HyperplaneLocation.MINUS) { 313 // splitter is on or above max boundary 314 low = this; 315 } else { 316 // the interval is split in two 317 low = new Interval(minBoundary, OrientedPoints.createPositiveFacing( 318 splitPoint, splitOrientedPoint.getPrecision())); 319 high = new Interval(OrientedPoints.createNegativeFacing( 320 splitPoint, splitOrientedPoint.getPrecision()), maxBoundary); 321 } 322 } 323 324 // assign minus/plus based on the orientation of the splitter 325 final boolean lowIsMinus = splitOrientedPoint.isPositiveFacing(); 326 final Interval minus = lowIsMinus ? low : high; 327 final Interval plus = lowIsMinus ? high : low; 328 329 return new Split<>(minus, plus); 330 } 331 332 /** Return a {@link RegionBSPTree1D} representing the same region as this instance. 333 * @return a BSP tree representing the same region 334 * @see RegionBSPTree1D#from(Interval, Interval...) 335 */ 336 public RegionBSPTree1D toTree() { 337 return RegionBSPTree1D.from(this); 338 } 339 340 /** {@inheritDoc} */ 341 @Override 342 public String toString() { 343 final StringBuilder sb = new StringBuilder(); 344 sb.append(this.getClass().getSimpleName()) 345 .append("[min= ") 346 .append(getMin()) 347 .append(", max= ") 348 .append(getMax()) 349 .append(']'); 350 351 return sb.toString(); 352 } 353 354 /** Create a new interval from the given point locations. The returned interval represents 355 * the region between the points, regardless of the order they are given as arguments. 356 * @param a first point location 357 * @param b second point location 358 * @param precision precision context used to compare floating point numbers 359 * @return a new interval representing the region between the given point locations 360 * @throws IllegalArgumentException if either number is {@link Double#NaN NaN} or the numbers 361 * are both infinite and have the same sign 362 */ 363 public static Interval of(final double a, final double b, final Precision.DoubleEquivalence precision) { 364 validateIntervalValues(a, b); 365 366 final double min = Math.min(a, b); 367 final double max = Math.max(a, b); 368 369 final OrientedPoint minBoundary = Double.isFinite(min) ? 370 OrientedPoints.fromLocationAndDirection(min, false, precision) : 371 null; 372 373 final OrientedPoint maxBoundary = Double.isFinite(max) ? 374 OrientedPoints.fromLocationAndDirection(max, true, precision) : 375 null; 376 377 if (minBoundary == null && maxBoundary == null) { 378 return FULL; 379 } 380 381 return new Interval(minBoundary, maxBoundary); 382 } 383 384 /** Create a new interval from the given points. The returned interval represents 385 * the region between the points, regardless of the order they are given as arguments. 386 * @param a first point 387 * @param b second point 388 * @param precision precision context used to compare floating point numbers 389 * @return a new interval representing the region between the given points 390 * @throws IllegalArgumentException if either point is {@link Vector1D#isNaN() NaN} or the points 391 * are both {@link Vector1D#isInfinite() infinite} and have the same sign 392 */ 393 public static Interval of(final Vector1D a, final Vector1D b, final Precision.DoubleEquivalence precision) { 394 return of(a.getX(), b.getX(), precision); 395 } 396 397 /** Create a new interval from the given hyperplanes. The hyperplanes may be given in 398 * any order but one must be positive-facing and the other negative-facing, with the positive-facing 399 * hyperplane located above the negative-facing hyperplane. Either or both argument may be null, 400 * in which case the returned interval will extend to infinity in the appropriate direction. If both 401 * arguments are null, an interval representing the full space is returned. 402 * @param a first hyperplane; may be null 403 * @param b second hyperplane; may be null 404 * @return a new interval representing the region between the given hyperplanes 405 * @throws IllegalArgumentException if the hyperplanes have the same orientation or 406 * do not form an interval (for example, if the positive-facing hyperplane is below 407 * the negative-facing hyperplane) 408 */ 409 public static Interval of(final OrientedPoint a, final OrientedPoint b) { 410 411 validateBoundaryRelationship(a, b); 412 413 final boolean hasA = a != null; 414 final boolean hasB = b != null; 415 416 if (!hasA && !hasB) { 417 // both boundaries null; return the full space 418 return FULL; 419 } 420 421 // determine the ordering of the hyperplanes; we know that at least one is non-null 422 final OrientedPoint minBoundary = ((hasA && !a.isPositiveFacing()) || (hasB && b.isPositiveFacing())) ? a : b; 423 final OrientedPoint maxBoundary = ((hasA && a.isPositiveFacing()) || (hasB && !b.isPositiveFacing())) ? a : b; 424 425 // validate the boundary locations; this will ensure that we don't have NaN values 426 final double minLoc = (minBoundary != null) ? minBoundary.getLocation() : Double.NEGATIVE_INFINITY; 427 final double maxLoc = (maxBoundary != null) ? maxBoundary.getLocation() : Double.POSITIVE_INFINITY; 428 429 validateIntervalValues(minLoc, maxLoc); 430 431 // create the interval, replacing infinites with nulls 432 return new Interval( 433 Double.isFinite(minLoc) ? minBoundary : null, 434 Double.isFinite(maxLoc) ? maxBoundary : null); 435 } 436 437 /** Return an interval with the given min value and no max. 438 * @param min min value for the interval 439 * @param precision precision context used to compare floating point numbers 440 * @return an interval with the given min value and no max. 441 */ 442 public static Interval min(final double min, final Precision.DoubleEquivalence precision) { 443 return of(min, Double.POSITIVE_INFINITY, precision); 444 } 445 446 /** Return an interval with the given max value and no min. 447 * @param max max value for the interval 448 * @param precision precision context used to compare floating point numbers 449 * @return an interval with the given max value and no min. 450 */ 451 public static Interval max(final double max, final Precision.DoubleEquivalence precision) { 452 return of(Double.NEGATIVE_INFINITY, max, precision); 453 } 454 455 /** Return an interval representing a single point at the given location. 456 * @param location the location of the interval 457 * @param precision precision context used to compare floating point numbers 458 * @return an interval representing a single point 459 */ 460 public static Interval point(final double location, final Precision.DoubleEquivalence precision) { 461 return of(location, location, precision); 462 } 463 464 /** Return an interval representing the entire real number line. The {@link #isFull()} 465 * method of the instance will return true. 466 * @return an interval representing the entire real number line 467 * @see #isFull() 468 */ 469 public static Interval full() { 470 return FULL; 471 } 472 473 /** Validate that the orientations and positions of the arguments may be used to create an interval. 474 * The arguments may be given in any order. Does nothing if one or both arguments are null. 475 * @param a first boundary; may be null 476 * @param b second boundary may be null 477 * @throws IllegalArgumentException is {@code a} and {@code b} have the same orientation or one does 478 * not lie on the plus side of the other. 479 */ 480 private static void validateBoundaryRelationship(final OrientedPoint a, final OrientedPoint b) { 481 if (a != null && b != null) { 482 if (a.isPositiveFacing() == b.isPositiveFacing()) { 483 throw new IllegalArgumentException( 484 MessageFormat.format("Invalid interval: hyperplanes have same orientation: {0}, {1}", a, b)); 485 } 486 487 if (a.classify(b.getPoint()) == HyperplaneLocation.PLUS || 488 b.classify(a.getPoint()) == HyperplaneLocation.PLUS) { 489 throw new IllegalArgumentException( 490 MessageFormat.format("Invalid interval: hyperplanes do not form interval: {0}, {1}", a, b)); 491 } 492 } 493 } 494 495 /** Validate that the given value can be used to construct an interval. The values 496 * must not be NaN and if infinite, must have opposite signs. 497 * @param a first value 498 * @param b second value 499 * @throws IllegalArgumentException if either value is NaN or if both values are infinite 500 * and have the same sign 501 */ 502 private static void validateIntervalValues(final double a, final double b) { 503 if (Double.isNaN(a) || Double.isNaN(b) || 504 (Double.isInfinite(a) && Double.compare(a, b) == 0)) { 505 506 throw new IllegalArgumentException( 507 MessageFormat.format("Invalid interval values: [{0}, {1}]", a, b)); 508 } 509 } 510}