001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.math.geometry;
006
007import static edu.wpi.first.units.Units.Radians;
008
009import com.fasterxml.jackson.annotation.JsonAutoDetect;
010import com.fasterxml.jackson.annotation.JsonCreator;
011import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
012import com.fasterxml.jackson.annotation.JsonProperty;
013import edu.wpi.first.math.MatBuilder;
014import edu.wpi.first.math.MathSharedStore;
015import edu.wpi.first.math.MathUtil;
016import edu.wpi.first.math.Matrix;
017import edu.wpi.first.math.Nat;
018import edu.wpi.first.math.geometry.proto.Rotation2dProto;
019import edu.wpi.first.math.geometry.struct.Rotation2dStruct;
020import edu.wpi.first.math.interpolation.Interpolatable;
021import edu.wpi.first.math.numbers.N2;
022import edu.wpi.first.math.util.Units;
023import edu.wpi.first.units.measure.Angle;
024import edu.wpi.first.util.protobuf.ProtobufSerializable;
025import edu.wpi.first.util.struct.StructSerializable;
026import java.util.Objects;
027
028/**
029 * A rotation in a 2D coordinate frame represented by a point on the unit circle (cosine and sine).
030 *
031 * <p>The angle is continuous, that is if a Rotation2d is constructed with 361 degrees, it will
032 * return 361 degrees. This allows algorithms that wouldn't want to see a discontinuity in the
033 * rotations as it sweeps past from 360 to 0 on the second time around.
034 */
035@JsonIgnoreProperties(ignoreUnknown = true)
036@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
037public class Rotation2d
038    implements Interpolatable<Rotation2d>, ProtobufSerializable, StructSerializable {
039  /**
040   * A preallocated Rotation2d representing no rotation.
041   *
042   * <p>This exists to avoid allocations for common rotations.
043   */
044  public static final Rotation2d kZero = new Rotation2d();
045
046  /**
047   * A preallocated Rotation2d representing a clockwise rotation by π/2 rad (90°).
048   *
049   * <p>This exists to avoid allocations for common rotations.
050   */
051  public static final Rotation2d kCW_Pi_2 = new Rotation2d(-Math.PI / 2);
052
053  /**
054   * A preallocated Rotation2d representing a clockwise rotation by 90° (π/2 rad).
055   *
056   * <p>This exists to avoid allocations for common rotations.
057   */
058  public static final Rotation2d kCW_90deg = kCW_Pi_2;
059
060  /**
061   * A preallocated Rotation2d representing a counterclockwise rotation by π/2 rad (90°).
062   *
063   * <p>This exists to avoid allocations for common rotations.
064   */
065  public static final Rotation2d kCCW_Pi_2 = new Rotation2d(Math.PI / 2);
066
067  /**
068   * A preallocated Rotation2d representing a counterclockwise rotation by 90° (π/2 rad).
069   *
070   * <p>This exists to avoid allocations for common rotations.
071   */
072  public static final Rotation2d kCCW_90deg = kCCW_Pi_2;
073
074  /**
075   * A preallocated Rotation2d representing a counterclockwise rotation by π rad (180°).
076   *
077   * <p>This exists to avoid allocations for common rotations.
078   */
079  public static final Rotation2d kPi = new Rotation2d(Math.PI);
080
081  /**
082   * A preallocated Rotation2d representing a counterclockwise rotation by 180° (π rad).
083   *
084   * <p>This exists to avoid allocations for common rotations.
085   */
086  public static final Rotation2d k180deg = kPi;
087
088  private final double m_value;
089  private final double m_cos;
090  private final double m_sin;
091
092  /** Constructs a Rotation2d with a default angle of 0 degrees. */
093  public Rotation2d() {
094    m_value = 0.0;
095    m_cos = 1.0;
096    m_sin = 0.0;
097  }
098
099  /**
100   * Constructs a Rotation2d with the given radian value.
101   *
102   * @param value The value of the angle in radians.
103   */
104  @JsonCreator
105  public Rotation2d(@JsonProperty(required = true, value = "radians") double value) {
106    m_value = value;
107    m_cos = Math.cos(value);
108    m_sin = Math.sin(value);
109  }
110
111  /**
112   * Constructs a Rotation2d with the given x and y (cosine and sine) components.
113   *
114   * @param x The x component or cosine of the rotation.
115   * @param y The y component or sine of the rotation.
116   */
117  public Rotation2d(double x, double y) {
118    double magnitude = Math.hypot(x, y);
119    if (magnitude > 1e-6) {
120      m_cos = x / magnitude;
121      m_sin = y / magnitude;
122    } else {
123      m_cos = 1.0;
124      m_sin = 0.0;
125      MathSharedStore.reportError(
126          "x and y components of Rotation2d are zero\n", Thread.currentThread().getStackTrace());
127    }
128    m_value = Math.atan2(m_sin, m_cos);
129  }
130
131  /**
132   * Constructs a Rotation2d with the given angle.
133   *
134   * @param angle The angle of the rotation.
135   */
136  public Rotation2d(Angle angle) {
137    this(angle.in(Radians));
138  }
139
140  /**
141   * Constructs a Rotation2d from a rotation matrix.
142   *
143   * @param rotationMatrix The rotation matrix.
144   * @throws IllegalArgumentException if the rotation matrix isn't special orthogonal.
145   */
146  public Rotation2d(Matrix<N2, N2> rotationMatrix) {
147    final var R = rotationMatrix;
148
149    // Require that the rotation matrix is special orthogonal. This is true if
150    // the matrix is orthogonal (RRᵀ = I) and normalized (determinant is 1).
151    if (R.times(R.transpose()).minus(Matrix.eye(Nat.N2())).normF() > 1e-9) {
152      var msg = "Rotation matrix isn't orthogonal\n\nR =\n" + R.getStorage().toString() + '\n';
153      MathSharedStore.reportError(msg, Thread.currentThread().getStackTrace());
154      throw new IllegalArgumentException(msg);
155    }
156    if (Math.abs(R.det() - 1.0) > 1e-9) {
157      var msg =
158          "Rotation matrix is orthogonal but not special orthogonal\n\nR =\n"
159              + R.getStorage().toString()
160              + '\n';
161      MathSharedStore.reportError(msg, Thread.currentThread().getStackTrace());
162      throw new IllegalArgumentException(msg);
163    }
164
165    // R = [cosθ  −sinθ]
166    //     [sinθ   cosθ]
167    m_cos = R.get(0, 0);
168    m_sin = R.get(1, 0);
169
170    m_value = Math.atan2(m_sin, m_cos);
171  }
172
173  /**
174   * Constructs and returns a Rotation2d with the given radian value.
175   *
176   * @param radians The value of the angle in radians.
177   * @return The rotation object with the desired angle value.
178   */
179  public static Rotation2d fromRadians(double radians) {
180    return new Rotation2d(radians);
181  }
182
183  /**
184   * Constructs and returns a Rotation2d with the given degree value.
185   *
186   * @param degrees The value of the angle in degrees.
187   * @return The rotation object with the desired angle value.
188   */
189  public static Rotation2d fromDegrees(double degrees) {
190    return new Rotation2d(Math.toRadians(degrees));
191  }
192
193  /**
194   * Constructs and returns a Rotation2d with the given number of rotations.
195   *
196   * @param rotations The value of the angle in rotations.
197   * @return The rotation object with the desired angle value.
198   */
199  public static Rotation2d fromRotations(double rotations) {
200    return new Rotation2d(Units.rotationsToRadians(rotations));
201  }
202
203  /**
204   * Adds two rotations together, with the result being bounded between -π and π.
205   *
206   * <p>For example, <code>Rotation2d.fromDegrees(30).plus(Rotation2d.fromDegrees(60))</code> equals
207   * <code>Rotation2d(Math.PI/2.0)</code>
208   *
209   * @param other The rotation to add.
210   * @return The sum of the two rotations.
211   */
212  public Rotation2d plus(Rotation2d other) {
213    return rotateBy(other);
214  }
215
216  /**
217   * Subtracts the new rotation from the current rotation and returns the new rotation.
218   *
219   * <p>For example, <code>Rotation2d.fromDegrees(10).minus(Rotation2d.fromDegrees(100))</code>
220   * equals <code>Rotation2d(-Math.PI/2.0)</code>
221   *
222   * @param other The rotation to subtract.
223   * @return The difference between the two rotations.
224   */
225  public Rotation2d minus(Rotation2d other) {
226    return rotateBy(other.unaryMinus());
227  }
228
229  /**
230   * Takes the inverse of the current rotation. This is simply the negative of the current angular
231   * value.
232   *
233   * @return The inverse of the current rotation.
234   */
235  public Rotation2d unaryMinus() {
236    return new Rotation2d(-m_value);
237  }
238
239  /**
240   * Multiplies the current rotation by a scalar.
241   *
242   * @param scalar The scalar.
243   * @return The new scaled Rotation2d.
244   */
245  public Rotation2d times(double scalar) {
246    return new Rotation2d(m_value * scalar);
247  }
248
249  /**
250   * Divides the current rotation by a scalar.
251   *
252   * @param scalar The scalar.
253   * @return The new scaled Rotation2d.
254   */
255  public Rotation2d div(double scalar) {
256    return times(1.0 / scalar);
257  }
258
259  /**
260   * Adds the new rotation to the current rotation using a rotation matrix.
261   *
262   * <p>The matrix multiplication is as follows:
263   *
264   * <pre>
265   * [cos_new]   [other.cos, -other.sin][cos]
266   * [sin_new] = [other.sin,  other.cos][sin]
267   * value_new = atan2(sin_new, cos_new)
268   * </pre>
269   *
270   * @param other The rotation to rotate by.
271   * @return The new rotated Rotation2d.
272   */
273  public Rotation2d rotateBy(Rotation2d other) {
274    return new Rotation2d(
275        m_cos * other.m_cos - m_sin * other.m_sin, m_cos * other.m_sin + m_sin * other.m_cos);
276  }
277
278  /**
279   * Returns matrix representation of this rotation.
280   *
281   * @return Matrix representation of this rotation.
282   */
283  public Matrix<N2, N2> toMatrix() {
284    // R = [cosθ  −sinθ]
285    //     [sinθ   cosθ]
286    return MatBuilder.fill(Nat.N2(), Nat.N2(), m_cos, -m_sin, m_sin, m_cos);
287  }
288
289  /**
290   * Returns the measure of the Rotation2d.
291   *
292   * @return The measure of the Rotation2d.
293   */
294  public Angle getMeasure() {
295    return Radians.of(getRadians());
296  }
297
298  /**
299   * Returns the radian value of the Rotation2d.
300   *
301   * @return The radian value of the Rotation2d.
302   * @see edu.wpi.first.math.MathUtil#angleModulus(double) to constrain the angle within (-π, π]
303   */
304  @JsonProperty
305  public double getRadians() {
306    return m_value;
307  }
308
309  /**
310   * Returns the degree value of the Rotation2d.
311   *
312   * @return The degree value of the Rotation2d.
313   * @see edu.wpi.first.math.MathUtil#inputModulus(double, double, double) to constrain the angle
314   *     within (-180, 180]
315   */
316  public double getDegrees() {
317    return Math.toDegrees(m_value);
318  }
319
320  /**
321   * Returns the number of rotations of the Rotation2d.
322   *
323   * @return The number of rotations of the Rotation2d.
324   */
325  public double getRotations() {
326    return Units.radiansToRotations(m_value);
327  }
328
329  /**
330   * Returns the cosine of the Rotation2d.
331   *
332   * @return The cosine of the Rotation2d.
333   */
334  public double getCos() {
335    return m_cos;
336  }
337
338  /**
339   * Returns the sine of the Rotation2d.
340   *
341   * @return The sine of the Rotation2d.
342   */
343  public double getSin() {
344    return m_sin;
345  }
346
347  /**
348   * Returns the tangent of the Rotation2d.
349   *
350   * @return The tangent of the Rotation2d.
351   */
352  public double getTan() {
353    return m_sin / m_cos;
354  }
355
356  @Override
357  public String toString() {
358    return String.format("Rotation2d(Rads: %.2f, Deg: %.2f)", m_value, Math.toDegrees(m_value));
359  }
360
361  /**
362   * Checks equality between this Rotation2d and another object.
363   *
364   * @param obj The other object.
365   * @return Whether the two objects are equal or not.
366   */
367  @Override
368  public boolean equals(Object obj) {
369    return obj instanceof Rotation2d other
370        && Math.hypot(m_cos - other.m_cos, m_sin - other.m_sin) < 1E-9;
371  }
372
373  @Override
374  public int hashCode() {
375    return Objects.hash(m_value);
376  }
377
378  @Override
379  public Rotation2d interpolate(Rotation2d endValue, double t) {
380    return plus(endValue.minus(this).times(MathUtil.clamp(t, 0, 1)));
381  }
382
383  /** Rotation2d protobuf for serialization. */
384  public static final Rotation2dProto proto = new Rotation2dProto();
385
386  /** Rotation2d struct for serialization. */
387  public static final Rotation2dStruct struct = new Rotation2dStruct();
388}