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}