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 org.wpilib.math.geometry;
006
007import org.wpilib.math.linalg.Matrix;
008import org.wpilib.math.util.MathSharedStore;
009import org.wpilib.math.util.Nat;
010
011/** A helper class that converts Pose3d objects between different standard coordinate frames. */
012public final class CoordinateSystem {
013  private static final CoordinateSystem m_nwu =
014      new CoordinateSystem(CoordinateAxis.N(), CoordinateAxis.W(), CoordinateAxis.U());
015  private static final CoordinateSystem m_edn =
016      new CoordinateSystem(CoordinateAxis.E(), CoordinateAxis.D(), CoordinateAxis.N());
017  private static final CoordinateSystem m_ned =
018      new CoordinateSystem(CoordinateAxis.N(), CoordinateAxis.E(), CoordinateAxis.D());
019
020  // Rotation from this coordinate system to NWU coordinate system when applied extrinsically
021  private final Rotation3d m_rotation;
022
023  /**
024   * Constructs a coordinate system with the given cardinal directions for each axis.
025   *
026   * @param positiveX The cardinal direction of the positive x-axis.
027   * @param positiveY The cardinal direction of the positive y-axis.
028   * @param positiveZ The cardinal direction of the positive z-axis.
029   * @throws IllegalArgumentException if the coordinate system isn't special orthogonal
030   */
031  public CoordinateSystem(
032      CoordinateAxis positiveX, CoordinateAxis positiveY, CoordinateAxis positiveZ) {
033    // Construct a change of basis matrix from the source coordinate system to the
034    // NWU coordinate system. Each column vector in the change of basis matrix is
035    // one of the old basis vectors mapped to its representation in the new basis.
036    var R = new Matrix<>(Nat.N3(), Nat.N3());
037    R.assignBlock(0, 0, positiveX.m_axis);
038    R.assignBlock(0, 1, positiveY.m_axis);
039    R.assignBlock(0, 2, positiveZ.m_axis);
040
041    // If determinant is -1, coordinate system is left-handed
042    if (Math.abs(R.det() + 1.0) < 1e-9) {
043      var msg =
044          "CoordinateSystem requires a right-handed system, but a left-handed one was provided";
045      MathSharedStore.reportError(msg, Thread.currentThread().getStackTrace());
046      throw new IllegalArgumentException(msg);
047    }
048
049    // The change of basis matrix should be a pure rotation. The Rotation3d
050    // constructor will verify this by checking for special orthogonality.
051    m_rotation = new Rotation3d(R);
052  }
053
054  /**
055   * Returns an instance of the North-West-Up (NWU) coordinate system.
056   *
057   * <p>The +X axis is north, the +Y axis is west, and the +Z axis is up.
058   *
059   * @return An instance of the North-West-Up (NWU) coordinate system.
060   */
061  public static CoordinateSystem NWU() {
062    return m_nwu;
063  }
064
065  /**
066   * Returns an instance of the East-Down-North (EDN) coordinate system.
067   *
068   * <p>The +X axis is east, the +Y axis is down, and the +Z axis is north.
069   *
070   * @return An instance of the East-Down-North (EDN) coordinate system.
071   */
072  public static CoordinateSystem EDN() {
073    return m_edn;
074  }
075
076  /**
077   * Returns an instance of the North-East-Down (NED) coordinate system.
078   *
079   * <p>The +X axis is north, the +Y axis is east, and the +Z axis is down.
080   *
081   * @return An instance of the North-East-Down (NED) coordinate system.
082   */
083  public static CoordinateSystem NED() {
084    return m_ned;
085  }
086
087  /**
088   * Converts the given translation from one coordinate system to another.
089   *
090   * @param translation The translation to convert.
091   * @param from The coordinate system the pose starts in.
092   * @param to The coordinate system to which to convert.
093   * @return The given translation in the desired coordinate system.
094   */
095  public static Translation3d convert(
096      Translation3d translation, CoordinateSystem from, CoordinateSystem to) {
097    // Convert to NWU, then convert to the new coordinate system
098    return translation.rotateBy(from.m_rotation).rotateBy(to.m_rotation.inverse());
099  }
100
101  /**
102   * Converts the given rotation from one coordinate system to another.
103   *
104   * @param rotation The rotation to convert.
105   * @param from The coordinate system the rotation starts in.
106   * @param to The coordinate system to which to convert.
107   * @return The given rotation in the desired coordinate system.
108   */
109  public static Rotation3d convert(
110      Rotation3d rotation, CoordinateSystem from, CoordinateSystem to) {
111    // Convert to NWU, then convert to the new coordinate system
112    return rotation.rotateBy(from.m_rotation).rotateBy(to.m_rotation.inverse());
113  }
114
115  /**
116   * Converts the given pose from one coordinate system to another.
117   *
118   * @param pose The pose to convert.
119   * @param from The coordinate system the pose starts in.
120   * @param to The coordinate system to which to convert.
121   * @return The given pose in the desired coordinate system.
122   */
123  public static Pose3d convert(Pose3d pose, CoordinateSystem from, CoordinateSystem to) {
124    return new Pose3d(
125        convert(pose.getTranslation(), from, to), convert(pose.getRotation(), from, to));
126  }
127
128  /**
129   * Converts the given transform from one coordinate system to another.
130   *
131   * @param transform The transform to convert.
132   * @param from The coordinate system the transform starts in.
133   * @param to The coordinate system to which to convert.
134   * @return The given transform in the desired coordinate system.
135   */
136  public static Transform3d convert(
137      Transform3d transform, CoordinateSystem from, CoordinateSystem to) {
138    // coordRot is the rotation that converts between the coordinate systems when applied
139    // extrinsically. It first converts to NWU, then converts to the new coordinate system.
140    var coordRot = from.m_rotation.rotateBy(to.m_rotation.inverse());
141    // The new rotation is the extrinsic rotation from convert(zero) to
142    // convert(transformRot). That is, applying convertedRot extrinsically to
143    // convert(zero) produces convert(transformRot). In the below snippet, we
144    // use matrix notation, so rotA rotB applies rotA extrinsically to rotB.
145    //
146    //   convertedRot convert(zero) = convert(transformRot)
147    //   convertedRot = convert(transformRot) convert(zero)⁻¹
148    //                = (coordRot transformRot) (coordRot zero)⁻¹
149    //                = (coordRot transformRot) coordRot⁻¹
150    //
151    // In code, the equivalent for rotA rotB is rotB.rotateBy(rotA) (note the
152    // change in order).
153    return new Transform3d(
154        convert(transform.getTranslation(), from, to),
155        coordRot.inverse().rotateBy(transform.getRotation().rotateBy(coordRot)));
156  }
157}