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}