/*******************************************************************************
* Copyright (c) 2006-2012
* Software Technology Group, Dresden University of Technology
* DevBoost GmbH, Berlin, Amtsgericht Charlottenburg, HRB 140026
*
* 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:
* Software Technology Group - TU Dresden, Germany;
* DevBoost GmbH - Berlin, Germany
* - initial API and implementation
******************************************************************************/
/*
* @(#)BezierFigure.java 3.2 2008-07-06
*
* Copyright (c) 1996-2008 by the original authors of JHotDraw
* and all its contributors.
* All rights reserved.
*
* The copyright of this software is owned by the authors and
* contributors of the JHotDraw project ("the copyright holders").
* You may not use, copy or modify this software, except in
* accordance with the license agreement you entered into with
* the copyright holders. For details see accompanying license terms.
*/
package org.jhotdraw.draw;
import org.jhotdraw.util.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.undo.*;
import java.io.*;
import static org.jhotdraw.draw.AttributeKeys.*;
import org.jhotdraw.geom.*;
import org.jhotdraw.xml.DOMInput;
import org.jhotdraw.xml.DOMOutput;
/**
* A BezierFigure can be used to draw arbitrary shapes using a <code>BezierPath</code>.
* It can be used to draw an open path or a closed shape.
* <p>
* A BezierFigure can have straight path segments and curved segments.
* A straight path segment can be added by clicking on the drawing area.
* Curved segments can be added by dragging the mouse pointer over the
* drawing area.
* <p>
* To creation of the BezierFigure can be finished by adding a segment
* which closes the path, or by double clicking on the drawing area, or by
* selecting a different tool in the DrawingEditor.
*
*
* @see org.jhotdraw.geom.BezierPath
*
* @version 3.2 2008-07-06 Create BezierOutlineHandle on mouse over.
* <br>3.1 2008-05-23 Added method findSegment with tolerance parameter.
* <br>3.0.1 2007-11-30 Changed method removeNode from protected to public.
* <br>3.0 2007-05-12 Got rid of basic methods.
* <br>2.2.1 2007-04-22 Method contains did not work as expected for filled
* unclosed beziers with thick line widths.
* <br>2.2 2007-04-14 Added BezierContourHandle. We fill now open
* paths as well.
* <br>2.1.1 2006-06-08 Fixed caps drawing.
* <br>2.1 2006-04-21 Improved caps drawing.
* <br>2.0 2006-01-14 Changed to support double precison coordinates.
* <br>1.0 March 14, 2004.
* @author Werner Randelshofer
*/
public class BezierFigure extends AbstractAttributedFigure {
/**
* The BezierPath.
*/
protected BezierPath path;
/**
* The cappedPath BezierPath is derived from variable path.
* We cache it to increase the drawing speed of the figure.
*/
private transient BezierPath cappedPath;
/**
* Creates an empty <code>BezierFigure</code>, for example without any
* <code>BezierPath.Node</code>s.
* The BezierFigure will not draw anything, if at least two nodes
* are added to it. The <code>BezierPath</code> created by this constructor
* is not closed.
*/
public BezierFigure() {
this(false);
}
/**
* Creates an empty BezierFigure, for example without any
* <code>BezierPath.Node</code>s.
* The BezierFigure will not draw anything, unless at least two nodes
* are added to it.
*
* @param isClosed Specifies whether the <code>BezierPath</code> shall
* be closed.
*/
public BezierFigure(boolean isClosed) {
path = new BezierPath();
CLOSED.basicSet(this, isClosed);
//path.setClosed(isClosed);
}
// DRAWING
// SHAPE AND BOUNDS
// ATTRIBUTES
// EDITING
// CONNECTING
/**
* Returns the Figures connector for the specified location.
* By default a ChopDiamondConnector is returned.
* @see ChopDiamondConnector
*/
public Connector findConnector(Point2D.Double p, ConnectionFigure prototype) {
return new ChopBezierConnector(this);
}
public Connector findCompatibleConnector(Connector c, boolean isStart) {
return new ChopBezierConnector(this);
}
// COMPOSITE FIGURES
// CLONING
// EVENT HANDLING
protected void drawStroke(Graphics2D g) {
if (isClosed()) {
double grow = AttributeKeys.getPerpendicularDrawGrowth(this);
if (grow == 0d) {
g.draw(path);
} else {
GrowStroke gs = new GrowStroke((float) grow,
(float) (AttributeKeys.getStrokeTotalWidth(this) *
STROKE_MITER_LIMIT.get(this))
);
g.draw(gs.createStrokedShape(path));
}
} else {
g.draw(getCappedPath());
}
drawCaps(g);
}
protected void drawCaps(Graphics2D g) {
if (getNodeCount() > 1) {
if (START_DECORATION.get(this) != null) {
BezierPath cp = getCappedPath();
Point2D.Double p1 = path.get(0,0);
Point2D.Double p2 = cp.get(0,0);
if (p2.equals(p1)) {
p2 = path.get(1,0);
}
START_DECORATION.get(this).draw(g, this, p1, p2);
}
if (END_DECORATION.get(this) != null) {
BezierPath cp = getCappedPath();
Point2D.Double p1 = path.get(path.size()-1,0);
Point2D.Double p2 = cp.get(path.size()-1,0);
if (p2.equals(p1)) {
p2 = path.get(path.size()-2,0);
}
END_DECORATION.get(this).draw(g, this, p1, p2);
}
}
}
protected void drawFill(Graphics2D g) {
if (isClosed() || FILL_OPEN_PATH.get(this)) {
double grow = AttributeKeys.getPerpendicularFillGrowth(this);
if (grow == 0d) {
g.fill(path);
} else {
GrowStroke gs = new GrowStroke((float) grow,
(float) (AttributeKeys.getStrokeTotalWidth(this) *
STROKE_MITER_LIMIT.get(this))
);
g.fill(gs.createStrokedShape(path));
}
}
}
public boolean contains(Point2D.Double p) {
double tolerance = Math.max(2f, AttributeKeys.getStrokeTotalWidth(this) / 2d);
if (isClosed() || FILL_COLOR.get(this) != null && FILL_OPEN_PATH.get(this)) {
if (path.contains(p)) {
return true;
}
double grow = AttributeKeys.getPerpendicularHitGrowth(this) * 2d;
GrowStroke gs = new GrowStroke((float) grow,
(float) (AttributeKeys.getStrokeTotalWidth(this) *
STROKE_MITER_LIMIT.get(this))
);
if (gs.createStrokedShape(path).contains(p)) {
return true;
} else {
if (isClosed()) {
return false;
}
}
}
if (! isClosed()) {
if (getCappedPath().outlineContains(p, tolerance)) {
return true;
}
if (START_DECORATION.get(this) != null) {
BezierPath cp = getCappedPath();
Point2D.Double p1 = path.get(0,0);
Point2D.Double p2 = cp.get(0,0);
// FIXME - Check here, if caps path contains the point
if (Geom.lineContainsPoint(p1.x,p1.y,p2.x,p2.y, p.x, p.y, tolerance)) {
return true;
}
}
if (END_DECORATION.get(this) != null) {
BezierPath cp = getCappedPath();
Point2D.Double p1 = path.get(path.size()-1,0);
Point2D.Double p2 = cp.get(path.size()-1,0);
// FIXME - Check here, if caps path contains the point
if (Geom.lineContainsPoint(p1.x,p1.y,p2.x,p2.y, p.x, p.y, tolerance)) {
return true;
}
}
}
return false;
}
/**
* Checks if this figure can be connected. By default
* filled BezierFigures can be connected.
*/
@Override
public boolean canConnect() {
return isClosed();
}
@Override
public Collection<Handle> createHandles(int detailLevel) {
LinkedList<Handle> handles = new LinkedList<Handle>();
switch (detailLevel % 2) {
case -1 : // Mouse hover handles
handles.add(new BezierOutlineHandle(this, true));
break;
case 0 :
handles.add(new BezierOutlineHandle(this));
for (int i=0, n = path.size(); i < n; i++) {
handles.add(new BezierNodeHandle(this, i));
}
break;
case 1 :
TransformHandleKit.addTransformHandles(this, handles);
handles.add(new BezierScaleHandle(this));
break;
}
return handles;
}
public Rectangle2D.Double getBounds() {
Rectangle2D.Double bounds =path.getBounds2D();
return bounds;
}
@Override
public Rectangle2D.Double getDrawingArea() {
Rectangle2D.Double r = super.getDrawingArea();
if (getNodeCount() > 1) {
if (START_DECORATION.get(this) != null) {
Point2D.Double p1 = getPoint(0, 0);
Point2D.Double p2 = getPoint(1, 0);
r.add(START_DECORATION.get(this).getDrawingArea(this, p1, p2));
}
if (END_DECORATION.get(this) != null) {
Point2D.Double p1 = getPoint(getNodeCount() - 1, 0);
Point2D.Double p2 = getPoint(getNodeCount() - 2, 0);
r.add(END_DECORATION.get(this).getDrawingArea(this, p1, p2));
}
}
return r;
}
@Override
protected void validate() {
super.validate();
path.invalidatePath();
cappedPath = null;
}
/**
* Returns a clone of the bezier path of this figure.
*/
public BezierPath getBezierPath() {
return (BezierPath) path.clone();
}
public void setBezierPath(BezierPath newValue) {
path = (BezierPath) newValue.clone();
this.setClosed(newValue.isClosed());
}
public Point2D.Double getPointOnPath(float relative, double flatness) {
return path.getPointOnPath(relative, flatness);
}
public boolean isClosed() {
return (Boolean) getAttribute(CLOSED);
}
public void setClosed(boolean newValue) {
CLOSED.set(this, newValue);
}
@Override
public <T> void setAttribute(AttributeKey<T> key, T newValue) {
if (key == CLOSED) {
path.setClosed((Boolean) newValue);
} else if (key == WINDING_RULE) {
path.setWindingRule(newValue == AttributeKeys.WindingRule.EVEN_ODD ? GeneralPath.WIND_EVEN_ODD : GeneralPath.WIND_NON_ZERO);
}
super.setAttribute(key, newValue);
invalidate();
}
/**
* Sets the location of the first and the last <code>BezierPath.Node</code>
* of the BezierFigure.
* If the BezierFigure has not at least two nodes, nodes are added
* to the figure until the BezierFigure has at least two nodes.
*/
@Override
public void setBounds(Point2D.Double anchor, Point2D.Double lead) {
setStartPoint(anchor);
setEndPoint(lead);
invalidate();
}
public void transform(AffineTransform tx) {
path.transform(tx);
invalidate();
}
@Override
public void invalidate() {
super.invalidate();
path.invalidatePath();
cappedPath = null;
}
/**
* Returns a path which is cappedPath at the ends, to prevent
* it from drawing under the end caps.
*/
protected BezierPath getCappedPath() {
if (cappedPath == null) {
cappedPath = (BezierPath) path.clone();
if (isClosed()) {
cappedPath.setClosed(true);
} else {
if (cappedPath.size() > 1) {
if (START_DECORATION.get(this) != null) {
BezierPath.Node p0 = cappedPath.get(0);
BezierPath.Node p1 = cappedPath.get(1);
Point2D.Double pp;
if ((p0.getMask() & BezierPath.C2_MASK) != 0) {
pp = p0.getControlPoint(2);
} else if ((p1.getMask() & BezierPath.C1_MASK) != 0) {
pp = p1.getControlPoint(1);
} else {
pp = p1.getControlPoint(0);
}
double radius = START_DECORATION.get(this).getDecorationRadius(this);
double lineLength = Geom.length(p0.getControlPoint(0), pp);
cappedPath.set(0,0, Geom.cap(pp, p0.getControlPoint(0), - Math.min(radius, lineLength)));
}
if (END_DECORATION.get(this) != null) {
BezierPath.Node p0 = cappedPath.get(cappedPath.size() - 1);
BezierPath.Node p1 = cappedPath.get(cappedPath.size() - 2);
Point2D.Double pp;
if ((p0.getMask() & BezierPath.C1_MASK) != 0) {
pp = p0.getControlPoint(1);
} else if ((p1.getMask() & BezierPath.C2_MASK) != 0) {
pp = p1.getControlPoint(2);
} else {
pp = p1.getControlPoint(0);
}
double radius = END_DECORATION.get(this).getDecorationRadius(this);
double lineLength = Geom.length(p0.getControlPoint(0), pp);
cappedPath.set(cappedPath.size() - 1, 0, Geom.cap(pp, p0.getControlPoint(0), -Math.min(radius, lineLength)));
}
cappedPath.invalidatePath();
}
}
}
return cappedPath;
}
public void layout() {
}
/**
* Adds a control point.
*/
public void addNode(BezierPath.Node p) {
addNode(getNodeCount(), p);
}
/**
* Adds a node to the list of points.
*/
public void addNode(final int index, BezierPath.Node p) {
final BezierPath.Node newPoint = new BezierPath.Node(p);
path.add(index, p);
invalidate();
}
/**
* Sets a control point.
*/
public void setNode(int index, BezierPath.Node p) {
path.set(index, p);
invalidate();
}
/**
* Gets a control point.
*/
public BezierPath.Node getNode(int index) {
return (BezierPath.Node) path.get(index).clone();
}
/**
* Convenience method for getting the point coordinate of
* the first control point of the specified node.
*/
public Point2D.Double getPoint(int index) {
return path.get(index).getControlPoint(0);
}
/**
* Gets the point coordinate of a control point.
*/
public Point2D.Double getPoint(int index, int coord) {
return path.get(index).getControlPoint(coord);
}
/**
* Sets the point coordinate of control point 0 at the specified node.
*/
public void setPoint(int index, Point2D.Double p) {
BezierPath.Node node = path.get(index);
double dx = p.x - node.x[0];
double dy = p.y - node.y[0];
for (int i=0; i < node.x.length; i++) {
node.x[i] += dx;
node.y[i] += dy;
}
invalidate();
}
/**
* Sets the point coordinate of a control point.
*/
public void setPoint(int index, int coord, Point2D.Double p) {
BezierPath.Node cp = new BezierPath.Node(path.get(index));
cp.setControlPoint(coord, p);
setNode(index, cp);
}
/**
* Convenience method for setting the point coordinate of the start point.
* If the BezierFigure has not at least two nodes, nodes are added
* to the figure until the BezierFigure has at least two nodes.
*/
public void setStartPoint(Point2D.Double p) {
// Add two nodes if we haven't at least two nodes
for (int i=getNodeCount(); i < 2; i++) {
addNode(0, new BezierPath.Node(p.x, p.y));
}
setPoint(0, p);
}
/**
* Convenience method for setting the point coordinate of the end point.
* If the BezierFigure has not at least two nodes, nodes are added
* to the figure until the BezierFigure has at least two nodes.
*/
public void setEndPoint(Point2D.Double p) {
// Add two nodes if we haven't at least two nodes
for (int i=getNodeCount(); i < 2; i++) {
addNode(0, new BezierPath.Node(p.x, p.y));
}
setPoint(getNodeCount() - 1, p);
}
/**
* Convenience method for getting the start point.
*/
@Override
public Point2D.Double getStartPoint() {
return getPoint(0, 0);
}
/**
* Convenience method for getting the end point.
*/
@Override
public Point2D.Double getEndPoint() {
return getPoint(getNodeCount() - 1, 0);
}
/**
* Finds a control point index.
* Returns -1 if no control point could be found.
* FIXME - Move this to BezierPath
*/
public int findNode(Point2D.Double p) {
BezierPath tp = path;
for (int i=0; i < tp.size(); i++) {
BezierPath.Node p2 = tp.get(i);
if (p2.x[0] == p.x && p2.y[0] == p.y) {
return i;
}
}
return -1;
}
/**
* Gets the segment of the polyline that is hit by
* the given Point2D.Double.
*
* @param find a Point on the bezier path
* @param tolerance a tolerance, tolerance should take into account
* the line width, plus 2 divided by the zoom factor.
* @return the index of the segment or -1 if no segment was hit.
*/
public int findSegment(Point2D.Double find, double tolerance) {
return getBezierPath().findSegment(find, tolerance);
}
/**
* Joins two segments into one if the given Point2D.Double hits a node
* of the polyline.
* @return true if the two segments were joined.
*
* @param join a Point at a node on the bezier path
* @param tolerance a tolerance, tolerance should take into account
* the line width, plus 2 divided by the zoom factor.
*/
public boolean joinSegments(Point2D.Double join, double tolerance) {
int i = findSegment(join, tolerance);
if (i != -1 && i > 1) {
removeNode(i);
return true;
}
return false;
}
/**
* Splits the segment at the given Point2D.Double if a segment was hit.
* @return the index of the segment or -1 if no segment was hit.
*
* @param split a Point on (or near) a line segment on the bezier path
* @param tolerance a tolerance, tolerance should take into account
* the line width, plus 2 divided by the zoom factor.
*/
public int splitSegment(Point2D.Double split, double tolerance) {
int i = findSegment(split, tolerance);
if (i != -1) {
addNode(i + 1, new BezierPath.Node(split));
}
return i+1;
}
/**
* Removes the Node at the specified index.
*/
public BezierPath.Node removeNode(int index) {
return path.remove(index);
}
/**
* Removes the Point2D.Double at the specified index.
*/
protected void removeAllNodes() {
path.clear();
}
/**
* Gets the node count.
*/
public int getNodeCount() {
return path.size();
}
@Override
public BezierFigure clone() {
BezierFigure that = (BezierFigure) super.clone();
that.path = (BezierPath) this.path.clone();
that.invalidate();
return that;
}
public void restoreTransformTo(Object geometry) {
path.setTo((BezierPath) geometry);
}
public Object getTransformRestoreData() {
return path.clone();
}
public Point2D.Double chop(Point2D.Double p) {
if (isClosed()) {
double grow = AttributeKeys.getPerpendicularHitGrowth(this);
if (grow == 0d) {
return path.chop(p);
} else {
GrowStroke gs = new GrowStroke((float) grow,
(float) (AttributeKeys.getStrokeTotalWidth(this) *
STROKE_MITER_LIMIT.get(this))
);
return Geom.chop(gs.createStrokedShape(path), p);
}
} else {
return path.chop(p);
}
}
public Point2D.Double getCenter() {
return path.getCenter();
}
public Point2D.Double getOutermostPoint() {
return path.get(path.indexOfOutermostNode()).getControlPoint(0);
}
/**
* Joins two segments into one if the given Point2D.Double hits a node
* of the polyline.
* @return true if the two segments were joined.
*/
public int joinSegments(Point2D.Double join, float tolerance) {
return path.joinSegments(join, tolerance);
}
/**
* Splits the segment at the given Point2D.Double if a segment was hit.
* @return the index of the segment or -1 if no segment was hit.
*/
public int splitSegment(Point2D.Double split, float tolerance) {
return path.splitSegment(split, tolerance);
}
/**
* Handles a mouse click.
*/
@Override public boolean handleMouseClick(Point2D.Double p, MouseEvent evt, DrawingView view) {
if (evt.getClickCount() == 2 && view.getHandleDetailLevel() % 2 == 0) {
willChange();
final int index = splitSegment(p, (float) (5f / view.getScaleFactor()));
if (index != -1) {
final BezierPath.Node newNode = getNode(index);
fireUndoableEditHappened(new AbstractUndoableEdit() {
@Override
public String getPresentationName() {
ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels");
return labels.getString("edit.bezierPath.splitSegment.text");
}
@Override
public void redo() throws CannotRedoException {
super.redo();
willChange();
addNode(index, newNode);
changed();
}
@Override
public void undo() throws CannotUndoException {
super.undo();
willChange();
removeNode(index);
changed();
}
});
changed();
evt.consume();
return true;
}
}
return false;
}
@Override
public void write(DOMOutput out) throws IOException {
writePoints(out);
writeAttributes(out);
}
protected void writePoints(DOMOutput out) throws IOException {
out.openElement("points");
if (isClosed()) {
out.addAttribute("closed", true);
}
for (int i=0, n = getNodeCount(); i < n; i++) {
BezierPath.Node node = getNode(i);
out.openElement("p");
out.addAttribute("mask", node.mask, 0);
out.addAttribute("colinear", true);
out.addAttribute("x", node.x[0]);
out.addAttribute("y", node.y[0]);
out.addAttribute("c1x", node.x[1], node.x[0]);
out.addAttribute("c1y", node.y[1], node.y[0]);
out.addAttribute("c2x", node.x[2], node.x[0]);
out.addAttribute("c2y", node.y[2], node.y[0]);
out.closeElement();
}
out.closeElement();
}
@Override public void read(DOMInput in) throws IOException {
readPoints(in);
readAttributes(in);
}
protected void readPoints(DOMInput in) throws IOException {
path.clear();
in.openElement("points");
setClosed(in.getAttribute("closed", false));
for (int i=0, n = in.getElementCount("p"); i < n; i++) {
in.openElement("p", i);
BezierPath.Node node = new BezierPath.Node(
in.getAttribute("mask", 0),
in.getAttribute("x", 0d),
in.getAttribute("y", 0d),
in.getAttribute("c1x", in.getAttribute("x", 0d)),
in.getAttribute("c1y", in.getAttribute("y", 0d)),
in.getAttribute("c2x", in.getAttribute("x", 0d)),
in.getAttribute("c2y", in.getAttribute("y", 0d))
);
node.keepColinear = in.getAttribute("colinear", true);
path.add(node);
path.invalidatePath();
in.closeElement();
}
in.closeElement();
}
}