1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.geometry.euclidean.twod.shape;
18
19 import java.util.ArrayList;
20 import java.util.Comparator;
21 import java.util.List;
22
23 import org.apache.commons.geometry.core.GeometryTestUtils;
24 import org.apache.commons.geometry.core.RegionLocation;
25 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
26 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
27 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
28 import org.apache.commons.geometry.euclidean.twod.Line;
29 import org.apache.commons.geometry.euclidean.twod.LineConvexSubset;
30 import org.apache.commons.geometry.euclidean.twod.LinecastPoint2D;
31 import org.apache.commons.geometry.euclidean.twod.Lines;
32 import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
33 import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
34 import org.apache.commons.geometry.euclidean.twod.Vector2D;
35 import org.apache.commons.geometry.euclidean.twod.path.LinePath;
36 import org.apache.commons.numbers.angle.PlaneAngleRadians;
37 import org.junit.Assert;
38 import org.junit.Test;
39
40 public class CircleTest {
41
42 private static final double TEST_EPS = 1e-10;
43
44 private static final DoublePrecisionContext TEST_PRECISION =
45 new EpsilonDoublePrecisionContext(TEST_EPS);
46
47 private static final Comparator<LineConvexSubset> SEGMENT_DIRECTION_COMPARATOR =
48 (a, b) -> Vector2D.COORDINATE_ASCENDING_ORDER.compare(
49 a.getLine().getDirection(),
50 b.getLine().getDirection());
51
52 @Test
53 public void testFrom() {
54
55 final Vector2D center = Vector2D.of(1, 2);
56
57
58 final Circle c = Circle.from(center, 3, TEST_PRECISION);
59
60
61 Assert.assertFalse(c.isFull());
62 Assert.assertFalse(c.isEmpty());
63
64 Assert.assertSame(center, c.getCenter());
65 Assert.assertSame(center, c.getCentroid());
66
67 Assert.assertEquals(3, c.getRadius(), 0.0);
68
69 Assert.assertSame(TEST_PRECISION, c.getPrecision());
70 }
71
72 @Test
73 public void testFrom_illegalCenter() {
74
75 GeometryTestUtils.assertThrows(() -> Circle.from(Vector2D.of(Double.POSITIVE_INFINITY, 1), 1, TEST_PRECISION),
76 IllegalArgumentException.class);
77 GeometryTestUtils.assertThrows(() -> Circle.from(Vector2D.of(Double.NaN, 1), 1, TEST_PRECISION),
78 IllegalArgumentException.class);
79 }
80
81 @Test
82 public void testFrom_illegalRadius() {
83
84 final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
85
86
87 GeometryTestUtils.assertThrows(() -> Circle.from(Vector2D.ZERO, -1, TEST_PRECISION),
88 IllegalArgumentException.class);
89 GeometryTestUtils.assertThrows(() -> Circle.from(Vector2D.ZERO, 0, TEST_PRECISION),
90 IllegalArgumentException.class);
91 GeometryTestUtils.assertThrows(() -> Circle.from(Vector2D.ZERO, Double.POSITIVE_INFINITY, TEST_PRECISION),
92 IllegalArgumentException.class);
93 GeometryTestUtils.assertThrows(() -> Circle.from(Vector2D.ZERO, Double.NaN, TEST_PRECISION),
94 IllegalArgumentException.class);
95
96 GeometryTestUtils.assertThrows(() -> Circle.from(Vector2D.ZERO, 1e-3, precision),
97 IllegalArgumentException.class);
98 }
99
100 @Test
101 public void testGeometricProperties() {
102
103 final double r = 2;
104 final Circle c = Circle.from(Vector2D.of(1, 2), r, TEST_PRECISION);
105
106
107 Assert.assertEquals(2 * Math.PI * r, c.getBoundarySize(), TEST_EPS);
108 Assert.assertEquals(Math.PI * r * r, c.getSize(), TEST_EPS);
109 }
110
111 @Test
112 public void testClassify() {
113
114 final Circle c = Circle.from(Vector2D.of(1, 2), 1, TEST_PRECISION);
115
116
117 EuclideanTestUtils.assertRegionLocation(c, RegionLocation.INSIDE,
118 Vector2D.of(1, 2),
119 Vector2D.of(0.5, 2), Vector2D.of(1.5, 2),
120 Vector2D.of(1, 1.5), Vector2D.of(1, 2.5),
121 Vector2D.of(0.5, 1.5), Vector2D.of(1.5, 2.5),
122 Vector2D.of(0.5, 2.5), Vector2D.of(1.5, 1.5));
123
124 EuclideanTestUtils.assertRegionLocation(c, RegionLocation.OUTSIDE,
125 Vector2D.of(-0.5, 2), Vector2D.of(2.5, 2),
126 Vector2D.of(1, 0.5), Vector2D.of(1, 3.5),
127 Vector2D.of(0.25, 1.25), Vector2D.of(1.75, 2.75),
128 Vector2D.of(0.25, 2.75), Vector2D.of(1.75, 1.25));
129
130 for (double angle = 0; angle < PlaneAngleRadians.TWO_PI; angle += 0.1) {
131 EuclideanTestUtils.assertRegionLocation(c, RegionLocation.BOUNDARY,
132 c.getCenter().add(PolarCoordinates.of(1, angle).toCartesian()));
133 }
134 }
135
136 @Test
137 public void testContains() {
138
139 final Circle c = Circle.from(Vector2D.of(1, 2), 1, TEST_PRECISION);
140
141
142 checkContains(c, true,
143 Vector2D.of(1, 2),
144 Vector2D.of(0.5, 2), Vector2D.of(1.5, 2),
145 Vector2D.of(1, 1.5), Vector2D.of(1, 2.5),
146 Vector2D.of(0.5, 1.5), Vector2D.of(1.5, 2.5),
147 Vector2D.of(0.5, 2.5), Vector2D.of(1.5, 1.5));
148
149 for (double angle = 0; angle < PlaneAngleRadians.TWO_PI; angle += 0.1) {
150 checkContains(c, true,
151 c.getCenter().add(PolarCoordinates.of(1, angle).toCartesian()));
152 }
153
154 checkContains(c, false,
155 Vector2D.of(-0.5, 2), Vector2D.of(2.5, 2),
156 Vector2D.of(1, 0.5), Vector2D.of(1, 3.5),
157 Vector2D.of(0.25, 1.25), Vector2D.of(1.75, 2.75),
158 Vector2D.of(0.25, 2.75), Vector2D.of(1.75, 1.25));
159 }
160
161 @Test
162 public void testProject() {
163
164 final Vector2D center = Vector2D.of(1.5, 2.5);
165 final double radius = 3;
166 final Circle c = Circle.from(center, radius, TEST_PRECISION);
167
168 EuclideanTestUtils.permute(-4, 4, 1, (x, y) -> {
169 final Vector2D pt = Vector2D.of(x, y);
170
171
172 final Vector2D projection = c.project(pt);
173
174
175 Assert.assertEquals(radius, center.distance(projection), TEST_EPS);
176 EuclideanTestUtils.assertCoordinatesEqual(center.directionTo(pt),
177 center.directionTo(projection), TEST_EPS);
178 });
179 }
180
181 @Test
182 public void testProject_argumentEqualsCenter() {
183
184 final Circle c = Circle.from(Vector2D.of(1, 2), 2, TEST_PRECISION);
185
186
187 final Vector2D projection = c.project(Vector2D.of(1, 2));
188
189
190 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 2), projection, TEST_EPS);
191 }
192
193 @Test
194 public void testIntersections() {
195
196 final Circle c = Circle.from(Vector2D.of(2, 1), 2, TEST_PRECISION);
197 final double sqrt3 = Math.sqrt(3);
198
199
200
201 checkIntersections(c, Lines.fromPoints(Vector2D.of(-1, 4), Vector2D.of(5, 4), TEST_PRECISION));
202 checkIntersections(c, Lines.fromPoints(Vector2D.of(-1, 3), Vector2D.of(5, 3), TEST_PRECISION),
203 Vector2D.of(2, 3));
204 checkIntersections(c, Lines.fromPoints(Vector2D.of(-1, 2), Vector2D.of(5, 2), TEST_PRECISION),
205 Vector2D.of(2 - sqrt3, 2), Vector2D.of(2 + sqrt3, 2));
206 checkIntersections(c, Lines.fromPoints(Vector2D.of(-1, 1), Vector2D.of(5, 1), TEST_PRECISION),
207 Vector2D.of(0, 1), Vector2D.of(4, 1));
208 checkIntersections(c, Lines.fromPoints(Vector2D.of(-1, 0), Vector2D.of(5, 0), TEST_PRECISION),
209 Vector2D.of(2 - sqrt3, 0), Vector2D.of(2 + sqrt3, 0));
210 checkIntersections(c, Lines.fromPoints(Vector2D.of(-1, -1), Vector2D.of(5, -1), TEST_PRECISION),
211 Vector2D.of(2, -1));
212 checkIntersections(c, Lines.fromPoints(Vector2D.of(-1, -2), Vector2D.of(5, -2), TEST_PRECISION));
213
214
215 checkIntersections(c, Lines.fromPoints(Vector2D.of(-1, -2), Vector2D.of(-1, 5), TEST_PRECISION));
216 checkIntersections(c, Lines.fromPoints(Vector2D.of(0, -2), Vector2D.of(0, 5), TEST_PRECISION),
217 Vector2D.of(0, 1));
218 checkIntersections(c, Lines.fromPoints(Vector2D.of(1, -2), Vector2D.of(1, 5), TEST_PRECISION),
219 Vector2D.of(1, 1 - sqrt3), Vector2D.of(1, 1 + sqrt3));
220 checkIntersections(c, Lines.fromPoints(Vector2D.of(2, -2), Vector2D.of(2, 5), TEST_PRECISION),
221 Vector2D.of(2, -1), Vector2D.of(2, 3));
222 checkIntersections(c, Lines.fromPoints(Vector2D.of(3, -2), Vector2D.of(3, 5), TEST_PRECISION),
223 Vector2D.of(3, 1 - sqrt3), Vector2D.of(3, 1 + sqrt3));
224 checkIntersections(c, Lines.fromPoints(Vector2D.of(4, -2), Vector2D.of(4, 5), TEST_PRECISION),
225 Vector2D.of(4, 1));
226 checkIntersections(c, Lines.fromPoints(Vector2D.of(5, -2), Vector2D.of(5, 5), TEST_PRECISION));
227
228
229 final Vector2D center = c.getCenter();
230 checkIntersections(c, Lines.fromPoints(Vector2D.ZERO, c.getCenter(), TEST_PRECISION),
231 center.withNorm(center.norm() - c.getRadius()), center.withNorm(center.norm() + c.getRadius()));
232 }
233
234 @Test
235 public void testLinecast() {
236
237 final Circle c = Circle.from(Vector2D.of(2, 1), 2, TEST_PRECISION);
238 final double sqrt3 = Math.sqrt(3);
239
240
241 checkLinecast(c, Lines.segmentFromPoints(Vector2D.of(-1, 0), Vector2D.of(5, 0), TEST_PRECISION),
242 Vector2D.of(2 - sqrt3, 0), Vector2D.of(2 + sqrt3, 0));
243 checkLinecast(c, Lines.segmentFromPoints(Vector2D.of(-1, 3), Vector2D.of(5, 3), TEST_PRECISION),
244 Vector2D.of(2, 3));
245 checkLinecast(c, Lines.segmentFromPoints(Vector2D.of(-1, -2), Vector2D.of(5, -2), TEST_PRECISION));
246 }
247
248 @Test
249 public void testLinecast_intersectionsNotInSegment() {
250
251 final Circle c = Circle.from(Vector2D.of(2, 1), 2, TEST_PRECISION);
252 final Line line = Lines.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION);
253
254
255 checkLinecast(c, line.segment(-1, 0));
256 checkLinecast(c, line.segment(1.5, 2.5));
257 checkLinecast(c, line.segment(1.5, 2.5));
258 checkLinecast(c, line.segment(4, 5));
259 }
260
261 @Test
262 public void testLinecast_segmentPointOnBoundary() {
263
264 final Circle c = Circle.from(Vector2D.of(2, 1), 2, TEST_PRECISION);
265 final Line line = Lines.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION);
266 final double sqrt3 = Math.sqrt(3);
267 final double start = 2 - sqrt3;
268 final double end = 2 + sqrt3;
269
270
271 checkLinecast(c, line.segment(start, 2), Vector2D.of(start, 0));
272 checkLinecast(c, line.segment(start, end), Vector2D.of(start, 0), Vector2D.of(end, 0));
273 checkLinecast(c, line.segment(end, 5), Vector2D.of(end, 0));
274 }
275
276 @Test
277 public void testToTree_threeSegments() {
278
279 final Circle c = Circle.from(Vector2D.of(2, 1), 2, TEST_PRECISION);
280
281
282 final RegionBSPTree2D tree = c.toTree(3);
283
284
285 checkBasicApproximationProperties(c, tree);
286
287 final List<LineConvexSubset> segments = new ArrayList<>(tree.getBoundaries());
288 segments.sort(SEGMENT_DIRECTION_COMPARATOR);
289
290 Assert.assertEquals(3, segments.size());
291
292 final double inc = PlaneAngleRadians.TWO_PI / 3.0;
293 final Vector2D p0 = Vector2D.of(4, 1);
294 final Vector2D p1 = Vector2D.of(
295 (2 * Math.cos(inc)) + 2,
296 (2 * Math.sin(inc)) + 1);
297 final Vector2D p2 = Vector2D.of(
298 (2 * Math.cos(2 * inc)) + 2,
299 (2 * Math.sin(2 * inc)) + 1);
300
301 assertFiniteSegment(segments.get(0), p0, p1);
302 assertFiniteSegment(segments.get(1), p1, p2);
303 assertFiniteSegment(segments.get(2), p2, p0);
304 }
305
306 @Test
307 public void testToTree_fourSegments() {
308
309 final Circle c = Circle.from(Vector2D.of(2, 1), 2, TEST_PRECISION);
310
311
312 final RegionBSPTree2D tree = c.toTree(4);
313
314
315 checkBasicApproximationProperties(c, tree);
316
317 final List<LineConvexSubset> segments = new ArrayList<>(tree.getBoundaries());
318 segments.sort(SEGMENT_DIRECTION_COMPARATOR);
319
320 Assert.assertEquals(4, segments.size());
321
322 final Vector2D p0 = Vector2D.of(4, 1);
323 final Vector2D p1 = Vector2D.of(2, 3);
324 final Vector2D p2 = Vector2D.of(0, 1);
325 final Vector2D p3 = Vector2D.of(2, -1);
326
327 assertFiniteSegment(segments.get(0), p1, p2);
328 assertFiniteSegment(segments.get(1), p0, p1);
329 assertFiniteSegment(segments.get(2), p2, p3);
330 assertFiniteSegment(segments.get(3), p3, p0);
331 }
332
333 @Test
334 public void testToTree_multipleApproximationSizes() {
335
336 final Circle c = Circle.from(Vector2D.of(-3, 5), 10, TEST_PRECISION);
337
338 final int min = 5;
339 final int max = 100;
340
341 RegionBSPTree2D tree;
342
343 double sizeDiff;
344 double prevSizeDiff = Double.POSITIVE_INFINITY;
345
346 for (int n = min; n <= max; ++n) {
347
348 tree = c.toTree(n);
349
350
351 checkBasicApproximationProperties(c, tree);
352
353
354 sizeDiff = c.getSize() - tree.getSize();
355 Assert.assertTrue("Expected size difference to decrease", sizeDiff < prevSizeDiff);
356
357 prevSizeDiff = sizeDiff;
358 }
359 }
360
361 @Test
362 public void testToTree_closeApproximation() {
363
364 final Circle c = Circle.from(Vector2D.of(-2, 0), 1, TEST_PRECISION);
365
366
367 final RegionBSPTree2D tree = c.toTree(100);
368
369
370 checkBasicApproximationProperties(c, tree);
371
372 final double eps = 5e-3;
373 Assert.assertEquals(c.getSize(), tree.getSize(), eps);
374 Assert.assertEquals(c.getBoundarySize(), tree.getBoundarySize(), eps);
375 EuclideanTestUtils.assertCoordinatesEqual(c.getCentroid(), tree.getCentroid(), eps);
376 }
377
378 @Test
379 public void testToTree_invalidSegmentCount() {
380
381 final Circle c = Circle.from(Vector2D.of(2, 1), 2, TEST_PRECISION);
382 final String baseMsg = "Circle approximation segment number must be greater than or equal to 3; was ";
383
384
385 GeometryTestUtils.assertThrows(() -> {
386 c.toTree(2);
387 }, IllegalArgumentException.class, baseMsg + "2");
388 GeometryTestUtils.assertThrows(() -> {
389 c.toTree(-1);
390 }, IllegalArgumentException.class, baseMsg + "-1");
391 }
392
393 @Test
394 public void testHashCode() {
395
396 final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
397
398 final Circle a = Circle.from(Vector2D.of(1, 2), 3, TEST_PRECISION);
399 final Circle b = Circle.from(Vector2D.of(1, 1), 3, TEST_PRECISION);
400 final Circle c = Circle.from(Vector2D.of(1, 2), 4, TEST_PRECISION);
401 final Circle d = Circle.from(Vector2D.of(1, 2), 3, precision);
402 final Circle e = Circle.from(Vector2D.of(1, 2), 3, TEST_PRECISION);
403
404
405 final int hash = a.hashCode();
406
407
408 Assert.assertEquals(hash, a.hashCode());
409
410 Assert.assertNotEquals(hash, b.hashCode());
411 Assert.assertNotEquals(hash, c.hashCode());
412 Assert.assertNotEquals(hash, d.hashCode());
413
414 Assert.assertEquals(hash, e.hashCode());
415 }
416
417 @Test
418 public void testEquals() {
419
420 final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
421
422 final Circle a = Circle.from(Vector2D.of(1, 2), 3, TEST_PRECISION);
423 final Circle b = Circle.from(Vector2D.of(1, 1), 3, TEST_PRECISION);
424 final Circle c = Circle.from(Vector2D.of(1, 2), 4, TEST_PRECISION);
425 final Circle d = Circle.from(Vector2D.of(1, 2), 3, precision);
426 final Circle e = Circle.from(Vector2D.of(1, 2), 3, TEST_PRECISION);
427
428
429 Assert.assertEquals(a, a);
430
431 Assert.assertFalse(a.equals(null));
432 Assert.assertFalse(a.equals(new Object()));
433
434 Assert.assertNotEquals(a, b);
435 Assert.assertNotEquals(a, c);
436 Assert.assertNotEquals(a, d);
437
438 Assert.assertEquals(a, e);
439 }
440
441 @Test
442 public void testToString() {
443
444 final Circle c = Circle.from(Vector2D.of(1, 2), 3, TEST_PRECISION);
445
446
447 final String str = c.toString();
448
449
450 Assert.assertEquals("Circle[center= (1.0, 2.0), radius= 3.0]", str);
451 }
452
453 private static void checkContains(final Circle circle, final boolean contains, final Vector2D... pts) {
454 for (final Vector2D pt : pts) {
455 Assert.assertEquals("Expected circle to " + (contains ? "" : "not") + "contain point " + pt,
456 contains, circle.contains(pt));
457 }
458 }
459
460 private static void checkIntersections(final Circle circle, final Line line, final Vector2D... expectedPts) {
461
462
463 final List<Vector2D> actualPtsForward = circle.intersections(line);
464 final List<Vector2D> actualPtsReverse = circle.intersections(line.reverse());
465
466 final Vector2D actualFirstForward = circle.firstIntersection(line);
467 final Vector2D actualFirstReverse = circle.firstIntersection(line.reverse());
468
469
470 final int len = expectedPts.length;
471
472
473 Assert.assertEquals(len, actualPtsForward.size());
474 Assert.assertEquals(len, actualPtsReverse.size());
475
476 for (int i = 0; i < len; ++i) {
477 EuclideanTestUtils.assertCoordinatesEqual(expectedPts[i], actualPtsForward.get(i), TEST_EPS);
478 Assert.assertEquals(circle.getRadius(), circle.getCenter().distance(actualPtsForward.get(i)), TEST_EPS);
479
480 EuclideanTestUtils.assertCoordinatesEqual(expectedPts[len - i - 1], actualPtsReverse.get(i), TEST_EPS);
481 Assert.assertEquals(circle.getRadius(), circle.getCenter().distance(actualPtsReverse.get(i)), TEST_EPS);
482 }
483
484
485 if (len > 0) {
486 Assert.assertNotNull(actualFirstForward);
487 Assert.assertNotNull(actualFirstReverse);
488
489 EuclideanTestUtils.assertCoordinatesEqual(expectedPts[0], actualFirstForward, TEST_EPS);
490 EuclideanTestUtils.assertCoordinatesEqual(expectedPts[len - 1], actualFirstReverse, TEST_EPS);
491 } else {
492 Assert.assertNull(actualFirstForward);
493 Assert.assertNull(actualFirstReverse);
494 }
495 }
496
497 private static void checkLinecast(final Circle c, final LineConvexSubset segment, final Vector2D... expectedPts) {
498
499 final List<LinecastPoint2D> results = c.linecast(segment);
500 Assert.assertEquals(expectedPts.length, results.size());
501
502 LinecastPoint2D actual;
503 Vector2D expected;
504 for (int i = 0; i < expectedPts.length; ++i) {
505 expected = expectedPts[i];
506 actual = results.get(i);
507
508 EuclideanTestUtils.assertCoordinatesEqual(expected, actual.getPoint(), TEST_EPS);
509 EuclideanTestUtils.assertCoordinatesEqual(c.getCenter().directionTo(expected), actual.getNormal(), TEST_EPS);
510 Assert.assertSame(segment.getLine(), actual.getLine());
511 }
512
513
514 final LinecastPoint2D firstResult = c.linecastFirst(segment);
515 if (expectedPts.length > 0) {
516 Assert.assertEquals(results.get(0), firstResult);
517 } else {
518 Assert.assertNull(firstResult);
519 }
520 }
521
522
523
524
525 private static void checkBasicApproximationProperties(final Circle c, final RegionBSPTree2D tree) {
526 Assert.assertFalse(tree.isFull());
527 Assert.assertFalse(tree.isEmpty());
528
529
530 final List<LinePath> paths = tree.getBoundaryPaths();
531 Assert.assertEquals(1, paths.size());
532
533 final LinePath path = paths.get(0);
534 Assert.assertTrue(path.isFinite());
535
536 for (final Vector2D vertex : path.getVertexSequence()) {
537 Assert.assertTrue("Expected vertex to be contained in circle: " + vertex, c.contains(vertex));
538 }
539
540
541 EuclideanTestUtils.assertRegionLocation(c, RegionLocation.INSIDE, tree.getCentroid());
542
543
544 Assert.assertTrue("Expected approximation area to be less than circle", tree.getSize() < c.getSize());
545 }
546
547 private static void assertFiniteSegment(final LineConvexSubset segment, final Vector2D start, final Vector2D end) {
548 Assert.assertFalse(segment.isInfinite());
549 Assert.assertTrue(segment.isFinite());
550
551 EuclideanTestUtils.assertCoordinatesEqual(start, segment.getStartPoint(), TEST_EPS);
552 EuclideanTestUtils.assertCoordinatesEqual(end, segment.getEndPoint(), TEST_EPS);
553 }
554 }