1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.geometry.spherical.twod;
18
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.Collections;
22 import java.util.List;
23 import java.util.stream.Collectors;
24 import java.util.stream.Stream;
25
26 import org.apache.commons.geometry.core.GeometryTestUtils;
27 import org.apache.commons.geometry.core.RegionLocation;
28 import org.apache.commons.geometry.core.partitioning.Split;
29 import org.apache.commons.geometry.core.partitioning.SplitLocation;
30 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
31 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
32 import org.apache.commons.geometry.euclidean.threed.Vector3D;
33 import org.apache.commons.geometry.spherical.SphericalTestUtils;
34 import org.apache.commons.geometry.spherical.oned.Point1S;
35 import org.apache.commons.geometry.spherical.twod.RegionBSPTree2S.RegionNode2S;
36 import org.apache.commons.numbers.angle.PlaneAngleRadians;
37 import org.junit.Assert;
38 import org.junit.Test;
39
40 public class RegionBSPTree2STest {
41
42 private static final double TEST_EPS = 1e-10;
43
44
45
46 private static final double CENTROID_EPS = 1e-5;
47
48 private static final DoublePrecisionContext TEST_PRECISION =
49 new EpsilonDoublePrecisionContext(TEST_EPS);
50
51 private static final GreatCircle EQUATOR = GreatCircles.fromPoleAndU(
52 Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_X, TEST_PRECISION);
53
54 private static final GreatCircle X_MERIDIAN = GreatCircles.fromPoleAndU(
55 Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_X, TEST_PRECISION);
56
57 private static final GreatCircle Y_MERIDIAN = GreatCircles.fromPoleAndU(
58 Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
59
60 @Test
61 public void testCtor_booleanArg_true() {
62
63 final RegionBSPTree2S tree = new RegionBSPTree2S(true);
64
65
66 Assert.assertTrue(tree.isFull());
67 Assert.assertFalse(tree.isEmpty());
68 Assert.assertEquals(1, tree.count());
69 }
70
71 @Test
72 public void testCtor_booleanArg_false() {
73
74 final RegionBSPTree2S tree = new RegionBSPTree2S(false);
75
76
77 Assert.assertFalse(tree.isFull());
78 Assert.assertTrue(tree.isEmpty());
79 Assert.assertEquals(1, tree.count());
80 }
81
82 @Test
83 public void testCtor_default() {
84
85 final RegionBSPTree2S tree = new RegionBSPTree2S();
86
87
88 Assert.assertFalse(tree.isFull());
89 Assert.assertTrue(tree.isEmpty());
90 Assert.assertEquals(1, tree.count());
91 }
92
93 @Test
94 public void testFull_factoryMethod() {
95
96 final RegionBSPTree2S tree = RegionBSPTree2S.full();
97
98
99 Assert.assertTrue(tree.isFull());
100 Assert.assertFalse(tree.isEmpty());
101 Assert.assertEquals(1, tree.count());
102 }
103
104 @Test
105 public void testEmpty_factoryMethod() {
106
107 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
108
109
110 Assert.assertFalse(tree.isFull());
111 Assert.assertTrue(tree.isEmpty());
112 Assert.assertEquals(1, tree.count());
113 }
114
115 @Test
116 public void testFrom_boundaries_noBoundaries() {
117
118 Assert.assertTrue(RegionBSPTree2S.from(Collections.emptyList()).isEmpty());
119 Assert.assertTrue(RegionBSPTree2S.from(Collections.emptyList(), true).isFull());
120 Assert.assertTrue(RegionBSPTree2S.from(Collections.emptyList(), false).isEmpty());
121 }
122
123 @Test
124 public void testFrom_boundaries() {
125
126 final RegionBSPTree2S tree = RegionBSPTree2S.from(Arrays.asList(
127 EQUATOR.arc(Point2S.PLUS_I, Point2S.PLUS_J),
128 X_MERIDIAN.arc(Point2S.PLUS_K, Point2S.PLUS_I),
129 Y_MERIDIAN.arc(Point2S.PLUS_J, Point2S.PLUS_K)
130 ));
131
132
133 Assert.assertFalse(tree.isFull());
134 Assert.assertFalse(tree.isEmpty());
135
136 Assert.assertEquals(RegionLocation.OUTSIDE, tree.getRoot().getLocation());
137
138 SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE, Point2S.of(1, 0.5));
139 SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE,
140 Point2S.of(-1, 0.5), Point2S.of(Math.PI, 0.5 * Math.PI));
141 }
142
143 @Test
144 public void testFrom_boundaries_fullIsTrue() {
145
146 final RegionBSPTree2S tree = RegionBSPTree2S.from(Arrays.asList(
147 EQUATOR.arc(Point2S.PLUS_I, Point2S.PLUS_J),
148 X_MERIDIAN.arc(Point2S.PLUS_K, Point2S.PLUS_I),
149 Y_MERIDIAN.arc(Point2S.PLUS_J, Point2S.PLUS_K)
150 ), true);
151
152
153 Assert.assertFalse(tree.isFull());
154 Assert.assertFalse(tree.isEmpty());
155
156 Assert.assertEquals(RegionLocation.INSIDE, tree.getRoot().getLocation());
157
158 SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE, Point2S.of(1, 0.5));
159 SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE,
160 Point2S.of(-1, 0.5), Point2S.of(Math.PI, 0.5 * Math.PI));
161 }
162
163 @Test
164 public void testCopy() {
165
166 final RegionBSPTree2S tree = new RegionBSPTree2S(true);
167 tree.getRoot().cut(EQUATOR);
168
169
170 final RegionBSPTree2S copy = tree.copy();
171
172
173 Assert.assertNotSame(tree, copy);
174 Assert.assertEquals(3, copy.count());
175 }
176
177 @Test
178 public void testBoundaries() {
179
180 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
181 insertPositiveQuadrant(tree);
182
183
184 final List<GreatArc> arcs = new ArrayList<>();
185 tree.boundaries().forEach(arcs::add);
186
187
188 Assert.assertEquals(3, arcs.size());
189 }
190
191 @Test
192 public void testGetBoundaries() {
193
194 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
195 insertPositiveQuadrant(tree);
196
197
198 final List<GreatArc> arcs = tree.getBoundaries();
199
200
201 Assert.assertEquals(3, arcs.size());
202 }
203
204 @Test
205 public void testBoundaryStream() {
206
207 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
208 insertPositiveQuadrant(tree);
209
210
211 final List<GreatArc> arcs = tree.boundaryStream().collect(Collectors.toList());
212
213
214 Assert.assertEquals(3, arcs.size());
215 }
216
217 @Test
218 public void testBoundaryStream_noBoundaries() {
219
220 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
221
222
223 final List<GreatArc> arcs = tree.boundaryStream().collect(Collectors.toList());
224
225
226 Assert.assertEquals(0, arcs.size());
227 }
228
229 @Test
230 public void testToTree_returnsSameInstance() {
231
232 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
233 insertPositiveQuadrant(tree);
234
235
236 Assert.assertSame(tree, tree.toTree());
237 }
238
239 @Test
240 public void testGetBoundaryPaths_cachesResult() {
241
242 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
243 insertPositiveQuadrant(tree);
244
245
246 final List<GreatArcPath> a = tree.getBoundaryPaths();
247 final List<GreatArcPath> b = tree.getBoundaryPaths();
248
249
250 Assert.assertSame(a, b);
251 }
252
253 @Test
254 public void testGetBoundaryPaths_recomputesResultOnChange() {
255
256 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
257 tree.insert(EQUATOR.span());
258
259
260 final List<GreatArcPath> a = tree.getBoundaryPaths();
261 tree.insert(X_MERIDIAN.span());
262 final List<GreatArcPath> b = tree.getBoundaryPaths();
263
264
265 Assert.assertNotSame(a, b);
266 }
267
268 @Test
269 public void testGetBoundaryPaths_isUnmodifiable() {
270
271 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
272 tree.insert(EQUATOR.span());
273
274
275 GeometryTestUtils.assertThrows(() -> {
276 tree.getBoundaryPaths().add(GreatArcPath.empty());
277 }, UnsupportedOperationException.class);
278 }
279
280 @Test
281 public void testToConvex_full() {
282
283 final RegionBSPTree2S tree = RegionBSPTree2S.full();
284
285
286 final List<ConvexArea2S> result = tree.toConvex();
287
288
289 Assert.assertEquals(1, result.size());
290 Assert.assertTrue(result.get(0).isFull());
291 }
292
293 @Test
294 public void testToConvex_empty() {
295
296 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
297
298
299 final List<ConvexArea2S> result = tree.toConvex();
300
301
302 Assert.assertEquals(0, result.size());
303 }
304
305 @Test
306 public void testToConvex_doubleLune() {
307
308 final RegionBSPTree2S tree = GreatArcPath.builder(TEST_PRECISION)
309 .append(EQUATOR.arc(0, PlaneAngleRadians.PI))
310 .append(X_MERIDIAN.arc(PlaneAngleRadians.PI, 0))
311 .append(EQUATOR.reverse().arc(0, PlaneAngleRadians.PI))
312 .append(X_MERIDIAN.reverse().arc(PlaneAngleRadians.PI, 0))
313 .build()
314 .toTree();
315
316
317 final List<ConvexArea2S> result = tree.toConvex();
318
319
320 Assert.assertEquals(2, result.size());
321
322 final double size = result.stream().mapToDouble(ConvexArea2S::getSize).sum();
323 Assert.assertEquals(PlaneAngleRadians.TWO_PI, size, TEST_EPS);
324 }
325
326 @Test
327 public void testToConvex_doubleLune_complement() {
328
329 final RegionBSPTree2S tree = GreatArcPath.builder(TEST_PRECISION)
330 .append(EQUATOR.arc(0, PlaneAngleRadians.PI))
331 .append(X_MERIDIAN.arc(PlaneAngleRadians.PI, 0))
332 .append(EQUATOR.reverse().arc(0, PlaneAngleRadians.PI))
333 .append(X_MERIDIAN.reverse().arc(PlaneAngleRadians.PI, 0))
334 .build()
335 .toTree();
336
337
338 final List<ConvexArea2S> result = tree.toConvex();
339
340
341 Assert.assertEquals(2, result.size());
342
343 final double size = result.stream().mapToDouble(ConvexArea2S::getSize).sum();
344 Assert.assertEquals(PlaneAngleRadians.TWO_PI, size, TEST_EPS);
345 }
346
347 @Test
348 public void testProject() {
349
350 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
351 tree.insert(EQUATOR.arc(0, PlaneAngleRadians.PI));
352 tree.insert(X_MERIDIAN.arc(PlaneAngleRadians.PI, 0));
353
354
355 SphericalTestUtils.assertPointsEq(Point2S.of(PlaneAngleRadians.PI_OVER_TWO, PlaneAngleRadians.PI_OVER_TWO),
356 tree.project(Point2S.of(PlaneAngleRadians.PI_OVER_TWO, PlaneAngleRadians.PI_OVER_TWO + 0.2)), TEST_EPS);
357 SphericalTestUtils.assertPointsEq(Point2S.PLUS_K,
358 tree.project(Point2S.of(-PlaneAngleRadians.PI_OVER_TWO, 0.2)), TEST_EPS);
359
360 SphericalTestUtils.assertPointsEq(Point2S.PLUS_I,
361 tree.project(Point2S.of(-0.5, PlaneAngleRadians.PI_OVER_TWO)), TEST_EPS);
362 SphericalTestUtils.assertPointsEq(Point2S.MINUS_I,
363 tree.project(Point2S.of(PlaneAngleRadians.PI + 0.5, PlaneAngleRadians.PI_OVER_TWO)), TEST_EPS);
364
365 final Point2S centroid = tree.getCentroid();
366 SphericalTestUtils.assertPointsEq(Point2S.PLUS_K,
367 tree.project(centroid.slerp(Point2S.PLUS_K, 1e-10)), TEST_EPS);
368 SphericalTestUtils.assertPointsEq(Point2S.PLUS_J,
369 tree.project(centroid.slerp(Point2S.PLUS_J, 1e-10)), TEST_EPS);
370 }
371
372 @Test
373 public void testProject_noBoundaries() {
374
375 Assert.assertNull(RegionBSPTree2S.empty().project(Point2S.PLUS_I));
376 Assert.assertNull(RegionBSPTree2S.full().project(Point2S.PLUS_I));
377 }
378
379 @Test
380 public void testGeometricProperties_full() {
381
382 final RegionBSPTree2S tree = RegionBSPTree2S.full();
383
384
385 Assert.assertEquals(4 * PlaneAngleRadians.PI, tree.getSize(), TEST_EPS);
386 Assert.assertNull(tree.getCentroid());
387
388 Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
389
390 Assert.assertEquals(0, tree.getBoundaries().size());
391 Assert.assertEquals(0, tree.getBoundaryPaths().size());
392 }
393
394 @Test
395 public void testGeometricProperties_empty() {
396
397 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
398
399
400 Assert.assertEquals(0, tree.getSize(), TEST_EPS);
401 Assert.assertNull(tree.getCentroid());
402
403 Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
404
405 Assert.assertEquals(0, tree.getBoundaries().size());
406 Assert.assertEquals(0, tree.getBoundaryPaths().size());
407 }
408
409 @Test
410 public void testGeometricProperties_halfSpace() {
411
412 final RegionBSPTree2S tree = RegionBSPTree2S.full();
413 tree.getRoot().cut(EQUATOR);
414
415
416 Assert.assertEquals(PlaneAngleRadians.TWO_PI, tree.getSize(), TEST_EPS);
417 Assert.assertEquals(PlaneAngleRadians.TWO_PI, tree.getBoundarySize(), TEST_EPS);
418 SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, tree.getCentroid(), TEST_EPS);
419
420 checkCentroidConsistency(tree);
421
422 final List<GreatArc> arcs = tree.getBoundaries();
423 Assert.assertEquals(1, arcs.size());
424
425 final GreatArc arc = arcs.get(0);
426 Assert.assertSame(EQUATOR, arc.getCircle());
427 Assert.assertNull(arc.getStartPoint());
428 Assert.assertNull(arc.getEndPoint());
429
430 final List<GreatArcPath> paths = tree.getBoundaryPaths();
431 Assert.assertEquals(1, paths.size());
432
433 final GreatArcPath path = paths.get(0);
434 Assert.assertEquals(1, path.getArcs().size());
435 Assert.assertTrue(path.getArcs().get(0).isFull());
436 }
437
438 @Test
439 public void testGeometricProperties_doubleLune() {
440
441 final RegionBSPTree2S tree = GreatArcPath.builder(TEST_PRECISION)
442 .append(EQUATOR.arc(0, PlaneAngleRadians.PI))
443 .append(X_MERIDIAN.arc(PlaneAngleRadians.PI, 0))
444 .append(EQUATOR.reverse().arc(0, PlaneAngleRadians.PI))
445 .append(X_MERIDIAN.reverse().arc(PlaneAngleRadians.PI, 0))
446 .build()
447 .toTree();
448
449
450 Assert.assertEquals(2 * PlaneAngleRadians.PI, tree.getSize(), TEST_EPS);
451 Assert.assertEquals(4 * PlaneAngleRadians.PI, tree.getBoundarySize(), TEST_EPS);
452 Assert.assertNull(tree.getCentroid());
453
454 final List<GreatArcPath> paths = tree.getBoundaryPaths();
455 Assert.assertEquals(2, paths.size());
456
457 assertPath(paths.get(0), Point2S.PLUS_I, Point2S.MINUS_I, Point2S.PLUS_I);
458 assertPath(paths.get(1), Point2S.PLUS_I, Point2S.MINUS_I, Point2S.PLUS_I);
459
460 SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE,
461 Point2S.of(0.5 * PlaneAngleRadians.PI, 0.25 * PlaneAngleRadians.PI),
462 Point2S.of(1.5 * PlaneAngleRadians.PI, 0.75 * PlaneAngleRadians.PI));
463
464 SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE,
465 Point2S.of(0.5 * PlaneAngleRadians.PI, 0.75 * PlaneAngleRadians.PI),
466 Point2S.of(1.5 * PlaneAngleRadians.PI, 0.25 * PlaneAngleRadians.PI));
467 }
468
469 @Test
470 public void testGeometricProperties_quadrant() {
471
472 final RegionBSPTree2S tree = GreatArcPath.builder(TEST_PRECISION)
473 .appendVertices(Point2S.MINUS_K, Point2S.PLUS_I, Point2S.MINUS_J)
474 .close()
475 .toTree();
476
477
478 Assert.assertEquals(0.5 * PlaneAngleRadians.PI, tree.getSize(), TEST_EPS);
479 Assert.assertEquals(1.5 * PlaneAngleRadians.PI, tree.getBoundarySize(), TEST_EPS);
480
481 final Point2S center = Point2S.from(Point2S.MINUS_K.getVector()
482 .add(Point2S.PLUS_I.getVector())
483 .add(Point2S.MINUS_J.getVector()));
484 SphericalTestUtils.assertPointsEq(center, tree.getCentroid(), TEST_EPS);
485
486 checkCentroidConsistency(tree);
487
488 final List<GreatArcPath> paths = tree.getBoundaryPaths();
489 Assert.assertEquals(1, paths.size());
490
491 assertPath(paths.get(0), Point2S.MINUS_J, Point2S.MINUS_K, Point2S.PLUS_I, Point2S.MINUS_J);
492
493 SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE,
494 Point2S.of(1.75 * PlaneAngleRadians.PI, 0.75 * PlaneAngleRadians.PI));
495
496 SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE,
497 Point2S.PLUS_J, Point2S.PLUS_K, Point2S.MINUS_I);
498 }
499
500 @Test
501 public void testGeometricProperties_quadrant_complement() {
502
503 final RegionBSPTree2S tree = GreatArcPath.builder(TEST_PRECISION)
504 .appendVertices(Point2S.MINUS_K, Point2S.PLUS_I, Point2S.MINUS_J)
505 .close()
506 .toTree();
507
508
509 tree.complement();
510
511
512 Assert.assertEquals(3.5 * PlaneAngleRadians.PI, tree.getSize(), TEST_EPS);
513 Assert.assertEquals(1.5 * PlaneAngleRadians.PI, tree.getBoundarySize(), TEST_EPS);
514
515 final Point2S center = Point2S.from(Point2S.MINUS_K.getVector()
516 .add(Point2S.PLUS_I.getVector())
517 .add(Point2S.MINUS_J.getVector()));
518 SphericalTestUtils.assertPointsEq(center.antipodal(), tree.getCentroid(), TEST_EPS);
519
520 checkCentroidConsistency(tree);
521
522 final List<GreatArcPath> paths = tree.getBoundaryPaths();
523 Assert.assertEquals(1, paths.size());
524
525 assertPath(paths.get(0), Point2S.MINUS_J, Point2S.PLUS_I, Point2S.MINUS_K, Point2S.MINUS_J);
526
527 SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE,
528 Point2S.of(1.75 * PlaneAngleRadians.PI, 0.75 * PlaneAngleRadians.PI));
529
530 SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE,
531 Point2S.PLUS_J, Point2S.PLUS_K, Point2S.MINUS_I);
532 }
533
534 @Test
535 public void testGeometricProperties_polygonWithHole() {
536
537 final Point2S center = Point2S.of(0.5, 2);
538
539 final double outerRadius = 1;
540 final double innerRadius = 0.5;
541
542 final RegionBSPTree2S outer = buildDiamond(center, outerRadius);
543 final RegionBSPTree2S inner = buildDiamond(center, innerRadius);
544
545
546 inner.transform(Transform2S.createRotation(center, 0.25 * Math.PI));
547
548
549 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
550 tree.difference(outer, inner);
551
552
553 final double area = 4 * (rightTriangleArea(outerRadius, outerRadius) - rightTriangleArea(innerRadius, innerRadius));
554 Assert.assertEquals(area, tree.getSize(), TEST_EPS);
555
556 final double outerSideLength = sphericalHypot(outerRadius, outerRadius);
557 final double innerSideLength = sphericalHypot(innerRadius, innerRadius);
558 final double boundarySize = 4 * (outerSideLength + innerSideLength);
559 Assert.assertEquals(boundarySize, tree.getBoundarySize(), TEST_EPS);
560
561 SphericalTestUtils.assertPointsEq(center, tree.getCentroid(), TEST_EPS);
562 checkCentroidConsistency(tree);
563
564 SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE, center);
565 }
566
567 @Test
568 public void testGeometricProperties_polygonWithHole_small() {
569
570 final Point2S center = Point2S.of(0.5, 2);
571
572 final double outerRadius = 1e-5;
573 final double innerRadius = 1e-7;
574
575 final RegionBSPTree2S outer = buildDiamond(center, outerRadius);
576 final RegionBSPTree2S inner = buildDiamond(center, innerRadius);
577
578
579 inner.transform(Transform2S.createRotation(center, 0.25 * Math.PI));
580
581
582 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
583 tree.difference(outer, inner);
584
585
586
587
588
589 final double area = (2 * outerRadius * outerRadius) - (2 * innerRadius * innerRadius);
590 Assert.assertEquals(area, tree.getSize(), TEST_EPS);
591
592 final double outerSideLength = Math.hypot(outerRadius, outerRadius);
593 final double innerSideLength = Math.hypot(innerRadius, innerRadius);
594 final double boundarySize = 4 * (outerSideLength + innerSideLength);
595 Assert.assertEquals(boundarySize, tree.getBoundarySize(), TEST_EPS);
596
597 SphericalTestUtils.assertPointsEq(center, tree.getCentroid(), TEST_EPS);
598 checkCentroidConsistency(tree);
599
600 SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE, center);
601 }
602
603 @Test
604 public void testGeometricProperties_polygonWithHole_complex() {
605
606 final Point2S center = Point2S.of(0.5, 2);
607
608 final double outerRadius = 2;
609 final double midRadius = 1;
610 final double innerRadius = 0.5;
611
612 final RegionBSPTree2S outer = buildDiamond(center, outerRadius);
613 final RegionBSPTree2S mid = buildDiamond(center, midRadius);
614 final RegionBSPTree2S inner = buildDiamond(center, innerRadius);
615
616
617 mid.transform(Transform2S.createRotation(center, 0.25 * Math.PI));
618
619
620 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
621 tree.difference(outer, mid);
622 tree.union(inner);
623 tree.complement();
624
625
626
627
628 final double nonComplementedArea = 4 * ((PlaneAngleRadians.PI - rightTriangleArea(outerRadius, outerRadius) -
629 rightTriangleArea(midRadius, midRadius) + rightTriangleArea(innerRadius, innerRadius)));
630 final double area = (4 * PlaneAngleRadians.PI) - nonComplementedArea;
631 Assert.assertEquals(area, tree.getSize(), TEST_EPS);
632
633 final double outerSideLength = sphericalHypot(outerRadius, outerRadius);
634 final double midSideLength = sphericalHypot(midRadius, midRadius);
635 final double innerSideLength = sphericalHypot(innerRadius, innerRadius);
636 final double boundarySize = 4 * (outerSideLength + midSideLength + innerSideLength);
637 Assert.assertEquals(boundarySize, tree.getBoundarySize(), TEST_EPS);
638
639 SphericalTestUtils.assertPointsEq(center.antipodal(), tree.getCentroid(), TEST_EPS);
640 checkCentroidConsistency(tree);
641
642 SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE, center);
643 }
644
645 @Test
646 public void testGeometricProperties_smallRightTriangle() {
647
648 final double azOffset = 1e-5;
649 final double polarOffset = 1e-6;
650
651 final double minAz = 0;
652 final double maxAz = minAz + azOffset;
653 final double maxPolar = PlaneAngleRadians.PI_OVER_TWO;
654 final double minPolar = maxPolar - polarOffset;
655
656 final Point2S p0 = Point2S.of(minAz, maxPolar);
657 final Point2S p1 = Point2S.of(maxAz, maxPolar);
658 final Point2S p2 = Point2S.of(maxAz, minPolar);
659
660
661 final RegionBSPTree2S tree = GreatArcPath.fromVertexLoop(Arrays.asList(p0, p1, p2), TEST_PRECISION)
662 .toTree();
663
664
665
666
667
668 final double expectedArea = 0.5 * azOffset * polarOffset;
669 Assert.assertEquals(expectedArea, tree.getSize(), TEST_EPS);
670
671 final double expectedBoundarySize = azOffset + polarOffset + Math.hypot(azOffset, polarOffset);
672 Assert.assertEquals(expectedBoundarySize, tree.getBoundarySize(), TEST_EPS);
673
674 Assert.assertTrue(tree.contains(tree.getCentroid()));
675 checkCentroidConsistency(tree);
676
677 SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE,
678 tree.getCentroid(),
679 Point2S.of(minAz + (0.75 * azOffset), minPolar + (0.75 * polarOffset)));
680
681 SphericalTestUtils.checkClassify(tree, RegionLocation.BOUNDARY,
682 p0, p1, p2, p0.slerp(p1, 0.5), p1.slerp(p2, 0.5), p2.slerp(p0, 0.5));
683
684 final double midAz = minAz + (0.5 * azOffset);
685 final double pastMinAz = minAz - azOffset;
686 final double pastMaxAz = maxAz + azOffset;
687
688 final double midPolar = minPolar + (0.5 * polarOffset);
689 final double pastMinPolar = minPolar - polarOffset;
690 final double pastMaxPolar = maxPolar + polarOffset;
691
692 SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE,
693 tree.getCentroid().antipodal(),
694 Point2S.of(pastMinAz, midPolar), Point2S.of(pastMaxAz, midPolar),
695 Point2S.of(midAz, pastMinPolar), Point2S.of(midAz, pastMaxPolar));
696 }
697
698 @Test
699 public void testGeometricProperties_equalAndOppositeRegions() {
700
701 final Point2S center = Point2S.PLUS_I;
702 final double radius = 0.25 * Math.PI;
703
704 final RegionBSPTree2S a = buildDiamond(center, radius);
705 final RegionBSPTree2S b = buildDiamond(center.antipodal(), radius);
706
707
708 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
709 tree.union(a, b);
710
711
712 final double area = 8 * rightTriangleArea(radius, radius);
713 Assert.assertEquals(area, tree.getSize(), TEST_EPS);
714
715 final double boundarySize = 8 * sphericalHypot(radius, radius);
716 Assert.assertEquals(boundarySize, tree.getBoundarySize(), TEST_EPS);
717
718
719 Assert.assertNull(tree.getCentroid());
720 }
721
722 @Test
723 public void testSplit_both() {
724
725 final GreatCircle c1 = GreatCircles.fromPole(Vector3D.Unit.MINUS_X, TEST_PRECISION);
726 final GreatCircle c2 = GreatCircles.fromPole(Vector3D.of(1, 1, 0), TEST_PRECISION);
727
728 final RegionBSPTree2S tree = ConvexArea2S.fromBounds(c1, c2).toTree();
729
730 final GreatCircle splitter = GreatCircles.fromPole(Vector3D.of(-1, 0, 1), TEST_PRECISION);
731
732
733 final Split<RegionBSPTree2S> split = tree.split(splitter);
734
735
736 Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
737
738 final Point2S p1 = c1.intersection(splitter);
739 final Point2S p2 = splitter.intersection(c2);
740
741 final RegionBSPTree2S minus = split.getMinus();
742 final List<GreatArcPath> minusPaths = minus.getBoundaryPaths();
743 Assert.assertEquals(1, minusPaths.size());
744 assertPath(minusPaths.get(0), Point2S.PLUS_K, p1, p2, Point2S.PLUS_K);
745
746 final RegionBSPTree2S plus = split.getPlus();
747 final List<GreatArcPath> plusPaths = plus.getBoundaryPaths();
748 Assert.assertEquals(1, plusPaths.size());
749 assertPath(plusPaths.get(0), p1, Point2S.MINUS_K, p2, p1);
750
751 Assert.assertEquals(tree.getSize(), minus.getSize() + plus.getSize(), TEST_EPS);
752 }
753
754 @Test
755 public void testSplit_minus() {
756
757 final RegionBSPTree2S tree = ConvexArea2S.fromVertexLoop(Arrays.asList(
758 Point2S.PLUS_I, Point2S.PLUS_K, Point2S.MINUS_J
759 ), TEST_PRECISION).toTree();
760
761 final GreatCircle splitter = GreatCircles.fromPole(Vector3D.of(0, -1, 1), TEST_PRECISION);
762
763
764 final Split<RegionBSPTree2S> split = tree.split(splitter);
765
766
767 Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
768
769 final RegionBSPTree2S minus = split.getMinus();
770 Assert.assertNotSame(tree, minus);
771 Assert.assertEquals(tree.getSize(), minus.getSize(), TEST_EPS);
772
773 Assert.assertNull(split.getPlus());
774 }
775
776 @Test
777 public void testSplit_plus() {
778
779 final RegionBSPTree2S tree = ConvexArea2S.fromVertexLoop(Arrays.asList(
780 Point2S.PLUS_I, Point2S.PLUS_K, Point2S.MINUS_J
781 ), TEST_PRECISION).toTree();
782
783 final GreatCircle splitter = GreatCircles.fromPole(Vector3D.of(0, 1, -1), TEST_PRECISION);
784
785
786 final Split<RegionBSPTree2S> split = tree.split(splitter);
787
788
789 Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
790
791 Assert.assertNull(split.getMinus());
792
793 final RegionBSPTree2S plus = split.getPlus();
794 Assert.assertNotSame(tree, plus);
795 Assert.assertEquals(tree.getSize(), plus.getSize(), TEST_EPS);
796 }
797
798 @Test
799 public void testTransform() {
800
801 final Transform2S t = Transform2S.createReflection(Point2S.PLUS_J);
802 final RegionBSPTree2S tree = ConvexArea2S.fromVertexLoop(
803 Arrays.asList(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K), TEST_PRECISION).toTree();
804
805
806 tree.transform(t);
807
808
809 Assert.assertFalse(tree.isFull());
810 Assert.assertFalse(tree.isEmpty());
811 Assert.assertEquals(1.5 * PlaneAngleRadians.PI, tree.getBoundarySize(), TEST_EPS);
812 Assert.assertEquals(PlaneAngleRadians.PI_OVER_TWO, tree.getSize(), TEST_EPS);
813
814 final Point2S expectedCentroid = triangleCentroid(Point2S.MINUS_J, Point2S.PLUS_I, Point2S.PLUS_K);
815 SphericalTestUtils.assertPointsEq(expectedCentroid, tree.getCentroid(), TEST_EPS);
816
817 checkCentroidConsistency(tree);
818
819 SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE,
820 Point2S.of(-0.25 * PlaneAngleRadians.PI, 0.25 * PlaneAngleRadians.PI));
821
822 SphericalTestUtils.checkClassify(tree, RegionLocation.BOUNDARY,
823 Point2S.PLUS_I, Point2S.MINUS_J, Point2S.PLUS_K,
824 Point2S.of(0, 0.25 * PlaneAngleRadians.PI), Point2S.of(-PlaneAngleRadians.PI_OVER_TWO, 0.304 * PlaneAngleRadians.PI),
825 Point2S.of(-0.25 * PlaneAngleRadians.PI, PlaneAngleRadians.PI_OVER_TWO));
826
827 SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE,
828 Point2S.PLUS_J, Point2S.MINUS_I, Point2S.MINUS_K);
829 }
830
831 @Test
832 public void testRegionNode_getNodeRegion() {
833
834 final RegionBSPTree2S tree = RegionBSPTree2S.empty();
835
836 final RegionNode2S root = tree.getRoot();
837 final RegionNode2S minus = root.cut(EQUATOR).getMinus();
838 final RegionNode2S minusPlus = minus.cut(X_MERIDIAN).getPlus();
839
840
841 final ConvexArea2S rootRegion = root.getNodeRegion();
842 Assert.assertEquals(4 * PlaneAngleRadians.PI, rootRegion.getSize(), TEST_EPS);
843 Assert.assertNull(rootRegion.getCentroid());
844
845 final ConvexArea2S minusRegion = minus.getNodeRegion();
846 Assert.assertEquals(2 * PlaneAngleRadians.PI, minusRegion.getSize(), TEST_EPS);
847 SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, minusRegion.getCentroid(), TEST_EPS);
848
849 final ConvexArea2S minusPlusRegion = minusPlus.getNodeRegion();
850 Assert.assertEquals(PlaneAngleRadians.PI, minusPlusRegion.getSize(), TEST_EPS);
851 SphericalTestUtils.assertPointsEq(Point2S.of(1.5 * PlaneAngleRadians.PI, 0.25 * PlaneAngleRadians.PI),
852 minusPlusRegion.getCentroid(), TEST_EPS);
853 }
854
855 @Test
856 public void testGeographicMap() {
857
858 final RegionBSPTree2S continental = latLongToTree(TEST_PRECISION, new double[][] {
859 {51.14850, 2.51357}, {50.94660, 1.63900}, {50.12717, 1.33876}, {49.34737, -0.98946},
860 {49.77634, -1.93349}, {48.64442, -1.61651}, {48.90169, -3.29581}, {48.68416, -4.59234},
861 {47.95495, -4.49155}, {47.57032, -2.96327}, {46.01491, -1.19379}, {44.02261, -1.38422},
862 {43.42280, -1.90135}, {43.03401, -1.50277}, {42.34338, 1.82679}, {42.47301, 2.98599},
863 {43.07520, 3.10041}, {43.39965, 4.55696}, {43.12889, 6.52924}, {43.69384, 7.43518},
864 {44.12790, 7.54959}, {45.02851, 6.74995}, {45.33309, 7.09665}, {46.42967, 6.50009},
865 {46.27298, 6.02260}, {46.72577, 6.03738}, {47.62058, 7.46675}, {49.01778, 8.09927},
866 {49.20195, 6.65822}, {49.44266, 5.89775}, {49.98537, 4.79922}
867 });
868 final RegionBSPTree2S corsica = latLongToTree(TEST_PRECISION, new double[][] {
869 {42.15249, 9.56001}, {43.00998, 9.39000}, {42.62812, 8.74600}, {42.25651, 8.54421},
870 {41.58361, 8.77572}, {41.38000, 9.22975}
871 });
872
873
874 final RegionBSPTree2S france = RegionBSPTree2S.empty();
875 france.union(continental, corsica);
876
877
878 Assert.assertEquals(0.6316801448267251, france.getBoundarySize(), TEST_EPS);
879 Assert.assertEquals(0.013964220234478741, france.getSize(), TEST_EPS);
880
881 SphericalTestUtils.assertPointsEq(Point2S.of(0.04368552749392928, 0.7590839905197961),
882 france.getCentroid(), CENTROID_EPS);
883
884 checkCentroidConsistency(france);
885 }
886
887 @Test
888 public void testCircleToPolygonCentroid() {
889 final double radius = 0.0001;
890 final Point2S center = Point2S.of(1.0, 1.0);
891 final int numPts = 200;
892
893
894 final RegionBSPTree2S ccw = circleToPolygon(center, radius, numPts, false, TEST_PRECISION);
895 SphericalTestUtils.assertPointsEq(center, ccw.getCentroid(), TEST_EPS);
896
897
898 final RegionBSPTree2S cw = circleToPolygon(center, radius, numPts, true, TEST_PRECISION);
899
900 SphericalTestUtils.assertPointsEq(center.antipodal(), cw.getCentroid(), CENTROID_EPS);
901 }
902
903 @Test
904 public void testCircleToPolygonSize() {
905 final double radius = 0.0001;
906 final Point2S center = Point2S.of(1.0, 1.0);
907 final int numPts = 200;
908
909
910 final double ccwArea = 4.0 * PlaneAngleRadians.PI * Math.pow(Math.sin(radius / 2.0), 2.0);
911 final double cwArea = 4.0 * PlaneAngleRadians.PI - ccwArea;
912
913 final RegionBSPTree2S ccw = circleToPolygon(center, radius, numPts, false, TEST_PRECISION);
914 Assert.assertEquals("Counterclockwise size", ccwArea, ccw.getSize(), TEST_EPS);
915
916 final RegionBSPTree2S cw = circleToPolygon(center, radius, numPts, true, TEST_PRECISION);
917 Assert.assertEquals("Clockwise size", cwArea, cw.getSize(), TEST_EPS);
918 }
919
920 @Test
921 public void testCircleToPolygonBoundarySize() {
922 final double radius = 0.0001;
923 final Point2S center = Point2S.of(1.0, 1.0);
924 final int numPts = 200;
925
926
927 final double boundary = PlaneAngleRadians.TWO_PI * Math.sin(radius);
928
929 final RegionBSPTree2S ccw = circleToPolygon(center, radius, numPts, false, TEST_PRECISION);
930 Assert.assertEquals("Counterclockwise boundary size", boundary, ccw.getBoundarySize(), 1.0e-7);
931
932 final RegionBSPTree2S cw = circleToPolygon(center, radius, numPts, true, TEST_PRECISION);
933 Assert.assertEquals("Clockwise boundary size", boundary, cw.getBoundarySize(), 1.0e-7);
934 }
935
936 @Test
937 public void testSmallCircleToPolygon() {
938
939 final double radius = 5.0e-8;
940 final Point2S center = Point2S.of(0.5, 1.5);
941 final int numPts = 100;
942
943
944 final RegionBSPTree2S circle = circleToPolygon(center, radius, numPts, false, TEST_PRECISION);
945
946
947
948 final double area = 4.0 * PlaneAngleRadians.PI * Math.pow(Math.sin(radius / 2.0), 2.0);
949 final double boundary = PlaneAngleRadians.TWO_PI * Math.sin(radius);
950
951 SphericalTestUtils.assertPointsEq(center, circle.getCentroid(), TEST_EPS);
952 Assert.assertEquals(area, circle.getSize(), TEST_EPS);
953 Assert.assertEquals(boundary, circle.getBoundarySize(), TEST_EPS);
954 }
955
956 @Test
957 public void testSmallGeographicalRectangle() {
958
959 final double[][] vertices = {
960 {42.656216727628696, -70.61919768884546},
961 {42.65612858998112, -70.61938607250165},
962 {42.65579098923594, -70.61909615581666},
963 {42.655879126692355, -70.61890777301083}
964 };
965
966
967 final RegionBSPTree2S rectangle = latLongToTree(TEST_PRECISION, vertices);
968
969
970
971 final double avgLat = Stream.of(vertices).mapToDouble(v -> v[0]).average().getAsDouble();
972 final double avgLon = Stream.of(vertices).mapToDouble(v -> v[1]).average().getAsDouble();
973 final Point2S expectedCentroid = latLongToPoint(avgLat, avgLon);
974
975 SphericalTestUtils.assertPointsEq(expectedCentroid, rectangle.getCentroid(), TEST_EPS);
976
977
978 Assert.assertEquals(1.997213869978027E-11, rectangle.getSize(), TEST_EPS);
979 Assert.assertEquals(1.9669710464585642E-5, rectangle.getBoundarySize(), TEST_EPS);
980 }
981
982
983
984
985
986 private static void insertPositiveQuadrant(final RegionBSPTree2S tree) {
987 tree.insert(Arrays.asList(
988 EQUATOR.arc(Point2S.PLUS_I, Point2S.PLUS_J),
989 X_MERIDIAN.arc(Point2S.PLUS_K, Point2S.PLUS_I),
990 Y_MERIDIAN.arc(Point2S.PLUS_J, Point2S.PLUS_K)
991 ));
992 }
993
994 private static Point2S triangleCentroid(final Point2S p1, final Point2S p2, final Point2S p3) {
995
996 final GreatCircle c1 = GreatCircles.fromPoints(p1, p2.slerp(p3, 0.5), TEST_PRECISION);
997 final GreatCircle c2 = GreatCircles.fromPoints(p2, p1.slerp(p3, 0.5), TEST_PRECISION);
998
999 return c1.intersection(c2);
1000 }
1001
1002 private static void assertPath(final GreatArcPath path, final Point2S... vertices) {
1003 final List<Point2S> expected = Arrays.asList(vertices);
1004 final List<Point2S> actual = path.getVertices();
1005
1006 if (expected.size() != actual.size()) {
1007 Assert.fail("Unexpected path size. Expected path " + expected +
1008 " but was " + actual);
1009 }
1010
1011 for (int i = 0; i < expected.size(); ++i) {
1012 if (!expected.get(i).eq(actual.get(i), TEST_PRECISION)) {
1013 Assert.fail("Unexpected path vertex at index " + i + ". Expected path " + expected +
1014 " but was " + actual);
1015 }
1016 }
1017 }
1018
1019 private static RegionBSPTree2S latLongToTree(final DoublePrecisionContext precision, final double[][] points) {
1020 final GreatArcPath.Builder pathBuilder = GreatArcPath.builder(precision);
1021
1022 for (int i = 0; i < points.length; ++i) {
1023 pathBuilder.append(latLongToPoint(points[i][0], points[i][1]));
1024 }
1025
1026 return pathBuilder.close().toTree();
1027 }
1028
1029 private static Point2S latLongToPoint(final double latitude, final double longitude) {
1030 return Point2S.of(Math.toRadians(longitude), Math.toRadians(90.0 - latitude));
1031 }
1032
1033 private static void checkCentroidConsistency(final RegionBSPTree2S region) {
1034 final Point2S centroid = region.getCentroid();
1035 final double size = region.getSize();
1036
1037 final GreatCircle circle = GreatCircles.fromPole(centroid.getVector(), TEST_PRECISION);
1038 for (double az = 0; az <= PlaneAngleRadians.TWO_PI; az += 0.2) {
1039 final Point2S pt = circle.toSpace(Point1S.of(az));
1040 final GreatCircle splitter = GreatCircles.fromPoints(centroid, pt, TEST_PRECISION);
1041
1042 final Split<RegionBSPTree2S> split = region.split(splitter);
1043
1044 Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
1045
1046 final RegionBSPTree2S minus = split.getMinus();
1047 final double minusSize = minus.getSize();
1048
1049 final RegionBSPTree2S plus = split.getPlus();
1050 final double plusSize = plus.getSize();
1051
1052 final Point2S computedCentroid = Point2S.from(weightedCentroidVector(minus)
1053 .add(weightedCentroidVector(plus)));
1054
1055 Assert.assertEquals(size, minusSize + plusSize, TEST_EPS);
1056 SphericalTestUtils.assertPointsEq(centroid, computedCentroid, TEST_EPS);
1057 }
1058 }
1059
1060 private static Vector3D weightedCentroidVector(final RegionBSPTree2S tree) {
1061 Vector3D sum = Vector3D.ZERO;
1062 for (final ConvexArea2S convex : tree.toConvex()) {
1063 sum = sum.add(convex.getWeightedCentroidVector());
1064 }
1065
1066 return sum;
1067 }
1068
1069 private static RegionBSPTree2S buildDiamond(final Point2S center, final double radius) {
1070 final Vector3D u = center.getVector();
1071 final Vector3D w = u.orthogonal(Vector3D.Unit.PLUS_Z);
1072 final Vector3D v = w.cross(u);
1073
1074 final Transform2S rotV = Transform2S.createRotation(v, radius);
1075 final Transform2S rotW = Transform2S.createRotation(w, radius);
1076
1077 final Point2S top = rotV.inverse().apply(center);
1078 final Point2S bottom = rotV.apply(center);
1079
1080 final Point2S right = rotW.apply(center);
1081 final Point2S left = rotW.inverse().apply(center);
1082
1083 return GreatArcPath.fromVertexLoop(Arrays.asList(top, left, bottom, right), TEST_PRECISION)
1084 .toTree();
1085 }
1086
1087
1088
1089
1090
1091
1092
1093 private static double sphericalHypot(final double a, final double b) {
1094
1095
1096 return Math.acos(Math.cos(a) * Math.cos(b));
1097 }
1098
1099
1100
1101
1102
1103
1104
1105
1106 private static double rightTriangleArea(final double a, final double b) {
1107 final double c = sphericalHypot(a, b);
1108
1109
1110
1111 final double sinC = Math.sin(c);
1112 final double angleA = Math.asin(Math.sin(a) / sinC);
1113 final double angleB = Math.asin(Math.sin(b) / sinC);
1114
1115
1116 return angleA + angleB - PlaneAngleRadians.PI_OVER_TWO;
1117 }
1118
1119 private static RegionBSPTree2S circleToPolygon(final Point2S center, final double radius, final int numPts,
1120 final boolean clockwise, final DoublePrecisionContext precision) {
1121 final List<Point2S> pts = new ArrayList<>(numPts);
1122
1123
1124 pts.add(Transform2S.createRotation(center.getVector().orthogonal(), radius).apply(center));
1125
1126
1127 final double span = PlaneAngleRadians.TWO_PI / numPts;
1128
1129
1130 final Transform2S rotate = Transform2S.createRotation(center, clockwise ? -span : span);
1131 for (int i = 1; i < numPts; ++i) {
1132 pts.add(rotate.apply(pts.get(i - 1)));
1133 }
1134
1135 return GreatArcPath.fromVertexLoop(pts, precision).toTree();
1136 }
1137 }