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.euclidean.oned;
18
19 import java.text.MessageFormat;
20
21 import org.apache.commons.geometry.core.RegionLocation;
22 import org.apache.commons.geometry.core.Transform;
23 import org.apache.commons.geometry.core.partitioning.Hyperplane;
24 import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
25 import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
26 import org.apache.commons.geometry.core.partitioning.Split;
27 import org.apache.commons.numbers.core.Precision;
28
29 /** Class representing an interval in one dimension. The interval is defined
30 * by minimum and maximum values. One or both of these values may be infinite
31 * although not with the same sign.
32 *
33 * <p>Instances of this class are guaranteed to be immutable.</p>
34 */
35 public final class Interval implements HyperplaneBoundedRegion<Vector1D> {
36 /** Interval instance representing the entire real number line. */
37 private static final Interval FULL = new Interval(null, null);
38
39 /** {@link OrientedPoint} instance representing the min boundary of the interval,
40 * or null if no min boundary exists. If present, this instance will be negative-facing.
41 * Infinite values are allowed but not NaN.
42 */
43 private final OrientedPoint minBoundary;
44
45 /** {@link OrientedPoint} instance representing the max boundary of the interval,
46 * or null if no max boundary exists. If present, this instance will be negative-facing.
47 * Infinite values are allowed but not NaN.
48 */
49 private final OrientedPoint maxBoundary;
50
51 /** Create an instance from min and max bounding hyperplanes. No validation is performed.
52 * Callers are responsible for ensuring that the given hyperplanes represent a valid
53 * interval.
54 * @param minBoundary the min (negative-facing) hyperplane
55 * @param maxBoundary the max (positive-facing) hyperplane
56 */
57 private Interval(final OrientedPoint minBoundary, final OrientedPoint maxBoundary) {
58 this.minBoundary = minBoundary;
59 this.maxBoundary = maxBoundary;
60 }
61
62 /** Get the minimum value for the interval or {@link Double#NEGATIVE_INFINITY}
63 * if no minimum value exists.
64 * @return the minimum value for the interval or {@link Double#NEGATIVE_INFINITY}
65 * if no minimum value exists.
66 */
67 public double getMin() {
68 return (minBoundary != null) ? minBoundary.getLocation() : Double.NEGATIVE_INFINITY;
69 }
70
71 /** Get the maximum value for the interval or {@link Double#POSITIVE_INFINITY}
72 * if no maximum value exists.
73 * @return the maximum value for the interval or {@link Double#POSITIVE_INFINITY}
74 * if no maximum value exists.
75 */
76 public double getMax() {
77 return (maxBoundary != null) ? maxBoundary.getLocation() : Double.POSITIVE_INFINITY;
78 }
79
80 /**
81 * Get the {@link OrientedPoint} forming the minimum bounding hyperplane
82 * of the interval, or null if none exists. If present, This hyperplane
83 * is oriented to point in the negative direction.
84 * @return the hyperplane forming the minimum boundary of the interval or
85 * null if no minimum boundary exists
86 */
87 public OrientedPoint getMinBoundary() {
88 return minBoundary;
89 }
90
91 /**
92 * Get the {@link OrientedPoint} forming the maximum bounding hyperplane
93 * of the interval, or null if none exists. If present, this hyperplane
94 * is oriented to point in the positive direction.
95 * @return the hyperplane forming the maximum boundary of the interval or
96 * null if no maximum boundary exists
97 */
98 public OrientedPoint getMaxBoundary() {
99 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 }