001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.math.trajectory;
006
007import edu.wpi.first.math.MathSharedStore;
008import edu.wpi.first.math.geometry.Pose2d;
009import edu.wpi.first.math.geometry.Rotation2d;
010import edu.wpi.first.math.geometry.Transform2d;
011import edu.wpi.first.math.geometry.Translation2d;
012import edu.wpi.first.math.spline.PoseWithCurvature;
013import edu.wpi.first.math.spline.Spline;
014import edu.wpi.first.math.spline.SplineHelper;
015import edu.wpi.first.math.spline.SplineParameterizer;
016import edu.wpi.first.math.spline.SplineParameterizer.MalformedSplineException;
017import java.util.ArrayList;
018import java.util.Collection;
019import java.util.List;
020import java.util.function.BiConsumer;
021
022/** Helper class used to generate trajectories with various constraints. */
023public final class TrajectoryGenerator {
024  private static final Trajectory kDoNothingTrajectory =
025      new Trajectory(List.of(new Trajectory.State()));
026  private static BiConsumer<String, StackTraceElement[]> errorFunc;
027
028  /** Private constructor because this is a utility class. */
029  private TrajectoryGenerator() {}
030
031  private static void reportError(String error, StackTraceElement[] stackTrace) {
032    if (errorFunc != null) {
033      errorFunc.accept(error, stackTrace);
034    } else {
035      MathSharedStore.reportError(error, stackTrace);
036    }
037  }
038
039  /**
040   * Set error reporting function. By default, DriverStation.reportError() is used.
041   *
042   * @param func Error reporting function, arguments are error and stackTrace.
043   */
044  public static void setErrorHandler(BiConsumer<String, StackTraceElement[]> func) {
045    errorFunc = func;
046  }
047
048  /**
049   * Generates a trajectory from the given control vectors and config. This method uses clamped
050   * cubic splines -- a method in which the exterior control vectors and interior waypoints are
051   * provided. The headings are automatically determined at the interior points to ensure continuous
052   * curvature.
053   *
054   * @param initial The initial control vector.
055   * @param interiorWaypoints The interior waypoints.
056   * @param end The ending control vector.
057   * @param config The configuration for the trajectory.
058   * @return The generated trajectory.
059   */
060  public static Trajectory generateTrajectory(
061      Spline.ControlVector initial,
062      List<Translation2d> interiorWaypoints,
063      Spline.ControlVector end,
064      TrajectoryConfig config) {
065    final var flip = new Transform2d(new Translation2d(), Rotation2d.fromDegrees(180.0));
066
067    // Clone the control vectors.
068    var newInitial = new Spline.ControlVector(initial.x, initial.y);
069    var newEnd = new Spline.ControlVector(end.x, end.y);
070
071    // Change the orientation if reversed.
072    if (config.isReversed()) {
073      newInitial.x[1] *= -1;
074      newInitial.y[1] *= -1;
075      newEnd.x[1] *= -1;
076      newEnd.y[1] *= -1;
077    }
078
079    // Get the spline points
080    List<PoseWithCurvature> points;
081    try {
082      points =
083          splinePointsFromSplines(
084              SplineHelper.getCubicSplinesFromControlVectors(
085                  newInitial, interiorWaypoints.toArray(new Translation2d[0]), newEnd));
086    } catch (MalformedSplineException ex) {
087      reportError(ex.getMessage(), ex.getStackTrace());
088      return kDoNothingTrajectory;
089    }
090
091    // Change the points back to their original orientation.
092    if (config.isReversed()) {
093      for (var point : points) {
094        point.poseMeters = point.poseMeters.plus(flip);
095        point.curvatureRadPerMeter *= -1;
096      }
097    }
098
099    // Generate and return trajectory.
100    return TrajectoryParameterizer.timeParameterizeTrajectory(
101        points,
102        config.getConstraints(),
103        config.getStartVelocity(),
104        config.getEndVelocity(),
105        config.getMaxVelocity(),
106        config.getMaxAcceleration(),
107        config.isReversed());
108  }
109
110  /**
111   * Generates a trajectory from the given waypoints and config. This method uses clamped cubic
112   * splines -- a method in which the initial pose, final pose, and interior waypoints are provided.
113   * The headings are automatically determined at the interior points to ensure continuous
114   * curvature.
115   *
116   * @param start The starting pose.
117   * @param interiorWaypoints The interior waypoints.
118   * @param end The ending pose.
119   * @param config The configuration for the trajectory.
120   * @return The generated trajectory.
121   */
122  public static Trajectory generateTrajectory(
123      Pose2d start, List<Translation2d> interiorWaypoints, Pose2d end, TrajectoryConfig config) {
124    var controlVectors =
125        SplineHelper.getCubicControlVectorsFromWaypoints(
126            start, interiorWaypoints.toArray(new Translation2d[0]), end);
127
128    // Return the generated trajectory.
129    return generateTrajectory(controlVectors[0], interiorWaypoints, controlVectors[1], config);
130  }
131
132  /**
133   * Generates a trajectory from the given quintic control vectors and config. This method uses
134   * quintic hermite splines -- therefore, all points must be represented by control vectors.
135   * Continuous curvature is guaranteed in this method.
136   *
137   * @param controlVectors List of quintic control vectors.
138   * @param config The configuration for the trajectory.
139   * @return The generated trajectory.
140   */
141  public static Trajectory generateTrajectory(
142      ControlVectorList controlVectors, TrajectoryConfig config) {
143    final var flip = new Transform2d(new Translation2d(), Rotation2d.fromDegrees(180.0));
144    final var newControlVectors = new ArrayList<Spline.ControlVector>(controlVectors.size());
145
146    // Create a new control vector list, flipping the orientation if reversed.
147    for (final var vector : controlVectors) {
148      var newVector = new Spline.ControlVector(vector.x, vector.y);
149      if (config.isReversed()) {
150        newVector.x[1] *= -1;
151        newVector.y[1] *= -1;
152      }
153      newControlVectors.add(newVector);
154    }
155
156    // Get the spline points
157    List<PoseWithCurvature> points;
158    try {
159      points =
160          splinePointsFromSplines(
161              SplineHelper.getQuinticSplinesFromControlVectors(
162                  newControlVectors.toArray(new Spline.ControlVector[] {})));
163    } catch (MalformedSplineException ex) {
164      reportError(ex.getMessage(), ex.getStackTrace());
165      return kDoNothingTrajectory;
166    }
167
168    // Change the points back to their original orientation.
169    if (config.isReversed()) {
170      for (var point : points) {
171        point.poseMeters = point.poseMeters.plus(flip);
172        point.curvatureRadPerMeter *= -1;
173      }
174    }
175
176    // Generate and return trajectory.
177    return TrajectoryParameterizer.timeParameterizeTrajectory(
178        points,
179        config.getConstraints(),
180        config.getStartVelocity(),
181        config.getEndVelocity(),
182        config.getMaxVelocity(),
183        config.getMaxAcceleration(),
184        config.isReversed());
185  }
186
187  /**
188   * Generates a trajectory from the given waypoints and config. This method uses quintic hermite
189   * splines -- therefore, all points must be represented by Pose2d objects. Continuous curvature is
190   * guaranteed in this method.
191   *
192   * @param waypoints List of waypoints..
193   * @param config The configuration for the trajectory.
194   * @return The generated trajectory.
195   */
196  public static Trajectory generateTrajectory(List<Pose2d> waypoints, TrajectoryConfig config) {
197    final var flip = new Transform2d(new Translation2d(), Rotation2d.fromDegrees(180.0));
198
199    List<Pose2d> newWaypoints = new ArrayList<>();
200    if (config.isReversed()) {
201      for (Pose2d originalWaypoint : waypoints) {
202        newWaypoints.add(originalWaypoint.plus(flip));
203      }
204    } else {
205      newWaypoints.addAll(waypoints);
206    }
207
208    // Get the spline points
209    List<PoseWithCurvature> points;
210    try {
211      points =
212          splinePointsFromSplines(
213              SplineHelper.optimizeCurvature(
214                  SplineHelper.getQuinticSplinesFromWaypoints(newWaypoints)));
215    } catch (MalformedSplineException ex) {
216      reportError(ex.getMessage(), ex.getStackTrace());
217      return kDoNothingTrajectory;
218    }
219
220    // Change the points back to their original orientation.
221    if (config.isReversed()) {
222      for (var point : points) {
223        point.poseMeters = point.poseMeters.plus(flip);
224        point.curvatureRadPerMeter *= -1;
225      }
226    }
227
228    // Generate and return trajectory.
229    return TrajectoryParameterizer.timeParameterizeTrajectory(
230        points,
231        config.getConstraints(),
232        config.getStartVelocity(),
233        config.getEndVelocity(),
234        config.getMaxVelocity(),
235        config.getMaxAcceleration(),
236        config.isReversed());
237  }
238
239  /**
240   * Generate spline points from a vector of splines by parameterizing the splines.
241   *
242   * @param splines The splines to parameterize.
243   * @return The spline points for use in time parameterization of a trajectory.
244   * @throws MalformedSplineException When the spline is malformed (e.g. has close adjacent points
245   *     with approximately opposing headings)
246   */
247  public static List<PoseWithCurvature> splinePointsFromSplines(Spline[] splines) {
248    // Create the vector of spline points.
249    var splinePoints = new ArrayList<PoseWithCurvature>();
250
251    // Add the first point to the vector.
252    splinePoints.add(splines[0].getPoint(0.0));
253
254    // Iterate through the vector and parameterize each spline, adding the
255    // parameterized points to the final vector.
256    for (final var spline : splines) {
257      var points = SplineParameterizer.parameterize(spline);
258
259      // Append the array of poses to the vector. We are removing the first
260      // point because it's a duplicate of the last point from the previous
261      // spline.
262      splinePoints.addAll(points.subList(1, points.size()));
263    }
264    return splinePoints;
265  }
266
267  /** Control vector list type that works around type erasure signatures. */
268  public static class ControlVectorList extends ArrayList<Spline.ControlVector> {
269    /** Default constructor. */
270    public ControlVectorList() {
271      super();
272    }
273
274    /**
275     * Constructs a ControlVectorList.
276     *
277     * @param initialCapacity The initial list capacity.
278     */
279    public ControlVectorList(int initialCapacity) {
280      super(initialCapacity);
281    }
282
283    /**
284     * Constructs a ControlVectorList.
285     *
286     * @param collection A collection of spline control vectors.
287     */
288    public ControlVectorList(Collection<? extends Spline.ControlVector> collection) {
289      super(collection);
290    }
291  }
292}