001// Copyright (c) Choreo contributors
002
003package choreo;
004
005import static org.wpilib.util.Alert.Level.HIGH;
006import static org.wpilib.util.ErrorMessages.requireNonNullParam;
007
008import choreo.trajectory.DifferentialSample;
009import choreo.trajectory.EventMarker;
010import choreo.trajectory.SwerveSample;
011import choreo.trajectory.Trajectory;
012import choreo.trajectory.TrajectorySample;
013import choreo.util.ChoreoAlert;
014import choreo.util.ChoreoAlert.*;
015import choreo.util.TrajSchemaVersion;
016import com.google.gson.Gson;
017import com.google.gson.GsonBuilder;
018import com.google.gson.JsonObject;
019import com.google.gson.JsonSyntaxException;
020import java.io.BufferedReader;
021import java.io.File;
022import java.io.FileNotFoundException;
023import java.io.FileReader;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Optional;
030import java.util.function.BiConsumer;
031import org.wpilib.driverstation.DriverStationErrors;
032import org.wpilib.hardware.hal.HAL;
033import org.wpilib.system.Filesystem;
034import org.wpilib.util.Alert.Level;
035
036/** Utilities to load and follow Choreo Trajectories */
037public final class Choreo {
038  private static final Gson GSON =
039      new GsonBuilder()
040          .registerTypeAdapter(EventMarker.class, new EventMarker.Deserializer())
041          .create();
042  private static final String TRAJECTORY_FILE_EXTENSION = ".traj";
043  private static final int TRAJ_SCHEMA_VERSION = TrajSchemaVersion.TRAJ_SCHEMA_VERSION;
044  private static final MultiAlert cantFindTrajectory =
045      ChoreoAlert.multiAlert(causes -> "Could not find trajectory files: " + causes, HIGH);
046  private static final MultiAlert cantParseTrajectory =
047      ChoreoAlert.multiAlert(causes -> "Could not parse trajectory files: " + causes, Level.HIGH);
048  private static final MultiAlert unknownTrajectoryError =
049      ChoreoAlert.multiAlert(
050          causes -> "Unknown error when parsing " + causes + "; check console for more details",
051          Level.HIGH);
052
053  private static File CHOREO_DIR = new File(Filesystem.getDeployDirectory(), "choreo");
054
055  /** This should only be used for unit testing. */
056  static void setChoreoDir(File choreoDir) {
057    CHOREO_DIR = choreoDir;
058  }
059
060  /**
061   * This interface exists as a type alias. A TrajectoryLogger has a signature of ({@link
062   * Trajectory}, {@link Boolean})->void, where the function consumes a trajectory and a boolean
063   * indicating whether the trajectory is starting or finishing.
064   *
065   * @param <ST> {@link choreo.trajectory.DifferentialSample} or {@link
066   *     choreo.trajectory.SwerveSample}
067   */
068  public interface TrajectoryLogger<ST extends TrajectorySample<ST>>
069      extends BiConsumer<Trajectory<ST>, Boolean> {}
070
071  /** Default constructor. */
072  private Choreo() {
073    throw new UnsupportedOperationException("This is a utility class!");
074  }
075
076  /**
077   * Load a trajectory from the deploy directory. Choreolib expects .traj files to be placed in
078   * src/main/deploy/choreo/[trajectoryName].traj.
079   *
080   * @param <SampleType> The type of samples in the trajectory.
081   * @param trajectoryName The path name in Choreo, which matches the file name in the deploy
082   *     directory, file extension is optional.
083   * @return The loaded trajectory, or `Optional.empty()` if the trajectory could not be loaded.
084   */
085  @SuppressWarnings("unchecked")
086  public static <SampleType extends TrajectorySample<SampleType>>
087      Optional<Trajectory<SampleType>> loadTrajectory(String trajectoryName) {
088    requireNonNullParam(trajectoryName, "trajectoryName", "Choreo.loadTrajectory");
089
090    if (trajectoryName.endsWith(TRAJECTORY_FILE_EXTENSION)) {
091      trajectoryName =
092          trajectoryName.substring(0, trajectoryName.length() - TRAJECTORY_FILE_EXTENSION.length());
093    }
094    File trajectoryFile = new File(CHOREO_DIR, trajectoryName + TRAJECTORY_FILE_EXTENSION);
095    try {
096      var reader = new BufferedReader(new FileReader(trajectoryFile));
097      String str = reader.lines().reduce("", (a, b) -> a + b);
098      reader.close();
099      Trajectory<SampleType> trajectory = (Trajectory<SampleType>) loadTrajectoryString(str);
100      return Optional.of(trajectory);
101    } catch (FileNotFoundException ex) {
102      cantFindTrajectory.addCause(trajectoryFile.toString());
103    } catch (JsonSyntaxException ex) {
104      cantParseTrajectory.addCause(trajectoryFile.toString());
105    } catch (Exception ex) {
106      unknownTrajectoryError.addCause(trajectoryFile.toString());
107      DriverStationErrors.reportError(ex.getMessage(), ex.getStackTrace());
108    }
109    return Optional.empty();
110  }
111
112  /**
113   * Fetches the names of all available trajectories in the deploy directory.
114   *
115   * @return A list of all available trajectory names.
116   */
117  public static String[] availableTrajectories() {
118    List<String> trajectories = new ArrayList<>();
119    File[] files = CHOREO_DIR.listFiles();
120    if (files != null) {
121      for (File file : files) {
122        if (file.getName().endsWith(TRAJECTORY_FILE_EXTENSION)) {
123          trajectories.add(
124              file.getName()
125                  .substring(0, file.getName().length() - TRAJECTORY_FILE_EXTENSION.length()));
126        }
127      }
128    }
129    return trajectories.toArray(new String[0]);
130  }
131
132  /**
133   * Load a trajectory from a string.
134   *
135   * @param trajectoryJsonString The JSON string.
136   * @return The loaded trajectory, or `empty std::optional` if the trajectory could not be loaded.
137   */
138  static Trajectory<? extends TrajectorySample<?>> loadTrajectoryString(
139      String trajectoryJsonString) {
140    JsonObject wholeTrajectory = GSON.fromJson(trajectoryJsonString, JsonObject.class);
141    String name = wholeTrajectory.get("name").getAsString();
142    int version;
143    try {
144      version = wholeTrajectory.get("version").getAsInt();
145      if (version != TRAJ_SCHEMA_VERSION) {
146        throw new RuntimeException(
147            name + ".traj: Wrong version: " + version + ". Expected " + TRAJ_SCHEMA_VERSION);
148      }
149    } catch (ClassCastException e) {
150      throw new RuntimeException(
151          name
152              + ".traj: Wrong version: "
153              + wholeTrajectory.get("version").getAsString()
154              + ". Expected "
155              + TRAJ_SCHEMA_VERSION);
156    }
157    // Filter out markers with negative timestamps or empty names
158    List<EventMarker> unfilteredEvents =
159        new ArrayList<EventMarker>(
160            Arrays.asList(GSON.fromJson(wholeTrajectory.get("events"), EventMarker[].class)));
161    unfilteredEvents.removeIf(marker -> marker.timestamp < 0 || marker.event.length() == 0);
162    EventMarker[] events = new EventMarker[unfilteredEvents.size()];
163    unfilteredEvents.toArray(events);
164
165    JsonObject trajectoryObj = wholeTrajectory.getAsJsonObject("trajectory");
166    Integer[] splits = GSON.fromJson(trajectoryObj.get("splits"), Integer[].class);
167    if (splits.length == 0 || splits[0] != 0) {
168      Integer[] newArray = new Integer[splits.length + 1];
169      newArray[0] = 0;
170      System.arraycopy(splits, 0, newArray, 1, splits.length);
171      splits = newArray;
172    }
173    String sampleType = trajectoryObj.get("sampleType").getAsString();
174    if (sampleType.equals("Swerve")) {
175      HAL.reportUsage("ChoreoTrajectory", 1, "Swerve");
176
177      SwerveSample[] samples = GSON.fromJson(trajectoryObj.get("samples"), SwerveSample[].class);
178      return new Trajectory<SwerveSample>(name, List.of(samples), List.of(splits), List.of(events));
179    } else if (sampleType.equals("Differential")) {
180      HAL.reportUsage("ChoreoTrajectory", 2, "Differential");
181
182      DifferentialSample[] sampleArray =
183          GSON.fromJson(trajectoryObj.get("samples"), DifferentialSample[].class);
184      return new Trajectory<DifferentialSample>(
185          name, List.of(sampleArray), List.of(splits), List.of(events));
186    } else {
187      throw new RuntimeException("Unknown drive type: " + sampleType);
188    }
189  }
190
191  /**
192   * A utility for caching loaded trajectories. This allows for loading trajectories only once, and
193   * then reusing them.
194   */
195  public static class TrajectoryCache {
196    private final Map<String, Trajectory<?>> cache;
197
198    /** Creates a new TrajectoryCache with a normal {@link HashMap} as the cache. */
199    public TrajectoryCache() {
200      cache = new HashMap<>();
201    }
202
203    /**
204     * Creates a new TrajectoryCache with a custom cache.
205     *
206     * <p>this could be useful if you want to use a concurrent map or a map with a maximum size.
207     *
208     * @param cache The cache to use.
209     */
210    public TrajectoryCache(Map<String, Trajectory<?>> cache) {
211      requireNonNullParam(cache, "cache", "TrajectoryCache.<init>");
212      this.cache = cache;
213    }
214
215    /**
216     * Load a trajectory from the deploy directory. Choreolib expects .traj files to be placed in
217     * src/main/deploy/choreo/[trajectoryName].traj.
218     *
219     * <p>This method will cache the loaded trajectory and reused it if it is requested again.
220     *
221     * @param trajectoryName the path name in Choreo, which matches the file name in the deploy
222     *     directory, file extension is optional.
223     * @return the loaded trajectory, or `Optional.empty()` if the trajectory could not be loaded.
224     * @see Choreo#loadTrajectory(String)
225     */
226    public Optional<? extends Trajectory<?>> loadTrajectory(String trajectoryName) {
227      requireNonNullParam(trajectoryName, "trajectoryName", "TrajectoryCache.loadTrajectory");
228      if (cache.containsKey(trajectoryName)) {
229        return Optional.of(cache.get(trajectoryName));
230      } else {
231        return Choreo.loadTrajectory(trajectoryName)
232            .map(
233                trajectory -> {
234                  cache.put(trajectoryName, trajectory);
235                  return trajectory;
236                });
237      }
238    }
239
240    /**
241     * Load a section of a split trajectory from the deploy directory. Choreolib expects .traj files
242     * to be placed in src/main/deploy/choreo/[trajectoryName].traj.
243     *
244     * <p>This method will cache the loaded trajectory and reused it if it is requested again. The
245     * trajectory that is split off of will also be cached.
246     *
247     * @param trajectoryName the path name in Choreo, which matches the file name in the deploy
248     *     directory, file extension is optional.
249     * @param splitIndex the index of the split trajectory to load
250     * @return the loaded trajectory, or `Optional.empty()` if the trajectory could not be loaded.
251     * @see Choreo#loadTrajectory(String)
252     */
253    public Optional<? extends Trajectory<?>> loadTrajectory(String trajectoryName, int splitIndex) {
254      requireNonNullParam(trajectoryName, "trajectoryName", "TrajectoryCache.loadTrajectory");
255      // make the key something that could never possibly be a valid trajectory name
256      String key = trajectoryName + ".:." + splitIndex;
257      if (cache.containsKey(key)) {
258        return Optional.of(cache.get(key));
259      } else if (cache.containsKey(trajectoryName)) {
260        return cache
261            .get(trajectoryName)
262            .getSplit(splitIndex)
263            .map(
264                trajectory -> {
265                  cache.put(key, trajectory);
266                  return trajectory;
267                });
268      } else {
269        return Choreo.loadTrajectory(trajectoryName)
270            .flatMap(
271                trajectory -> {
272                  cache.put(trajectoryName, trajectory);
273                  return trajectory
274                      .getSplit(splitIndex)
275                      .map(
276                          split -> {
277                            cache.put(key, split);
278                            return split;
279                          });
280                });
281      }
282    }
283
284    /** Clear the cache. */
285    public void clear() {
286      cache.clear();
287    }
288  }
289}