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.euclidean.twod;
18  
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Collections;
22  import java.util.Comparator;
23  import java.util.List;
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.numbers.angle.Angle;
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 Vector2DTest {
34  
35      private static final double EPS = Math.ulp(1d);
36  
37      @Test
38      void testConstants() {
39          // act/assert
40          checkVector(Vector2D.ZERO, 0, 0);
41          checkVector(Vector2D.Unit.PLUS_X, 1, 0);
42          checkVector(Vector2D.Unit.MINUS_X, -1, 0);
43          checkVector(Vector2D.Unit.PLUS_Y, 0, 1);
44          checkVector(Vector2D.Unit.MINUS_Y, 0, -1);
45          checkVector(Vector2D.NaN, Double.NaN, Double.NaN);
46          checkVector(Vector2D.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
47          checkVector(Vector2D.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
48      }
49  
50      @Test
51      void testConstants_normalize() {
52          // act/assert
53          Assertions.assertThrows(IllegalArgumentException.class, Vector2D.ZERO::normalize);
54          Assertions.assertThrows(IllegalArgumentException.class, Vector2D.NaN::normalize);
55          Assertions.assertThrows(IllegalArgumentException.class, Vector2D.POSITIVE_INFINITY::normalize);
56          Assertions.assertThrows(IllegalArgumentException.class, Vector2D.NEGATIVE_INFINITY::normalize);
57  
58          Assertions.assertSame(Vector2D.Unit.PLUS_X, Vector2D.Unit.PLUS_X.normalize());
59          Assertions.assertSame(Vector2D.Unit.MINUS_X, Vector2D.Unit.MINUS_X.normalize());
60  
61          Assertions.assertSame(Vector2D.Unit.PLUS_Y, Vector2D.Unit.PLUS_Y.normalize());
62          Assertions.assertSame(Vector2D.Unit.MINUS_Y, Vector2D.Unit.MINUS_Y.normalize());
63      }
64  
65      @Test
66      void testCoordinateAscendingOrder() {
67          // arrange
68          final Comparator<Vector2D> cmp = Vector2D.COORDINATE_ASCENDING_ORDER;
69  
70          // act/assert
71          Assertions.assertEquals(0, cmp.compare(Vector2D.of(1, 2), Vector2D.of(1, 2)));
72  
73          Assertions.assertEquals(-1, cmp.compare(Vector2D.of(0, 2), Vector2D.of(1, 2)));
74          Assertions.assertEquals(-1, cmp.compare(Vector2D.of(1, 1), Vector2D.of(1, 2)));
75  
76          Assertions.assertEquals(1, cmp.compare(Vector2D.of(2, 2), Vector2D.of(1, 2)));
77          Assertions.assertEquals(1, cmp.compare(Vector2D.of(1, 3), Vector2D.of(1, 2)));
78  
79          Assertions.assertEquals(-1, cmp.compare(Vector2D.of(1, 3), null));
80          Assertions.assertEquals(1, cmp.compare(null, Vector2D.of(1, 2)));
81          Assertions.assertEquals(0, cmp.compare(null, null));
82      }
83  
84      @Test
85      void testCoordinates() {
86          // arrange
87          final Vector2D v = Vector2D.of(1, 2);
88  
89          // act/assert
90          Assertions.assertEquals(1.0, v.getX(), EPS);
91          Assertions.assertEquals(2.0, v.getY(), EPS);
92      }
93  
94      @Test
95      void testToArray() {
96          // arrange
97          final Vector2D oneTwo = Vector2D.of(1, 2);
98  
99          // act
100         final double[] array = oneTwo.toArray();
101 
102         // assert
103         Assertions.assertEquals(2, array.length);
104         Assertions.assertEquals(1.0, array[0], EPS);
105         Assertions.assertEquals(2.0, array[1], EPS);
106     }
107 
108     @Test
109     void testDimension() {
110         // arrange
111         final Vector2D v = Vector2D.of(1, 2);
112 
113         // act/assert
114         Assertions.assertEquals(2, v.getDimension());
115     }
116 
117     @Test
118     void testNaN() {
119         // act/assert
120         Assertions.assertTrue(Vector2D.of(0, Double.NaN).isNaN());
121         Assertions.assertTrue(Vector2D.of(Double.NaN, 0).isNaN());
122 
123         Assertions.assertFalse(Vector2D.of(1, 1).isNaN());
124         Assertions.assertFalse(Vector2D.of(1, Double.NEGATIVE_INFINITY).isNaN());
125         Assertions.assertFalse(Vector2D.of(Double.POSITIVE_INFINITY, 1).isNaN());
126     }
127 
128     @Test
129     void testInfinite() {
130         // act/assert
131         Assertions.assertTrue(Vector2D.of(0, Double.NEGATIVE_INFINITY).isInfinite());
132         Assertions.assertTrue(Vector2D.of(Double.NEGATIVE_INFINITY, 0).isInfinite());
133         Assertions.assertTrue(Vector2D.of(0, Double.POSITIVE_INFINITY).isInfinite());
134         Assertions.assertTrue(Vector2D.of(Double.POSITIVE_INFINITY, 0).isInfinite());
135 
136         Assertions.assertFalse(Vector2D.of(1, 1).isInfinite());
137         Assertions.assertFalse(Vector2D.of(0, Double.NaN).isInfinite());
138         Assertions.assertFalse(Vector2D.of(Double.NEGATIVE_INFINITY, Double.NaN).isInfinite());
139         Assertions.assertFalse(Vector2D.of(Double.NaN, Double.NEGATIVE_INFINITY).isInfinite());
140         Assertions.assertFalse(Vector2D.of(Double.POSITIVE_INFINITY, Double.NaN).isInfinite());
141         Assertions.assertFalse(Vector2D.of(Double.NaN, Double.POSITIVE_INFINITY).isInfinite());
142     }
143 
144     @Test
145     void testFinite() {
146         // act/assert
147         Assertions.assertTrue(Vector2D.ZERO.isFinite());
148         Assertions.assertTrue(Vector2D.of(1, 1).isFinite());
149 
150         Assertions.assertFalse(Vector2D.of(0, Double.NEGATIVE_INFINITY).isFinite());
151         Assertions.assertFalse(Vector2D.of(Double.NEGATIVE_INFINITY, 0).isFinite());
152         Assertions.assertFalse(Vector2D.of(0, Double.POSITIVE_INFINITY).isFinite());
153         Assertions.assertFalse(Vector2D.of(Double.POSITIVE_INFINITY, 0).isFinite());
154 
155         Assertions.assertFalse(Vector2D.of(0, Double.NaN).isFinite());
156         Assertions.assertFalse(Vector2D.of(Double.NEGATIVE_INFINITY, Double.NaN).isFinite());
157         Assertions.assertFalse(Vector2D.of(Double.NaN, Double.NEGATIVE_INFINITY).isFinite());
158         Assertions.assertFalse(Vector2D.of(Double.POSITIVE_INFINITY, Double.NaN).isFinite());
159         Assertions.assertFalse(Vector2D.of(Double.NaN, Double.POSITIVE_INFINITY).isFinite());
160     }
161 
162     @Test
163     void testGetZero() {
164         // act/assert
165         checkVector(Vector2D.of(1.0, 1.0).getZero(), 0, 0);
166     }
167 
168     @Test
169     void testNorm() {
170         // act/assert
171         Assertions.assertEquals(0.0, Vector2D.of(0, 0).norm(), EPS);
172 
173         Assertions.assertEquals(5.0, Vector2D.of(3, 4).norm(), EPS);
174         Assertions.assertEquals(5.0, Vector2D.of(3, -4).norm(), EPS);
175         Assertions.assertEquals(5.0, Vector2D.of(-3, 4).norm(), EPS);
176         Assertions.assertEquals(5.0, Vector2D.of(-3, -4).norm(), EPS);
177 
178         Assertions.assertEquals(Math.sqrt(5.0), Vector2D.of(-1, -2).norm(), EPS);
179     }
180 
181     @Test
182     void testNorm_unitVectors() {
183         // arrange
184         final Vector2D v = Vector2D.of(2.0, 3.0).normalize();
185 
186         // act/assert
187         Assertions.assertEquals(1.0, v.norm(), 0.0);
188     }
189 
190     @Test
191     void testNormSq() {
192         // act/assert
193         Assertions.assertEquals(0.0, Vector2D.of(0, 0).normSq(), EPS);
194 
195         Assertions.assertEquals(25.0, Vector2D.of(3, 4).normSq(), EPS);
196         Assertions.assertEquals(25.0, Vector2D.of(3, -4).normSq(), EPS);
197         Assertions.assertEquals(25.0, Vector2D.of(-3, 4).normSq(), EPS);
198         Assertions.assertEquals(25.0, Vector2D.of(-3, -4).normSq(), EPS);
199 
200         Assertions.assertEquals(5.0, Vector2D.of(-1, -2).normSq(), EPS);
201     }
202 
203     @Test
204     void testNormSq_unitVectors() {
205         // arrange
206         final Vector2D v = Vector2D.of(2.0, 3.0).normalize();
207 
208         // act/assert
209         Assertions.assertEquals(1.0, v.normSq(), 0.0);
210     }
211 
212     @Test
213     void testWithNorm() {
214         // act/assert
215         checkVector(Vector2D.of(3, 4).withNorm(1.0), 0.6, 0.8);
216         checkVector(Vector2D.of(4, 3).withNorm(1.0), 0.8, 0.6);
217 
218         checkVector(Vector2D.of(-3, 4).withNorm(0.5), -0.3, 0.4);
219         checkVector(Vector2D.of(3, -4).withNorm(2.0), 1.2, -1.6);
220         checkVector(Vector2D.of(-3, -4).withNorm(3.0), -1.8, 3.0 * Math.sin(Math.atan2(-4, -3)));
221 
222         checkVector(Vector2D.of(0.5, 0.5).withNorm(2), Math.sqrt(2), Math.sqrt(2));
223     }
224 
225     @Test
226     void testWithNorm_illegalNorm() {
227         // act/assert
228         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.ZERO.withNorm(2.0));
229         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.NaN.withNorm(2.0));
230         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.POSITIVE_INFINITY.withNorm(2.0));
231         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.NEGATIVE_INFINITY.withNorm(2.0));
232     }
233 
234     @Test
235     void testWithNorm_unitVectors() {
236         // arrange
237         final double eps = 1e-14;
238         final Vector2D v = Vector2D.of(2.0, -3.0).normalize();
239 
240         // act/assert
241         checkVector(Vector2D.Unit.PLUS_X.withNorm(2.5), 2.5, 0.0);
242         checkVector(Vector2D.Unit.MINUS_Y.withNorm(3.14), 0.0, -3.14);
243 
244         for (int i = -10; i <= 10; i++) {
245             Assertions.assertEquals(Math.abs((double) i), v.withNorm(i).norm(), eps);
246         }
247     }
248 
249     @Test
250     void testAdd() {
251         // arrange
252         final Vector2D v1 = Vector2D.of(-1, 2);
253         final Vector2D v2 = Vector2D.of(3, -4);
254         final Vector2D v3 = Vector2D.of(5, 6);
255 
256         // act/assert
257         checkVector(v1.add(v1), -2, 4);
258 
259         checkVector(v1.add(v2), 2, -2);
260         checkVector(v2.add(v1), 2, -2);
261 
262         checkVector(v1.add(v3), 4, 8);
263         checkVector(v3.add(v1), 4, 8);
264     }
265 
266     @Test
267     void testAdd_scaled() {
268         // arrange
269         final Vector2D v1 = Vector2D.of(-1, 2);
270         final Vector2D v2 = Vector2D.of(3, -4);
271         final Vector2D v3 = Vector2D.of(5, 6);
272 
273         // act/assert
274         checkVector(v1.add(2, v1), -3, 6);
275 
276         checkVector(v1.add(0, v2), -1, 2);
277         checkVector(v2.add(1, v1), 2, -2);
278 
279         checkVector(v1.add(-1, v3), -6, -4);
280         checkVector(v3.add(-2, v1), 7, 2);
281     }
282 
283     @Test
284     void testSubtract() {
285         // arrange
286         final Vector2D v1 = Vector2D.of(-1, 2);
287         final Vector2D v2 = Vector2D.of(3, -4);
288         final Vector2D v3 = Vector2D.of(5, 6);
289 
290         // act/assert
291         checkVector(v1.subtract(v1), 0, 0);
292 
293         checkVector(v1.subtract(v2), -4, 6);
294         checkVector(v2.subtract(v1), 4, -6);
295 
296         checkVector(v1.subtract(v3), -6, -4);
297         checkVector(v3.subtract(v1), 6, 4);
298     }
299 
300     @Test
301     void testSubtract_scaled() {
302         // arrange
303         final Vector2D v1 = Vector2D.of(-1, 2);
304         final Vector2D v2 = Vector2D.of(3, -4);
305         final Vector2D v3 = Vector2D.of(5, 6);
306 
307         // act/assert
308         checkVector(v1.subtract(2, v1), 1, -2);
309 
310         checkVector(v1.subtract(0, v2), -1, 2);
311         checkVector(v2.subtract(1, v1), 4, -6);
312 
313         checkVector(v1.subtract(-1, v3), 4, 8);
314         checkVector(v3.subtract(-2, v1), 3, 10);
315     }
316 
317     @Test
318     void testNormalize() {
319         // arrange
320         final double invSqrt2 = 1.0 / Math.sqrt(2);
321 
322         // act/assert
323         checkVector(Vector2D.of(100, 0).normalize(), 1, 0);
324         checkVector(Vector2D.of(-100, 0).normalize(), -1, 0);
325         checkVector(Vector2D.of(0, 100).normalize(), 0, 1);
326         checkVector(Vector2D.of(0, -100).normalize(), 0, -1);
327         checkVector(Vector2D.of(-1, 2).normalize(), -1.0 / Math.sqrt(5), 2.0 / Math.sqrt(5));
328 
329         checkVector(Vector2D.of(Double.MIN_VALUE, 0).normalize(), 1, 0);
330         checkVector(Vector2D.of(0, Double.MIN_VALUE).normalize(), 0, 1);
331 
332         checkVector(Vector2D.of(-Double.MIN_VALUE, Double.MIN_VALUE).normalize(), -invSqrt2, invSqrt2);
333 
334         checkVector(Vector2D.of(Double.MIN_NORMAL, 0).normalize(), 1, 0, 0);
335         checkVector(Vector2D.of(0, Double.MIN_NORMAL).normalize(), 0, 1, 0);
336 
337         checkVector(Vector2D.of(Double.MIN_NORMAL, -Double.MIN_NORMAL).normalize(), invSqrt2, -invSqrt2);
338 
339         checkVector(Vector2D.of(-Double.MAX_VALUE, -Double.MAX_VALUE).normalize(), -invSqrt2, -invSqrt2);
340     }
341 
342     @Test
343     void testNormalize_illegalNorm() {
344         // arrange
345         final Pattern illegalNorm = Pattern.compile("^Illegal norm: (0\\.0|-?Infinity|NaN)");
346 
347         // act/assert
348         GeometryTestUtils.assertThrowsWithMessage(Vector2D.ZERO::normalize,
349                 IllegalArgumentException.class, illegalNorm);
350         GeometryTestUtils.assertThrowsWithMessage(Vector2D.NaN::normalize,
351                 IllegalArgumentException.class, illegalNorm);
352         GeometryTestUtils.assertThrowsWithMessage(Vector2D.POSITIVE_INFINITY::normalize,
353                 IllegalArgumentException.class, illegalNorm);
354         GeometryTestUtils.assertThrowsWithMessage(Vector2D.NEGATIVE_INFINITY::normalize,
355                 IllegalArgumentException.class, illegalNorm);
356     }
357 
358     @Test
359     void testNormalize_isIdempotent() {
360         // arrange
361         final double invSqrt2 = 1.0 / Math.sqrt(2);
362         final Vector2D v = Vector2D.of(2, 2).normalize();
363 
364         // act/assert
365         Assertions.assertSame(v, v.normalize());
366         checkVector(v.normalize(), invSqrt2, invSqrt2);
367     }
368 
369     @Test
370     void testNormalizeOrNull() {
371         // arrange
372         final double invSqrt2 = 1 / Math.sqrt(2);
373 
374         // act/assert
375         checkVector(Vector2D.of(100, 0).normalizeOrNull(), 1, 0);
376         checkVector(Vector2D.of(-100, 0).normalizeOrNull(), -1, 0);
377 
378         checkVector(Vector2D.of(2, 2).normalizeOrNull(), invSqrt2, invSqrt2);
379         checkVector(Vector2D.of(-2, -2).normalizeOrNull(), -invSqrt2, -invSqrt2);
380 
381         checkVector(Vector2D.of(Double.MIN_VALUE, 0).normalizeOrNull(), 1, 0);
382         checkVector(Vector2D.of(0, Double.MIN_VALUE).normalizeOrNull(), 0, 1);
383 
384         checkVector(Vector2D.of(-Double.MIN_VALUE, -Double.MIN_VALUE).normalizeOrNull(), -invSqrt2, -invSqrt2);
385 
386         checkVector(Vector2D.of(Double.MIN_NORMAL, -Double.MIN_NORMAL).normalizeOrNull(), invSqrt2, -invSqrt2);
387 
388         checkVector(Vector2D.of(Double.MAX_VALUE, -Double.MAX_VALUE).normalizeOrNull(), invSqrt2, -invSqrt2);
389 
390         Assertions.assertNull(Vector2D.ZERO.normalizeOrNull());
391         Assertions.assertNull(Vector2D.NaN.normalizeOrNull());
392         Assertions.assertNull(Vector2D.POSITIVE_INFINITY.normalizeOrNull());
393         Assertions.assertNull(Vector2D.NEGATIVE_INFINITY.normalizeOrNull());
394     }
395 
396     @Test
397     void testNormalizeOrNull_isIdempotent() {
398         // arrange
399         final double invSqrt2 = 1 / Math.sqrt(2);
400         final Vector2D v = Vector2D.of(2, 2).normalizeOrNull();
401 
402         // act/assert
403         Assertions.assertSame(v, v.normalizeOrNull());
404         checkVector(v.normalizeOrNull(), invSqrt2, invSqrt2);
405     }
406 
407     @Test
408     void testNegate() {
409         // act/assert
410         checkVector(Vector2D.of(1, 2).negate(), -1, -2);
411         checkVector(Vector2D.of(-3, -4).negate(), 3, 4);
412         checkVector(Vector2D.of(5, -6).negate().negate(), 5, -6);
413     }
414 
415     @Test
416     void testNegate_unitVectors() {
417         // arrange
418         final Vector2D v1 = Vector2D.of(1.0, 1.0).normalize();
419         final Vector2D v2 = Vector2D.of(-1.0, -2.0).normalize();
420         final Vector2D v3 = Vector2D.of(2.0, -3.0).normalize();
421 
422         // act/assert
423         checkVector(v1.negate(), -1.0 / Math.sqrt(2.0), -1.0 / Math.sqrt(2.0));
424         checkVector(v2.negate(), 1.0 / Math.sqrt(5.0), 2.0 / Math.sqrt(5.0));
425         checkVector(v3.negate(), -2.0 / Math.sqrt(13.0), 3.0 / Math.sqrt(13.0));
426     }
427 
428     @Test
429     void testScalarMultiply() {
430         // act/assert
431         checkVector(Vector2D.of(1, 2).multiply(0), 0, 0);
432 
433         checkVector(Vector2D.of(1, 2).multiply(3), 3, 6);
434         checkVector(Vector2D.of(1, 2).multiply(-3), -3, -6);
435 
436         checkVector(Vector2D.of(2, 3).multiply(1.5), 3, 4.5);
437         checkVector(Vector2D.of(2, 3).multiply(-1.5), -3, -4.5);
438     }
439 
440     @Test
441     void testDistance() {
442         // arrange
443         final Vector2D v1 = Vector2D.of(1, 1);
444         final Vector2D v2 = Vector2D.of(4, 5);
445         final Vector2D v3 = Vector2D.of(-1, 0);
446 
447         // act/assert
448         Assertions.assertEquals(0, v1.distance(v1), EPS);
449 
450         Assertions.assertEquals(5, v1.distance(v2), EPS);
451         Assertions.assertEquals(5, v2.distance(v1), EPS);
452 
453         Assertions.assertEquals(Math.sqrt(5), v1.distance(v3), EPS);
454         Assertions.assertEquals(Math.sqrt(5), v3.distance(v1), EPS);
455     }
456 
457     @Test
458     void testDistanceSq() {
459         // arrange
460         final Vector2D v1 = Vector2D.of(1, 1);
461         final Vector2D v2 = Vector2D.of(4, 5);
462         final Vector2D v3 = Vector2D.of(-1, 0);
463 
464         // act/assert
465         Assertions.assertEquals(0, v1.distanceSq(v1), EPS);
466 
467         Assertions.assertEquals(25, v1.distanceSq(v2), EPS);
468         Assertions.assertEquals(25, v2.distanceSq(v1), EPS);
469 
470         Assertions.assertEquals(5, v1.distanceSq(v3), EPS);
471         Assertions.assertEquals(5, v3.distanceSq(v1), EPS);
472     }
473 
474     @Test
475     void testDotProduct() {
476         // arrange
477         final Vector2D v1 = Vector2D.of(1, 1);
478         final Vector2D v2 = Vector2D.of(4, 5);
479         final Vector2D v3 = Vector2D.of(-1, 0);
480 
481         // act/assert
482         Assertions.assertEquals(2, v1.dot(v1), EPS);
483         Assertions.assertEquals(41, v2.dot(v2), EPS);
484         Assertions.assertEquals(1, v3.dot(v3), EPS);
485 
486         Assertions.assertEquals(9, v1.dot(v2), EPS);
487         Assertions.assertEquals(9, v2.dot(v1), EPS);
488 
489         Assertions.assertEquals(-1, v1.dot(v3), EPS);
490         Assertions.assertEquals(-1, v3.dot(v1), EPS);
491 
492         Assertions.assertEquals(1, Vector2D.Unit.PLUS_X.dot(Vector2D.Unit.PLUS_X), EPS);
493         Assertions.assertEquals(0, Vector2D.Unit.PLUS_X.dot(Vector2D.Unit.PLUS_Y), EPS);
494         Assertions.assertEquals(-1, Vector2D.Unit.PLUS_X.dot(Vector2D.Unit.MINUS_X), EPS);
495         Assertions.assertEquals(0, Vector2D.Unit.PLUS_X.dot(Vector2D.Unit.MINUS_Y), EPS);
496     }
497 
498     @Test
499     void testOrthogonal() {
500         // arrange
501         final double invSqrt2 = 1.0 / Math.sqrt(2.0);
502 
503         // act/assert
504         checkVector(Vector2D.of(3, 0).orthogonal(), 0.0, 1.0);
505         checkVector(Vector2D.of(1.0, 1.0).orthogonal(), -invSqrt2, invSqrt2);
506 
507         checkVector(Vector2D.of(0, 2).orthogonal(), -1.0, 0.0);
508         checkVector(Vector2D.of(-1.0, 1.0).orthogonal(), -invSqrt2, -invSqrt2);
509 
510         checkVector(Vector2D.Unit.MINUS_X.orthogonal(), 0.0, -1.0);
511         checkVector(Vector2D.of(-1.0, -1.0).orthogonal(), invSqrt2, -invSqrt2);
512 
513         checkVector(Vector2D.Unit.MINUS_Y.orthogonal(), 1.0, 0.0);
514         checkVector(Vector2D.of(1.0, -1.0).orthogonal(), invSqrt2, invSqrt2);
515     }
516 
517     @Test
518     void testOrthogonal_fullCircle() {
519         for (double az = 0.0; az <= Angle.TWO_PI; az += 0.25) {
520             // arrange
521             final Vector2D v = PolarCoordinates.toCartesian(Math.PI, az);
522 
523             //act
524             final Vector2D ortho = v.orthogonal();
525 
526             // assert
527             Assertions.assertEquals(1.0, ortho.norm(), EPS);
528             Assertions.assertEquals(0.0, v.dot(ortho), EPS);
529         }
530     }
531 
532     @Test
533     void testOrthogonal_illegalNorm() {
534         // act/assert
535         Assertions.assertThrows(IllegalArgumentException.class, Vector2D.ZERO::orthogonal);
536         Assertions.assertThrows(IllegalArgumentException.class, Vector2D.NaN::orthogonal);
537         Assertions.assertThrows(IllegalArgumentException.class, Vector2D.POSITIVE_INFINITY::orthogonal);
538         Assertions.assertThrows(IllegalArgumentException.class, Vector2D.NEGATIVE_INFINITY::orthogonal);
539     }
540 
541     @Test
542     void testOrthogonal_givenDirection() {
543         // arrange
544         final double invSqrt2 = 1.0 / Math.sqrt(2.0);
545 
546         // act/assert
547         checkVector(Vector2D.Unit.PLUS_X.orthogonal(Vector2D.of(-1.0, 0.1)), 0.0, 1.0);
548         checkVector(Vector2D.Unit.PLUS_Y.orthogonal(Vector2D.of(2.0, 2.0)), 1.0, 0.0);
549 
550         checkVector(Vector2D.of(2.9, 2.9).orthogonal(Vector2D.of(1.0, 0.22)), invSqrt2, -invSqrt2);
551         checkVector(Vector2D.of(2.9, 2.9).orthogonal(Vector2D.of(0.22, 1.0)), -invSqrt2, invSqrt2);
552     }
553 
554     @Test
555     void testOrthogonal_givenDirection_illegalNorm() {
556         // act/assert
557         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.ZERO.orthogonal(Vector2D.Unit.PLUS_X));
558         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.NaN.orthogonal(Vector2D.Unit.PLUS_X));
559         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.POSITIVE_INFINITY.orthogonal(Vector2D.Unit.PLUS_X));
560         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.NEGATIVE_INFINITY.orthogonal(Vector2D.Unit.PLUS_X));
561         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.Unit.PLUS_X.orthogonal(Vector2D.ZERO));
562         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.Unit.PLUS_X.orthogonal(Vector2D.NaN));
563         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.Unit.PLUS_X.orthogonal(Vector2D.POSITIVE_INFINITY));
564         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.Unit.PLUS_X.orthogonal(Vector2D.NEGATIVE_INFINITY));
565     }
566 
567     @Test
568     void testOrthogonal_givenDirection_directionIsCollinear() {
569         // act/assert
570         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.Unit.PLUS_X.orthogonal(Vector2D.Unit.PLUS_X));
571         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.Unit.PLUS_X.orthogonal(Vector2D.Unit.MINUS_X));
572         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.of(1.0, 1.0).orthogonal(Vector2D.of(2.0, 2.0)));
573         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.of(-1.01, -1.01).orthogonal(Vector2D.of(20.1, 20.1)));
574     }
575 
576     @Test
577     void testAngle() {
578         // act/assert
579         Assertions.assertEquals(0, Vector2D.Unit.PLUS_X.angle(Vector2D.Unit.PLUS_X), EPS);
580 
581         Assertions.assertEquals(Math.PI, Vector2D.Unit.PLUS_X.angle(Vector2D.Unit.MINUS_X), EPS);
582         Assertions.assertEquals(Angle.PI_OVER_TWO, Vector2D.Unit.PLUS_X.angle(Vector2D.Unit.PLUS_Y), EPS);
583         Assertions.assertEquals(Angle.PI_OVER_TWO, Vector2D.Unit.PLUS_X.angle(Vector2D.Unit.MINUS_Y), EPS);
584 
585         Assertions.assertEquals(Math.PI / 4, Vector2D.of(1, 1).angle(Vector2D.of(1, 0)), EPS);
586         Assertions.assertEquals(Math.PI / 4, Vector2D.of(1, 0).angle(Vector2D.of(1, 1)), EPS);
587 
588         Assertions.assertEquals(0.004999958333958323, Vector2D.of(20.0, 0.0).angle(Vector2D.of(20.0, 0.1)), EPS);
589     }
590 
591 
592     @Test
593     void testAngle_illegalNorm() {
594         // arrange
595         final Vector2D v = Vector2D.of(1.0, 1.0);
596 
597         // act/assert
598         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.ZERO.angle(v));
599         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.NaN.angle(v));
600         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.POSITIVE_INFINITY.angle(v));
601         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.NEGATIVE_INFINITY.angle(v));
602         Assertions.assertThrows(IllegalArgumentException.class, () -> v.angle(Vector2D.ZERO));
603         Assertions.assertThrows(IllegalArgumentException.class, () -> v.angle(Vector2D.NaN));
604         Assertions.assertThrows(IllegalArgumentException.class, () -> v.angle(Vector2D.POSITIVE_INFINITY));
605         Assertions.assertThrows(IllegalArgumentException.class, () -> v.angle(Vector2D.NEGATIVE_INFINITY));
606     }
607 
608     @Test
609     void testSignedArea() {
610         // arrange
611         final double eps = 1e-10;
612 
613         final Vector2D a = Vector2D.Unit.PLUS_X;
614         final Vector2D b = Vector2D.Unit.PLUS_Y;
615         final Vector2D c = Vector2D.of(1, 1).withNorm(2.0);
616         final Vector2D d = Vector2D.of(-1, 1).withNorm(3.0);
617 
618         // act/assert
619         Assertions.assertEquals(1.0, a.signedArea(b), eps);
620         Assertions.assertEquals(-1.0, b.signedArea(a), eps);
621 
622         final double xAxisAndCArea = 2 * Math.cos(0.25 * Math.PI);
623         Assertions.assertEquals(xAxisAndCArea, a.signedArea(c), eps);
624         Assertions.assertEquals(-xAxisAndCArea, c.signedArea(a), eps);
625 
626         final double xAxisAndDArea = 3 * Math.cos(0.25 * Math.PI);
627         Assertions.assertEquals(xAxisAndDArea, a.signedArea(d), eps);
628         Assertions.assertEquals(-xAxisAndDArea, d.signedArea(a), eps);
629 
630         Assertions.assertEquals(6.0, c.signedArea(d), eps);
631         Assertions.assertEquals(-6.0, d.signedArea(c), eps);
632     }
633 
634     @Test
635     void testSignedArea_collinear() {
636         // arrange
637         final Vector2D a = Vector2D.Unit.PLUS_X;
638         final Vector2D b = Vector2D.Unit.PLUS_Y;
639         final Vector2D c = Vector2D.of(-3, 8);
640 
641         // act/assert
642         Assertions.assertEquals(0.0, a.signedArea(a), EPS);
643         Assertions.assertEquals(0.0, b.signedArea(b), EPS);
644         Assertions.assertEquals(0.0, c.signedArea(c), EPS);
645 
646         Assertions.assertEquals(0.0, a.signedArea(a.multiply(100.0)), EPS);
647         Assertions.assertEquals(0.0, b.signedArea(b.negate()), EPS);
648         Assertions.assertEquals(0.0, c.signedArea(c.multiply(-0.03)), EPS);
649     }
650 
651     @Test
652     void testProject() {
653         // arrange
654         final Vector2D v1 = Vector2D.of(3.0, 4.0);
655         final Vector2D v2 = Vector2D.of(1.0, 4.0);
656 
657         // act/assert
658         checkVector(Vector2D.ZERO.project(v1), 0.0, 0.0);
659 
660         checkVector(v1.project(v1), 3.0, 4.0);
661         checkVector(v1.project(v1.negate()), 3.0, 4.0);
662 
663         checkVector(v1.project(Vector2D.Unit.PLUS_X), 3.0, 0.0);
664         checkVector(v1.project(Vector2D.Unit.MINUS_X), 3.0, 0.0);
665 
666         checkVector(v1.project(Vector2D.Unit.PLUS_Y), 0.0, 4.0);
667         checkVector(v1.project(Vector2D.Unit.MINUS_Y), 0.0, 4.0);
668 
669         checkVector(v2.project(v1), (19.0 / 25.0) * 3.0, (19.0 / 25.0) * 4.0);
670     }
671 
672     @Test
673     void testProject_baseHasIllegalNorm() {
674         // arrange
675         final Vector2D v = Vector2D.of(1.0, 1.0);
676 
677         // act/assert
678         Assertions.assertThrows(IllegalArgumentException.class, () -> v.project(Vector2D.ZERO));
679         Assertions.assertThrows(IllegalArgumentException.class, () -> v.project(Vector2D.NaN));
680         Assertions.assertThrows(IllegalArgumentException.class, () -> v.project(Vector2D.POSITIVE_INFINITY));
681         Assertions.assertThrows(IllegalArgumentException.class, () -> v.project(Vector2D.NEGATIVE_INFINITY));
682     }
683 
684     @Test
685     void testReject() {
686         // arrange
687         final Vector2D v1 = Vector2D.of(3.0, 4.0);
688         final Vector2D v2 = Vector2D.of(1.0, 4.0);
689 
690         // act/assert
691         checkVector(Vector2D.ZERO.reject(v1), 0.0, 0.0);
692 
693         checkVector(v1.reject(v1), 0.0, 0.0);
694         checkVector(v1.reject(v1.negate()), 0.0, 0.0);
695 
696         checkVector(v1.reject(Vector2D.Unit.PLUS_X), 0.0, 4.0);
697         checkVector(v1.reject(Vector2D.Unit.MINUS_X), 0.0, 4.0);
698 
699         checkVector(v1.reject(Vector2D.Unit.PLUS_Y), 3.0, 0.0);
700         checkVector(v1.reject(Vector2D.Unit.MINUS_Y), 3.0, 0.0);
701 
702         checkVector(v2.reject(v1), -32.0 / 25.0, (6.0 / 25.0) * 4.0);
703     }
704 
705     @Test
706     void testReject_baseHasIllegalNorm() {
707         // arrange
708         final Vector2D v = Vector2D.of(1.0, 1.0);
709 
710         // act/assert
711         Assertions.assertThrows(IllegalArgumentException.class, () -> v.reject(Vector2D.ZERO));
712         Assertions.assertThrows(IllegalArgumentException.class, () -> v.reject(Vector2D.NaN));
713         Assertions.assertThrows(IllegalArgumentException.class, () -> v.reject(Vector2D.POSITIVE_INFINITY));
714         Assertions.assertThrows(IllegalArgumentException.class, () -> v.reject(Vector2D.NEGATIVE_INFINITY));
715     }
716 
717     @Test
718     void testProjectAndReject_areComplementary() {
719         // arrange
720         final double eps = 1e-12;
721 
722         // act/assert
723         checkProjectAndRejectFullCircle(Vector2D.of(1.0, 0.0), 1.0, eps);
724         checkProjectAndRejectFullCircle(Vector2D.of(0.0, 1.0), 2.0, eps);
725         checkProjectAndRejectFullCircle(Vector2D.of(1.0, 1.0), 3.0, eps);
726 
727         checkProjectAndRejectFullCircle(Vector2D.of(-2.0, 0.0), 4.0, eps);
728         checkProjectAndRejectFullCircle(Vector2D.of(0.0, -2.0), 5.0, eps);
729         checkProjectAndRejectFullCircle(Vector2D.of(-2.0, -2.0), 6.0, eps);
730     }
731 
732     private void checkProjectAndRejectFullCircle(final Vector2D vec, final double baseMag, final double eps) {
733         for (double theta = 0.0; theta <= Angle.TWO_PI; theta += 0.5) {
734             final Vector2D base = PolarCoordinates.toCartesian(baseMag, theta);
735 
736             final Vector2D proj = vec.project(base);
737             final Vector2D rej = vec.reject(base);
738 
739             // ensure that the projection and rejection sum to the original vector
740             EuclideanTestUtils.assertCoordinatesEqual(vec, proj.add(rej), eps);
741 
742             final double angle = base.angle(vec);
743 
744             // check the angle between the projection and the base; this will
745             // be undefined when the angle between the original vector and the
746             // base is pi/2 (which means that the projection is the zero vector)
747             if (angle < Angle.PI_OVER_TWO) {
748                 Assertions.assertEquals(0.0, proj.angle(base), eps);
749             } else if (angle > Angle.PI_OVER_TWO) {
750                 Assertions.assertEquals(Math.PI, proj.angle(base), eps);
751             }
752 
753             // check the angle between the rejection and the base; this should
754             // always be pi/2 except for when the angle between the original vector
755             // and the base is 0 or pi, in which case the rejection is the zero vector.
756             if (angle > 0.0 && angle < Math.PI) {
757                 Assertions.assertEquals(Angle.PI_OVER_TWO, rej.angle(base), eps);
758             }
759         }
760     }
761 
762     @Test
763     void testVectorTo() {
764         // arrange
765         final Vector2D p1 = Vector2D.of(1, 1);
766         final Vector2D p2 = Vector2D.of(4, 5);
767         final Vector2D p3 = Vector2D.of(-1, 0);
768 
769         // act/assert
770         checkVector(p1.vectorTo(p1), 0, 0);
771         checkVector(p1.vectorTo(p2), 3, 4);
772         checkVector(p2.vectorTo(p1), -3, -4);
773 
774         checkVector(p1.vectorTo(p3), -2, -1);
775         checkVector(p3.vectorTo(p1), 2, 1);
776     }
777 
778     @Test
779     void testDirectionTo() {
780         // act/assert
781         final double invSqrt2 = 1.0 / Math.sqrt(2);
782 
783         final Vector2D p1 = Vector2D.of(1, 1);
784         final Vector2D p2 = Vector2D.of(1, 5);
785         final Vector2D p3 = Vector2D.of(-2, -2);
786 
787         // act/assert
788         checkVector(p1.directionTo(p2), 0, 1);
789         checkVector(p2.directionTo(p1), 0, -1);
790 
791         checkVector(p1.directionTo(p3), -invSqrt2, -invSqrt2);
792         checkVector(p3.directionTo(p1), invSqrt2, invSqrt2);
793     }
794 
795     @Test
796     void testDirectionTo_illegalNorm() {
797         // arrange
798         final Vector2D p = Vector2D.of(1, 2);
799 
800         // act/assert
801         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.ZERO.directionTo(Vector2D.ZERO));
802         Assertions.assertThrows(IllegalArgumentException.class, () -> p.directionTo(p));
803         Assertions.assertThrows(IllegalArgumentException.class, () -> p.directionTo(Vector2D.NaN));
804         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.NEGATIVE_INFINITY.directionTo(p));
805         Assertions.assertThrows(IllegalArgumentException.class, () -> p.directionTo(Vector2D.POSITIVE_INFINITY));
806     }
807 
808     @Test
809     void testLerp() {
810         // arrange
811         final Vector2D v1 = Vector2D.of(1, -5);
812         final Vector2D v2 = Vector2D.of(-4, 0);
813         final Vector2D v3 = Vector2D.of(10, -4);
814 
815         // act/assert
816         checkVector(v1.lerp(v1, 0), 1, -5);
817         checkVector(v1.lerp(v1, 1), 1, -5);
818 
819         checkVector(v1.lerp(v2, -0.25), 2.25, -6.25);
820         checkVector(v1.lerp(v2, 0), 1, -5);
821         checkVector(v1.lerp(v2, 0.25), -0.25, -3.75);
822         checkVector(v1.lerp(v2, 0.5), -1.5, -2.5);
823         checkVector(v1.lerp(v2, 0.75), -2.75, -1.25);
824         checkVector(v1.lerp(v2, 1), -4, 0);
825         checkVector(v1.lerp(v2, 1.25), -5.25, 1.25);
826 
827         checkVector(v1.lerp(v3, 0), 1, -5);
828         checkVector(v1.lerp(v3, 0.25), 3.25, -4.75);
829         checkVector(v1.lerp(v3, 0.5), 5.5, -4.5);
830         checkVector(v1.lerp(v3, 0.75), 7.75, -4.25);
831         checkVector(v1.lerp(v3, 1), 10, -4);
832     }
833 
834     @Test
835     void testTransform() {
836         // arrange
837         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
838                 .scale(2)
839                 .translate(1, 2);
840 
841         final Vector2D v1 = Vector2D.of(1, 2);
842         final Vector2D v2 = Vector2D.of(-4, -5);
843 
844         // act/assert
845         checkVector(v1.transform(transform), 3, 6);
846         checkVector(v2.transform(transform), -7, -8);
847     }
848 
849     @Test
850     void testPrecisionEquals() {
851         // arrange
852         final Precision.DoubleEquivalence smallEps = Precision.doubleEquivalenceOfEpsilon(1e-6);
853         final Precision.DoubleEquivalence largeEps = Precision.doubleEquivalenceOfEpsilon(1e-1);
854 
855         final Vector2D vec = Vector2D.of(1, -2);
856 
857         // act/assert
858         Assertions.assertTrue(vec.eq(vec, smallEps));
859         Assertions.assertTrue(vec.eq(vec, largeEps));
860 
861         Assertions.assertTrue(vec.eq(Vector2D.of(1.0000007, -2.0000009), smallEps));
862         Assertions.assertTrue(vec.eq(Vector2D.of(1.0000007, -2.0000009), largeEps));
863 
864         Assertions.assertFalse(vec.eq(Vector2D.of(1.004, -2), smallEps));
865         Assertions.assertFalse(vec.eq(Vector2D.of(1, -2.004), smallEps));
866         Assertions.assertTrue(vec.eq(Vector2D.of(1.004, -2.004), largeEps));
867 
868         Assertions.assertFalse(vec.eq(Vector2D.of(1, -3), smallEps));
869         Assertions.assertFalse(vec.eq(Vector2D.of(2, -2), smallEps));
870         Assertions.assertFalse(vec.eq(Vector2D.of(1, -3), largeEps));
871         Assertions.assertFalse(vec.eq(Vector2D.of(2, -2), largeEps));
872     }
873 
874     @Test
875     void testIsZero() {
876         // arrange
877         final Precision.DoubleEquivalence smallEps = Precision.doubleEquivalenceOfEpsilon(1e-6);
878         final Precision.DoubleEquivalence largeEps = Precision.doubleEquivalenceOfEpsilon(1e-1);
879 
880         // act/assert
881         Assertions.assertTrue(Vector2D.of(0.0, -0.0).isZero(smallEps));
882         Assertions.assertTrue(Vector2D.of(-0.0, 0.0).isZero(largeEps));
883 
884         Assertions.assertTrue(Vector2D.of(-1e-7, 1e-7).isZero(smallEps));
885         Assertions.assertTrue(Vector2D.of(1e-7, 1e-7).isZero(largeEps));
886 
887         Assertions.assertFalse(Vector2D.of(1e-2, 0.0).isZero(smallEps));
888         Assertions.assertFalse(Vector2D.of(0.0, 1e-2).isZero(smallEps));
889         Assertions.assertTrue(Vector2D.of(1e-2, -1e-2).isZero(largeEps));
890 
891         Assertions.assertFalse(Vector2D.of(0.2, 0.0).isZero(smallEps));
892         Assertions.assertFalse(Vector2D.of(0.0, 0.2).isZero(smallEps));
893         Assertions.assertFalse(Vector2D.of(0.2, 0.2).isZero(smallEps));
894         Assertions.assertFalse(Vector2D.of(-0.2, 0.0).isZero(largeEps));
895         Assertions.assertFalse(Vector2D.of(0.0, -0.2).isZero(largeEps));
896         Assertions.assertFalse(Vector2D.of(-0.2, -0.2).isZero(largeEps));
897     }
898 
899     @Test
900     void testHashCode() {
901         // arrange
902         final Vector2D u = Vector2D.of(1, 1);
903         final Vector2D v = Vector2D.of(1 + 10 * Precision.EPSILON, 1 + 10 * Precision.EPSILON);
904         final Vector2D w = Vector2D.of(1, 1);
905 
906         // act/assert
907         Assertions.assertTrue(u.hashCode() != v.hashCode());
908         Assertions.assertEquals(u.hashCode(), w.hashCode());
909 
910         Assertions.assertEquals(Vector2D.of(0, Double.NaN).hashCode(), Vector2D.NaN.hashCode());
911         Assertions.assertEquals(Vector2D.of(Double.NaN, 0).hashCode(), Vector2D.NaN.hashCode());
912         Assertions.assertEquals(Vector2D.of(0, Double.NaN).hashCode(), Vector2D.of(Double.NaN, 0).hashCode());
913     }
914 
915     @Test
916     void testEquals() {
917         // arrange
918         final Vector2D u1 = Vector2D.of(1, 2);
919         final Vector2D u2 = Vector2D.of(1, 2);
920 
921         // act/assert
922         GeometryTestUtils.assertSimpleEqualsCases(u1);
923         Assertions.assertEquals(u1, u2);
924 
925         Assertions.assertNotEquals(u1, Vector2D.of(-1, -2));
926         Assertions.assertNotEquals(u1, Vector2D.of(1 + 10 * Precision.EPSILON, 2));
927         Assertions.assertNotEquals(u1, Vector2D.of(1, 2 + 10 * Precision.EPSILON));
928 
929         Assertions.assertEquals(Vector2D.of(0, Double.NaN), Vector2D.of(Double.NaN, 0));
930 
931         Assertions.assertEquals(Vector2D.of(0, Double.POSITIVE_INFINITY), Vector2D.of(0, Double.POSITIVE_INFINITY));
932         Assertions.assertNotEquals(Vector2D.of(Double.POSITIVE_INFINITY, 0), Vector2D.of(0, Double.POSITIVE_INFINITY));
933 
934         Assertions.assertEquals(Vector2D.of(Double.NEGATIVE_INFINITY, 0), Vector2D.of(Double.NEGATIVE_INFINITY, 0));
935         Assertions.assertNotEquals(Vector2D.of(0, Double.NEGATIVE_INFINITY), Vector2D.of(Double.NEGATIVE_INFINITY, 0));
936     }
937 
938     @Test
939     void testEqualsAndHashCode_signedZeroConsistency() {
940         // arrange
941         final Vector2D a = Vector2D.of(0.0, 0.0);
942         final Vector2D b = Vector2D.of(-0.0, -0.0);
943         final Vector2D c = Vector2D.of(0.0, 0.0);
944         final Vector2D d = Vector2D.of(-0.0, -0.0);
945 
946         // act/assert
947         Assertions.assertFalse(a.equals(b));
948 
949         Assertions.assertTrue(a.equals(c));
950         Assertions.assertEquals(a.hashCode(), c.hashCode());
951 
952         Assertions.assertTrue(b.equals(d));
953         Assertions.assertEquals(b.hashCode(), d.hashCode());
954     }
955 
956     @Test
957     void testToString() {
958         // arrange
959         final Vector2D v = Vector2D.of(1, 2);
960         final Pattern pattern = Pattern.compile("\\(1.{0,2}, 2.{0,2}\\)");
961 
962         // act
963         final String str = v.toString();
964 
965         // assert
966         Assertions.assertTrue(pattern.matcher(str).matches(), "Expected string " + str + " to match regex " + pattern);
967     }
968 
969     @Test
970     void testParse() {
971         // act/assert
972         checkVector(Vector2D.parse("(1, 2)"), 1, 2);
973         checkVector(Vector2D.parse("(-1, -2)"), -1, -2);
974 
975         checkVector(Vector2D.parse("(0.01, -1e-3)"), 1e-2, -1e-3);
976 
977         checkVector(Vector2D.parse("(NaN, -Infinity)"), Double.NaN, Double.NEGATIVE_INFINITY);
978 
979         checkVector(Vector2D.parse(Vector2D.ZERO.toString()), 0, 0);
980         checkVector(Vector2D.parse(Vector2D.Unit.MINUS_X.toString()), -1, 0);
981     }
982 
983     @Test
984     void testParse_failure() {
985         // act/assert
986         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.parse("abc"));
987     }
988 
989     @Test
990     void testOf() {
991         // act/assert
992         checkVector(Vector2D.of(0, 1), 0, 1);
993         checkVector(Vector2D.of(-1, -2), -1, -2);
994         checkVector(Vector2D.of(Math.PI, Double.NaN), Math.PI, Double.NaN);
995         checkVector(Vector2D.of(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY), Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY);
996     }
997 
998     @Test
999     void testOf_arrayArg() {
1000         // act/assert
1001         checkVector(Vector2D.of(new double[] {0, 1}), 0, 1);
1002         checkVector(Vector2D.of(new double[] {-1, -2}), -1, -2);
1003         checkVector(Vector2D.of(new double[] {Math.PI, Double.NaN}), Math.PI, Double.NaN);
1004         checkVector(Vector2D.of(new double[] {Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}), Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY);
1005     }
1006 
1007     @Test
1008     void testOf_arrayArg_invalidDimensions() {
1009         // act/assert
1010         Assertions.assertThrows(IllegalArgumentException.class,     () -> Vector2D.of(new double[] {0.0}));
1011     }
1012 
1013     @Test
1014     void testUnitFrom_coordinates() {
1015         // arrange
1016         final double invSqrt2 = 1.0 / Math.sqrt(2.0);
1017 
1018         // act/assert
1019         checkVector(Vector2D.Unit.from(2.0, -2.0), invSqrt2, -invSqrt2);
1020         checkVector(Vector2D.Unit.from(-4.0, 4.0), -invSqrt2, invSqrt2);
1021     }
1022 
1023     @Test
1024     void testUnitFrom_vector() {
1025         // arrange
1026         final double invSqrt2 = 1.0 / Math.sqrt(2.0);
1027         final Vector2D vec = Vector2D.of(2.0, -2.0);
1028         final Vector2D.Unit unitVec = Vector2D.Unit.from(2.0, -2.0);
1029 
1030         // act/assert
1031         checkVector(Vector2D.Unit.from(vec), invSqrt2, -invSqrt2);
1032         Assertions.assertSame(unitVec, Vector2D.Unit.from(unitVec));
1033     }
1034 
1035     @Test
1036     void testUnitFrom_illegalNorm() {
1037 
1038         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.Unit.from(0.0, 0.0));
1039         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.Unit.from(Double.NaN, 1.0));
1040         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.Unit.from(1.0, Double.NEGATIVE_INFINITY));
1041         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector2D.Unit.from(1.0, Double.POSITIVE_INFINITY));
1042     }
1043 
1044     @Test
1045     void testMax() {
1046         // act/assert
1047         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-100, 1),
1048                 Vector2D.max(Collections.singletonList(Vector2D.of(-100, 1))), EPS);
1049 
1050         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 1),
1051                 Vector2D.max(Arrays.asList(Vector2D.of(-100, 1), Vector2D.of(0, 1))), EPS);
1052 
1053         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 0),
1054                 Vector2D.max(Vector2D.of(-2, 0), Vector2D.of(-1, -5), Vector2D.of(-10, -10)), EPS);
1055     }
1056 
1057     @Test
1058     void testMax_noPointsGiven() {
1059         // arrange
1060         final String msg = "Cannot compute vector max: no vectors given";
1061 
1062         // act/assert
1063         GeometryTestUtils.assertThrowsWithMessage(() -> {
1064             Vector2D.max(new ArrayList<>());
1065         }, IllegalArgumentException.class, msg);
1066     }
1067 
1068     @Test
1069     void testMin() {
1070         // act/assert
1071         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-100, 1),
1072                 Vector2D.min(Collections.singletonList(Vector2D.of(-100, 1))), EPS);
1073 
1074         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-100, 1),
1075                 Vector2D.min(Arrays.asList(Vector2D.of(-100, 1), Vector2D.of(0, 1))), EPS);
1076 
1077         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-10, -10),
1078                 Vector2D.min(Vector2D.of(-2, 0), Vector2D.of(-1, -5), Vector2D.of(-10, -10)), EPS);
1079     }
1080 
1081     @Test
1082     void testMin_noPointsGiven() {
1083         // arrange
1084         final String msg = "Cannot compute vector min: no vectors given";
1085 
1086         // act/assert
1087         GeometryTestUtils.assertThrowsWithMessage(() -> {
1088             Vector2D.min(new ArrayList<>());
1089         }, IllegalArgumentException.class, msg);
1090     }
1091 
1092     @Test
1093     void testCentroid() {
1094         // act/assert
1095         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2),
1096                 Vector2D.centroid(Vector2D.of(1, 2)), EPS);
1097 
1098         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2.5, 3.5),
1099                 Vector2D.centroid(Vector2D.of(1, 2), Vector2D.of(2, 3),
1100                         Vector2D.of(3, 4), Vector2D.of(4, 5)), EPS);
1101 
1102         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2),
1103                 Vector2D.centroid(Collections.singletonList(Vector2D.of(1, 2))), EPS);
1104 
1105         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 1),
1106                 Vector2D.centroid(Arrays.asList(Vector2D.of(1, 2), Vector2D.of(1, 2),
1107                         Vector2D.ZERO, Vector2D.ZERO)), EPS);
1108     }
1109 
1110     @Test
1111     void testCentroid_noPointsGiven() {
1112         // arrange
1113         final String msg = "Cannot compute centroid: no points given";
1114 
1115         // act/assert
1116         GeometryTestUtils.assertThrowsWithMessage(() -> {
1117             Vector2D.centroid(new ArrayList<>());
1118         }, IllegalArgumentException.class, msg);
1119     }
1120 
1121     @Test
1122     void testSum_factoryMethods() {
1123         // act/assert
1124         checkVector(Vector2D.Sum.create().get(), 0, 0);
1125         checkVector(Vector2D.Sum.of(Vector2D.of(1, 2)).get(), 1, 2);
1126         checkVector(Vector2D.Sum.of(
1127                 Vector2D.of(1, 2),
1128                 Vector2D.Unit.PLUS_X,
1129                 Vector2D.Unit.PLUS_Y).get(), 2, 3);
1130     }
1131 
1132     @Test
1133     void testSum_instanceMethods() {
1134         // arrange
1135         final Vector2D p1 = Vector2D.of(1, 2);
1136         final Vector2D p2 = Vector2D.of(4, 6);
1137 
1138         // act/assert
1139         checkVector(Vector2D.Sum.create()
1140                 .add(p1)
1141                 .addScaled(0.5, p2)
1142                 .get(), 3, 5);
1143     }
1144 
1145     @Test
1146     void testSum_accept() {
1147         // arrange
1148         final Vector2D p1 = Vector2D.of(1, 2);
1149         final Vector2D p2 = Vector2D.of(3, -6);
1150 
1151         final List<Vector2D.Unit> units = Arrays.asList(
1152                 Vector2D.Unit.PLUS_X,
1153                 Vector2D.Unit.PLUS_Y);
1154 
1155         final Vector2D.Sum s = Vector2D.Sum.create();
1156 
1157         // act/assert
1158         Arrays.asList(p1, Vector2D.ZERO, p2).forEach(s);
1159         units.forEach(s);
1160 
1161         // assert
1162         checkVector(s.get(), 5, -3);
1163     }
1164 
1165     @Test
1166     void testUnitFactoryOptimization() {
1167         // An already normalized vector will avoid unnecessary creation.
1168         final Vector2D v = Vector2D.of(4, 5).normalize();
1169         Assertions.assertSame(v, v.normalize());
1170     }
1171 
1172     private void checkVector(final Vector2D v, final double x, final double y) {
1173         checkVector(v, x, y, EPS);
1174     }
1175 
1176     private void checkVector(final Vector2D v, final double x, final double y, final double eps) {
1177         Assertions.assertEquals(x, v.getX(), eps);
1178         Assertions.assertEquals(y, v.getY(), eps);
1179     }
1180 }