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