001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.geometry.euclidean.oned; 018 019import java.util.ArrayList; 020import java.util.Comparator; 021import java.util.List; 022import java.util.Objects; 023import java.util.function.BiConsumer; 024 025import org.apache.commons.geometry.core.RegionLocation; 026import org.apache.commons.geometry.core.Transform; 027import org.apache.commons.geometry.core.partitioning.Hyperplane; 028import org.apache.commons.geometry.core.partitioning.Split; 029import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree; 030import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree; 031import org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule; 032 033/** Binary space partitioning (BSP) tree representing a region in one dimensional 034 * Euclidean space. 035 */ 036public final class RegionBSPTree1D extends AbstractRegionBSPTree<Vector1D, RegionBSPTree1D.RegionNode1D> { 037 /** Comparator used to sort BoundaryPairs by ascending location. */ 038 private static final Comparator<BoundaryPair> BOUNDARY_PAIR_COMPARATOR = 039 Comparator.comparingDouble(BoundaryPair::getMinValue); 040 041 /** Create a new, empty region. 042 */ 043 public RegionBSPTree1D() { 044 this(false); 045 } 046 047 /** Create a new region. If {@code full} is true, then the region will 048 * represent the entire number line. Otherwise, it will be empty. 049 * @param full whether or not the region should contain the entire 050 * number line or be empty 051 */ 052 public RegionBSPTree1D(final boolean full) { 053 super(full); 054 } 055 056 /** Return a deep copy of this instance. 057 * @return a deep copy of this instance. 058 * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree) 059 */ 060 public RegionBSPTree1D copy() { 061 final RegionBSPTree1D result = RegionBSPTree1D.empty(); 062 result.copy(this); 063 064 return result; 065 } 066 067 /** Add an interval to this region. The resulting region will be the 068 * union of the interval and the region represented by this instance. 069 * @param interval the interval to add 070 */ 071 public void add(final Interval interval) { 072 union(intervalToTree(interval)); 073 } 074 075 /** Classify a point location with respect to the region. 076 * @param x the point to classify 077 * @return the location of the point with respect to the region 078 * @see #classify(org.apache.commons.geometry.core.Point) 079 */ 080 public RegionLocation classify(final double x) { 081 return classify(Vector1D.of(x)); 082 } 083 084 /** Return true if the given point location is on the inside or boundary 085 * of the region. 086 * @param x the location to test 087 * @return true if the location is on the inside or boundary of the region 088 * @see #contains(org.apache.commons.geometry.core.Point) 089 */ 090 public boolean contains(final double x) { 091 return contains(Vector1D.of(x)); 092 } 093 094 /** {@inheritDoc} 095 * 096 * <p>This method simply returns 0 because boundaries in one dimension do not 097 * have any size.</p> 098 */ 099 @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}