1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.geometry.euclidean.oned;
18
19 import java.util.ArrayList;
20 import java.util.Comparator;
21 import java.util.List;
22 import java.util.Objects;
23 import java.util.function.BiConsumer;
24
25 import org.apache.commons.geometry.core.RegionLocation;
26 import org.apache.commons.geometry.core.Transform;
27 import org.apache.commons.geometry.core.partitioning.Hyperplane;
28 import org.apache.commons.geometry.core.partitioning.Split;
29 import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
30 import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
31 import org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule;
32
33 /** Binary space partitioning (BSP) tree representing a region in one dimensional
34 * Euclidean space.
35 */
36 public final class RegionBSPTree1D extends AbstractRegionBSPTree<Vector1D, RegionBSPTree1D.RegionNode1D> {
37 /** Comparator used to sort BoundaryPairs by ascending location. */
38 private static final Comparator<BoundaryPair> BOUNDARY_PAIR_COMPARATOR =
39 Comparator.comparingDouble(BoundaryPair::getMinValue);
40
41 /** Create a new, empty region.
42 */
43 public RegionBSPTree1D() {
44 this(false);
45 }
46
47 /** Create a new region. If {@code full} is true, then the region will
48 * represent the entire number line. Otherwise, it will be empty.
49 * @param full whether or not the region should contain the entire
50 * number line or be empty
51 */
52 public RegionBSPTree1D(final boolean full) {
53 super(full);
54 }
55
56 /** Return a deep copy of this instance.
57 * @return a deep copy of this instance.
58 * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
59 */
60 public RegionBSPTree1D copy() {
61 final RegionBSPTree1D result = RegionBSPTree1D.empty();
62 result.copy(this);
63
64 return result;
65 }
66
67 /** Add an interval to this region. The resulting region will be the
68 * union of the interval and the region represented by this instance.
69 * @param interval the interval to add
70 */
71 public void add(final Interval interval) {
72 union(intervalToTree(interval));
73 }
74
75 /** Classify a point location with respect to the region.
76 * @param x the point to classify
77 * @return the location of the point with respect to the region
78 * @see #classify(org.apache.commons.geometry.core.Point)
79 */
80 public RegionLocation classify(final double x) {
81 return classify(Vector1D.of(x));
82 }
83
84 /** Return true if the given point location is on the inside or boundary
85 * of the region.
86 * @param x the location to test
87 * @return true if the location is on the inside or boundary of the region
88 * @see #contains(org.apache.commons.geometry.core.Point)
89 */
90 public boolean contains(final double x) {
91 return contains(Vector1D.of(x));
92 }
93
94 /** {@inheritDoc}
95 *
96 * <p>This method simply returns 0 because boundaries in one dimension do not
97 * have any size.</p>
98 */
99 @Override
100 public double getBoundarySize() {
101 return 0;
102 }
103
104 /** {@inheritDoc} */
105 @Override
106 public Vector1D project(final Vector1D pt) {
107 // use our custom projector so that we can disambiguate points that are
108 // actually equidistant from the target point
109 final BoundaryProjector1D projector = new BoundaryProjector1D(pt);
110 accept(projector);
111
112 return projector.getProjected();
113 }
114
115 /** {@inheritDoc}
116 *
117 * <p>When splitting trees representing single points with a splitter lying directly
118 * on the point, the result point is placed on one side of the splitter based on its
119 * orientation: if the splitter is positive-facing, the point is placed on the plus
120 * side of the split; if the splitter is negative-facing, the point is placed on the
121 * minus side of the split.</p>
122 */
123 @Override
124 public Split<RegionBSPTree1D> split(final Hyperplane<Vector1D> splitter) {
125 return split(splitter, RegionBSPTree1D.empty(), RegionBSPTree1D.empty());
126 }
127
128 /** Get the minimum value on the inside of the region; returns {@link Double#NEGATIVE_INFINITY}
129 * if the region does not have a minimum value and {@link Double#POSITIVE_INFINITY} if
130 * the region is empty.
131 * @return the minimum value on the inside of the region
132 */
133 public double getMin() {
134 double min = Double.POSITIVE_INFINITY;
135
136 RegionNode1D node = getRoot();
137 OrientedPoint pt;
138
139 while (!node.isLeaf()) {
140 pt = (OrientedPoint) node.getCutHyperplane();
141
142 min = pt.getLocation();
143 node = pt.isPositiveFacing() ? node.getMinus() : node.getPlus();
144 }
145
146 return node.isInside() ? Double.NEGATIVE_INFINITY : min;
147 }
148
149 /** Get the maximum value on the inside of the region; returns {@link Double#POSITIVE_INFINITY}
150 * if the region does not have a maximum value and {@link Double#NEGATIVE_INFINITY} if
151 * the region is empty.
152 * @return the maximum value on the inside of the region
153 */
154 public double getMax() {
155 double max = Double.NEGATIVE_INFINITY;
156
157 RegionNode1D node = getRoot();
158 OrientedPoint pt;
159
160 while (!node.isLeaf()) {
161 pt = (OrientedPoint) node.getCutHyperplane();
162
163 max = pt.getLocation();
164 node = pt.isPositiveFacing() ? node.getPlus() : node.getMinus();
165 }
166
167 return node.isInside() ? Double.POSITIVE_INFINITY : max;
168 }
169
170 /** Convert the region represented by this tree into a list of separate
171 * {@link Interval}s, arranged in order of ascending min value.
172 * @return list of {@link Interval}s representing this region in order of
173 * ascending min value
174 */
175 public List<Interval> toIntervals() {
176
177 final List<BoundaryPair> boundaryPairs = new ArrayList<>();
178
179 visitInsideIntervals((min, max) -> boundaryPairs.add(new BoundaryPair(min, max)));
180 boundaryPairs.sort(BOUNDARY_PAIR_COMPARATOR);
181
182 final List<Interval> intervals = new ArrayList<>();
183
184 BoundaryPair start = null;
185 BoundaryPair end = null;
186
187 for (final BoundaryPair current : boundaryPairs) {
188 if (start == null) {
189 start = current;
190 end = current;
191 } else if (Objects.equals(end.getMax(), current.getMin())) {
192 // these intervals should be merged
193 end = current;
194 } else {
195 // these intervals should not be merged
196 intervals.add(createInterval(start, end));
197
198 // queue up the next pair
199 start = current;
200 end = current;
201 }
202 }
203
204 if (start != null) {
205 intervals.add(createInterval(start, end));
206 }
207
208 return intervals;
209 }
210
211 /** Create an interval instance from the min boundary from the start boundary pair and
212 * the max boundary from the end boundary pair. The hyperplane directions are adjusted
213 * as needed.
214 * @param start starting boundary pair
215 * @param end ending boundary pair
216 * @return an interval created from the min boundary of the given start pair and the
217 * max boundary from the given end pair
218 */
219 private Interval createInterval(final BoundaryPair start, final BoundaryPair end) {
220 OrientedPoint min = start.getMin();
221 OrientedPoint max = end.getMax();
222
223 // flip the hyperplanes if needed since there's no
224 // guarantee that the inside will be on the minus side
225 // of the hyperplane (for example, if the region is complemented)
226
227 if (min != null && min.isPositiveFacing()) {
228 min = min.reverse();
229 }
230 if (max != null && !max.isPositiveFacing()) {
231 max = max.reverse();
232 }
233
234 return Interval.of(min, max);
235 }
236
237 /** Compute the min/max intervals for all interior convex regions in the tree and
238 * pass the values to the given visitor function.
239 * @param visitor the object that will receive the calculated min and max boundary for each
240 * insides node's convex region
241 */
242 private void visitInsideIntervals(final BiConsumer<OrientedPoint, OrientedPoint> visitor) {
243 for (final RegionNode1D node : nodes()) {
244 if (node.isInside()) {
245 node.visitNodeInterval(visitor);
246 }
247 }
248 }
249
250 /** {@inheritDoc} */
251 @Override
252 protected RegionNode1D createNode() {
253 return new RegionNode1D(this);
254 }
255
256 /** {@inheritDoc} */
257 @Override
258 protected RegionSizeProperties<Vector1D> computeRegionSizeProperties() {
259 final RegionSizePropertiesVisitor visitor = new RegionSizePropertiesVisitor();
260
261 visitInsideIntervals(visitor);
262
263 return visitor.getRegionSizeProperties();
264 }
265
266 /** Returns true if the given transform would result in a swapping of the interior
267 * and exterior of the region if applied.
268 *
269 * <p>This method always returns false since no swapping of this kind occurs in
270 * 1D.</p>
271 */
272 @Override
273 protected boolean swapsInsideOutside(final Transform<Vector1D> transform) {
274 return false;
275 }
276
277 /** Return a new {@link RegionBSPTree1D} instance containing the entire space.
278 * @return a new {@link RegionBSPTree1D} instance containing the entire space
279 */
280 public static RegionBSPTree1D full() {
281 return new RegionBSPTree1D(true);
282 }
283
284 /** Return a new, empty {@link RegionBSPTree1D} instance.
285 * @return a new, empty {@link RegionBSPTree1D} instance
286 */
287 public static RegionBSPTree1D empty() {
288 return new RegionBSPTree1D(false);
289 }
290
291 /** Construct a new instance from one or more intervals. The returned tree
292 * represents the same region as the union of all of the input intervals.
293 * @param interval the input interval
294 * @param more additional intervals to add to the region
295 * @return a new instance representing the same region as the union
296 * of all of the given intervals
297 */
298 public static RegionBSPTree1D from(final Interval interval, final Interval... more) {
299 final RegionBSPTree1D tree = intervalToTree(interval);
300
301 for (final Interval additional : more) {
302 tree.add(additional);
303 }
304
305 return tree;
306 }
307
308 /** Construct a new instance from the given collection of intervals.
309 * @param intervals the intervals to populate the region with
310 * @return a new instance constructed from the given collection of intervals
311 */
312 public static RegionBSPTree1D from(final Iterable<Interval> intervals) {
313 final RegionBSPTree1D tree = new RegionBSPTree1D(false);
314
315 for (final Interval interval : intervals) {
316 tree.add(interval);
317 }
318
319 return tree;
320 }
321
322 /** Return a tree representing the same region as the given interval.
323 * @param interval interval to create a tree from
324 * @return a tree representing the same region as the given interval
325 */
326 private static RegionBSPTree1D intervalToTree(final Interval interval) {
327 final OrientedPoint minBoundary = interval.getMinBoundary();
328 final OrientedPoint maxBoundary = interval.getMaxBoundary();
329
330 final RegionBSPTree1D tree = full();
331
332 RegionNode1D node = tree.getRoot();
333
334 if (minBoundary != null) {
335 tree.setNodeCut(node, minBoundary.span(), tree.getSubtreeInitializer(RegionCutRule.MINUS_INSIDE));
336
337 node = node.getMinus();
338 }
339
340 if (maxBoundary != null) {
341 tree.setNodeCut(node, maxBoundary.span(), tree.getSubtreeInitializer(RegionCutRule.MINUS_INSIDE));
342 }
343
344 return tree;
345 }
346
347 /** BSP tree node for one dimensional Euclidean space.
348 */
349 public static final class RegionNode1D extends AbstractRegionBSPTree.AbstractRegionNode<Vector1D, RegionNode1D> {
350 /** Simple constructor.
351 * @param tree the owning tree instance
352 */
353 private RegionNode1D(final AbstractBSPTree<Vector1D, RegionNode1D> tree) {
354 super(tree);
355 }
356
357 /** Get the region represented by this node. The returned region contains
358 * the entire area contained in this node, regardless of the attributes of
359 * any child nodes.
360 * @return the region represented by this node
361 */
362 public Interval getNodeRegion() {
363 final NodeRegionVisitor visitor = new NodeRegionVisitor();
364 visitNodeInterval(visitor);
365
366 return visitor.getInterval();
367 }
368
369 /** Determine the min/max boundaries for the convex region represented by this node and pass
370 * the values to the visitor function.
371 * @param visitor the object that will receive the min and max boundaries for the node's
372 * convex region
373 */
374 private void visitNodeInterval(final BiConsumer<? super OrientedPoint, ? super OrientedPoint> visitor) {
375 OrientedPoint min = null;
376 OrientedPoint max = null;
377
378 OrientedPoint pt;
379 RegionNode1D child = this;
380 RegionNode1D parent;
381
382 while ((min == null || max == null) && (parent = child.getParent()) != null) {
383 pt = (OrientedPoint) parent.getCutHyperplane();
384
385 if ((pt.isPositiveFacing() && child.isMinus()) ||
386 (!pt.isPositiveFacing() && child.isPlus())) {
387
388 if (max == null) {
389 max = pt;
390 }
391 } else if (min == null) {
392 min = pt;
393 }
394
395 child = parent;
396 }
397
398 visitor.accept(min, max);
399 }
400
401 /** {@inheritDoc} */
402 @Override
403 protected RegionNode1D getSelf() {
404 return this;
405 }
406 }
407
408 /** Internal class containing pairs of interval boundaries.
409 */
410 private static final class BoundaryPair {
411
412 /** The min boundary. */
413 private final OrientedPoint min;
414
415 /** The max boundary. */
416 private final OrientedPoint max;
417
418 /** Simple constructor.
419 * @param min min boundary hyperplane
420 * @param max max boundary hyperplane
421 */
422 BoundaryPair(final OrientedPoint min, final OrientedPoint max) {
423 this.min = min;
424 this.max = max;
425 }
426
427 /** Get the minimum boundary hyperplane.
428 * @return the minimum boundary hyperplane.
429 */
430 public OrientedPoint getMin() {
431 return min;
432 }
433
434 /** Get the maximum boundary hyperplane.
435 * @return the maximum boundary hyperplane.
436 */
437 public OrientedPoint getMax() {
438 return max;
439 }
440
441 /** Get the minimum value of the interval or {@link Double#NEGATIVE_INFINITY}
442 * if no minimum value exists.
443 * @return the minimum value of the interval or {@link Double#NEGATIVE_INFINITY}
444 * if no minimum value exists.
445 */
446 public double getMinValue() {
447 return (min != null) ? min.getLocation() : Double.NEGATIVE_INFINITY;
448 }
449 }
450
451 /** Class used to project points onto the region boundary.
452 */
453 private static final class BoundaryProjector1D extends BoundaryProjector<Vector1D, RegionNode1D> {
454 /** Simple constructor.
455 * @param point the point to project onto the region's boundary
456 */
457 BoundaryProjector1D(final Vector1D point) {
458 super(point);
459 }
460
461 /** {@inheritDoc} */
462 @Override
463 protected Vector1D disambiguateClosestPoint(final Vector1D target, final Vector1D a, final Vector1D b) {
464 final int cmp = Vector1D.COORDINATE_ASCENDING_ORDER.compare(a, b);
465
466 if (target.isInfinite() && target.getX() > 0) {
467 // return the largest value (closest to +Infinity)
468 return cmp < 0 ? b : a;
469 }
470
471 // return the smallest value
472 return cmp < 0 ? a : b;
473 }
474 }
475
476 /** Internal class for calculating the region of a single tree node.
477 */
478 private static final class NodeRegionVisitor implements BiConsumer<OrientedPoint, OrientedPoint> {
479
480 /** The min boundary for the region. */
481 private OrientedPoint min;
482
483 /** The max boundary for the region. */
484 private OrientedPoint max;
485
486 /** {@inheritDoc} */
487 @Override
488 public void accept(final OrientedPoint minBoundary, final OrientedPoint maxBoundary) {
489 // reverse the oriented point directions if needed
490 this.min = (minBoundary != null && minBoundary.isPositiveFacing()) ? minBoundary.reverse() : minBoundary;
491 this.max = (maxBoundary != null && !maxBoundary.isPositiveFacing()) ? maxBoundary.reverse() : maxBoundary;
492 }
493
494 /** Return the computed interval.
495 * @return the computed interval.
496 */
497 public Interval getInterval() {
498 return Interval.of(min, max);
499 }
500 }
501
502 /** Internal class for calculating size-related properties for a {@link RegionBSPTree1D}.
503 */
504 private static final class RegionSizePropertiesVisitor implements BiConsumer<OrientedPoint, OrientedPoint> {
505 /** Number of inside intervals visited. */
506 private int count;
507
508 /** Total computed size of all inside regions. */
509 private double size;
510
511 /** Raw sum of the centroids of each inside interval. */
512 private double rawCentroidSum;
513
514 /** The sum of the centroids of each inside interval, scaled by the size of the interval. */
515 private double scaledCentroidSum;
516
517 /** {@inheritDoc} */
518 @Override
519 public void accept(final OrientedPoint min, final OrientedPoint max) {
520 ++count;
521
522 final double minLoc = (min != null) ? min.getLocation() : Double.NEGATIVE_INFINITY;
523 final double maxLoc = (max != null) ? max.getLocation() : Double.POSITIVE_INFINITY;
524
525 final double intervalSize = maxLoc - minLoc;
526 final double intervalCentroid = 0.5 * (maxLoc + minLoc);
527
528 size += intervalSize;
529 rawCentroidSum += intervalCentroid;
530 scaledCentroidSum += intervalSize * intervalCentroid;
531 }
532
533 /** Get the computed properties for the region. This must only be called after
534 * every inside interval has been visited.
535 * @return properties for the region
536 */
537 public RegionSizeProperties<Vector1D> getRegionSizeProperties() {
538 Vector1D centroid = null;
539
540 if (count > 0 && Double.isFinite(size)) {
541 if (size > 0) {
542 // use the scaled sum if we have a non-zero size
543 centroid = Vector1D.of(scaledCentroidSum / size);
544 } else {
545 // use the raw sum if we don't have a size; this will be
546 // the case if the region only contains points with zero size
547 centroid = Vector1D.of(rawCentroidSum / count);
548 }
549 }
550
551 return new RegionSizeProperties<>(size, centroid);
552 }
553 }
554 }