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