View Javadoc
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 }