001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.euclidean.twod.rotation;
018
019import org.apache.commons.geometry.euclidean.EuclideanTransform;
020import org.apache.commons.geometry.euclidean.internal.Vectors;
021import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
022import org.apache.commons.geometry.euclidean.twod.Vector2D;
023
024/** Class representing a rotation in 2 dimensional Euclidean space. Positive
025 * rotations are in a <em>counter-clockwise</em> direction.
026 */
027public final class Rotation2D implements EuclideanTransform<Vector2D> {
028
029    /** Instance representing a rotation of zero radians. */
030    private static final Rotation2D IDENTITY = new Rotation2D(0);
031
032    /** The angle of the rotation in radians. */
033    private final double angle;
034
035    /** The cosine of the angle of rotation, cached to avoid repeated computation. */
036    private final double cosAngle;
037
038    /** The sine of the angle of rotation, cached to avoid repeated computation. */
039    private final double sinAngle;
040
041    /** Create a new instance representing the given angle.
042     * @param angle the angle of rotation, in radians
043     */
044    private Rotation2D(final double angle) {
045        this.angle = angle;
046        this.cosAngle = Math.cos(angle);
047        this.sinAngle = Math.sin(angle);
048    }
049
050    /** Get the angle of rotation in radians.
051     * @return the angle of rotation in radians
052     */
053    public double getAngle() {
054        return angle;
055    }
056
057    /** {@inheritDoc} */
058    @Override
059    public Rotation2D inverse() {
060        return new Rotation2D(-angle);
061    }
062
063    /** {@inheritDoc}
064     *
065     * <p>This method simply returns true since rotations always preserve the orientation
066     * of the space.</p>
067     */
068    @Override
069    public boolean preservesOrientation() {
070        return true;
071    }
072
073    /** {@inheritDoc} */
074    @Override
075    public Vector2D apply(final Vector2D pt) {
076        final double x = pt.getX();
077        final double y = pt.getY();
078
079        return Vector2D.of(
080                    (x * cosAngle) - (y * sinAngle),
081                    (x * sinAngle) + (y * cosAngle)
082                );
083    }
084
085    /** {@inheritDoc}
086     *
087     * <p>This method simply calls {@code apply(vec)} since rotations treat
088     * points and vectors similarly.</p>
089     * */
090    @Override
091    public Vector2D applyVector(final Vector2D vec) {
092        return apply(vec);
093    }
094
095    /** Return an {@link AffineTransformMatrix2D} representing the same rotation
096     * as this instance.
097     * @return a transform matrix representing the same rotation
098     */
099    public AffineTransformMatrix2D toMatrix() {
100        return AffineTransformMatrix2D.of(
101                    cosAngle, -sinAngle, 0.0,
102                    sinAngle, cosAngle, 0.0
103                );
104    }
105
106    /** {@inheritDoc} */
107    @Override
108    public int hashCode() {
109        return Double.hashCode(angle);
110    }
111
112    /** {@inheritDoc} */
113    @Override
114    public boolean equals(final Object obj) {
115        if (this == obj) {
116            return true;
117        }
118        if (!(obj instanceof Rotation2D)) {
119            return false;
120        }
121
122        final Rotation2D other = (Rotation2D) obj;
123
124        return Double.compare(this.angle, other.angle) == 0;
125    }
126
127    /** {@inheritDoc} */
128    @Override
129    public String toString() {
130        final StringBuilder sb = new StringBuilder();
131        sb.append(this.getClass().getSimpleName())
132            .append("[angle=")
133            .append(angle)
134            .append(']');
135
136        return sb.toString();
137    }
138
139    /** Create a new instance with the given angle of rotation.
140     * @param angle the angle of rotation in radians
141     * @return a new instance with the given angle of rotation
142     */
143    public static Rotation2D of(final double angle) {
144        return new Rotation2D(angle);
145    }
146
147    /** Return an instance representing the identity rotation, ie a rotation
148     * of zero radians.
149     * @return an instance representing a rotation of zero radians
150     */
151    public static Rotation2D identity() {
152        return IDENTITY;
153    }
154
155    /** Create a rotation instance that rotates the vector {@code u} to point in the direction of
156     * vector {@code v}.
157     * @param u input vector
158     * @param v target vector
159     * @return a rotation instance that rotates {@code u} to point in the direction of {@code v}
160     * @throws IllegalArgumentException if either vector cannot be normalized
161     */
162    public static Rotation2D createVectorRotation(final Vector2D u, final Vector2D v) {
163        // make sure that the vectors are real-valued and of non-zero length; we don't
164        // actually need to use the norm value; we just need to check its properties
165        Vectors.checkedNorm(u);
166        Vectors.checkedNorm(v);
167
168        final double uAzimuth = Math.atan2(u.getY(), u.getX());
169        final double vAzimuth = Math.atan2(v.getY(), v.getX());
170
171        return of(vAzimuth - uAzimuth);
172    }
173}