All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.geometry.euclidean.oned.Interval Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.geometry.euclidean.oned;

import java.text.MessageFormat;

import org.apache.commons.geometry.core.RegionLocation;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
import org.apache.commons.geometry.core.partitioning.Split;
import org.apache.commons.numbers.core.Precision;

/** Class representing an interval in one dimension. The interval is defined
 * by minimum and maximum values. One or both of these values may be infinite
 * although not with the same sign.
 *
 * 

Instances of this class are guaranteed to be immutable.

*/ public final class Interval implements HyperplaneBoundedRegion { /** Interval instance representing the entire real number line. */ private static final Interval FULL = new Interval(null, null); /** {@link OrientedPoint} instance representing the min boundary of the interval, * or null if no min boundary exists. If present, this instance will be negative-facing. * Infinite values are allowed but not NaN. */ private final OrientedPoint minBoundary; /** {@link OrientedPoint} instance representing the max boundary of the interval, * or null if no max boundary exists. If present, this instance will be negative-facing. * Infinite values are allowed but not NaN. */ private final OrientedPoint maxBoundary; /** Create an instance from min and max bounding hyperplanes. No validation is performed. * Callers are responsible for ensuring that the given hyperplanes represent a valid * interval. * @param minBoundary the min (negative-facing) hyperplane * @param maxBoundary the max (positive-facing) hyperplane */ private Interval(final OrientedPoint minBoundary, final OrientedPoint maxBoundary) { this.minBoundary = minBoundary; this.maxBoundary = maxBoundary; } /** Get the minimum value for the interval or {@link Double#NEGATIVE_INFINITY} * if no minimum value exists. * @return the minimum value for the interval or {@link Double#NEGATIVE_INFINITY} * if no minimum value exists. */ public double getMin() { return (minBoundary != null) ? minBoundary.getLocation() : Double.NEGATIVE_INFINITY; } /** Get the maximum value for the interval or {@link Double#POSITIVE_INFINITY} * if no maximum value exists. * @return the maximum value for the interval or {@link Double#POSITIVE_INFINITY} * if no maximum value exists. */ public double getMax() { return (maxBoundary != null) ? maxBoundary.getLocation() : Double.POSITIVE_INFINITY; } /** * Get the {@link OrientedPoint} forming the minimum bounding hyperplane * of the interval, or null if none exists. If present, This hyperplane * is oriented to point in the negative direction. * @return the hyperplane forming the minimum boundary of the interval or * null if no minimum boundary exists */ public OrientedPoint getMinBoundary() { return minBoundary; } /** * Get the {@link OrientedPoint} forming the maximum bounding hyperplane * of the interval, or null if none exists. If present, this hyperplane * is oriented to point in the positive direction. * @return the hyperplane forming the maximum boundary of the interval or * null if no maximum boundary exists */ public OrientedPoint getMaxBoundary() { return maxBoundary; } /** Return true if the interval has a minimum (lower) boundary. * @return true if the interval has minimum (lower) boundary */ public boolean hasMinBoundary() { return minBoundary != null; } /** Return true if the interval has a maximum (upper) boundary. * @return true if the interval has maximum (upper) boundary */ public boolean hasMaxBoundary() { return maxBoundary != null; } /** True if the region is infinite, meaning that at least one of the boundaries * does not exist. * @return true if the region is infinite */ @Override public boolean isInfinite() { return minBoundary == null || maxBoundary == null; } /** True if the region is finite, meaning that both the minimum and maximum * boundaries exist and the region size is finite. * @return true if the region is finite */ @Override public boolean isFinite() { return !isInfinite(); } /** {@inheritDoc} */ @Override public RegionLocation classify(final Vector1D pt) { return classify(pt.getX()); } /** Classify a point with respect to the interval. * @param location the location to classify * @return the classification of the point with respect to the interval * @see #classify(Vector1D) */ public RegionLocation classify(final double location) { final RegionLocation minLoc = classifyWithBoundary(location, minBoundary); final RegionLocation maxLoc = classifyWithBoundary(location, maxBoundary); if (minLoc == RegionLocation.BOUNDARY || maxLoc == RegionLocation.BOUNDARY) { return RegionLocation.BOUNDARY; } else if (minLoc == RegionLocation.INSIDE && maxLoc == RegionLocation.INSIDE) { return RegionLocation.INSIDE; } return RegionLocation.OUTSIDE; } /** Classify the location using the given interval boundary, which may be null. * @param location the location to classify * @param boundary interval boundary to classify against * @return the location of the point relative to the boundary */ private RegionLocation classifyWithBoundary(final double location, final OrientedPoint boundary) { if (Double.isNaN(location)) { return RegionLocation.OUTSIDE; } else if (boundary == null) { return RegionLocation.INSIDE; } else { final HyperplaneLocation hyperLoc = boundary.classify(location); if (hyperLoc == HyperplaneLocation.ON) { return RegionLocation.BOUNDARY; } else if (hyperLoc == HyperplaneLocation.PLUS) { return RegionLocation.OUTSIDE; } return RegionLocation.INSIDE; } } /** Return true if the given point location is on the inside or boundary * of the region. * @param x the location to test * @return true if the location is on the inside or boundary of the region * @see #contains(org.apache.commons.geometry.core.Point) */ public boolean contains(final double x) { return classify(x) != RegionLocation.OUTSIDE; } /** {@inheritDoc} * *

The point is projected onto the nearest interval boundary. When a point * is on the inside of the interval and is equidistant from both boundaries, * then the minimum boundary is selected. when a point is on the outside of the * interval and is equidistant from both boundaries (as is the case for intervals * representing a single point), then the boundary facing the point is returned, * ensuring that the returned offset is positive. *

*/ @Override public Vector1D project(final Vector1D pt) { OrientedPoint boundary = null; if (minBoundary != null && maxBoundary != null) { // both boundaries are present; use the closest final double minOffset = minBoundary.offset(pt.getX()); final double maxOffset = maxBoundary.offset(pt.getX()); final double minDist = Math.abs(minOffset); final double maxDist = Math.abs(maxOffset); // Project onto the max boundary if it's the closest or the point is on its plus side. // Otherwise, project onto the min boundary. if (maxDist < minDist || maxOffset > 0) { boundary = maxBoundary; } else { boundary = minBoundary; } } else if (minBoundary != null) { // only the min boundary is present boundary = minBoundary; } else if (maxBoundary != null) { // only the max boundary is present boundary = maxBoundary; } return (boundary != null) ? boundary.project(pt) : null; } /** Return a new instance transformed by the argument. * @param transform transform to apply * @return a new instance transformed by the argument */ public Interval transform(final Transform transform) { final OrientedPoint transformedMin = (minBoundary != null) ? minBoundary.transform(transform) : null; final OrientedPoint transformedMax = (maxBoundary != null) ? maxBoundary.transform(transform) : null; return of(transformedMin, transformedMax); } /** {@inheritDoc} * *

This method always returns false since there is always at least * one point that can be classified as not being on the outside of * the region.

*/ @Override public boolean isEmpty() { return false; } /** {@inheritDoc} */ @Override public boolean isFull() { return minBoundary == null && maxBoundary == null; } /** {@inheritDoc} */ @Override public double getSize() { if (isInfinite()) { return Double.POSITIVE_INFINITY; } return getMax() - getMin(); } /** {@inheritDoc} * *

This method simply returns 0 because boundaries in one dimension do not * have any size.

*/ @Override public double getBoundarySize() { return 0; } /** {@inheritDoc} */ @Override public Vector1D getCentroid() { if (isInfinite()) { return null; } final double min = getMin(); final double max = getMax(); return Vector1D.of((0.5 * (max - min)) + min); } /** {@inheritDoc} */ @Override public Split split(final Hyperplane splitter) { final OrientedPoint splitOrientedPoint = (OrientedPoint) splitter; final Vector1D splitPoint = splitOrientedPoint.getPoint(); final HyperplaneLocation splitterMinLoc = (minBoundary != null) ? minBoundary.classify(splitPoint) : null; final HyperplaneLocation splitterMaxLoc = (maxBoundary != null) ? maxBoundary.classify(splitPoint) : null; Interval low = null; Interval high = null; if (splitterMinLoc != HyperplaneLocation.ON || splitterMaxLoc != HyperplaneLocation.ON) { if (splitterMinLoc != null && splitterMinLoc != HyperplaneLocation.MINUS) { // splitter is on or below min boundary high = this; } else if (splitterMaxLoc != null && splitterMaxLoc != HyperplaneLocation.MINUS) { // splitter is on or above max boundary low = this; } else { // the interval is split in two low = new Interval(minBoundary, OrientedPoints.createPositiveFacing( splitPoint, splitOrientedPoint.getPrecision())); high = new Interval(OrientedPoints.createNegativeFacing( splitPoint, splitOrientedPoint.getPrecision()), maxBoundary); } } // assign minus/plus based on the orientation of the splitter final boolean lowIsMinus = splitOrientedPoint.isPositiveFacing(); final Interval minus = lowIsMinus ? low : high; final Interval plus = lowIsMinus ? high : low; return new Split<>(minus, plus); } /** Return a {@link RegionBSPTree1D} representing the same region as this instance. * @return a BSP tree representing the same region * @see RegionBSPTree1D#from(Interval, Interval...) */ public RegionBSPTree1D toTree() { return RegionBSPTree1D.from(this); } /** {@inheritDoc} */ @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append(this.getClass().getSimpleName()) .append("[min= ") .append(getMin()) .append(", max= ") .append(getMax()) .append(']'); return sb.toString(); } /** Create a new interval from the given point locations. The returned interval represents * the region between the points, regardless of the order they are given as arguments. * @param a first point location * @param b second point location * @param precision precision context used to compare floating point numbers * @return a new interval representing the region between the given point locations * @throws IllegalArgumentException if either number is {@link Double#NaN NaN} or the numbers * are both infinite and have the same sign */ public static Interval of(final double a, final double b, final Precision.DoubleEquivalence precision) { validateIntervalValues(a, b); final double min = Math.min(a, b); final double max = Math.max(a, b); final OrientedPoint minBoundary = Double.isFinite(min) ? OrientedPoints.fromLocationAndDirection(min, false, precision) : null; final OrientedPoint maxBoundary = Double.isFinite(max) ? OrientedPoints.fromLocationAndDirection(max, true, precision) : null; if (minBoundary == null && maxBoundary == null) { return FULL; } return new Interval(minBoundary, maxBoundary); } /** Create a new interval from the given points. The returned interval represents * the region between the points, regardless of the order they are given as arguments. * @param a first point * @param b second point * @param precision precision context used to compare floating point numbers * @return a new interval representing the region between the given points * @throws IllegalArgumentException if either point is {@link Vector1D#isNaN() NaN} or the points * are both {@link Vector1D#isInfinite() infinite} and have the same sign */ public static Interval of(final Vector1D a, final Vector1D b, final Precision.DoubleEquivalence precision) { return of(a.getX(), b.getX(), precision); } /** Create a new interval from the given hyperplanes. The hyperplanes may be given in * any order but one must be positive-facing and the other negative-facing, with the positive-facing * hyperplane located above the negative-facing hyperplane. Either or both argument may be null, * in which case the returned interval will extend to infinity in the appropriate direction. If both * arguments are null, an interval representing the full space is returned. * @param a first hyperplane; may be null * @param b second hyperplane; may be null * @return a new interval representing the region between the given hyperplanes * @throws IllegalArgumentException if the hyperplanes have the same orientation or * do not form an interval (for example, if the positive-facing hyperplane is below * the negative-facing hyperplane) */ public static Interval of(final OrientedPoint a, final OrientedPoint b) { validateBoundaryRelationship(a, b); final boolean hasA = a != null; final boolean hasB = b != null; if (!hasA && !hasB) { // both boundaries null; return the full space return FULL; } // determine the ordering of the hyperplanes; we know that at least one is non-null final OrientedPoint minBoundary = ((hasA && !a.isPositiveFacing()) || (hasB && b.isPositiveFacing())) ? a : b; final OrientedPoint maxBoundary = ((hasA && a.isPositiveFacing()) || (hasB && !b.isPositiveFacing())) ? a : b; // validate the boundary locations; this will ensure that we don't have NaN values final double minLoc = (minBoundary != null) ? minBoundary.getLocation() : Double.NEGATIVE_INFINITY; final double maxLoc = (maxBoundary != null) ? maxBoundary.getLocation() : Double.POSITIVE_INFINITY; validateIntervalValues(minLoc, maxLoc); // create the interval, replacing infinites with nulls return new Interval( Double.isFinite(minLoc) ? minBoundary : null, Double.isFinite(maxLoc) ? maxBoundary : null); } /** Return an interval with the given min value and no max. * @param min min value for the interval * @param precision precision context used to compare floating point numbers * @return an interval with the given min value and no max. */ public static Interval min(final double min, final Precision.DoubleEquivalence precision) { return of(min, Double.POSITIVE_INFINITY, precision); } /** Return an interval with the given max value and no min. * @param max max value for the interval * @param precision precision context used to compare floating point numbers * @return an interval with the given max value and no min. */ public static Interval max(final double max, final Precision.DoubleEquivalence precision) { return of(Double.NEGATIVE_INFINITY, max, precision); } /** Return an interval representing a single point at the given location. * @param location the location of the interval * @param precision precision context used to compare floating point numbers * @return an interval representing a single point */ public static Interval point(final double location, final Precision.DoubleEquivalence precision) { return of(location, location, precision); } /** Return an interval representing the entire real number line. The {@link #isFull()} * method of the instance will return true. * @return an interval representing the entire real number line * @see #isFull() */ public static Interval full() { return FULL; } /** Validate that the orientations and positions of the arguments may be used to create an interval. * The arguments may be given in any order. Does nothing if one or both arguments are null. * @param a first boundary; may be null * @param b second boundary may be null * @throws IllegalArgumentException is {@code a} and {@code b} have the same orientation or one does * not lie on the plus side of the other. */ private static void validateBoundaryRelationship(final OrientedPoint a, final OrientedPoint b) { if (a != null && b != null) { if (a.isPositiveFacing() == b.isPositiveFacing()) { throw new IllegalArgumentException( MessageFormat.format("Invalid interval: hyperplanes have same orientation: {0}, {1}", a, b)); } if (a.classify(b.getPoint()) == HyperplaneLocation.PLUS || b.classify(a.getPoint()) == HyperplaneLocation.PLUS) { throw new IllegalArgumentException( MessageFormat.format("Invalid interval: hyperplanes do not form interval: {0}, {1}", a, b)); } } } /** Validate that the given value can be used to construct an interval. The values * must not be NaN and if infinite, must have opposite signs. * @param a first value * @param b second value * @throws IllegalArgumentException if either value is NaN or if both values are infinite * and have the same sign */ private static void validateIntervalValues(final double a, final double b) { if (Double.isNaN(a) || Double.isNaN(b) || (Double.isInfinite(a) && Double.compare(a, b) == 0)) { throw new IllegalArgumentException( MessageFormat.format("Invalid interval values: [{0}, {1}]", a, b)); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy