| /* |
| * 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.spherical.oned; |
| |
| import java.text.MessageFormat; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.function.BiFunction; |
| |
| import org.apache.commons.geometry.core.Geometry; |
| import org.apache.commons.geometry.core.RegionLocation; |
| import org.apache.commons.geometry.core.Transform; |
| import org.apache.commons.geometry.core.exception.GeometryException; |
| 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.geometry.core.precision.DoublePrecisionContext; |
| |
| /** Class representing an angular interval of size greater than zero to {@code 2pi}. The interval is |
| * defined by two azimuth angles: a min and a max. The interval starts at the min azimuth angle and |
| * contains all points in the direction of increasing azimuth angles up to max. |
| * |
| * <p>Instances of this class are guaranteed to be immutable.</p> |
| */ |
| public class AngularInterval implements HyperplaneBoundedRegion<Point1S> { |
| /** The minimum boundary of the interval. */ |
| private final CutAngle minBoundary; |
| |
| /** The maximum boundary of the interval. */ |
| private final CutAngle maxBoundary; |
| |
| /** Point halfway between the min and max boundaries. */ |
| private final Point1S midpoint; |
| |
| /** Construct a new instance representing the angular region between the given |
| * min and max azimuth boundaries. The arguments must be either all finite or all |
| * null (to indicate the full space). If the boundaries are finite, then the min |
| * boundary azimuth value must be numerically less than the max boundary. Callers are |
| * responsible for enforcing these constraints. No validation is performed. |
| * @param minBoundary minimum boundary for the interval |
| * @param maxBoundary maximum boundary for the interval |
| */ |
| private AngularInterval(final CutAngle minBoundary, final CutAngle maxBoundary) { |
| |
| this.minBoundary = minBoundary; |
| this.maxBoundary = maxBoundary; |
| this.midpoint = (minBoundary != null && maxBoundary != null) ? |
| Point1S.of(0.5 * (minBoundary.getAzimuth() + maxBoundary.getAzimuth())) : |
| null; |
| } |
| |
| /** Get the minimum azimuth angle for the interval, or {@code 0} |
| * if the interval is full. |
| * @return the minimum azimuth angle for the interval or {@code 0} |
| * if the interval represents the full space. |
| */ |
| public double getMin() { |
| return (minBoundary != null) ? |
| minBoundary.getAzimuth() : |
| Geometry.ZERO_PI; |
| } |
| |
| /** Get the minimum boundary for the interval, or null if the |
| * interval represents the full space. |
| * @return the minimum point for the interval or null if |
| * the interval represents the full space |
| */ |
| public CutAngle getMinBoundary() { |
| return minBoundary; |
| } |
| |
| /** Get the maximum azimuth angle for the interval, or {@code 2pi} if |
| * the interval represents the full space. |
| * @return the maximum azimuth angle for the interval or {@code 2pi} if |
| * the interval represents the full space. |
| */ |
| public double getMax() { |
| return (maxBoundary != null) ? |
| maxBoundary.getAzimuth() : |
| Geometry.TWO_PI; |
| } |
| |
| /** Get the maximum point for the interval. This will be null if the |
| * interval represents the full space. |
| * @return the maximum point for the interval or null if |
| * the interval represents the full space |
| */ |
| public CutAngle getMaxBoundary() { |
| return maxBoundary; |
| } |
| |
| /** Get the midpoint of the interval or null if the interval represents |
| * the full space. |
| * @return the midpoint of the interval or null if the interval represents |
| * the full space |
| * @see #getBarycenter() |
| */ |
| public Point1S getMidPoint() { |
| return midpoint; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public boolean isFull() { |
| // minBoundary and maxBoundary are either both null or both not null |
| return minBoundary == null; |
| } |
| |
| /** {@inheritDoc} |
| * |
| * <p>This method always returns false.</p> |
| */ |
| @Override |
| public boolean isEmpty() { |
| return false; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public double getSize() { |
| return getMax() - getMin(); |
| } |
| |
| /** {@inheritDoc} |
| * |
| * <p>This method simply returns 0 because boundaries in one dimension do not |
| * have any size.</p> |
| */ |
| @Override |
| public double getBoundarySize() { |
| return 0; |
| } |
| |
| /** {@inheritDoc} |
| * |
| * <p>This method is an alias for {@link #getMidPoint()}.</p> |
| * @see #getMidPoint() |
| */ |
| @Override |
| public Point1S getBarycenter() { |
| return getMidPoint(); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public RegionLocation classify(final Point1S pt) { |
| if (!isFull()) { |
| final HyperplaneLocation minLoc = minBoundary.classify(pt); |
| final HyperplaneLocation maxLoc = maxBoundary.classify(pt); |
| |
| final boolean wraps = wrapsZero(); |
| |
| if ((!wraps && (minLoc == HyperplaneLocation.PLUS || maxLoc == HyperplaneLocation.PLUS)) || |
| (wraps && minLoc == HyperplaneLocation.PLUS && maxLoc == HyperplaneLocation.PLUS)) { |
| return RegionLocation.OUTSIDE; |
| } else if (minLoc == HyperplaneLocation.ON || maxLoc == HyperplaneLocation.ON) { |
| return RegionLocation.BOUNDARY; |
| } |
| } |
| return RegionLocation.INSIDE; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Point1S project(final Point1S pt) { |
| if (!isFull()) { |
| final double minDist = minBoundary.getPoint().distance(pt); |
| final double maxDist = maxBoundary.getPoint().distance(pt); |
| |
| return (minDist <= maxDist) ? |
| minBoundary.getPoint() : |
| maxBoundary.getPoint(); |
| } |
| return null; |
| } |
| |
| /** Return true if the interval wraps around the zero/{@code 2pi} point. In this |
| * case, the max boundary azimuth is less than that of the min boundary when both |
| * values are normalized to the range {@code [0, 2pi)}. |
| * @return true if the interval wraps around the zero/{@code 2pi} point |
| */ |
| public boolean wrapsZero() { |
| if (!isFull()) { |
| final double minNormAz = minBoundary.getPoint().getNormalizedAzimuth(); |
| final double maxNormAz = maxBoundary.getPoint().getNormalizedAzimuth(); |
| |
| return maxNormAz < minNormAz; |
| } |
| return false; |
| } |
| |
| /** Return a new instance transformed by the argument. If the transformed size |
| * of the interval is greater than or equal to 2pi, then an interval representing |
| * the full space is returned. |
| * @param transform transform to apply |
| * @return a new instance transformed by the argument |
| */ |
| public AngularInterval transform(final Transform<Point1S> transform) { |
| return AngularInterval.transform(this, transform, AngularInterval::of); |
| } |
| |
| /** {@inheritDoc} |
| * |
| * <p>This method returns instances of {@link RegionBSPTree1S} instead of |
| * {@link AngularInterval} since it is possible for a convex angular interval |
| * to be split into disjoint regions by a single hyperplane. These disjoint |
| * regions cannot be represented by this class and require the use of a BSP |
| * tree.</p> |
| * |
| * @see RegionBSPTree1S#split(Hyperplane) |
| */ |
| @Override |
| public Split<RegionBSPTree1S> split(final Hyperplane<Point1S> splitter) { |
| return toTree().split(splitter); |
| } |
| |
| /** Return a {@link RegionBSPTree1S} instance representing the same region |
| * as this instance. |
| * @return a BSP tree representing the same region as this instance |
| */ |
| public RegionBSPTree1S toTree() { |
| return RegionBSPTree1S.fromInterval(this); |
| } |
| |
| /** Return a list of convex intervals comprising this region. |
| * @return a list of convex intervals comprising this region |
| * @see Convex |
| */ |
| public List<AngularInterval.Convex> toConvex() { |
| if (isConvex(minBoundary, maxBoundary)) { |
| return Collections.singletonList(new Convex(minBoundary, maxBoundary)); |
| } |
| |
| final CutAngle midPos = CutAngle.createPositiveFacing(midpoint, minBoundary.getPrecision()); |
| final CutAngle midNeg = CutAngle.createNegativeFacing(midpoint, maxBoundary.getPrecision()); |
| |
| return Arrays.asList( |
| new Convex(minBoundary, midPos), |
| new Convex(midNeg, maxBoundary) |
| ); |
| } |
| |
| /** {@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(); |
| } |
| |
| /** Return an instance representing the full space. The returned instance contains all |
| * possible azimuth angles. |
| * @return an interval representing the full space |
| */ |
| public static AngularInterval.Convex full() { |
| return Convex.FULL; |
| } |
| |
| /** Return an instance representing the angular interval between the given min and max azimuth |
| * values. The max value is adjusted to be numerically above the min value, even if the resulting |
| * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space |
| * is returned if either point is infinite or min and max are equivalent as evaluated by the |
| * given precision context. |
| * @param min min azimuth value |
| * @param max max azimuth value |
| * @param precision precision precision context used to compare floating point values |
| * @return a new instance resulting the angular region between the given min and max azimuths |
| * @throws GeometryException if either azimuth is infinite or NaN |
| */ |
| public static AngularInterval of(final double min, final double max, final DoublePrecisionContext precision) { |
| return of(Point1S.of(min), Point1S.of(max), precision); |
| } |
| |
| /** Return an instance representing the angular interval between the given min and max azimuth |
| * points. The max point is adjusted to be numerically above the min point, even if the resulting |
| * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space |
| * is returned if either point is infinite or min and max are equivalent as evaluated by the |
| * given precision context. |
| * @param min min azimuth value |
| * @param max max azimuth value |
| * @param precision precision precision context used to compare floating point values |
| * @return a new instance resulting the angular region between the given min and max points |
| * @throws GeometryException if either azimuth is infinite or NaN |
| */ |
| public static AngularInterval of(final Point1S min, final Point1S max, final DoublePrecisionContext precision) { |
| return createInterval(min, max, precision, AngularInterval::new, Convex.FULL); |
| } |
| |
| /** Return an instance representing the angular interval between the given oriented points. |
| * The negative-facing point is used as the minimum boundary and the positive-facing point is |
| * adjusted to be above the minimum. The arguments can be given in any order. The full space |
| * is returned if the points are equivalent or are oriented in the same direction. |
| * @param a first oriented point |
| * @param b second oriented point |
| * @return an instance representing the angular interval between the given oriented points |
| * @throws GeometryException if either argument is infinite or NaN |
| */ |
| public static AngularInterval of(final CutAngle a, final CutAngle b) { |
| return createInterval(a, b, AngularInterval::new, Convex.FULL); |
| } |
| |
| /** Internal method to create an interval between the given min and max points. The max point |
| * is adjusted to be numerically above the min point, even if the resulting |
| * azimuth value is greater than or equal to {@code 2pi}. The full instance argument |
| * is returned if either point is infinite or min and max are equivalent as evaluated by the |
| * given precision context. |
| * @param min min azimuth value |
| * @param max max azimuth value |
| * @param precision precision precision context used to compare floating point values |
| * @param factory factory object used to create new instances; this object is passed the validated |
| * min (negative-facing) cut and the max (positive-facing) cut, in that order |
| * @param <T> Angular interval implementation type |
| * @param fullSpace instance returned if the interval should represent the full space |
| * @return a new instance resulting the angular region between the given min and max points |
| * @throws GeometryException if either azimuth is infinite or NaN |
| */ |
| private static <T extends AngularInterval> T createInterval(final Point1S min, final Point1S max, |
| final DoublePrecisionContext precision, final BiFunction<CutAngle, CutAngle, T> factory, |
| final T fullSpace) { |
| |
| validateIntervalValues(min, max); |
| |
| // return the full space if either point is infinite or the points are equivalent |
| if (min.eq(max, precision)) { |
| return fullSpace; |
| } |
| |
| final Point1S adjustedMax = max.above(min); |
| |
| return factory.apply( |
| CutAngle.createNegativeFacing(min, precision), |
| CutAngle.createPositiveFacing(adjustedMax, precision) |
| ); |
| } |
| |
| /** Internal method to create a new interval instance from the given cut angles. |
| * The negative-facing point is used as the minimum boundary and the positive-facing point is |
| * adjusted to be above the minimum. The arguments can be given in any order. The full space |
| * argument is returned if the points are equivalent or are oriented in the same direction. |
| * @param a first cut point |
| * @param b second cut point |
| * @param factory factory object used to create new instances; this object is passed the validated |
| * min (negative-facing) cut and the max (positive-facing) cut, in that order |
| * @param fullSpace instance returned if the interval should represent the full space |
| * @param <T> Angular interval implementation type |
| * @return a new interval instance created from the given cut angles |
| * @throws GeometryException if either argument is infinite or NaN |
| */ |
| private static <T extends AngularInterval> T createInterval(final CutAngle a, final CutAngle b, |
| final BiFunction<CutAngle, CutAngle, T> factory, final T fullSpace) { |
| |
| final Point1S aPoint = a.getPoint(); |
| final Point1S bPoint = b.getPoint(); |
| |
| validateIntervalValues(aPoint, bPoint); |
| |
| if (a.isPositiveFacing() == b.isPositiveFacing() || |
| aPoint.eq(bPoint, a.getPrecision()) || |
| bPoint.eq(aPoint, b.getPrecision())) { |
| // points are equivalent or facing in the same direction |
| return fullSpace; |
| } |
| |
| final CutAngle min = a.isPositiveFacing() ? b : a; |
| final CutAngle max = a.isPositiveFacing() ? a : b; |
| final CutAngle adjustedMax = CutAngle.createPositiveFacing( |
| max.getPoint().above(min.getPoint()), |
| max.getPrecision()); |
| |
| return factory.apply(min, adjustedMax); |
| } |
| |
| /** Validate that the given points can be used to specify an angular interval. |
| * @param a first point |
| * @param b second point |
| * @throws GeometryException if either point is infinite NaN |
| */ |
| private static void validateIntervalValues(final Point1S a, final Point1S b) { |
| if (!a.isFinite() || !b.isFinite()) { |
| throw new GeometryException(MessageFormat.format("Invalid angular interval: [{0}, {1}]", |
| a.getAzimuth(), b.getAzimuth())); |
| } |
| } |
| |
| /** Return true if the given cut angles define a convex region. By convention, the |
| * precision context from the min cut is used for the floating point comparison. |
| * @param min min (negative-facing) cut angle |
| * @param max max (positive-facing) cut angle |
| * @return true if the given cut angles define a convex region |
| */ |
| private static boolean isConvex(final CutAngle min, final CutAngle max) { |
| if (min != null && max != null) { |
| final double dist = max.getAzimuth() - min.getAzimuth(); |
| final DoublePrecisionContext precision = min.getPrecision(); |
| return precision.lte(dist, Geometry.PI); |
| } |
| |
| return true; |
| } |
| |
| /** Internal transform method that transforms the given instance, using the factory |
| * method to create a new instance if needed. |
| * @param interval interval to transform |
| * @param transform transform to apply |
| * @param factory object used to create new instances |
| * @param <T> Angular interval implementation type |
| * @return a transformed instance |
| */ |
| private static <T extends AngularInterval> T transform(final T interval, |
| final Transform<Point1S> transform, |
| final BiFunction<CutAngle, CutAngle, T> factory) { |
| |
| if (!interval.isFull()) { |
| final CutAngle tMin = interval.getMinBoundary().transform(transform); |
| final CutAngle tMax = interval.getMaxBoundary().transform(transform); |
| |
| return factory.apply(tMin, tMax); |
| } |
| |
| return interval; |
| } |
| |
| /** Class representing an angular interval with the additional property that the |
| * region is convex. By convex, it is meant that the shortest path between any |
| * two points in the region is also contained entirely in the region. If there is |
| * a tie for shortest path, then it is sufficient that at least one lie entirely |
| * within the region. For spherical 1D space, this means that the angular interval |
| * is either completely full or has a length less than or equal to {@code pi}. |
| */ |
| public static final class Convex extends AngularInterval { |
| /** Interval instance representing the full space. */ |
| private static final Convex FULL = new Convex(null, null); |
| |
| /** Construct a new convex instance from its boundaries and midpoint. No validation |
| * of the argument is performed. Callers are responsible for ensuring that the size |
| * of interval is less than or equal to {@code pi}. |
| * @param minBoundary minimum boundary for the interval |
| * @param maxBoundary maximum boundary for the interval |
| * @throws GeometryException if the interval is not convex |
| */ |
| private Convex(final CutAngle minBoundary, final CutAngle maxBoundary) { |
| super(minBoundary, maxBoundary); |
| |
| if (!isConvex(minBoundary, maxBoundary)) { |
| throw new GeometryException(MessageFormat.format("Interval is not convex: [{0}, {1}]", |
| minBoundary.getAzimuth(), maxBoundary.getAzimuth())); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public List<AngularInterval.Convex> toConvex() { |
| return Collections.singletonList(this); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Convex transform(final Transform<Point1S> transform) { |
| return AngularInterval.transform(this, transform, Convex::of); |
| } |
| |
| /** Split the instance along a circle diameter.The diameter is defined by the given split point and |
| * its reversed antipodal point. |
| * @param splitter split point defining one side of the split diameter |
| * @return result of the split operation |
| */ |
| public Split<Convex> splitDiameter(final CutAngle splitter) { |
| |
| final CutAngle opposite = CutAngle.fromPointAndDirection( |
| splitter.getPoint().antipodal(), |
| !splitter.isPositiveFacing(), |
| splitter.getPrecision()); |
| |
| if (isFull()) { |
| final Convex minus = Convex.of(splitter, opposite); |
| final Convex plus = Convex.of(splitter.reverse(), opposite.reverse()); |
| |
| return new Split<>(minus, plus); |
| } |
| |
| final CutAngle minBoundary = getMinBoundary(); |
| final CutAngle maxBoundary = getMaxBoundary(); |
| |
| final Point1S posPole = Point1S.of(splitter.getPoint().getAzimuth() + Geometry.HALF_PI); |
| |
| final int minLoc = minBoundary.getPrecision().compare(Geometry.HALF_PI, |
| posPole.distance(minBoundary.getPoint())); |
| final int maxLoc = maxBoundary.getPrecision().compare(Geometry.HALF_PI, |
| posPole.distance(maxBoundary.getPoint())); |
| |
| final boolean positiveFacingSplit = splitter.isPositiveFacing(); |
| |
| // assume a positive orientation of the splitter for region location |
| // purposes and adjust later |
| Convex pos = null; |
| Convex neg = null; |
| |
| if (minLoc > 0) { |
| // min is on the pos side |
| |
| if (maxLoc >= 0) { |
| // max is directly on the splitter or on the pos side |
| pos = this; |
| } else { |
| // min is on the pos side and max is on the neg side |
| final CutAngle posCut = positiveFacingSplit ? |
| opposite.reverse() : |
| opposite; |
| pos = Convex.of(minBoundary, posCut); |
| |
| final CutAngle negCut = positiveFacingSplit ? |
| opposite : |
| opposite.reverse(); |
| neg = Convex.of(negCut, maxBoundary); |
| } |
| } else if (minLoc < 0) { |
| // min is on the neg side |
| |
| if (maxLoc <= 0) { |
| // max is directly on the splitter or on the neg side |
| neg = this; |
| } else { |
| // min is on the neg side and max is on the pos side |
| final CutAngle posCut = positiveFacingSplit ? |
| splitter.reverse() : |
| splitter; |
| pos = Convex.of(maxBoundary, posCut); |
| |
| final CutAngle negCut = positiveFacingSplit ? |
| splitter : |
| splitter.reverse(); |
| neg = Convex.of(negCut, minBoundary); |
| } |
| } else { |
| // min is directly on the splitter; determine whether it was on the main split |
| // point or its antipodal point |
| if (splitter.getPoint().distance(minBoundary.getPoint()) < Geometry.HALF_PI) { |
| // on main splitter; interval will be located on pos side of split |
| pos = this; |
| } else { |
| // on antipodal point; interval will be located on neg side of split |
| neg = this; |
| } |
| } |
| |
| // adjust for the actual orientation of the splitter |
| final Convex minus = positiveFacingSplit ? neg : pos; |
| final Convex plus = positiveFacingSplit ? pos : neg; |
| |
| return new Split<>(minus, plus); |
| } |
| |
| /** Return an instance representing the convex angular interval between the given min and max azimuth |
| * values. The max value is adjusted to be numerically above the min value, even if the resulting |
| * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space |
| * is returned if either point is infinite or min and max are equivalent as evaluated by the |
| * given precision context. |
| * @param min min azimuth value |
| * @param max max azimuth value |
| * @param precision precision precision context used to compare floating point values |
| * @return a new instance resulting the angular region between the given min and max azimuths |
| * @throws GeometryException if either azimuth is infinite or NaN, or the given angular |
| * interval is not convex (meaning it has a size of greater than {@code pi}) |
| */ |
| public static Convex of(final double min, final double max, final DoublePrecisionContext precision) { |
| return of(Point1S.of(min), Point1S.of(max), precision); |
| } |
| |
| /** Return an instance representing the convex angular interval between the given min and max azimuth |
| * points. The max point is adjusted to be numerically above the min point, even if the resulting |
| * azimuth value is greater than or equal to {@code 2pi}. An instance representing the full space |
| * is returned if either point is infinite or min and max are equivalent as evaluated by the |
| * given precision context. |
| * @param min min azimuth value |
| * @param max max azimuth value |
| * @param precision precision precision context used to compare floating point values |
| * @return a new instance resulting the angular region between the given min and max points |
| * @throws GeometryException if either azimuth is infinite or NaN, or the given angular |
| * interval is not convex (meaning it has a size of greater than {@code pi}) |
| */ |
| public static Convex of(final Point1S min, final Point1S max, final DoublePrecisionContext precision) { |
| return createInterval(min, max, precision, Convex::new, Convex.FULL); |
| } |
| |
| /** Return an instance representing the convex angular interval between the given oriented points. |
| * The negative-facing point is used as the minimum boundary and the positive-facing point is |
| * adjusted to be above the minimum. The arguments can be given in any order. The full space |
| * is returned if the points are equivalent or are oriented in the same direction. |
| * @param a first oriented point |
| * @param b second oriented point |
| * @return an instance representing the angular interval between the given oriented points |
| * @throws GeometryException if either azimuth is infinite or NaN, or the given angular |
| * interval is not convex (meaning it has a size of greater than {@code pi}) |
| */ |
| public static Convex of(final CutAngle a, final CutAngle b) { |
| return createInterval(a, b, Convex::new, Convex.FULL); |
| } |
| } |
| } |