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 }