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}