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 }