001// Copyright (c) Choreo contributors
002
003package choreo.util;
004
005import static choreo.util.FieldDimensions.FIELD_LENGTH;
006import static choreo.util.FieldDimensions.FIELD_WIDTH;
007
008import edu.wpi.first.math.geometry.Pose2d;
009import edu.wpi.first.math.geometry.Pose3d;
010import edu.wpi.first.math.geometry.Rotation2d;
011import edu.wpi.first.math.geometry.Rotation3d;
012import edu.wpi.first.math.geometry.Translation2d;
013import edu.wpi.first.math.geometry.Translation3d;
014import edu.wpi.first.wpilibj.DriverStation;
015import edu.wpi.first.wpilibj.DriverStation.Alliance;
016import java.util.HashMap;
017import java.util.Optional;
018import java.util.function.Supplier;
019
020/**
021 * A utility to standardize flipping of coordinate data based on the current alliance across
022 * different years.
023 *
024 * <p>If every vendor used this, the user would be able to specify the year and no matter the year
025 * the vendor's code is from, the user would be able to flip as expected.
026 *
027 * <p>This API still allows vendors and users to match case against the flipping variant as a way to
028 * specially handle cases or throw errors if a variant is explicitly not supported.
029 */
030public class ChoreoAllianceFlipUtil {
031  /** The flipper to use for flipping coordinates. */
032  public static enum Flipper {
033    /**
034     * X becomes fieldLength - x, leaves the y coordinate unchanged, and heading becomes PI -
035     * heading.
036     */
037    MIRRORED {
038      public double flipX(double x) {
039        return activeYear.fieldLength - x;
040      }
041
042      public double flipY(double y) {
043        return y;
044      }
045
046      public double flipHeading(double heading) {
047        return Math.PI - heading;
048      }
049    },
050    /** X becomes fieldLength - x, Y becomes fieldWidth - y, and heading becomes PI - heading. */
051    ROTATE_AROUND {
052      public double flipX(double x) {
053        return activeYear.fieldLength - x;
054      }
055
056      public double flipY(double y) {
057        return activeYear.fieldWidth - y;
058      }
059
060      public double flipHeading(double heading) {
061        return Math.PI + heading;
062      }
063    };
064
065    /**
066     * Flips the X coordinate.
067     *
068     * @param x The X coordinate to flip.
069     * @return The flipped X coordinate.
070     */
071    public abstract double flipX(double x);
072
073    /**
074     * Flips the Y coordinate.
075     *
076     * @param y The Y coordinate to flip.
077     * @return The flipped Y coordinate.
078     */
079    public abstract double flipY(double y);
080
081    /**
082     * Flips the heading.
083     *
084     * @param heading The heading to flip.
085     * @return The flipped heading.
086     */
087    public abstract double flipHeading(double heading);
088  }
089
090  private static record YearInfo(Flipper flipper, double fieldLength, double fieldWidth) {}
091
092  // TODO: Update and expand this map
093  private static final HashMap<Integer, YearInfo> flipperMap =
094      new HashMap<Integer, YearInfo>() {
095        {
096          put(2020, new YearInfo(Flipper.ROTATE_AROUND, 16.5811, 8.19912));
097          put(2021, new YearInfo(Flipper.ROTATE_AROUND, 16.5811, 8.19912));
098          put(2022, new YearInfo(Flipper.ROTATE_AROUND, 16.5811, 8.19912));
099          put(2023, new YearInfo(Flipper.MIRRORED, 16.5811, 8.19912));
100          put(2024, new YearInfo(Flipper.MIRRORED, 16.5811, 8.19912));
101          put(2025, new YearInfo(Flipper.ROTATE_AROUND, 17.548, 8.052));
102          put(2026, new YearInfo(Flipper.ROTATE_AROUND, FIELD_LENGTH, FIELD_WIDTH));
103        }
104      };
105
106  private static YearInfo activeYear = flipperMap.get(2026);
107
108  /** Default constructor. */
109  private ChoreoAllianceFlipUtil() {}
110
111  /**
112   * Get the flipper that is currently active for flipping coordinates. It's recommended not to
113   * store this locally as the flipper may change.
114   *
115   * @return The active flipper.
116   */
117  public static Flipper getFlipper() {
118    return activeYear.flipper;
119  }
120
121  /**
122   * Returns if you are on red alliance.
123   *
124   * @return If you are on red alliance.
125   */
126  public static boolean shouldFlip() {
127    return DriverStation.getAlliance().orElse(Alliance.Blue) == Alliance.Red;
128  }
129
130  /**
131   * Set the year to determine the Alliance Coordinate Flipper to use.
132   *
133   * @param year The year to set the flipper to. [2020 - 2024]
134   */
135  public static void setYear(int year) {
136    if (!flipperMap.containsKey(year)) {
137      throw new IllegalArgumentException("Year " + year + " is not supported.");
138    }
139    activeYear = flipperMap.get(year);
140  }
141
142  /**
143   * Flips the X coordinate.
144   *
145   * @param x The X coordinate to flip.
146   * @return The flipped X coordinate.
147   */
148  public static double flipX(double x) {
149    return activeYear.flipper.flipX(x);
150  }
151
152  /**
153   * Flips the Y coordinate.
154   *
155   * @param y The Y coordinate to flip.
156   * @return The flipped Y coordinate.
157   */
158  public static double flipY(double y) {
159    return activeYear.flipper.flipY(y);
160  }
161
162  /**
163   * Flips the heading.
164   *
165   * @param heading The heading to flip.
166   * @return The flipped heading.
167   */
168  public static double flipHeading(double heading) {
169    return activeYear.flipper.flipHeading(heading);
170  }
171
172  /**
173   * Flips the translation.
174   *
175   * @param translation The translation to flip.
176   * @return The flipped translation.
177   */
178  public static Translation2d flip(Translation2d translation) {
179    return new Translation2d(flipX(translation.getX()), flipY(translation.getY()));
180  }
181
182  /**
183   * Flips the rotation.
184   *
185   * @param rotation The rotation to flip.
186   * @return The flipped rotation.
187   */
188  public static Rotation2d flip(Rotation2d rotation) {
189    return switch (activeYear.flipper) {
190      case MIRRORED -> new Rotation2d(-rotation.getCos(), rotation.getSin());
191      case ROTATE_AROUND -> new Rotation2d(-rotation.getCos(), -rotation.getSin());
192    };
193  }
194
195  /**
196   * Flips the pose.
197   *
198   * @param pose The pose to flip.
199   * @return The flipped pose.
200   */
201  public static Pose2d flip(Pose2d pose) {
202    return new Pose2d(flip(pose.getTranslation()), flip(pose.getRotation()));
203  }
204
205  /**
206   * Flips the translation.
207   *
208   * @param translation The translation to flip.
209   * @return The flipped translation.
210   */
211  public static Translation3d flip(Translation3d translation) {
212    return new Translation3d(
213        flipX(translation.getX()), flipY(translation.getY()), translation.getZ());
214  }
215
216  /**
217   * Flips the rotation.
218   *
219   * @param rotation The rotation to flip.
220   * @return The flipped rotation.
221   */
222  public static Rotation3d flip(Rotation3d rotation) {
223    return new Rotation3d(
224        rotation.getX(), rotation.getY(), flip(rotation.toRotation2d()).getRadians());
225  }
226
227  /**
228   * Flips the pose.
229   *
230   * @param pose The pose to flip.
231   * @return The flipped pose.
232   */
233  public static Pose3d flip(Pose3d pose) {
234    return new Pose3d(flip(pose.getTranslation()), flip(pose.getRotation()));
235  }
236
237  /**
238   * Creates a Supplier&lt;Optional&lt;Pose2d&gt;&gt; based on a
239   * Supplier&lt;Optional&lt;Alliance&gt;&gt; and original Optional&lt;Pose2d&gt;
240   *
241   * @param poseOpt The pose to flip
242   * @param allianceOpt The current alliance
243   * @param doFlip Returns true if flipping based on the alliance should be done
244   * @return empty if the alliance is empty; the original pose optional if the alliance is blue or
245   *     doFlip is false; the flipped pose optional if the alliance is red and doFlip is true
246   */
247  public static Supplier<Optional<Pose2d>> optionalFlippedPose2d(
248      Optional<Pose2d> poseOpt, Supplier<Optional<Alliance>> allianceOpt, boolean doFlip) {
249    if (poseOpt.isEmpty()) {
250      return () -> Optional.empty();
251    }
252    Optional<Pose2d> flippedPose = poseOpt.map(ChoreoAllianceFlipUtil::flip);
253
254    return () ->
255        doFlip
256            ? allianceOpt.get().flatMap(ally -> ally == Alliance.Red ? flippedPose : poseOpt)
257            : poseOpt;
258  }
259
260  /**
261   * Creates a Supplier&lt;Optional&lt;Translation2d&gt;&gt; that is flipped based on a
262   * Supplier&lt;Optional&lt;Alliance&gt;&gt; and original Optional&lt;Translation2d&gt;
263   *
264   * @param translationOpt The translation to flip
265   * @param allianceOpt The current alliance
266   * @param doFlip Returns true if flipping based on the alliance should be done
267   * @return empty if the alliance is empty; the original translation optional if the alliance is
268   *     blue or doFlip is false; the flipped translation optional if the alliance is red and doFlip
269   *     is true
270   */
271  public static Supplier<Optional<Translation2d>> optionalFlippedTranslation2d(
272      Optional<Translation2d> translationOpt,
273      Supplier<Optional<Alliance>> allianceOpt,
274      boolean doFlip) {
275    if (translationOpt.isEmpty()) {
276      return () -> Optional.empty();
277    }
278    Optional<Translation2d> flippedTranslation = translationOpt.map(ChoreoAllianceFlipUtil::flip);
279
280    return () ->
281        doFlip
282            ? allianceOpt
283                .get()
284                .flatMap(ally -> ally == Alliance.Red ? flippedTranslation : translationOpt)
285            : translationOpt;
286  }
287}