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