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-2023 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 provided point and return a point with the transformed coordinate.
218 * @param pointIterator Iterator<Point2d>; generates the points to be transformed
219 * @return Iterator<Point2d>; 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 /** {@inheritDoc} */
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 /** {@inheritDoc} */
263 @Override
264 @SuppressWarnings("checkstyle:needbraces")
265 public boolean equals(final Object obj)
266 {
267 if (this == obj)
268 return true;
269 if (obj == null)
270 return false;
271 if (getClass() != obj.getClass())
272 return false;
273 Transform2d other = (Transform2d) obj;
274 if (!Arrays.equals(this.mat, other.mat))
275 return false;
276 return true;
277 }
278
279 /** {@inheritDoc} */
280 @Override
281 public String toString()
282 {
283 return "Transform2d [mat=" + Arrays.toString(this.mat) + "]";
284 }
285
286 }