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 }