1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.geometry.euclidean.twod;
18
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.Collections;
22 import java.util.function.BiFunction;
23 import java.util.function.ToDoubleFunction;
24 import java.util.regex.Pattern;
25
26 import org.apache.commons.geometry.core.GeometryTestUtils;
27 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
28 import org.apache.commons.geometry.euclidean.twod.shape.Parallelogram;
29 import org.apache.commons.numbers.core.Precision;
30 import org.junit.jupiter.api.Assertions;
31 import org.junit.jupiter.api.Test;
32
33 class Bounds2DTest {
34
35 private static final double TEST_EPS = 1e-10;
36
37 private static final Precision.DoubleEquivalence TEST_PRECISION =
38 Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
39
40 private static final String NO_POINTS_MESSAGE = "Cannot construct bounds: no points given";
41
42 private static final Pattern INVALID_BOUNDS_PATTERN =
43 Pattern.compile("^Invalid bounds: min= \\([^\\)]+\\), max= \\([^\\)]+\\)");
44
45 @Test
46 void testFrom_varargs_singlePoint() {
47
48 final Vector2D p1 = Vector2D.of(-1, 2);
49
50
51 final Bounds2D b = Bounds2D.from(p1);
52
53
54 EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMin(), TEST_EPS);
55 EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMax(), TEST_EPS);
56 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, b.getDiagonal(), TEST_EPS);
57 EuclideanTestUtils.assertCoordinatesEqual(p1, b.getCentroid(), TEST_EPS);
58 }
59
60 @Test
61 void testFrom_varargs_multiplePoints() {
62
63 final Vector2D p1 = Vector2D.of(1, 6);
64 final Vector2D p2 = Vector2D.of(0, 5);
65 final Vector2D p3 = Vector2D.of(3, 6);
66
67
68 final Bounds2D b = Bounds2D.from(p1, p2, p3);
69
70
71 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 5), b.getMin(), TEST_EPS);
72 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 6), b.getMax(), TEST_EPS);
73 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 1), b.getDiagonal(), TEST_EPS);
74 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 5.5), b.getCentroid(), TEST_EPS);
75 }
76
77 @Test
78 void testFrom_iterable_singlePoint() {
79
80 final Vector2D p1 = Vector2D.of(-1, 2);
81
82
83 final Bounds2D b = Bounds2D.from(Collections.singletonList(p1));
84
85
86 EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMin(), TEST_EPS);
87 EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMax(), TEST_EPS);
88 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, b.getDiagonal(), TEST_EPS);
89 EuclideanTestUtils.assertCoordinatesEqual(p1, b.getCentroid(), TEST_EPS);
90 }
91
92 @Test
93 void testFrom_iterable_multiplePoints() {
94
95 final Vector2D p1 = Vector2D.of(1, 6);
96 final Vector2D p2 = Vector2D.of(2, 5);
97 final Vector2D p3 = Vector2D.of(3, 4);
98
99
100 final Bounds2D b = Bounds2D.from(Arrays.asList(p1, p2, p3));
101
102
103 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 4), b.getMin(), TEST_EPS);
104 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 6), b.getMax(), TEST_EPS);
105 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 2), b.getDiagonal(), TEST_EPS);
106 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 5), b.getCentroid(), TEST_EPS);
107 }
108
109 @Test
110 void testFrom_iterable_noPoints() {
111
112 GeometryTestUtils.assertThrowsWithMessage(() -> {
113 Bounds2D.from(new ArrayList<>());
114 }, IllegalStateException.class, NO_POINTS_MESSAGE);
115 }
116
117 @Test
118 void testFrom_invalidBounds() {
119
120 final Vector2D good = Vector2D.of(1, 1);
121
122 final Vector2D nan = Vector2D.of(Double.NaN, 1);
123 final Vector2D posInf = Vector2D.of(1, Double.POSITIVE_INFINITY);
124 final Vector2D negInf = Vector2D.of(1, Double.NEGATIVE_INFINITY);
125
126
127 GeometryTestUtils.assertThrowsWithMessage(() -> {
128 Bounds2D.from(Vector2D.NaN);
129 }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
130
131 GeometryTestUtils.assertThrowsWithMessage(() -> {
132 Bounds2D.from(Vector2D.POSITIVE_INFINITY);
133 }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
134
135 GeometryTestUtils.assertThrowsWithMessage(() -> {
136 Bounds2D.from(Vector2D.NEGATIVE_INFINITY);
137 }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
138
139 GeometryTestUtils.assertThrowsWithMessage(() -> {
140 Bounds2D.from(good, nan);
141 }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
142
143 GeometryTestUtils.assertThrowsWithMessage(() -> {
144 Bounds2D.from(posInf, good);
145 }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
146
147 GeometryTestUtils.assertThrowsWithMessage(() -> {
148 Bounds2D.from(good, negInf, good);
149 }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
150 }
151
152 @Test
153 void testHasSize() {
154
155 final Precision.DoubleEquivalence low = Precision.doubleEquivalenceOfEpsilon(1e-2);
156 final Precision.DoubleEquivalence high = Precision.doubleEquivalenceOfEpsilon(1e-10);
157
158 final Vector2D p1 = Vector2D.ZERO;
159
160 final Vector2D p2 = Vector2D.of(1e-5, 1);
161 final Vector2D p3 = Vector2D.of(1, 1e-5);
162
163 final Vector2D p4 = Vector2D.of(1, 1);
164
165
166 Assertions.assertFalse(Bounds2D.from(p1).hasSize(high));
167 Assertions.assertFalse(Bounds2D.from(p1).hasSize(low));
168
169 Assertions.assertTrue(Bounds2D.from(p1, p2).hasSize(high));
170 Assertions.assertFalse(Bounds2D.from(p1, p2).hasSize(low));
171
172 Assertions.assertTrue(Bounds2D.from(p1, p3).hasSize(high));
173 Assertions.assertFalse(Bounds2D.from(p1, p3).hasSize(low));
174
175 Assertions.assertTrue(Bounds2D.from(p1, p4).hasSize(high));
176 Assertions.assertTrue(Bounds2D.from(p1, p4).hasSize(low));
177 }
178
179 @Test
180 void testContains_strict() {
181
182 final Bounds2D b = Bounds2D.from(
183 Vector2D.of(0, 4),
184 Vector2D.of(2, 6));
185
186
187 assertContainsStrict(b, true,
188 b.getCentroid(),
189 Vector2D.of(0, 4), Vector2D.of(2, 6),
190 Vector2D.of(1, 5),
191 Vector2D.of(0, 5), Vector2D.of(2, 5),
192 Vector2D.of(1, 4), Vector2D.of(1, 6));
193
194 assertContainsStrict(b, false,
195 Vector2D.ZERO,
196 Vector2D.of(-1, 5), Vector2D.of(3, 5),
197 Vector2D.of(1, 3), Vector2D.of(1, 7),
198 Vector2D.of(-1e-15, 4), Vector2D.of(2, 6 + 1e-15));
199 }
200
201 @Test
202 void testContains_precision() {
203
204 final Bounds2D b = Bounds2D.from(
205 Vector2D.of(0, 4),
206 Vector2D.of(2, 6));
207
208
209 assertContainsWithPrecision(b, true,
210 b.getCentroid(),
211 Vector2D.of(1, 5), Vector2D.of(0, 4), Vector2D.of(2, 6),
212 Vector2D.of(0, 5), Vector2D.of(2, 5),
213 Vector2D.of(1, 4), Vector2D.of(1, 6),
214 Vector2D.of(-1e-15, 4), Vector2D.of(2, 6 + 1e-15));
215
216 assertContainsWithPrecision(b, false,
217 Vector2D.ZERO,
218 Vector2D.of(-1, 5), Vector2D.of(3, 5),
219 Vector2D.of(1, 3), Vector2D.of(1, 7));
220 }
221
222 @Test
223 void testIntersects() {
224
225 final Bounds2D b = Bounds2D.from(Vector2D.ZERO, Vector2D.of(1, 1));
226
227
228 checkIntersects(b, Vector2D::getX, (v, x) -> Vector2D.of(x, v.getY()));
229 checkIntersects(b, Vector2D::getY, (v, y) -> Vector2D.of(v.getX(), y));
230 }
231
232 private void checkIntersects(final Bounds2D b, final ToDoubleFunction<? super Vector2D> getter,
233 final BiFunction<? super Vector2D, Double, ? extends Vector2D> setter) {
234
235 final Vector2D min = b.getMin();
236 final Vector2D max = b.getMax();
237
238 final double minValue = getter.applyAsDouble(min);
239 final double maxValue = getter.applyAsDouble(max);
240 final double midValue = (0.5 * (maxValue - minValue)) + minValue;
241
242
243
244
245 Assertions.assertFalse(b.intersects(Bounds2D.from(
246 setter.apply(min, minValue - 2), setter.apply(max, minValue - 1))));
247
248 Assertions.assertTrue(b.intersects(Bounds2D.from(
249 setter.apply(min, minValue - 2), setter.apply(max, minValue))));
250 Assertions.assertTrue(b.intersects(Bounds2D.from(
251 setter.apply(min, minValue - 2), setter.apply(max, midValue))));
252 Assertions.assertTrue(b.intersects(Bounds2D.from(
253 setter.apply(min, minValue - 2), setter.apply(max, maxValue))));
254 Assertions.assertTrue(b.intersects(Bounds2D.from(
255 setter.apply(min, minValue - 2), setter.apply(max, maxValue + 1))));
256
257
258 Assertions.assertTrue(b.intersects(Bounds2D.from(
259 setter.apply(min, minValue), setter.apply(max, minValue))));
260 Assertions.assertTrue(b.intersects(Bounds2D.from(
261 setter.apply(min, minValue), setter.apply(max, midValue))));
262 Assertions.assertTrue(b.intersects(Bounds2D.from(
263 setter.apply(min, minValue), setter.apply(max, maxValue))));
264 Assertions.assertTrue(b.intersects(Bounds2D.from(
265 setter.apply(min, minValue), setter.apply(max, maxValue + 1))));
266
267
268 Assertions.assertTrue(b.intersects(Bounds2D.from(
269 setter.apply(min, midValue), setter.apply(max, midValue))));
270 Assertions.assertTrue(b.intersects(Bounds2D.from(
271 setter.apply(min, midValue), setter.apply(max, maxValue))));
272 Assertions.assertTrue(b.intersects(Bounds2D.from(
273 setter.apply(min, midValue), setter.apply(max, maxValue + 1))));
274
275
276 Assertions.assertTrue(b.intersects(Bounds2D.from(
277 setter.apply(min, maxValue), setter.apply(max, maxValue))));
278 Assertions.assertTrue(b.intersects(Bounds2D.from(
279 setter.apply(min, maxValue), setter.apply(max, maxValue + 1))));
280
281
282 Assertions.assertFalse(b.intersects(Bounds2D.from(
283 setter.apply(min, maxValue + 1), setter.apply(max, maxValue + 2))));
284 }
285
286 @Test
287 void testIntersection() {
288
289 final Bounds2D b = Bounds2D.from(Vector2D.ZERO, Vector2D.of(1, 1));
290
291
292
293
294 Assertions.assertNull(b.intersection(Bounds2D.from(Vector2D.of(-2, 0), Vector2D.of(-1, 1))));
295 checkIntersection(b, Vector2D.of(-1, 0), Vector2D.of(0, 1),
296 Vector2D.of(0, 0), Vector2D.of(0, 1));
297 checkIntersection(b, Vector2D.of(-1, 0), Vector2D.of(0.5, 1),
298 Vector2D.of(0, 0), Vector2D.of(0.5, 1));
299 checkIntersection(b, Vector2D.of(-1, 0), Vector2D.of(1, 1),
300 Vector2D.of(0, 0), Vector2D.of(1, 1));
301 checkIntersection(b, Vector2D.of(-1, 0), Vector2D.of(2, 1),
302 Vector2D.of(0, 0), Vector2D.of(1, 1));
303 checkIntersection(b, Vector2D.of(0, 0), Vector2D.of(2, 1),
304 Vector2D.of(0, 0), Vector2D.of(1, 1));
305 checkIntersection(b, Vector2D.of(0.5, 0), Vector2D.of(2, 1),
306 Vector2D.of(0.5, 0), Vector2D.of(1, 1));
307 checkIntersection(b, Vector2D.of(1, 0), Vector2D.of(2, 1),
308 Vector2D.of(1, 0), Vector2D.of(1, 1));
309 Assertions.assertNull(b.intersection(Bounds2D.from(Vector2D.of(2, 0), Vector2D.of(3, 1))));
310
311
312 Assertions.assertNull(b.intersection(Bounds2D.from(Vector2D.of(0, -2), Vector2D.of(1, -1))));
313 checkIntersection(b, Vector2D.of(0, -1), Vector2D.of(1, 0),
314 Vector2D.of(0, 0), Vector2D.of(1, 0));
315 checkIntersection(b, Vector2D.of(0, -1), Vector2D.of(1, 0.5),
316 Vector2D.of(0, 0), Vector2D.of(1, 0.5));
317 checkIntersection(b, Vector2D.of(0, -1), Vector2D.of(1, 1),
318 Vector2D.of(0, 0), Vector2D.of(1, 1));
319 checkIntersection(b, Vector2D.of(0, -1), Vector2D.of(1, 2),
320 Vector2D.of(0, 0), Vector2D.of(1, 1));
321 checkIntersection(b, Vector2D.of(0, 0), Vector2D.of(1, 2),
322 Vector2D.of(0, 0), Vector2D.of(1, 1));
323 checkIntersection(b, Vector2D.of(0, 0.5), Vector2D.of(1, 2),
324 Vector2D.of(0, 0.5), Vector2D.of(1, 1));
325 checkIntersection(b, Vector2D.of(0, 1), Vector2D.of(1, 2),
326 Vector2D.of(0, 1), Vector2D.of(1, 1));
327 Assertions.assertNull(b.intersection(Bounds2D.from(Vector2D.of(0, 2), Vector2D.of(1, 3))));
328 }
329
330 private void checkIntersection(final Bounds2D b, final Vector2D a1, final Vector2D a2, final Vector2D r1, final Vector2D r2) {
331 final Bounds2D a = Bounds2D.from(a1, a2);
332 final Bounds2D result = b.intersection(a);
333
334 checkBounds(result, r1, r2);
335 }
336
337 @Test
338 void toRegion() {
339
340 final Bounds2D b = Bounds2D.from(
341 Vector2D.of(0, 4),
342 Vector2D.of(2, 6));
343
344
345 final Parallelogram p = b.toRegion(TEST_PRECISION);
346
347
348 Assertions.assertEquals(4, p.getSize(), TEST_EPS);
349 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 5), p.getCentroid(), TEST_EPS);
350 }
351
352 @Test
353 void toRegion_boundingBoxTooSmall() {
354
355 Assertions.assertThrows(IllegalArgumentException.class, () -> Bounds2D.from(Vector2D.ZERO, Vector2D.of(1e-12, 1e-12)).toRegion(TEST_PRECISION));
356 }
357
358 @Test
359 void testEq() {
360
361 final Precision.DoubleEquivalence low = Precision.doubleEquivalenceOfEpsilon(1e-2);
362 final Precision.DoubleEquivalence high = Precision.doubleEquivalenceOfEpsilon(1e-10);
363
364 final Bounds2D b1 = Bounds2D.from(Vector2D.of(1, 1), Vector2D.of(2, 2));
365
366 final Bounds2D b2 = Bounds2D.from(Vector2D.of(1.1, 1), Vector2D.of(2, 2));
367 final Bounds2D b3 = Bounds2D.from(Vector2D.of(1, 1), Vector2D.of(1.9, 2));
368
369 final Bounds2D b4 = Bounds2D.from(Vector2D.of(1.001, 1.001), Vector2D.of(2.001, 2.001));
370
371
372 Assertions.assertTrue(b1.eq(b1, low));
373
374 Assertions.assertFalse(b1.eq(b2, low));
375 Assertions.assertFalse(b1.eq(b3, low));
376
377 Assertions.assertTrue(b1.eq(b4, low));
378 Assertions.assertTrue(b4.eq(b1, low));
379
380 Assertions.assertFalse(b1.eq(b4, high));
381 Assertions.assertFalse(b4.eq(b1, high));
382 }
383
384 @Test
385 void testHashCode() {
386
387 final Bounds2D b1 = Bounds2D.from(Vector2D.of(1, 1), Vector2D.of(2, 2));
388
389 final Bounds2D b2 = Bounds2D.from(Vector2D.of(-2, 1), Vector2D.of(2, 2));
390 final Bounds2D b3 = Bounds2D.from(Vector2D.of(1, 1), Vector2D.of(3, 2));
391 final Bounds2D b4 = Bounds2D.from(Vector2D.of(1 + 1e-15, 1), Vector2D.of(2, 2));
392 final Bounds2D b5 = Bounds2D.from(Vector2D.of(1, 1), Vector2D.of(2 + 1e-15, 2));
393
394 final Bounds2D b6 = Bounds2D.from(Vector2D.of(1, 1), Vector2D.of(2, 2));
395
396
397 final int hash = b1.hashCode();
398
399
400 Assertions.assertEquals(hash, b1.hashCode());
401
402 Assertions.assertNotEquals(hash, b2.hashCode());
403 Assertions.assertNotEquals(hash, b3.hashCode());
404 Assertions.assertNotEquals(hash, b4.hashCode());
405 Assertions.assertNotEquals(hash, b5.hashCode());
406
407 Assertions.assertEquals(hash, b6.hashCode());
408 }
409
410 @Test
411 void testEquals() {
412
413 final Bounds2D b1 = Bounds2D.from(Vector2D.of(1, 1), Vector2D.of(2, 2));
414
415 final Bounds2D b2 = Bounds2D.from(Vector2D.of(-1, 1), Vector2D.of(2, 2));
416 final Bounds2D b3 = Bounds2D.from(Vector2D.of(1, 1), Vector2D.of(3, 2));
417 final Bounds2D b4 = Bounds2D.from(Vector2D.of(1 + 1e-15, 1), Vector2D.of(2, 2));
418 final Bounds2D b5 = Bounds2D.from(Vector2D.of(1, 1), Vector2D.of(2 + 1e-15, 2));
419
420 final Bounds2D b6 = Bounds2D.from(Vector2D.of(1, 1), Vector2D.of(2, 2));
421
422
423 GeometryTestUtils.assertSimpleEqualsCases(b1);
424
425 Assertions.assertNotEquals(b1, b2);
426 Assertions.assertNotEquals(b1, b3);
427 Assertions.assertNotEquals(b1, b4);
428 Assertions.assertNotEquals(b1, b5);
429
430 Assertions.assertEquals(b1, b6);
431 }
432
433 @Test
434 void testToString() {
435
436 final Bounds2D b = Bounds2D.from(Vector2D.of(1, 1), Vector2D.of(2, 2));
437
438
439 final String str = b.toString();
440
441
442 GeometryTestUtils.assertContains("Bounds2D[min= (1", str);
443 GeometryTestUtils.assertContains(", max= (2", str);
444 }
445
446 @Test
447 void testBuilder_addMethods() {
448
449 final Vector2D p1 = Vector2D.of(1, 10);
450 final Vector2D p2 = Vector2D.of(2, 9);
451 final Vector2D p3 = Vector2D.of(3, 8);
452 final Vector2D p4 = Vector2D.of(4, 7);
453 final Vector2D p5 = Vector2D.of(5, 6);
454
455
456 final Bounds2D b = Bounds2D.builder()
457 .add(p1)
458 .addAll(Arrays.asList(p2, p3))
459 .add(Bounds2D.from(p4, p5))
460 .build();
461
462
463 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 6), b.getMin(), TEST_EPS);
464 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(5, 10), b.getMax(), TEST_EPS);
465 EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 8), b.getCentroid(), TEST_EPS);
466 }
467
468 @Test
469 void testBuilder_hasBounds() {
470
471 Assertions.assertFalse(Bounds2D.builder().hasBounds());
472
473 Assertions.assertFalse(Bounds2D.builder().add(Vector2D.of(Double.NaN, 1)).hasBounds());
474 Assertions.assertFalse(Bounds2D.builder().add(Vector2D.of(1, Double.NaN)).hasBounds());
475
476 Assertions.assertFalse(Bounds2D.builder().add(Vector2D.of(Double.POSITIVE_INFINITY, 1)).hasBounds());
477 Assertions.assertFalse(Bounds2D.builder().add(Vector2D.of(1, Double.POSITIVE_INFINITY)).hasBounds());
478
479 Assertions.assertFalse(Bounds2D.builder().add(Vector2D.of(Double.NEGATIVE_INFINITY, 1)).hasBounds());
480 Assertions.assertFalse(Bounds2D.builder().add(Vector2D.of(1, Double.NEGATIVE_INFINITY)).hasBounds());
481
482 Assertions.assertTrue(Bounds2D.builder().add(Vector2D.ZERO).hasBounds());
483 }
484
485 private static void checkBounds(final Bounds2D b, final Vector2D min, final Vector2D max) {
486 EuclideanTestUtils.assertCoordinatesEqual(min, b.getMin(), TEST_EPS);
487 EuclideanTestUtils.assertCoordinatesEqual(max, b.getMax(), TEST_EPS);
488 }
489
490 private static void assertContainsStrict(final Bounds2D bounds, final boolean contains, final Vector2D... pts) {
491 for (final Vector2D pt : pts) {
492 Assertions.assertEquals(contains, bounds.contains(pt), "Unexpected location for point " + pt);
493 }
494 }
495
496 private static void assertContainsWithPrecision(final Bounds2D bounds, final boolean contains, final Vector2D... pts) {
497 for (final Vector2D pt : pts) {
498 Assertions.assertEquals(contains, bounds.contains(pt, TEST_PRECISION), "Unexpected location for point " + pt);
499 }
500 }
501 }