/*******************************************************************************
* Copyright (c) 2011, 2016 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.awt.geom.Area;
import java.awt.geom.Path2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.eclipse.gef.geometry.convert.awt.AWT2Geometry;
import org.eclipse.gef.geometry.convert.awt.Geometry2AWT;
import org.eclipse.gef.geometry.internal.utils.PrecisionUtils;
/**
* Represents the geometric shape of a path, which may consist of independent
* subgraphs.
*
* 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
*
*/
public class Path extends AbstractGeometry implements IGeometry {
/**
* Representation for different types of {@link Segment}s.
*
* @see #MOVE_TO
* @see #LINE_TO
* @see #QUAD_TO
* @see #CUBIC_TO
*/
public static class Segment {
/**
* A {@link #MOVE_TO} {@link Segment} represents a change of position
* while piecewise building a {@link Path}, without inserting a new
* curve.
*
* @see Path#moveTo(double, double)
*/
public static final int MOVE_TO = 0;
/**
* A {@link #LINE_TO} {@link Segment} represents a {@link Line} from the
* previous position of a {@link Path} to the {@link Point} at index 0
* associated with the {@link Segment}.
*
* @see Path#lineTo(double, double)
*/
public static final int LINE_TO = 1;
/**
* A {@link #QUAD_TO} {@link Segment} represents a
* {@link QuadraticCurve} from the previous position of a {@link Path}
* to the {@link Point} at index 1 associated with the {@link Segment}.
* The {@link Point} at index 0 is used as the handle {@link Point} of
* the {@link QuadraticCurve}.
*
* @see Path#quadTo(double, double, double, double)
*/
public static final int QUAD_TO = 2;
/**
* A {@link #CUBIC_TO} {@link Segment} represents a {@link CubicCurve}
* from the previous position of a {@link Path} to the {@link Point} at
* index 2 associated with the {@link Segment}. The {@link Point}s at
* indices 0 and 1 are used as the handle {@link Point}s of the
* {@link CubicCurve}.
*
* @see Path#cubicTo(double, double, double, double, double, double)
*/
public static final int CUBIC_TO = 3;
/**
* A {@link #CLOSE} {@link Segment} represents the link from the current
* position of a {@link Path} to the position of the last
* {@link #MOVE_TO} {@link Segment}.
*
* @see Path#close()
*/
public static final int CLOSE = 4;
private int type;
private Point[] points;
/**
* Constructs a new {@link Segment} of the given type. The passed-in
* {@link Point}s are associated with this {@link Segment}.
*
* @param type
* The type of the new {@link Segment}. It is one of
* <ul>
* <li>{@link #MOVE_TO}</li>
* <li>{@link #LINE_TO}</li>
* <li>{@link #QUAD_TO}</li>
* <li>{@link #CUBIC_TO}</li>
* </ul>
* @param points
* the {@link Point}s to associate with this {@link Segment}
*/
public Segment(int type, Point... points) {
switch (type) {
case MOVE_TO:
if (points == null || points.length != 1) {
throw new IllegalArgumentException(
"A Segment of type MOVE_TO has to be associate with exactly 1 point: new Segment("
+ type + ", " + (points == null ? "null"
: Arrays.asList(points))
+ ")");
}
break;
case LINE_TO:
if (points == null || points.length != 1) {
throw new IllegalArgumentException(
"A Segment of type LINE_TO has to be associate with exactly 1 point: new Segment("
+ type + ", " + (points == null ? "null"
: Arrays.asList(points))
+ ")");
}
break;
case QUAD_TO:
if (points == null || points.length != 2) {
throw new IllegalArgumentException(
"A Segment of type QUAD_TO has to be associate with exactly 2 points: new Segment("
+ type + ", " + (points == null ? "null"
: Arrays.asList(points))
+ ")");
}
break;
case CUBIC_TO:
if (points == null || points.length != 3) {
throw new IllegalArgumentException(
"A Segment of type CUBIC_TO has to be associate with exactly 3 point: new Segment("
+ type + ", " + (points == null ? "null"
: Arrays.asList(points))
+ ")");
}
break;
case CLOSE:
if (points != null && points.length != 0) {
throw new IllegalArgumentException(
"A Segment of type CLOSE is not to be associated with any points: new Segment("
+ type + ", " + (points == null ? "null"
: Arrays.asList(points))
+ ")");
}
break;
default:
throw new IllegalArgumentException(
"You can only create Segments of types MOVE_TO, LINE_TO, QUAD_TO, or CUBIC_TO: new Segment("
+ type + ", " + (points == null ? "null"
: Arrays.asList(points))
+ ")");
}
this.type = type;
this.points = points == null ? new Point[] {}
: Point.getCopy(points);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Segment) {
Segment s = (Segment) obj;
if (s.type == type && Arrays.equals(s.points, points)) {
return true;
}
}
return false;
}
/**
* Returns a copy of this {@link Segment}. The associated {@link Point}s
* are copied, too.
*
* @return a copy of this {@link Segment}
*/
public Segment getCopy() {
return new Segment(type, getPoints());
}
/**
* Returns a copy of the {@link Point}s associated with this
* {@link Segment}.
*
* @return a copy of the {@link Point}s associated with this
* {@link Segment}.
*/
public Point[] getPoints() {
return Point.getCopy(points);
}
/**
* Returns the type of this {@link Segment}.
*
* @return the type of this {@link Segment}
* @see #MOVE_TO
* @see #LINE_TO
* @see #QUAD_TO
* @see #CUBIC_TO
*/
public int getType() {
return type;
}
@Override
public int hashCode() {
return type;
}
}
/**
* Winding rule for determining the interior of the {@link Path}. Indicates
* that a {@link Point} is regarded to lie inside the {@link Path}, if any
* ray starting in that {@link Point} and pointing to infinity crosses the
* {@link Segment}s of the {@link Path} an odd number of times.
*/
public static final int WIND_EVEN_ODD = 0;
/**
* Winding rule for determining the interior of the {@link Path}. Indicates
* that a {@link Point} is regarded to lie inside the {@link Path}, if any
* ray starting from that {@link Point} and pointing to infinity is crossed
* by {@link Path} {@link Segment}s a different number of times in the
* counter-clockwise direction than in the clockwise direction.
*/
public static final int WIND_NON_ZERO = 1;
private static final long serialVersionUID = 1L;
/**
* Unions the two specified {@link Path}s
*
* @param pa
* the first area to add
* @param pb
* the second area to add
* @return the sum of the areas
*/
public static Path add(Path pa, Path pb) {
Area a = new Area(Geometry2AWT.toAWTPath(pa));
Area b = new Area(Geometry2AWT.toAWTPath(pb));
a.add(b);
return AWT2Geometry.toPath(new Path2D.Double(a));
}
/**
* Computes the area covered by the first or the second but not both given
* areas.
*
* @param pa
* the first area to compute the xor for
* @param pb
* the second area to compute the xor for
* @return the exclusive-or of the areas
*/
public static Path exclusiveOr(Path pa, Path pb) {
Area a = new Area(Geometry2AWT.toAWTPath(pa));
Area b = new Area(Geometry2AWT.toAWTPath(pb));
a.exclusiveOr(b);
return AWT2Geometry.toPath(new Path2D.Double(a));
}
/**
* Intersects the given areas.
*
* @param pa
* the first area to intersect
* @param pb
* the second area to intersect
* @return the intersection of the areas, i.e. the area covered by both
* areas
*/
public static Path intersect(Path pa, Path pb) {
Area a = new Area(Geometry2AWT.toAWTPath(pa));
Area b = new Area(Geometry2AWT.toAWTPath(pb));
a.intersect(b);
return AWT2Geometry.toPath(new Path2D.Double(a));
}
/**
* Subtracts the second given area from the first given area.
*
* @param pa
* the area to subtract from
* @param pb
* the area to subtract
* @return the area covered by the first but not the second given area
*/
public static Path subtract(Path pa, Path pb) {
Area a = new Area(Geometry2AWT.toAWTPath(pa));
Area b = new Area(Geometry2AWT.toAWTPath(pb));
a.subtract(b);
return AWT2Geometry.toPath(new Path2D.Double(a));
}
private int windingRule = WIND_NON_ZERO;
private List<Segment> segments = new ArrayList<>();
/**
* Creates a new empty path with a default winding rule of
* {@link #WIND_NON_ZERO}.
*/
public Path() {
}
/**
* Creates a new empty path with given winding rule.
*
* @param windingRule
* the winding rule to use; one of {@link #WIND_EVEN_ODD} or
* {@link #WIND_NON_ZERO}
*/
public Path(int windingRule) {
this.windingRule = windingRule;
}
/**
* Creates a path from the given segments, using the given winding rule.
*
* @param windingRule
* the winding rule to use; one of {@link #WIND_EVEN_ODD} or
* {@link #WIND_NON_ZERO}
* @param segments
* The segments to initialize the path with
*/
public Path(int windingRule, Segment... segments) {
this(windingRule);
for (Segment s : segments) {
this.segments.add(s.getCopy());
}
}
/**
* Creates a path from the given segments, using the default winding rule
* {@link #WIND_NON_ZERO}.
*
* @param segments
* The segments to initialize the path with
*/
public Path(Segment... segments) {
this(WIND_NON_ZERO, segments);
}
/**
* Adds the given {@link List} of {@link Segment}s to this {@link Path}.
*
* @param segments
* The {@link Segment}s to add to this {@link Path}.
* @return <code>this</code> for convenience.
*/
public final Path add(List<Segment> segments) {
this.segments.addAll(segments);
return this;
}
/**
* Adds the given {@link Segment}s to this {@link Path}.
*
* @param segments
* The {@link Segment}s to add to this {@link Path}.
* @return <code>this</code> for convenience.
*/
public final Path add(Segment... segments) {
this.segments.addAll(Arrays.asList(segments));
return this;
}
/**
* Closes the current sub-path by drawing a straight line (line-to) to the
* location of the last move to.
*
* @return <code>this</code> for convenience
*/
public final Path close() {
segments.add(new Segment(Segment.CLOSE));
return this;
}
@Override
public boolean contains(Point p) {
return Geometry2AWT.toAWTPath(this)
.contains(Geometry2AWT.toAWTPoint(p));
}
/**
* Returns <code>true</code> if the given {@link Rectangle} is contained
* within {@link IGeometry}, <code>false</code> otherwise.
*
* TODO: Generalize to arbitrary {@link IGeometry} objects.
*
* @param r
* The {@link Rectangle} to test
* @return <code>true</code> if the {@link Rectangle} is fully contained
* within this {@link IGeometry}
*/
public boolean contains(Rectangle r) {
return Geometry2AWT.toAWTPath(this)
.contains(Geometry2AWT.toAWTRectangle(r));
}
/**
* Adds a cubic Bezier curve segment from the current position to the
* specified end position, using the two provided control points as Bezier
* control points.
*
* @param control1X
* The x-coordinate of the first Bezier control point
* @param control1Y
* The y-coordinate of the first Bezier control point
* @param control2X
* The x-coordinate of the second Bezier control point
* @param control2Y
* The y-coordinate of the second Bezier control point
* @param x
* The x-coordinate of the desired target point
* @param y
* The y-coordinate of the desired target point
* @return <code>this</code> for convenience
*/
public final Path cubicTo(double control1X, double control1Y,
double control2X, double control2Y, double x, double y) {
segments.add(
new Segment(Segment.CUBIC_TO, new Point(control1X, control1Y),
new Point(control2X, control2Y), new Point(x, y)));
return this;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Path) {
// test if they are composed by the same segments
// TODO: Even if the segments are not identical, the two Path
// objects can be equal to each other.
Segment[] thisSegments = getSegments();
Segment[] objSegments = ((Path) obj).getSegments();
return Arrays.equals(thisSegments, objSegments);
}
return false;
}
@Override
public Rectangle getBounds() {
List<ICurve> outlines = getOutlines();
if (outlines.size() > 0) {
Rectangle outlineBounds = outlines.get(0).getBounds();
for (int i = 1; i < outlines.size(); i++) {
outlineBounds.union(outlines.get(i).getBounds());
}
return outlineBounds;
}
return new Rectangle();
}
@Override
public Path getCopy() {
return new Path(getWindingRule(), getSegments());
}
/**
* Returns a {@link List} of {@link ICurve}s, representing the outline of
* <code>this</code> {@link Path}. For every {@link Segment#LINE_TO},
* {@link Segment#QUAD_TO}, {@link Segment#CUBIC_TO}, and
* {@link Segment#CLOSE}, one {@link BezierCurve} is created that resembles
* that segment.
*
* @return A {@link List} of {@link ICurve}s representing the outline of the
* given {@link Path}.
*/
public List<ICurve> getOutlines() {
List<ICurve> curves = new ArrayList<>();
Segment[] segments = getSegments();
// save the segment start point as it is not contained within individual
// path segments
Point segmentStart = null;
// save the last move_to position which is later needed for a close
// segment
Point moveTo = null;
for (Segment s : segments) {
if (s.getType() == Segment.MOVE_TO) {
// save MOVE_TO position
moveTo = s.getPoints()[0];
// set segment start position to the move_to position
segmentStart = moveTo;
} else {
// for all other segments a curve is created
if (segmentStart == null) {
throw new IllegalStateException(
"This Path does not start with a MOVE_TO, therefore, no start position could be determined.");
} else {
if (s.getType() == Segment.LINE_TO) {
curves.add(new Line(segmentStart, s.getPoints()[0]));
segmentStart = s.getPoints()[0];
} else if (s.getType() == Segment.QUAD_TO) {
curves.add(new QuadraticCurve(segmentStart,
s.getPoints()[0], s.getPoints()[1]));
segmentStart = s.getPoints()[1];
} else if (s.getType() == Segment.CUBIC_TO) {
curves.add(
new org.eclipse.gef.geometry.planar.CubicCurve(
segmentStart, s.getPoints()[0],
s.getPoints()[1], s.getPoints()[2]));
segmentStart = s.getPoints()[2];
} else if (s.getType() == Segment.CLOSE) {
curves.add(new Line(segmentStart, moveTo));
segmentStart = moveTo;
} else {
throw new IllegalStateException(
"This Path contains an unsupported Segment: <"
+ s + ">.");
}
}
}
}
return curves;
}
/**
* Returns the segments that make up this path.
*
* @return an array of {@link Segment}s representing the segments of this
* path
*/
public Segment[] getSegments() {
Segment[] segments = new Segment[this.segments.size()];
for (int i = 0; i < segments.length; i++) {
segments[i] = this.segments.get(i).getCopy();
}
return segments;
}
@Override
public Path getTransformed(AffineTransform t) {
return AWT2Geometry
.toPath(new Path2D.Double(Geometry2AWT.toAWTPath(this),
Geometry2AWT.toAWTAffineTransform(t)));
}
/**
* Returns the winding rule used to determine the interior of this path.
*
* @return the winding rule, i.e. one of {@link #WIND_EVEN_ODD} or
* {@link #WIND_NON_ZERO}
*/
public int getWindingRule() {
return windingRule;
}
/**
* Adds a straight line segment from the current position to the specified
* end position.
*
* @param x
* The x-coordinate of the desired target point
* @param y
* The y-coordinate of the desired target point
* @return <code>this</code> for convenience
*/
public final Path lineTo(double x, double y) {
segments.add(new Segment(Segment.LINE_TO, new Point(x, y)));
return this;
}
/**
* Changes the current position. A new {@link Segment} of type
* {@link Segment#MOVE_TO} is added to this Path.
*
* @param x
* The x-coordinate of the desired target point
* @param y
* The y-coordinate of the desired target point
* @return <code>this</code> for convenience
*/
public final Path moveTo(double x, double y) {
segments.add(new Segment(Segment.MOVE_TO, new Point(x, y)));
return this;
}
/**
* Adds a quadratic curve segment from the current position to the specified
* end position, using the provided control point as a parametric control
* point.
*
* @param controlX
* The x-coordinate of the control point
* @param controlY
* The y-coordinate of the control point
* @param x
* The x-coordinate of the desired target point
* @param y
* The y-coordinate of the desired target point
* @return <code>this</code> for convenience
*/
public final Path quadTo(double controlX, double controlY, double x,
double y) {
segments.add(new Segment(Segment.QUAD_TO, new Point(controlX, controlY),
new Point(x, y)));
return this;
}
/**
* Resets the path to be empty.
*
* @return <code>this</code> for convenience
*/
public final Path reset() {
segments.clear();
return this;
}
/**
* Sets the winding rule of this {@link Path} to the passed-in integer
* constant which is either of:
* <ul>
* <li>{@link #WIND_NON_ZERO} (default)</li>
* <li>{@link #WIND_EVEN_ODD}</li>
* </ul>
*
* @param windingRule
* the new winding rule of this {@link Path}
* @return <code>this</code> for convenience
*/
public Path setWindingRule(int windingRule) {
this.windingRule = windingRule;
return this;
}
/**
* @see IGeometry#toPath()
*/
@Override
public Path toPath() {
return getCopy();
}
/**
* Tests whether this {@link Path} and the given {@link Rectangle} touch,
* i.e. they have at least one {@link Point} in common.
*
* @param r
* the {@link Rectangle} to test for at least one {@link Point}
* in common with this {@link Path}
* @return <code>true</code> if this {@link Path} and the {@link Rectangle}
* touch, otherwise <code>false</code>
* @see IGeometry#touches(IGeometry)
*/
public boolean touches(Rectangle r) {
return Geometry2AWT.toAWTPath(this)
.intersects(Geometry2AWT.toAWTRectangle(r));
}
}