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