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