AbstractPartitionedRegionBuilder.java
- /*
- * 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.core.partitioning.bsp;
- import java.util.ArrayList;
- import java.util.Comparator;
- import java.util.HashSet;
- import java.util.List;
- import java.util.Set;
- import java.util.function.BiConsumer;
- import org.apache.commons.geometry.core.Point;
- import org.apache.commons.geometry.core.RegionLocation;
- import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
- import org.apache.commons.geometry.core.partitioning.Split;
- import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree.SubtreeInitializer;
- import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree.AbstractRegionNode;
- /** Class encapsulating logic for building regions by inserting boundaries into a BSP
- * tree containing structural cuts, i.e. cuts where both sides of the cut have the same region
- * location. This technique only produces accurate results when the inserted boundaries define
- * the entire surface of the region. However, for valid input boundaries, significant performance
- * improvements can be achieved due to the reduced height of the tree, especially where large
- * numbers of boundaries are involved and/or the defined region is convex.
- *
- * <h2>Implementation Notes</h2>
- *
- * <p>This class constructs regions in two phases: (1) <em>partition insertion</em> and (2) <em>boundary insertion</em>.
- * Instances begin in the <em>partition insertion</em> phase. Here, partitions can be inserted into the empty tree
- * using the standard BSP insertion logic. The {@link RegionCutRule#INHERIT INHERIT} cut rule is used so that the
- * represented region remains empty even as partitions are inserted.
- * </p>
- *
- * <p>The instance moves into the <em>boundary insertion</em> phase when the caller inserts the first region boundary.
- * Attempting to insert a partition after this point results in an {@code IllegalStateException}. This ensures that
- * partitioning cuts are always located higher up the tree than boundary cuts.</p>
- *
- * <p>After all boundaries are inserted, the tree undergoes final processing to ensure that the region is consistent
- * and that unnecessary nodes are removed.</p>
- *
- * <p>This class does not expose any public methods so that subclasses can present their own
- * public API, tailored to the specific types being worked with. In particular, most subclasses
- * will want to restrict the tree types used with the algorithm, which is difficult to implement
- * cleanly at this level.</p>
- * @param <P> Point implementation type
- * @param <N> BSP tree node implementation type
- */
- public abstract class AbstractPartitionedRegionBuilder<
- P extends Point<P>,
- N extends AbstractRegionNode<P, N>> {
- /** Comparator for sorting nodes with the deepest nodes first. */
- private static final Comparator<BSPTree.Node<?, ?>> DEEPEST_FIRST_ORDER =
- (a, b) -> Integer.compare(b.depth(), a.depth());
- /** Tree being constructed. */
- private final AbstractRegionBSPTree<P, N> tree;
- /** Subtree initializer for inserted boundaries. */
- private final SubtreeInitializer<N> subtreeInit;
- /** Flag indicating whether or not partitions may still be inserted into the tree. */
- private boolean insertingPartitions = true;
- /** Set of all internal nodes used as partitioning nodes. */
- private final Set<N> partitionNodes = new HashSet<>();
- /** Construct a new instance that builds a partitioned region in the given tree. The tree must
- * be empty.
- * @param tree tree to build the region in; must be empty
- * @throws IllegalArgumentException if the tree is not empty
- */
- protected AbstractPartitionedRegionBuilder(final AbstractRegionBSPTree<P, N> tree) {
- if (!tree.isEmpty()) {
- throw new IllegalArgumentException("Tree must be empty");
- }
- this.tree = tree;
- this.subtreeInit = tree.getSubtreeInitializer(RegionCutRule.MINUS_INSIDE);
- }
- /** Internal method to build and return the tree representing the final partitioned region.
- * @return the partitioned region
- */
- protected AbstractRegionBSPTree<P, N> buildInternal() {
- // condense to combine homogenous leaf nodes
- tree.condense();
- // propagate region interiors to partitioned nodes that have not received
- // a boundary
- if (propagateRegionInterior()) {
- // condense again since some leaf nodes changed
- tree.condense();
- }
- return tree;
- }
- /** Internal method to insert a partition into the tree.
- * @param partition partition to insert
- * @throws IllegalStateException if a boundary has previously been inserted
- */
- protected void insertPartitionInternal(final HyperplaneConvexSubset<P> partition) {
- ensureInsertingPartitions();
- tree.insert(partition, RegionCutRule.INHERIT);
- }
- /** Internal method to insert a region boundary into the tree.
- * @param boundary boundary to insert
- */
- protected void insertBoundaryInternal(final HyperplaneConvexSubset<P> boundary) {
- if (insertingPartitions) {
- // switch to inserting boundaries; place all current internal nodes into
- // a set for easy identification
- for (final N node : tree.nodes()) {
- if (node.isInternal()) {
- partitionNodes.add(node);
- }
- }
- insertingPartitions = false;
- }
- insertBoundaryRecursive(tree.getRoot(), boundary, boundary.getHyperplane().span(),
- (leaf, cut) -> tree.setNodeCut(leaf, cut, subtreeInit));
- }
- /** Insert a region boundary into the tree.
- * @param node node to insert into
- * @param insert the hyperplane convex subset to insert
- * @param trimmed version of the hyperplane convex subset filling the entire space of {@code node}
- * @param leafFn function to apply to leaf nodes
- */
- private void insertBoundaryRecursive(final N node, final HyperplaneConvexSubset<P> insert,
- final HyperplaneConvexSubset<P> trimmed, final BiConsumer<N, HyperplaneConvexSubset<P>> leafFn) {
- if (node.isLeaf()) {
- leafFn.accept(node, trimmed);
- } else {
- insertBoundaryRecursiveInternalNode(node, insert, trimmed, leafFn);
- }
- }
- /** Recursive boundary insertion method for internal nodes.
- * @param node node to insert into
- * @param insert the hyperplane convex subset to insert
- * @param trimmed version of the hyperplane convex subset filling the entire space of {@code node}
- * @param leafFn function to apply to leaf nodes
- * @see #insertBoundaryRecursive(AbstractRegionNode, HyperplaneConvexSubset, HyperplaneConvexSubset, BiConsumer)
- */
- private void insertBoundaryRecursiveInternalNode(final N node, final HyperplaneConvexSubset<P> insert,
- final HyperplaneConvexSubset<P> trimmed, final BiConsumer<N, HyperplaneConvexSubset<P>> leafFn) {
- final Split<? extends HyperplaneConvexSubset<P>> insertSplit =
- insert.split(node.getCutHyperplane());
- final HyperplaneConvexSubset<P> minus = insertSplit.getMinus();
- final HyperplaneConvexSubset<P> plus = insertSplit.getPlus();
- if (minus == null && plus == null && isPartitionNode(node)) {
- // the inserted boundary lies directly on a partition; proceed down the tree with the
- // rest of the insertion algorithm but instead of cutting the final leaf nodes, just
- // set the location
- // remove this node from the set of partition nodes since this is now a boundary cut
- partitionNodes.remove(node);
- final boolean sameOrientation = node.getCutHyperplane().similarOrientation(insert.getHyperplane());
- final N insertMinus = sameOrientation ? node.getMinus() : node.getPlus();
- final N insertPlus = sameOrientation ? node.getPlus() : node.getMinus();
- insertBoundaryRecursive(insertMinus, insert, trimmed,
- (leaf, cut) -> leaf.setLocation(RegionLocation.INSIDE));
- insertBoundaryRecursive(insertPlus, insert, trimmed,
- (leaf, cut) -> leaf.setLocation(RegionLocation.OUTSIDE));
- } else if (minus != null || plus != null) {
- final Split<? extends HyperplaneConvexSubset<P>> trimmedSplit =
- trimmed.split(node.getCutHyperplane());
- final HyperplaneConvexSubset<P> trimmedMinus = trimmedSplit.getMinus();
- final HyperplaneConvexSubset<P> trimmedPlus = trimmedSplit.getPlus();
- if (minus != null) {
- insertBoundaryRecursive(node.getMinus(), minus, trimmedMinus, leafFn);
- }
- if (plus != null) {
- insertBoundaryRecursive(node.getPlus(), plus, trimmedPlus, leafFn);
- }
- }
- }
- /** Propagate the region interior to partitioned leaf nodes that have not had a boundary
- * inserted.
- * @return true if any nodes were changed
- */
- private boolean propagateRegionInterior() {
- final List<N> outsidePartitionedLeaves = getOutsidePartitionedLeaves();
- outsidePartitionedLeaves.sort(DEEPEST_FIRST_ORDER);
- int changeCount = 0;
- N parent;
- N sibling;
- for (final N leaf : outsidePartitionedLeaves) {
- parent = leaf.getParent();
- // check if the parent cut touches the inside anywhere on the side opposite of
- // this leaf; if so, then this node should also be inside
- sibling = leaf.isMinus() ?
- parent.getPlus() :
- parent.getMinus();
- if (touchesInside(parent.getCut(), sibling)) {
- leaf.setLocation(RegionLocation.INSIDE);
- ++changeCount;
- }
- }
- return changeCount > 0;
- }
- /** Return a list containing all outside leaf nodes that have a parent marked as a partition node.
- * @return a list containing all outside leaf nodes that have a parent marked as a partition node
- */
- private List<N> getOutsidePartitionedLeaves() {
- final List<N> result = new ArrayList<>();
- final N root = tree.getRoot();
- collectOutsidePartitionedLeavesRecursive(root, false, result);
- return result;
- }
- /** Recursively collect all outside leaf nodes that have a parent marked as a partition node.
- * @param node root of the subtree to collect nodes from
- * @param parentIsPartitionNode true if the parent of {@code node} is a partition node
- * @param result list of accumulated results
- */
- private void collectOutsidePartitionedLeavesRecursive(final N node, final boolean parentIsPartitionNode,
- final List<N> result) {
- if (node != null) {
- if (parentIsPartitionNode && node.isOutside()) {
- result.add(node);
- }
- final boolean partitionNode = isPartitionNode(node);
- collectOutsidePartitionedLeavesRecursive(node.getMinus(), partitionNode, result);
- collectOutsidePartitionedLeavesRecursive(node.getPlus(), partitionNode, result);
- }
- }
- /** Return true if {@code sub} touches an inside leaf node anywhere in the subtree rooted at {@code node}.
- * @param sub convex subset to check
- * @param node root node of the subtree to test against
- * @return true if {@code sub} touches an inside leaf node anywhere in the subtree rooted at {@code node}
- */
- private boolean touchesInside(final HyperplaneConvexSubset<P> sub, final N node) {
- if (sub != null) {
- if (node.isLeaf()) {
- return node.isInside();
- } else {
- final Split<? extends HyperplaneConvexSubset<P>> split = sub.split(node.getCutHyperplane());
- return touchesInside(split.getMinus(), node.getMinus()) ||
- touchesInside(split.getPlus(), node.getPlus());
- }
- }
- return false;
- }
- /** Return true if the given node is marked as a partition node.
- * @param node node to check
- * @return true if the given node is marked as a partition node
- */
- private boolean isPartitionNode(final N node) {
- return partitionNodes.contains(node);
- }
- /** Throw an exception if the instance is no longer accepting partitions.
- * @throws IllegalStateException if the instance is no longer accepting partitions
- */
- private void ensureInsertingPartitions() {
- if (!insertingPartitions) {
- throw new IllegalStateException("Cannot insert partitions after boundaries have been inserted");
- }
- }
- }