View Javadoc
1   package org.djutils.draw;
2   
3   import java.util.Arrays;
4   import java.util.Iterator;
5   
6   import org.djutils.draw.bounds.Bounds2d;
7   import org.djutils.draw.point.Point2d;
8   
9   /**
10   * Transform2d contains a MUTABLE transformation object that can transform points (x,y) based on e.g, rotation and translation.
11   * It uses an affine transform matrix that can be built up from different components (translation, rotation, scaling,
12   * reflection, shearing).
13   * <p>
14   * Copyright (c) 2020-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
15   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
16   * </p>
17   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
18   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
19   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
20   */
21  public class Transform2d implements Cloneable
22  {
23      /** The 3x3 transformation matrix, initialized as the Identity matrix. */
24      private double[] mat = new double[] {1, 0, 0, 0, 1, 0, 0, 0, 1};
25  
26      /**
27       * Multiply a 3x3 matrix (stored as a 9-value array by row) with a 4-value vector.
28       * @param m the matrix
29       * @param v the vector
30       * @return double[3]; the result of <code>m x v</code>
31       */
32      protected static double[] mulMatVec(final double[] m, final double[] v)
33      {
34          double[] result = new double[3];
35          for (int i = 0; i < 3; i++)
36          {
37              result[i] = m[3 * i] * v[0] + m[3 * i + 1] * v[1] + m[3 * i + 2] * v[2];
38          }
39          return result;
40      }
41  
42      /**
43       * Multiply a 3x3 matrix (stored as a 9-value array by row) with a 3-value vector and a 1 for the 3rd value.
44       * @param m the matrix
45       * @param v the vector
46       * @return double[2]; the result of <code>m x (v1, v2, 1)</code>, with the last value left out
47       */
48      protected static double[] mulMatVec2(final double[] m, final double[] v)
49      {
50          double[] result = new double[2];
51          for (int i = 0; i < 2; i++)
52          {
53              result[i] = m[3 * i] * v[0] + m[3 * i + 1] * v[1] + m[3 * i + 2];
54          }
55          return result;
56      }
57  
58      /**
59       * Multiply a 3x3 matrix (stored as a 9-value array by row) with another 3x3-matrix.
60       * @param m1 the first matrix
61       * @param m2 the second matrix
62       * @return double[9]; the result of <code>m1 x m2</code>
63       */
64      protected static double[] mulMatMat(final double[] m1, final double[] m2)
65      {
66          double[] result = new double[9];
67          for (int i = 0; i < 3; i++)
68          {
69              for (int j = 0; j < 3; j++)
70              {
71                  result[3 * i + j] = m1[3 * i] * m2[j] + m1[3 * i + 1] * m2[j + 3] + +m1[3 * i + 2] * m2[j + 6];
72              }
73          }
74          return result;
75      }
76  
77      /**
78       * Get a safe copy of the affine transformation matrix.
79       * @return a safe copy of the affine transformation matrix
80       */
81      public double[] getMat()
82      {
83          return this.mat.clone();
84      }
85  
86      /**
87       * Transform coordinates by a vector (tx, ty). Note that to carry out multiple operations, the steps have to be built in the
88       * OPPOSITE order since matrix multiplication operates from RIGHT to LEFT.
89       * @param tx the translation value for the x-coordinates
90       * @param ty the translation value for the y-coordinates
91       * @return the new transformation matrix after applying the transform
92       */
93      public Transform2d translate(final double tx, final double ty)
94      {
95          if (tx == 0.0 && ty == 0.0)
96          {
97              return this;
98          }
99          this.mat = mulMatMat(this.mat, new double[] {1, 0, tx, 0, 1, ty, 0, 0, 1});
100         return this;
101     }
102 
103     /**
104      * Translate coordinates by a the x and y values contained in a Point2d. Note that to carry out multiple operations, the
105      * steps have to be built in the OPPOSITE order since matrix multiplication operates from RIGHT to LEFT.
106      * @param point the point containing the x and y translation values
107      * @return the new transformation matrix after applying the transform
108      */
109     public Transform2d translate(final Point2d point)
110     {
111         if (point.x == 0.0 && point.y == 0.0)
112         {
113             return this;
114         }
115         this.mat = mulMatMat(this.mat, new double[] {1, 0, point.x, 0, 1, point.y, 0, 0, 1});
116         return this;
117     }
118 
119     /**
120      * Scale all coordinates with a factor for x, and y. A scale factor of 1 leaves the coordinate unchanged. Note that to carry
121      * out multiple operations, the steps have to be built in the OPPOSITE order since matrix multiplication operates from RIGHT
122      * to LEFT.
123      * @param sx the scale factor for the x-coordinates
124      * @param sy the scale factor for the y-coordinates
125      * @return the new transformation matrix after applying the transform
126      */
127     public Transform2d scale(final double sx, final double sy)
128     {
129         if (sx == 1.0 && sy == 1.0)
130         {
131             return this;
132         }
133         this.mat = mulMatMat(this.mat, new double[] {sx, 0, 0, 0, sy, 0, 0, 0, 1});
134         return this;
135     }
136 
137     /**
138      * The rotation around the origin with an angle in radians. Note that to carry out multiple operations, the steps have to be
139      * built in the OPPOSITE order since matrix multiplication operates from RIGHT to LEFT.
140      * @param angle the angle to rotate the coordinates with with around the origin
141      * @return the new transformation matrix after applying the transform
142      */
143     public Transform2d rotation(final double angle)
144     {
145         if (angle == 0.0)
146         {
147             return this;
148         }
149         double c = Math.cos(angle);
150         double s = Math.sin(angle);
151         this.mat = mulMatMat(this.mat, new double[] {c, -s, 0, s, c, 0, 0, 0, 1});
152         return this;
153     }
154 
155     /**
156      * The 2d shear leaves the xy-coordinate plane for z=0 untouched. An x-coordinate with a value of 1 is translated by sx, and
157      * an x-coordinate with another value is translated by x*sx. Similarly, a y-coordinate with a value of 1 is translated by xy
158      * and a y-coordinate with another value is translated by y*sy. Note that to carry out multiple operations, the steps have
159      * to be built in the OPPOSITE order since matrix multiplication operates from RIGHT to LEFT.
160      * @param sx the shear factor in the x-direction
161      * @param sy the shear factor in the y-direction
162      * @return the new transformation matrix after applying the transform
163      */
164     public Transform2d shear(final double sx, final double sy)
165     {
166         if (sx == 0.0 && sy == 0.0)
167         {
168             return this;
169         }
170         this.mat = mulMatMat(this.mat, new double[] {1, sx, 0, sy, 1, 0, 0, 0, 1});
171         return this;
172     }
173 
174     /**
175      * The reflection of the x-coordinate, by mirroring it in the yz-plane (the plane with x=0). Note that to carry out multiple
176      * operations, the steps have to be built in the OPPOSITE order since matrix multiplication operates from RIGHT to LEFT.
177      * @return the new transformation matrix after applying the transform
178      */
179     public Transform2d reflectX()
180     {
181         this.mat = mulMatMat(this.mat, new double[] {-1, 0, 0, 0, 1, 0, 0, 0, 1});
182         return this;
183     }
184 
185     /**
186      * The reflection of the y-coordinate, by mirroring it in the xz-plane (the plane with y=0). Note that to carry out multiple
187      * operations, the steps have to be built in the OPPOSITE order since matrix multiplication operates from RIGHT to LEFT.
188      * @return the new transformation matrix after applying the transform
189      */
190     public Transform2d reflectY()
191     {
192         this.mat = mulMatMat(this.mat, new double[] {1, 0, 0, 0, -1, 0, 0, 0, 1});
193         return this;
194     }
195 
196     /**
197      * Apply the stored transform on the xy-vector and return the transformed vector. For speed reasons, no checks on correct
198      * size of the vector is done.
199      * @param xy double[2] the provided vector
200      * @return double[2]; the transformed vector
201      */
202     public double[] transform(final double[] xy)
203     {
204         return mulMatVec2(this.mat, xy);
205     }
206 
207     /**
208      * Apply the stored transform on the provided point and return a point with the transformed coordinate.
209      * @param point the point to be transformed
210      * @return a point with the transformed coordinates
211      */
212     public Point2d transform(final Point2d point)
213     {
214         return new Point2d(mulMatVec2(this.mat, new double[] {point.x, point.y}));
215     }
216 
217     /**
218      * Apply the stored transform on the points generated by the provided pointIterator.
219      * @param pointIterator generates the points to be transformed
220      * @return an iterator that will generator all transformed points
221      */
222     public Iterator<Point2d> transform(final Iterator<Point2d> pointIterator)
223     {
224         return new Iterator<Point2d>()
225         {
226 
227             @Override
228             public boolean hasNext()
229             {
230                 return pointIterator.hasNext();
231             }
232 
233             @Override
234             public Point2d next()
235             {
236                 return transform(pointIterator.next());
237             }
238         };
239     }
240 
241     /**
242      * Apply the stored transform on the provided Bounds2d and return a new Bounds2d with the bounds of the transformed
243      * coordinates. All 4 corner points have to be transformed, since we do not know which of the 4 points will result in the
244      * lowest and highest x and y coordinates.
245      * @param boundingRectangle the bounds to be transformed
246      * @return the new bounds based on the transformed coordinates
247      */
248     public Bounds2d transform(final Bounds2d boundingRectangle)
249     {
250         return new Bounds2d(transform(boundingRectangle.iterator()));
251     }
252 
253     @Override
254     public int hashCode()
255     {
256         final int prime = 31;
257         int result = 1;
258         result = prime * result + Arrays.hashCode(this.mat);
259         return result;
260     }
261 
262     @Override
263     @SuppressWarnings("checkstyle:needbraces")
264     public boolean equals(final Object obj)
265     {
266         if (this == obj)
267             return true;
268         if (obj == null)
269             return false;
270         if (getClass() != obj.getClass())
271             return false;
272         Transform2d other = (Transform2d) obj;
273         if (!Arrays.equals(this.mat, other.mat))
274             return false;
275         return true;
276     }
277 
278     @Override
279     public String toString()
280     {
281         return "Transform2d [mat=" + Arrays.toString(this.mat) + "]";
282     }
283 
284 }