/*******************************************************************************
* 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
******************************************************************************/
/*
* @(#)SVGPathFigure.java 1.2 2009-04-17
*
* Copyright (c) 1996-2009 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.samples.svg.figures;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.undo.*;
import org.jhotdraw.draw.*;
import org.jhotdraw.geom.*;
import org.jhotdraw.samples.svg.*;
import org.jhotdraw.util.*;
import org.jhotdraw.xml.*;
import static org.jhotdraw.samples.svg.SVGAttributeKeys.*;
/**
* SVGPath is a composite Figure which contains one or more
* SVGBezierFigures as its children.
*
* @author Werner Randelshofer
* @version 2.1 2009-04-17 Method contains() takes now into account
* whether the figure is filled.
* <br>1.1.1 2008-03-20 Attributes must be set on child figures in order
* to ensure that the drawing area of the child figures is computed properly.
* <br>1.1 2007-12-21 Only close/open last path.
* <br>1.0 July 8, 2006 Created.
*/
public class SVGPathFigure extends AbstractAttributedCompositeFigure implements SVGFigure {
/**
* This cachedPath is used for drawing.
*/
private transient GeneralPath cachedPath;
// private transient Rectangle2D.Double cachedDrawingArea;
/**
* This is used to perform faster hit testing.
*/
private transient Shape cachedHitShape;
private final static boolean DEBUG = false;
/** Creates a new instance. */
public SVGPathFigure() {
add(new SVGBezierFigure());
SVGAttributeKeys.setDefaults(this);
}
public SVGPathFigure(boolean isEmpty) {
if (! isEmpty) { add(new SVGBezierFigure()); }
SVGAttributeKeys.setDefaults(this);
}
public void draw(Graphics2D g) {
double opacity = OPACITY.get(this);
opacity = Math.min(Math.max(0d, opacity), 1d);
if (opacity != 0d) {
if (opacity != 1d) {
Rectangle2D.Double drawingArea = getDrawingArea();
Rectangle2D clipBounds = g.getClipBounds();
if (clipBounds != null) {
Rectangle2D.intersect(drawingArea, clipBounds, drawingArea);
}
if (!drawingArea.isEmpty()) {
BufferedImage buf = new BufferedImage(
Math.max(1, (int) ((2 + drawingArea.width) * g.getTransform().getScaleX())),
Math.max(1, (int) ((2 + drawingArea.height) * g.getTransform().getScaleY())),
BufferedImage.TYPE_INT_ARGB);
Graphics2D gr = buf.createGraphics();
gr.scale(g.getTransform().getScaleX(), g.getTransform().getScaleY());
gr.translate((int) -drawingArea.x, (int) -drawingArea.y);
gr.setRenderingHints(g.getRenderingHints());
drawFigure(gr);
gr.dispose();
Composite savedComposite = g.getComposite();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) opacity));
g.drawImage(buf, (int) drawingArea.x, (int) drawingArea.y,
2 + (int) drawingArea.width, 2 + (int) drawingArea.height, null);
g.setComposite(savedComposite);
}
} else {
drawFigure(g);
}
}
}
public void drawFigure(Graphics2D g) {
AffineTransform savedTransform = null;
if (TRANSFORM.get(this) != null) {
savedTransform = g.getTransform();
g.transform(TRANSFORM.get(this));
}
Paint paint = SVGAttributeKeys.getFillPaint(this);
if (paint != null) {
g.setPaint(paint);
drawFill(g);
}
paint = SVGAttributeKeys.getStrokePaint(this);
if (paint != null) {
g.setPaint(paint);
g.setStroke(SVGAttributeKeys.getStroke(this));
drawStroke(g);
}
if (TRANSFORM.get(this) != null) {
g.setTransform(savedTransform);
}
}
protected void drawChildren(Graphics2D g) {
// empty
}
public void drawFill(Graphics2D g) {
g.fill(getPath());
}
public void drawStroke(Graphics2D g) {
g.draw(getPath());
}
@Override protected void invalidate() {
super.invalidate();
cachedPath = null;
cachedDrawingArea = null;
cachedHitShape = null;
}
protected GeneralPath getPath() {
if (cachedPath == null) {
cachedPath = new GeneralPath();
cachedPath.setWindingRule(WINDING_RULE.get(this) == WindingRule.EVEN_ODD ? GeneralPath.WIND_EVEN_ODD : GeneralPath.WIND_NON_ZERO);
for (Figure child : getChildren()) {
SVGBezierFigure b = (SVGBezierFigure) child;
cachedPath.append(b.getBezierPath(), false);
}
}
return cachedPath;
}
protected Shape getHitShape() {
if (cachedHitShape == null) {
cachedHitShape = getPath();
if (FILL_COLOR.get(this) == null && FILL_GRADIENT.get(this) == null) {
cachedHitShape = SVGAttributeKeys.getHitStroke(this).createStrokedShape(cachedHitShape);
}
}
return cachedHitShape;
}
// int count;
public Rectangle2D.Double getDrawingArea() {
if (cachedDrawingArea == null) {
double strokeTotalWidth = AttributeKeys.getStrokeTotalWidth(this);
double width = strokeTotalWidth / 2d;
if (STROKE_JOIN.get(this) == BasicStroke.JOIN_MITER) {
width *= STROKE_MITER_LIMIT.get(this);
} else if (STROKE_CAP.get(this) != BasicStroke.CAP_BUTT) {
width += strokeTotalWidth * 2;
}
GeneralPath gp = (GeneralPath) getPath();
Rectangle2D strokeRect = new Rectangle2D.Double(0, 0, width, width);
if (TRANSFORM.get(this) != null) {
gp = (GeneralPath) gp.clone();
gp.transform(TRANSFORM.get(this));
strokeRect = TRANSFORM.get(this).createTransformedShape(strokeRect).getBounds2D();
}
Rectangle2D rx = gp.getBounds2D();
Rectangle2D.Double r = (rx instanceof Rectangle2D.Double) ? (Rectangle2D.Double) rx : new Rectangle2D.Double(rx.getX(), rx.getY(), rx.getWidth(), rx.getHeight());
Geom.grow(r, strokeRect.getWidth(), strokeRect.getHeight());
cachedDrawingArea = r;
}
return (Rectangle2D.Double) cachedDrawingArea.clone();
}
@Override
final public void write(DOMOutput out) throws IOException {
throw new UnsupportedOperationException("Use SVGStorableOutput to write this Figure.");
}
@Override
final public void read(DOMInput in) throws IOException {
throw new UnsupportedOperationException("Use SVGStorableInput to read this Figure.");
}
public boolean contains(Point2D.Double p) {
getPath();
if (TRANSFORM.get(this) != null) {
try {
p = (Point2D.Double) TRANSFORM.get(this).inverseTransform(p, new Point2D.Double());
} catch (NoninvertibleTransformException ex) {
ex.printStackTrace();
}
}
boolean isClosed = CLOSED.get(getChild(0));
if (isClosed && FILL_COLOR.get(this) == null && FILL_GRADIENT.get(this)==null) {
return getHitShape().contains(p);
}
/*
return cachedPath.contains(p2);
*/
double tolerance = Math.max(2f, AttributeKeys.getStrokeTotalWidth(this) / 2d);
if (isClosed || FILL_COLOR.get(this) != null || FILL_GRADIENT.get(this)!=null) {
if (getPath().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(getPath()).contains(p)) {
return true;
} else {
if (isClosed) {
return false;
}
}
}
if (!isClosed) {
if (Shapes.outlineContains(getPath(), p, tolerance)) {
return true;
}
}
return false;
}
public void setBounds(Point2D.Double anchor, Point2D.Double lead) {
if (getChildCount() == 1 && ((SVGBezierFigure) getChild(0)).getNodeCount() <= 2) {
SVGBezierFigure b = (SVGBezierFigure) getChild(0);
b.setBounds(anchor, lead);
invalidate();
} else {
super.setBounds(anchor, lead);
}
}
public void transform(AffineTransform tx) {
if (TRANSFORM.get(this) != null ||
(tx.getType() & (AffineTransform.TYPE_TRANSLATION)) != tx.getType()) {
if (TRANSFORM.get(this) == null) {
TRANSFORM.basicSetClone(this, tx);
} else {
AffineTransform t = TRANSFORM.getClone(this);
t.preConcatenate(tx);
TRANSFORM.basicSet(this, t);
}
} else {
for (Figure f : getChildren()) {
f.transform(tx);
}
if (FILL_GRADIENT.get(this) != null &&
!FILL_GRADIENT.get(this).isRelativeToFigureBounds()) {
Gradient g = FILL_GRADIENT.getClone(this);
g.transform(tx);
FILL_GRADIENT.basicSet(this, g);
}
if (STROKE_GRADIENT.get(this) != null &&
!STROKE_GRADIENT.get(this).isRelativeToFigureBounds()) {
Gradient g = STROKE_GRADIENT.getClone(this);
g.transform(tx);
STROKE_GRADIENT.basicSet(this, g);
}
}
invalidate();
}
@SuppressWarnings("unchecked")
@Override
public void restoreTransformTo(Object geometry) {
invalidate();
Object[] restoreData = (Object[]) geometry;
ArrayList<Object> paths = (ArrayList<Object>) restoreData[0];
for (int i = 0, n = getChildCount(); i < n; i++) {
getChild(i).restoreTransformTo(paths.get(i));
}
TRANSFORM.basicSetClone(this, (AffineTransform) restoreData[1]);
FILL_GRADIENT.basicSetClone(this, (Gradient) restoreData[2]);
STROKE_GRADIENT.basicSetClone(this, (Gradient) restoreData[3]);
}
@Override
public Object getTransformRestoreData() {
ArrayList<Object> paths = new ArrayList<Object>(getChildCount());
for (int i = 0, n = getChildCount(); i < n; i++) {
paths.add(getChild(i).getTransformRestoreData());
}
return new Object[] {
paths,
TRANSFORM.getClone(this),
FILL_GRADIENT.getClone(this),
STROKE_GRADIENT.getClone(this)
};
}
@Override
public <T> void setAttribute(AttributeKey<T> key, T newValue) {
super.setAttribute(key, newValue);
invalidate();
}
public boolean isEmpty() {
for (Figure child : getChildren()) {
SVGBezierFigure b = (SVGBezierFigure) child;
if (b.getNodeCount() > 0) {
return false;
}
}
return true;
}
@Override
public Collection<Handle> createHandles(int detailLevel) {
LinkedList<Handle> handles = new LinkedList<Handle>();
switch (detailLevel % 2) {
case -1 : // Mouse hover handles
handles.add(new SVGPathOutlineHandle(this, true));
break;
case 0:
handles.add(new SVGPathOutlineHandle(this));
for (Figure child : getChildren()) {
handles.addAll(((SVGBezierFigure) child).createHandles(this, detailLevel));
}
handles.add(new LinkHandle(this));
break;
case 1:
TransformHandleKit.addTransformHandles(this, handles);
break;
default:
break;
}
return handles;
}
@Override
public Collection<Action> getActions(Point2D.Double p) {
final ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.samples.svg.Labels");
LinkedList<Action> actions = new LinkedList<Action>();
if (TRANSFORM.get(this) != null) {
actions.add(new AbstractAction(labels.getString("edit.removeTransform.text")) {
public void actionPerformed(ActionEvent evt) {
ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.samples.svg.Labels");
SVGPathFigure.this.willChange();
fireUndoableEditHappened(
TRANSFORM.setUndoable(SVGPathFigure.this, null));
SVGPathFigure.this.changed();
}
});
actions.add(new AbstractAction(labels.getString("edit.flattenTransform.text")) {
public void actionPerformed(ActionEvent evt) {
// CompositeEdit edit = new CompositeEdit(labels.getString("flattenTransform"));
//TransformEdit edit = new TransformEdit(SVGPathFigure.this, )
final Object restoreData = getTransformRestoreData();
UndoableEdit edit = new AbstractUndoableEdit() {
@Override
public String getPresentationName() {
return labels.getString("edit.flattenTransform.text");
}
@Override
public void undo() throws CannotUndoException {
super.undo();
willChange();
restoreTransformTo(restoreData);
changed();
}
@Override
public void redo() throws CannotRedoException {
super.redo();
willChange();
restoreTransformTo(restoreData);
flattenTransform();
changed();
}
};
willChange();
flattenTransform();
changed();
fireUndoableEditHappened(edit);
}
});
}
if (CLOSED.get(getChild(getChildCount() - 1))) {
actions.add(new AbstractAction(labels.getString("attribute.openPath.text")) {
public void actionPerformed(ActionEvent evt) {
SVGPathFigure.this.willChange();
for (Figure child : getChildren()) {
getDrawing().fireUndoableEditHappened(
CLOSED.setUndoable(child, false));
}
SVGPathFigure.this.changed();
}
});
} else {
actions.add(new AbstractAction(labels.getString("attribute.closePath.text")) {
public void actionPerformed(ActionEvent evt) {
SVGPathFigure.this.willChange();
for (Figure child : getChildren()) {
getDrawing().fireUndoableEditHappened(
CLOSED.setUndoable(child, true));
}
SVGPathFigure.this.changed();
}
});
}
if (WINDING_RULE.get(this) != WindingRule.EVEN_ODD) {
actions.add(new AbstractAction(labels.getString("attribute.windingRule.evenOdd.text")) {
public void actionPerformed(ActionEvent evt) {
SVGPathFigure.this.willChange();
getDrawing().fireUndoableEditHappened(
WINDING_RULE.setUndoable(SVGPathFigure.this, WindingRule.EVEN_ODD));
SVGPathFigure.this.changed();
}
});
} else {
actions.add(new AbstractAction(labels.getString("attribute.windingRule.nonZero.text")) {
public void actionPerformed(ActionEvent evt) {
WINDING_RULE.set(SVGPathFigure.this, WindingRule.NON_ZERO);
getDrawing().fireUndoableEditHappened(
WINDING_RULE.setUndoable(SVGPathFigure.this, WindingRule.NON_ZERO));
}
});
}
return actions;
}
// CONNECTING
public boolean canConnect() {
return false; // SVG does not support connecting
}
public Connector findConnector(Point2D.Double p, ConnectionFigure prototype) {
return null; // SVG does not support connectors
}
public Connector findCompatibleConnector(Connector c, boolean isStartConnector) {
return null; // SVG does not support connectors
}
/**
* Handles a mouse click.
*/
@Override
public boolean handleMouseClick(Point2D.Double p, MouseEvent evt, DrawingView view) {
if (evt.getClickCount() == 2 && view.getHandleDetailLevel() % 2 == 0) {
for (Figure child : getChildren()) {
SVGBezierFigure bf = (SVGBezierFigure) child;
int index = bf.findSegment(p, (float) (5f / view.getScaleFactor()));
if (index != -1) {
bf.handleMouseClick(p, evt, view);
evt.consume();
return true;
}
}
}
return false;
}
@Override
public void add(final int index, final Figure figure) {
super.add(index, (SVGBezierFigure) figure);
}
@Override
public SVGBezierFigure getChild(int index) {
return (SVGBezierFigure) super.getChild(index);
}
public SVGPathFigure clone() {
SVGPathFigure that = (SVGPathFigure) super.clone();
return that;
}
public void flattenTransform() {
willChange();
AffineTransform tx = TRANSFORM.get(this);
if (tx != null) {
for (Figure child : getChildren()) {
//((SVGBezierFigure) child).transform(tx);
((SVGBezierFigure) child).flattenTransform();
}
}
if (FILL_GRADIENT.get(this) != null) {
FILL_GRADIENT.get(this).transform(tx);
}
if (STROKE_GRADIENT.get(this) != null) {
STROKE_GRADIENT.get(this).transform(tx);
}
TRANSFORM.basicSet(this, null);
changed();
}
}