/*******************************************************************************
* Copyright (c) 2012, 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:
* Matthias Wienand (itemis AG) - initial API and implementation
*
*******************************************************************************/
package org.eclipse.gef.geometry.planar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.eclipse.gef.geometry.euclidean.Angle;
import org.eclipse.gef.geometry.internal.utils.PointListUtils;
/**
* A {@link PolyBezier} is an {@link ICurve} which consists of one or more
* connected {@link BezierCurve}s.
*
* @author mwienand
*
*/
public class PolyBezier extends AbstractGeometry
implements ICurve, ITranslatable<PolyBezier>, IScalable<PolyBezier>,
IRotatable<PolyBezier> {
/**
* The default curve width coefficient used in interpolateCubic(...)
* methods.
*/
public static final double INTERPOLATION_CURVE_WIDTH_COEFFICIENT = 1d;
private static final long serialVersionUID = 1L;
private static BezierCurve[] copy(BezierCurve... beziers) {
BezierCurve[] copy = new BezierCurve[beziers.length];
for (int i = 0; i < beziers.length; i++) {
copy[i] = beziers[i].getCopy();
}
return copy;
}
/**
* <p>
* Creates a {@link PolyBezier} consisting of continuous {@link CubicCurve}
* segments through the given {@link Point}s.
* </p>
* <p>
* The start angle and start length constrain the curve. Way points are
* added to assure the curve starts off with a straight line of said length
* in the direction specified by said angle.
* </p>
* <p>
* The end angle and end length constrain the curve. Way points are added to
* assure the curve ends in a straight line of said length in the direction
* specified by said angle.
* </p>
*
* @param startAngle
* start direction of the curve
* @param startLength
* length of the straight start segment of the curve
* @param endAngle
* end direction of the curve
* @param endLength
* length of the straight end segment of the curve
* @param curveWidthCoefficient
* value in the range <code>]0;+Inf[</code> that adjusts the
* width of the curve. A value smaller than 1 sharpens the curve
* and a value greater than 1 thickens the curve.
* @param wayPoints
* The array of {@link Point}s which lie on the resulting
* {@link PolyBezier}.
* @return {@link PolyBezier} consisting of continuous {@link CubicCurve}
* segments through the given {@link Point}s
*/
public static PolyBezier interpolateCubic(Angle startAngle,
double startLength, Angle endAngle, double endLength,
double curveWidthCoefficient, Point... wayPoints) {
// TODO: alter way points => leave space for start/end segments
PolyBezier interpolation = interpolateCubic(curveWidthCoefficient,
wayPoints);
// TODO: compute cubic curves for start/end segments
return interpolation;
}
/**
* Creates a {@link PolyBezier} with continuous {@link CubicCurve} segments
* through the given {@link Point}s.
*
* @see #interpolateCubic(Point...)
* @param coordinates
* the coordinates of the points that are to be interpolated.
* @return {@link PolyBezier} with continuous {@link CubicCurve} segments
* through the points, specified via the given coordinates.
*/
public static PolyBezier interpolateCubic(double... coordinates) {
Point[] points = new Point[coordinates.length / 2];
for (int i = 0; i < coordinates.length / 2; i++) {
points[i] = new Point(coordinates[i * 2], coordinates[i * 2 + 1]);
}
return interpolateCubic(points);
}
/**
* Creates a {@link PolyBezier} with continuous {@link CubicCurve} segments
* through the given {@link Point}s.
*
* @param curveWidthCoefficient
* value in the range <code>]0;+Inf[</code> that adjusts the
* width of the curve. A value smaller than one sharpens the
* curve and a value greater than one thickens the curve.
* @param points
* The array of {@link Point}s which lie on the resulting
* {@link PolyBezier}.
* @return {@link PolyBezier} with continuous {@link CubicCurve} segments
* through the given {@link Point}s.
*/
public static PolyBezier interpolateCubic(double curveWidthCoefficient,
Point... points) {
if (points == null || points.length < 2) {
throw new IllegalArgumentException(
"Not enough points specified (at least 2 required).");
} else if (points.length == 2) {
return new PolyBezier(new Line(points[0], points[1]));
}
/*
* Computes the control points for the cubic bezier curve. The algorithm
* is based on what has been published by Maxim Shemanarev for Polygons
* (http://www.antigrain.com/research/bezier_interpolation/index.html)
* with a modified calculation of the first and the last control points,
* so that it can be applied to Polylines.
*/
Point[] mids = new Point[points.length - 1];
for (int i = 0; i < mids.length; i++) {
mids[i] = points[i].getTranslated(points[i + 1]).getScaled(0.5);
}
Line[] lines = PointListUtils.toSegmentsArray(points, true);
Line[] handleLines = PointListUtils.toSegmentsArray(mids, true);
Point[] handleAnchors = new Point[handleLines.length];
for (int i = 0; i < handleLines.length; i++) {
double d0 = lines[i].getP1().getDistance(lines[i].getP2());
double d1 = lines[i + 1].getP1().getDistance(lines[i + 1].getP2());
if (d0 + d1 == 0) {
d0 = 1;
}
handleAnchors[i] = handleLines[i].get(d0 / (d0 + d1));
}
for (int i = 0; i < handleLines.length; i++) {
handleLines[i].scale(curveWidthCoefficient, handleAnchors[i]);
handleLines[i].translate(points[i + 1].x - handleAnchors[i].x,
points[i + 1].y - handleAnchors[i].y);
}
CubicCurve[] interpolation = new CubicCurve[handleLines.length];
interpolation[0] = new CubicCurve(points[0], handleLines[0].getP1(),
points[1], points[1]);
interpolation[interpolation.length - 1] = new CubicCurve(
points[points.length - 2],
handleLines[handleLines.length - 2].getP2(),
points[points.length - 1], points[points.length - 1]);
for (int i = 1; i < interpolation.length - 1; i++) {
interpolation[i] = new CubicCurve(points[i],
handleLines[i - 1].getP2(), handleLines[i].getP1(),
points[i + 1]);
}
return new PolyBezier(interpolation);
}
/**
* @see #interpolateCubic(double, Point...)
* @param points
* The array of {@link Point}s which lie on the resulting
* {@link PolyBezier}.
* @return {@link PolyBezier} with continuous {@link CubicCurve} segments
* through the given {@link Point}s.
*/
public static PolyBezier interpolateCubic(Point... points) {
PolyBezier interp = interpolateCubic(
INTERPOLATION_CURVE_WIDTH_COEFFICIENT, points);
return interp;
}
private BezierCurve[] beziers;
/**
* Constructs a new {@link PolyBezier} of the given {@link BezierCurve}s.
* The {@link BezierCurve}s are expected to be connected with each other.
*
* @param beziers
* the {@link BezierCurve}s which will constitute this
* {@link PolyBezier}
*/
public PolyBezier(BezierCurve... beziers) {
this.beziers = copy(beziers);
}
@Override
public boolean contains(Point p) {
for (BezierCurve c : beziers) {
if (c.contains(p)) {
return true;
}
}
return false;
}
@Override
public Rectangle getBounds() {
if (beziers == null || beziers.length == 0) {
return new Rectangle();
}
Rectangle bounds = beziers[0].getBounds();
for (BezierCurve c : beziers) {
bounds.union(c.getBounds());
}
return bounds;
}
@Override
public PolyBezier getCopy() {
return new PolyBezier(beziers);
}
@Override
public Point[] getIntersections(ICurve c) {
return CurveUtils.getIntersections(this, c);
}
@Override
public ICurve[] getOverlaps(ICurve c) {
return CurveUtils.getOverlaps(this, c);
}
@Override
public Point getP1() {
return beziers[0].getP1();
}
@Override
public Point getP2() {
return beziers[beziers.length - 1].getP2();
}
@Override
public Point getProjection(Point reference) {
double minDistance = 0;
Point minProjection = null;
for (BezierCurve bc : beziers) {
Point projection = bc.getProjection(reference);
double distance = projection.getDistance(reference);
if (minProjection == null || distance < minDistance) {
minProjection = projection;
minDistance = distance;
}
}
return minProjection;
}
@Override
public PolyBezier getRotatedCCW(Angle angle) {
return getCopy().rotateCCW(angle);
}
@Override
public PolyBezier getRotatedCCW(Angle angle, double cx, double cy) {
return getCopy().getRotatedCCW(angle, cx, cy);
}
@Override
public PolyBezier getRotatedCCW(Angle angle, Point center) {
return getCopy().getRotatedCCW(angle, center);
}
@Override
public PolyBezier getRotatedCW(Angle angle) {
return getCopy().getRotatedCW(angle);
}
@Override
public PolyBezier getRotatedCW(Angle angle, double cx, double cy) {
return getCopy().getRotatedCW(angle, cx, cy);
}
@Override
public PolyBezier getRotatedCW(Angle angle, Point center) {
return getCopy().getRotatedCW(angle, center);
}
@Override
public PolyBezier getScaled(double factor) {
return getCopy().scale(factor);
}
@Override
public PolyBezier getScaled(double fx, double fy) {
return getCopy().scale(fx, fy);
}
@Override
public PolyBezier getScaled(double factor, double cx, double cy) {
return getCopy().scale(factor, cx, cy);
}
@Override
public PolyBezier getScaled(double fx, double fy, double cx, double cy) {
return getCopy().scale(fx, fy, cx, cy);
}
@Override
public PolyBezier getScaled(double fx, double fy, Point center) {
return getCopy().scale(fx, fy, center);
}
@Override
public PolyBezier getScaled(double factor, Point center) {
return getCopy().scale(factor, center);
}
@Override
public PolyBezier getTransformed(AffineTransform t) {
List<BezierCurve> transformedCurves = new ArrayList<>();
for (BezierCurve c : beziers) {
transformedCurves.add(c.getTransformed(t));
}
return new PolyBezier(transformedCurves.toArray(new BezierCurve[] {}));
}
@Override
public PolyBezier getTranslated(double dx, double dy) {
return getCopy().translate(dx, dy);
}
@Override
public PolyBezier getTranslated(Point d) {
return getCopy().translate(d.x, d.y);
}
@Override
public double getX1() {
return getP1().x;
}
@Override
public double getX2() {
return getP2().x;
}
@Override
public double getY1() {
return getP1().y;
}
@Override
public double getY2() {
return getP2().y;
}
@Override
public boolean intersects(ICurve c) {
return CurveUtils.intersect(c, this);
}
@Override
public boolean overlaps(ICurve c) {
return CurveUtils.overlap(c, this);
}
/**
* Directly rotates this {@link PolyBezier} counter-clock-wise around its
* center {@link Point} by the given {@link Angle}. Direct adaptation means,
* that <code>this</code> {@link PolyBezier} is modified in-place.
*
* @param angle
* rotation {@link Angle}
* @return <code>this</code> for convenience
*/
public PolyBezier rotateCCW(Angle angle) {
ArrayList<Point> points = new ArrayList<>();
for (BezierCurve c : beziers) {
points.addAll(Arrays.asList(c.getPoints()));
}
Point centroid = Point.getCentroid(points.toArray(new Point[] {}));
return rotateCCW(angle, centroid.x, centroid.y);
}
/**
* Directly rotates this {@link PolyBezier} counter-clock-wise around the
* given point (specified by cx and cy) by the given {@link Angle}. Direct
* adaptation means, that <code>this</code> {@link PolyBezier} is modified
* in-place.
*
* @param angle
* rotation {@link Angle}
* @param cx
* x-coordinate of the {@link Point} to rotate around
* @param cy
* y-coordinate of the {@link Point} to rotate around
* @return <code>this</code> for convenience
*/
public PolyBezier rotateCCW(Angle angle, double cx, double cy) {
for (BezierCurve c : beziers) {
c.rotateCCW(angle, cx, cy);
}
return this;
}
/**
* Directly rotates this {@link PolyBezier} counter-clock-wise around the
* given {@link Point} by the given {@link Angle}. Direct adaptation means,
* that <code>this</code> {@link PolyBezier} is modified in-place.
*
* @param angle
* rotation {@link Angle}
* @param center
* {@link Point} to rotate around
* @return <code>this</code> for convenience
*/
public PolyBezier rotateCCW(Angle angle, Point center) {
return rotateCCW(angle, center.x, center.y);
}
/**
* Directly rotates this {@link PolyBezier} clock-wise around its center
* {@link Point} by the given {@link Angle}. Direct adaptation means, that
* <code>this</code> {@link PolyBezier} is modified in-place.
*
* @param angle
* rotation {@link Angle}
* @return <code>this</code> for convenience
*/
public PolyBezier rotateCW(Angle angle) {
ArrayList<Point> points = new ArrayList<>();
for (BezierCurve c : beziers) {
points.addAll(Arrays.asList(c.getPoints()));
}
Point centroid = Point.getCentroid(points.toArray(new Point[] {}));
return rotateCW(angle, centroid.x, centroid.y);
}
/**
* Directly rotates this {@link PolyBezier} clock-wise around the given
* point (specified by cx and cy) by the given {@link Angle}. Direct
* adaptation means, that <code>this</code> {@link PolyBezier} is modified
* in-place.
*
* @param angle
* rotation {@link Angle}
* @param cx
* x-coordinate of the {@link Point} to rotate around
* @param cy
* y-coordinate of the {@link Point} to rotate around
* @return <code>this</code> for convenience
*/
public PolyBezier rotateCW(Angle angle, double cx, double cy) {
for (BezierCurve c : beziers) {
c.rotateCW(angle, cx, cy);
}
return this;
}
/**
* Directly rotates this {@link PolyBezier} clock-wise around the given
* {@link Point} by the given {@link Angle}. Direct adaptation means, that
* <code>this</code> {@link PolyBezier} is modified in-place.
*
* @param angle
* rotation {@link Angle}
* @param center
* {@link Point} to rotate around
* @return <code>this</code> for convenience
*/
public PolyBezier rotateCW(Angle angle, Point center) {
return rotateCW(angle, center.x, center.y);
}
@Override
public PolyBezier scale(double factor) {
return scale(factor, factor);
}
@Override
public PolyBezier scale(double fx, double fy) {
ArrayList<Point> points = new ArrayList<>();
for (BezierCurve c : beziers) {
points.addAll(Arrays.asList(c.getPoints()));
}
Point centroid = Point.getCentroid(points.toArray(new Point[] {}));
return scale(fx, fy, centroid.x, centroid.y);
}
@Override
public PolyBezier scale(double factor, double cx, double cy) {
return scale(factor, factor, cx, cy);
}
@Override
public PolyBezier scale(double fx, double fy, double cx, double cy) {
for (BezierCurve c : beziers) {
c.scale(fx, fy, cx, cy);
}
return this;
}
@Override
public PolyBezier scale(double fx, double fy, Point center) {
return scale(fx, fx, center.x, center.y);
}
@Override
public PolyBezier scale(double factor, Point center) {
return scale(factor, factor, center.x, center.y);
}
@Override
public BezierCurve[] toBezier() {
return copy(beziers);
}
@Override
public Path toPath() {
return CurveUtils.toPath(beziers);
}
@Override
public String toString() {
StringBuffer str = new StringBuffer();
str.append("PolyBezier(");
for (int i = 0; i < beziers.length; i++) {
str.append(beziers[i].toString());
if (i < beziers.length - 1) {
str.append(", ");
}
}
return str + ")";
}
@Override
public PolyBezier translate(double dx, double dy) {
for (BezierCurve c : beziers) {
c.translate(dx, dy);
}
return this;
}
@Override
public PolyBezier translate(Point d) {
return translate(d.x, d.y);
}
}