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<Optional<Pose2d>> based on a 239 * Supplier<Optional<Alliance>> and original Optional<Pose2d> 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<Optional<Translation2d>> that is flipped based on a 262 * Supplier<Optional<Alliance>> and original Optional<Translation2d> 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}