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.function.UnaryOperator;
20  
21  import org.apache.commons.geometry.core.GeometryTestUtils;
22  import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
23  import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
24  import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
25  import org.apache.commons.geometry.euclidean.twod.rotation.Rotation2D;
26  import org.apache.commons.numbers.angle.PlaneAngleRadians;
27  import org.junit.Assert;
28  import org.junit.Test;
29  
30  public class AffineTransformMatrix2DTest {
31  
32      private static final double EPS = 1e-12;
33  
34      private static final DoublePrecisionContext TEST_PRECISION =
35              new EpsilonDoublePrecisionContext(EPS);
36  
37      @Test
38      public void testOf() {
39          // arrange
40          final double[] arr = {
41              1, 2, 3,
42              4, 5, 6
43          };
44  
45          // act
46          final AffineTransformMatrix2D transform = AffineTransformMatrix2D.of(arr);
47  
48          // assert
49          final double[] result = transform.toArray();
50          Assert.assertNotSame(arr, result);
51          Assert.assertArrayEquals(arr, result, 0.0);
52      }
53  
54      @Test
55      public void testOf_invalidDimensions() {
56          // act/assert
57          GeometryTestUtils.assertThrows(() -> AffineTransformMatrix2D.of(1, 2),
58                  IllegalArgumentException.class, "Dimension mismatch: 2 != 6");
59      }
60  
61      @Test
62      public void testFromColumnVectors_twoVector() {
63          // arrange
64          final Vector2D u = Vector2D.of(1, 2);
65          final Vector2D v = Vector2D.of(3, 4);
66  
67          // act
68          final AffineTransformMatrix2D transform = AffineTransformMatrix2D.fromColumnVectors(u, v);
69  
70          // assert
71          Assert.assertArrayEquals(new double[] {
72              1, 3, 0,
73              2, 4, 0
74          }, transform.toArray(), 0.0);
75      }
76  
77      @Test
78      public void testFromColumnVectors_threeVectors() {
79          // arrange
80          final Vector2D u = Vector2D.of(1, 2);
81          final Vector2D v = Vector2D.of(3, 4);
82          final Vector2D t = Vector2D.of(5, 6);
83  
84          // act
85          final AffineTransformMatrix2D transform = AffineTransformMatrix2D.fromColumnVectors(u, v, t);
86  
87          // assert
88          Assert.assertArrayEquals(new double[] {
89              1, 3, 5,
90              2, 4, 6
91          }, transform.toArray(), 0.0);
92      }
93  
94      @Test
95      public void testIdentity() {
96          // act
97          final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity();
98  
99          // assert
100         final double[] expected = {
101             1, 0, 0,
102             0, 1, 0
103         };
104         Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
105     }
106 
107     @Test
108     public void testFrom() {
109         // act/assert
110         Assert.assertArrayEquals(new double[] {
111             1, 0, 0,
112             0, 1, 0
113         }, AffineTransformMatrix2D.from(UnaryOperator.identity()).toArray(), EPS);
114         Assert.assertArrayEquals(new double[] {
115             1, 0, 2,
116             0, 1, 3
117         }, AffineTransformMatrix2D.from(v -> v.add(Vector2D.of(2, 3))).toArray(), EPS);
118         Assert.assertArrayEquals(new double[] {
119             3, 0, 0,
120             0, 3, 0
121         }, AffineTransformMatrix2D.from(v -> v.multiply(3)).toArray(), EPS);
122         Assert.assertArrayEquals(new double[] {
123             3, 0, 6,
124             0, 3, 9
125         }, AffineTransformMatrix2D.from(v -> v.add(Vector2D.of(2, 3)).multiply(3)).toArray(), EPS);
126     }
127 
128     @Test
129     public void testFrom_invalidFunction() {
130         // act/assert
131         GeometryTestUtils.assertThrows(() -> {
132             AffineTransformMatrix2D.from(v -> v.multiply(0));
133         }, IllegalArgumentException.class);
134     }
135 
136     @Test
137     public void testCreateTranslation_xy() {
138         // act
139         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createTranslation(2, 3);
140 
141         // assert
142         final double[] expected = {
143             1, 0, 2,
144             0, 1, 3
145         };
146         Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
147     }
148 
149     @Test
150     public void testCreateTranslation_vector() {
151         // act
152         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createTranslation(Vector2D.of(5, 6));
153 
154         // assert
155         final double[] expected = {
156             1, 0, 5,
157             0, 1, 6
158         };
159         Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
160     }
161 
162     @Test
163     public void testCreateScale_xy() {
164         // act
165         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(2, 3);
166 
167         // assert
168         final double[] expected = {
169             2, 0, 0,
170             0, 3, 0
171         };
172         Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
173     }
174 
175     @Test
176     public void testTranslate_xy() {
177         // arrange
178         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
179                     2, 0, 10,
180                     0, 3, 11
181                 );
182 
183         // act
184         final AffineTransformMatrix2D result = a.translate(4, 5);
185 
186         // assert
187         final double[] expected = {
188             2, 0, 14,
189             0, 3, 16
190         };
191         Assert.assertArrayEquals(expected, result.toArray(), 0.0);
192     }
193 
194     @Test
195     public void testTranslate_vector() {
196         // arrange
197         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
198                     2, 0, 10,
199                     0, 3, 11
200                 );
201 
202         // act
203         final AffineTransformMatrix2D result = a.translate(Vector2D.of(7, 8));
204 
205         // assert
206         final double[] expected = {
207             2, 0, 17,
208             0, 3, 19
209         };
210         Assert.assertArrayEquals(expected, result.toArray(), 0.0);
211     }
212 
213     @Test
214     public void testCreateScale_vector() {
215         // act
216         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(Vector2D.of(4, 5));
217 
218         // assert
219         final double[] expected = {
220             4, 0, 0,
221             0, 5, 0
222         };
223         Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
224     }
225 
226     @Test
227     public void testCreateScale_singleValue() {
228         // act
229         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(7);
230 
231         // assert
232         final double[] expected = {
233             7, 0, 0,
234             0, 7, 0
235         };
236         Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
237     }
238 
239     @Test
240     public void testScale_xy() {
241         // arrange
242         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
243                     2, 0, 10,
244                     0, 3, 11
245                 );
246 
247         // act
248         final AffineTransformMatrix2D result = a.scale(4, 5);
249 
250         // assert
251         final double[] expected = {
252             8, 0, 40,
253             0, 15, 55
254         };
255         Assert.assertArrayEquals(expected, result.toArray(), 0.0);
256     }
257 
258     @Test
259     public void testScale_vector() {
260         // arrange
261         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
262                     2, 0, 10,
263                     0, 3, 11
264                 );
265 
266         // act
267         final AffineTransformMatrix2D result = a.scale(Vector2D.of(7, 8));
268 
269         // assert
270         final double[] expected = {
271             14, 0, 70,
272             0, 24, 88
273         };
274         Assert.assertArrayEquals(expected, result.toArray(), 0.0);
275     }
276 
277     @Test
278     public void testScale_singleValue() {
279         // arrange
280         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
281                     2, 0, 10,
282                     0, 3, 11
283                 );
284 
285         // act
286         final AffineTransformMatrix2D result = a.scale(10);
287 
288         // assert
289         final double[] expected = {
290             20, 0, 100,
291             0, 30, 110
292         };
293         Assert.assertArrayEquals(expected, result.toArray(), 0.0);
294     }
295 
296     @Test
297     public void testCreateRotation() {
298         // act
299         final double angle = PlaneAngleRadians.PI * 2.0 / 3.0;
300         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createRotation(angle);
301 
302         // assert
303         final double sin = Math.sin(angle);
304         final double cos = Math.cos(angle);
305 
306         final double[] expected = {
307             cos, -sin, 0,
308             sin, cos, 0
309         };
310         Assert.assertArrayEquals(expected, transform.toArray(), EPS);
311     }
312 
313     @Test
314     public void testCreateRotation_aroundCenter_rawAngle() {
315         // act
316         final Vector2D center = Vector2D.of(1, 2);
317         final double angle = PlaneAngleRadians.PI * 2.0 / 3.0;
318         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createRotation(center, angle);
319 
320         // assert
321         final double sin = Math.sin(angle);
322         final double cos = Math.cos(angle);
323 
324         final double[] expected = {
325             cos, -sin, -cos + (2 * sin) + 1,
326             sin, cos, -sin - (2 * cos) + 2
327         };
328         Assert.assertArrayEquals(expected, transform.toArray(), EPS);
329     }
330 
331     @Test
332     public void testCreateRotation_aroundCenter_rotationInstance() {
333         // act
334         final Vector2D center = Vector2D.of(1, 2);
335         final double angle = PlaneAngleRadians.PI * 4.0 / 3.0;
336         final Rotation2D rotation = Rotation2D.of(angle);
337         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createRotation(center, rotation);
338 
339         // assert
340         final double sin = Math.sin(angle);
341         final double cos = Math.cos(angle);
342 
343         final double[] expected = {
344             cos, -sin, -cos + (2 * sin) + 1,
345             sin, cos, -sin - (2 * cos) + 2
346         };
347         Assert.assertArrayEquals(expected, transform.toArray(), EPS);
348     }
349 
350     @Test
351     public void testRotate_rawAngle() {
352         // arrange
353         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
354                     1, 2, 3,
355                     4, 5, 6
356                 );
357 
358         // act
359         final AffineTransformMatrix2D result = a.rotate(PlaneAngleRadians.PI_OVER_TWO);
360 
361         // assert
362         final double[] expected = {
363             -4, -5, -6,
364             1, 2, 3
365         };
366         Assert.assertArrayEquals(expected, result.toArray(), EPS);
367     }
368 
369     @Test
370     public void testRotate_rotationInstance() {
371         // arrange
372         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
373                     1, 2, 3,
374                     4, 5, 6
375                 );
376 
377         // act
378         final AffineTransformMatrix2D result = a.rotate(Rotation2D.of(PlaneAngleRadians.PI_OVER_TWO));
379 
380         // assert
381         final double[] expected = {
382             -4, -5, -6,
383             1, 2, 3
384         };
385         Assert.assertArrayEquals(expected, result.toArray(), EPS);
386     }
387 
388     @Test
389     public void testRotate_aroundCenter_rawAngle() {
390         // arrange
391         final Vector2D center = Vector2D.of(1, 2);
392 
393         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
394                     1, 2, 3,
395                     4, 5, 6
396                 );
397 
398         // act
399         final AffineTransformMatrix2D result = a.rotate(center, PlaneAngleRadians.PI_OVER_TWO);
400 
401         // assert
402         final double[] expected = {
403             -4, -5, -3,
404             1, 2, 4
405         };
406         Assert.assertArrayEquals(expected, result.toArray(), EPS);
407     }
408 
409     @Test
410     public void testRotate_aroundCenter_rotationInstance() {
411         // arrange
412         final Vector2D center = Vector2D.of(1, 2);
413 
414         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
415                     1, 2, 3,
416                     4, 5, 6
417                 );
418 
419         // act
420         final AffineTransformMatrix2D result = a.rotate(center, Rotation2D.of(PlaneAngleRadians.PI_OVER_TWO));
421 
422         // assert
423         final double[] expected = {
424             -4, -5, -3,
425             1, 2, 4
426         };
427         Assert.assertArrayEquals(expected, result.toArray(), EPS);
428     }
429 
430     @Test
431     public void testApply_identity() {
432         // arrange
433         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity();
434 
435         // act/assert
436         runWithCoordinates((x, y) -> {
437             final Vector2D v = Vector2D.of(x, y);
438 
439             EuclideanTestUtils.assertCoordinatesEqual(v, transform.apply(v), EPS);
440         });
441     }
442 
443     @Test
444     public void testApply_translate() {
445         // arrange
446         final Vector2D translation = Vector2D.of(1.1, -PlaneAngleRadians.PI);
447 
448         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
449                 .translate(translation);
450 
451         // act/assert
452         runWithCoordinates((x, y) -> {
453             final Vector2D vec = Vector2D.of(x, y);
454 
455             final Vector2D expectedVec = vec.add(translation);
456 
457             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
458         });
459     }
460 
461     @Test
462     public void testApply_scale() {
463         // arrange
464         final Vector2D factors = Vector2D.of(2.0, -3.0);
465 
466         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
467                 .scale(factors);
468 
469         // act/assert
470         runWithCoordinates((x, y) -> {
471             final Vector2D vec = Vector2D.of(x, y);
472 
473             final Vector2D expectedVec = Vector2D.of(factors.getX() * x, factors.getY() * y);
474 
475             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
476         });
477     }
478 
479     @Test
480     public void testApply_rotate() {
481         // arrange
482         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
483                 .rotate(-PlaneAngleRadians.PI_OVER_TWO);
484 
485         // act/assert
486         runWithCoordinates((x, y) -> {
487             final Vector2D vec = Vector2D.of(x, y);
488 
489             final Vector2D expectedVec = Vector2D.of(y, -x);
490 
491             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
492         });
493     }
494 
495     @Test
496     public void testApply_rotate_aroundCenter_minusHalfPi() {
497         // arrange
498         final Vector2D center = Vector2D.of(1, 2);
499         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
500                 .rotate(center, -PlaneAngleRadians.PI_OVER_TWO);
501 
502         // act/assert
503         runWithCoordinates((x, y) -> {
504             final Vector2D vec = Vector2D.of(x, y);
505 
506             final Vector2D centered = vec.subtract(center);
507             final Vector2D expectedVec = Vector2D.of(centered.getY(), -centered.getX()).add(center);
508 
509             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
510         });
511     }
512 
513     @Test
514     public void testApply_rotate_aroundCenter_pi() {
515         // arrange
516         final Vector2D center = Vector2D.of(1, 2);
517         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
518                 .rotate(center, PlaneAngleRadians.PI);
519 
520         // act/assert
521         runWithCoordinates((x, y) -> {
522             final Vector2D vec = Vector2D.of(x, y);
523 
524             final Vector2D centered = vec.subtract(center);
525             final Vector2D expectedVec = Vector2D.of(-centered.getX(), -centered.getY()).add(center);
526 
527             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
528         });
529     }
530 
531     @Test
532     public void testApply_translateScaleRotate() {
533         // arrange
534         final Vector2D translation = Vector2D.of(-2.0, -3.0);
535         final Vector2D scale = Vector2D.of(5.0, 6.0);
536 
537         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
538                 .translate(translation)
539                 .scale(scale)
540                 .rotate(PlaneAngleRadians.PI_OVER_TWO);
541 
542         // act/assert
543         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(12, -5), transform.apply(Vector2D.of(1, 1)), EPS);
544 
545         runWithCoordinates((x, y) -> {
546             final Vector2D vec = Vector2D.of(x, y);
547 
548             final Vector2D temp = Vector2D.of(
549                         (x + translation.getX()) * scale.getX(),
550                         (y + translation.getY()) * scale.getY()
551                     );
552             final Vector2D expectedVec = Vector2D.of(-temp.getY(), temp.getX());
553 
554             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
555         });
556     }
557 
558     @Test
559     public void testApply_scaleTranslateRotate() {
560         // arrange
561         final Vector2D scale = Vector2D.of(5.0, 6.0);
562         final Vector2D translation = Vector2D.of(-2.0, -3.0);
563 
564         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
565                 .scale(scale)
566                 .translate(translation)
567                 .rotate(-PlaneAngleRadians.PI_OVER_TWO);
568 
569         // act/assert
570         runWithCoordinates((x, y) -> {
571             final Vector2D vec = Vector2D.of(x, y);
572 
573             final Vector2D temp = Vector2D.of(
574                         (x * scale.getX()) + translation.getX(),
575                         (y * scale.getY()) + translation.getY()
576                     );
577             final Vector2D expectedVec = Vector2D.of(temp.getY(), -temp.getX());
578 
579             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
580         });
581     }
582 
583     @Test
584     public void testApplyVector_identity() {
585         // arrange
586         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity();
587 
588         // act/assert
589         runWithCoordinates((x, y) -> {
590             final Vector2D v = Vector2D.of(x, y);
591 
592             EuclideanTestUtils.assertCoordinatesEqual(v, transform.applyVector(v), EPS);
593         });
594     }
595 
596     @Test
597     public void testApplyVector_translate() {
598         // arrange
599         final Vector2D translation = Vector2D.of(1.1, -PlaneAngleRadians.PI);
600 
601         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
602                 .translate(translation);
603 
604         // act/assert
605         runWithCoordinates((x, y) -> {
606             final Vector2D vec = Vector2D.of(x, y);
607 
608             EuclideanTestUtils.assertCoordinatesEqual(vec, transform.applyVector(vec), EPS);
609         });
610     }
611 
612     @Test
613     public void testApplyVector_scale() {
614         // arrange
615         final Vector2D factors = Vector2D.of(2.0, -3.0);
616 
617         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
618                 .scale(factors);
619 
620         // act/assert
621         runWithCoordinates((x, y) -> {
622             final Vector2D vec = Vector2D.of(x, y);
623 
624             final Vector2D expectedVec = Vector2D.of(factors.getX() * x, factors.getY() * y);
625 
626             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.applyVector(vec), EPS);
627         });
628     }
629 
630     @Test
631     public void testApplyVector_representsDisplacement() {
632         // arrange
633         final Vector2D p1 = Vector2D.of(2, 3);
634 
635         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
636                 .scale(1.5)
637                 .translate(4, 6)
638                 .rotate(PlaneAngleRadians.PI_OVER_TWO);
639 
640         // act/assert
641         runWithCoordinates((x, y) -> {
642             final Vector2D p2 = Vector2D.of(x, y);
643             final Vector2D input = p1.subtract(p2);
644 
645             final Vector2D expected = transform.apply(p1).subtract(transform.apply(p2));
646 
647             EuclideanTestUtils.assertCoordinatesEqual(expected, transform.applyVector(input), EPS);
648         });
649     }
650 
651     @Test
652     public void testApplyDirection_identity() {
653         // arrange
654         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity();
655 
656         // act/assert
657         EuclideanTestUtils.permuteSkipZero(-5, 5, 0.5, (x, y) -> {
658             final Vector2D v = Vector2D.of(x, y);
659 
660             EuclideanTestUtils.assertCoordinatesEqual(v.normalize(), transform.applyDirection(v), EPS);
661         });
662     }
663 
664     @Test
665     public void testApplyDirection_translate() {
666         // arrange
667         final Vector2D translation = Vector2D.of(1.1, -PlaneAngleRadians.PI);
668 
669         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
670                 .translate(translation);
671 
672         // act/assert
673         EuclideanTestUtils.permuteSkipZero(-5, 5, 0.5, (x, y) -> {
674             final Vector2D vec = Vector2D.of(x, y);
675 
676             EuclideanTestUtils.assertCoordinatesEqual(vec.normalize(), transform.applyDirection(vec), EPS);
677         });
678     }
679 
680     @Test
681     public void testApplyDirection_scale() {
682         // arrange
683         final Vector2D factors = Vector2D.of(2.0, -3.0);
684 
685         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
686                 .scale(factors);
687 
688         // act/assert
689         EuclideanTestUtils.permuteSkipZero(-5, 5, 0.5, (x, y) -> {
690             final Vector2D vec = Vector2D.of(x, y);
691 
692             final Vector2D expectedVec = Vector2D.of(factors.getX() * x, factors.getY() * y).normalize();
693 
694             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.applyDirection(vec), EPS);
695         });
696     }
697 
698     @Test
699     public void testApplyDirection_representsNormalizedDisplacement() {
700         // arrange
701         final Vector2D p1 = Vector2D.of(2.1, 3.2);
702 
703         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
704                 .scale(1.5)
705                 .translate(4, 6)
706                 .rotate(PlaneAngleRadians.PI_OVER_TWO);
707 
708         // act/assert
709         EuclideanTestUtils.permute(-5, 5, 0.5, (x, y) -> {
710             final Vector2D p2 = Vector2D.of(x, y);
711             final Vector2D input = p1.subtract(p2);
712 
713             final Vector2D expected = transform.apply(p1).subtract(transform.apply(p2)).normalize();
714 
715             EuclideanTestUtils.assertCoordinatesEqual(expected, transform.applyDirection(input), EPS);
716         });
717     }
718 
719     @Test
720     public void testApplyDirection_illegalNorm() {
721         // act/assert
722         GeometryTestUtils.assertThrows(() -> AffineTransformMatrix2D.createScale(1, 0).applyDirection(Vector2D.Unit.PLUS_Y),
723                 IllegalArgumentException.class);
724         GeometryTestUtils.assertThrows(() -> AffineTransformMatrix2D.createScale(2).applyDirection(Vector2D.ZERO),
725                 IllegalArgumentException.class);
726     }
727 
728     @Test
729     public void testDeterminant() {
730         // act/assert
731         Assert.assertEquals(1.0, AffineTransformMatrix2D.identity().determinant(), EPS);
732         Assert.assertEquals(6.0, AffineTransformMatrix2D.of(
733                 2, 0, 4,
734                 0, 3, 5
735             ).determinant(), EPS);
736         Assert.assertEquals(-6.0, AffineTransformMatrix2D.of(
737                 2, 0, 4,
738                 0, -3, 5
739             ).determinant(), EPS);
740         Assert.assertEquals(-5.0, AffineTransformMatrix2D.of(
741                 1, 3, 0,
742                 2, 1, 0
743             ).determinant(), EPS);
744         Assert.assertEquals(-0.0, AffineTransformMatrix2D.of(
745                 0, 0, 1,
746                 0, 0, 2
747             ).determinant(), EPS);
748     }
749 
750     @Test
751     public void testPreservesOrientation() {
752         // act/assert
753         Assert.assertTrue(AffineTransformMatrix2D.identity().preservesOrientation());
754         Assert.assertTrue(AffineTransformMatrix2D.of(
755                 2, 0, 4,
756                 0, 3, 5
757             ).preservesOrientation());
758 
759         Assert.assertFalse(AffineTransformMatrix2D.of(
760                 2, 0, 4,
761                 0, -3, 5
762             ).preservesOrientation());
763         Assert.assertFalse(AffineTransformMatrix2D.of(
764                 1, 3, 0,
765                 2, 1, 0
766             ).preservesOrientation());
767         Assert.assertFalse(AffineTransformMatrix2D.of(
768                 0, 0, 1,
769                 0, 0, 2
770             ).preservesOrientation());
771     }
772 
773     @Test
774     public void testMultiply() {
775         // arrange
776         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
777                     1, 2, 3,
778                     5, 6, 7
779                 );
780         final AffineTransformMatrix2D b = AffineTransformMatrix2D.of(
781                     13, 14, 15,
782                     17, 18, 19
783                 );
784 
785         // act
786         final AffineTransformMatrix2D result = a.multiply(b);
787 
788         // assert
789         final double[] arr = result.toArray();
790         Assert.assertArrayEquals(new double[] {
791             47, 50, 56,
792             167, 178, 196
793         }, arr, EPS);
794     }
795 
796     @Test
797     public void testMultiply_combinesTransformOperations() {
798         // arrange
799         final Vector2D translation1 = Vector2D.of(1, 2);
800         final double scale = 2.0;
801         final Vector2D translation2 = Vector2D.of(4, 5);
802 
803         final AffineTransformMatrix2D a = AffineTransformMatrix2D.createTranslation(translation1);
804         final AffineTransformMatrix2D b = AffineTransformMatrix2D.createScale(scale);
805         final AffineTransformMatrix2D c = AffineTransformMatrix2D.identity();
806         final AffineTransformMatrix2D d = AffineTransformMatrix2D.createTranslation(translation2);
807 
808         // act
809         final AffineTransformMatrix2D transform = d.multiply(c).multiply(b).multiply(a);
810 
811         // assert
812         runWithCoordinates((x, y) -> {
813             final Vector2D vec = Vector2D.of(x, y);
814 
815             final Vector2D expectedVec = vec
816                     .add(translation1)
817                     .multiply(scale)
818                     .add(translation2);
819 
820             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
821         });
822     }
823 
824     @Test
825     public void testPremultiply() {
826         // arrange
827         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
828                     1, 2, 3,
829                     5, 6, 7
830                 );
831         final AffineTransformMatrix2D b = AffineTransformMatrix2D.of(
832                     13, 14, 15,
833                     17, 18, 19
834                 );
835 
836         // act
837         final AffineTransformMatrix2D result = b.premultiply(a);
838 
839         // assert
840         final double[] arr = result.toArray();
841         Assert.assertArrayEquals(new double[] {
842             47, 50, 56,
843             167, 178, 196
844         }, arr, EPS);
845     }
846 
847     @Test
848     public void testPremultiply_combinesTransformOperations() {
849         // arrange
850         final Vector2D translation1 = Vector2D.of(1, 2);
851         final double scale = 2.0;
852         final Vector2D translation2 = Vector2D.of(4, 5);
853 
854         final AffineTransformMatrix2D a = AffineTransformMatrix2D.createTranslation(translation1);
855         final AffineTransformMatrix2D b = AffineTransformMatrix2D.createScale(scale);
856         final AffineTransformMatrix2D c = AffineTransformMatrix2D.identity();
857         final AffineTransformMatrix2D d = AffineTransformMatrix2D.createTranslation(translation2);
858 
859         // act
860         final AffineTransformMatrix2D transform = a.premultiply(b).premultiply(c).premultiply(d);
861 
862         // assert
863         runWithCoordinates((x, y) -> {
864             final Vector2D vec = Vector2D.of(x, y);
865 
866             final Vector2D expectedVec = vec
867                     .add(translation1)
868                     .multiply(scale)
869                     .add(translation2);
870 
871             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
872         });
873     }
874 
875     @Test
876     public void testInverse_identity() {
877         // act
878         final AffineTransformMatrix2D inverse = AffineTransformMatrix2D.identity().inverse();
879 
880         // assert
881         final double[] expected = {
882             1, 0, 0,
883             0, 1, 0
884         };
885         Assert.assertArrayEquals(expected, inverse.toArray(), 0.0);
886     }
887 
888     @Test
889     public void testInverse_multiplyByInverse_producesIdentity() {
890         // arrange
891         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
892                     1, 3, 7,
893                     2, 4, 9
894                 );
895 
896         final AffineTransformMatrix2D inv = a.inverse();
897 
898         // act
899         final AffineTransformMatrix2D result = inv.multiply(a);
900 
901         // assert
902         final double[] expected = {
903             1, 0, 0,
904             0, 1, 0
905         };
906         Assert.assertArrayEquals(expected, result.toArray(), EPS);
907     }
908 
909     @Test
910     public void testInverse_translate() {
911         // arrange
912         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createTranslation(1, -2);
913 
914         // act
915         final AffineTransformMatrix2D inverse = transform.inverse();
916 
917         // assert
918         final double[] expected = {
919             1, 0, -1,
920             0, 1, 2
921         };
922         Assert.assertArrayEquals(expected, inverse.toArray(), 0.0);
923     }
924 
925     @Test
926     public void testInverse_scale() {
927         // arrange
928         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(10, -2);
929 
930         // act
931         final AffineTransformMatrix2D inverse = transform.inverse();
932 
933         // assert
934         final double[] expected = {
935             0.1, 0, 0,
936             0, -0.5, 0
937         };
938         Assert.assertArrayEquals(expected, inverse.toArray(), 0.0);
939     }
940 
941     @Test
942     public void testInverse_rotate() {
943         // arrange
944         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createRotation(PlaneAngleRadians.PI_OVER_TWO);
945 
946         // act
947         final AffineTransformMatrix2D inverse = transform.inverse();
948 
949         // assert
950         final double[] expected = {
951             0, 1, 0,
952             -1, 0, 0
953         };
954         Assert.assertArrayEquals(expected, inverse.toArray(), EPS);
955     }
956 
957     @Test
958     public void testInverse_rotate_aroundCenter() {
959         // arrange
960         final Vector2D center = Vector2D.of(1, 2);
961         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createRotation(center, PlaneAngleRadians.PI_OVER_TWO);
962 
963         // act
964         final AffineTransformMatrix2D inverse = transform.inverse();
965 
966         // assert
967         final double[] expected = {
968             0, 1, -1,
969             -1, 0, 3
970         };
971         Assert.assertArrayEquals(expected, inverse.toArray(), EPS);
972     }
973 
974     @Test
975     public void testInverse_undoesOriginalTransform() {
976         // arrange
977         final Vector2D v1 = Vector2D.ZERO;
978         final Vector2D v2 = Vector2D.Unit.PLUS_X;
979         final Vector2D v3 = Vector2D.of(1, 1);
980         final Vector2D v4 = Vector2D.of(-2, 3);
981 
982         final Vector2D center = Vector2D.of(-0.5, 2);
983 
984         // act/assert
985         runWithCoordinates((x, y) -> {
986             final AffineTransformMatrix2D transform = AffineTransformMatrix2D
987                         .createTranslation(x, y)
988                         .scale(2, 3)
989                         .translate(x / 3, y / 3)
990                         .rotate(x / 4)
991                         .rotate(center, y / 2);
992 
993             final AffineTransformMatrix2D inverse = transform.inverse();
994 
995             EuclideanTestUtils.assertCoordinatesEqual(v1, inverse.apply(transform.apply(v1)), EPS);
996             EuclideanTestUtils.assertCoordinatesEqual(v2, inverse.apply(transform.apply(v2)), EPS);
997             EuclideanTestUtils.assertCoordinatesEqual(v3, inverse.apply(transform.apply(v3)), EPS);
998             EuclideanTestUtils.assertCoordinatesEqual(v4, inverse.apply(transform.apply(v4)), EPS);
999         });
1000     }
1001 
1002     @Test
1003     public void testInverse_nonInvertible() {
1004         // act/assert
1005         GeometryTestUtils.assertThrows(() -> {
1006             AffineTransformMatrix2D.of(
1007                     0, 0, 0,
1008                     0, 0, 0).inverse();
1009         }, IllegalStateException.class, "Matrix is not invertible; matrix determinant is 0.0");
1010 
1011         GeometryTestUtils.assertThrows(() -> {
1012             AffineTransformMatrix2D.of(
1013                     1, 0, 0,
1014                     0, Double.NaN, 0).inverse();
1015         }, IllegalStateException.class, "Matrix is not invertible; matrix determinant is NaN");
1016 
1017         GeometryTestUtils.assertThrows(() -> {
1018             AffineTransformMatrix2D.of(
1019                     1, 0, 0,
1020                     0, Double.NEGATIVE_INFINITY, 0).inverse();
1021         }, IllegalStateException.class, "Matrix is not invertible; matrix determinant is -Infinity");
1022 
1023         GeometryTestUtils.assertThrows(() -> {
1024             AffineTransformMatrix2D.of(
1025                     Double.POSITIVE_INFINITY, 0, 0,
1026                     0, 1, 0).inverse();
1027         }, IllegalStateException.class, "Matrix is not invertible; matrix determinant is Infinity");
1028 
1029         GeometryTestUtils.assertThrows(() -> {
1030             AffineTransformMatrix2D.of(
1031                     1, 0, Double.NaN,
1032                     0, 1, 0).inverse();
1033         }, IllegalStateException.class, "Matrix is not invertible; invalid matrix element: NaN");
1034 
1035         GeometryTestUtils.assertThrows(() -> {
1036             AffineTransformMatrix2D.of(
1037                     1, 0, Double.POSITIVE_INFINITY,
1038                     0, 1, 0).inverse();
1039         }, IllegalStateException.class, "Matrix is not invertible; invalid matrix element: Infinity");
1040 
1041         GeometryTestUtils.assertThrows(() -> {
1042             AffineTransformMatrix2D.of(
1043                     1, 0, Double.NEGATIVE_INFINITY,
1044                     0, 1, 0).inverse();
1045         }, IllegalStateException.class, "Matrix is not invertible; invalid matrix element: -Infinity");
1046     }
1047 
1048     @Test
1049     public void testLinear() {
1050         // arrange
1051         final AffineTransformMatrix2D mat = AffineTransformMatrix2D.of(
1052                 2, 3, 4,
1053                 5, 6, 7);
1054 
1055         // act
1056         final AffineTransformMatrix2D result = mat.linear();
1057 
1058         // assert
1059         final double[] expected = {
1060             2, 3, 0,
1061             5, 6, 0
1062         };
1063         Assert.assertArrayEquals(expected, result.toArray(), 0.0);
1064     }
1065 
1066     @Test
1067     public void testLinearTranspose() {
1068         // arrange
1069         final AffineTransformMatrix2D mat = AffineTransformMatrix2D.of(
1070                 2, 3, 4,
1071                 5, 6, 7);
1072 
1073         // act
1074         final AffineTransformMatrix2D result = mat.linearTranspose();
1075 
1076         // assert
1077         final double[] expected = {
1078             2, 5, 0,
1079             3, 6, 0
1080         };
1081         Assert.assertArrayEquals(expected, result.toArray(), 0.0);
1082     }
1083 
1084     @Test
1085     public void testNormalTransform() {
1086         // act/assert
1087         checkNormalTransform(AffineTransformMatrix2D.identity());
1088 
1089         checkNormalTransform(AffineTransformMatrix2D.createTranslation(2, 3));
1090         checkNormalTransform(AffineTransformMatrix2D.createTranslation(-3, -4));
1091 
1092         checkNormalTransform(AffineTransformMatrix2D.createScale(2, 5));
1093         checkNormalTransform(AffineTransformMatrix2D.createScale(-3, 4));
1094         checkNormalTransform(AffineTransformMatrix2D.createScale(-2, -5));
1095 
1096         checkNormalTransform(AffineTransformMatrix2D.createRotation(PlaneAngleRadians.PI_OVER_TWO));
1097         checkNormalTransform(AffineTransformMatrix2D.createRotation(PlaneAngleRadians.THREE_PI_OVER_TWO));
1098 
1099         checkNormalTransform(AffineTransformMatrix2D.createRotation(Vector2D.of(3, 4), PlaneAngleRadians.THREE_PI_OVER_TWO)
1100                 .translate(8, 2)
1101                 .scale(-3, -2));
1102         checkNormalTransform(AffineTransformMatrix2D.createScale(2, -1)
1103                 .translate(-3, -4)
1104                 .rotate(Vector2D.of(-0.5, 0.5), 0.75 * Math.PI));
1105     }
1106 
1107     private void checkNormalTransform(final AffineTransformMatrix2D transform) {
1108         final AffineTransformMatrix2D normalTransform = transform.normalTransform();
1109 
1110         final Vector2D p1 = Vector2D.of(-0.25, 0.75);
1111         final Vector2D t1 = transform.apply(p1);
1112 
1113         EuclideanTestUtils.permute(-10, 10, 1, (x, y) -> {
1114             final Vector2D p2 = Vector2D.of(x, y);
1115             final Vector2D n = Lines.fromPoints(p1, p2, TEST_PRECISION).getOffsetDirection();
1116 
1117             final Vector2D t2 = transform.apply(p2);
1118 
1119             final Line tLine = transform.preservesOrientation() ?
1120                     Lines.fromPoints(t1, t2, TEST_PRECISION) :
1121                     Lines.fromPoints(t2, t1, TEST_PRECISION);
1122             final Vector2D expected = tLine.getOffsetDirection();
1123 
1124             final Vector2D actual = normalTransform.apply(n).normalize();
1125 
1126             EuclideanTestUtils.assertCoordinatesEqual(expected, actual, EPS);
1127         });
1128     }
1129 
1130     @Test
1131     public void testNormalTransform_nonInvertible() {
1132         // act/assert
1133         GeometryTestUtils.assertThrows(() -> {
1134             AffineTransformMatrix2D.createScale(0).normalTransform();
1135         }, IllegalStateException.class);
1136     }
1137 
1138     @Test
1139     public void testHashCode() {
1140         // arrange
1141         final double[] values = {
1142             1, 2, 3,
1143             5, 6, 7
1144         };
1145 
1146         // act/assert
1147         final int orig = AffineTransformMatrix2D.of(values).hashCode();
1148         final int same = AffineTransformMatrix2D.of(values).hashCode();
1149 
1150         Assert.assertEquals(orig, same);
1151 
1152         double[] temp;
1153         for (int i = 0; i < values.length; ++i) {
1154             temp = values.clone();
1155             temp[i] = 0;
1156 
1157             final int modified = AffineTransformMatrix2D.of(temp).hashCode();
1158 
1159             Assert.assertNotEquals(orig, modified);
1160         }
1161     }
1162 
1163     @Test
1164     public void testEquals() {
1165         // arrange
1166         final double[] values = {
1167             1, 2, 3,
1168             5, 6, 7
1169         };
1170 
1171         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(values);
1172 
1173         // act/assert
1174         Assert.assertEquals(a, a);
1175 
1176         Assert.assertFalse(a.equals(null));
1177         Assert.assertFalse(a.equals(new Object()));
1178 
1179         double[] temp;
1180         for (int i = 0; i < values.length; ++i) {
1181             temp = values.clone();
1182             temp[i] = 0;
1183 
1184             final AffineTransformMatrix2D modified = AffineTransformMatrix2D.of(temp);
1185 
1186             Assert.assertNotEquals(a, modified);
1187         }
1188     }
1189 
1190     @Test
1191     public void testEqualsAndHashCode_signedZeroConsistency() {
1192         // arrange
1193         final double[] arrWithPosZero = {
1194             1.0, 0.0, 0.0,
1195             0.0, 1.0, 0.0
1196         };
1197         final double[] arrWithNegZero = {
1198             1.0, 0.0, 0.0,
1199             0.0, 1.0, -0.0
1200         };
1201         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(arrWithPosZero);
1202         final AffineTransformMatrix2D b = AffineTransformMatrix2D.of(arrWithNegZero);
1203         final AffineTransformMatrix2D c = AffineTransformMatrix2D.of(arrWithPosZero);
1204         final AffineTransformMatrix2D d = AffineTransformMatrix2D.of(arrWithNegZero);
1205 
1206         // act/assert
1207         Assert.assertFalse(a.equals(b));
1208         Assert.assertNotEquals(a.hashCode(), b.hashCode());
1209 
1210         Assert.assertTrue(a.equals(c));
1211         Assert.assertEquals(a.hashCode(), c.hashCode());
1212 
1213         Assert.assertTrue(b.equals(d));
1214         Assert.assertEquals(b.hashCode(), d.hashCode());
1215     }
1216 
1217     @Test
1218     public void testToString() {
1219         // arrange
1220         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
1221                     1, 2, 3,
1222                     5, 6, 7
1223                 );
1224 
1225         // act
1226         final String result = a.toString();
1227 
1228         // assert
1229         Assert.assertEquals(
1230                 "[ 1.0, 2.0, 3.0; " +
1231                 "5.0, 6.0, 7.0 ]", result);
1232     }
1233 
1234     @FunctionalInterface
1235     private interface Coordinate2DTest {
1236 
1237         void run(double x, double y);
1238     }
1239 
1240     private static void runWithCoordinates(final Coordinate2DTest test) {
1241         runWithCoordinates(test, -1e-2, 1e-2, 5e-3);
1242         runWithCoordinates(test, -1e2, 1e2, 5);
1243     }
1244 
1245     private static void runWithCoordinates(final Coordinate2DTest test, final double min, final double max, final double step) {
1246         for (double x = min; x <= max; x += step) {
1247             for (double y = min; y <= max; y += step) {
1248                 test.run(x, y);
1249             }
1250         }
1251     }
1252 }