AbstractPartitionedRegionBuilder.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.core.partitioning.bsp;

  18. import java.util.ArrayList;
  19. import java.util.Comparator;
  20. import java.util.HashSet;
  21. import java.util.List;
  22. import java.util.Set;
  23. import java.util.function.BiConsumer;

  24. import org.apache.commons.geometry.core.Point;
  25. import org.apache.commons.geometry.core.RegionLocation;
  26. import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
  27. import org.apache.commons.geometry.core.partitioning.Split;
  28. import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree.SubtreeInitializer;
  29. import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree.AbstractRegionNode;

  30. /** Class encapsulating logic for building regions by inserting boundaries into a BSP
  31.  * tree containing structural cuts, i.e. cuts where both sides of the cut have the same region
  32.  * location. This technique only produces accurate results when the inserted boundaries define
  33.  * the entire surface of the region. However, for valid input boundaries, significant performance
  34.  * improvements can be achieved due to the reduced height of the tree, especially where large
  35.  * numbers of boundaries are involved and/or the defined region is convex.
  36.  *
  37.  * <h2>Implementation Notes</h2>
  38.  *
  39.  * <p>This class constructs regions in two phases: (1) <em>partition insertion</em> and (2) <em>boundary insertion</em>.
  40.  * Instances begin in the <em>partition insertion</em> phase. Here, partitions can be inserted into the empty tree
  41.  * using the standard BSP insertion logic. The {@link RegionCutRule#INHERIT INHERIT} cut rule is used so that the
  42.  * represented region remains empty even as partitions are inserted.
  43.  * </p>
  44.  *
  45.  * <p>The instance moves into the <em>boundary insertion</em> phase when the caller inserts the first region boundary.
  46.  * Attempting to insert a partition after this point results in an {@code IllegalStateException}. This ensures that
  47.  * partitioning cuts are always located higher up the tree than boundary cuts.</p>
  48.  *
  49.  * <p>After all boundaries are inserted, the tree undergoes final processing to ensure that the region is consistent
  50.  * and that unnecessary nodes are removed.</p>
  51.  *
  52.  * <p>This class does not expose any public methods so that subclasses can present their own
  53.  * public API, tailored to the specific types being worked with. In particular, most subclasses
  54.  * will want to restrict the tree types used with the algorithm, which is difficult to implement
  55.  * cleanly at this level.</p>
  56.  * @param <P> Point implementation type
  57.  * @param <N> BSP tree node implementation type
  58.  */
  59. public abstract class AbstractPartitionedRegionBuilder<
  60.     P extends Point<P>,
  61.     N extends AbstractRegionNode<P, N>> {

  62.     /** Comparator for sorting nodes with the deepest nodes first. */
  63.     private static final Comparator<BSPTree.Node<?, ?>> DEEPEST_FIRST_ORDER =
  64.         (a, b) -> Integer.compare(b.depth(), a.depth());

  65.     /** Tree being constructed. */
  66.     private final AbstractRegionBSPTree<P, N> tree;

  67.     /** Subtree initializer for inserted boundaries. */
  68.     private final SubtreeInitializer<N> subtreeInit;

  69.     /** Flag indicating whether or not partitions may still be inserted into the tree. */
  70.     private boolean insertingPartitions = true;

  71.     /** Set of all internal nodes used as partitioning nodes. */
  72.     private final Set<N> partitionNodes = new HashSet<>();

  73.     /** Construct a new instance that builds a partitioned region in the given tree. The tree must
  74.      * be empty.
  75.      * @param tree tree to build the region in; must be empty
  76.      * @throws IllegalArgumentException if the tree is not empty
  77.      */
  78.     protected AbstractPartitionedRegionBuilder(final AbstractRegionBSPTree<P, N> tree) {
  79.         if (!tree.isEmpty()) {
  80.             throw new IllegalArgumentException("Tree must be empty");
  81.         }

  82.         this.tree = tree;
  83.         this.subtreeInit = tree.getSubtreeInitializer(RegionCutRule.MINUS_INSIDE);
  84.     }

  85.     /** Internal method to build and return the tree representing the final partitioned region.
  86.      * @return the partitioned region
  87.      */
  88.     protected AbstractRegionBSPTree<P, N> buildInternal() {
  89.         // condense to combine homogenous leaf nodes
  90.         tree.condense();

  91.         // propagate region interiors to partitioned nodes that have not received
  92.         // a boundary
  93.         if (propagateRegionInterior()) {
  94.             // condense again since some leaf nodes changed
  95.             tree.condense();
  96.         }

  97.         return tree;
  98.     }

  99.     /** Internal method to insert a partition into the tree.
  100.      * @param partition partition to insert
  101.      * @throws IllegalStateException if a boundary has previously been inserted
  102.      */
  103.     protected void insertPartitionInternal(final HyperplaneConvexSubset<P> partition) {
  104.         ensureInsertingPartitions();

  105.         tree.insert(partition, RegionCutRule.INHERIT);
  106.     }

  107.     /** Internal method to insert a region boundary into the tree.
  108.      * @param boundary boundary to insert
  109.      */
  110.     protected void insertBoundaryInternal(final HyperplaneConvexSubset<P> boundary) {
  111.         if (insertingPartitions) {
  112.             // switch to inserting boundaries; place all current internal nodes into
  113.             // a set for easy identification
  114.             for (final N node : tree.nodes()) {
  115.                 if (node.isInternal()) {
  116.                     partitionNodes.add(node);
  117.                 }
  118.             }

  119.             insertingPartitions = false;
  120.         }

  121.         insertBoundaryRecursive(tree.getRoot(), boundary, boundary.getHyperplane().span(),
  122.             (leaf, cut) -> tree.setNodeCut(leaf, cut, subtreeInit));
  123.     }

  124.     /** Insert a region boundary into the tree.
  125.      * @param node node to insert into
  126.      * @param insert the hyperplane convex subset to insert
  127.      * @param trimmed version of the hyperplane convex subset filling the entire space of {@code node}
  128.      * @param leafFn function to apply to leaf nodes
  129.      */
  130.     private void insertBoundaryRecursive(final N node, final HyperplaneConvexSubset<P> insert,
  131.             final HyperplaneConvexSubset<P> trimmed, final BiConsumer<N, HyperplaneConvexSubset<P>> leafFn) {
  132.         if (node.isLeaf()) {
  133.             leafFn.accept(node, trimmed);
  134.         } else {
  135.             insertBoundaryRecursiveInternalNode(node, insert, trimmed, leafFn);
  136.         }
  137.     }

  138.     /** Recursive boundary insertion method for internal nodes.
  139.      * @param node node to insert into
  140.      * @param insert the hyperplane convex subset to insert
  141.      * @param trimmed version of the hyperplane convex subset filling the entire space of {@code node}
  142.      * @param leafFn function to apply to leaf nodes
  143.      * @see #insertBoundaryRecursive(AbstractRegionNode, HyperplaneConvexSubset, HyperplaneConvexSubset, BiConsumer)
  144.      */
  145.     private void insertBoundaryRecursiveInternalNode(final N node, final HyperplaneConvexSubset<P> insert,
  146.             final HyperplaneConvexSubset<P> trimmed, final BiConsumer<N, HyperplaneConvexSubset<P>> leafFn) {

  147.         final Split<? extends HyperplaneConvexSubset<P>> insertSplit =
  148.                 insert.split(node.getCutHyperplane());

  149.         final HyperplaneConvexSubset<P> minus = insertSplit.getMinus();
  150.         final HyperplaneConvexSubset<P> plus = insertSplit.getPlus();

  151.         if (minus == null && plus == null && isPartitionNode(node)) {
  152.             // the inserted boundary lies directly on a partition; proceed down the tree with the
  153.             // rest of the insertion algorithm but instead of cutting the final leaf nodes, just
  154.             // set the location

  155.             // remove this node from the set of partition nodes since this is now a boundary cut
  156.             partitionNodes.remove(node);

  157.             final boolean sameOrientation = node.getCutHyperplane().similarOrientation(insert.getHyperplane());
  158.             final N insertMinus = sameOrientation ? node.getMinus() : node.getPlus();
  159.             final N insertPlus = sameOrientation ? node.getPlus() : node.getMinus();

  160.             insertBoundaryRecursive(insertMinus, insert, trimmed,
  161.                 (leaf, cut) -> leaf.setLocation(RegionLocation.INSIDE));

  162.             insertBoundaryRecursive(insertPlus, insert, trimmed,
  163.                 (leaf, cut) -> leaf.setLocation(RegionLocation.OUTSIDE));

  164.         } else if (minus != null || plus != null) {
  165.             final Split<? extends HyperplaneConvexSubset<P>> trimmedSplit =
  166.                     trimmed.split(node.getCutHyperplane());

  167.             final HyperplaneConvexSubset<P> trimmedMinus = trimmedSplit.getMinus();
  168.             final HyperplaneConvexSubset<P> trimmedPlus = trimmedSplit.getPlus();

  169.             if (minus != null) {
  170.                 insertBoundaryRecursive(node.getMinus(), minus, trimmedMinus, leafFn);
  171.             }
  172.             if (plus != null) {
  173.                 insertBoundaryRecursive(node.getPlus(), plus, trimmedPlus, leafFn);
  174.             }
  175.         }
  176.     }

  177.     /** Propagate the region interior to partitioned leaf nodes that have not had a boundary
  178.      * inserted.
  179.      * @return true if any nodes were changed
  180.      */
  181.     private boolean propagateRegionInterior() {
  182.         final List<N> outsidePartitionedLeaves = getOutsidePartitionedLeaves();
  183.         outsidePartitionedLeaves.sort(DEEPEST_FIRST_ORDER);

  184.         int changeCount = 0;

  185.         N parent;
  186.         N sibling;
  187.         for (final N leaf : outsidePartitionedLeaves) {
  188.             parent = leaf.getParent();

  189.             // check if the parent cut touches the inside anywhere on the side opposite of
  190.             // this leaf; if so, then this node should also be inside
  191.             sibling = leaf.isMinus() ?
  192.                     parent.getPlus() :
  193.                     parent.getMinus();

  194.             if (touchesInside(parent.getCut(), sibling)) {
  195.                 leaf.setLocation(RegionLocation.INSIDE);

  196.                 ++changeCount;
  197.             }
  198.         }

  199.         return changeCount > 0;
  200.     }

  201.     /** Return a list containing all outside leaf nodes that have a parent marked as a partition node.
  202.      * @return a list containing all outside leaf nodes that have a parent marked as a partition node
  203.      */
  204.     private List<N> getOutsidePartitionedLeaves() {
  205.         final List<N> result = new ArrayList<>();

  206.         final N root = tree.getRoot();
  207.         collectOutsidePartitionedLeavesRecursive(root, false, result);

  208.         return result;
  209.     }

  210.    /** Recursively collect all outside leaf nodes that have a parent marked as a partition node.
  211.     * @param node root of the subtree to collect nodes from
  212.     * @param parentIsPartitionNode true if the parent of {@code node} is a partition node
  213.     * @param result list of accumulated results
  214.     */
  215.     private void collectOutsidePartitionedLeavesRecursive(final N node, final boolean parentIsPartitionNode,
  216.             final List<N> result) {
  217.         if (node != null) {
  218.             if (parentIsPartitionNode && node.isOutside()) {
  219.                 result.add(node);
  220.             }

  221.             final boolean partitionNode = isPartitionNode(node);

  222.             collectOutsidePartitionedLeavesRecursive(node.getMinus(), partitionNode, result);
  223.             collectOutsidePartitionedLeavesRecursive(node.getPlus(), partitionNode, result);
  224.         }
  225.     }

  226.     /** Return true if {@code sub} touches an inside leaf node anywhere in the subtree rooted at {@code node}.
  227.      * @param sub convex subset to check
  228.      * @param node root node of the subtree to test against
  229.      * @return true if {@code sub} touches an inside leaf node anywhere in the subtree rooted at {@code node}
  230.      */
  231.     private boolean touchesInside(final HyperplaneConvexSubset<P> sub, final N node) {
  232.         if (sub != null) {
  233.             if (node.isLeaf()) {
  234.                 return node.isInside();
  235.             } else {
  236.                 final Split<? extends HyperplaneConvexSubset<P>> split = sub.split(node.getCutHyperplane());

  237.                 return touchesInside(split.getMinus(), node.getMinus()) ||
  238.                         touchesInside(split.getPlus(), node.getPlus());

  239.             }
  240.         }

  241.         return false;
  242.     }

  243.     /** Return true if the given node is marked as a partition node.
  244.      * @param node node to check
  245.      * @return true if the given node is marked as a partition node
  246.      */
  247.     private boolean isPartitionNode(final N node) {
  248.         return partitionNodes.contains(node);
  249.     }

  250.     /** Throw an exception if the instance is no longer accepting partitions.
  251.      * @throws IllegalStateException if the instance is no longer accepting partitions
  252.      */
  253.     private void ensureInsertingPartitions() {
  254.         if (!insertingPartitions) {
  255.             throw new IllegalStateException("Cannot insert partitions after boundaries have been inserted");
  256.         }
  257.     }
  258. }