/*******************************************************************************
* Copyright (c) 2011, 2015 itemis AG and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Alexander Nyßen (itemis AG) - initial API and implementation
* Matthias Wienand (itemis AG) - contribution for Bugzilla #355997
*
*******************************************************************************/
package org.eclipse.gef.geometry.planar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import org.eclipse.gef.geometry.euclidean.Angle;
import org.eclipse.gef.geometry.internal.utils.PrecisionUtils;
/**
* Represents the geometric shape of an ellipse.
*
* Note that while all manipulations (e.g. within shrink, expand) within this
* class are based on double precision, all comparisons (e.g. within contains,
* intersects, equals, etc.) are based on a limited precision (with an accuracy
* defined within {@link PrecisionUtils}) to compensate for rounding effects.
*
* @author anyssen
* @author mwienand
*
*/
public class Ellipse extends AbstractRectangleBasedGeometry<Ellipse, PolyBezier>
implements IShape {
private static final long serialVersionUID = 1L;
/**
* Constructs a new {@link Ellipse} so that it is fully contained within the
* framing rectangle defined by (x, y, width, height).
*
* @param x
* The x-coordinate of the framing rectangle
* @param y
* The y-coordinate of the framing rectangle
* @param width
* The width of the framing rectangle
* @param height
* The height of the framing rectangle
*/
public Ellipse(double x, double y, double width, double height) {
super(x, y, width, height);
}
/**
* Constructs a new {@link Ellipse} so that it is fully contained within the
* given framing {@link Rectangle}.
*
* @param r
* The framing {@link Rectangle} used to construct this
* {@link Ellipse}.
*/
public Ellipse(Rectangle r) {
this(r.getX(), r.getY(), r.getWidth(), r.getHeight());
}
/**
* @see IShape#contains(IGeometry)
*/
@Override
public boolean contains(IGeometry g) {
return ShapeUtils.contains(this, g);
}
/**
* Tests whether the given {@link Line} is fully contained within this
* {@link Ellipse}.
*
* @param l
* the {@link Line} to test for containment
* @return <code>true</code> in case the given {@link Line} is fully
* contained, <code>false</code> otherwise
*/
public boolean contains(Line l) {
return contains(l.getP1()) && contains(l.getP2());
}
/**
* @see IGeometry#contains(Point)
*/
@Override
public boolean contains(Point p) {
/*
* point has to fulfill (x/a)^2 + (y/b)^2 <= 1, where a = width/2 and b
* = height/2, if ellipse is centered around origin, so we have to
* normalize point p by subtracting the center
*/
double normalizedX = p.x - (x + width / 2);
double normalizedY = p.y - (y + height / 2);
// then check if it fulfills the ellipse equation
if (PrecisionUtils.smallerEqual(
normalizedX * normalizedX / (width * width * 0.25d)
+ normalizedY * normalizedY / (height * height * 0.25d),
1d)) {
return true;
}
// is it "on" the outline?
for (CubicCurve seg : getOutlineSegments()) {
if (seg.contains(p)) {
return true;
}
}
// TODO: what about the small space between outline and ellipse area?
return false;
}
/**
* Tests whether this {@link Ellipse} and the ellipse defined by the given
* bounds are equal.
*
* @param x
* the x-coordinate of the bounds defining define the ellipse to
* test
* @param y
* the y-coordinate of the bounds defining the ellipse to test
* @param width
* the width of the bounds defining the ellipse to test
* @param height
* the height of the bounds defining the ellipse to test
* @return <code>true</code> if this {@link Ellipse} and the ellipse defined
* via the given bounds are (imprecisely) regarded to be equal,
* <code>false</code> otherwise
*/
public boolean equals(double x, double y, double width, double height) {
return PrecisionUtils.equal(this.x, x)
&& PrecisionUtils.equal(this.y, y)
&& PrecisionUtils.equal(this.width, width)
&& PrecisionUtils.equal(this.height, height);
}
/**
* Tests whether this {@link Ellipse} is equal to the given {@link Object}.
*
* @return <code>true</code> if the given {@link Object} is an
* {@link Ellipse}, which is (imprecisely) equal to this
* {@link Ellipse}, i.e. whose bounds are (imprecisely) equal to
* this {@link Ellipse}'s bounds
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof Ellipse) {
Ellipse e = (Ellipse) o;
return equals(e.getX(), e.getY(), e.getWidth(), e.getHeight());
}
return false;
}
/**
* Returns a new {@link Ellipse} with the same location and size than this
* one.
*
* @return A copy of this {@link Ellipse}, having the same x, y, width, and
* height values
*/
@Override
public Ellipse getCopy() {
return new Ellipse(x, y, width, height);
}
/**
* Calculates the intersections of this {@link Ellipse} with the given other
* {@link Ellipse}.
*
* @param e2
* The {@link Ellipse} for which intersections are computed.
* @return {@link Point}s of intersection
*/
public Point[] getIntersections(Ellipse e2) {
if (equals(e2)) {
return new Point[] {};
}
HashSet<Point> intersections = new HashSet<>();
for (CubicCurve seg : getOutlineSegments()) {
intersections.addAll(Arrays.asList(e2.getIntersections(seg)));
}
return intersections.toArray(new Point[] {});
}
/**
* Calculates the intersections of this {@link Ellipse} with the given
* {@link ICurve}.
*
* @param c
* The {@link ICurve} for which intersections are computed.
* @return {@link Point}s of intersection
*/
public Point[] getIntersections(ICurve c) {
if (c instanceof Line) {
return getIntersections((Line) c);
}
return CurveUtils.getIntersections(c, this);
}
/**
* Returns the intersection points of this {@link Ellipse}'s outline with
* the given {@link Line}.
*
* @param line
* the {@link Line} to test for intersection
* @return an array containing the intersection points of this
* {@link Ellipse}'s outline with the given {@link Line} in case
* such intersection points exist, an empty array otherwise
*/
public Point[] getIntersections(Line line) {
List<Point> intersections = new ArrayList<>(2);
// ellipse equation x^2/(width/2)^2 + y^2/(height/2)^2 = 1 may be
// written as x^2/aSq + y^2/bSq = 1 with:
double a = width / 2;
double b = height / 2;
double aSq = width * width * 0.25d;
double bSq = height * height * 0.25d;
// ellipse's center
double xOffset = x + a;
double yOffset = y + b;
// normalized line points as if the ellipse was centered around origin
double x1 = line.getX1() - xOffset;
double y1 = line.getY1() - yOffset;
double x2 = line.getX2() - xOffset;
double y2 = line.getY2() - yOffset;
// deltas to calculate the line's slope
double dy = y2 - y1;
double dx = x2 - x1;
// special-case the vertical line
if (PrecisionUtils.equal(dx, 0, +2)) {
// vertical line
if (PrecisionUtils.smallerEqual(-a, x1)
&& PrecisionUtils.smallerEqual(x1, a)) { // -a <= x1 <= a
// inside the ellipse
// x is known (x = x1), so we do only need to calculate y.
// y = +- sqrt(rad), follows from the equations above.
double rad = bSq * (1 - x1 * x1 / aSq);
double y = rad < 0 ? 0 : Math.sqrt(rad);
if (y == 0) {
if (isInBetween(0, y1, y2)) {
intersections.add(new Point(x1, 0));
}
} else {
if (isInBetween(y, y1, y2)) {
intersections.add(new Point(x1, y));
}
if (isInBetween(-y, y1, y2)) {
intersections.add(new Point(x1, -y));
}
}
}
} else {
// calculating the line function's slope and y-offset:
double m = dy / dx;
double n = y1 - m * x1;
// substituting y within ellipse equation leads to a quadratic
// equation of the form q2*x^2 + q1*x + q0 = 0 with:
double q2 = bSq + aSq * m * m;
double q1 = 2 * m * aSq * n;
double q0 = aSq * (n * n - bSq);
// values for the pq-formula:
double p = q1 / q2;
double q = q0 / q2;
// check if equation has at least one solution
double d = p * p / 4 - q;
if (PrecisionUtils.equal(d, 0, +2)) {
// discriminant equals zero, so one possible solution
double px = -p / 2;
double py = px * m + n;
intersections.add(new Point(px, py));
} else if (d > 0) {
// discriminant greater than zero, so two possible solutions
double sqrt = d < 0 ? 0 : Math.sqrt(d);
// first
double px = -p / 2 + sqrt;
double py = px * m + n;
intersections.add(new Point(px, py));
// second
px = -p / 2 - sqrt;
py = px * m + n;
intersections.add(new Point(px, py));
}
}
// sort out all points that do not lie on the given line and translate
// the coordinates back:
for (Iterator<Point> i = intersections.iterator(); i.hasNext();) {
Point p = i.next();
p.translate(xOffset, yOffset);
if (!line.contains(p)) {
i.remove();
}
}
return intersections.toArray(new Point[] {});
}
@Override
public ICurve getOutline() {
return ShapeUtils.getOutline(this);
}
/**
* Calculates the outline segments of this {@link Ellipse}. The outline
* segments are approximated by {@link CubicCurve}s. The outline segments
* are returned in the following order:
* <ol>
* <li>from <code>0deg</code> to <code>90deg</code> (quadrant I)</li>
* <li>from <code>90deg</code> to <code>180deg</code> (quadrant II)</li>
* <li>from <code>180deg</code> to <code>270deg</code> (quadrant III)</li>
* <li>from <code>270deg</code> to <code>360deg</code> (quadrant IV)</li>
* </ol>
* An {@link Angle} of <code>0deg</code> is oriented to the right.
* Increasing an {@link Angle} rotates counter-clockwise (CCW).
*
* @return an array of {@link CubicCurve}s representing the outline of this
* {@link Ellipse}
*/
@Override
public CubicCurve[] getOutlineSegments() {
return new CubicCurve[] {
ShapeUtils.computeEllipticalArcApproximation(x, y, width,
height, Angle.fromDeg(0), Angle.fromDeg(90)),
ShapeUtils.computeEllipticalArcApproximation(x, y, width,
height, Angle.fromDeg(90), Angle.fromDeg(180)),
ShapeUtils.computeEllipticalArcApproximation(x, y, width,
height, Angle.fromDeg(180), Angle.fromDeg(270)),
ShapeUtils.computeEllipticalArcApproximation(x, y, width,
height, Angle.fromDeg(270), Angle.fromDeg(360)), };
}
@Override
public PolyBezier getRotatedCCW(Angle angle) {
return new PolyBezier(getOutlineSegments()).rotateCCW(angle);
}
@Override
public PolyBezier getRotatedCCW(Angle angle, double cx, double cy) {
return new PolyBezier(getOutlineSegments()).rotateCCW(angle, cx, cy);
}
@Override
public PolyBezier getRotatedCCW(Angle angle, Point center) {
return new PolyBezier(getOutlineSegments()).rotateCCW(angle, center);
}
@Override
public PolyBezier getRotatedCW(Angle angle) {
return new PolyBezier(getOutlineSegments()).rotateCW(angle);
}
@Override
public PolyBezier getRotatedCW(Angle angle, double cx, double cy) {
return new PolyBezier(getOutlineSegments()).rotateCW(angle, cx, cy);
}
@Override
public PolyBezier getRotatedCW(Angle angle, Point center) {
return new PolyBezier(getOutlineSegments()).rotateCW(angle, center);
}
/**
* @see IGeometry#getTransformed(AffineTransform)
*/
@Override
public CurvedPolygon getTransformed(AffineTransform t) {
return new CurvedPolygon(getOutlineSegments()).getTransformed(t);
}
private boolean isInBetween(double a, double lower, double upper) {
if (upper < lower) {
double tmp = upper;
upper = lower;
lower = tmp;
}
return PrecisionUtils.greaterEqual(a, lower)
&& PrecisionUtils.smallerEqual(a, upper);
}
/**
* Returns a {@link Path} representation of this {@link Ellipse}, which is
* an approximation of the four {@link #getOutlineSegments() outline
* segments} by means of {@link CubicCurve}s.
*
* @see IGeometry#toPath()
*/
@Override
public Path toPath() {
return CurveUtils.toPath(getOutlineSegments()).close();
}
@Override
public String toString() {
return "Ellipse (" + x + ", " + y + ", " + //$NON-NLS-3$//$NON-NLS-2$//$NON-NLS-1$
width + ", " + height + ")";//$NON-NLS-2$//$NON-NLS-1$
}
}