1 /*
2 * Copyright (C) 2012 Alberto Irurueta Carro (alberto@irurueta.com)
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.irurueta.geometry;
17
18 import com.irurueta.algebra.AlgebraException;
19 import com.irurueta.algebra.Matrix;
20 import com.irurueta.algebra.Utils;
21 import com.irurueta.algebra.WrongSizeException;
22
23 import java.io.Serializable;
24 import java.util.Objects;
25
26 /**
27 * This class defines the amount of rotation for 2D points or lines.
28 */
29 @SuppressWarnings("DuplicatedCode")
30 public class Rotation2D implements Serializable {
31
32 /**
33 * Constant defining the number of rows on a 2D rotation matrix expressed
34 * in inhomogeneous coordinates.
35 */
36 public static final int ROTATION2D_INHOM_MATRIX_ROWS = 2;
37
38 /**
39 * Constant defining the number of columns on a 2D rotation matrix expressed
40 * in inhomogeneous coordinates.
41 */
42 public static final int ROTATION2D_INHOM_MATRIX_COLS = 2;
43
44 /**
45 * Constant defining the number of rows on a 2D rotation matrix expressed
46 * in homogeneous coordinates.
47 */
48 public static final int ROTATION2D_HOM_MATRIX_ROWS = 3;
49
50 /**
51 * Constant defining the number of columns on a 2D rotation matrix expressed
52 * in homogeneous coordinates.
53 */
54 public static final int ROTATION2D_HOM_MATRIX_COLS = 3;
55
56 /**
57 * Constant defining threshold to determine whether a matrix is orthogonal
58 * or not and has determinant equal to 1. Rotation matrices must fulfill
59 * those requirements.
60 */
61 public static final double MATRIX_VALID_THRESHOLD = 1e-9;
62
63 /**
64 * Constant defining minimum allowed threshold.
65 */
66 public static final double MIN_THRESHOLD = 0.0;
67
68 /**
69 * Default threshold to determine if two instances are equal.
70 */
71 public static final double DEFAULT_COMPARISON_THRESHOLD = 1e-9;
72
73 /**
74 * Constant defining minimum allowed comparison threshold.
75 */
76 public static final double MIN_COMPARISON_THRESHOLD = 0.0;
77
78 /**
79 * Private member containing amount of rotation expressed in radians.
80 */
81 private double theta;
82
83 /**
84 * Empty Constructor.
85 * Initializes rotation to zero radians.
86 */
87 public Rotation2D() {
88 theta = 0.0;
89 }
90
91 /**
92 * Copy constructor.
93 * Copies provided rotation into this instance.
94 *
95 * @param rotation Instance to be copied.
96 */
97 public Rotation2D(final Rotation2D rotation) {
98 theta = rotation.theta;
99 }
100
101 /**
102 * Constructor.
103 * Creates a 2D rotation using provided matrix.
104 * Provided matrix can be expressed in either homogeneous or inhomogeneous
105 * coordinates, and it must also be orthogonal and having determinant equal
106 * to 1.
107 * The threshold to determine whether provided matrix is orthonormal will
108 * be MATRIX_VALID_THRESHOLD.
109 *
110 * @param m Matrix to create rotation from.
111 * @throws InvalidRotationMatrixException Raised if provided matrix is not
112 * valid (its size is wrong, or it is not orthonormal).
113 * @see #isValidRotationMatrix(Matrix)
114 */
115 public Rotation2D(final Matrix m) throws InvalidRotationMatrixException {
116 fromMatrix(m);
117 }
118
119 /**
120 * Constructor.
121 * Creates a 2D rotation using provided matrix.
122 * Provided matrix can be expressed in either homogeneous or inhomogeneous
123 * coordinates, and it must also be orthogonal up to provided threshold, and
124 * must have determinant equal to 1.
125 *
126 * @param m Matrix to create rotation from.
127 * @param threshold Threshold to determine whether matrix is orthonormal.
128 * @throws InvalidRotationMatrixException Raised if provided matrix is not
129 * valid (its size is wrong, or it is not orthonormal).
130 * @throws IllegalArgumentException Raised if provided threshold is negative
131 * @see #isValidRotationMatrix(Matrix)
132 */
133 public Rotation2D(final Matrix m, final double threshold) throws InvalidRotationMatrixException {
134 fromMatrix(m, threshold);
135 }
136
137 /**
138 * Constructor.
139 * Creates a 2D rotation using provided rotation value expressed in radians
140 *
141 * @param theta Rotation amount expressed in radians.
142 */
143 public Rotation2D(final double theta) {
144 this.theta = theta;
145 }
146
147 /**
148 * Returns rotation amount expressed in radians.
149 *
150 * @return Rotation amount expressed in radians.
151 */
152 public double getTheta() {
153 return theta;
154 }
155
156 /**
157 * Sets rotation amount expressed in radians.
158 *
159 * @param theta rotation angle.
160 */
161 public void setTheta(final double theta) {
162 this.theta = theta;
163 }
164
165 /**
166 * Returns a 2D rotation which is inverse to this instance.
167 * In other words, the combination of this rotation with its inverse
168 * produces no change.
169 *
170 * @return Inverse 2D rotation.
171 */
172 public Rotation2D inverseRotation() {
173 final var result = new Rotation2D();
174 inverseRotation(result);
175 return result;
176 }
177
178 /**
179 * Sets into provided Rotation2D instance a rotation inverse to this
180 * instance.
181 * The combination of this rotation with its inverse produces no change.
182 *
183 * @param result Instance where inverse rotation will be set.
184 */
185 public void inverseRotation(final Rotation2D result) {
186 result.setTheta(-theta);
187 }
188
189 /**
190 * Returns this 2D rotation instance expressed as a 2x2 inhomogeneous matrix.
191 *
192 * @return Rotation matrix expressed in inhomogeneous coordinates.
193 */
194 public Matrix asInhomogeneousMatrix() {
195 Matrix result = null;
196 try {
197 result = new Matrix(ROTATION2D_INHOM_MATRIX_ROWS, ROTATION2D_INHOM_MATRIX_COLS);
198 asInhomogeneousMatrix(result);
199 } catch (final WrongSizeException ignore) {
200 // never happens
201 }
202 return result;
203 }
204
205 /**
206 * Sets into provided Matrix instance this 2D rotation expressed as a
207 * 2x2 inhomogeneous matrix.
208 *
209 * @param result Matrix where rotation will be set.
210 * @throws IllegalArgumentException Raised if provided instance does not
211 * have size 2x2.
212 */
213 public void asInhomogeneousMatrix(final Matrix result) {
214 if (result.getRows() != ROTATION2D_INHOM_MATRIX_ROWS || result.getColumns() != ROTATION2D_INHOM_MATRIX_COLS) {
215 throw new IllegalArgumentException();
216 }
217
218 // set result
219 final var sinTheta = Math.sin(theta);
220 final var cosTheta = Math.cos(theta);
221 result.setElementAt(0, 0, cosTheta);
222 result.setElementAt(1, 0, sinTheta);
223 result.setElementAt(0, 1, -sinTheta);
224 result.setElementAt(1, 1, cosTheta);
225 }
226
227 /**
228 * Returns this 2D rotation instance expressed as a 3x3 homogeneous matrix.
229 *
230 * @return Rotation matrix expressed in homogeneous coordinates.
231 */
232 public Matrix asHomogeneousMatrix() {
233 Matrix result = null;
234 try {
235 result = new Matrix(ROTATION2D_HOM_MATRIX_ROWS, ROTATION2D_HOM_MATRIX_COLS);
236 asHomogeneousMatrix(result);
237 } catch (final WrongSizeException ignore) {
238 // never happens
239 }
240 return result;
241 }
242
243 /**
244 * Sets into provided Matrix instance this 2D rotation expressed as a
245 * 3x3 homogeneous matrix.
246 *
247 * @param result Matrix where rotation will be set.
248 * @throws IllegalArgumentException Raised if provided instance does not
249 * have size 3x3.
250 */
251 public void asHomogeneousMatrix(final Matrix result) {
252 if (result.getRows() != ROTATION2D_HOM_MATRIX_ROWS || result.getColumns() != ROTATION2D_HOM_MATRIX_COLS) {
253 throw new IllegalArgumentException();
254 }
255
256 // set result
257 final var sinTheta = Math.sin(theta);
258 final var cosTheta = Math.cos(theta);
259 result.setElementAt(0, 0, cosTheta);
260 result.setElementAt(1, 0, sinTheta);
261 result.setElementAt(2, 0, 0.0);
262 result.setElementAt(0, 1, -sinTheta);
263 result.setElementAt(1, 1, cosTheta);
264 result.setElementAt(2, 1, 0.0);
265 result.setElementAt(0, 2, 0.0);
266 result.setElementAt(1, 2, 0.0);
267 result.setElementAt(2, 2, 1.0);
268 }
269
270 /**
271 * Sets amount of rotation from provided rotation matrix.
272 * Provided matrix must be orthogonal (i.e. squared, non-singular, it's
273 * transpose must be its inverse) and must have determinant equal to 1.
274 * Provided matrix can be expressed in either inhomogeneous (2x2) or
275 * homogeneous (3x3) coordinates.
276 *
277 * @param m Provided rotation matrix.
278 * @param threshold Threshold to determine whether matrix is orthonormal.
279 * @throws InvalidRotationMatrixException Raised if provided matrix is not
280 * valid (has wrong size, or it is not orthonormal).
281 * @throws IllegalArgumentException Raised if provided threshold is negative
282 * @see #isValidRotationMatrix(Matrix)
283 */
284 public final void fromMatrix(final Matrix m, final double threshold) throws InvalidRotationMatrixException {
285 if (m.getRows() == ROTATION2D_INHOM_MATRIX_ROWS && m.getColumns() == ROTATION2D_INHOM_MATRIX_COLS) {
286 // inhomogeneous matrix
287 fromInhomogeneousMatrix(m, threshold);
288 } else if (m.getRows() == ROTATION2D_HOM_MATRIX_ROWS && m.getColumns() == ROTATION2D_HOM_MATRIX_COLS) {
289 // homogeneous matrix
290 fromHomogeneousMatrix(m, threshold);
291 } else {
292 throw new InvalidRotationMatrixException();
293 }
294 }
295
296 /**
297 * Sets amount of rotation from provided rotation matrix.
298 * Provided matrix must be orthogonal (i.e. squared, non-singular, it's
299 * transpose must be its inverse) and must have determinant equal to 1.
300 * Provided matrix can be expressed in either inhomogeneous (2x2) or
301 * homogeneous (3x3) coordinates.
302 * Because threshold is not provided it is used MATRIX_VALID_THRESHOLD
303 * instead.
304 *
305 * @param m Provided rotation matrix.
306 * @throws InvalidRotationMatrixException Raised if provided matrix is not
307 * valid (has wrong size, or it is not orthonormal).
308 * @see #isValidRotationMatrix(Matrix)
309 */
310 public final void fromMatrix(final Matrix m) throws InvalidRotationMatrixException {
311 fromMatrix(m, MATRIX_VALID_THRESHOLD);
312 }
313
314 /**
315 * Sets amount of rotation from provided inhomogeneous rotation matrix.
316 * Provided matrix must be orthogonal (i.e. squared, non-singular, it's
317 * transpose must be its inverse) and must have determinant equal to 1.
318 * Provided matrix must also have size 2x2.
319 *
320 * @param m Provided rotation matrix.
321 * @param threshold Threshold to determine whether matrix is orthonormal.
322 * @throws InvalidRotationMatrixException Raised if provided matrix is not
323 * valid (has wrong size, or it is not orthonormal).
324 * @throws IllegalArgumentException Raised if provided threshold is negative.
325 * @see #isValidRotationMatrix(Matrix)
326 */
327 public void fromInhomogeneousMatrix(final Matrix m, final double threshold) throws InvalidRotationMatrixException {
328 if (m.getRows() != ROTATION2D_INHOM_MATRIX_ROWS || m.getColumns() != ROTATION2D_INHOM_MATRIX_COLS) {
329 throw new InvalidRotationMatrixException();
330 }
331 if (!isValidRotationMatrix(m, threshold)) {
332 throw new InvalidRotationMatrixException();
333 }
334
335 final var cosTheta = m.getElementAt(0, 0);
336 final var sinTheta = m.getElementAt(1, 0);
337
338 // estimated theta will be in the range -pi, pi.
339 theta = Math.atan2(sinTheta, cosTheta);
340 }
341
342 /**
343 * Sets amount of rotation from provided inhomogeneous rotation matrix.
344 * Provided matrix must be orthogonal (i.e. squared, non-singular, it's
345 * transpose must be its inverse) and must have determinant equal to 1.
346 * Provided matrix must also have size 2x2.
347 * Because threshold is not provided it is used MATRIX_VALID_THRESHOLD
348 * instead.
349 *
350 * @param m Provided rotation matrix.
351 * @throws InvalidRotationMatrixException Raised if provided matrix is not
352 * valid (has wrong size, or it is not orthonormal).
353 * @see #isValidRotationMatrix(Matrix)
354 */
355 public void fromInhomogeneousMatrix(final Matrix m) throws InvalidRotationMatrixException {
356 fromInhomogeneousMatrix(m, MATRIX_VALID_THRESHOLD);
357 }
358
359 /**
360 * Sets amount of rotation from provided homogeneous rotation matrix.
361 * Provided matrix must be orthogonal (i.e. squared, non-singular, it's
362 * transpose must be its inverse) and must have determinant equal to 1.
363 * Provided matrix must also have size 3x3, and its last row and column must
364 * be zero, except for element in last row and column which must be 1.
365 *
366 * @param m Provided rotation matrix.
367 * @param threshold Threshold to determine whether matrix is orthonormal.
368 * @throws InvalidRotationMatrixException Raised if provided matrix is not
369 * valid (has wrong size, or it is not orthonormal).
370 * @throws IllegalArgumentException Raised if provided threshold is negative.
371 * @see #isValidRotationMatrix(Matrix)
372 */
373 public void fromHomogeneousMatrix(final Matrix m, final double threshold) throws InvalidRotationMatrixException {
374 if (m.getRows() != ROTATION2D_HOM_MATRIX_ROWS || m.getColumns() != ROTATION2D_HOM_MATRIX_COLS) {
375 throw new InvalidRotationMatrixException();
376 }
377 if (!isValidRotationMatrix(m, threshold)) {
378 throw new InvalidRotationMatrixException();
379 }
380 if (Math.abs(m.getElementAt(2, 0)) > threshold
381 || Math.abs(m.getElementAt(2, 1)) > threshold
382 || Math.abs(m.getElementAt(0, 2)) > threshold
383 || Math.abs(m.getElementAt(1, 2)) > threshold
384 || Math.abs(m.getElementAt(2, 2) - 1.0) > threshold) {
385 throw new InvalidRotationMatrixException();
386 }
387
388 final var cosTheta = m.getElementAt(0, 0);
389 final var sinTheta = m.getElementAt(1, 0);
390
391 // estimated theta will be in the range -pi, pi.
392 theta = Math.atan2(sinTheta, cosTheta);
393 }
394
395 /**
396 * Sets amount of rotation from provided homogeneous rotation matrix.
397 * Provided matrix must be orthogonal (i.e. squared, non-singular), its
398 * transpose must be its inverse and must have determinant equal to 1.
399 * Provided matrix must also have size 3x3, and its last row and column must
400 * be zero, except for element in last row and column which must be 1
401 * Because threshold is not provided it is used MATRIX_VALID_THRESHOLD
402 * instead.
403 *
404 * @param m Provided rotation matrix.
405 * @throws InvalidRotationMatrixException Raised if provided matrix is not
406 * valid (has wrong size, or it is not orthonormal).
407 * @see #isValidRotationMatrix(Matrix)
408 */
409 public void fromHomogeneousMatrix(final Matrix m) throws InvalidRotationMatrixException {
410 fromHomogeneousMatrix(m, MATRIX_VALID_THRESHOLD);
411 }
412
413 /**
414 * Rotates a 2D point using the origin of coordinates as the axis of
415 * rotation.
416 * Point will be rotated by the amount of rotation contained in this
417 * instance.
418 *
419 * @param inputPoint Input point to be rotated.
420 * @param resultPoint Rotated point.
421 */
422 public void rotate(final Point2D inputPoint, final Point2D resultPoint) {
423 try {
424 final var r = asHomogeneousMatrix();
425 final var p = new Matrix(Point2D.POINT2D_HOMOGENEOUS_COORDINATES_LENGTH, 1);
426
427 // to increase accuracy
428 inputPoint.normalize();
429 p.setElementAt(0, 0, inputPoint.getHomX());
430 p.setElementAt(1, 0, inputPoint.getHomY());
431 p.setElementAt(2, 0, inputPoint.getHomW());
432
433 // Rotated point below is R * p
434 r.multiply(p);
435
436 resultPoint.setHomogeneousCoordinates(r.getElementAt(0, 0), r.getElementAt(1, 0),
437 r.getElementAt(2, 0));
438 } catch (final WrongSizeException ignore) {
439 // never happens
440 }
441 }
442
443 /**
444 * Returns a 2D point containing a rotated version of provided point.
445 * Point will be rotated using the origin of the coordinates as the axis of
446 * rotation.
447 * Point will be rotated by the amount of rotation contained in this
448 * instance.
449 *
450 * @param point Point to be rotated.
451 * @return Rotated point.
452 */
453 public Point2D rotate(final Point2D point) {
454 final var result = new HomogeneousPoint2D();
455 rotate(point, result);
456 return result;
457 }
458
459 /**
460 * Rotates a line using the origin of coordinates as the axis of rotation.
461 * Line2D will be rotated by the amount of rotation contained in this
462 * instance.
463 *
464 * @param inputLine Input line to be rotated.
465 * @param resultLine Rotated line.
466 */
467 public void rotate(final Line2D inputLine, final Line2D resultLine) {
468 try {
469 final var r = asHomogeneousMatrix();
470 // because of the duality theorem:
471 // l'*m = 0 --> l*R^-1*R*m = 0 --> l2' = l'*R^-1 and m2 = R*m
472 // where l2 and m2 are rotated line and point, however rotated
473 // line uses the inverse rotation, which is the transposed matrix
474 // Hence l2' = l' * R', and by undoing the transposition
475 // l2 = (l' * R')' = R'' * l'' = R * l
476
477 final var l = new Matrix(Line2D.LINE_NUMBER_PARAMS, 1);
478
479 // to increase accuracy
480 inputLine.normalize();
481 l.setElementAt(0, 0, inputLine.getA());
482 l.setElementAt(1, 0, inputLine.getB());
483 l.setElementAt(2, 0, inputLine.getC());
484
485 // Rotated line below is R * l
486 r.multiply(l);
487
488 resultLine.setParameters(r.getElementAt(0, 0), r.getElementAt(1, 0),
489 r.getElementAt(2, 0));
490 } catch (final WrongSizeException ignore) {
491 // never happens
492 }
493 }
494
495 /**
496 * Returns a line containing a rotated version of provided line.
497 * Line2D will be rotated using the origin of the coordinates as the axis of
498 * rotation.
499 * Line2D will be rotated by the amount of rotation contained in this
500 * instance.
501 *
502 * @param line Line2D to be rotated.
503 * @return Rotated line.
504 */
505 public Line2D rotate(final Line2D line) {
506 final var result = new Line2D();
507 rotate(line, result);
508 return result;
509 }
510
511 /**
512 * Returns boolean indicating whether provided matrix is a valid matrix for
513 * a rotation.
514 * Rotation matrices must be orthogonal and must have determinant equal to 1.
515 *
516 * @param m Input matrix to be checked.
517 * @param threshold Threshold to determine whether matrix is orthogonal and
518 * whether determinant is one.
519 * @return True if matrix is valid, false otherwise.
520 * @throws IllegalArgumentException Raised if provided threshold is negative.
521 */
522 public static boolean isValidRotationMatrix(final Matrix m, final double threshold) {
523 if (threshold < MIN_THRESHOLD) {
524 throw new IllegalArgumentException();
525 }
526
527 try {
528 return Utils.isOrthogonal(m, threshold) && (Math.abs(Utils.det(m)) - 1.0) < threshold;
529 } catch (final AlgebraException e) {
530 return false;
531 }
532 }
533
534 /**
535 * Returns boolean indicating whether provided matrix is a valid matrix for
536 * a rotation.
537 * Rotation matrices must be orthogonal and must have determinant equal to 1
538 * Because threshold is not provided, it is used MATRIX_VALID_THRESHOLD
539 * instead.
540 *
541 * @param m Input matrix to be checked.
542 * @return True if matrix is valid, false otherwise.
543 * @throws IllegalArgumentException Raised if provided threshold is negative.
544 */
545 public static boolean isValidRotationMatrix(final Matrix m) {
546 return isValidRotationMatrix(m, MATRIX_VALID_THRESHOLD);
547 }
548
549 /**
550 * Combines provided rotation with this rotation and returns the result as
551 * a new Rotation2D instance.
552 *
553 * @param rotation Input rotation to be combined.
554 * @return Combined rotation, which is equal to the sum of provided rotation
555 * with this rotation.
556 */
557 public Rotation2D combineAndReturnNew(final Rotation2D rotation) {
558 final var result = new Rotation2D();
559 combine(this, rotation, result);
560 return result;
561 }
562
563 /**
564 * Combines provided rotation into this rotation resulting in the sum of
565 * both rotations.
566 *
567 * @param rotation Input rotation to be combined.
568 */
569 public void combine(final Rotation2D rotation) {
570 combine(this, rotation, this);
571 }
572
573 /**
574 * Combines the rotation of instances rot1 and rot1 into provided result
575 * instance.
576 *
577 * @param rot1 1st input rotation.
578 * @param rot2 2nd input rotation.
579 * @param result Combined rotation, which is equal to the sum of provided
580 * input rotations.
581 */
582 public static void combine(final Rotation2D rot1, final Rotation2D rot2, final Rotation2D result) {
583 result.theta = rot1.theta + rot2.theta;
584 }
585
586 /**
587 * Determines if two Rotation2D instances are equal up to provided threshold
588 * or not (i.e. have the same rotation).
589 *
590 * @param other other rotation to compare.
591 * @param threshold threshold to determine if they are equal.
592 * @return true if they are equal, false otherwise.
593 * @throws IllegalArgumentException if threshold is negative.
594 */
595 public boolean equals(final Rotation2D other, final double threshold) {
596 if (threshold < MIN_COMPARISON_THRESHOLD) {
597 throw new IllegalArgumentException();
598 }
599
600 return Math.abs(other.theta - theta) <= threshold;
601 }
602
603 /**
604 * Determines if two Rotation2D instances are equal or not (i.e. have the
605 * same rotation).
606 *
607 * @param other other object to compare.
608 * @return true if they are equal, false otherwise.
609 */
610 public boolean equals(final Rotation2D other) {
611 return equals(other, DEFAULT_COMPARISON_THRESHOLD);
612 }
613
614 /**
615 * Determines if two Rotation2D instances are equal or not (i.e. have the
616 * same rotation).
617 *
618 * @param obj other object to compare.
619 * @return true if they are equal, false otherwise.
620 */
621 @Override
622 public boolean equals(final Object obj) {
623 if (obj == this) {
624 return true;
625 }
626 if (!(obj instanceof Rotation2D)) {
627 return false;
628 }
629
630 return equals((Rotation2D) obj);
631 }
632
633 /**
634 * Hash code to compare instances.
635 *
636 * @return hash code to compare instances.
637 */
638 @Override
639 public int hashCode() {
640 return Objects.hash(theta);
641 }
642 }