001// Copyright (c) Choreo contributors
002
003package choreo.trajectory;
004
005import edu.wpi.first.math.geometry.Pose2d;
006import java.util.ArrayList;
007import java.util.List;
008import java.util.Optional;
009
010/**
011 * A trajectory loaded from Choreo.
012 *
013 * @param <SampleType> DifferentialSample or SwerveSample.
014 */
015public class Trajectory<SampleType extends TrajectorySample<SampleType>> {
016  private final String name;
017  private final List<SampleType> samples;
018  private final List<Integer> splits;
019  private final List<EventMarker> events;
020
021  /**
022   * Constructs a Trajectory with the specified parameters.
023   *
024   * @param name The name of the trajectory.
025   * @param samples The samples of the trajectory.
026   * @param splits The indices of the splits in the trajectory.
027   * @param events The events in the trajectory.
028   */
029  public Trajectory(
030      String name, List<SampleType> samples, List<Integer> splits, List<EventMarker> events) {
031    this.name = name;
032    this.samples = samples;
033    this.splits = splits;
034    this.events = events;
035  }
036
037  /**
038   * Returns the name of the trajectory.
039   *
040   * @return the name of the trajectory.
041   */
042  public String name() {
043    return name;
044  }
045
046  /**
047   * Returns the samples of the trajectory.
048   *
049   * @return the samples of the trajectory.
050   */
051  public List<SampleType> samples() {
052    return samples;
053  }
054
055  /**
056   * Returns the indices of the splits in the trajectory.
057   *
058   * @return the indices of the splits in the trajectory.
059   */
060  public List<Integer> splits() {
061    return splits;
062  }
063
064  /**
065   * Returns the events in the trajectory.
066   *
067   * @return the events in the trajectory.
068   */
069  public List<EventMarker> events() {
070    return events;
071  }
072
073  /**
074   * Returns the first {@link SampleType} in the trajectory.
075   *
076   * <p><b>NULL SAFETY:</b> This function will return null if the trajectory is empty.
077   *
078   * @param mirrorForRedAlliance whether or not to return the sample as mirrored across the field
079   * @return The first {@link SampleType} in the trajectory.
080   */
081  public SampleType getInitialSample(boolean mirrorForRedAlliance) {
082    if (samples.isEmpty()) {
083      return null;
084    }
085    return mirrorForRedAlliance ? samples.get(0).flipped() : samples.get(0);
086  }
087
088  /**
089   * Returns the last {@link SampleType} in the trajectory.
090   *
091   * <p><b>NULL SAFETY:</b> This function will return null if the trajectory is empty.
092   *
093   * @param mirrorForRedAlliance whether or not to return the sample as mirrored across the field
094   * @return The last {@link SampleType} in the trajectory.
095   */
096  public SampleType getFinalSample(boolean mirrorForRedAlliance) {
097    if (samples.isEmpty()) {
098      return null;
099    }
100    return mirrorForRedAlliance
101        ? samples.get(samples.size() - 1).flipped()
102        : samples.get(samples.size() - 1);
103  }
104
105  private SampleType sampleInternal(double timestamp) {
106    if (timestamp < samples.get(0).getTimestamp()) {
107      // timestamp oob, return the initial state
108      return getInitialSample(false);
109    }
110    if (timestamp >= getTotalTime()) {
111      // timestamp oob, return the final state
112      return getFinalSample(false);
113    }
114
115    // binary search to find the sample before and ahead of the timestamp
116    int low = 0;
117    int high = samples.size() - 1;
118
119    while (low != high) {
120      int mid = (low + high) / 2;
121      if (samples.get(mid).getTimestamp() < timestamp) {
122        low = mid + 1;
123      } else {
124        high = mid;
125      }
126    }
127
128    if (low == 0) {
129      return samples.get(low);
130    }
131
132    var behindState = samples.get(low - 1);
133    var aheadState = samples.get(low);
134
135    if ((aheadState.getTimestamp() - behindState.getTimestamp()) < 1e-6) {
136      return aheadState;
137    }
138
139    return behindState.interpolate(aheadState, timestamp);
140  }
141
142  /**
143   * Return an interpolated sample of the trajectory at the given timestamp.
144   *
145   * <p><b>NULL SAFETY:</b> This function will return null if the trajectory is empty.
146   *
147   * @param timestamp The timestamp of this sample relative to the beginning of the trajectory.
148   * @param mirrorForRedAlliance whether or not to return the sample as mirrored across the field
149   *     midline (as in 2023).
150   * @return The SampleType at the given time.
151   */
152  public SampleType sampleAt(double timestamp, boolean mirrorForRedAlliance) {
153    SampleType state;
154    if (samples.isEmpty()) {
155      return null;
156    } else if (samples.size() == 1) {
157      state = samples.get(0);
158    } else {
159      state = sampleInternal(timestamp);
160    }
161    return mirrorForRedAlliance ? state.flipped() : state;
162  }
163
164  /**
165   * Returns the initial pose of the trajectory.
166   *
167   * <p><b>NULL SAFETY:</b> This function will return null if the trajectory is empty.
168   *
169   * @param mirrorForRedAlliance whether or not to return the pose as mirrored across the field
170   * @return the initial pose of the trajectory.
171   */
172  public Pose2d getInitialPose(boolean mirrorForRedAlliance) {
173    if (samples.isEmpty()) {
174      return null;
175    }
176    return getInitialSample(mirrorForRedAlliance).getPose();
177  }
178
179  /**
180   * Returns the final pose of the trajectory.
181   *
182   * <p><b>NULL SAFETY:</b> This function will return null if the trajectory is empty.
183   *
184   * @param mirrorForRedAlliance whether or not to return the pose as mirrored across the field
185   * @return the final pose of the trajectory.
186   */
187  public Pose2d getFinalPose(boolean mirrorForRedAlliance) {
188    if (samples.isEmpty()) {
189      return null;
190    }
191    return getFinalSample(mirrorForRedAlliance).getPose();
192  }
193
194  /**
195   * Returns the total time of the trajectory (the timestamp of the last sample)
196   *
197   * @return the total time of the trajectory (the timestamp of the last sample)
198   */
199  public double getTotalTime() {
200    if (samples.isEmpty()) {
201      return 0;
202    }
203    return getFinalSample(false).getTimestamp();
204  }
205
206  /**
207   * Returns the array of poses corresponding to the trajectory.
208   *
209   * @return the array of poses corresponding to the trajectory.
210   */
211  public Pose2d[] getPoses() {
212    return samples.stream().map(SampleType::getPose).toArray(Pose2d[]::new);
213  }
214
215  /**
216   * Returns an array of samples
217   *
218   * @return an array of samples
219   */
220  @SuppressWarnings("unchecked")
221  public SampleType[] sampleArray() {
222    if (!samples.isEmpty()) {
223      return samples.toArray(samples.get(0).makeArray(samples.size()));
224    } else {
225      return (SampleType[]) new Object[0];
226    }
227  }
228
229  /**
230   * Returns this trajectory, mirrored across the field midline.
231   *
232   * @return this trajectory, mirrored across the field midline.
233   */
234  public Trajectory<SampleType> flipped() {
235    var flippedStates = new ArrayList<SampleType>();
236    for (var state : samples) {
237      flippedStates.add(state.flipped());
238    }
239    return new Trajectory<SampleType>(this.name, flippedStates, this.splits, this.events);
240  }
241
242  /**
243   * Returns a list of all events with the given name in the trajectory.
244   *
245   * @param eventName The name of the event.
246   * @return A list of all events with the given name in the trajectory, if no events are found, an
247   *     empty list is returned.
248   */
249  public List<EventMarker> getEvents(String eventName) {
250    return events.stream().filter(event -> event.event.equals(eventName)).toList();
251  }
252
253  /**
254   * Returns a choreo trajectory that represents the split of the trajectory at the given index.
255   *
256   * @param splitIndex the index of the split trajectory to return.
257   * @return a choreo trajectory that represents the split of the trajectory at the given index.
258   */
259  public Optional<Trajectory<SampleType>> getSplit(int splitIndex) {
260    if (splitIndex < 0 || splitIndex >= splits.size()) {
261      return Optional.empty();
262    }
263    int start = splits.get(splitIndex);
264    int end = splitIndex + 1 < splits.size() ? splits.get(splitIndex + 1) + 1 : samples.size();
265    var sublist = samples.subList(start, end);
266    double startTime = sublist.get(0).getTimestamp();
267    double endTime = sublist.get(sublist.size() - 1).getTimestamp();
268    return Optional.of(
269        new Trajectory<SampleType>(
270            this.name + "[" + splitIndex + "]",
271            sublist.stream().map(s -> s.offsetBy(-startTime)).toList(),
272            List.of(),
273            events.stream()
274                .filter(e -> e.timestamp >= startTime && e.timestamp <= endTime)
275                .map(e -> e.offsetBy(-startTime))
276                .toList()));
277  }
278
279  @Override
280  public boolean equals(Object obj) {
281    if (!(obj instanceof Trajectory<?>)) {
282      return false;
283    }
284
285    var other = (Trajectory<?>) obj;
286    return this.name.equals(other.name)
287        && this.samples.equals(other.samples)
288        && this.splits.equals(other.splits)
289        && this.events.equals(other.events);
290  }
291}