Interval.java

  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. import java.text.MessageFormat;

  19. import org.apache.commons.geometry.core.RegionLocation;
  20. import org.apache.commons.geometry.core.Transform;
  21. import org.apache.commons.geometry.core.partitioning.Hyperplane;
  22. import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
  23. import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
  24. import org.apache.commons.geometry.core.partitioning.Split;
  25. import org.apache.commons.numbers.core.Precision;

  26. /** Class representing an interval in one dimension. The interval is defined
  27.  * by minimum and maximum values. One or both of these values may be infinite
  28.  * although not with the same sign.
  29.  *
  30.  * <p>Instances of this class are guaranteed to be immutable.</p>
  31.  */
  32. public final class Interval implements HyperplaneBoundedRegion<Vector1D> {
  33.     /** Interval instance representing the entire real number line. */
  34.     private static final Interval FULL = new Interval(null, null);

  35.     /** {@link OrientedPoint} instance representing the min boundary of the interval,
  36.      * or null if no min boundary exists. If present, this instance will be negative-facing.
  37.      * Infinite values are allowed but not NaN.
  38.      */
  39.     private final OrientedPoint minBoundary;

  40.     /** {@link OrientedPoint} instance representing the max boundary of the interval,
  41.      * or null if no max boundary exists. If present, this instance will be negative-facing.
  42.      * Infinite values are allowed but not NaN.
  43.      */
  44.     private final OrientedPoint maxBoundary;

  45.     /** Create an instance from min and max bounding hyperplanes. No validation is performed.
  46.      * Callers are responsible for ensuring that the given hyperplanes represent a valid
  47.      * interval.
  48.      * @param minBoundary the min (negative-facing) hyperplane
  49.      * @param maxBoundary the max (positive-facing) hyperplane
  50.      */
  51.     private Interval(final OrientedPoint minBoundary, final OrientedPoint maxBoundary) {
  52.         this.minBoundary = minBoundary;
  53.         this.maxBoundary = maxBoundary;
  54.     }

  55.     /** Get the minimum value for the interval or {@link Double#NEGATIVE_INFINITY}
  56.      * if no minimum value exists.
  57.      * @return the minimum value for the interval or {@link Double#NEGATIVE_INFINITY}
  58.      *      if no minimum value exists.
  59.      */
  60.     public double getMin() {
  61.         return (minBoundary != null) ? minBoundary.getLocation() : Double.NEGATIVE_INFINITY;
  62.     }

  63.     /** Get the maximum value for the interval or {@link Double#POSITIVE_INFINITY}
  64.      * if no maximum value exists.
  65.      * @return the maximum value for the interval or {@link Double#POSITIVE_INFINITY}
  66.      *      if no maximum value exists.
  67.      */
  68.     public double getMax() {
  69.         return (maxBoundary != null) ? maxBoundary.getLocation() : Double.POSITIVE_INFINITY;
  70.     }

  71.     /**
  72.      * Get the {@link OrientedPoint} forming the minimum bounding hyperplane
  73.      * of the interval, or null if none exists. If present, This hyperplane
  74.      * is oriented to point in the negative direction.
  75.      * @return the hyperplane forming the minimum boundary of the interval or
  76.      *      null if no minimum boundary exists
  77.      */
  78.     public OrientedPoint getMinBoundary() {
  79.         return minBoundary;
  80.     }

  81.     /**
  82.      * Get the {@link OrientedPoint} forming the maximum bounding hyperplane
  83.      * of the interval, or null if none exists. If present, this hyperplane
  84.      * is oriented to point in the positive direction.
  85.      * @return the hyperplane forming the maximum boundary of the interval or
  86.      *      null if no maximum boundary exists
  87.      */
  88.     public OrientedPoint getMaxBoundary() {
  89.         return maxBoundary;
  90.     }

  91.     /** Return true if the interval has a minimum (lower) boundary.
  92.      * @return true if the interval has minimum (lower) boundary
  93.      */
  94.     public boolean hasMinBoundary() {
  95.         return minBoundary != null;
  96.     }

  97.     /** Return true if the interval has a maximum (upper) boundary.
  98.      * @return true if the interval has maximum (upper) boundary
  99.      */
  100.     public boolean hasMaxBoundary() {
  101.         return maxBoundary != null;
  102.     }

  103.     /** True if the region is infinite, meaning that at least one of the boundaries
  104.      * does not exist.
  105.      * @return true if the region is infinite
  106.      */
  107.     @Override
  108.     public boolean isInfinite() {
  109.         return minBoundary == null || maxBoundary == null;
  110.     }

  111.     /** True if the region is finite, meaning that both the minimum and maximum
  112.      * boundaries exist and the region size is finite.
  113.      * @return true if the region is finite
  114.      */
  115.     @Override
  116.     public boolean isFinite() {
  117.         return !isInfinite();
  118.     }

  119.     /** {@inheritDoc} */
  120.     @Override
  121.     public RegionLocation classify(final Vector1D pt) {
  122.         return classify(pt.getX());
  123.     }

  124.     /** Classify a point with respect to the interval.
  125.      * @param location the location to classify
  126.      * @return the classification of the point with respect to the interval
  127.      * @see #classify(Vector1D)
  128.      */
  129.     public RegionLocation classify(final double location) {
  130.         final RegionLocation minLoc = classifyWithBoundary(location, minBoundary);
  131.         final RegionLocation maxLoc = classifyWithBoundary(location, maxBoundary);

  132.         if (minLoc == RegionLocation.BOUNDARY || maxLoc == RegionLocation.BOUNDARY) {
  133.             return RegionLocation.BOUNDARY;
  134.         } else if (minLoc == RegionLocation.INSIDE && maxLoc == RegionLocation.INSIDE) {
  135.             return RegionLocation.INSIDE;
  136.         }
  137.         return RegionLocation.OUTSIDE;
  138.     }

  139.     /** Classify the location using the given interval boundary, which may be null.
  140.      * @param location the location to classify
  141.      * @param boundary interval boundary to classify against
  142.      * @return the location of the point relative to the boundary
  143.      */
  144.     private RegionLocation classifyWithBoundary(final double location, final OrientedPoint boundary) {
  145.         if (Double.isNaN(location)) {
  146.             return RegionLocation.OUTSIDE;
  147.         } else if (boundary == null) {
  148.             return RegionLocation.INSIDE;
  149.         } else {
  150.             final HyperplaneLocation hyperLoc = boundary.classify(location);

  151.             if (hyperLoc == HyperplaneLocation.ON) {
  152.                 return RegionLocation.BOUNDARY;
  153.             } else if (hyperLoc == HyperplaneLocation.PLUS) {
  154.                 return RegionLocation.OUTSIDE;
  155.             }
  156.             return RegionLocation.INSIDE;
  157.         }
  158.     }

  159.     /** Return true if the given point location is on the inside or boundary
  160.      * of the region.
  161.      * @param x the location to test
  162.      * @return true if the location is on the inside or boundary of the region
  163.      * @see #contains(org.apache.commons.geometry.core.Point)
  164.      */
  165.     public boolean contains(final double x) {
  166.         return classify(x) != RegionLocation.OUTSIDE;
  167.     }

  168.     /** {@inheritDoc}
  169.      *
  170.      * <p>The point is projected onto the nearest interval boundary. When a point
  171.      * is on the inside of the interval and is equidistant from both boundaries,
  172.      * then the minimum boundary is selected. when a point is on the outside of the
  173.      * interval and is equidistant from both boundaries (as is the case for intervals
  174.      * representing a single point), then the boundary facing the point is returned,
  175.      * ensuring that the returned offset is positive.
  176.      * </p>
  177.      */
  178.     @Override
  179.     public Vector1D project(final Vector1D pt) {

  180.         OrientedPoint boundary = null;

  181.         if (minBoundary != null && maxBoundary != null) {
  182.             // both boundaries are present; use the closest
  183.             final double minOffset = minBoundary.offset(pt.getX());
  184.             final double maxOffset = maxBoundary.offset(pt.getX());

  185.             final double minDist = Math.abs(minOffset);
  186.             final double maxDist = Math.abs(maxOffset);

  187.             // Project onto the max boundary if it's the closest or the point is on its plus side.
  188.             // Otherwise, project onto the min boundary.
  189.             if (maxDist < minDist || maxOffset > 0) {
  190.                 boundary = maxBoundary;
  191.             } else {
  192.                 boundary = minBoundary;
  193.             }
  194.         } else if (minBoundary != null) {
  195.             // only the min boundary is present
  196.             boundary = minBoundary;
  197.         } else if (maxBoundary != null) {
  198.             // only the max boundary is present
  199.             boundary = maxBoundary;
  200.         }

  201.         return (boundary != null) ? boundary.project(pt) : null;
  202.     }

  203.     /** Return a new instance transformed by the argument.
  204.      * @param transform transform to apply
  205.      * @return a new instance transformed by the argument
  206.      */
  207.     public Interval transform(final Transform<Vector1D> transform) {
  208.         final OrientedPoint transformedMin = (minBoundary != null) ?
  209.                 minBoundary.transform(transform) :
  210.                 null;
  211.         final OrientedPoint transformedMax = (maxBoundary != null) ?
  212.                 maxBoundary.transform(transform) :
  213.                 null;

  214.         return of(transformedMin, transformedMax);
  215.     }

  216.     /** {@inheritDoc}
  217.      *
  218.      *  <p>This method always returns false since there is always at least
  219.      *  one point that can be classified as not being on the outside of
  220.      *  the region.</p>
  221.      */
  222.     @Override
  223.     public boolean isEmpty() {
  224.         return false;
  225.     }

  226.     /** {@inheritDoc} */
  227.     @Override
  228.     public boolean isFull() {
  229.         return minBoundary == null && maxBoundary == null;
  230.     }

  231.     /** {@inheritDoc} */
  232.     @Override
  233.     public double getSize() {
  234.         if (isInfinite()) {
  235.             return Double.POSITIVE_INFINITY;
  236.         }

  237.         return getMax() - getMin();
  238.     }

  239.     /** {@inheritDoc}
  240.      *
  241.      *  <p>This method simply returns 0 because boundaries in one dimension do not
  242.      *  have any size.</p>
  243.      */
  244.     @Override
  245.     public double getBoundarySize() {
  246.         return 0;
  247.     }

  248.     /** {@inheritDoc} */
  249.     @Override
  250.     public Vector1D getCentroid() {
  251.         if (isInfinite()) {
  252.             return null;
  253.         }

  254.         final double min = getMin();
  255.         final double max = getMax();

  256.         return Vector1D.of((0.5 * (max - min)) + min);
  257.     }

  258.     /** {@inheritDoc} */
  259.     @Override
  260.     public Split<Interval> split(final Hyperplane<Vector1D> splitter) {
  261.         final OrientedPoint splitOrientedPoint = (OrientedPoint) splitter;
  262.         final Vector1D splitPoint = splitOrientedPoint.getPoint();

  263.         final HyperplaneLocation splitterMinLoc = (minBoundary != null) ? minBoundary.classify(splitPoint) : null;
  264.         final HyperplaneLocation splitterMaxLoc = (maxBoundary != null) ? maxBoundary.classify(splitPoint) : null;

  265.         Interval low = null;
  266.         Interval high = null;

  267.         if (splitterMinLoc != HyperplaneLocation.ON || splitterMaxLoc != HyperplaneLocation.ON) {

  268.             if (splitterMinLoc != null && splitterMinLoc != HyperplaneLocation.MINUS) {
  269.                 // splitter is on or below min boundary
  270.                 high = this;
  271.             } else if (splitterMaxLoc != null && splitterMaxLoc != HyperplaneLocation.MINUS) {
  272.                 // splitter is on or above max boundary
  273.                 low = this;
  274.             } else {
  275.                 // the interval is split in two
  276.                 low = new Interval(minBoundary, OrientedPoints.createPositiveFacing(
  277.                         splitPoint, splitOrientedPoint.getPrecision()));
  278.                 high = new Interval(OrientedPoints.createNegativeFacing(
  279.                         splitPoint, splitOrientedPoint.getPrecision()), maxBoundary);
  280.             }
  281.         }

  282.         // assign minus/plus based on the orientation of the splitter
  283.         final boolean lowIsMinus = splitOrientedPoint.isPositiveFacing();
  284.         final Interval minus = lowIsMinus ? low : high;
  285.         final Interval plus = lowIsMinus ? high : low;

  286.         return new Split<>(minus, plus);
  287.     }

  288.     /** Return a {@link RegionBSPTree1D} representing the same region as this instance.
  289.      * @return a BSP tree representing the same region
  290.      * @see RegionBSPTree1D#from(Interval, Interval...)
  291.      */
  292.     public RegionBSPTree1D toTree() {
  293.         return RegionBSPTree1D.from(this);
  294.     }

  295.     /** {@inheritDoc} */
  296.     @Override
  297.     public String toString() {
  298.         final StringBuilder sb = new StringBuilder();
  299.         sb.append(this.getClass().getSimpleName())
  300.             .append("[min= ")
  301.             .append(getMin())
  302.             .append(", max= ")
  303.             .append(getMax())
  304.             .append(']');

  305.         return sb.toString();
  306.     }

  307.     /** Create a new interval from the given point locations. The returned interval represents
  308.      * the region between the points, regardless of the order they are given as arguments.
  309.      * @param a first point location
  310.      * @param b second point location
  311.      * @param precision precision context used to compare floating point numbers
  312.      * @return a new interval representing the region between the given point locations
  313.      * @throws IllegalArgumentException if either number is {@link Double#NaN NaN} or the numbers
  314.      *      are both infinite and have the same sign
  315.      */
  316.     public static Interval of(final double a, final double b, final Precision.DoubleEquivalence precision) {
  317.         validateIntervalValues(a, b);

  318.         final double min = Math.min(a, b);
  319.         final double max = Math.max(a, b);

  320.         final OrientedPoint minBoundary = Double.isFinite(min) ?
  321.                 OrientedPoints.fromLocationAndDirection(min, false, precision) :
  322.                 null;

  323.         final OrientedPoint maxBoundary = Double.isFinite(max) ?
  324.                 OrientedPoints.fromLocationAndDirection(max, true, precision) :
  325.                 null;

  326.         if (minBoundary == null && maxBoundary == null) {
  327.             return FULL;
  328.         }

  329.         return new Interval(minBoundary, maxBoundary);
  330.     }

  331.     /** Create a new interval from the given points. The returned interval represents
  332.      * the region between the points, regardless of the order they are given as arguments.
  333.      * @param a first point
  334.      * @param b second point
  335.      * @param precision precision context used to compare floating point numbers
  336.      * @return a new interval representing the region between the given points
  337.      * @throws IllegalArgumentException if either point is {@link Vector1D#isNaN() NaN} or the points
  338.      *      are both {@link Vector1D#isInfinite() infinite} and have the same sign
  339.      */
  340.     public static Interval of(final Vector1D a, final Vector1D b, final Precision.DoubleEquivalence precision) {
  341.         return of(a.getX(), b.getX(), precision);
  342.     }

  343.     /** Create a new interval from the given hyperplanes. The hyperplanes may be given in
  344.      * any order but one must be positive-facing and the other negative-facing, with the positive-facing
  345.      * hyperplane located above the negative-facing hyperplane. Either or both argument may be null,
  346.      * in which case the returned interval will extend to infinity in the appropriate direction. If both
  347.      * arguments are null, an interval representing the full space is returned.
  348.      * @param a first hyperplane; may be null
  349.      * @param b second hyperplane; may be null
  350.      * @return a new interval representing the region between the given hyperplanes
  351.      * @throws IllegalArgumentException if the hyperplanes have the same orientation or
  352.      *      do not form an interval (for example, if the positive-facing hyperplane is below
  353.      *      the negative-facing hyperplane)
  354.      */
  355.     public static Interval of(final OrientedPoint a, final OrientedPoint b) {

  356.         validateBoundaryRelationship(a, b);

  357.         final boolean hasA = a != null;
  358.         final boolean hasB = b != null;

  359.         if (!hasA && !hasB) {
  360.             // both boundaries null; return the full space
  361.             return FULL;
  362.         }

  363.         // determine the ordering of the hyperplanes; we know that at least one is non-null
  364.         final OrientedPoint minBoundary = ((hasA && !a.isPositiveFacing()) || (hasB && b.isPositiveFacing())) ? a : b;
  365.         final OrientedPoint maxBoundary = ((hasA && a.isPositiveFacing()) || (hasB && !b.isPositiveFacing())) ? a : b;

  366.         // validate the boundary locations; this will ensure that we don't have NaN values
  367.         final double minLoc = (minBoundary != null) ? minBoundary.getLocation() : Double.NEGATIVE_INFINITY;
  368.         final double maxLoc = (maxBoundary != null) ? maxBoundary.getLocation() : Double.POSITIVE_INFINITY;

  369.         validateIntervalValues(minLoc, maxLoc);

  370.         // create the interval, replacing infinites with nulls
  371.         return new Interval(
  372.                 Double.isFinite(minLoc) ? minBoundary : null,
  373.                 Double.isFinite(maxLoc) ? maxBoundary : null);
  374.     }

  375.     /** Return an interval with the given min value and no max.
  376.      * @param min min value for the interval
  377.      * @param precision precision context used to compare floating point numbers
  378.      * @return an interval with the given min value and no max.
  379.      */
  380.     public static Interval min(final double min, final Precision.DoubleEquivalence precision) {
  381.         return of(min, Double.POSITIVE_INFINITY, precision);
  382.     }

  383.     /** Return an interval with the given max value and no min.
  384.      * @param max max value for the interval
  385.      * @param precision precision context used to compare floating point numbers
  386.      * @return an interval with the given max value and no min.
  387.      */
  388.     public static Interval max(final double max, final Precision.DoubleEquivalence precision) {
  389.         return of(Double.NEGATIVE_INFINITY, max, precision);
  390.     }

  391.     /** Return an interval representing a single point at the given location.
  392.      * @param location the location of the interval
  393.      * @param precision precision context used to compare floating point numbers
  394.      * @return an interval representing a single point
  395.      */
  396.     public static Interval point(final double location, final Precision.DoubleEquivalence precision) {
  397.         return of(location, location, precision);
  398.     }

  399.     /** Return an interval representing the entire real number line. The {@link #isFull()}
  400.      * method of the instance will return true.
  401.      * @return an interval representing the entire real number line
  402.      * @see #isFull()
  403.      */
  404.     public static Interval full() {
  405.         return FULL;
  406.     }

  407.     /** Validate that the orientations and positions of the arguments may be used to create an interval.
  408.      * The arguments may be given in any order. Does nothing if one or both arguments are null.
  409.      * @param a first boundary; may be null
  410.      * @param b second boundary may be null
  411.      * @throws IllegalArgumentException is {@code a} and {@code b} have the same orientation or one does
  412.      *      not lie on the plus side of the other.
  413.      */
  414.     private static void validateBoundaryRelationship(final OrientedPoint a, final OrientedPoint b) {
  415.         if (a != null && b != null) {
  416.             if (a.isPositiveFacing() == b.isPositiveFacing()) {
  417.                 throw new IllegalArgumentException(
  418.                         MessageFormat.format("Invalid interval: hyperplanes have same orientation: {0}, {1}", a, b));
  419.             }

  420.             if (a.classify(b.getPoint()) == HyperplaneLocation.PLUS ||
  421.                     b.classify(a.getPoint()) == HyperplaneLocation.PLUS) {
  422.                 throw new IllegalArgumentException(
  423.                         MessageFormat.format("Invalid interval: hyperplanes do not form interval: {0}, {1}", a, b));
  424.             }
  425.         }
  426.     }

  427.     /** Validate that the given value can be used to construct an interval. The values
  428.      * must not be NaN and if infinite, must have opposite signs.
  429.      * @param a first value
  430.      * @param b second value
  431.      * @throws IllegalArgumentException if either value is NaN or if both values are infinite
  432.      *      and have the same sign
  433.      */
  434.     private static void validateIntervalValues(final double a, final double b) {
  435.         if (Double.isNaN(a) || Double.isNaN(b) ||
  436.                 (Double.isInfinite(a) && Double.compare(a, b) == 0)) {

  437.             throw new IllegalArgumentException(
  438.                     MessageFormat.format("Invalid interval values: [{0}, {1}]", a, b));
  439.         }
  440.     }
  441. }