001// Copyright (c) Choreo contributors 002 003package choreo.auto; 004 005import static choreo.util.ChoreoAlert.allianceNotReady; 006 007import choreo.Choreo.TrajectoryLogger; 008import choreo.auto.AutoFactory.AllianceContext; 009import choreo.auto.AutoFactory.AutoBindings; 010import choreo.trajectory.DifferentialSample; 011import choreo.trajectory.SwerveSample; 012import choreo.trajectory.Trajectory; 013import choreo.trajectory.TrajectorySample; 014import choreo.util.ChoreoAlert; 015import choreo.util.ChoreoAlert.MultiAlert; 016import choreo.util.ChoreoAllianceFlipUtil; 017import java.util.Optional; 018import java.util.OptionalInt; 019import java.util.function.BooleanSupplier; 020import java.util.function.Consumer; 021import java.util.function.Supplier; 022import org.wpilib.command2.Command; 023import org.wpilib.command2.Commands; 024import org.wpilib.command2.FunctionalCommand; 025import org.wpilib.command2.ScheduleCommand; 026import org.wpilib.command2.Subsystem; 027import org.wpilib.command2.button.Trigger; 028import org.wpilib.math.geometry.Pose2d; 029import org.wpilib.math.geometry.Rotation2d; 030import org.wpilib.math.geometry.Translation2d; 031import org.wpilib.system.Timer; 032import org.wpilib.util.Alert.Level; 033 034/** 035 * A class that represents a trajectory that can be used in an autonomous routine and have triggers 036 * based off of it. 037 */ 038public class AutoTrajectory { 039 // For any devs looking through this class wondering 040 // about all the type casting and `?` for generics it's intentional. 041 // My goal was to make the sample type minimally leak into user code 042 // so you don't have to retype the sample type everywhere in your auto 043 // code. This also makes the places with generics exposed to users few 044 // and far between. This helps with more novice users 045 046 private static final MultiAlert triggerTimeNegative = 047 ChoreoAlert.multiAlert(causes -> "Trigger time cannot be negative for " + causes, Level.HIGH); 048 private static final MultiAlert triggerTimeAboveMax = 049 ChoreoAlert.multiAlert( 050 causes -> "Trigger time cannot be greater than total trajectory time for " + causes + ".", 051 Level.HIGH); 052 private static final MultiAlert eventNotFound = 053 ChoreoAlert.multiAlert(causes -> "Event Markers " + causes + " not found.", Level.HIGH); 054 private static final MultiAlert noSamples = 055 ChoreoAlert.multiAlert(causes -> "Trajectories " + causes + " have no samples.", Level.HIGH); 056 private static final MultiAlert noInitialPose = 057 ChoreoAlert.multiAlert( 058 causes -> "Unable to get initial pose for trajectories " + causes + ".", Level.HIGH); 059 060 final String name; 061 final Trajectory<? extends TrajectorySample<?>> trajectory; 062 final TrajectoryLogger<? extends TrajectorySample<?>> trajectoryLogger; 063 final Supplier<Pose2d> poseSupplier; 064 final Consumer<Pose2d> resetOdometry; 065 final Consumer<? extends TrajectorySample<?>> controller; 066 final AllianceContext allianceCtx; 067 final Subsystem driveSubsystem; 068 final AutoRoutine routine; 069 final AutoBindings bindings; 070 071 private final Timer activeTimer = new Timer(); 072 private final Timer inactiveTimer = new Timer(); 073 074 /** If this trajectory us currently running */ 075 private boolean isActive = false; 076 077 /** If the trajectory ran to completion. */ 078 private boolean isCompleted = false; 079 080 /** Whether to suppress warnings for this trajectory. */ 081 private boolean warnUser = true; 082 083 /** 084 * Constructs an AutoTrajectory. 085 * 086 * @param name The trajectory name. 087 * @param trajectory The trajectory samples. 088 * @param poseSupplier The pose supplier. 089 * @param controller The controller function. 090 * @param allianceCtx The alliance context. 091 * @param trajectoryLogger Optional trajectory logger. 092 * @param driveSubsystem Drive subsystem. 093 * @param routine Event loop. 094 * @param bindings {@link AutoFactory} 095 */ 096 <SampleType extends TrajectorySample<SampleType>> AutoTrajectory( 097 String name, 098 Trajectory<SampleType> trajectory, 099 Supplier<Pose2d> poseSupplier, 100 Consumer<Pose2d> resetOdometry, 101 Consumer<SampleType> controller, 102 AllianceContext allianceCtx, 103 TrajectoryLogger<SampleType> trajectoryLogger, 104 Subsystem driveSubsystem, 105 AutoRoutine routine, 106 AutoBindings bindings) { 107 this.name = name; 108 this.trajectory = trajectory; 109 this.poseSupplier = poseSupplier; 110 this.resetOdometry = resetOdometry; 111 this.controller = controller; 112 this.allianceCtx = allianceCtx; 113 this.driveSubsystem = driveSubsystem; 114 this.routine = routine; 115 this.trajectoryLogger = trajectoryLogger; 116 this.bindings = bindings; 117 118 bindings.getBindings().forEach((key, value) -> active().and(atTime(key)).onTrue(value)); 119 } 120 121 @SuppressWarnings("unchecked") 122 private void logTrajectory(boolean starting) { 123 var sampleOpt = trajectory.getInitialSample(false); 124 if (sampleOpt.isEmpty()) { 125 return; 126 } 127 var sample = sampleOpt.get(); 128 if (sample instanceof SwerveSample) { 129 TrajectoryLogger<SwerveSample> swerveLogger = 130 (TrajectoryLogger<SwerveSample>) trajectoryLogger; 131 Trajectory<SwerveSample> swerveTrajectory = (Trajectory<SwerveSample>) trajectory; 132 swerveLogger.accept(swerveTrajectory, starting); 133 } else if (sample instanceof DifferentialSample) { 134 TrajectoryLogger<DifferentialSample> differentialLogger = 135 (TrajectoryLogger<DifferentialSample>) trajectoryLogger; 136 Trajectory<DifferentialSample> differentialTrajectory = 137 (Trajectory<DifferentialSample>) trajectory; 138 differentialLogger.accept(differentialTrajectory, starting); 139 } 140 ; 141 } 142 143 private void cmdInitialize() { 144 activeTimer.start(); 145 inactiveTimer.stop(); 146 inactiveTimer.reset(); 147 isActive = true; 148 isCompleted = false; 149 logTrajectory(true); 150 } 151 152 @SuppressWarnings("unchecked") 153 private void cmdExecute() { 154 if (!allianceCtx.allianceKnownOrIgnored()) { 155 allianceNotReady.set(true); 156 return; 157 } 158 var sampleOpt = trajectory.sampleAt(activeTimer.get(), allianceCtx.doFlip()); 159 if (sampleOpt.isEmpty()) { 160 return; 161 } 162 var sample = sampleOpt.get(); 163 if (sample instanceof SwerveSample swerveSample) { 164 var swerveController = (Consumer<SwerveSample>) this.controller; 165 swerveController.accept(swerveSample); 166 } else if (sample instanceof DifferentialSample differentialSample) { 167 var differentialController = (Consumer<DifferentialSample>) this.controller; 168 differentialController.accept(differentialSample); 169 } 170 } 171 172 @SuppressWarnings("unchecked") 173 private void cmdEnd(boolean interrupted) { 174 activeTimer.stop(); 175 activeTimer.reset(); 176 inactiveTimer.start(); 177 isActive = false; 178 isCompleted = !interrupted; 179 180 if (!interrupted && allianceCtx.allianceKnownOrIgnored()) { 181 var sampleOpt = trajectory.getFinalSample(allianceCtx.doFlip()); 182 if (sampleOpt.isPresent()) { 183 var sample = sampleOpt.get(); 184 if (sample instanceof SwerveSample swerveSample) { 185 var swerveController = (Consumer<SwerveSample>) this.controller; 186 swerveController.accept(swerveSample); 187 } else if (sample instanceof DifferentialSample differentialSample) { 188 var differentialController = (Consumer<DifferentialSample>) this.controller; 189 differentialController.accept(differentialSample); 190 } 191 } 192 } 193 194 logTrajectory(false); 195 } 196 197 private boolean cmdIsFinished() { 198 return activeTimer.get() > trajectory.getTotalTime() 199 || !routine.active().getAsBoolean() 200 || !allianceCtx.allianceKnownOrIgnored(); 201 } 202 203 /** Suppresses warnings for this trajectory. */ 204 void suppressWarnings() { 205 warnUser = false; 206 } 207 208 /** 209 * Creates a command that allocates the drive subsystem and follows the trajectory using the 210 * factories control function 211 * 212 * @return The command that will follow the trajectory 213 */ 214 public Command cmd() { 215 if (trajectory.samples().isEmpty() && warnUser) { 216 return driveSubsystem.runOnce(() -> noSamples.addCause(name)).withName("Trajectory_" + name); 217 } 218 return new FunctionalCommand( 219 this::cmdInitialize, 220 this::cmdExecute, 221 this::cmdEnd, 222 this::cmdIsFinished, 223 driveSubsystem) 224 .withName("Trajectory_" + name); 225 } 226 227 /** 228 * Creates a command that will schedule <b>another</b> command that will follow the trajectory. 229 * 230 * <p>This can be useful when putting {@link AutoTrajectory} commands in sequences that require 231 * subsystems also required by in AutoTrajectory-bound subsystems. 232 * 233 * @return The command that will schedule the trajectory following command. 234 */ 235 public Command spawnCmd() { 236 return new ScheduleCommand(cmd()).withName("Trajectory_" + name + "_Spawner"); 237 } 238 239 /** 240 * Creates a command that resets the robot's odometry to the start of this trajectory. 241 * 242 * @return A command that resets the robot's odometry. 243 */ 244 public Command resetOdometry() { 245 return Commands.runOnce( 246 () -> 247 getInitialPose() 248 .ifPresentOrElse( 249 resetOdometry, 250 () -> { 251 if (warnUser) { 252 noInitialPose.addCause(name); 253 } 254 }), 255 driveSubsystem) 256 .withName("Trajectory_ResetOdometry_" + name); 257 } 258 259 /** 260 * Will get the underlying {@link Trajectory} object. 261 * 262 * <p><b>WARNING:</b> This method is not type safe and should be used with caution. The sample 263 * type of the trajectory should be known before calling this method. 264 * 265 * @param <SampleType> The type of the trajectory samples. 266 * @return The underlying {@link Trajectory} object. 267 */ 268 @SuppressWarnings("unchecked") 269 public <SampleType extends TrajectorySample<SampleType>> 270 Trajectory<SampleType> getRawTrajectory() { 271 return (Trajectory<SampleType>) trajectory; 272 } 273 274 /** 275 * Returns this auto trajectory, mirrored to the other alliance. 276 * 277 * @param <SampleType> The type of the trajectory samples. Due to Java limitations, you have to 278 * specify the sample type again here even if it was already specified when creating the 279 * AutoTrajectory. 280 * @return this auto trajectory, mirrored to the other alliance. 281 */ 282 @SuppressWarnings("unchecked") 283 public <SampleType extends TrajectorySample<SampleType>> AutoTrajectory mirrorX() { 284 return new AutoTrajectory( 285 name, 286 (Trajectory<SampleType>) trajectory.mirrorX(), 287 poseSupplier, 288 resetOdometry, 289 (Consumer<SampleType>) controller, 290 allianceCtx, 291 (TrajectoryLogger<SampleType>) trajectoryLogger, 292 driveSubsystem, 293 routine, 294 bindings); 295 } 296 297 /** 298 * Returns this auto trajectory, mirrored left-to-right from the driver's perspective. 299 * 300 * @param <SampleType> The type of the trajectory samples. Due to Java limitations, you have to 301 * specify the sample type again here even if it was already specified when creating the 302 * AutoTrajectory. 303 * @return this auto trajectory, mirrored left-to-right from the driver's perspective. 304 */ 305 @SuppressWarnings("unchecked") 306 public <SampleType extends TrajectorySample<SampleType>> AutoTrajectory mirrorY() { 307 return new AutoTrajectory( 308 name, 309 (Trajectory<SampleType>) trajectory.mirrorY(), 310 poseSupplier, 311 resetOdometry, 312 (Consumer<SampleType>) controller, 313 allianceCtx, 314 (TrajectoryLogger<SampleType>) trajectoryLogger, 315 driveSubsystem, 316 routine, 317 bindings); 318 } 319 320 /** 321 * Returns this auto trajectory, rotated 180 degrees around the field center. 322 * 323 * @param <SampleType> The type of the trajectory samples. Due to Java limitations, you have to 324 * specify the sample type again here even if it was already specified when creating the 325 * AutoTrajectory. 326 * @return this auto trajectory, rotated 180 degrees around the field center. 327 */ 328 @SuppressWarnings("unchecked") 329 public <SampleType extends TrajectorySample<SampleType>> AutoTrajectory rotateAround() { 330 return new AutoTrajectory( 331 name, 332 (Trajectory<SampleType>) trajectory.rotateAround(), 333 poseSupplier, 334 resetOdometry, 335 (Consumer<SampleType>) controller, 336 allianceCtx, 337 (TrajectoryLogger<SampleType>) trajectoryLogger, 338 driveSubsystem, 339 routine, 340 bindings); 341 } 342 343 /** 344 * Will get the starting pose of the trajectory. 345 * 346 * <p>This position is flipped if alliance flipping is enabled and the alliance supplier returns 347 * Red. 348 * 349 * <p>This method returns an empty Optional if the trajectory is empty. This method returns an 350 * empty Optional if alliance flipping is enabled and the alliance supplier returns an empty 351 * Optional. 352 * 353 * @return The starting pose 354 */ 355 public Optional<Pose2d> getInitialPose() { 356 if (!allianceCtx.allianceKnownOrIgnored()) { 357 allianceNotReady.set(true); 358 return Optional.empty(); 359 } 360 return trajectory.getInitialPose(allianceCtx.doFlip()); 361 } 362 363 /** 364 * Will get the ending pose of the trajectory. 365 * 366 * <p>This position is flipped if alliance flipping is enabled and the alliance supplier returns 367 * Red. 368 * 369 * <p>This method returns an empty Optional if the trajectory is empty. This method returns an 370 * empty Optional if alliance flipping is enabled and the alliance supplier returns an empty 371 * Optional. 372 * 373 * @return The starting pose 374 */ 375 public Optional<Pose2d> getFinalPose() { 376 if (!allianceCtx.allianceKnownOrIgnored()) { 377 allianceNotReady.set(true); 378 return Optional.empty(); 379 } 380 return trajectory.getFinalPose(allianceCtx.doFlip()); 381 } 382 383 /** 384 * Returns a trigger that is true while the trajectory is scheduled. 385 * 386 * @return A trigger that is true while the trajectory is scheduled. 387 */ 388 public Trigger active() { 389 return routine.active().and(new Trigger(routine.loop(), () -> this.isActive)); 390 } 391 392 /** 393 * Returns a trigger that is true while the command is not scheduled. 394 * 395 * <p>The same as calling <code>active().negate()</code>. 396 * 397 * @return A trigger that is true while the command is not scheduled. 398 */ 399 public Trigger inactive() { 400 return active().negate(); 401 } 402 403 private Trigger enterExitTrigger(Trigger enter, Trigger exit) { 404 return new Trigger( 405 routine.loop(), 406 new BooleanSupplier() { 407 private boolean output = false; 408 409 @Override 410 public boolean getAsBoolean() { 411 if (enter.getAsBoolean()) { 412 output = true; 413 } 414 if (exit.getAsBoolean()) { 415 output = false; 416 } 417 return output; 418 } 419 }); 420 } 421 422 /** 423 * Returns a trigger that is true after the trajectory completes. 424 * 425 * @return a trigger that is true after the trajectory completes 426 */ 427 public Trigger done() { 428 return doneDelayed(0); 429 } 430 431 /** 432 * Returns a trigger that becomes true after the trajectory completes and the given delay. 433 * 434 * @param seconds the delay after completion, in seconds 435 * @return a trigger that is true after the trajectory completes and the given delay 436 */ 437 public Trigger doneDelayed(double seconds) { 438 return timeTrigger(seconds, inactiveTimer).and(new Trigger(routine.loop(), () -> isCompleted)); 439 } 440 441 /** 442 * Returns a trigger that remains true for the given duration after completion. 443 * 444 * @param seconds the duration for which the trigger remains true, in seconds 445 * @return a trigger that remains true for the given duration after completion 446 */ 447 public Trigger doneFor(double seconds) { 448 return enterExitTrigger(doneDelayed(0), doneDelayed(seconds)); 449 } 450 451 /** 452 * Returns a trigger that remains true after completion until the routine becomes idle. 453 * 454 * @return a trigger that remains true after completion until the routine becomes idle 455 */ 456 public Trigger recentlyDone() { 457 return enterExitTrigger(doneDelayed(0), routine.idle().negate()); 458 } 459 460 /** 461 * A shorthand for `.done().onTrue(otherTrajectory.cmd())` 462 * 463 * @param otherTrajectory The other trajectory to run when this one is done. 464 */ 465 public void chain(AutoTrajectory otherTrajectory) { 466 done().onTrue(otherTrajectory.cmd()); 467 } 468 469 private Trigger timeTrigger(double targetTime, Timer timer) { 470 // Make the trigger only be high for 1 cycle when the time has elapsed 471 return new Trigger( 472 routine.loop(), 473 new BooleanSupplier() { 474 double lastTimestamp = -1.0; 475 OptionalInt pollTarget = OptionalInt.empty(); 476 477 @Override 478 public boolean getAsBoolean() { 479 if (!timer.isRunning()) { 480 lastTimestamp = -1.0; 481 pollTarget = OptionalInt.empty(); 482 return false; 483 } 484 double nowTimestamp = timer.get(); 485 try { 486 boolean timeAligns = lastTimestamp < targetTime && nowTimestamp >= targetTime; 487 if (pollTarget.isEmpty() && timeAligns) { 488 pollTarget = OptionalInt.of(routine.pollCount()); 489 return true; 490 } else if (pollTarget.isPresent() && routine.pollCount() == pollTarget.getAsInt()) { 491 return true; 492 } else if (pollTarget.isPresent()) { 493 pollTarget = OptionalInt.empty(); 494 return false; 495 } 496 return false; 497 } finally { 498 lastTimestamp = nowTimestamp; 499 } 500 } 501 }); 502 } 503 504 /** 505 * Returns a trigger that will go true for 1 cycle when the desired time has elapsed 506 * 507 * @param timeSinceStart The time since the command started in seconds. 508 * @return A trigger that is true when timeSinceStart has elapsed. 509 */ 510 public Trigger atTime(double timeSinceStart) { 511 // The timer should never be negative so report this as a warning 512 if (timeSinceStart < 0) { 513 if (warnUser) { 514 triggerTimeNegative.addCause(name); 515 } 516 return new Trigger(routine.loop(), () -> false); 517 } 518 519 // The timer should never exceed the total trajectory time so report this as a warning 520 if (timeSinceStart > trajectory.getTotalTime()) { 521 if (warnUser) { 522 triggerTimeAboveMax.addCause(name); 523 } 524 return new Trigger(routine.loop(), () -> false); 525 } 526 527 return timeTrigger(timeSinceStart, activeTimer); 528 } 529 530 /** 531 * Returns a trigger that will go true for 1 cycle when the desired before the end of the 532 * trajectory time. 533 * 534 * @param timeBeforeEnd The time before the end of the trajectory. 535 * @return A trigger that is true when timeBeforeEnd has elapsed. 536 */ 537 public Trigger atTimeBeforeEnd(double timeBeforeEnd) { 538 return atTime(trajectory.getTotalTime() - timeBeforeEnd); 539 } 540 541 /** 542 * Returns a trigger that is true when the event with the given name has been reached based on 543 * time. 544 * 545 * <p>A warning will be printed to the DriverStation if the event is not found and the trigger 546 * will always be false. 547 * 548 * @param eventName The name of the event. 549 * @return A trigger that is true when the event with the given name has been reached based on 550 * time. 551 * @see <a href="https://choreo.autos/usage/editing-paths/#event-markers">Event Markers in the 552 * GUI</a> 553 */ 554 public Trigger atTime(String eventName) { 555 boolean foundEvent = false; 556 Trigger trig = new Trigger(routine.loop(), () -> false); 557 558 for (var event : trajectory.getEvents(eventName)) { 559 // This could create a lot of objects, could be done a more efficient way 560 // with having it all be 1 trigger that just has a list of times and checks each one each 561 // cycle 562 // or something like that. If choreo starts proposing memory issues we can look into this. 563 trig = trig.or(atTime(event.timestamp)); 564 foundEvent = true; 565 } 566 567 // The user probably expects an event to exist if they're trying to do something at that event, 568 // report the missing event. 569 if (!foundEvent && warnUser) { 570 eventNotFound.addCause(name); 571 } 572 573 return trig; 574 } 575 576 private boolean withinTolerance(Rotation2d lhs, Rotation2d rhs, double toleranceRadians) { 577 if (Math.abs(toleranceRadians) > Math.PI) { 578 return true; 579 } 580 double dot = lhs.getCos() * rhs.getCos() + lhs.getSin() * rhs.getSin(); 581 // cos(θ) >= cos(tolerance) means |θ| <= tolerance, for tolerance in [-pi, pi], as pre-checked 582 // above. 583 return dot > Math.cos(toleranceRadians); 584 } 585 586 /** 587 * Returns a trigger that is true when the robot is within toleranceMeters of the given pose. 588 * 589 * <p>The pose is flipped if alliance flipping is enabled and the alliance supplier returns Red. 590 * 591 * <p>While alliance flipping is enabled and the alliance supplier returns empty, the trigger will 592 * return false. 593 * 594 * @param pose The pose to check against, unflipped. 595 * @param toleranceMeters The tolerance in meters. 596 * @param toleranceRadians The heading tolerance in radians. 597 * @return A trigger that is true when the robot is within toleranceMeters of the given pose. 598 */ 599 public Trigger atPose(Pose2d pose, double toleranceMeters, double toleranceRadians) { 600 Pose2d flippedPose = ChoreoAllianceFlipUtil.flip(pose); 601 return new Trigger( 602 () -> { 603 if (allianceCtx.allianceKnownOrIgnored()) { 604 final Pose2d currentPose = poseSupplier.get(); 605 if (allianceCtx.doFlip()) { 606 boolean transValid = 607 currentPose.getTranslation().getDistance(flippedPose.getTranslation()) 608 < toleranceMeters; 609 boolean rotValid = 610 withinTolerance( 611 currentPose.getRotation(), flippedPose.getRotation(), toleranceRadians); 612 return transValid && rotValid; 613 } else { 614 boolean transValid = 615 currentPose.getTranslation().getDistance(pose.getTranslation()) 616 < toleranceMeters; 617 boolean rotValid = 618 withinTolerance( 619 currentPose.getRotation(), pose.getRotation(), toleranceRadians); 620 return transValid && rotValid; 621 } 622 } else { 623 allianceNotReady.set(true); 624 return false; 625 } 626 }) 627 .and(active()); 628 } 629 630 /** 631 * Returns a trigger that is true when the robot is within toleranceMeters and toleranceRadians of 632 * the given event's pose. 633 * 634 * <p>A warning will be printed to the DriverStation if the event is not found and the trigger 635 * will always be false. 636 * 637 * @param eventName The name of the event. 638 * @param toleranceMeters The tolerance in meters. 639 * @param toleranceRadians The heading tolerance in radians. 640 * @return A trigger that is true when the robot is within toleranceMeters of the given events 641 * pose. 642 * @see <a href="https://choreo.autos/usage/editing-paths/#event-markers">Event Markers in the 643 * GUI</a> 644 */ 645 public Trigger atPose(String eventName, double toleranceMeters, double toleranceRadians) { 646 boolean foundEvent = false; 647 Trigger trig = new Trigger(() -> false); 648 649 for (var event : trajectory.getEvents(eventName)) { 650 // This could create a lot of objects, could be done a more efficient way 651 // with having it all be 1 trigger that just has a list of possess and checks each one each 652 // cycle or something like that. 653 // If choreo starts showing memory issues we can look into this. 654 Optional<Pose2d> poseOpt = 655 trajectory 656 // don't mirror here because the poses are mirrored themselves 657 // this also lets atPose be called before the alliance is ready 658 .sampleAt(event.timestamp, false) 659 .map(TrajectorySample::getPose); 660 if (poseOpt.isPresent()) { 661 trig = trig.or(atPose(poseOpt.get(), toleranceMeters, toleranceRadians)); 662 foundEvent = true; 663 } 664 } 665 666 // The user probably expects an event to exist if they're trying to do something at that event, 667 // report the missing event. 668 if (!foundEvent && warnUser) { 669 eventNotFound.addCause(name); 670 } 671 672 return trig; 673 } 674 675 /** 676 * Returns a trigger that is true when the robot is within toleranceMeters of the given 677 * translation. 678 * 679 * <p>The translation is flipped if alliance flipping is enabled and the alliance supplier returns 680 * Red. 681 * 682 * <p>While alliance flipping is enabled and the alliance supplier returns empty, the trigger will 683 * return false. 684 * 685 * @param translation The translation to check against, unflipped. 686 * @param toleranceMeters The tolerance in meters. 687 * @return A trigger that is true when the robot is within toleranceMeters of the given 688 * translation. 689 */ 690 public Trigger atTranslation(Translation2d translation, double toleranceMeters) { 691 Translation2d flippedTranslation = ChoreoAllianceFlipUtil.flip(translation); 692 return new Trigger( 693 () -> { 694 if (allianceCtx.allianceKnownOrIgnored()) { 695 final Translation2d currentTrans = poseSupplier.get().getTranslation(); 696 if (allianceCtx.doFlip()) { 697 return currentTrans.getDistance(flippedTranslation) < toleranceMeters; 698 } else { 699 return currentTrans.getDistance(translation) < toleranceMeters; 700 } 701 } else { 702 allianceNotReady.set(true); 703 return false; 704 } 705 }) 706 .and(active()); 707 } 708 709 /** 710 * Returns a trigger that is true when the robot is within toleranceMeters and toleranceRadians of 711 * the given event's translation. 712 * 713 * <p>A warning will be printed to the DriverStation if the event is not found and the trigger 714 * will always be false. 715 * 716 * @param eventName The name of the event. 717 * @param toleranceMeters The tolerance in meters. 718 * @return A trigger that is true when the robot is within toleranceMeters of the given events 719 * translation. 720 * @see <a href="https://choreo.autos/usage/editing-paths/#event-markers">Event Markers in the 721 * GUI</a> 722 */ 723 public Trigger atTranslation(String eventName, double toleranceMeters) { 724 boolean foundEvent = false; 725 Trigger trig = new Trigger(() -> false); 726 727 for (var event : trajectory.getEvents(eventName)) { 728 // This could create a lot of objects, could be done a more efficient way 729 // with having it all be 1 trigger that just has a list of poses and checks each one each 730 // cycle or something like that. 731 // If choreo starts showing memory issues we can look into this. 732 Optional<Translation2d> translationOpt = 733 trajectory 734 // don't mirror here because the translations are mirrored themselves 735 // this also lets atTranslation be called before the alliance is ready 736 .sampleAt(event.timestamp, false) 737 .map(TrajectorySample::getPose) 738 .map(Pose2d::getTranslation); 739 if (translationOpt.isPresent()) { 740 trig = trig.or(atTranslation(translationOpt.get(), toleranceMeters)); 741 foundEvent = true; 742 } 743 } 744 745 // The user probably expects an event to exist if they're trying to do something at that event, 746 // report the missing event. 747 if (!foundEvent && warnUser) { 748 eventNotFound.addCause(name); 749 } 750 751 return trig; 752 } 753 754 /** 755 * Returns an array of all the timestamps of the events with the given name. 756 * 757 * @param eventName The name of the event. 758 * @return An array of all the timestamps of the events with the given name. 759 * @see <a href="https://choreo.autos/usage/editing-paths/#event-markers">Event Markers in the 760 * GUI</a> 761 */ 762 public double[] collectEventTimes(String eventName) { 763 double[] times = 764 trajectory.getEvents(eventName).stream() 765 .filter(e -> e.timestamp >= 0 && e.timestamp <= trajectory.getTotalTime()) 766 .mapToDouble(e -> e.timestamp) 767 .toArray(); 768 769 if (times.length == 0 && warnUser) { 770 eventNotFound.addCause("collectEvents(" + eventName + ")"); 771 } 772 773 return times; 774 } 775 776 /** 777 * Returns an array of all the poses of the events with the given name. 778 * 779 * <p>The returned poses are always unflipped. If you use them in a trigger like `atPose` or 780 * `atTranslation`, the library will automatically flip them if necessary. If you intend using 781 * them in a different context, you can use {@link ChoreoAllianceFlipUtil#flip} to flip them. 782 * 783 * @param eventName The name of the event. 784 * @return An array of all the poses of the events with the given name. 785 * @see <a href="https://choreo.autos/usage/editing-paths/#event-markers">Event Markers in the 786 * GUI</a> 787 */ 788 public Pose2d[] collectEventPoses(String eventName) { 789 double[] times = collectEventTimes(eventName); 790 Pose2d[] poses = new Pose2d[times.length]; 791 for (int i = 0; i < times.length; i++) { 792 Pose2d pose = 793 trajectory 794 .sampleAt(times[i], false) 795 .map(TrajectorySample::getPose) 796 .get(); // the event times are guaranteed to be valid 797 poses[i] = pose; 798 } 799 return poses; 800 } 801 802 @Override 803 public boolean equals(Object obj) { 804 return obj instanceof AutoTrajectory traj && name.equals(traj.name); 805 } 806}