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}