/*******************************************************************************
* 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
*
*******************************************************************************/
package org.eclipse.gef.geometry.planar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.gef.geometry.euclidean.Angle;
import org.eclipse.gef.geometry.internal.utils.PrecisionUtils;
import org.eclipse.gef.geometry.planar.BezierCurve.IntervalPair;
/**
* The {@link ShapeUtils} class provides functionality that can be used for all
* shapes, independent on their construction kind.
*
* @author anyssen
*
*/
class ShapeUtils {
/**
* <p>
* Computes a {@link CubicCurve} that approximates the elliptical
* {@link Arc} given by the location, the width, and the height of the
* implied ellipse and the start and end {@link Angle}s of the arc.
* </p>
*
* <p>
* The given start and end {@link Angle}s may not span an {@link Angle} of
* more than 90 degrees.
* </p>
*
* @param x
* left coordinate value of the aforementioned ellipse
* @param y
* top coordinate value of the aforementioned ellipse
* @param width
* width of the aforementioned ellipse
* @param height
* height of the aforementioned ellipse
* @param start
* start angle (in radiant) of the elliptical arc
* @param end
* end angle (in radiant) of the elliptical arc
* @return {@link CubicCurve} approximating the determinated elliptical arc
*/
public static CubicCurve computeEllipticalArcApproximation(double x,
double y, double width, double height, Angle start, Angle end) {
// TODO: verify that the following test is valid
if (!PrecisionUtils.smallerEqual(
end.getAdded(start.getOppositeFull()).deg(), 90)) {
throw new IllegalArgumentException(
"Only angular extents of up to 90 degrees are allowed.");
}
// compute major and minor axis length
double a = width / 2;
double b = height / 2;
double srad = start.rad();
double erad = end.rad();
// calculate start and end points of the arc from start to end
Point startPoint = new Point(x + a + a * Math.cos(srad),
y + b - b * Math.sin(srad));
Point endPoint = new Point(x + a + a * Math.cos(erad),
y + b - b * Math.sin(erad));
// approximation by cubic Bezier according to approximation provided in:
// http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
double t = Math.tan((erad - srad) / 2);
double alpha = Math.sin(erad - srad)
* (Math.sqrt(4.0d + 3.0d * t * t) - 1) / 3;
Point controlPoint1 = new Point(
startPoint.x + alpha * -a * Math.sin(srad),
startPoint.y - alpha * b * Math.cos(srad));
Point controlPoint2 = new Point(
endPoint.x - alpha * -a * Math.sin(erad),
endPoint.y + alpha * b * Math.cos(erad));
Point[] points = new Point[] { startPoint, controlPoint1, controlPoint2,
endPoint };
return new CubicCurve(points);
}
/**
* Returns <code>true</code> if the second {@link IGeometry} is fully
* contained by the first {@link IGeometry}. Otherwise, <code>false</code>
* is returned.
*
* @param geom1
* The {@link IGeometry} which is tested to contain the given
* other {@link IGeometry}.
* @param geom2
* The {@link IGeometry} which is tested for containment.
* @return <code>true</code> if the first {@link IGeometry} contains the
* second {@link IGeometry}, otherwise <code>false</code>
*/
public static boolean contains(IGeometry geom1, IGeometry geom2) {
if (geom1 instanceof IShape) {
return contains((IShape) geom1, geom2);
} else if (geom1 instanceof IMultiShape) {
return contains((IMultiShape) geom1, geom2);
} else {
return false;
}
}
/**
* Returns <code>true</code> if the given {@link BezierCurve} is fully
* contained by the given {@link IMultiShape}.
*
* @param multiShape
* The {@link IMultiShape} which is tested to contain the given
* {@link BezierCurve}.
* @param c
* The {@link BezierCurve} which is tested for containment.
* @return <code>true</code> if the {@link BezierCurve} is contained by the
* {@link IMultiShape}, otherwise <code>false</code>
*/
public static boolean contains(IMultiShape multiShape, BezierCurve c) {
// TODO: generalize the contains() method for IShape and IMultiShape.
if (!(multiShape.contains(c.getP1())
&& multiShape.contains(c.getP2()))) {
return false;
}
Set<Double> intersectionParams = new HashSet<>();
for (ICurve segC : multiShape.getOutlineSegments()) {
for (BezierCurve seg : segC.toBezier()) {
Set<Point> inters = new HashSet<>();
Set<IntervalPair> ips = c.getIntersectionIntervalPairs(seg,
inters);
for (IntervalPair ip : ips) {
intersectionParams
.add(ip.p == c ? ip.pi.getMid() : ip.qi.getMid());
}
for (Point poi : inters) {
intersectionParams.add(c.getParameterAt(poi));
}
}
}
/*
* Start and end point of the curve are guaranteed to lie inside the
* IMultiShape. If the curve would not be contained by the shape, at
* least two intersections could be found.
*
* TODO: Special case! There is a special case where the Bezier curve
* leaves and enters the shape in the same point. This is only possible
* if the Bezier curve has a self intersection at that point.
*/
if (intersectionParams.size() <= 1) {
return true;
}
Double[] poiParams = intersectionParams.toArray(new Double[] {});
Arrays.sort(poiParams, new Comparator<Double>() {
@Override
public int compare(Double t, Double u) {
double d = t - u;
return d < 0 ? -1 : d > 0 ? 1 : 0;
}
});
// check the points between the intersections for containment
if (!multiShape.contains(c.get(poiParams[0] / 2))) {
return false;
}
for (int i = 0; i < poiParams.length - 1; i++) {
if (!multiShape
.contains(c.get((poiParams[i] + poiParams[i + 1]) / 2))) {
return false;
}
}
return multiShape
.contains(c.get((poiParams[poiParams.length - 1] + 1) / 2));
}
/**
* Checks if the given {@link ICurve} is contained by the given
* {@link IMultiShape}.
*
* @param ps
* The {@link IMultiShape} which is tested to contain the given
* {@link ICurve}.
* @param c
* The {@link ICurve} which is tested for containment.
* @return <code>true</code> if the {@link ICurve} is contained by the
* {@link IMultiShape}, otherwise <code>false</code>
*/
public static boolean contains(IMultiShape ps, ICurve c) {
for (BezierCurve bc : c.toBezier()) {
if (!contains(ps, bc)) {
return false;
}
}
return true;
}
/**
* Checks if the {@link IGeometry} is contained by the {@link IMultiShape}.
*
* @param ps
* The {@link IMultiShape} which is tested to contain the given
* {@link IGeometry}.
* @param g
* The {@link IGeometry} which is tested for containment.
* @return <code>true</code> if the {@link IGeometry} is contained by the
* {@link IMultiShape}, otherwise <code>false</code>
*/
public static boolean contains(IMultiShape ps, IGeometry g) {
if (g instanceof ICurve) {
return contains(ps, (ICurve) g);
} else if (g instanceof IShape) {
return contains(ps, (IShape) g);
} else if (g instanceof IMultiShape) {
return contains(ps, (IMultiShape) g);
} else {
throw new UnsupportedOperationException("Not yet implemented.");
}
}
/**
* Checks if the second {@link IMultiShape} is contained by the first
* {@link IMultiShape}.
*
* @param ps
* The {@link IMultiShape} which is tested to contain the given
* other {@link IMultiShape}.
* @param ps2
* The {@link IMultiShape} which is tested for containment.
* @return <code>true</code> if the second {@link IMultiShape} is contained
* by the first {@link IMultiShape}, otherwise <code>false</code>
*/
public static boolean contains(IMultiShape ps, IMultiShape ps2) {
for (IShape s : ps2.getShapes()) {
if (!contains(ps, s)) {
return false;
}
}
return true;
}
/**
* Checks if the {@link IShape} is contained by the {@link IMultiShape}.
*
* @param ps
* The {@link IMultiShape} which is tested to contain the given
* {@link IShape}.
* @param s
* The {@link IShape} which is tested for containment.
* @return <code>true</code> if the {@link IShape} is contained by the
* {@link IMultiShape}, otherwise <code>false</code>
*/
public static boolean contains(IMultiShape ps, IShape s) {
for (ICurve c : s.getOutlineSegments()) {
if (!contains(ps, c)) {
return false;
}
}
return true;
}
/**
* <p>
* Tests if the given {@link BezierCurve} is fully contained by the given
* {@link IShape}. Returns <code>true</code> if the given
* {@link BezierCurve} is fully contained by the given {@link IShape}.
* Otherwise, returns <code>false</code>.
* </p>
*
* <p>
* At first, the algorithm checks if start and end {@link Point} of the
* {@link BezierCurve} are contained by the {@link IShape}. If this is not
* the case, <code>false</code> is returned.
* </p>
*
* <p>
* Subsequently, the {@link Point}s of intersection of the
* {@link BezierCurve} and the {@link IShape} are computed. If there are
* less then two intersection {@link Point}s, <code>true</code> is returned.
* </p>
*
* <p>
* Alternatively, the {@link BezierCurve}'s parameter values for the
* individual {@link Point}s of intersection are sorted. For every two
* consecutive parameter values, a {@link Point} on the {@link BezierCurve}
* between those two parameter values is computed. If any of those
* {@link Point}s is not contained by the {@link IShape}, <code>false</code>
* is returned. Otherwise, <code>true</code> is returned.
* </p>
*
* <p>
* Self-intersection-problem: If the {@link BezierCurve} has a
* self-intersection p and p lies on an outline segment of the
* {@link IShape} ( {@link IShape#getOutlineSegments()}), <code>true</code>
* is returned, although <code>false</code> would be the right answer.
* </p>
*
* @param shape
* the {@link IShape} that is tested to contain the given
* {@link BezierCurve}
* @param c
* the {@link BezierCurve} that is tested to be contained by the
* given {@link IShape}
* @return <code>true</code> if the given {@link BezierCurve} is fully
* contained by the given {@link IShape}
*/
public static boolean contains(IShape shape, BezierCurve c) {
if (!(shape.contains(c.getP1()) && shape.contains(c.getP2()))) {
return false;
}
Set<Double> intersectionParams = new HashSet<>();
for (ICurve segC : shape.getOutlineSegments()) {
for (BezierCurve seg : segC.toBezier()) {
Set<Point> inters = new HashSet<>();
c.getIntersectionIntervalPairs(seg, inters);
for (Point poi : inters) {
intersectionParams.add(c.getParameterAt(poi));
}
}
}
/*
* Start and end point of the curve are guaranteed to lie inside the
* IShape. If the curve would not be contained by the shape, at least
* two intersections could be found.
*
* TODO: Special case! There is a special case where the Bezier curve
* leaves and enters the shape in the same point. This is only possible
* if the Bezier curve has a self intersection at that point.
*/
if (intersectionParams.size() <= 1) {
return true;
}
Double[] poiParams = intersectionParams.toArray(new Double[] {});
Arrays.sort(poiParams, new Comparator<Double>() {
@Override
public int compare(Double t, Double u) {
double d = t - u;
return d < 0 ? -1 : d > 0 ? 1 : 0;
}
});
// check the points between the intersections for containment
if (!shape.contains(c.get(poiParams[0] / 2))) {
return false;
}
for (int i = 0; i < poiParams.length - 1; i++) {
if (!shape.contains(c.get((poiParams[i] + poiParams[i + 1]) / 2))) {
return false;
}
}
return shape.contains(c.get((poiParams[poiParams.length - 1] + 1) / 2));
}
/**
* Returns <code>true</code> if the given {@link IShape} fully contains the
* given {@link ICurve}. Otherwise, <code>false</code> is returned. A
* {@link ICurve} is contained by a {@link IShape} if the {@link ICurve}'s
* Bezier approximation ({@link ICurve#toBezier()}) is contained by the
* {@link IShape} ({@link ShapeUtils#contains(IShape, BezierCurve)}).
*
* @param shape
* the {@link IShape} that is tested to contain the
* {@link ICurve}
* @param curve
* the {@link ICurve} that is tested to be contained by the
* {@link IShape}
* @return <code>true</code> if the given {@link IShape} contains the
* {@link ICurve}, otherwise <code>false</code>
*/
public static boolean contains(IShape shape, ICurve curve) {
for (BezierCurve seg : curve.toBezier()) {
if (!contains(shape, seg)) {
return false;
}
}
return true;
}
/**
* Returns <code>true</code> if the given {@link IShape} fully contains the
* given {@link IGeometry}. Otherwise, <code>false</code> is returned.
*
* An <code>instanceof</code> test delegates to the appropriate method.
*
* @see ShapeUtils#contains(IShape, ICurve)
* @see ShapeUtils#contains(IShape, IShape)
* @see ShapeUtils#contains(IShape, IMultiShape)
* @param shape
* the {@link IShape} that is tested to contain the
* {@link IGeometry}
* @param geom
* the {@link IGeometry} that is tested to be contained by the
* {@link IShape}
* @return <code>true</code> if the {@link IShape} contains the
* {@link IGeometry}, otherwise <code>false</code>
*/
public static boolean contains(IShape shape, IGeometry geom) {
if (geom instanceof ICurve) {
return contains(shape, (ICurve) geom);
} else if (geom instanceof IShape) {
return contains(shape, (IShape) geom);
} else if (geom instanceof IMultiShape) {
return contains(shape, (IMultiShape) geom);
} else {
throw new UnsupportedOperationException("Not yet implemented.");
}
}
/**
* Returns <code>true</code> if the given {@link IShape} fully contains the
* given {@link IMultiShape}. Otherwise, <code>false</code> is returned.
*
* A {@link IMultiShape} is contained by a {@link IShape} if all of its sub-
* {@link IShape}s are contained by the {@link IShape} (see
* {@link IMultiShape#getShapes()} and
* {@link ShapeUtils#contains(IShape, IShape)}).
*
* @param shape
* the {@link IShape} that is tested to contain the
* {@link IMultiShape}
* @param multiShape
* the {@link IMultiShape} that is tested to be contained by the
* {@link IShape}
* @return <code>true</code> if the {@link IShape} contains the
* {@link IMultiShape}, otherwise <code>false</code>
*/
public static boolean contains(IShape shape, IMultiShape multiShape) {
for (IShape seg : multiShape.getShapes()) {
if (!contains(shape, seg)) {
return false;
}
}
return true;
}
/**
* Returns <code>true</code> if the second {@link IShape} is fully contained
* by the first {@link IShape}. Otherwise, <code>false</code> is returned.
*
* A {@link IShape} is contained by another {@link IShape} if all of its
* outline segments ({@link IShape#getOutlineSegments()}) are contained by
* the other {@link IShape} ({@link ShapeUtils#contains(IShape, ICurve)}).
*
* @param shape1
* the {@link IShape} that is tested to contain the other
* {@link IShape}
* @param shape2
* the {@link IShape} that is tested to be contained by the other
* {@link IShape}
* @return <code>true</code> if the second {@link IShape} is contained by
* the first {@link IShape}, otherwise <code>false</code>
*/
public static boolean contains(IShape shape1, IShape shape2) {
for (ICurve seg : shape2.getOutlineSegments()) {
if (!contains(shape1, seg)) {
return false;
}
}
return true;
}
/**
* Returns a {@link PolyBezier} that is constructed from the outline
* segments ( {@link IShape#getOutlineSegments()}) of the given
* {@link IShape}.
*
* @param shape
* the {@link IShape} to compute the outline for
* @return a {@link PolyBezier} that is constructed from the outline
* segments of the {@link IShape}
*/
public static PolyBezier getOutline(IShape shape) {
ICurve[] curves = shape.getOutlineSegments();
ArrayList<BezierCurve> beziers = new ArrayList<>(
curves.length);
for (ICurve c : curves) {
for (BezierCurve b : c.toBezier()) {
beziers.add(b);
}
}
return new PolyBezier(beziers.toArray(new BezierCurve[] {}));
}
private ShapeUtils() {
// this class should not be instantiated by clients
}
}