View Javadoc
1   package org.djutils.draw;
2   
3   import static org.junit.jupiter.api.Assertions.assertEquals;
4   import static org.junit.jupiter.api.Assertions.assertFalse;
5   import static org.junit.jupiter.api.Assertions.assertNotEquals;
6   import static org.junit.jupiter.api.Assertions.assertTrue;
7   import static org.junit.jupiter.api.Assertions.fail;
8   
9   import java.lang.reflect.Field;
10  import java.util.Arrays;
11  
12  import org.djutils.draw.bounds.Bounds2d;
13  import org.djutils.draw.point.Point2d;
14  import org.junit.jupiter.api.Test;
15  
16  /**
17   * Transform2dTest.java.
18   * <p>
19   * Copyright (c) 2020-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
20   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
21   * </p>
22   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
23   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
24   */
25  public class Transform2dTest
26  {
27      /**
28       * Test the matrix / vector multiplication.
29       */
30      @Test
31      public void testMatrixMultiplication()
32      {
33          double[] mA = new double[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
34          double[] mB = new double[] {2, 1, 0, 2, 4, 3, 3, 1, 2};
35          double[] mAmB = Transform2d.mulMatMat(mA, mB);
36          double[] expected = new double[] {15, 12, 12, 36, 30, 27, 57, 48, 42};
37          for (int i = 0; i < 9; i++)
38          {
39              if (mAmB[i] != expected[i])
40              {
41                  fail(String.format("difference MA x MB at %d: expected %f, was: %f", i, expected[i], mAmB[i]));
42              }
43          }
44  
45          double[] m = new double[] {1, 4, 2, 5, 3, 1, 4, 2, 5};
46          double[] v = new double[] {2, 5, 1};
47          double[] mv = Transform2d.mulMatVec(m, v);
48          double[] ev = new double[] {24, 26, 23};
49          for (int i = 0; i < 3; i++)
50          {
51              if (mv[i] != ev[i])
52              {
53                  fail(String.format("difference M x V at %d: expected %f, was: %f", i, ev[i], mv[i]));
54              }
55          }
56  
57          v = new double[] {1, 2};
58          mv = Transform2d.mulMatVec2(m, v);
59          ev = new double[] {11, 12};
60          for (int i = 0; i < 2; i++)
61          {
62              if (mv[i] != ev[i])
63              {
64                  fail(String.format("difference M x V3 at %d: expected %f, was: %f", i, ev[i], mv[i]));
65              }
66          }
67      }
68  
69      /**
70       * Test that the constructor creates an Identity matrix.
71       */
72      @Test
73      public void testConstructor()
74      {
75          // TODO: decide whether the internal (flattened) matrix should be visible at all, or add a getter
76          Transform2d t = new Transform2d();
77          assertEquals(9, t.getMat().length, "matrix contians 9 values");
78          for (int row = 0; row < 3; row++)
79          {
80              for (int col = 0; col < 3; col++)
81              {
82                  int e = row == col ? 1 : 0;
83                  assertEquals(e, t.getMat()[3 * row + col], 0, "Value in identity matrix matches");
84              }
85          }
86      }
87  
88      /**
89       * Test the translate, scale, rotate, shear and reflect methods.
90       */
91      @Test
92      public void testTranslateScaleRotateShearAndReflect()
93      {
94          Transform2d t;
95          // Test time grows (explodes) with the 4th power of the length of values.
96          double[] values = new double[] {-100000, -100, -10, -3, -1, -0.3, -0.1, 0, 0.1, 0.3, 1, 3, 10, 100, 100000};
97          for (double dx : values)
98          {
99              for (double dy : values)
100             {
101                 // Translate defined with a double[]
102                 t = new Transform2d();
103                 t.translate(dx, dy);
104                 for (double px : values)
105                 {
106                     for (double py : values)
107                     {
108                         Point2d p = t.transform(new Point2d(px, py));
109                         assertEquals(px + dx, p.x, 0.001, "translated x matches");
110                         assertEquals(py + dy, p.y, 0.001, "translated y matches");
111                         double[] result = t.transform(new double[] {px, py});
112                         assertEquals(px + dx, result[0], 0.001, "translated x matches");
113                         assertEquals(py + dy, result[1], 0.001, "translated y matches");
114                     }
115                 }
116                 // Translate defined with a Point
117                 t = new Transform2d();
118                 t.translate(new Point2d(dx, dy));
119                 for (double px : values)
120                 {
121                     for (double py : values)
122                     {
123                         Point2d p = t.transform(new Point2d(px, py));
124                         assertEquals(px + dx, p.x, 0.001, "transformed x matches");
125                         assertEquals(py + dy, p.y, 0.001, "transformed y matches");
126                         double[] result = t.transform(new double[] {px, py});
127                         assertEquals(px + dx, result[0], 0.001, "transformed x matches");
128                         assertEquals(py + dy, result[1], 0.001, "transformed y matches");
129                     }
130                 }
131                 // Scale
132                 t = new Transform2d();
133                 t.scale(dx, dy);
134                 for (double px : values)
135                 {
136                     for (double py : values)
137                     {
138                         Point2d p = t.transform(new Point2d(px, py));
139                         assertEquals(px * dx, p.x, 0.001, "scaled x matches");
140                         assertEquals(py * dy, p.y, 0.001, "scaled y matches");
141                         double[] result = t.transform(new double[] {px, py});
142                         assertEquals(px * dx, result[0], 0.001, "scaled x matches");
143                         assertEquals(py * dy, result[1], 0.001, "scaled y matches");
144                     }
145                 }
146                 // Shear
147                 t = new Transform2d();
148                 t.shear(dx, dy);
149                 for (double px : values)
150                 {
151                     for (double py : values)
152                     {
153                         Point2d p = t.transform(new Point2d(px, py));
154                         assertEquals(px + py * dx, p.x, 0.001, "sheared x matches");
155                         assertEquals(py + px * dy, p.y, 0.001, "sheared y matches");
156                         double[] result = t.transform(new double[] {px, py});
157                         assertEquals(px + py * dx, result[0], 0.001, "sheared x matches");
158                         assertEquals(py + px * dy, result[1], 0.001, "sheared y matches");
159                     }
160                 }
161             }
162             // Rotate (using dx as angle)
163             t = new Transform2d();
164             t.rotation(dx);
165             double sine = Math.sin(dx);
166             double cosine = Math.cos(dx);
167             for (double px : values)
168             {
169                 for (double py : values)
170                 {
171                     Point2d p = t.transform(new Point2d(px, py));
172                     assertEquals(px * cosine - py * sine, p.x, 0.001, "rotated x matches");
173                     assertEquals(py * cosine + px * sine, p.y, 0.001, "rotated y matches");
174                     double[] result = t.transform(new double[] {px, py});
175                     assertEquals(px * cosine - py * sine, result[0], 0.001, "rotated x matches");
176                     assertEquals(py * cosine + px * sine, result[1], 0.001, "rotated y matches");
177                 }
178             }
179         }
180         // ReflectX
181         t = new Transform2d();
182         t.reflectX();
183         for (double px : values)
184         {
185             for (double py : values)
186             {
187                 Point2d p = t.transform(new Point2d(px, py));
188                 assertEquals(-px, p.x, 0.001, "x-reflected x matches");
189                 assertEquals(py, p.y, 0.001, "x-reflected y  matches");
190                 double[] result = t.transform(new double[] {px, py});
191                 assertEquals(-px, result[0], 0.001, "x-reflected x  matches");
192                 assertEquals(py, result[1], 0.001, "x-reflected y  matches");
193             }
194         }
195         // ReflectY
196         t = new Transform2d();
197         t.reflectY();
198         for (double px : values)
199         {
200             for (double py : values)
201             {
202                 Point2d p = t.transform(new Point2d(px, py));
203                 assertEquals(px, p.x, 0.001, "y-reflected x matches");
204                 assertEquals(-py, p.y, 0.001, "y-reflected y  matches");
205                 double[] result = t.transform(new double[] {px, py});
206                 assertEquals(px, result[0], 0.001, "y-reflected x  matches");
207                 assertEquals(-py, result[1], 0.001, "y-reflected y  matches");
208             }
209         }
210     }
211 
212     /**
213      * Test the transform method.
214      */
215     @Test
216     public void transformTest()
217     {
218         Transform2d reflectionX = new Transform2d().reflectX();
219         Transform2d reflectionY = new Transform2d().reflectY();
220         // Test time explodes with the 6th power of the length of this array
221         double[] values = new double[] {-100, -0.1, 0, 0.01, 1, 100};
222         for (double translateX : values)
223         {
224             for (double translateY : values)
225             {
226                 Transform2d translation = new Transform2d().translate(translateX, translateY);
227                 for (double scaleX : values)
228                 {
229                     for (double scaleY : values)
230                     {
231                         Transform2d scaling = new Transform2d().scale(scaleX, scaleY);
232                         for (double angle : new double[] {-2, 0, 0.5})
233                         {
234                             Transform2d rotation = new Transform2d().rotation(angle);
235                             for (double shearX : values)
236                             {
237                                 for (double shearY : values)
238                                 {
239                                     Transform2d t = new Transform2d().translate(translateX, translateY).scale(scaleX, scaleY)
240                                             .rotation(angle).shear(shearX, shearY);
241                                     Transform2d tReflectX = new Transform2d().reflectX().translate(translateX, translateY)
242                                             .scale(scaleX, scaleY).rotation(angle).shear(shearX, shearY);
243                                     Transform2d tReflectY = new Transform2d().reflectY().translate(translateX, translateY)
244                                             .scale(scaleX, scaleY).rotation(angle).shear(shearX, shearY);
245                                     Transform2d shearing = new Transform2d().shear(shearX, shearY);
246                                     for (double px : values)
247                                     {
248                                         for (double py : values)
249                                         {
250                                             Point2d p = new Point2d(px, py);
251                                             Point2d tp = t.transform(p);
252                                             Point2d chainP = translation
253                                                     .transform(scaling.transform(rotation.transform(shearing.transform(p))));
254                                             assertEquals(chainP.x, tp.x, 0.0000001, "X");
255                                             assertEquals(chainP.y, tp.y, 0.0000001, "Y");
256                                             tp = tReflectX.transform(p);
257                                             Point2d chainPReflectX = reflectionX.transform(chainP);
258                                             assertEquals(chainPReflectX.x, tp.x, 0.0000001, "RX X");
259                                             assertEquals(chainPReflectX.y, tp.y, 0.0000001, "RX Y");
260                                             tp = tReflectY.transform(p);
261                                             Point2d chainPReflectY = reflectionY.transform(chainP);
262                                             assertEquals(chainPReflectY.x, tp.x, 0.0000001, "RY X");
263                                             assertEquals(chainPReflectY.y, tp.y, 0.0000001, "RY Y");
264                                         }
265                                     }
266                                 }
267                             }
268                         }
269                     }
270                 }
271             }
272         }
273     }
274 
275     /**
276      * Test transformation of a bounding rectangle.
277      */
278     @Test
279     public void transformBounds2dTest()
280     {
281         double[] values = new double[] {-100, 0.1, 0, 0.1, 100};
282         double[] sizes = new double[] {0, 10, 100};
283         Transform2d t = new Transform2d().rotation(0.4).reflectX().scale(0.5, 1.5).shear(2, 3).translate(123, 456);
284         // System.out.println(t);
285         for (double x : values)
286         {
287             for (double y : values)
288             {
289                 for (double xSize : sizes)
290                 {
291                     for (double ySize : sizes)
292                     {
293                         Bounds2d bb = new Bounds2d(x, x + xSize, y, y + ySize);
294                         Point2d[] points = new Point2d[] {new Point2d(x, y), new Point2d(x + xSize, y),
295                                 new Point2d(x, y + ySize), new Point2d(x + xSize, y + ySize)};
296                         Point2d[] transformedPoints = new Point2d[4];
297                         for (int i = 0; i < points.length; i++)
298                         {
299                             transformedPoints[i] = t.transform(points[i]);
300                         }
301                         Bounds2d expected = new Bounds2d(Arrays.stream(transformedPoints).iterator());
302                         Bounds2d got = t.transform(bb);
303                         assertEquals(expected.getMinX(), got.getMinX(), 0.0001, "bb minX");
304                         assertEquals(expected.getMaxX(), got.getMaxX(), 0.0001, "bb maxX");
305                         assertEquals(expected.getMinY(), got.getMinY(), 0.0001, "bb minY");
306                         assertEquals(expected.getMaxY(), got.getMaxY(), 0.0001, "bb maxY");
307                     }
308                 }
309             }
310         }
311     }
312 
313     /**
314      * Reproducible test of multiple transformations on a bounding rectangle.
315      */
316     @Test
317     public void testBoundingRectangle2d()
318     {
319         Bounds2d bounds = new Bounds2d(-4, 4, -4, 4);
320 
321         // identical transformation
322         Transform2d transform = new Transform2d();
323         Bounds2d b = transform.transform(bounds);
324         testBounds2d(b, -4, 4, -4, 4);
325 
326         // translate x, y
327         transform = new Transform2d();
328         transform.translate(20, 10);
329         b = transform.transform(bounds);
330         testBounds2d(b, 20 - 4, 20 + 4, 10 - 4, 10 + 4);
331 
332         // rotate 90 degrees (should be same)
333         transform = new Transform2d();
334         transform.rotation(Math.toRadians(90.0));
335         b = transform.transform(bounds);
336         testBounds2d(b, -4, 4, -4, 4);
337 
338         // rotate 45 degrees in the XY-plane
339         transform = new Transform2d();
340         transform.rotation(Math.toRadians(45.0));
341         double d = 4.0 * Math.sqrt(2.0);
342         b = transform.transform(bounds);
343         testBounds2d(b, -d, d, -d, d);
344 
345         // rotate 45 degrees in the XY-plane and then translate to (10, 20)
346         // note that to do FIRST rotation and THEN translation, the steps have to be built in the OPPOSITE order
347         // since matrix multiplication operates from RIGHT to LEFT.
348         transform = new Transform2d();
349         transform.translate(10, 20);
350         transform.rotation(Math.toRadians(45.0));
351         b = transform.transform(bounds);
352         testBounds2d(b, 10 - d, 10 + d, 20 - d, 20 + d);
353     }
354 
355     /**
356      * Check bounds values.
357      * @param b Bounds2d; the box to test
358      * @param minX double; expected value
359      * @param maxX double; expected value
360      * @param minY double; expected value
361      * @param maxY double; expected value
362      */
363     private void testBounds2d(final Bounds2d b, final double minX, final double maxX, final double minY, final double maxY)
364     {
365         assertEquals(minX, b.getMinX(), 0.001);
366         assertEquals(maxX, b.getMaxX(), 0.001);
367         assertEquals(minY, b.getMinY(), 0.001);
368         assertEquals(maxY, b.getMaxY(), 0.001);
369     }
370 
371     /**
372      * Check that toString returns something descriptive.
373      */
374     @Test
375     public void toStringTest()
376     {
377         assertTrue(new Transform2d().toString().startsWith("Transform2d "), "toString returns something descriptive");
378     }
379 
380     /**
381      * Test the hashCode and equals methods.
382      * @throws SecurityException if that happens uncaught; this test has failed
383      * @throws NoSuchFieldException if that happens uncaught; this test has failed
384      * @throws IllegalAccessException if that happens uncaught; this test has failed
385      * @throws IllegalArgumentException if that happens uncaught; this test has failed
386      */
387     @Test
388     @SuppressWarnings({"unlikely-arg-type"})
389     public void testHashCodeAndEquals()
390             throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException
391     {
392         // Difficult to write a complete test because we can't control the values of the internal fields directly.
393         // We'll "solve" that using reflection.
394         Transform2d reference = new Transform2d();
395         assertEquals(reference, new Transform2d(), "Two different instances with same matrix do test equal");
396         assertEquals(reference.hashCode(), new Transform2d().hashCode(),
397                 "Two different instances with same matrix have same hash code");
398         for (int index = 0; index < 9; index++)
399         {
400             // Alter one element in the mat array at a time and expect the hash code to change and equals to return false.
401             for (double alteration : new double[] {-100, -10, -Math.PI, -0.1, 0.3, Math.E, 123})
402             {
403                 Transform2d other = new Transform2d();
404                 Field matrix = other.getClass().getDeclaredField("mat");
405                 matrix.setAccessible(true);
406                 double[] matrixValues = (double[]) matrix.get(other);
407                 matrixValues[index] = alteration;
408                 assertNotEquals(reference, other, "Modified transform should not be equals");
409                 assertNotEquals(reference.hashCode(), other.hashCode(), "HashCode should be different "
410                         + "(or it does not take all elements of the internal array into account");
411             }
412         }
413         assertTrue(reference.equals(reference), "equal to itself");
414         assertFalse(reference.equals(null), "not equal to null");
415         assertFalse(reference.equals("nope"), "not equal to some other object");
416     }
417 
418 }