View Javadoc
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.List;
20  
21  import org.apache.commons.geometry.core.Region;
22  import org.apache.commons.geometry.core.RegionLocation;
23  import org.apache.commons.geometry.core.partitioning.Split;
24  import org.apache.commons.geometry.core.partitioning.SplitLocation;
25  import org.apache.commons.numbers.angle.Angle;
26  import org.apache.commons.numbers.core.Precision;
27  import org.junit.jupiter.api.Assertions;
28  import org.junit.jupiter.api.Test;
29  
30  class AngularIntervalTest {
31  
32      private static final double TEST_EPS = 1e-10;
33  
34      private static final Precision.DoubleEquivalence TEST_PRECISION =
35              Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
36  
37      @Test
38      void testOf_doubles() {
39          // act/assert
40          checkInterval(AngularInterval.of(0, 1, TEST_PRECISION), 0, 1);
41          checkInterval(AngularInterval.of(1, 0, TEST_PRECISION), 1, Angle.TWO_PI);
42          checkInterval(AngularInterval.of(-2, -1.5, TEST_PRECISION), -2, -1.5);
43          checkInterval(AngularInterval.of(-2, -2.5, TEST_PRECISION), -2, Angle.TWO_PI - 2.5);
44  
45          checkFull(AngularInterval.of(1, 1, TEST_PRECISION));
46          checkFull(AngularInterval.of(0, 1e-11, TEST_PRECISION));
47          checkFull(AngularInterval.of(0, -1e-11, TEST_PRECISION));
48          checkFull(AngularInterval.of(0, Angle.TWO_PI, TEST_PRECISION));
49      }
50  
51      @Test
52      void testOf_doubles_invalidArgs() {
53          // act/assert
54          Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(Double.NEGATIVE_INFINITY, 0, TEST_PRECISION));
55          Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(0, Double.POSITIVE_INFINITY, TEST_PRECISION));
56          Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, TEST_PRECISION));
57          Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(Double.NaN, 0, TEST_PRECISION));
58          Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(0, Double.NaN, TEST_PRECISION));
59          Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(Double.NaN, Double.NaN, TEST_PRECISION));
60      }
61  
62      @Test
63      void testOf_points() {
64          // act/assert
65          checkInterval(AngularInterval.of(Point1S.of(0), Point1S.of(1), TEST_PRECISION), 0, 1);
66          checkInterval(AngularInterval.of(Point1S.of(1), Point1S.of(0), TEST_PRECISION), 1, Angle.TWO_PI);
67          checkInterval(AngularInterval.of(Point1S.of(-2), Point1S.of(-1.5), TEST_PRECISION), -2, -1.5);
68          checkInterval(AngularInterval.of(Point1S.of(-2), Point1S.of(-2.5), TEST_PRECISION), -2, Angle.TWO_PI - 2.5);
69  
70          checkFull(AngularInterval.of(Point1S.of(1), Point1S.of(1), TEST_PRECISION));
71          checkFull(AngularInterval.of(Point1S.of(0), Point1S.of(1e-11), TEST_PRECISION));
72          checkFull(AngularInterval.of(Point1S.of(0), Point1S.of(-1e-11), TEST_PRECISION));
73      }
74  
75      @Test
76      void testOf_points_invalidArgs() {
77          // act/assert
78          Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(Point1S.of(Double.NEGATIVE_INFINITY), Point1S.ZERO, TEST_PRECISION));
79          Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(Point1S.ZERO, Point1S.of(Double.POSITIVE_INFINITY), TEST_PRECISION));
80          Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(Point1S.of(Double.POSITIVE_INFINITY), Point1S.of(Double.NEGATIVE_INFINITY), TEST_PRECISION));
81          Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(Point1S.NaN, Point1S.ZERO, TEST_PRECISION));
82          Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(Point1S.ZERO, Point1S.NaN, TEST_PRECISION));
83          Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(Point1S.NaN, Point1S.NaN, TEST_PRECISION));
84      }
85  
86      @Test
87      void testOf_orientedPoints() {
88          // arrange
89          final Precision.DoubleEquivalence precisionA = Precision.doubleEquivalenceOfEpsilon(1e-3);
90          final Precision.DoubleEquivalence precisionB = Precision.doubleEquivalenceOfEpsilon(1e-2);
91  
92          final CutAngle zeroPos = CutAngles.createPositiveFacing(Point1S.ZERO, precisionA);
93          final CutAngle zeroNeg = CutAngles.createNegativeFacing(Point1S.ZERO, precisionA);
94  
95          final CutAngle piPos = CutAngles.createPositiveFacing(Point1S.PI, precisionA);
96          final CutAngle piNeg = CutAngles.createNegativeFacing(Point1S.PI, precisionA);
97  
98          final CutAngle almostPiPos = CutAngles.createPositiveFacing(Point1S.of(Math.PI + 5e-3), precisionB);
99  
100         // act/assert
101         checkInterval(AngularInterval.of(zeroNeg, piPos), 0, Math.PI);
102         checkInterval(AngularInterval.of(zeroPos, piNeg), Math.PI, Angle.TWO_PI);
103 
104         checkFull(AngularInterval.of(zeroPos, zeroNeg));
105         checkFull(AngularInterval.of(zeroPos, piPos));
106         checkFull(AngularInterval.of(piNeg, zeroNeg));
107 
108         checkFull(AngularInterval.of(almostPiPos, piNeg));
109         checkFull(AngularInterval.of(piNeg, almostPiPos));
110     }
111 
112     @Test
113     void testOf_orientedPoints_invalidArgs() {
114         // arrange
115         final CutAngle pt = CutAngles.createNegativeFacing(Point1S.ZERO, TEST_PRECISION);
116         final CutAngle nan = CutAngles.createPositiveFacing(Point1S.NaN, TEST_PRECISION);
117 
118         // act/assert
119         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(pt, nan));
120         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(nan, pt));
121         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.of(nan, nan));
122     }
123 
124     @Test
125     void testFull() {
126         // act
127         final AngularInterval.Convex interval = AngularInterval.full();
128 
129         // assert
130         checkFull(interval);
131     }
132 
133     @Test
134     void testClassify_full() {
135         // arrange
136         final AngularInterval interval = AngularInterval.full();
137 
138         // act/assert
139         for (double a = -2 * Math.PI; a >= 4 * Math.PI; a += 0.5) {
140             checkClassify(interval, RegionLocation.INSIDE, Point1S.of(a));
141         }
142     }
143 
144     @Test
145     void testClassify_almostFull() {
146         // arrange
147         final AngularInterval interval = AngularInterval.of(1 + 2e-10, 1, TEST_PRECISION);
148 
149         // act/assert
150         checkClassify(interval, RegionLocation.BOUNDARY,
151                 Point1S.of(1 + 2e-10), Point1S.of(1 + 6e-11), Point1S.of(1));
152 
153         checkClassify(interval, RegionLocation.INSIDE, Point1S.of(1 + 6e-11 + Math.PI));
154 
155         for (double a = 1 + 1e-9; a >= 1 - 1e-9 + Angle.TWO_PI; a += 0.5) {
156             checkClassify(interval, RegionLocation.INSIDE, Point1S.of(a));
157         }
158     }
159 
160     @Test
161     void testClassify_sizeableGap() {
162         // arrange
163         final AngularInterval interval = AngularInterval.of(0.25, -0.25, TEST_PRECISION);
164 
165         // act/assert
166         checkClassify(interval, RegionLocation.OUTSIDE,
167                 Point1S.ZERO, Point1S.of(-0.2), Point1S.of(0.2));
168         checkClassify(interval, RegionLocation.BOUNDARY,
169                 Point1S.of(-0.25), Point1S.of(0.2499999999999));
170         checkClassify(interval, RegionLocation.INSIDE,
171                 Point1S.of(1), Point1S.PI, Point1S.of(-1));
172     }
173 
174     @Test
175     void testClassify_halfPi() {
176         // arrange
177         final AngularInterval interval = AngularInterval.of(Angle.PI_OVER_TWO, -Angle.PI_OVER_TWO, TEST_PRECISION);
178 
179         // act/assert
180         checkClassify(interval, RegionLocation.OUTSIDE,
181                 Point1S.ZERO, Point1S.of(Angle.PI_OVER_TWO - 0.1), Point1S.of(-Angle.PI_OVER_TWO + 0.1));
182         checkClassify(interval, RegionLocation.BOUNDARY,
183                 Point1S.of(Angle.PI_OVER_TWO), Point1S.of(1.5 * Math.PI));
184         checkClassify(interval, RegionLocation.INSIDE,
185                 Point1S.PI, Point1S.of(Angle.PI_OVER_TWO + 0.1), Point1S.of(-Angle.PI_OVER_TWO - 0.1));
186     }
187 
188     @Test
189     void testClassify_almostEmpty() {
190         // arrange
191         final AngularInterval interval = AngularInterval.of(1, 1 + 2e-10, TEST_PRECISION);
192 
193         // act/assert
194         checkClassify(interval, RegionLocation.BOUNDARY,
195                 Point1S.of(1 + 2e-10), Point1S.of(1 + 6e-11), Point1S.of(1));
196 
197         checkClassify(interval, RegionLocation.OUTSIDE, Point1S.of(1 + 6e-11 + Math.PI));
198 
199         for (double a = 1 + 1e-9; a >= 1 - 1e-9 + Angle.TWO_PI; a += 0.5) {
200             checkClassify(interval, RegionLocation.OUTSIDE, Point1S.of(a));
201         }
202     }
203 
204     @Test
205     void testProject_full() {
206         // arrange
207         final AngularInterval interval = AngularInterval.full();
208 
209         // act/assert
210         Assertions.assertNull(interval.project(Point1S.ZERO));
211         Assertions.assertNull(interval.project(Point1S.PI));
212     }
213 
214     @Test
215     void testProject() {
216         // arrange
217         final AngularInterval interval = AngularInterval.of(1, 2, TEST_PRECISION);
218 
219         // act/assert
220         Assertions.assertEquals(1, interval.project(Point1S.ZERO).getAzimuth(), TEST_EPS);
221         Assertions.assertEquals(1, interval.project(Point1S.of(1)).getAzimuth(), TEST_EPS);
222         Assertions.assertEquals(1, interval.project(Point1S.of(1.5)).getAzimuth(), TEST_EPS);
223 
224         Assertions.assertEquals(2, interval.project(Point1S.of(2)).getAzimuth(), TEST_EPS);
225         Assertions.assertEquals(2, interval.project(Point1S.PI).getAzimuth(), TEST_EPS);
226         Assertions.assertEquals(2, interval.project(Point1S.of(1.4 + Math.PI)).getAzimuth(), TEST_EPS);
227 
228         Assertions.assertEquals(1, interval.project(Point1S.of(1.5 + Math.PI)).getAzimuth(), TEST_EPS);
229         Assertions.assertEquals(1, interval.project(Point1S.of(1.6 + Math.PI)).getAzimuth(), TEST_EPS);
230     }
231 
232     @Test
233     void testTransform_full() {
234         // arrange
235         final AngularInterval interval = AngularInterval.full();
236 
237         final Transform1S rotate = Transform1S.createRotation(Angle.PI_OVER_TWO);
238         final Transform1S invert = Transform1S.createNegation().rotate(Angle.PI_OVER_TWO);
239 
240         // act/assert
241         checkFull(interval.transform(rotate));
242         checkFull(interval.transform(invert));
243     }
244 
245     @Test
246     void testTransform() {
247         // arrange
248         final AngularInterval interval = AngularInterval.of(Angle.PI_OVER_TWO, Math.PI, TEST_PRECISION);
249 
250         final Transform1S rotate = Transform1S.createRotation(Angle.PI_OVER_TWO);
251         final Transform1S invert = Transform1S.createNegation().rotate(Angle.PI_OVER_TWO);
252 
253         // act/assert
254         checkInterval(interval.transform(rotate), Math.PI, 1.5 * Math.PI);
255         checkInterval(interval.transform(invert), -0.5 * Math.PI, 0.0);
256     }
257 
258     @Test
259     void testWrapsZero() {
260         // act/assert
261         Assertions.assertFalse(AngularInterval.full().wrapsZero());
262         Assertions.assertFalse(AngularInterval.of(0, Angle.PI_OVER_TWO, TEST_PRECISION).wrapsZero());
263         Assertions.assertFalse(AngularInterval.of(Angle.PI_OVER_TWO, Math.PI, TEST_PRECISION).wrapsZero());
264         Assertions.assertFalse(AngularInterval.of(Math.PI, 1.5 * Math.PI, TEST_PRECISION).wrapsZero());
265         Assertions.assertFalse(AngularInterval.of(1.5 * Math.PI, Angle.TWO_PI - 1e-5, TEST_PRECISION).wrapsZero());
266 
267         Assertions.assertTrue(AngularInterval.of(1.5 * Math.PI, Angle.TWO_PI, TEST_PRECISION).wrapsZero());
268         Assertions.assertTrue(AngularInterval.of(1.5 * Math.PI, 2.5 * Math.PI, TEST_PRECISION).wrapsZero());
269         Assertions.assertTrue(AngularInterval.of(-2.5 * Math.PI, -1.5 * Math.PI, TEST_PRECISION).wrapsZero());
270     }
271 
272     @Test
273     void testToTree_full() {
274         // arrange
275         final AngularInterval interval = AngularInterval.full();
276 
277         // act
278         final RegionBSPTree1S tree = interval.toTree();
279 
280         // assert
281         Assertions.assertTrue(tree.isFull());
282         Assertions.assertFalse(tree.isEmpty());
283 
284         checkClassify(tree, RegionLocation.INSIDE,
285                 Point1S.ZERO, Point1S.of(Angle.PI_OVER_TWO),
286                 Point1S.PI, Point1S.of(-Angle.PI_OVER_TWO));
287     }
288 
289     @Test
290     void testToTree_intervalEqualToPi() {
291         // arrange
292         final AngularInterval interval = AngularInterval.of(0.0, Math.PI, TEST_PRECISION);
293 
294         // act
295         final RegionBSPTree1S tree = interval.toTree();
296 
297         // assert
298         Assertions.assertFalse(tree.isFull());
299         Assertions.assertFalse(tree.isEmpty());
300 
301         checkClassify(tree, RegionLocation.BOUNDARY,
302                 Point1S.ZERO, Point1S.PI);
303 
304         checkClassify(tree, RegionLocation.INSIDE,
305                 Point1S.of(1e-4), Point1S.of(0.25 * Math.PI),
306                 Point1S.of(-1.25 * Math.PI), Point1S.of(Math.PI - 1e-4));
307 
308         checkClassify(tree, RegionLocation.OUTSIDE,
309                 Point1S.of(-1e-4), Point1S.of(-0.25 * Math.PI),
310                 Point1S.of(1.25 * Math.PI), Point1S.of(-Math.PI + 1e-4));
311     }
312 
313     @Test
314     void testToTree_intervalLessThanPi() {
315         // arrange
316         final AngularInterval interval = AngularInterval.of(Angle.PI_OVER_TWO, Math.PI, TEST_PRECISION);
317 
318         // act
319         final RegionBSPTree1S tree = interval.toTree();
320 
321         // assert
322         Assertions.assertFalse(tree.isFull());
323         Assertions.assertFalse(tree.isEmpty());
324 
325         checkClassify(tree, RegionLocation.BOUNDARY,
326                 Point1S.of(Angle.PI_OVER_TWO), Point1S.PI);
327 
328         checkClassify(tree, RegionLocation.INSIDE,
329                 Point1S.of(0.51 * Math.PI), Point1S.of(0.75 * Math.PI),
330                 Point1S.of(0.99 * Math.PI));
331 
332         checkClassify(tree, RegionLocation.OUTSIDE,
333                 Point1S.ZERO, Point1S.of(0.25 * Math.PI),
334                 Point1S.of(1.25 * Math.PI), Point1S.of(1.75 * Math.PI));
335     }
336 
337     @Test
338     void testToTree_intervalGreaterThanPi() {
339         // arrange
340         final AngularInterval interval = AngularInterval.of(Math.PI, Angle.PI_OVER_TWO, TEST_PRECISION);
341 
342         // act
343         final RegionBSPTree1S tree = interval.toTree();
344 
345         // assert
346         Assertions.assertFalse(tree.isFull());
347         Assertions.assertFalse(tree.isEmpty());
348 
349         checkClassify(tree, RegionLocation.BOUNDARY,
350                 Point1S.of(Angle.PI_OVER_TWO), Point1S.PI);
351 
352         checkClassify(tree, RegionLocation.INSIDE,
353                 Point1S.ZERO, Point1S.of(0.25 * Math.PI),
354                 Point1S.of(1.25 * Math.PI), Point1S.of(1.75 * Math.PI));
355 
356         checkClassify(tree, RegionLocation.OUTSIDE,
357                 Point1S.of(0.51 * Math.PI), Point1S.of(0.75 * Math.PI),
358                 Point1S.of(0.99 * Math.PI));
359     }
360 
361     @Test
362     void testToConvex_lessThanPi() {
363         // arrange
364         final AngularInterval interval = AngularInterval.of(0, Angle.PI_OVER_TWO, TEST_PRECISION);
365 
366         //act
367         final List<AngularInterval.Convex> result = interval.toConvex();
368 
369         // assert
370         Assertions.assertEquals(1, result.size());
371         checkInterval(interval, 0, Angle.PI_OVER_TWO);
372     }
373 
374     @Test
375     void testToConvex_equalToPi() {
376         // arrange
377         final AngularInterval interval = AngularInterval.of(Math.PI, Angle.TWO_PI, TEST_PRECISION);
378 
379         //act
380         final List<AngularInterval.Convex> result = interval.toConvex();
381 
382         // assert
383         Assertions.assertEquals(1, result.size());
384         checkInterval(interval, Math.PI, Angle.TWO_PI);
385     }
386 
387     @Test
388     void testToConvex_overPi() {
389         // arrange
390         final AngularInterval interval = AngularInterval.of(Math.PI, Angle.PI_OVER_TWO, TEST_PRECISION);
391 
392         // act
393         final List<AngularInterval.Convex> result = interval.toConvex();
394 
395         // assert
396         Assertions.assertEquals(2, result.size());
397         checkInterval(result.get(0), Math.PI, 1.75 * Math.PI);
398         checkInterval(result.get(1), 1.75 * Math.PI, 2.5 * Math.PI);
399     }
400 
401     @Test
402     void testToConvex_overPi_splitAtZero() {
403         // arrange
404         final AngularInterval interval = AngularInterval.of(1.25 * Math.PI, 2.75 * Math.PI, TEST_PRECISION);
405 
406         // act
407         final List<AngularInterval.Convex> result = interval.toConvex();
408 
409         // assert
410         Assertions.assertEquals(2, result.size());
411         checkInterval(result.get(0), 1.25 * Math.PI, Angle.TWO_PI);
412         checkInterval(result.get(1), Angle.TWO_PI, 2.75 * Math.PI);
413     }
414 
415     @Test
416     void testSplit_full() {
417         // arrange
418         final AngularInterval interval = AngularInterval.full();
419         final CutAngle pt = CutAngles.createNegativeFacing(Angle.PI_OVER_TWO, TEST_PRECISION);
420 
421         // act
422         final Split<RegionBSPTree1S> split = interval.split(pt);
423 
424         // assert
425         Assertions.assertEquals(SplitLocation.BOTH, split.getLocation());
426 
427         final RegionBSPTree1S minus = split.getMinus();
428         checkClassify(minus, RegionLocation.BOUNDARY, Point1S.of(Angle.PI_OVER_TWO));
429         checkClassify(minus, RegionLocation.INSIDE,
430                 Point1S.PI, Point1S.of(-Angle.PI_OVER_TWO), Point1S.of(-0.25 * Math.PI));
431         checkClassify(minus, RegionLocation.OUTSIDE,
432                 Point1S.ZERO, Point1S.of(0.25 * Math.PI));
433 
434         final RegionBSPTree1S plus = split.getPlus();
435         checkClassify(plus, RegionLocation.BOUNDARY, Point1S.of(Angle.PI_OVER_TWO));
436         checkClassify(plus, RegionLocation.INSIDE,
437                 Point1S.ZERO, Point1S.of(0.25 * Math.PI));
438         checkClassify(plus, RegionLocation.OUTSIDE,
439                 Point1S.PI, Point1S.of(-Angle.PI_OVER_TWO), Point1S.of(-0.25 * Math.PI));
440     }
441 
442     @Test
443     void testSplit_interval_both() {
444         // arrange
445         final AngularInterval interval = AngularInterval.of(Angle.PI_OVER_TWO, Math.PI, TEST_PRECISION);
446         final CutAngle cut = CutAngles.createNegativeFacing(0.75 * Math.PI, TEST_PRECISION);
447 
448         // act
449         final Split<RegionBSPTree1S> split = interval.split(cut);
450 
451         // assert
452         Assertions.assertEquals(SplitLocation.BOTH, split.getLocation());
453 
454         final RegionBSPTree1S minus = split.getMinus();
455         checkClassify(minus, RegionLocation.BOUNDARY, Point1S.of(Math.PI), cut.getPoint());
456         checkClassify(minus, RegionLocation.INSIDE, Point1S.of(0.8 * Math.PI));
457         checkClassify(minus, RegionLocation.OUTSIDE,
458                 Point1S.ZERO, Point1S.of(Angle.TWO_PI), Point1S.of(-Angle.PI_OVER_TWO),
459                 Point1S.of(0.7 * Math.PI));
460 
461         final RegionBSPTree1S plus = split.getPlus();
462         checkClassify(plus, RegionLocation.BOUNDARY, Point1S.of(Angle.PI_OVER_TWO), cut.getPoint());
463         checkClassify(plus, RegionLocation.INSIDE, Point1S.of(0.6 * Math.PI));
464         checkClassify(plus, RegionLocation.OUTSIDE,
465                 Point1S.ZERO, Point1S.of(Angle.TWO_PI), Point1S.of(-Angle.PI_OVER_TWO),
466                 Point1S.of(0.8 * Math.PI));
467     }
468 
469     @Test
470     void testToString() {
471         // arrange
472         final AngularInterval interval = AngularInterval.of(1, 2, TEST_PRECISION);
473 
474         // act
475         final String str = interval.toString();
476 
477         // assert
478         Assertions.assertTrue(str.contains("AngularInterval"));
479         Assertions.assertTrue(str.contains("min= 1.0"));
480         Assertions.assertTrue(str.contains("max= 2.0"));
481     }
482 
483     @Test
484     void testConvex_of_doubles() {
485         // act/assert
486         checkInterval(AngularInterval.Convex.of(0, 1, TEST_PRECISION), 0, 1);
487         checkInterval(AngularInterval.Convex.of(0, Math.PI, TEST_PRECISION), 0, Math.PI);
488         checkInterval(AngularInterval.Convex.of(Math.PI + 2, 1, TEST_PRECISION), Math.PI + 2, Angle.TWO_PI + 1);
489         checkInterval(AngularInterval.Convex.of(-2, -1.5, TEST_PRECISION), -2, -1.5);
490 
491         checkFull(AngularInterval.Convex.of(1, 1, TEST_PRECISION));
492         checkFull(AngularInterval.Convex.of(0, 1e-11, TEST_PRECISION));
493         checkFull(AngularInterval.Convex.of(0, -1e-11, TEST_PRECISION));
494         checkFull(AngularInterval.Convex.of(0, Angle.TWO_PI, TEST_PRECISION));
495     }
496 
497     @Test
498     void testConvex_of_doubles_invalidArgs() {
499         // act/assert
500         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.Convex.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_PRECISION));
501         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.Convex.of(0, Math.PI + 1e-1, TEST_PRECISION));
502         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.Convex.of(Angle.PI_OVER_TWO, -Angle.PI_OVER_TWO + 1, TEST_PRECISION));
503         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.Convex.of(0, -0.5, TEST_PRECISION));
504     }
505 
506     @Test
507     void testConvex_of_points() {
508         // act/assert
509         checkInterval(AngularInterval.Convex.of(Point1S.of(0), Point1S.of(1), TEST_PRECISION), 0, 1);
510         checkInterval(AngularInterval.Convex.of(Point1S.of(0), Point1S.of(Math.PI), TEST_PRECISION),
511                 0, Math.PI);
512         checkInterval(AngularInterval.Convex.of(Point1S.of(Math.PI + 2), Point1S.of(1), TEST_PRECISION),
513                 Math.PI + 2, Angle.TWO_PI + 1);
514         checkInterval(AngularInterval.Convex.of(Point1S.of(-2), Point1S.of(-1.5), TEST_PRECISION), -2, -1.5);
515 
516         checkFull(AngularInterval.Convex.of(Point1S.of(1), Point1S.of(1), TEST_PRECISION));
517         checkFull(AngularInterval.Convex.of(Point1S.of(0), Point1S.of(1e-11), TEST_PRECISION));
518         checkFull(AngularInterval.Convex.of(Point1S.of(0), Point1S.of(-1e-11), TEST_PRECISION));
519         checkFull(AngularInterval.Convex.of(Point1S.of(0), Point1S.of(Angle.TWO_PI), TEST_PRECISION));
520     }
521 
522     @Test
523     void testConvex_of_points_invalidArgs() {
524         // act/assert
525         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.Convex.of(Point1S.of(Double.NEGATIVE_INFINITY),
526                 Point1S.of(Double.POSITIVE_INFINITY), TEST_PRECISION));
527         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.Convex.of(Point1S.of(0), Point1S.of(Math.PI + 1e-1), TEST_PRECISION));
528         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.Convex.of(Point1S.of(Angle.PI_OVER_TWO),
529                 Point1S.of(-Angle.PI_OVER_TWO + 1), TEST_PRECISION));
530         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.Convex.of(Point1S.of(0), Point1S.of(-0.5), TEST_PRECISION));
531     }
532 
533     @Test
534     void testConvex_of_cutAngles() {
535         // arrange
536         final Precision.DoubleEquivalence precisionA = Precision.doubleEquivalenceOfEpsilon(1e-3);
537         final Precision.DoubleEquivalence precisionB = Precision.doubleEquivalenceOfEpsilon(1e-2);
538 
539         final CutAngle zeroPos = CutAngles.createPositiveFacing(Point1S.ZERO, precisionA);
540         final CutAngle zeroNeg = CutAngles.createNegativeFacing(Point1S.ZERO, precisionA);
541 
542         final CutAngle piPos = CutAngles.createPositiveFacing(Point1S.PI, precisionA);
543         final CutAngle piNeg = CutAngles.createNegativeFacing(Point1S.PI, precisionA);
544 
545         final CutAngle almostPiPos = CutAngles.createPositiveFacing(Point1S.of(Math.PI + 5e-3), precisionB);
546 
547         // act/assert
548         checkInterval(AngularInterval.Convex.of(zeroNeg, piPos), 0, Math.PI);
549         checkInterval(AngularInterval.Convex.of(zeroPos, piNeg), Math.PI, Angle.TWO_PI);
550 
551         checkFull(AngularInterval.Convex.of(zeroPos, zeroNeg));
552         checkFull(AngularInterval.Convex.of(zeroPos, piPos));
553         checkFull(AngularInterval.Convex.of(piNeg, zeroNeg));
554 
555         checkFull(AngularInterval.Convex.of(almostPiPos, piNeg));
556         checkFull(AngularInterval.Convex.of(piNeg, almostPiPos));
557     }
558 
559     @Test
560     void testConvex_of_cutAngles_invalidArgs() {
561         // arrange
562         final CutAngle pt = CutAngles.createNegativeFacing(Point1S.ZERO, TEST_PRECISION);
563         final CutAngle nan = CutAngles.createPositiveFacing(Point1S.NaN, TEST_PRECISION);
564 
565         // act/assert
566         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.Convex.of(pt, nan));
567         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.Convex.of(nan, pt));
568         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.Convex.of(nan, nan));
569         Assertions.assertThrows(IllegalArgumentException.class, () -> AngularInterval.Convex.of(
570                 CutAngles.createNegativeFacing(1, TEST_PRECISION),
571                 CutAngles.createPositiveFacing(0.5, TEST_PRECISION)));
572     }
573 
574     @Test
575     void testConvex_toConvex() {
576         // arrange
577         final AngularInterval.Convex full = AngularInterval.full();
578         final AngularInterval.Convex interval = AngularInterval.Convex.of(0, 1, TEST_PRECISION);
579 
580         List<AngularInterval.Convex> result;
581 
582         // act/assert
583         result = full.toConvex();
584         Assertions.assertEquals(1, result.size());
585         Assertions.assertSame(full, result.get(0));
586 
587         result = interval.toConvex();
588         Assertions.assertEquals(1, result.size());
589         Assertions.assertSame(interval, result.get(0));
590     }
591 
592     @Test
593     void testSplitDiameter_full() {
594         // arrange
595         final AngularInterval.Convex full = AngularInterval.full();
596         final CutAngle splitter = CutAngles.createPositiveFacing(Point1S.of(Angle.PI_OVER_TWO), TEST_PRECISION);
597 
598         // act
599         final Split<AngularInterval.Convex> split = full.splitDiameter(splitter);
600 
601         // assert
602         Assertions.assertEquals(SplitLocation.BOTH, split.getLocation());
603 
604         checkInterval(split.getMinus(), 1.5 * Math.PI, 2.5 * Math.PI);
605         checkInterval(split.getPlus(), 0.5 * Math.PI, 1.5 * Math.PI);
606     }
607 
608     @Test
609     void testSplitDiameter_full_splitOnZero() {
610         // arrange
611         final AngularInterval.Convex full = AngularInterval.full();
612         final CutAngle splitter = CutAngles.createNegativeFacing(Point1S.ZERO, TEST_PRECISION);
613 
614         // act
615         final Split<AngularInterval.Convex> split = full.splitDiameter(splitter);
616 
617         // assert
618         Assertions.assertEquals(SplitLocation.BOTH, split.getLocation());
619 
620         checkInterval(split.getMinus(), 0, Math.PI);
621         checkInterval(split.getPlus(), Math.PI, Angle.TWO_PI);
622     }
623 
624     @Test
625     void testSplitDiameter_minus() {
626         // arrange
627         final AngularInterval.Convex interval = AngularInterval.Convex.of(0.1, Angle.PI_OVER_TWO, TEST_PRECISION);
628         final CutAngle splitter = CutAngles.createNegativeFacing(Point1S.ZERO, TEST_PRECISION);
629 
630         // act
631         final Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
632 
633         // assert
634         Assertions.assertEquals(SplitLocation.MINUS, split.getLocation());
635 
636         Assertions.assertSame(interval, split.getMinus());
637         Assertions.assertNull(split.getPlus());
638     }
639 
640     @Test
641     void testSplitDiameter_plus() {
642         // arrange
643         final AngularInterval.Convex interval = AngularInterval.Convex.of(-0.4 * Math.PI, 0.4 * Math.PI, TEST_PRECISION);
644         final CutAngle splitter = CutAngles.createNegativeFacing(Point1S.of(Angle.PI_OVER_TWO), TEST_PRECISION);
645 
646         // act
647         final Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
648 
649         // assert
650         Assertions.assertEquals(SplitLocation.PLUS, split.getLocation());
651 
652         Assertions.assertNull(split.getMinus());
653         Assertions.assertSame(interval, split.getPlus());
654     }
655 
656     @Test
657     void testSplitDiameter_both_negativeFacingSplitter() {
658         // arrange
659         final AngularInterval.Convex interval = AngularInterval.Convex.of(Angle.PI_OVER_TWO, -Angle.PI_OVER_TWO, TEST_PRECISION);
660         final CutAngle splitter = CutAngles.createNegativeFacing(Point1S.of(Math.PI), TEST_PRECISION);
661 
662         // act
663         final Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
664 
665         // assert
666         Assertions.assertEquals(SplitLocation.BOTH, split.getLocation());
667 
668         checkInterval(split.getMinus(), Math.PI, 1.5 * Math.PI);
669         checkInterval(split.getPlus(), Angle.PI_OVER_TWO, Math.PI);
670     }
671 
672     @Test
673     void testSplitDiameter_both_positiveFacingSplitter() {
674         // arrange
675         final AngularInterval.Convex interval = AngularInterval.Convex.of(Angle.PI_OVER_TWO, -Angle.PI_OVER_TWO, TEST_PRECISION);
676         final CutAngle splitter = CutAngles.createPositiveFacing(Point1S.of(Math.PI), TEST_PRECISION);
677 
678         // act
679         final Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
680 
681         // assert
682         Assertions.assertEquals(SplitLocation.BOTH, split.getLocation());
683 
684         checkInterval(split.getMinus(), Angle.PI_OVER_TWO, Math.PI);
685         checkInterval(split.getPlus(), Math.PI, 1.5 * Math.PI);
686     }
687 
688     @Test
689     void testSplitDiameter_both_antipodal_negativeFacingSplitter() {
690         // arrange
691         final AngularInterval.Convex interval = AngularInterval.Convex.of(Angle.PI_OVER_TWO, -Angle.PI_OVER_TWO, TEST_PRECISION);
692         final CutAngle splitter = CutAngles.createNegativeFacing(Point1S.ZERO, TEST_PRECISION);
693 
694         // act
695         final Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
696 
697         // assert
698         Assertions.assertEquals(SplitLocation.BOTH, split.getLocation());
699 
700         checkInterval(split.getMinus(), Angle.PI_OVER_TWO, Math.PI);
701         checkInterval(split.getPlus(), Math.PI, 1.5 * Math.PI);
702     }
703 
704     @Test
705     void testSplitDiameter_both_antipodal_positiveFacingSplitter() {
706         // arrange
707         final AngularInterval.Convex interval = AngularInterval.Convex.of(Angle.PI_OVER_TWO, -Angle.PI_OVER_TWO, TEST_PRECISION);
708         final CutAngle splitter = CutAngles.createPositiveFacing(Point1S.ZERO, TEST_PRECISION);
709 
710         // act
711         final Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
712 
713         // assert
714         Assertions.assertEquals(SplitLocation.BOTH, split.getLocation());
715 
716         checkInterval(split.getMinus(), Math.PI, 1.5 * Math.PI);
717         checkInterval(split.getPlus(), Angle.PI_OVER_TWO, Math.PI);
718     }
719 
720     @Test
721     void testSplitDiameter_splitOnBoundary_negativeFacing() {
722         // arrange
723         final AngularInterval.Convex interval = AngularInterval.Convex.of(Angle.PI_OVER_TWO, -Angle.PI_OVER_TWO, TEST_PRECISION);
724         final CutAngle splitter = CutAngles.createNegativeFacing(Point1S.of(Angle.PI_OVER_TWO), TEST_PRECISION);
725 
726         // act
727         final Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
728 
729         // assert
730         Assertions.assertEquals(SplitLocation.MINUS, split.getLocation());
731 
732         Assertions.assertSame(interval, split.getMinus());
733         Assertions.assertNull(split.getPlus());
734     }
735 
736     @Test
737     void testSplitDiameter_splitOnBoundary_positiveFacing() {
738         // arrange
739         final AngularInterval.Convex interval = AngularInterval.Convex.of(0, Math.PI, TEST_PRECISION);
740         final CutAngle splitter = CutAngles.createPositiveFacing(Point1S.of(Math.PI), TEST_PRECISION);
741 
742         // act
743         final Split<AngularInterval.Convex> split = interval.splitDiameter(splitter);
744 
745         // assert
746         Assertions.assertEquals(SplitLocation.MINUS, split.getLocation());
747 
748         Assertions.assertSame(interval, split.getMinus());
749         Assertions.assertNull(split.getPlus());
750     }
751 
752     @Test
753     void testConvex_transform() {
754         // arrange
755         final AngularInterval.Convex interval = AngularInterval.Convex.of(Angle.PI_OVER_TWO, Math.PI, TEST_PRECISION);
756 
757         final Transform1S rotate = Transform1S.createRotation(Angle.PI_OVER_TWO);
758         final Transform1S invert = Transform1S.createNegation().rotate(Angle.PI_OVER_TWO);
759 
760         // act/assert
761         checkInterval(interval.transform(rotate), Math.PI, 1.5 * Math.PI);
762         checkInterval(interval.transform(invert), -0.5 * Math.PI, 0.0);
763     }
764 
765     private static void checkFull(final AngularInterval interval) {
766         Assertions.assertTrue(interval.isFull());
767         Assertions.assertFalse(interval.isEmpty());
768 
769         Assertions.assertNull(interval.getMinBoundary());
770         Assertions.assertEquals(0, interval.getMin(), TEST_EPS);
771         Assertions.assertNull(interval.getMaxBoundary());
772         Assertions.assertEquals(Angle.TWO_PI, interval.getMax(), TEST_EPS);
773 
774         Assertions.assertNull(interval.getCentroid());
775         Assertions.assertNull(interval.getMidPoint());
776 
777         Assertions.assertEquals(Angle.TWO_PI, interval.getSize(), TEST_EPS);
778         Assertions.assertEquals(0, interval.getBoundarySize(), TEST_EPS);
779 
780         checkClassify(interval, RegionLocation.INSIDE, Point1S.ZERO, Point1S.of(Math.PI));
781     }
782 
783     private static void checkInterval(final AngularInterval interval, final double min, final double max) {
784 
785         Assertions.assertFalse(interval.isFull());
786         Assertions.assertFalse(interval.isEmpty());
787 
788         final CutAngle minBoundary = interval.getMinBoundary();
789         Assertions.assertEquals(min, minBoundary.getAzimuth(), TEST_EPS);
790         Assertions.assertFalse(minBoundary.isPositiveFacing());
791 
792         final CutAngle maxBoundary = interval.getMaxBoundary();
793         Assertions.assertEquals(max, maxBoundary.getAzimuth(), TEST_EPS);
794         Assertions.assertTrue(maxBoundary.isPositiveFacing());
795 
796         Assertions.assertEquals(min, interval.getMin(), TEST_EPS);
797         Assertions.assertEquals(max, interval.getMax(), TEST_EPS);
798 
799         Assertions.assertEquals(0.5 * (max + min), interval.getMidPoint().getAzimuth(), TEST_EPS);
800         Assertions.assertSame(interval.getMidPoint(), interval.getCentroid());
801 
802         Assertions.assertEquals(0, interval.getBoundarySize(), TEST_EPS);
803         Assertions.assertEquals(max - min, interval.getSize(), TEST_EPS);
804 
805         checkClassify(interval, RegionLocation.INSIDE, interval.getMidPoint());
806         checkClassify(interval, RegionLocation.BOUNDARY,
807                 interval.getMinBoundary().getPoint(), interval.getMaxBoundary().getPoint());
808         checkClassify(interval, RegionLocation.OUTSIDE, Point1S.of(interval.getMidPoint().getAzimuth() + Math.PI));
809     }
810 
811     private static void checkClassify(final Region<Point1S> region, final RegionLocation loc, final Point1S... pts) {
812         for (final Point1S pt : pts) {
813             Assertions.assertEquals(loc, region.classify(pt), "Unexpected location for point " + pt);
814         }
815     }
816 }