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<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 @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 }