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.rotation;
    18
    19  import java.util.function.BiFunction;
    20  import java.util.function.DoubleFunction;
    21
    22  import org.apache.commons.geometry.core.GeometryTestUtils;
    23  import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
    24  import org.apache.commons.geometry.euclidean.EuclideanTransform;
    25  import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
    26  import org.apache.commons.geometry.euclidean.twod.Line;
    27  import org.apache.commons.geometry.euclidean.twod.Lines;
    28  import org.apache.commons.geometry.euclidean.twod.Vector2D;
    29  import org.apache.commons.numbers.angle.Angle;
    30  import org.apache.commons.numbers.core.Precision;
    31  import org.junit.jupiter.api.Assertions;
    32  import org.junit.jupiter.api.Test;
    33
    34  class Rotation2DTest {
    35
    36      private static final double TEST_EPS = 1e-10;
    37
    38      private static final Precision.DoubleEquivalence TEST_PRECISION =
    39              Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
    40
    41      private static final double THREE_PI_OVER_TWO = 3 * Math.PI / 2;
    42
    43      @Test
    44      void testIdentity() {
    45          // act
    46          final Rotation2D r = Rotation2D.identity();
    47
    48          // assert
    49          Assertions.assertEquals(0.0, r.getAngle(), 0.0);
    50          Assertions.assertTrue(r.preservesOrientation());
    51      }
    52
    53      @Test
    54      void testProperties() {
    55          // act
    56          final Rotation2D r = Rotation2D.of(100.0);
    57
    58          // assert
    59          Assertions.assertEquals(100.0, r.getAngle(), 0.0);
    60          Assertions.assertTrue(r.preservesOrientation());
    61      }
    62
    63      @Test
    64      void testApply() {
    65          // act/assert
    66          checkApply(1.0, Vector2D.ZERO, Vector2D.ZERO);
    67
    68          checkApply(0.0, Vector2D.Unit.PLUS_X, Vector2D.Unit.PLUS_X);
    69          checkApply(Angle.PI_OVER_TWO, Vector2D.Unit.PLUS_X, Vector2D.Unit.PLUS_Y);
    70          checkApply(Math.PI, Vector2D.Unit.PLUS_X, Vector2D.Unit.MINUS_X);
    71          checkApply(THREE_PI_OVER_TWO, Vector2D.Unit.PLUS_X, Vector2D.Unit.MINUS_Y);
    72          checkApply(Angle.TWO_PI, Vector2D.Unit.PLUS_X, Vector2D.Unit.PLUS_X);
    73
    74          checkRotate(Rotation2D::of, Rotation2D::apply);
    75      }
    76
    77      @Test
    78      void testApplyVector() {
    79          // act/assert
    80          checkApplyVector(1.0, Vector2D.ZERO, Vector2D.ZERO);
    81
    82          checkApplyVector(0.0, Vector2D.Unit.PLUS_X, Vector2D.Unit.PLUS_X);
    83          checkApplyVector(Angle.PI_OVER_TWO, Vector2D.Unit.PLUS_X, Vector2D.Unit.PLUS_Y);
    84          checkApplyVector(Math.PI, Vector2D.Unit.PLUS_X, Vector2D.Unit.MINUS_X);
    85          checkApplyVector(THREE_PI_OVER_TWO, Vector2D.Unit.PLUS_X, Vector2D.Unit.MINUS_Y);
    86          checkApplyVector(Angle.TWO_PI, Vector2D.Unit.PLUS_X, Vector2D.Unit.PLUS_X);
    87
    88          checkRotate(Rotation2D::of, Rotation2D::applyVector);
    89      }
    90
    91      @Test
    92      void testInverse_properties() {
    93          // arrange
    94          final Rotation2D orig = Rotation2D.of(100.0);
    95
    96          // act
    97          final Rotation2D r = orig.inverse();
    98
    99          // assert
    100         Assertions.assertEquals(-100.0, r.getAngle(), 0.0);
    101         Assertions.assertTrue(r.preservesOrientation());
    102     }
    103
    104     @Test
    105     void testInverse_apply() {
    106         // arrange
    107         final Rotation2D orig = Rotation2D.of(100.0);
    108         final Rotation2D inv = orig.inverse();
    109
    110         final Vector2D v1 = Vector2D.of(1, 2);
    111         final Vector2D v2 = Vector2D.of(-3, 4);
    112         final Vector2D v3 = Vector2D.of(-5, -6);
    113         final Vector2D v4 = Vector2D.of(7, -8);
    114
    115         // act/assert
    116         EuclideanTestUtils.assertCoordinatesEqual(v1, orig.apply(inv.apply(v1)), TEST_EPS);
    117         EuclideanTestUtils.assertCoordinatesEqual(v2, inv.apply(orig.apply(v2)), TEST_EPS);
    118         EuclideanTestUtils.assertCoordinatesEqual(v3, orig.apply(inv.apply(v3)), TEST_EPS);
    119         EuclideanTestUtils.assertCoordinatesEqual(v4, inv.apply(orig.apply(v4)), TEST_EPS);
    120     }
    121
    122     @Test
    123     void testToMatrix() {
    124         // arrange
    125         final double angle = 0.1 * Math.PI;
    126
    127         // act
    128         final AffineTransformMatrix2D m = Rotation2D.of(angle).toMatrix();
    129
    130         // assert
    131         final double sin = Math.sin(angle);
    132         final double cos = Math.cos(angle);
    133
    134         final double[] expected = {
    135             cos, -sin, 0,
    136             sin, cos, 0
    137         };
    138         Assertions.assertArrayEquals(expected, m.toArray(), 0.0);
    139     }
    140
    141     @Test
    142     void testToMatrix_apply() {
    143         // act/assert
    144         checkRotate(angle -> Rotation2D.of(angle).toMatrix(), AffineTransformMatrix2D::apply);
    145     }
    146
    147     @Test
    148     void testCreateRotationVector() {
    149         // arrange
    150         final double min = -8;
    151         final double max = 8;
    152         final double step = 1;
    153
    154         EuclideanTestUtils.permuteSkipZero(min, max, step, (ux, uy) -> {
    155             EuclideanTestUtils.permuteSkipZero(min, max, step, (vx, vy) -> {
    156
    157                 final Vector2D u = Vector2D.of(ux, uy);
    158                 final Vector2D v = Vector2D.of(vx, vy);
    159
    160                 // act
    161                 final Rotation2D r = Rotation2D.createVectorRotation(u, v);
    162
    163                 // assert
    164                 EuclideanTestUtils.assertCoordinatesEqual(v.normalize(), r.apply(u).normalize(), TEST_EPS); // u -> v
    165                 Assertions.assertEquals(0.0, v.dot(r.apply(u.orthogonal())), TEST_EPS); // preserves orthogonality
    166             });
    167         });
    168     }
    169
    170     @Test
    171     void testCreateRotationVector_invalidVectors() {
    172         // arrange
    173         final Vector2D vec = Vector2D.of(1, 1);
    174
    175         final Vector2D zero = Vector2D.ZERO;
    176         final Vector2D nan = Vector2D.NaN;
    177         final Vector2D posInf = Vector2D.POSITIVE_INFINITY;
    178         final Vector2D negInf = Vector2D.POSITIVE_INFINITY;
    179
    180         // act/assert
    181         Assertions.assertThrows(IllegalArgumentException.class, () -> Rotation2D.createVectorRotation(zero, vec));
    182         Assertions.assertThrows(IllegalArgumentException.class, () -> Rotation2D.createVectorRotation(vec, zero));
    183         Assertions.assertThrows(IllegalArgumentException.class, () -> Rotation2D.createVectorRotation(nan, vec));
    184         Assertions.assertThrows(IllegalArgumentException.class, () -> Rotation2D.createVectorRotation(vec, nan));
    185         Assertions.assertThrows(IllegalArgumentException.class, () -> Rotation2D.createVectorRotation(posInf, vec));
    186         Assertions.assertThrows(IllegalArgumentException.class, () -> Rotation2D.createVectorRotation(vec, negInf));
    187         Assertions.assertThrows(IllegalArgumentException.class, () -> Rotation2D.createVectorRotation(zero, nan));
    188         Assertions.assertThrows(IllegalArgumentException.class, () -> Rotation2D.createVectorRotation(negInf, posInf));
    189     }
    190
    191     @Test
    192     void testHashCode() {
    193         // arrange
    194         final Rotation2D a = Rotation2D.of(1.0);
    195         final Rotation2D b = Rotation2D.of(0.0);
    196         final Rotation2D c = Rotation2D.of(-1.0);
    197         final Rotation2D d = Rotation2D.of(1.0);
    198
    199         final int hash = a.hashCode();
    200
    201         // act/assert
    202         Assertions.assertEquals(hash, a.hashCode());
    203
    204         Assertions.assertNotEquals(hash, b.hashCode());
    205         Assertions.assertNotEquals(hash, c.hashCode());
    206
    207         Assertions.assertEquals(hash, d.hashCode());
    208     }
    209
    210     @Test
    211     void testEquals() {
    212         // arrange
    213         final Rotation2D a = Rotation2D.of(1.0);
    214         final Rotation2D b = Rotation2D.of(0.0);
    215         final Rotation2D c = Rotation2D.of(-1.0);
    216         final Rotation2D d = Rotation2D.of(1.0);
    217
    218         // act/assert
    219         GeometryTestUtils.assertSimpleEqualsCases(a);
    220
    221         Assertions.assertNotEquals(a, b);
    222         Assertions.assertNotEquals(a, c);
    223
    224         Assertions.assertEquals(a, d);
    225         Assertions.assertEquals(d, a);
    226     }
    227
    228     @Test
    229     void testToString() {
    230         // arrange
    231         final Rotation2D r = Rotation2D.of(1.0);
    232
    233         // act
    234         final String str = r.toString();
    235
    236         // assert
    237         Assertions.assertEquals("Rotation2D[angle=1.0]", str);
    238     }
    239
    240     private static void checkApply(final double angle, final Vector2D pt, final Vector2D expectedPt) {
    241         final Rotation2D r = Rotation2D.of(angle);
    242         EuclideanTestUtils.assertCoordinatesEqual(expectedPt, r.apply(pt), TEST_EPS);
    243     }
    244
    245     private static void checkApplyVector(final double angle, final Vector2D pt, final Vector2D expectedPt) {
    246         final Rotation2D r = Rotation2D.of(angle);
    247         EuclideanTestUtils.assertCoordinatesEqual(expectedPt, r.applyVector(pt), TEST_EPS);
    248     }
    249
    250     /** Check a rotation transform for consistency against a variety of points and rotation angles.
    251      * @param factory function used to create a rotation transform from an input angle
    252      * @param transformFn function that accepts the transform and a point and returns
    253      *      the transformed point
    254      */
    255     private static <T extends EuclideanTransform<Vector2D>> void checkRotate(
    256             final DoubleFunction<T> factory, final BiFunction<T, Vector2D, Vector2D> transformFn) {
    257
    258         // check zero
    259         final T transform = factory.apply(0);
    260         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, transformFn.apply(transform, Vector2D.ZERO), TEST_EPS);
    261
    262         // check a variety of non-zero points
    263         EuclideanTestUtils.permuteSkipZero(-2, -2, 1, (x, y) -> {
    264             checkRotatePoint(Vector2D.of(x, y), factory, transformFn);
    265         });
    266     }
    267
    268     /** Check a rotation transform for consistency when transforming a single point against a
    269      * variety of rotation angles.
    270      * @param pt point to transform
    271      * @param factory function used to create a rotation transform from an input angle
    272      * @param transformFn function that accepts the transform and a point and returns
    273      *      the transformed point
    274      */
    275     private static <T extends EuclideanTransform<Vector2D>> void checkRotatePoint(
    276             final Vector2D pt, final DoubleFunction<T> factory, final BiFunction<T, ? super Vector2D, ? extends Vector2D> transformFn) {
    277
    278         // arrange
    279         final double limit = 4 * Math.PI;
    280         final double inc = 0.25;
    281
    282         final Line line = Lines.fromPointAndDirection(Vector2D.ZERO, pt, TEST_PRECISION);
    283
    284         T transform;
    285         Vector2D resultPt;
    286         Line resultLine;
    287         for (double angle = -limit; angle < limit; angle += inc) {
    288             transform = factory.apply(angle);
    289
    290             // act
    291             resultPt = transformFn.apply(transform, pt);
    292
    293             // assert
    294             // check that the norm is unchanged
    295             Assertions.assertEquals(pt.norm(), resultPt.norm(), TEST_EPS);
    296
    297             resultLine = Lines.fromPointAndDirection(Vector2D.ZERO, resultPt, TEST_PRECISION);
    298             final double lineAngle = line.angle(resultLine);
    299
    300             // check that the angle is what we expect
    301             Assertions.assertEquals(Angle.Rad.WITHIN_MINUS_PI_AND_PI.applyAsDouble(angle), lineAngle, TEST_EPS);
    302         }
    303     }
    304 }