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 }