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 org.wpilib.math.geometry;
006
007import static org.wpilib.math.autodiff.Variable.pow;
008import static org.wpilib.math.optimization.Constraints.eq;
009import static org.wpilib.units.Units.Meters;
010
011import java.util.Objects;
012import org.wpilib.math.geometry.proto.Ellipse2dProto;
013import org.wpilib.math.geometry.struct.Ellipse2dStruct;
014import org.wpilib.math.optimization.Problem;
015import org.wpilib.math.util.Pair;
016import org.wpilib.units.measure.Distance;
017import org.wpilib.util.protobuf.ProtobufSerializable;
018import org.wpilib.util.struct.StructSerializable;
019
020/** Represents a 2d ellipse space containing translational, rotational, and scaling components. */
021public final class Ellipse2d implements ProtobufSerializable, StructSerializable {
022  private final Pose2d m_center;
023  private final double m_xSemiAxis;
024  private final double m_ySemiAxis;
025
026  /**
027   * Constructs an ellipse around a center point and two semi-axes, a horizontal and vertical one.
028   *
029   * @param center The center of the ellipse.
030   * @param xSemiAxis The x semi-axis.
031   * @param ySemiAxis The y semi-axis.
032   */
033  public Ellipse2d(Pose2d center, double xSemiAxis, double ySemiAxis) {
034    if (xSemiAxis <= 0 || ySemiAxis <= 0) {
035      throw new IllegalArgumentException("Ellipse2d semi-axes must be positive");
036    }
037
038    m_center = center;
039    m_xSemiAxis = xSemiAxis;
040    m_ySemiAxis = ySemiAxis;
041  }
042
043  /**
044   * Constructs an ellipse around a center point and two semi-axes, a horizontal and vertical one.
045   * The X and Y semi-axis will be converted to and tracked as meters.
046   *
047   * @param center The center of the ellipse.
048   * @param xSemiAxis The x semi-axis.
049   * @param ySemiAxis The y semi-axis.
050   */
051  public Ellipse2d(Pose2d center, Distance xSemiAxis, Distance ySemiAxis) {
052    this(center, xSemiAxis.in(Meters), ySemiAxis.in(Meters));
053  }
054
055  /**
056   * Constructs a perfectly circular ellipse with the specified radius.
057   *
058   * @param center The center of the circle.
059   * @param radius The radius of the circle.
060   */
061  public Ellipse2d(Translation2d center, double radius) {
062    this(new Pose2d(center, Rotation2d.kZero), radius, radius);
063  }
064
065  /**
066   * Constructs a perfectly circular ellipse with the specified radius. The radius will be converted
067   * to and tracked as meters.
068   *
069   * @param center The center of the circle.
070   * @param radius The radius of the circle.
071   */
072  public Ellipse2d(Translation2d center, Distance radius) {
073    this(new Pose2d(center, Rotation2d.kZero), radius, radius);
074  }
075
076  /**
077   * Returns the center of the ellipse.
078   *
079   * @return The center of the ellipse.
080   */
081  public Pose2d getCenter() {
082    return m_center;
083  }
084
085  /**
086   * Returns the rotational component of the ellipse.
087   *
088   * @return The rotational component of the ellipse.
089   */
090  public Rotation2d getRotation() {
091    return m_center.getRotation();
092  }
093
094  /**
095   * Returns the x semi-axis.
096   *
097   * @return The x semi-axis.
098   */
099  public double getXSemiAxis() {
100    return m_xSemiAxis;
101  }
102
103  /**
104   * Returns the y semi-axis.
105   *
106   * @return The y semi-axis.
107   */
108  public double getYSemiAxis() {
109    return m_ySemiAxis;
110  }
111
112  /**
113   * Returns the x semi-axis in a measure.
114   *
115   * @return The x semi-axis in a measure.
116   */
117  public Distance getMeasureXSemiAxis() {
118    return Meters.of(m_xSemiAxis);
119  }
120
121  /**
122   * Returns the y semi-axis in a measure.
123   *
124   * @return The y semi-axis in a measure.
125   */
126  public Distance getMeasureYSemiAxis() {
127    return Meters.of(m_ySemiAxis);
128  }
129
130  /**
131   * Returns the focal points of the ellipse. In a perfect circle, this will always return the
132   * center.
133   *
134   * @return The focal points.
135   */
136  public Pair<Translation2d, Translation2d> getFocalPoints() {
137    // Major semi-axis
138    double a = Math.max(m_xSemiAxis, m_ySemiAxis);
139
140    // Minor semi-axis
141    double b = Math.min(m_xSemiAxis, m_ySemiAxis);
142
143    double c = Math.sqrt(a * a - b * b);
144
145    if (m_xSemiAxis > m_ySemiAxis) {
146      return new Pair<>(
147          m_center.plus(new Transform2d(-c, 0.0, Rotation2d.kZero)).getTranslation(),
148          m_center.plus(new Transform2d(c, 0.0, Rotation2d.kZero)).getTranslation());
149    } else {
150      return new Pair<>(
151          m_center.plus(new Transform2d(0.0, -c, Rotation2d.kZero)).getTranslation(),
152          m_center.plus(new Transform2d(0.0, c, Rotation2d.kZero)).getTranslation());
153    }
154  }
155
156  /**
157   * Transforms the center of the ellipse and returns the new ellipse.
158   *
159   * @param other The transform to transform by.
160   * @return The transformed ellipse.
161   */
162  public Ellipse2d transformBy(Transform2d other) {
163    return new Ellipse2d(m_center.transformBy(other), m_xSemiAxis, m_ySemiAxis);
164  }
165
166  /**
167   * Rotates the center of the ellipse and returns the new ellipse.
168   *
169   * @param other The rotation to transform by.
170   * @return The rotated ellipse.
171   */
172  public Ellipse2d rotateBy(Rotation2d other) {
173    return new Ellipse2d(m_center.rotateBy(other), m_xSemiAxis, m_ySemiAxis);
174  }
175
176  /**
177   * Checks if a point is intersected by this ellipse's circumference.
178   *
179   * @param point The point to check.
180   * @return True, if this ellipse's circumference intersects the point.
181   */
182  public boolean intersects(Translation2d point) {
183    return Math.abs(1.0 - solveEllipseEquation(point)) <= 1E-9;
184  }
185
186  /**
187   * Checks if a point is contained within this ellipse. This is inclusive, if the point lies on the
188   * circumference this will return {@code true}.
189   *
190   * @param point The point to check.
191   * @return True, if the point is within or on the ellipse.
192   */
193  public boolean contains(Translation2d point) {
194    return solveEllipseEquation(point) <= 1.0;
195  }
196
197  /**
198   * Returns the distance between the perimeter of the ellipse and the point.
199   *
200   * @param point The point to check.
201   * @return The distance (0, if the point is contained by the ellipse)
202   */
203  public double getDistance(Translation2d point) {
204    return nearest(point).getDistance(point);
205  }
206
207  /**
208   * Returns the distance between the perimeter of the ellipse and the point in a measure.
209   *
210   * @param point The point to check.
211   * @return The distance (0, if the point is contained by the ellipse) in a measure.
212   */
213  public Distance getMeasureDistance(Translation2d point) {
214    return Meters.of(getDistance(point));
215  }
216
217  /**
218   * Returns the nearest point that is contained within the ellipse.
219   *
220   * @param point The point that this will find the nearest point to.
221   * @return A new point that is nearest to {@code point} and contained in the ellipse.
222   */
223  public Translation2d nearest(Translation2d point) {
224    // Check if already in ellipse
225    if (contains(point)) {
226      return point;
227    }
228
229    // Rotate the point by the inverse of the ellipse's rotation
230    var rotPoint =
231        point.rotateAround(m_center.getTranslation(), m_center.getRotation().unaryMinus());
232
233    // Find nearest point
234    try (var problem = new Problem()) {
235      // Point on ellipse
236      var x = problem.decisionVariable();
237      x.setValue(rotPoint.getX());
238      var y = problem.decisionVariable();
239      y.setValue(rotPoint.getY());
240
241      problem.minimize(pow(x.minus(rotPoint.getX()), 2).plus(pow(y.minus(rotPoint.getY()), 2)));
242
243      // (x − x_c)²/a² + (y − y_c)²/b² = 1
244      // b²(x − x_c)² + a²(y − y_c)² = a²b²
245      double a2 = m_xSemiAxis * m_xSemiAxis;
246      double b2 = m_ySemiAxis * m_ySemiAxis;
247      problem.subjectTo(
248          eq(
249              pow(x.minus(m_center.getX()), 2)
250                  .times(b2)
251                  .plus(pow(y.minus(m_center.getY()), 2).times(a2)),
252              a2 * b2));
253
254      problem.solve();
255
256      rotPoint = new Translation2d(x.value(), y.value());
257    }
258
259    // Undo rotation
260    return rotPoint.rotateAround(m_center.getTranslation(), m_center.getRotation());
261  }
262
263  @Override
264  public String toString() {
265    return String.format(
266        "Ellipse2d(center: %s, x: %.2f, y:%.2f)", m_center, m_xSemiAxis, m_ySemiAxis);
267  }
268
269  /**
270   * Checks equality between this Ellipse2d and another object.
271   *
272   * @param obj The other object.
273   * @return Whether the two objects are equal or not.
274   */
275  @Override
276  public boolean equals(Object obj) {
277    if (obj instanceof Ellipse2d) {
278      return ((Ellipse2d) obj).getCenter().equals(m_center)
279          && ((Ellipse2d) obj).getXSemiAxis() == m_xSemiAxis
280          && ((Ellipse2d) obj).getYSemiAxis() == m_ySemiAxis;
281    }
282    return false;
283  }
284
285  @Override
286  public int hashCode() {
287    return Objects.hash(m_center, m_xSemiAxis, m_ySemiAxis);
288  }
289
290  /**
291   * Solves the equation of an ellipse from the given point. This is a helper function used to
292   * determine if that point lies inside of or on an ellipse.
293   *
294   * <pre>
295   * (x - h)²/a² + (y - k)²/b² = 1
296   * </pre>
297   *
298   * @param point The point to solve for.
299   * @return < 1.0 if the point lies inside the ellipse, == 1.0 if a point lies on the ellipse, and
300   *     > 1.0 if the point lies outsides the ellipse.
301   */
302  private double solveEllipseEquation(Translation2d point) {
303    // Rotate the point by the inverse of the ellipse's rotation
304    point = point.rotateAround(m_center.getTranslation(), m_center.getRotation().unaryMinus());
305
306    double x = point.getX() - m_center.getX();
307    double y = point.getY() - m_center.getY();
308
309    return (x * x) / (m_xSemiAxis * m_xSemiAxis) + (y * y) / (m_ySemiAxis * m_ySemiAxis);
310  }
311
312  /** Ellipse2d protobuf for serialization. */
313  public static final Ellipse2dProto proto = new Ellipse2dProto();
314
315  /** Ellipse2d struct for serialization. */
316  public static final Ellipse2dStruct struct = new Ellipse2dStruct();
317}