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 Transform2d kFlip = new Transform2d(Translation2d.kZero, Rotation2d.kPi);
025
026  private static final Trajectory kDoNothingTrajectory =
027      new Trajectory(List.of(new Trajectory.State()));
028  private static BiConsumer<String, StackTraceElement[]> errorFunc;
029
030  /** Private constructor because this is a utility class. */
031  private TrajectoryGenerator() {}
032
033  private static void reportError(String error, StackTraceElement[] stackTrace) {
034    if (errorFunc != null) {
035      errorFunc.accept(error, stackTrace);
036    } else {
037      MathSharedStore.reportError(error, stackTrace);
038    }
039  }
040
041  /**
042   * Set error reporting function. By default, DriverStation.reportError() is used.
043   *
044   * @param func Error reporting function, arguments are error and stackTrace.
045   */
046  public static void setErrorHandler(BiConsumer<String, StackTraceElement[]> func) {
047    errorFunc = func;
048  }
049
050  /**
051   * Generates a trajectory from the given control vectors and config. This method uses clamped
052   * cubic splines -- a method in which the exterior control vectors and interior waypoints are
053   * provided. The headings are automatically determined at the interior points to ensure continuous
054   * curvature.
055   *
056   * @param initial The initial control vector.
057   * @param interiorWaypoints The interior waypoints.
058   * @param end The ending control vector.
059   * @param config The configuration for the trajectory.
060   * @return The generated trajectory.
061   */
062  public static Trajectory generateTrajectory(
063      Spline.ControlVector initial,
064      List<Translation2d> interiorWaypoints,
065      Spline.ControlVector end,
066      TrajectoryConfig config) {
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(kFlip);
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 newControlVectors = new ArrayList<Spline.ControlVector>(controlVectors.size());
144
145    // Create a new control vector list, flipping the orientation if reversed.
146    for (final var vector : controlVectors) {
147      var newVector = new Spline.ControlVector(vector.x, vector.y);
148      if (config.isReversed()) {
149        newVector.x[1] *= -1;
150        newVector.y[1] *= -1;
151      }
152      newControlVectors.add(newVector);
153    }
154
155    // Get the spline points
156    List<PoseWithCurvature> points;
157    try {
158      points =
159          splinePointsFromSplines(
160              SplineHelper.getQuinticSplinesFromControlVectors(
161                  newControlVectors.toArray(new Spline.ControlVector[] {})));
162    } catch (MalformedSplineException ex) {
163      reportError(ex.getMessage(), ex.getStackTrace());
164      return kDoNothingTrajectory;
165    }
166
167    // Change the points back to their original orientation.
168    if (config.isReversed()) {
169      for (var point : points) {
170        point.poseMeters = point.poseMeters.plus(kFlip);
171        point.curvatureRadPerMeter *= -1;
172      }
173    }
174
175    // Generate and return trajectory.
176    return TrajectoryParameterizer.timeParameterizeTrajectory(
177        points,
178        config.getConstraints(),
179        config.getStartVelocity(),
180        config.getEndVelocity(),
181        config.getMaxVelocity(),
182        config.getMaxAcceleration(),
183        config.isReversed());
184  }
185
186  /**
187   * Generates a trajectory from the given waypoints and config. This method uses quintic hermite
188   * splines -- therefore, all points must be represented by Pose2d objects. Continuous curvature is
189   * guaranteed in this method.
190   *
191   * @param waypoints List of waypoints..
192   * @param config The configuration for the trajectory.
193   * @return The generated trajectory.
194   */
195  public static Trajectory generateTrajectory(List<Pose2d> waypoints, TrajectoryConfig config) {
196    List<Pose2d> newWaypoints = new ArrayList<>();
197    if (config.isReversed()) {
198      for (Pose2d originalWaypoint : waypoints) {
199        newWaypoints.add(originalWaypoint.plus(kFlip));
200      }
201    } else {
202      newWaypoints.addAll(waypoints);
203    }
204
205    // Get the spline points
206    List<PoseWithCurvature> points;
207    try {
208      points =
209          splinePointsFromSplines(
210              SplineHelper.optimizeCurvature(
211                  SplineHelper.getQuinticSplinesFromWaypoints(newWaypoints)));
212    } catch (MalformedSplineException ex) {
213      reportError(ex.getMessage(), ex.getStackTrace());
214      return kDoNothingTrajectory;
215    }
216
217    // Change the points back to their original orientation.
218    if (config.isReversed()) {
219      for (var point : points) {
220        point.poseMeters = point.poseMeters.plus(kFlip);
221        point.curvatureRadPerMeter *= -1;
222      }
223    }
224
225    // Generate and return trajectory.
226    return TrajectoryParameterizer.timeParameterizeTrajectory(
227        points,
228        config.getConstraints(),
229        config.getStartVelocity(),
230        config.getEndVelocity(),
231        config.getMaxVelocity(),
232        config.getMaxAcceleration(),
233        config.isReversed());
234  }
235
236  /**
237   * Generate spline points from a vector of splines by parameterizing the splines.
238   *
239   * @param splines The splines to parameterize.
240   * @return The spline points for use in time parameterization of a trajectory.
241   * @throws MalformedSplineException When the spline is malformed (e.g. has close adjacent points
242   *     with approximately opposing headings)
243   */
244  public static List<PoseWithCurvature> splinePointsFromSplines(Spline[] splines) {
245    // Create the vector of spline points.
246    var splinePoints = new ArrayList<PoseWithCurvature>();
247
248    // Add the first point to the vector.
249    splinePoints.add(splines[0].getPoint(0.0));
250
251    // Iterate through the vector and parameterize each spline, adding the
252    // parameterized points to the final vector.
253    for (final var spline : splines) {
254      var points = SplineParameterizer.parameterize(spline);
255
256      // Append the array of poses to the vector. We are removing the first
257      // point because it's a duplicate of the last point from the previous
258      // spline.
259      splinePoints.addAll(points.subList(1, points.size()));
260    }
261    return splinePoints;
262  }
263
264  /** Control vector list type that works around type erasure signatures. */
265  public static class ControlVectorList extends ArrayList<Spline.ControlVector> {
266    /** Default constructor. */
267    public ControlVectorList() {
268      super();
269    }
270
271    /**
272     * Constructs a ControlVectorList.
273     *
274     * @param initialCapacity The initial list capacity.
275     */
276    public ControlVectorList(int initialCapacity) {
277      super(initialCapacity);
278    }
279
280    /**
281     * Constructs a ControlVectorList.
282     *
283     * @param collection A collection of spline control vectors.
284     */
285    public ControlVectorList(Collection<? extends Spline.ControlVector> collection) {
286      super(collection);
287    }
288  }
289}