RegionBSPTree1D.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.util.ArrayList;
  19. import java.util.Comparator;
  20. import java.util.List;
  21. import java.util.Objects;
  22. import java.util.function.BiConsumer;

  23. import org.apache.commons.geometry.core.RegionLocation;
  24. import org.apache.commons.geometry.core.Transform;
  25. import org.apache.commons.geometry.core.partitioning.Hyperplane;
  26. import org.apache.commons.geometry.core.partitioning.Split;
  27. import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
  28. import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
  29. import org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule;

  30. /** Binary space partitioning (BSP) tree representing a region in one dimensional
  31.  * Euclidean space.
  32.  */
  33. public final class RegionBSPTree1D extends AbstractRegionBSPTree<Vector1D, RegionBSPTree1D.RegionNode1D> {
  34.     /** Comparator used to sort BoundaryPairs by ascending location.  */
  35.     private static final Comparator<BoundaryPair> BOUNDARY_PAIR_COMPARATOR =
  36.             Comparator.comparingDouble(BoundaryPair::getMinValue);

  37.     /** Create a new, empty region.
  38.      */
  39.     public RegionBSPTree1D() {
  40.         this(false);
  41.     }

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

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

  57.         return result;
  58.     }

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

  66.     /** Classify a point location with respect to the region.
  67.      * @param x the point to classify
  68.      * @return the location of the point with respect to the region
  69.      * @see #classify(org.apache.commons.geometry.core.Point)
  70.      */
  71.     public RegionLocation classify(final double x) {
  72.         return classify(Vector1D.of(x));
  73.     }

  74.     /** Return true if the given point location is on the inside or boundary
  75.      * of the region.
  76.      * @param x the location to test
  77.      * @return true if the location is on the inside or boundary of the region
  78.      * @see #contains(org.apache.commons.geometry.core.Point)
  79.      */
  80.     public boolean contains(final double x) {
  81.         return contains(Vector1D.of(x));
  82.     }

  83.     /** {@inheritDoc}
  84.     *
  85.     *  <p>This method simply returns 0 because boundaries in one dimension do not
  86.     *  have any size.</p>
  87.     */
  88.     @Override
  89.     public double getBoundarySize() {
  90.         return 0;
  91.     }

  92.     /** {@inheritDoc} */
  93.     @Override
  94.     public Vector1D project(final Vector1D pt) {
  95.         // use our custom projector so that we can disambiguate points that are
  96.         // actually equidistant from the target point
  97.         final BoundaryProjector1D projector = new BoundaryProjector1D(pt);
  98.         accept(projector);

  99.         return projector.getProjected();
  100.     }

  101.     /** {@inheritDoc}
  102.      *
  103.      * <p>When splitting trees representing single points with a splitter lying directly
  104.      * on the point, the result point is placed on one side of the splitter based on its
  105.      * orientation: if the splitter is positive-facing, the point is placed on the plus
  106.      * side of the split; if the splitter is negative-facing, the point is placed on the
  107.      * minus side of the split.</p>
  108.      */
  109.     @Override
  110.     public Split<RegionBSPTree1D> split(final Hyperplane<Vector1D> splitter) {
  111.         return split(splitter, RegionBSPTree1D.empty(), RegionBSPTree1D.empty());
  112.     }

  113.     /** Get the minimum value on the inside of the region; returns {@link Double#NEGATIVE_INFINITY}
  114.      * if the region does not have a minimum value and {@link Double#POSITIVE_INFINITY} if
  115.      * the region is empty.
  116.      * @return the minimum value on the inside of the region
  117.      */
  118.     public double getMin() {
  119.         double min = Double.POSITIVE_INFINITY;

  120.         RegionNode1D node = getRoot();
  121.         OrientedPoint pt;

  122.         while (!node.isLeaf()) {
  123.             pt = (OrientedPoint) node.getCutHyperplane();

  124.             min = pt.getLocation();
  125.             node = pt.isPositiveFacing() ? node.getMinus() : node.getPlus();
  126.         }

  127.         return node.isInside() ? Double.NEGATIVE_INFINITY : min;
  128.     }

  129.     /** Get the maximum value on the inside of the region; returns {@link Double#POSITIVE_INFINITY}
  130.      * if the region does not have a maximum value and {@link Double#NEGATIVE_INFINITY} if
  131.      * the region is empty.
  132.      * @return the maximum value on the inside of the region
  133.      */
  134.     public double getMax() {
  135.         double max = Double.NEGATIVE_INFINITY;

  136.         RegionNode1D node = getRoot();
  137.         OrientedPoint pt;

  138.         while (!node.isLeaf()) {
  139.             pt = (OrientedPoint) node.getCutHyperplane();

  140.             max = pt.getLocation();
  141.             node = pt.isPositiveFacing() ? node.getPlus() : node.getMinus();
  142.         }

  143.         return node.isInside() ? Double.POSITIVE_INFINITY : max;
  144.     }

  145.     /** Convert the region represented by this tree into a list of separate
  146.      * {@link Interval}s, arranged in order of ascending min value.
  147.      * @return list of {@link Interval}s representing this region in order of
  148.      *      ascending min value
  149.      */
  150.     public List<Interval> toIntervals() {

  151.         final List<BoundaryPair> boundaryPairs = new ArrayList<>();

  152.         visitInsideIntervals((min, max) -> boundaryPairs.add(new BoundaryPair(min, max)));
  153.         boundaryPairs.sort(BOUNDARY_PAIR_COMPARATOR);

  154.         final List<Interval> intervals = new ArrayList<>();

  155.         BoundaryPair start = null;
  156.         BoundaryPair end = null;

  157.         for (final BoundaryPair current : boundaryPairs) {
  158.             if (start == null) {
  159.                 start = current;
  160.                 end = current;
  161.             } else if (Objects.equals(end.getMax(), current.getMin())) {
  162.                 // these intervals should be merged
  163.                 end = current;
  164.             } else {
  165.                 // these intervals should not be merged
  166.                 intervals.add(createInterval(start, end));

  167.                 // queue up the next pair
  168.                 start = current;
  169.                 end = current;
  170.             }
  171.         }

  172.         if (start != null) {
  173.             intervals.add(createInterval(start, end));
  174.         }

  175.         return intervals;
  176.     }

  177.     /** Create an interval instance from the min boundary from the start boundary pair and
  178.      * the max boundary from the end boundary pair. The hyperplane directions are adjusted
  179.      * as needed.
  180.      * @param start starting boundary pair
  181.      * @param end ending boundary pair
  182.      * @return an interval created from the min boundary of the given start pair and the
  183.      *      max boundary from the given end pair
  184.      */
  185.     private Interval createInterval(final BoundaryPair start, final BoundaryPair end) {
  186.         OrientedPoint min = start.getMin();
  187.         OrientedPoint max = end.getMax();

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

  191.         if (min != null && min.isPositiveFacing()) {
  192.             min = min.reverse();
  193.         }
  194.         if (max != null && !max.isPositiveFacing()) {
  195.             max = max.reverse();
  196.         }

  197.         return Interval.of(min, max);
  198.     }

  199.     /** Compute the min/max intervals for all interior convex regions in the tree and
  200.      * pass the values to the given visitor function.
  201.      * @param visitor the object that will receive the calculated min and max boundary for each
  202.      *      insides node's convex region
  203.      */
  204.     private void visitInsideIntervals(final BiConsumer<OrientedPoint, OrientedPoint> visitor) {
  205.         for (final RegionNode1D node : nodes()) {
  206.             if (node.isInside()) {
  207.                 node.visitNodeInterval(visitor);
  208.             }
  209.         }
  210.     }

  211.     /** {@inheritDoc} */
  212.     @Override
  213.     protected RegionNode1D createNode() {
  214.         return new RegionNode1D(this);
  215.     }

  216.     /** {@inheritDoc} */
  217.     @Override
  218.     protected RegionSizeProperties<Vector1D> computeRegionSizeProperties() {
  219.         final RegionSizePropertiesVisitor visitor = new RegionSizePropertiesVisitor();

  220.         visitInsideIntervals(visitor);

  221.         return visitor.getRegionSizeProperties();
  222.     }

  223.     /** Returns true if the given transform would result in a swapping of the interior
  224.      * and exterior of the region if applied.
  225.      *
  226.      * <p>This method always returns false since no swapping of this kind occurs in
  227.      * 1D.</p>
  228.      */
  229.     @Override
  230.     protected boolean swapsInsideOutside(final Transform<Vector1D> transform) {
  231.         return false;
  232.     }

  233.     /** Return a new {@link RegionBSPTree1D} instance containing the entire space.
  234.      * @return a new {@link RegionBSPTree1D} instance containing the entire space
  235.      */
  236.     public static RegionBSPTree1D full() {
  237.         return new RegionBSPTree1D(true);
  238.     }

  239.     /** Return a new, empty {@link RegionBSPTree1D} instance.
  240.      * @return a new, empty {@link RegionBSPTree1D} instance
  241.      */
  242.     public static RegionBSPTree1D empty() {
  243.         return new RegionBSPTree1D(false);
  244.     }

  245.     /** Construct a new instance from one or more intervals. The returned tree
  246.      * represents the same region as the union of all of the input intervals.
  247.      * @param interval the input interval
  248.      * @param more additional intervals to add to the region
  249.      * @return a new instance representing the same region as the union
  250.      *      of all of the given intervals
  251.      */
  252.     public static RegionBSPTree1D from(final Interval interval, final Interval... more) {
  253.         final RegionBSPTree1D tree = intervalToTree(interval);

  254.         for (final Interval additional : more) {
  255.             tree.add(additional);
  256.         }

  257.         return tree;
  258.     }

  259.     /** Construct a new instance from the given collection of intervals.
  260.      * @param intervals the intervals to populate the region with
  261.      * @return a new instance constructed from the given collection of intervals
  262.      */
  263.     public static RegionBSPTree1D from(final Iterable<Interval> intervals) {
  264.         final RegionBSPTree1D tree = new RegionBSPTree1D(false);

  265.         for (final Interval interval : intervals) {
  266.             tree.add(interval);
  267.         }

  268.         return tree;
  269.     }

  270.     /** Return a tree representing the same region as the given interval.
  271.      * @param interval interval to create a tree from
  272.      * @return a tree representing the same region as the given interval
  273.      */
  274.     private static RegionBSPTree1D intervalToTree(final Interval interval) {
  275.         final OrientedPoint minBoundary = interval.getMinBoundary();
  276.         final OrientedPoint maxBoundary = interval.getMaxBoundary();

  277.         final RegionBSPTree1D tree = full();

  278.         RegionNode1D node = tree.getRoot();

  279.         if (minBoundary != null) {
  280.             tree.setNodeCut(node, minBoundary.span(), tree.getSubtreeInitializer(RegionCutRule.MINUS_INSIDE));

  281.             node = node.getMinus();
  282.         }

  283.         if (maxBoundary != null) {
  284.             tree.setNodeCut(node, maxBoundary.span(), tree.getSubtreeInitializer(RegionCutRule.MINUS_INSIDE));
  285.         }

  286.         return tree;
  287.     }

  288.     /** BSP tree node for one dimensional Euclidean space.
  289.      */
  290.     public static final class RegionNode1D extends AbstractRegionBSPTree.AbstractRegionNode<Vector1D, RegionNode1D> {
  291.         /** Simple constructor.
  292.          * @param tree the owning tree instance
  293.          */
  294.         private RegionNode1D(final AbstractBSPTree<Vector1D, RegionNode1D> tree) {
  295.             super(tree);
  296.         }

  297.         /** Get the region represented by this node. The returned region contains
  298.          * the entire area contained in this node, regardless of the attributes of
  299.          * any child nodes.
  300.          * @return the region represented by this node
  301.          */
  302.         public Interval getNodeRegion() {
  303.             final NodeRegionVisitor visitor = new NodeRegionVisitor();
  304.             visitNodeInterval(visitor);

  305.             return visitor.getInterval();
  306.         }

  307.         /** Determine the min/max boundaries for the convex region represented by this node and pass
  308.          * the values to the visitor function.
  309.          * @param visitor the object that will receive the min and max boundaries for the node's
  310.          *      convex region
  311.          */
  312.         private void visitNodeInterval(final BiConsumer<? super OrientedPoint, ? super OrientedPoint> visitor) {
  313.             OrientedPoint min = null;
  314.             OrientedPoint max = null;

  315.             OrientedPoint pt;
  316.             RegionNode1D child = this;
  317.             RegionNode1D parent;

  318.             while ((min == null || max == null) && (parent = child.getParent()) != null) {
  319.                 pt = (OrientedPoint) parent.getCutHyperplane();

  320.                 if ((pt.isPositiveFacing() && child.isMinus()) ||
  321.                         (!pt.isPositiveFacing() && child.isPlus())) {

  322.                     if (max == null) {
  323.                         max = pt;
  324.                     }
  325.                 } else if (min == null) {
  326.                     min = pt;
  327.                 }

  328.                 child = parent;
  329.             }

  330.             visitor.accept(min, max);
  331.         }

  332.         /** {@inheritDoc} */
  333.         @Override
  334.         protected RegionNode1D getSelf() {
  335.             return this;
  336.         }
  337.     }

  338.     /** Internal class containing pairs of interval boundaries.
  339.      */
  340.     private static final class BoundaryPair {

  341.         /** The min boundary. */
  342.         private final OrientedPoint min;

  343.         /** The max boundary. */
  344.         private final OrientedPoint max;

  345.         /** Simple constructor.
  346.          * @param min min boundary hyperplane
  347.          * @param max max boundary hyperplane
  348.          */
  349.         BoundaryPair(final OrientedPoint min, final OrientedPoint max) {
  350.             this.min = min;
  351.             this.max = max;
  352.         }

  353.         /** Get the minimum boundary hyperplane.
  354.          * @return the minimum boundary hyperplane.
  355.          */
  356.         public OrientedPoint getMin() {
  357.             return min;
  358.         }

  359.         /** Get the maximum boundary hyperplane.
  360.          * @return the maximum boundary hyperplane.
  361.          */
  362.         public OrientedPoint getMax() {
  363.             return max;
  364.         }

  365.         /** Get the minimum value of the interval or {@link Double#NEGATIVE_INFINITY}
  366.          * if no minimum value exists.
  367.          * @return the minimum value of the interval or {@link Double#NEGATIVE_INFINITY}
  368.          *      if no minimum value exists.
  369.          */
  370.         public double getMinValue() {
  371.             return (min != null) ? min.getLocation() : Double.NEGATIVE_INFINITY;
  372.         }
  373.     }

  374.     /** Class used to project points onto the region boundary.
  375.      */
  376.     private static final class BoundaryProjector1D extends BoundaryProjector<Vector1D, RegionNode1D> {
  377.         /** Simple constructor.
  378.          * @param point the point to project onto the region's boundary
  379.          */
  380.         BoundaryProjector1D(final Vector1D point) {
  381.             super(point);
  382.         }

  383.         /** {@inheritDoc} */
  384.         @Override
  385.         protected Vector1D disambiguateClosestPoint(final Vector1D target, final Vector1D a, final Vector1D b) {
  386.             final int cmp = Vector1D.COORDINATE_ASCENDING_ORDER.compare(a, b);

  387.             if (target.isInfinite() && target.getX() > 0) {
  388.                 // return the largest value (closest to +Infinity)
  389.                 return cmp < 0 ? b : a;
  390.             }

  391.             // return the smallest value
  392.             return cmp < 0 ? a : b;
  393.         }
  394.     }

  395.     /** Internal class for calculating the region of a single tree node.
  396.      */
  397.     private static final class NodeRegionVisitor implements BiConsumer<OrientedPoint, OrientedPoint> {

  398.         /** The min boundary for the region. */
  399.         private OrientedPoint min;

  400.         /** The max boundary for the region. */
  401.         private OrientedPoint max;

  402.         /** {@inheritDoc} */
  403.         @Override
  404.         public void accept(final OrientedPoint minBoundary, final OrientedPoint maxBoundary) {
  405.             // reverse the oriented point directions if needed
  406.             this.min = (minBoundary != null && minBoundary.isPositiveFacing()) ? minBoundary.reverse() : minBoundary;
  407.             this.max = (maxBoundary != null && !maxBoundary.isPositiveFacing()) ? maxBoundary.reverse() : maxBoundary;
  408.         }

  409.         /** Return the computed interval.
  410.          * @return the computed interval.
  411.          */
  412.         public Interval getInterval() {
  413.             return Interval.of(min, max);
  414.         }
  415.     }

  416.     /** Internal class for calculating size-related properties for a {@link RegionBSPTree1D}.
  417.      */
  418.     private static final class RegionSizePropertiesVisitor implements BiConsumer<OrientedPoint, OrientedPoint> {
  419.         /** Number of inside intervals visited. */
  420.         private int count;

  421.         /** Total computed size of all inside regions. */
  422.         private double size;

  423.         /** Raw sum of the centroids of each inside interval. */
  424.         private double rawCentroidSum;

  425.         /** The sum of the centroids of each inside interval, scaled by the size of the interval. */
  426.         private double scaledCentroidSum;

  427.         /** {@inheritDoc} */
  428.         @Override
  429.         public void accept(final OrientedPoint min, final OrientedPoint max) {
  430.             ++count;

  431.             final double minLoc = (min != null) ? min.getLocation() : Double.NEGATIVE_INFINITY;
  432.             final double maxLoc = (max != null) ? max.getLocation() : Double.POSITIVE_INFINITY;

  433.             final double intervalSize = maxLoc - minLoc;
  434.             final double intervalCentroid = 0.5 * (maxLoc + minLoc);

  435.             size += intervalSize;
  436.             rawCentroidSum += intervalCentroid;
  437.             scaledCentroidSum += intervalSize * intervalCentroid;
  438.         }

  439.         /** Get the computed properties for the region. This must only be called after
  440.          * every inside interval has been visited.
  441.          * @return properties for the region
  442.          */
  443.         public RegionSizeProperties<Vector1D> getRegionSizeProperties() {
  444.             Vector1D centroid = null;

  445.             if (count > 0 && Double.isFinite(size)) {
  446.                 if (size > 0) {
  447.                     // use the scaled sum if we have a non-zero size
  448.                     centroid = Vector1D.of(scaledCentroidSum / size);
  449.                 } else {
  450.                     // use the raw sum if we don't have a size; this will be
  451.                     // the case if the region only contains points with zero size
  452.                     centroid = Vector1D.of(rawCentroidSum / count);
  453.                 }
  454.             }

  455.             return new RegionSizeProperties<>(size, centroid);
  456.         }
  457.     }
  458. }