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 }