RegionBSPTree1S.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.spherical.oned;

  18. import java.util.ArrayList;
  19. import java.util.Collections;
  20. import java.util.Comparator;
  21. import java.util.List;
  22. import java.util.Objects;

  23. import org.apache.commons.geometry.core.Transform;
  24. import org.apache.commons.geometry.core.partitioning.Hyperplane;
  25. import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
  26. import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
  27. import org.apache.commons.geometry.core.partitioning.Split;
  28. import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
  29. import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
  30. import org.apache.commons.geometry.euclidean.twod.Vector2D;
  31. import org.apache.commons.numbers.angle.Angle;
  32. import org.apache.commons.numbers.core.Precision;

  33. /** BSP tree representing regions in 1D spherical space.
  34.  */
  35. public class RegionBSPTree1S extends AbstractRegionBSPTree<Point1S, RegionBSPTree1S.RegionNode1S> {
  36.     /** Comparator used to sort BoundaryPairs by ascending azimuth.  */
  37.     private static final Comparator<BoundaryPair> BOUNDARY_PAIR_COMPARATOR =
  38.             Comparator.comparingDouble(BoundaryPair::getMinValue);

  39.     /** Create a new, empty instance.
  40.      */
  41.     public RegionBSPTree1S() {
  42.         this(false);
  43.     }

  44.     /** Create a new region. If {@code full} is true, then the region will
  45.      * represent the entire circle. Otherwise, it will be empty.
  46.      * @param full whether or not the region should contain the entire
  47.      *      circle or be empty
  48.      */
  49.     public RegionBSPTree1S(final boolean full) {
  50.         super(full);
  51.     }

  52.     /** Return a deep copy of this instance.
  53.      * @return a deep copy of this instance.
  54.      * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
  55.      */
  56.     public RegionBSPTree1S copy() {
  57.         final RegionBSPTree1S result = RegionBSPTree1S.empty();
  58.         result.copy(this);

  59.         return result;
  60.     }

  61.     /** Add an interval to this region. The resulting region will be the
  62.      * union of the interval and the region represented by this instance.
  63.      * @param interval the interval to add
  64.      */
  65.     public void add(final AngularInterval interval) {
  66.         union(fromInterval(interval));
  67.     }

  68.     /** {@inheritDoc} */
  69.     @Override
  70.     public Point1S project(final Point1S pt) {
  71.         final BoundaryProjector1S projector = new BoundaryProjector1S(pt);
  72.         accept(projector);

  73.         return projector.getProjected();
  74.     }

  75.     /** {@inheritDoc}
  76.      *
  77.      * <p>Each interval of the region is transformed individually and the
  78.      * results are unioned. If the size of any transformed interval is greater
  79.      * than or equal to 2pi, then the region is set to the full space.</p>
  80.      */
  81.     @Override
  82.     public void transform(final Transform<Point1S> transform) {
  83.         if (!isFull() && !isEmpty()) {
  84.             // transform each interval individually to handle wrap-around
  85.             final List<AngularInterval> intervals = toIntervals();

  86.             setEmpty();

  87.             for (final AngularInterval interval : intervals) {
  88.                 union(interval.transform(transform).toTree());
  89.             }
  90.         }
  91.     }

  92.     /** {@inheritDoc}
  93.      *
  94.      * <p>It is important to note that split operations occur according to the rules of the
  95.      * {@link CutAngle} hyperplane class. In this class, the continuous circle is viewed
  96.      * as a non-circular segment of the number line in the range {@code [0, 2pi)}. Hyperplanes
  97.      * are placed along this line and partition it into the segments {@code [0, x]}
  98.      * and {@code [x, 2pi)}, where {@code x} is the location of the hyperplane. For example,
  99.      * a positive-facing {@link CutAngle} instance with an azimuth of {@code 0.5pi} has
  100.      * a minus side consisting of the angles {@code [0, 0.5pi]} and a plus side consisting of
  101.      * the angles {@code [0.5pi, 2pi)}. Similarly, a positive-facing {@link CutAngle} with
  102.      * an azimuth of {@code 0pi} has a plus side of {@code [0, 2pi)} (the full space) and
  103.      * a minus side that is completely empty (since no points exist in our domain that are
  104.      * less than zero). These rules can result in somewhat non-intuitive behavior at times.
  105.      * For example, splitting a non-empty region with a hyperplane at {@code 0pi} is
  106.      * essentially a no-op, since the region will either lie entirely on the plus or minus
  107.      * side of the hyperplane (depending on the hyperplane's orientation) regardless of the actual
  108.      * content of the region. In these situations, a copy of the tree is returned on the
  109.      * appropriate side of the split.</p>
  110.      *
  111.      * @see CutAngle
  112.      * @see #splitDiameter(CutAngle)
  113.      */
  114.     @Override
  115.     public Split<RegionBSPTree1S> split(final Hyperplane<Point1S> splitter) {
  116.         // Handle the special case where the cut is on the azimuth equivalent to zero.
  117.         // In this case, it is not possible for any points to lie between it and zero.
  118.         if (!isEmpty() && splitter.classify(Point1S.ZERO) == HyperplaneLocation.ON) {
  119.             final CutAngle cut = (CutAngle) splitter;
  120.             if (cut.isPositiveFacing()) {
  121.                 return new Split<>(null, copy());
  122.             } else {
  123.                 return new Split<>(copy(), null);
  124.             }
  125.         }

  126.         return split(splitter, RegionBSPTree1S.empty(), RegionBSPTree1S.empty());
  127.     }

  128.     /** Split the instance along a circle diameter.The diameter is defined by the given
  129.      * split point and its reversed antipodal point.
  130.      * @param splitter split point defining one side of the split diameter
  131.      * @return result of the split operation
  132.      */
  133.     public Split<RegionBSPTree1S> splitDiameter(final CutAngle splitter) {

  134.         final CutAngle opposite = CutAngles.fromPointAndDirection(
  135.                 splitter.getPoint().antipodal(),
  136.                 !splitter.isPositiveFacing(),
  137.                 splitter.getPrecision());

  138.         final double plusPoleOffset = splitter.isPositiveFacing() ?
  139.                 +Angle.PI_OVER_TWO :
  140.                 -Angle.PI_OVER_TWO;
  141.         final Point1S plusPole = Point1S.of(splitter.getAzimuth() + plusPoleOffset);

  142.         final boolean zeroOnPlusSide = splitter.getPrecision()
  143.                 .lte(plusPole.distance(Point1S.ZERO), Angle.PI_OVER_TWO);

  144.         final Split<RegionBSPTree1S> firstSplit = split(splitter);
  145.         final Split<RegionBSPTree1S> secondSplit = split(opposite);

  146.         RegionBSPTree1S minus = RegionBSPTree1S.empty();
  147.         RegionBSPTree1S plus = RegionBSPTree1S.empty();

  148.         if (zeroOnPlusSide) {
  149.             // zero wrap-around needs to be handled on the plus side of the split
  150.             safeUnion(plus, firstSplit.getPlus());
  151.             safeUnion(plus, secondSplit.getPlus());

  152.             minus = firstSplit.getMinus();
  153.             if (minus != null) {
  154.                 minus = minus.split(opposite).getMinus();
  155.             }
  156.         } else {
  157.             // zero wrap-around needs to be handled on the minus side of the split
  158.             safeUnion(minus, firstSplit.getMinus());
  159.             safeUnion(minus, secondSplit.getMinus());

  160.             plus = firstSplit.getPlus();
  161.             if (plus != null) {
  162.                 plus = plus.split(opposite).getPlus();
  163.             }
  164.         }

  165.         return new Split<>(
  166.                 (minus != null && !minus.isEmpty()) ? minus : null,
  167.                 (plus != null && !plus.isEmpty()) ? plus : null);
  168.     }


  169.     /** Convert the region represented by this tree into a list of separate
  170.      * {@link AngularInterval}s, arranged in order of ascending min value.
  171.      * @return list of {@link AngularInterval}s representing this region in order of
  172.      *      ascending min value
  173.      */
  174.     public List<AngularInterval> toIntervals() {
  175.         if (isFull()) {
  176.             return Collections.singletonList(AngularInterval.full());
  177.         }

  178.         final List<BoundaryPair> insideBoundaryPairs = new ArrayList<>();
  179.         for (final RegionNode1S node : nodes()) {
  180.             if (node.isInside()) {
  181.                 insideBoundaryPairs.add(getNodeBoundaryPair(node));
  182.             }
  183.         }

  184.         insideBoundaryPairs.sort(BOUNDARY_PAIR_COMPARATOR);

  185.         final int boundaryPairCount = insideBoundaryPairs.size();

  186.         // Get the start point for merging intervals together.
  187.         final int startOffset = getIntervalStartIndex(insideBoundaryPairs);

  188.         // Go through the pairs starting at the start offset and create intervals
  189.         // for each set of adjacent pairs.
  190.         final List<AngularInterval> intervals = new ArrayList<>();

  191.         BoundaryPair start = null;
  192.         BoundaryPair end = null;
  193.         BoundaryPair current;

  194.         for (int i = 0; i < boundaryPairCount; ++i) {
  195.             current = insideBoundaryPairs.get((i + startOffset) % boundaryPairCount);

  196.             if (start == null) {
  197.                 start = current;
  198.                 end = current;
  199.             } else if (Objects.equals(end.getMax(), current.getMin())) {
  200.                 // these intervals should be merged
  201.                 end = current;
  202.             } else {
  203.                 // these intervals should be separate
  204.                 intervals.add(createInterval(start, end));

  205.                 // queue up the next pair
  206.                 start = current;
  207.                 end = current;
  208.             }
  209.         }

  210.         if (start != null && end != null) {
  211.             intervals.add(createInterval(start, end));
  212.         }

  213.         return intervals;
  214.     }

  215.     /** Get the index that should be used as the starting point for combining adjacent boundary pairs
  216.      * into contiguous intervals. This is computed as the first boundary pair found that is not connected
  217.      * to the pair before it, or {@code 0} if no such entry exists.
  218.      * @param boundaryPairs list of boundary pairs to search; must be ordered by increasing azimuth
  219.      * @return the index to use as a starting place for combining adjacent boundary pairs
  220.      *      into contiguous intervals
  221.      */
  222.     private int getIntervalStartIndex(final List<BoundaryPair> boundaryPairs) {
  223.         final int size = boundaryPairs.size();

  224.         if (size > 0) {
  225.             BoundaryPair current;
  226.             BoundaryPair previous = boundaryPairs.get(size - 1);

  227.             for (int i = 0; i < size; ++i, previous = current) {
  228.                 current = boundaryPairs.get(i);

  229.                 if (!Objects.equals(current.getMin(), previous.getMax())) {
  230.                     return i;
  231.                 }
  232.             }
  233.         }

  234.         return 0;
  235.     }

  236.     /** Create an interval instance from the min boundary from the start boundary pair and
  237.      * the max boundary from the end boundary pair. The hyperplane directions are adjusted
  238.      * as needed.
  239.      * @param start starting boundary pair
  240.      * @param end ending boundary pair
  241.      * @return an interval created from the min boundary of the given start pair and the
  242.      *      max boundary from the given end pair
  243.      */
  244.     private AngularInterval createInterval(final BoundaryPair start, final BoundaryPair end) {
  245.         CutAngle min = start.getMin();
  246.         CutAngle max = end.getMax();

  247.         final Precision.DoubleEquivalence precision = (min != null) ? min.getPrecision() : max.getPrecision();

  248.         // flip the hyperplanes if needed since there's no
  249.         // guarantee that the inside will be on the minus side
  250.         // of the hyperplane (for example, if the region is complemented)

  251.         if (min != null) {
  252.             if (min.isPositiveFacing()) {
  253.                 min = min.reverse();
  254.             }
  255.         } else {
  256.             min = CutAngles.createNegativeFacing(0.0, precision);
  257.         }

  258.         if (max != null) {
  259.             if (!max.isPositiveFacing()) {
  260.                 max = max.reverse();
  261.             }
  262.         } else {
  263.             max = CutAngles.createPositiveFacing(Angle.TWO_PI, precision);
  264.         }

  265.         return AngularInterval.of(min, max);
  266.     }

  267.     /** Return the min/max boundary pair for the convex region represented by the given node.
  268.      * @param node the node to compute the interval for
  269.      * @return the min/max boundary pair for the convex region represented by the given node
  270.      */
  271.     private BoundaryPair getNodeBoundaryPair(final RegionNode1S node) {
  272.         CutAngle min = null;
  273.         CutAngle max = null;

  274.         CutAngle pt;
  275.         RegionNode1S child = node;
  276.         RegionNode1S parent;

  277.         while ((min == null || max == null) && (parent = child.getParent()) != null) {
  278.             pt = (CutAngle) parent.getCutHyperplane();

  279.             if ((pt.isPositiveFacing() && child.isMinus()) ||
  280.                     (!pt.isPositiveFacing() && child.isPlus())) {

  281.                 if (max == null) {
  282.                     max = pt;
  283.                 }
  284.             } else if (min == null) {
  285.                 min = pt;
  286.             }

  287.             child = parent;
  288.         }

  289.         return new BoundaryPair(min, max);
  290.     }

  291.     /** {@inheritDoc} */
  292.     @Override
  293.     protected RegionSizeProperties<Point1S> computeRegionSizeProperties() {
  294.         if (isFull()) {
  295.             return new RegionSizeProperties<>(Angle.TWO_PI, null);
  296.         } else if (isEmpty()) {
  297.             return new RegionSizeProperties<>(0, null);
  298.         }

  299.         double size = 0;
  300.         Vector2D scaledCentroidSum = Vector2D.ZERO;

  301.         double intervalSize;

  302.         for (final AngularInterval interval : toIntervals()) {
  303.             intervalSize = interval.getSize();

  304.             size += intervalSize;
  305.             scaledCentroidSum = scaledCentroidSum.add(interval.getCentroid().getVector().withNorm(intervalSize));
  306.         }

  307.         final Precision.DoubleEquivalence precision = ((CutAngle) getRoot().getCutHyperplane()).getPrecision();

  308.         final Point1S centroid = scaledCentroidSum.eq(Vector2D.ZERO, precision) ?
  309.                  null :
  310.                  Point1S.from(scaledCentroidSum);

  311.         return new RegionSizeProperties<>(size, centroid);
  312.     }

  313.     /** {@inheritDoc} */
  314.     @Override
  315.     protected RegionNode1S createNode() {
  316.         return new RegionNode1S(this);
  317.     }

  318.     /** Return a new, empty BSP tree.
  319.      * @return a new, empty BSP tree.
  320.      */
  321.     public static RegionBSPTree1S empty() {
  322.         return new RegionBSPTree1S(false);
  323.     }

  324.     /** Return a new, full BSP tree. The returned tree represents the
  325.      * full space.
  326.      * @return a new, full BSP tree.
  327.      */
  328.     public static RegionBSPTree1S full() {
  329.         return new RegionBSPTree1S(true);
  330.     }

  331.     /** Return a new BSP tree representing the same region as the given angular interval.
  332.      * @param interval the input interval
  333.      * @return a new BSP tree representing the same region as the given angular interval
  334.      */
  335.     public static RegionBSPTree1S fromInterval(final AngularInterval interval) {
  336.         final CutAngle minBoundary = interval.getMinBoundary();
  337.         final CutAngle maxBoundary = interval.getMaxBoundary();

  338.         final RegionBSPTree1S tree = full();

  339.         if (minBoundary != null) {
  340.             tree.insert(minBoundary.span());
  341.         }

  342.         if (maxBoundary != null) {
  343.             tree.insert(maxBoundary.span());
  344.         }

  345.         return tree;
  346.     }

  347.     /** Perform a union operation with {@code target} and {@code input}, storing the result
  348.      * in {@code target}; does nothing if {@code input} is null.
  349.      * @param target target tree
  350.      * @param input input tree
  351.      */
  352.     private static void safeUnion(final RegionBSPTree1S target, final RegionBSPTree1S input) {
  353.         if (input != null) {
  354.             target.union(input);
  355.         }
  356.     }

  357.     /** BSP tree node for one dimensional spherical space.
  358.      */
  359.     public static final class RegionNode1S extends AbstractRegionBSPTree.AbstractRegionNode<Point1S, RegionNode1S> {
  360.         /** Simple constructor.
  361.          * @param tree the owning tree instance
  362.          */
  363.         private RegionNode1S(final AbstractBSPTree<Point1S, RegionNode1S> tree) {
  364.             super(tree);
  365.         }

  366.         /** {@inheritDoc} */
  367.         @Override
  368.         protected RegionNode1S getSelf() {
  369.             return this;
  370.         }
  371.     }

  372.     /** Internal class containing pairs of interval boundaries.
  373.      */
  374.     private static final class BoundaryPair {

  375.         /** The min boundary. */
  376.         private final CutAngle min;

  377.         /** The max boundary. */
  378.         private final CutAngle max;

  379.         /** Simple constructor.
  380.          * @param min min boundary hyperplane
  381.          * @param max max boundary hyperplane
  382.          */
  383.         BoundaryPair(final CutAngle min, final CutAngle max) {
  384.             this.min = min;
  385.             this.max = max;
  386.         }

  387.         /** Get the minimum boundary hyperplane.
  388.          * @return the minimum boundary hyperplane.
  389.          */
  390.         public CutAngle getMin() {
  391.             return min;
  392.         }

  393.         /** Get the maximum boundary hyperplane.
  394.          * @return the maximum boundary hyperplane.
  395.          */
  396.         public CutAngle getMax() {
  397.             return max;
  398.         }

  399.         /** Get the minimum value of the interval or zero if no minimum value exists.
  400.          * @return the minimum value of the interval or zero
  401.          *      if no minimum value exists.
  402.          */
  403.         public double getMinValue() {
  404.             return (min != null) ? min.getNormalizedAzimuth() : 0;
  405.         }
  406.     }

  407.     /** Class used to project points onto the region boundary.
  408.      */
  409.     private static final class BoundaryProjector1S extends BoundaryProjector<Point1S, RegionNode1S> {
  410.         /** Simple constructor.
  411.          * @param point the point to project onto the region's boundary
  412.          */
  413.         BoundaryProjector1S(final Point1S point) {
  414.             super(point);
  415.         }

  416.         /** {@inheritDoc} */
  417.         @Override
  418.         protected boolean isPossibleClosestCut(final HyperplaneSubset<Point1S> cut, final Point1S target,
  419.                 final double minDist) {
  420.             // since the space wraps around, consider any cut as possibly being the closest
  421.             return true;
  422.         }

  423.         /** {@inheritDoc} */
  424.         @Override
  425.         protected Point1S disambiguateClosestPoint(final Point1S target, final Point1S a, final Point1S b) {
  426.             // prefer the point with the smaller normalize azimuth value
  427.             return a.getNormalizedAzimuth() < b.getNormalizedAzimuth() ? a : b;
  428.         }
  429.     }
  430. }