View Javadoc
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  
19  import java.util.ArrayList;
20  import java.util.Iterator;
21  import java.util.List;
22  import java.util.function.Function;
23  import java.util.stream.Stream;
24  
25  import org.apache.commons.geometry.core.Point;
26  import org.apache.commons.geometry.core.RegionLocation;
27  import org.apache.commons.geometry.core.internal.IteratorTransform;
28  import org.apache.commons.geometry.core.partitioning.BoundarySource;
29  import org.apache.commons.geometry.core.partitioning.Hyperplane;
30  import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
31  import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
32  import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
33  import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
34  import org.apache.commons.geometry.core.partitioning.Split;
35  import org.apache.commons.geometry.core.partitioning.SplitLocation;
36  import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.ClosestFirstVisitor;
37  
38  /** Abstract {@link BSPTree} specialized for representing regions of space. For example,
39   * this class can be used to represent polygons in Euclidean 2D space and polyhedrons
40   * in Euclidean 3D space.
41   *
42   * <p>This class is not thread safe.</p>
43   * @param <P> Point implementation type
44   * @param <N> BSP tree node implementation type
45   * @see HyperplaneBoundedRegion
46   */
47  public abstract class AbstractRegionBSPTree<
48          P extends Point<P>,
49          N extends AbstractRegionBSPTree.AbstractRegionNode<P, N>>
50      extends AbstractBSPTree<P, N> implements HyperplaneBoundedRegion<P> {
51  
52      /** The default {@link RegionCutRule}. */
53      private static final RegionCutRule DEFAULT_REGION_CUT_RULE = RegionCutRule.MINUS_INSIDE;
54  
55      /** Value used to indicate an unknown size. */
56      private static final double UNKNOWN_SIZE = -1.0;
57  
58      /** The region boundary size; this is computed when requested and then cached. */
59      private double boundarySize = UNKNOWN_SIZE;
60  
61      /** The current size properties for the region. */
62      private RegionSizeProperties<P> regionSizeProperties;
63  
64      /** Construct a new region will the given boolean determining whether or not the
65       * region will be full (including the entire space) or empty (excluding the entire
66       * space).
67       * @param full if true, the region will cover the entire space, otherwise it will
68       *      be empty
69       */
70      protected AbstractRegionBSPTree(final boolean full) {
71          getRoot().setLocationValue(full ? RegionLocation.INSIDE : RegionLocation.OUTSIDE);
72      }
73  
74      /** {@inheritDoc} */
75      @Override
76      public boolean isEmpty() {
77          return !hasNodeWithLocationRecursive(getRoot(), RegionLocation.INSIDE);
78      }
79  
80      /** {@inheritDoc} */
81      @Override
82      public boolean isFull() {
83          return !hasNodeWithLocationRecursive(getRoot(), RegionLocation.OUTSIDE);
84      }
85  
86      /** Return true if any node in the subtree rooted at the given node has a location with the
87       * given value.
88       * @param node the node at the root of the subtree to search
89       * @param location the location to find
90       * @return true if any node in the subtree has the given location
91       */
92      private boolean hasNodeWithLocationRecursive(final AbstractRegionNode<P, N> node, final RegionLocation location) {
93          if (node == null) {
94              return false;
95          }
96  
97          return node.getLocation() == location ||
98                  hasNodeWithLocationRecursive(node.getMinus(), location) ||
99                  hasNodeWithLocationRecursive(node.getPlus(), location);
100     }
101 
102     /** Modify this instance so that it contains the entire space.
103      * @see #isFull()
104      */
105     public void setFull() {
106         final N root = getRoot();
107 
108         root.clearCut();
109         root.setLocationValue(RegionLocation.INSIDE);
110     }
111 
112     /** Modify this instance so that is is completely empty.
113      * @see #isEmpty()
114      */
115     public void setEmpty() {
116         final N root = getRoot();
117 
118         root.clearCut();
119         root.setLocationValue(RegionLocation.OUTSIDE);
120     }
121 
122     /** {@inheritDoc} */
123     @Override
124     public double getSize() {
125         return getRegionSizeProperties().getSize();
126     }
127 
128     /** {@inheritDoc} */
129     @Override
130     public double getBoundarySize() {
131         if (boundarySize < 0) {
132             double sum = 0.0;
133 
134             RegionCutBoundary<P> boundary;
135             for (final AbstractRegionNode<P, N> node : nodes()) {
136                 boundary = node.getCutBoundary();
137                 if (boundary != null) {
138                     sum += boundary.getSize();
139                 }
140             }
141 
142             boundarySize = sum;
143         }
144 
145         return boundarySize;
146     }
147 
148     /** Insert a hyperplane subset into the tree, using the default {@link RegionCutRule} of
149      * {@link RegionCutRule#MINUS_INSIDE MINUS_INSIDE}.
150      * @param sub the hyperplane subset to insert into the tree
151      */
152     public void insert(final HyperplaneSubset<P> sub) {
153         insert(sub, DEFAULT_REGION_CUT_RULE);
154     }
155 
156     /** Insert a hyperplane subset into the tree.
157      * @param sub the hyperplane subset to insert into the tree
158      * @param cutRule rule used to determine the region locations of new child nodes
159      */
160     public void insert(final HyperplaneSubset<P> sub, final RegionCutRule cutRule) {
161         insert(sub.toConvex(), cutRule);
162     }
163 
164     /** Insert a hyperplane convex subset into the tree, using the default {@link RegionCutRule} of
165      * {@link RegionCutRule#MINUS_INSIDE MINUS_INSIDE}.
166      * @param convexSub the hyperplane convex subset to insert into the tree
167      */
168     public void insert(final HyperplaneConvexSubset<P> convexSub) {
169         insert(convexSub, DEFAULT_REGION_CUT_RULE);
170     }
171 
172     /** Insert a hyperplane convex subset into the tree.
173      * @param convexSub the hyperplane convex subset to insert into the tree
174      * @param cutRule rule used to determine the region locations of new child nodes
175      */
176     public void insert(final HyperplaneConvexSubset<P> convexSub, final RegionCutRule cutRule) {
177         insert(convexSub, getSubtreeInitializer(cutRule));
178     }
179 
180     /** Insert a set of hyperplane convex subsets into the tree, using the default {@link RegionCutRule} of
181      * {@link RegionCutRule#MINUS_INSIDE MINUS_INSIDE}.
182      * @param convexSubs iterable containing a collection of hyperplane convex subsets
183      *      to insert into the tree
184      */
185     public void insert(final Iterable<? extends HyperplaneConvexSubset<P>> convexSubs) {
186         insert(convexSubs, DEFAULT_REGION_CUT_RULE);
187     }
188 
189     /** Insert a set of hyperplane convex subsets into the tree.
190      * @param convexSubs iterable containing a collection of hyperplane convex subsets
191      *      to insert into the tree
192      * @param cutRule rule used to determine the region locations of new child nodes
193      */
194     public void insert(final Iterable<? extends HyperplaneConvexSubset<P>> convexSubs, final RegionCutRule cutRule) {
195         for (final HyperplaneConvexSubset<P> convexSub : convexSubs) {
196             insert(convexSub, cutRule);
197         }
198     }
199 
200     /** Insert all hyperplane convex subsets from the given source into the tree, using the default
201      * {@link RegionCutRule} of {@link RegionCutRule#MINUS_INSIDE MINUS_INSIDE}.
202      * @param boundarySrc source of boundary hyperplane subsets to insert
203      *      into the tree
204      */
205     public void insert(final BoundarySource<? extends HyperplaneConvexSubset<P>> boundarySrc) {
206         insert(boundarySrc, DEFAULT_REGION_CUT_RULE);
207     }
208 
209     /** Insert all hyperplane convex subsets from the given source into the tree.
210      * @param boundarySrc source of boundary hyperplane subsets to insert
211      *      into the tree
212      * @param cutRule rule used to determine the region locations of new child nodes
213      */
214     public void insert(final BoundarySource<? extends HyperplaneConvexSubset<P>> boundarySrc,
215             final RegionCutRule cutRule) {
216         try (Stream<? extends HyperplaneConvexSubset<P>> stream = boundarySrc.boundaryStream()) {
217             stream.forEach(c -> insert(c, cutRule));
218         }
219     }
220 
221     /** Get the subtree initializer to use for the given region cut rule.
222      * @param cutRule the cut rule to get an initializer for
223      * @return the subtree initializer for the given region cut rule
224      */
225     protected SubtreeInitializer<N> getSubtreeInitializer(final RegionCutRule cutRule) {
226         switch (cutRule) {
227         case INHERIT:
228             return root -> {
229                 final RegionLocation rootLoc = root.getLocation();
230 
231                 root.getMinus().setLocationValue(rootLoc);
232                 root.getPlus().setLocationValue(rootLoc);
233             };
234         case PLUS_INSIDE:
235             return root -> {
236                 root.getMinus().setLocationValue(RegionLocation.OUTSIDE);
237                 root.getPlus().setLocationValue(RegionLocation.INSIDE);
238             };
239         default:
240             return root -> {
241                 root.getMinus().setLocationValue(RegionLocation.INSIDE);
242                 root.getPlus().setLocationValue(RegionLocation.OUTSIDE);
243             };
244         }
245     }
246 
247     /** Return an {@link Iterable} for iterating over the boundaries of the region.
248      * Each boundary is oriented such that its plus side points to the outside of the
249      * region. The exact ordering of the boundaries is determined by the internal structure
250      * of the tree.
251      * @return an {@link Iterable} for iterating over the boundaries of the region
252      * @see #getBoundaries()
253      */
254     public Iterable<? extends HyperplaneConvexSubset<P>> boundaries() {
255         return createBoundaryIterable(Function.identity());
256     }
257 
258     /** Internal method for creating the iterable instances used to iterate the region boundaries.
259      * @param typeConverter function to convert the generic hyperplane subset type into
260      *      the type specific for this tree
261      * @param <C> HyperplaneConvexSubset implementation type
262      * @return an iterable to iterating the region boundaries
263      */
264     protected <C extends HyperplaneConvexSubset<P>> Iterable<C> createBoundaryIterable(
265             final Function<HyperplaneConvexSubset<P>, C> typeConverter) {
266 
267         return () -> new RegionBoundaryIterator<>(
268                 getRoot().nodes().iterator(),
269                 typeConverter);
270     }
271 
272     /** Return a list containing the boundaries of the region. Each boundary is oriented such
273      * that its plus side points to the outside of the region. The exact ordering of
274      * the boundaries is determined by the internal structure of the tree.
275      * @return a list of the boundaries of the region
276      */
277     public List<? extends HyperplaneConvexSubset<P>> getBoundaries() {
278         return createBoundaryList(Function.identity());
279     }
280 
281     /** Internal method for creating a list of the region boundaries.
282      * @param typeConverter function to convert the generic convex subset type into
283      *      the type specific for this tree
284      * @param <C> HyperplaneConvexSubset implementation type
285      * @return a list of the region boundaries
286      */
287     protected <C extends HyperplaneConvexSubset<P>> List<C> createBoundaryList(
288             final Function<HyperplaneConvexSubset<P>, C> typeConverter) {
289 
290         final List<C> result = new ArrayList<>();
291 
292         final RegionBoundaryIterator<P, C, N> it = new RegionBoundaryIterator<>(nodes().iterator(), typeConverter);
293         it.forEachRemaining(result::add);
294 
295         return result;
296     }
297 
298     /** {@inheritDoc} */
299     @Override
300     public P project(final P pt) {
301         final BoundaryProjector<P, N> projector = new BoundaryProjector<>(pt);
302         accept(projector);
303 
304         return projector.getProjected();
305     }
306 
307     /** {@inheritDoc} */
308     @Override
309     public P getCentroid() {
310         return getRegionSizeProperties().getCentroid();
311     }
312 
313     /** Helper method implementing the algorithm for splitting a tree by a hyperplane. Subclasses
314      * should call this method with two instantiated trees of the correct type.
315      * @param splitter splitting hyperplane
316      * @param minus tree that will contain the minus side of the split result
317      * @param plus tree that will contain the plus side of the split result
318      * @param <T> Tree implementation type
319      * @return result of splitting this tree with the given hyperplane
320      */
321     protected <T extends AbstractRegionBSPTree<P, N>> Split<T> split(final Hyperplane<P> splitter,
322             final T minus, final T plus) {
323 
324         splitIntoTrees(splitter, minus, plus);
325 
326         T splitMinus = null;
327         T splitPlus = null;
328 
329         if (minus != null) {
330             minus.getRoot().getPlus().setLocationValue(RegionLocation.OUTSIDE);
331             minus.condense();
332 
333             splitMinus = minus.isEmpty() ? null : minus;
334         }
335         if (plus != null) {
336             plus.getRoot().getMinus().setLocationValue(RegionLocation.OUTSIDE);
337             plus.condense();
338 
339             splitPlus = plus.isEmpty() ? null : plus;
340         }
341 
342         return new Split<>(splitMinus, splitPlus);
343     }
344 
345     /** Get the size-related properties for the region. The value is computed
346      * lazily and cached.
347      * @return the size-related properties for the region
348      */
349     protected RegionSizeProperties<P> getRegionSizeProperties() {
350         if (regionSizeProperties == null) {
351             regionSizeProperties = computeRegionSizeProperties();
352         }
353 
354         return regionSizeProperties;
355     }
356 
357     /** Compute the size-related properties of the region.
358      * @return object containing size properties for the region
359      */
360     protected abstract RegionSizeProperties<P> computeRegionSizeProperties();
361 
362     /** {@inheritDoc}
363      *
364      * <p>If the point is {@link org.apache.commons.geometry.core.Spatial#isNaN() NaN}, then
365      * {@link RegionLocation#OUTSIDE} is returned.</p>
366      */
367     @Override
368     public RegionLocation classify(final P point) {
369         if (point.isNaN()) {
370             return RegionLocation.OUTSIDE;
371         }
372 
373         return classifyRecursive(getRoot(), point);
374     }
375 
376     /** Recursively classify a point with respect to the region.
377      * @param node the node to classify against
378      * @param point the point to classify
379      * @return the classification of the point with respect to the region rooted
380      *      at the given node
381      */
382     private RegionLocation classifyRecursive(final AbstractRegionNode<P, N> node, final P point) {
383         if (node.isLeaf()) {
384             // the point is in a leaf, so the classification is just the leaf location
385             return node.getLocation();
386         } else {
387             final HyperplaneLocation cutLoc = node.getCutHyperplane().classify(point);
388 
389             if (cutLoc == HyperplaneLocation.MINUS) {
390                 return classifyRecursive(node.getMinus(), point);
391             } else if (cutLoc == HyperplaneLocation.PLUS) {
392                 return classifyRecursive(node.getPlus(), point);
393             } else {
394                 // the point is on the cut boundary; classify against both child
395                 // subtrees and see if we end up with the same result or not
396                 final RegionLocation minusLoc = classifyRecursive(node.getMinus(), point);
397                 final RegionLocation plusLoc = classifyRecursive(node.getPlus(), point);
398 
399                 if (minusLoc == plusLoc) {
400                     return minusLoc;
401                 }
402                 return RegionLocation.BOUNDARY;
403             }
404         }
405     }
406 
407     /** Change this region into its complement. All inside nodes become outside
408      * nodes and vice versa. The orientations of the node cuts are not modified.
409      */
410     public void complement() {
411         complementRecursive(getRoot());
412     }
413 
414     /** Set this instance to be the complement of the given tree. The argument
415      * is not modified.
416      * @param tree the tree to become the complement of
417      */
418     public void complement(final AbstractRegionBSPTree<P, N> tree) {
419         copySubtree(tree.getRoot(), getRoot());
420         complementRecursive(getRoot());
421     }
422 
423     /** Recursively switch all inside nodes to outside nodes and vice versa.
424      * @param node the node at the root of the subtree to switch
425      */
426     private void complementRecursive(final AbstractRegionNode<P, N> node) {
427         if (node != null) {
428             final RegionLocation newLoc = (node.getLocation() == RegionLocation.INSIDE) ?
429                     RegionLocation.OUTSIDE :
430                     RegionLocation.INSIDE;
431 
432             node.setLocationValue(newLoc);
433 
434             complementRecursive(node.getMinus());
435             complementRecursive(node.getPlus());
436         }
437     }
438 
439     /** Compute the union of this instance and the given region, storing the result back in
440      * this instance. The argument is not modified.
441      * @param other the tree to compute the union with
442      */
443     public void union(final AbstractRegionBSPTree<P, N> other) {
444         new UnionOperator<P, N>().apply(this, other, this);
445     }
446 
447     /** Compute the union of the two regions passed as arguments and store the result in
448      * this instance. Any nodes currently existing in this instance are removed.
449      * @param a first argument to the union operation
450      * @param b second argument to the union operation
451      */
452     public void union(final AbstractRegionBSPTree<P, N> a, final AbstractRegionBSPTree<P, N> b) {
453         new UnionOperator<P, N>().apply(a, b, this);
454     }
455 
456     /** Compute the intersection of this instance and the given region, storing the result back in
457      * this instance. The argument is not modified.
458      * @param other the tree to compute the intersection with
459      */
460     public void intersection(final AbstractRegionBSPTree<P, N> other) {
461         new IntersectionOperator<P, N>().apply(this, other, this);
462     }
463 
464     /** Compute the intersection of the two regions passed as arguments and store the result in
465      * this instance. Any nodes currently existing in this instance are removed.
466      * @param a first argument to the intersection operation
467      * @param b second argument to the intersection operation
468      */
469     public void intersection(final AbstractRegionBSPTree<P, N> a, final AbstractRegionBSPTree<P, N> b) {
470         new IntersectionOperator<P, N>().apply(a, b, this);
471     }
472 
473     /** Compute the difference of this instance and the given region, storing the result back in
474      * this instance. The argument is not modified.
475      * @param other the tree to compute the difference with
476      */
477     public void difference(final AbstractRegionBSPTree<P, N> other) {
478         new DifferenceOperator<P, N>().apply(this, other, this);
479     }
480 
481     /** Compute the difference of the two regions passed as arguments and store the result in
482      * this instance. Any nodes currently existing in this instance are removed.
483      * @param a first argument to the difference operation
484      * @param b second argument to the difference operation
485      */
486     public void difference(final AbstractRegionBSPTree<P, N> a, final AbstractRegionBSPTree<P, N> b) {
487         new DifferenceOperator<P, N>().apply(a, b, this);
488     }
489 
490     /** Compute the symmetric difference (xor) of this instance and the given region, storing the result back in
491      * this instance. The argument is not modified.
492      * @param other the tree to compute the symmetric difference with
493      */
494     public void xor(final AbstractRegionBSPTree<P, N> other) {
495         new XorOperator<P, N>().apply(this, other, this);
496     }
497 
498     /** Compute the symmetric difference (xor) of the two regions passed as arguments and store the result in
499      * this instance. Any nodes currently existing in this instance are removed.
500      * @param a first argument to the symmetric difference operation
501      * @param b second argument to the symmetric difference operation
502      */
503     public void xor(final AbstractRegionBSPTree<P, N> a, final AbstractRegionBSPTree<P, N> b) {
504         new XorOperator<P, N>().apply(a, b, this);
505     }
506 
507     /** Condense this tree by removing redundant subtrees, returning true if the
508      * tree structure was modified.
509      *
510      * <p>This operation can be used to reduce the total number of nodes in the
511      * tree after performing node manipulations. For example, if two sibling leaf
512      * nodes both represent the same {@link RegionLocation}, then there is no reason
513      * from the perspective of the geometric region to retain both nodes. They are
514      * therefore both merged into their parent node. This method performs this
515      * simplification process.
516      * </p>
517      * @return true if the tree structure was modified, otherwise false
518      */
519     public boolean condense() {
520         return new Condenser<P, N>().condense(getRoot());
521     }
522 
523     /** {@inheritDoc} */
524     @Override
525     protected void copyNodeProperties(final N src, final N dst) {
526         dst.setLocationValue(src.getLocation());
527     }
528 
529     /** {@inheritDoc} */
530     @Override
531     protected void invalidate() {
532         super.invalidate();
533 
534         // clear cached region properties
535         boundarySize = UNKNOWN_SIZE;
536         regionSizeProperties = null;
537     }
538 
539     /** {@link BSPTree.Node} implementation for use with {@link AbstractRegionBSPTree}s.
540      * @param <P> Point implementation type
541      * @param <N> BSP tree node implementation type
542      */
543     public abstract static class AbstractRegionNode<P extends Point<P>, N extends AbstractRegionNode<P, N>>
544         extends AbstractBSPTree.AbstractNode<P, N> {
545         /** The location for the node. This will only be set on leaf nodes. */
546         private RegionLocation location;
547 
548         /** Object representing the part of the node cut hyperplane subset that lies on the
549          * region boundary. This is calculated lazily and is only present on internal nodes.
550          */
551         private RegionCutBoundary<P> cutBoundary;
552 
553         /** Simple constructor.
554          * @param tree owning tree instance
555          */
556         protected AbstractRegionNode(final AbstractBSPTree<P, N> tree) {
557             super(tree);
558         }
559 
560         /** {@inheritDoc} */
561         @Override
562         public AbstractRegionBSPTree<P, N> getTree() {
563             // cast to our parent tree type
564             return (AbstractRegionBSPTree<P, N>) super.getTree();
565         }
566 
567         /** Get the location property of the node. Only the locations of leaf nodes are meaningful
568          * as they relate to the region represented by the BSP tree. For example, changing
569          * the location of an internal node will only affect the geometric properties
570          * of the region if the node later becomes a leaf node.
571          * @return the location of the node
572          */
573         public RegionLocation getLocation() {
574             return location;
575         }
576 
577         /** Set the location property for the node. If the location is changed, the tree is
578          * invalidated.
579          *
580          * <p>Only the locations of leaf nodes are meaningful
581          * as they relate to the region represented by the BSP tree. For example, changing
582          * the location of an internal node will only affect the geometric properties
583          * of the region if the node later becomes a leaf node.</p>
584          * @param location the location for the node
585          * @throws IllegalArgumentException if {@code location} is not one of
586          *      {@link RegionLocation#INSIDE INSIDE} or {@link RegionLocation#OUTSIDE OUTSIDE}
587          */
588         public void setLocation(final RegionLocation location) {
589             if (location != RegionLocation.INSIDE && location != RegionLocation.OUTSIDE) {
590                 throw new IllegalArgumentException("Invalid node location: " + location);
591             }
592             if (this.location != location) {
593                 this.location = location;
594 
595                 getTree().invalidate();
596             }
597         }
598 
599         /** True if the node is a leaf node and has a location of {@link RegionLocation#INSIDE}.
600          * @return true if the node is a leaf node and has a location of
601          *      {@link RegionLocation#INSIDE}
602          */
603         public boolean isInside() {
604             return isLeaf() && getLocation() == RegionLocation.INSIDE;
605         }
606 
607         /** True if the node is a leaf node and has a location of {@link RegionLocation#OUTSIDE}.
608          * @return true if the node is a leaf node and has a location of
609          *      {@link RegionLocation#OUTSIDE}
610          */
611         public boolean isOutside() {
612             return isLeaf() && getLocation() == RegionLocation.OUTSIDE;
613         }
614 
615         /** Insert a cut into this node, using the default region cut rule of
616          * {@link RegionCutRule#MINUS_INSIDE}.
617          * @param cutter the hyperplane to cut the node's region with
618          * @return true if the cutting hyperplane intersected the node's region, resulting
619          *      in the creation of new child nodes
620          * @see #insertCut(Hyperplane, RegionCutRule)
621          */
622         public boolean insertCut(final Hyperplane<P> cutter) {
623             return insertCut(cutter, DEFAULT_REGION_CUT_RULE);
624         }
625 
626         /** Insert a cut into this node. If the given hyperplane intersects
627          * this node's region, then the node's cut is set to the {@link HyperplaneConvexSubset}
628          * representing the intersection, new plus and minus child leaf nodes
629          * are assigned, and true is returned. If the hyperplane does not intersect
630          * the node's region, then the node's cut and plus and minus child references
631          * are all set to null (ie, it becomes a leaf node) and false is returned. In
632          * either case, any existing cut and/or child nodes are removed by this method.
633          * @param cutter the hyperplane to cut the node's region with
634          * @param cutRule rule used to determine the region locations of newly created
635          *      child nodes
636          * @return true if the cutting hyperplane intersected the node's region, resulting
637          *      in the creation of new child nodes
638          */
639         public boolean insertCut(final Hyperplane<P> cutter, final RegionCutRule cutRule) {
640             final AbstractRegionBSPTree<P, N> tree = getTree();
641             return tree.cutNode(getSelf(), cutter, tree.getSubtreeInitializer(cutRule));
642         }
643 
644         /** Remove the cut from this node. Returns true if the node previously had a cut.
645          * @return true if the node had a cut before the call to this method
646          */
647         public boolean clearCut() {
648             return getTree().removeNodeCut(getSelf());
649         }
650 
651         /** Cut this node with the given hyperplane. The same node is returned, regardless of
652          * the outcome of the cut operation. If the operation succeeded, then the node will
653          * have plus and minus child nodes.
654          * @param cutter the hyperplane to cut the node's region with
655          * @return this node
656          * @see #insertCut(Hyperplane)
657          */
658         public N cut(final Hyperplane<P> cutter) {
659             return cut(cutter, DEFAULT_REGION_CUT_RULE);
660         }
661 
662         /** Cut this node with the given hyperplane, using {@code cutRule} to determine the region
663          * locations of any new child nodes. The same node is returned, regardless of
664          * the outcome of the cut operation. If the operation succeeded, then the node will
665          * have plus and minus child nodes.
666          * @param cutter the hyperplane to cut the node's region with
667          * @param cutRule rule used to determine the region locations of newly created
668          *      child nodes
669          * @return this node
670          * @see #insertCut(Hyperplane, RegionCutRule)
671          */
672         public N cut(final Hyperplane<P> cutter, final RegionCutRule cutRule) {
673             this.insertCut(cutter, cutRule);
674 
675             return getSelf();
676         }
677 
678         /** Get the portion of the node's cut that lies on the boundary of the region.
679          * @return the portion of the node's cut that lies on the boundary of
680          *      the region
681          */
682         public RegionCutBoundary<P> getCutBoundary() {
683             if (!isLeaf()) {
684                 checkValid();
685 
686                 if (cutBoundary == null) {
687                     cutBoundary = computeBoundary();
688                 }
689             }
690 
691             return cutBoundary;
692         }
693 
694         /** Compute the portion of the node's cut that lies on the boundary of the region.
695          * This method must only be called on internal nodes.
696          * @return object representing the portions of the node's cut that lie on the region's boundary
697          */
698         private RegionCutBoundary<P> computeBoundary() {
699             final HyperplaneConvexSubset<P> sub = getCut();
700 
701             // find the portions of the node cut hyperplane subset that touch inside and
702             // outside cells in the minus sub-tree
703             final List<HyperplaneConvexSubset<P>> minusIn = new ArrayList<>();
704             final List<HyperplaneConvexSubset<P>> minusOut = new ArrayList<>();
705 
706             characterizeHyperplaneSubset(sub, getMinus(), minusIn, minusOut);
707 
708             final ArrayList<HyperplaneConvexSubset<P>> insideFacing = new ArrayList<>();
709             final ArrayList<HyperplaneConvexSubset<P>> outsideFacing = new ArrayList<>();
710 
711             if (!minusIn.isEmpty()) {
712                 // Add to the boundary anything that touches an inside cell in the minus sub-tree
713                 // and an outside cell in the plus sub-tree. These portions are oriented with their
714                 // plus side pointing to the outside of the region.
715                 for (final HyperplaneConvexSubset<P> minusInFragment : minusIn) {
716                     characterizeHyperplaneSubset(minusInFragment, getPlus(), null, outsideFacing);
717                 }
718             }
719 
720             if (!minusOut.isEmpty()) {
721                 // Add to the boundary anything that touches an outside cell in the minus sub-tree
722                 // and an inside cell in the plus sub-tree. These portions are oriented with their
723                 // plus side pointing to the inside of the region.
724                 for (final HyperplaneConvexSubset<P> minusOutFragment : minusOut) {
725                     characterizeHyperplaneSubset(minusOutFragment, getPlus(), insideFacing, null);
726                 }
727             }
728 
729             insideFacing.trimToSize();
730             outsideFacing.trimToSize();
731 
732             return new RegionCutBoundary<>(
733                     insideFacing.isEmpty() ? null : insideFacing,
734                     outsideFacing.isEmpty() ? null : outsideFacing);
735         }
736 
737         /** Recursive method to characterize a hyperplane convex subset with respect to the region's
738          * boundaries.
739          * @param sub the hyperplane convex subset to characterize
740          * @param node the node to characterize the hyperplane convex subset against
741          * @param in list that will receive the portions of the subset that lie in the inside
742          *      of the region; may be null
743          * @param out list that will receive the portions of the subset that lie on the outside
744          *      of the region; may be null
745          */
746         private void characterizeHyperplaneSubset(final HyperplaneConvexSubset<P> sub,
747                 final AbstractRegionNode<P, N> node, final List<? super HyperplaneConvexSubset<P>> in,
748                 final List<? super HyperplaneConvexSubset<P>> out) {
749 
750             if (sub != null) {
751                 if (node.isLeaf()) {
752                     if (node.isInside() && in != null) {
753                         in.add(sub);
754                     } else if (node.isOutside() && out != null) {
755                         out.add(sub);
756                     }
757                 } else {
758                     final Split<? extends HyperplaneConvexSubset<P>> split = sub.split(node.getCutHyperplane());
759 
760                     // Continue further on down the subtree with the same subset if the
761                     // subset lies directly on the current node's cut
762                     if (split.getLocation() == SplitLocation.NEITHER) {
763                         characterizeHyperplaneSubset(sub, node.getPlus(), in, out);
764                         characterizeHyperplaneSubset(sub, node.getMinus(), in, out);
765                     } else {
766                         characterizeHyperplaneSubset(split.getPlus(), node.getPlus(), in, out);
767                         characterizeHyperplaneSubset(split.getMinus(), node.getMinus(), in, out);
768                     }
769                 }
770             }
771         }
772 
773         /** {@inheritDoc} */
774         @Override
775         public String toString() {
776             final StringBuilder sb = new StringBuilder();
777             sb.append(this.getClass().getSimpleName())
778                 .append("[cut= ")
779                 .append(getCut())
780                 .append(", location= ")
781                 .append(getLocation())
782                 .append("]");
783 
784             return sb.toString();
785         }
786 
787         /** {@inheritDoc} */
788         @Override
789         protected void nodeInvalidated() {
790             super.nodeInvalidated();
791 
792             // null any computed boundary value since it is no longer valid
793             cutBoundary = null;
794         }
795 
796         /** Directly set the value of the location property for the node. No input validation
797          * is performed and the tree is not invalidated.
798          * @param locationValue the new location value for the node
799          * @see #setLocation(RegionLocation)
800          */
801         protected void setLocationValue(final RegionLocation locationValue) {
802             this.location = locationValue;
803         }
804     }
805 
806     /** Class used to compute the point on the region's boundary that is closest to a target point.
807      * @param <P> Point implementation type
808      * @param <N> BSP tree node implementation type
809      */
810     protected static class BoundaryProjector<P extends Point<P>, N extends AbstractRegionNode<P, N>>
811         extends ClosestFirstVisitor<P, N> {
812         /** The projected point. */
813         private P projected;
814 
815         /** The current closest distance to the boundary found. */
816         private double minDist = -1.0;
817 
818         /** Simple constructor.
819          * @param point the point to project onto the region's boundary
820          */
821         public BoundaryProjector(final P point) {
822             super(point);
823         }
824 
825         /** {@inheritDoc} */
826         @Override
827         public Result visit(final N node) {
828             final P point = getTarget();
829 
830             if (node.isInternal() && (minDist < 0.0 || isPossibleClosestCut(node.getCut(), point, minDist))) {
831                 final RegionCutBoundary<P> boundary = node.getCutBoundary();
832                 final P boundaryPt = boundary.closest(point);
833 
834                 final double dist = boundaryPt.distance(point);
835                 final int cmp = Double.compare(dist, minDist);
836 
837                 if (minDist < 0.0 || cmp < 0) {
838                     projected = boundaryPt;
839                     minDist = dist;
840                 } else if (cmp == 0) {
841                     // the two points are the _exact_ same distance from the reference point, so use
842                     // a separate method to disambiguate them
843                     projected = disambiguateClosestPoint(point, projected, boundaryPt);
844                 }
845             }
846 
847             return Result.CONTINUE;
848         }
849 
850         /** Return true if the given node cut is a possible candidate for containing the closest region
851          * boundary point to the target.
852          * @param cut the node cut to test
853          * @param target the target point being projected
854          * @param currentMinDist the smallest distance found so far to a region boundary; this value is guaranteed
855          *      to be non-negative
856          * @return true if the cut is a possible candidate for containing the closest region
857          *      boundary point to the target
858          */
859         protected boolean isPossibleClosestCut(final HyperplaneSubset<P> cut, final P target,
860                 final double currentMinDist) {
861             return Math.abs(cut.getHyperplane().offset(target)) <= currentMinDist;
862         }
863 
864         /** Method used to determine which of points {@code a} and {@code b} should be considered
865          * as the "closest" point to {@code target} when the points are exactly equidistant.
866          * @param target the target point
867          * @param a first point to consider
868          * @param b second point to consider
869          * @return which of {@code a} or {@code b} should be considered as the one closest to
870          *      {@code target}
871          */
872         protected P disambiguateClosestPoint(final P target, final P a, final P b) {
873             return a;
874         }
875 
876         /** Get the projected point on the region's boundary, or null if no point could be found.
877          * @return the projected point on the region's boundary
878          */
879         public P getProjected() {
880             return projected;
881         }
882     }
883 
884     /** Class containing the primary size-related properties of a region. These properties
885      * are typically computed at the same time, so this class serves to encapsulate the result
886      * of the combined computation.
887      * @param <P> Point implementation type
888      */
889     protected static class RegionSizeProperties<P extends Point<P>> {
890         /** The size of the region. */
891         private final double size;
892 
893         /** The centroid of the region. */
894         private final P centroid;
895 
896         /** Simple constructor.
897          * @param size the region size
898          * @param centroid the region centroid
899          */
900         public RegionSizeProperties(final double size, final P centroid) {
901             this.size = size;
902             this.centroid = centroid;
903         }
904 
905         /** Get the size of the region.
906          * @return the size of the region
907          */
908         public double getSize() {
909             return size;
910         }
911 
912         /** Get the centroid of the region.
913          * @return the centroid of the region
914          */
915         public P getCentroid() {
916             return centroid;
917         }
918     }
919 
920     /** Class containing the basic algorithm for merging region BSP trees.
921      * @param <P> Point implementation type
922      * @param <N> BSP tree node implementation type
923      */
924     private abstract static class RegionMergeOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
925         extends AbstractBSPTreeMergeOperator<P, N> {
926 
927         /** Merge two input trees, storing the output in the third. The output tree can be one of the
928          * input trees. The output tree is condensed before the method returns.
929          * @param inputTree1 first input tree
930          * @param inputTree2 second input tree
931          * @param outputTree the tree that will contain the result of the merge; may be one
932          *      of the input trees
933          */
934         public void apply(final AbstractRegionBSPTree<P, N> inputTree1, final AbstractRegionBSPTree<P, N> inputTree2,
935                 final AbstractRegionBSPTree<P, N> outputTree) {
936 
937             this.performMerge(inputTree1, inputTree2, outputTree);
938 
939             outputTree.condense();
940         }
941     }
942 
943     /** Class for performing boolean union operations on region trees.
944      * @param <P> Point implementation type
945      * @param <N> BSP tree node implementation type
946      */
947     private static final class UnionOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
948         extends RegionMergeOperator<P, N> {
949 
950         /** {@inheritDoc} */
951         @Override
952         protected N mergeLeaf(final N node1, final N node2) {
953             if (node1.isLeaf()) {
954                 return node1.isInside() ? node1 : node2;
955             }
956 
957             // call again with flipped arguments
958             return mergeLeaf(node2, node1);
959         }
960     }
961 
962     /** Class for performing boolean intersection operations on region trees.
963      * @param <P> Point implementation type
964      * @param <N> BSP tree node implementation type
965      */
966     private static final class IntersectionOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
967         extends RegionMergeOperator<P, N> {
968 
969         /** {@inheritDoc} */
970         @Override
971         protected N mergeLeaf(final N node1, final N node2) {
972             if (node1.isLeaf()) {
973                 return node1.isInside() ? node2 : node1;
974             }
975 
976             // call again with flipped arguments
977             return mergeLeaf(node2, node1);
978         }
979     }
980 
981     /** Class for performing boolean difference operations on region trees.
982      * @param <P> Point implementation type
983      * @param <N> BSP tree node implementation type
984      */
985     private static final class DifferenceOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
986         extends RegionMergeOperator<P, N> {
987 
988         /** {@inheritDoc} */
989         @Override
990         protected N mergeLeaf(final N node1, final N node2) {
991             // a region is included if it belongs in tree1 and is not in tree2
992 
993             if (node1.isInside()) {
994                 // this region is inside of tree1, so only include subregions that are
995                 // not in tree2, ie include everything in node2's complement
996                 final N output = outputSubtree(node2);
997                 output.getTree().complementRecursive(output);
998 
999                 return output;
1000             } else if (node2.isInside()) {
1001                 // this region is inside of tree2 and so cannot be in the result region
1002                 final N output = outputNode();
1003                 output.setLocationValue(RegionLocation.OUTSIDE);
1004 
1005                 return output;
1006             }
1007 
1008             // this region is not in tree2, so we can include everything in tree1
1009             return node1;
1010         }
1011     }
1012 
1013     /** Class for performing boolean symmetric difference (xor) operations on region trees.
1014      * @param <P> Point implementation type
1015      * @param <N> BSP tree node implementation type
1016      */
1017     private static final class XorOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
1018         extends RegionMergeOperator<P, N> {
1019 
1020         /** {@inheritDoc} */
1021         @Override
1022         protected N mergeLeaf(final N node1, final N node2) {
1023             // a region is included if it belongs in tree1 and is not in tree2 OR
1024             // it belongs in tree2 and is not in tree1
1025 
1026             if (node1.isLeaf()) {
1027                 if (node1.isInside()) {
1028                     // this region is inside node1, so only include subregions that are
1029                     // not in node2, ie include everything in node2's complement
1030                     final N output = outputSubtree(node2);
1031                     output.getTree().complementRecursive(output);
1032 
1033                     return output;
1034                 } else {
1035                     // this region is not in node1, so only include subregions that
1036                     // in node2
1037                     return node2;
1038                 }
1039             }
1040 
1041             // the operation is symmetric, so perform the same operation but with the
1042             // nodes flipped
1043             return mergeLeaf(node2, node1);
1044         }
1045     }
1046 
1047     /** Internal class used to perform tree condense operations.
1048      * @param <P> Point implementation type
1049      * @param <N> BSP tree node implementation type
1050      */
1051     private static final class Condenser<P extends Point<P>, N extends AbstractRegionNode<P, N>> {
1052         /** Flag set to true if the tree was modified during the operation. */
1053         private boolean modifiedTree;
1054 
1055         /** Condense the nodes in the subtree rooted at the given node. Redundant child nodes are
1056          * removed. The tree is invalidated if the tree structure was modified.
1057          * @param node the root node of the subtree to condense
1058          * @return true if the tree was modified.
1059          */
1060         boolean condense(final N node) {
1061             modifiedTree = false;
1062 
1063             condenseRecursive(node);
1064 
1065             return modifiedTree;
1066         }
1067 
1068         /** Recursively condense nodes that have children with homogenous location attributes
1069          * (eg, both inside, both outside) into single nodes.
1070          * @param node the root of the subtree to condense
1071          * @return the location of the successfully condensed subtree or null if no condensing was
1072          *      able to be performed
1073          */
1074         private RegionLocation condenseRecursive(final N node) {
1075             if (node.isLeaf()) {
1076                 return node.getLocation();
1077             }
1078 
1079             final RegionLocation minusLocation = condenseRecursive(node.getMinus());
1080             final RegionLocation plusLocation = condenseRecursive(node.getPlus());
1081 
1082             if (minusLocation == plusLocation && minusLocation != null) {
1083                 node.setLocationValue(minusLocation);
1084                 node.clearCut();
1085 
1086                 modifiedTree = true;
1087 
1088                 return minusLocation;
1089             }
1090 
1091             return null;
1092         }
1093     }
1094 
1095     /** Class that iterates over the boundary hyperplane convex subsets from a set of region nodes.
1096      * @param <P> Point implementation type
1097      * @param <C> Boundary hyperplane convex subset implementation type
1098      * @param <N> BSP tree node implementation type
1099      */
1100     private static final class RegionBoundaryIterator<
1101             P extends Point<P>,
1102             C extends HyperplaneConvexSubset<P>,
1103             N extends AbstractRegionNode<P, N>>
1104         extends IteratorTransform<N, C> {
1105 
1106         /** Function that converts from the convex subset type to the output type. */
1107         private final Function<? super HyperplaneConvexSubset<P>, C> typeConverter;
1108 
1109         /** Simple constructor.
1110          * @param inputIterator iterator that will provide all nodes in the tree
1111          * @param typeConverter function that converts from the convex subset type to the output type
1112          */
1113         RegionBoundaryIterator(final Iterator<N> inputIterator,
1114                 final Function<? super HyperplaneConvexSubset<P>, C> typeConverter) {
1115             super(inputIterator);
1116 
1117             this.typeConverter = typeConverter;
1118         }
1119 
1120         /** {@inheritDoc} */
1121         @Override
1122         protected void acceptInput(final N input) {
1123             if (input.isInternal()) {
1124                 final RegionCutBoundary<P> cutBoundary = input.getCutBoundary();
1125 
1126                 for (final HyperplaneConvexSubset<P> boundary : cutBoundary.getOutsideFacing()) {
1127                     addOutput(typeConverter.apply(boundary));
1128                 }
1129 
1130                 for (final HyperplaneConvexSubset<P> boundary : cutBoundary.getInsideFacing()) {
1131                     final HyperplaneConvexSubset<P> reversed = boundary.reverse();
1132 
1133                     addOutput(typeConverter.apply(reversed));
1134                 }
1135             }
1136         }
1137     }
1138 }