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}