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.spherical.oned;
18
19 import java.util.ArrayList;
20 import java.util.Collections;
21 import java.util.Comparator;
22 import java.util.List;
23 import java.util.Objects;
24
25 import org.apache.commons.geometry.core.Transform;
26 import org.apache.commons.geometry.core.partitioning.Hyperplane;
27 import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
28 import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
29 import org.apache.commons.geometry.core.partitioning.Split;
30 import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
31 import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
32 import org.apache.commons.geometry.euclidean.twod.Vector2D;
33 import org.apache.commons.numbers.angle.Angle;
34 import org.apache.commons.numbers.core.Precision;
35
36 /** BSP tree representing regions in 1D spherical space.
37 */
38 public class RegionBSPTree1S extends AbstractRegionBSPTree<Point1S, RegionBSPTree1S.RegionNode1S> {
39 /** Comparator used to sort BoundaryPairs by ascending azimuth. */
40 private static final Comparator<BoundaryPair> BOUNDARY_PAIR_COMPARATOR =
41 Comparator.comparingDouble(BoundaryPair::getMinValue);
42
43 /** Create a new, empty instance.
44 */
45 public RegionBSPTree1S() {
46 this(false);
47 }
48
49 /** Create a new region. If {@code full} is true, then the region will
50 * represent the entire circle. Otherwise, it will be empty.
51 * @param full whether or not the region should contain the entire
52 * circle or be empty
53 */
54 public RegionBSPTree1S(final boolean full) {
55 super(full);
56 }
57
58 /** Return a deep copy of this instance.
59 * @return a deep copy of this instance.
60 * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
61 */
62 public RegionBSPTree1S copy() {
63 final RegionBSPTree1S result = RegionBSPTree1S.empty();
64 result.copy(this);
65
66 return result;
67 }
68
69 /** Add an interval to this region. The resulting region will be the
70 * union of the interval and the region represented by this instance.
71 * @param interval the interval to add
72 */
73 public void add(final AngularInterval interval) {
74 union(fromInterval(interval));
75 }
76
77 /** {@inheritDoc} */
78 @Override
79 public Point1S project(final Point1S pt) {
80 final BoundaryProjector1S projector = new BoundaryProjector1S(pt);
81 accept(projector);
82
83 return projector.getProjected();
84 }
85
86 /** {@inheritDoc}
87 *
88 * <p>Each interval of the region is transformed individually and the
89 * results are unioned. If the size of any transformed interval is greater
90 * than or equal to 2pi, then the region is set to the full space.</p>
91 */
92 @Override
93 public void transform(final Transform<Point1S> transform) {
94 if (!isFull() && !isEmpty()) {
95 // transform each interval individually to handle wrap-around
96 final List<AngularInterval> intervals = toIntervals();
97
98 setEmpty();
99
100 for (final AngularInterval interval : intervals) {
101 union(interval.transform(transform).toTree());
102 }
103 }
104 }
105
106 /** {@inheritDoc}
107 *
108 * <p>It is important to note that split operations occur according to the rules of the
109 * {@link CutAngle} hyperplane class. In this class, the continuous circle is viewed
110 * as a non-circular segment of the number line in the range {@code [0, 2pi)}. Hyperplanes
111 * are placed along this line and partition it into the segments {@code [0, x]}
112 * and {@code [x, 2pi)}, where {@code x} is the location of the hyperplane. For example,
113 * a positive-facing {@link CutAngle} instance with an azimuth of {@code 0.5pi} has
114 * a minus side consisting of the angles {@code [0, 0.5pi]} and a plus side consisting of
115 * the angles {@code [0.5pi, 2pi)}. Similarly, a positive-facing {@link CutAngle} with
116 * an azimuth of {@code 0pi} has a plus side of {@code [0, 2pi)} (the full space) and
117 * a minus side that is completely empty (since no points exist in our domain that are
118 * less than zero). These rules can result in somewhat non-intuitive behavior at times.
119 * For example, splitting a non-empty region with a hyperplane at {@code 0pi} is
120 * essentially a no-op, since the region will either lie entirely on the plus or minus
121 * side of the hyperplane (depending on the hyperplane's orientation) regardless of the actual
122 * content of the region. In these situations, a copy of the tree is returned on the
123 * appropriate side of the split.</p>
124 *
125 * @see CutAngle
126 * @see #splitDiameter(CutAngle)
127 */
128 @Override
129 public Split<RegionBSPTree1S> split(final Hyperplane<Point1S> splitter) {
130 // Handle the special case where the cut is on the azimuth equivalent to zero.
131 // In this case, it is not possible for any points to lie between it and zero.
132 if (!isEmpty() && splitter.classify(Point1S.ZERO) == HyperplaneLocation.ON) {
133 final CutAngle cut = (CutAngle) splitter;
134 if (cut.isPositiveFacing()) {
135 return new Split<>(null, copy());
136 } else {
137 return new Split<>(copy(), null);
138 }
139 }
140
141 return split(splitter, RegionBSPTree1S.empty(), RegionBSPTree1S.empty());
142 }
143
144 /** Split the instance along a circle diameter.The diameter is defined by the given
145 * split point and its reversed antipodal point.
146 * @param splitter split point defining one side of the split diameter
147 * @return result of the split operation
148 */
149 public Split<RegionBSPTree1S> splitDiameter(final CutAngle splitter) {
150
151 final CutAngle opposite = CutAngles.fromPointAndDirection(
152 splitter.getPoint().antipodal(),
153 !splitter.isPositiveFacing(),
154 splitter.getPrecision());
155
156 final double plusPoleOffset = splitter.isPositiveFacing() ?
157 +Angle.PI_OVER_TWO :
158 -Angle.PI_OVER_TWO;
159 final Point1S plusPole = Point1S.of(splitter.getAzimuth() + plusPoleOffset);
160
161 final boolean zeroOnPlusSide = splitter.getPrecision()
162 .lte(plusPole.distance(Point1S.ZERO), Angle.PI_OVER_TWO);
163
164 final Split<RegionBSPTree1S> firstSplit = split(splitter);
165 final Split<RegionBSPTree1S> secondSplit = split(opposite);
166
167 RegionBSPTree1S minus = RegionBSPTree1S.empty();
168 RegionBSPTree1S plus = RegionBSPTree1S.empty();
169
170 if (zeroOnPlusSide) {
171 // zero wrap-around needs to be handled on the plus side of the split
172 safeUnion(plus, firstSplit.getPlus());
173 safeUnion(plus, secondSplit.getPlus());
174
175 minus = firstSplit.getMinus();
176 if (minus != null) {
177 minus = minus.split(opposite).getMinus();
178 }
179 } else {
180 // zero wrap-around needs to be handled on the minus side of the split
181 safeUnion(minus, firstSplit.getMinus());
182 safeUnion(minus, secondSplit.getMinus());
183
184 plus = firstSplit.getPlus();
185 if (plus != null) {
186 plus = plus.split(opposite).getPlus();
187 }
188 }
189
190 return new Split<>(
191 (minus != null && !minus.isEmpty()) ? minus : null,
192 (plus != null && !plus.isEmpty()) ? plus : null);
193 }
194
195
196 /** Convert the region represented by this tree into a list of separate
197 * {@link AngularInterval}s, arranged in order of ascending min value.
198 * @return list of {@link AngularInterval}s representing this region in order of
199 * ascending min value
200 */
201 public List<AngularInterval> toIntervals() {
202 if (isFull()) {
203 return Collections.singletonList(AngularInterval.full());
204 }
205
206 final List<BoundaryPair> insideBoundaryPairs = new ArrayList<>();
207 for (final RegionNode1S node : nodes()) {
208 if (node.isInside()) {
209 insideBoundaryPairs.add(getNodeBoundaryPair(node));
210 }
211 }
212
213 insideBoundaryPairs.sort(BOUNDARY_PAIR_COMPARATOR);
214
215 final int boundaryPairCount = insideBoundaryPairs.size();
216
217 // Get the start point for merging intervals together.
218 final int startOffset = getIntervalStartIndex(insideBoundaryPairs);
219
220 // Go through the pairs starting at the start offset and create intervals
221 // for each set of adjacent pairs.
222 final List<AngularInterval> intervals = new ArrayList<>();
223
224 BoundaryPair start = null;
225 BoundaryPair end = null;
226 BoundaryPair current;
227
228 for (int i = 0; i < boundaryPairCount; ++i) {
229 current = insideBoundaryPairs.get((i + startOffset) % boundaryPairCount);
230
231 if (start == null) {
232 start = current;
233 end = current;
234 } else if (Objects.equals(end.getMax(), current.getMin())) {
235 // these intervals should be merged
236 end = current;
237 } else {
238 // these intervals should be separate
239 intervals.add(createInterval(start, end));
240
241 // queue up the next pair
242 start = current;
243 end = current;
244 }
245 }
246
247 if (start != null && end != null) {
248 intervals.add(createInterval(start, end));
249 }
250
251 return intervals;
252 }
253
254 /** Get the index that should be used as the starting point for combining adjacent boundary pairs
255 * into contiguous intervals. This is computed as the first boundary pair found that is not connected
256 * to the pair before it, or {@code 0} if no such entry exists.
257 * @param boundaryPairs list of boundary pairs to search; must be ordered by increasing azimuth
258 * @return the index to use as a starting place for combining adjacent boundary pairs
259 * into contiguous intervals
260 */
261 private int getIntervalStartIndex(final List<BoundaryPair> boundaryPairs) {
262 final int size = boundaryPairs.size();
263
264 if (size > 0) {
265 BoundaryPair current;
266 BoundaryPair previous = boundaryPairs.get(size - 1);
267
268 for (int i = 0; i < size; ++i, previous = current) {
269 current = boundaryPairs.get(i);
270
271 if (!Objects.equals(current.getMin(), previous.getMax())) {
272 return i;
273 }
274 }
275 }
276
277 return 0;
278 }
279
280 /** Create an interval instance from the min boundary from the start boundary pair and
281 * the max boundary from the end boundary pair. The hyperplane directions are adjusted
282 * as needed.
283 * @param start starting boundary pair
284 * @param end ending boundary pair
285 * @return an interval created from the min boundary of the given start pair and the
286 * max boundary from the given end pair
287 */
288 private AngularInterval createInterval(final BoundaryPair start, final BoundaryPair end) {
289 CutAngle min = start.getMin();
290 CutAngle max = end.getMax();
291
292 final Precision.DoubleEquivalence precision = (min != null) ? min.getPrecision() : max.getPrecision();
293
294 // flip the hyperplanes if needed since there's no
295 // guarantee that the inside will be on the minus side
296 // of the hyperplane (for example, if the region is complemented)
297
298 if (min != null) {
299 if (min.isPositiveFacing()) {
300 min = min.reverse();
301 }
302 } else {
303 min = CutAngles.createNegativeFacing(0.0, precision);
304 }
305
306 if (max != null) {
307 if (!max.isPositiveFacing()) {
308 max = max.reverse();
309 }
310 } else {
311 max = CutAngles.createPositiveFacing(Angle.TWO_PI, precision);
312 }
313
314 return AngularInterval.of(min, max);
315 }
316
317 /** Return the min/max boundary pair for the convex region represented by the given node.
318 * @param node the node to compute the interval for
319 * @return the min/max boundary pair for the convex region represented by the given node
320 */
321 private BoundaryPair getNodeBoundaryPair(final RegionNode1S node) {
322 CutAngle min = null;
323 CutAngle max = null;
324
325 CutAngle pt;
326 RegionNode1S child = node;
327 RegionNode1S parent;
328
329 while ((min == null || max == null) && (parent = child.getParent()) != null) {
330 pt = (CutAngle) parent.getCutHyperplane();
331
332 if ((pt.isPositiveFacing() && child.isMinus()) ||
333 (!pt.isPositiveFacing() && child.isPlus())) {
334
335 if (max == null) {
336 max = pt;
337 }
338 } else if (min == null) {
339 min = pt;
340 }
341
342 child = parent;
343 }
344
345 return new BoundaryPair(min, max);
346 }
347
348 /** {@inheritDoc} */
349 @Override
350 protected RegionSizeProperties<Point1S> computeRegionSizeProperties() {
351 if (isFull()) {
352 return new RegionSizeProperties<>(Angle.TWO_PI, null);
353 } else if (isEmpty()) {
354 return new RegionSizeProperties<>(0, null);
355 }
356
357 double size = 0;
358 Vector2D scaledCentroidSum = Vector2D.ZERO;
359
360 double intervalSize;
361
362 for (final AngularInterval interval : toIntervals()) {
363 intervalSize = interval.getSize();
364
365 size += intervalSize;
366 scaledCentroidSum = scaledCentroidSum.add(interval.getCentroid().getVector().withNorm(intervalSize));
367 }
368
369 final Precision.DoubleEquivalence precision = ((CutAngle) getRoot().getCutHyperplane()).getPrecision();
370
371 final Point1S centroid = scaledCentroidSum.eq(Vector2D.ZERO, precision) ?
372 null :
373 Point1S.from(scaledCentroidSum);
374
375 return new RegionSizeProperties<>(size, centroid);
376 }
377
378 /** {@inheritDoc} */
379 @Override
380 protected RegionNode1S createNode() {
381 return new RegionNode1S(this);
382 }
383
384 /** Return a new, empty BSP tree.
385 * @return a new, empty BSP tree.
386 */
387 public static RegionBSPTree1S empty() {
388 return new RegionBSPTree1S(false);
389 }
390
391 /** Return a new, full BSP tree. The returned tree represents the
392 * full space.
393 * @return a new, full BSP tree.
394 */
395 public static RegionBSPTree1S full() {
396 return new RegionBSPTree1S(true);
397 }
398
399 /** Return a new BSP tree representing the same region as the given angular interval.
400 * @param interval the input interval
401 * @return a new BSP tree representing the same region as the given angular interval
402 */
403 public static RegionBSPTree1S fromInterval(final AngularInterval interval) {
404 final CutAngle minBoundary = interval.getMinBoundary();
405 final CutAngle maxBoundary = interval.getMaxBoundary();
406
407 final RegionBSPTree1S tree = full();
408
409 if (minBoundary != null) {
410 tree.insert(minBoundary.span());
411 }
412
413 if (maxBoundary != null) {
414 tree.insert(maxBoundary.span());
415 }
416
417 return tree;
418 }
419
420 /** Perform a union operation with {@code target} and {@code input}, storing the result
421 * in {@code target}; does nothing if {@code input} is null.
422 * @param target target tree
423 * @param input input tree
424 */
425 private static void safeUnion(final RegionBSPTree1S target, final RegionBSPTree1S input) {
426 if (input != null) {
427 target.union(input);
428 }
429 }
430
431 /** BSP tree node for one dimensional spherical space.
432 */
433 public static final class RegionNode1S extends AbstractRegionBSPTree.AbstractRegionNode<Point1S, RegionNode1S> {
434 /** Simple constructor.
435 * @param tree the owning tree instance
436 */
437 private RegionNode1S(final AbstractBSPTree<Point1S, RegionNode1S> tree) {
438 super(tree);
439 }
440
441 /** {@inheritDoc} */
442 @Override
443 protected RegionNode1S getSelf() {
444 return this;
445 }
446 }
447
448 /** Internal class containing pairs of interval boundaries.
449 */
450 private static final class BoundaryPair {
451
452 /** The min boundary. */
453 private final CutAngle min;
454
455 /** The max boundary. */
456 private final CutAngle max;
457
458 /** Simple constructor.
459 * @param min min boundary hyperplane
460 * @param max max boundary hyperplane
461 */
462 BoundaryPair(final CutAngle min, final CutAngle max) {
463 this.min = min;
464 this.max = max;
465 }
466
467 /** Get the minimum boundary hyperplane.
468 * @return the minimum boundary hyperplane.
469 */
470 public CutAngle getMin() {
471 return min;
472 }
473
474 /** Get the maximum boundary hyperplane.
475 * @return the maximum boundary hyperplane.
476 */
477 public CutAngle getMax() {
478 return max;
479 }
480
481 /** Get the minimum value of the interval or zero if no minimum value exists.
482 * @return the minimum value of the interval or zero
483 * if no minimum value exists.
484 */
485 public double getMinValue() {
486 return (min != null) ? min.getNormalizedAzimuth() : 0;
487 }
488 }
489
490 /** Class used to project points onto the region boundary.
491 */
492 private static final class BoundaryProjector1S extends BoundaryProjector<Point1S, RegionNode1S> {
493 /** Simple constructor.
494 * @param point the point to project onto the region's boundary
495 */
496 BoundaryProjector1S(final Point1S point) {
497 super(point);
498 }
499
500 /** {@inheritDoc} */
501 @Override
502 protected boolean isPossibleClosestCut(final HyperplaneSubset<Point1S> cut, final Point1S target,
503 final double minDist) {
504 // since the space wraps around, consider any cut as possibly being the closest
505 return true;
506 }
507
508 /** {@inheritDoc} */
509 @Override
510 protected Point1S disambiguateClosestPoint(final Point1S target, final Point1S a, final Point1S b) {
511 // prefer the point with the smaller normalize azimuth value
512 return a.getNormalizedAzimuth() < b.getNormalizedAzimuth() ? a : b;
513 }
514 }
515 }