blob: a4c8e4c5e82ac660e28de6c84ad13a23d1077d26 [file] [log] [blame]
/*******************************************************************************
* Copyright (C) 2007 The University of Manchester
*
* Modifications to the initial code base are copyright of their
* respective authors, or their employers as appropriate.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
******************************************************************************/
package net.sf.taverna.t2.workbench.models.graph.svg;
import static java.awt.Color.BLACK;
import static java.awt.Color.GREEN;
import static java.lang.Float.parseFloat;
import static net.sf.taverna.t2.workbench.models.graph.svg.SVGUtil.animate;
import static net.sf.taverna.t2.workbench.models.graph.svg.SVGUtil.calculateAngle;
import static net.sf.taverna.t2.workbench.models.graph.svg.SVGUtil.createAnimationElement;
import static net.sf.taverna.t2.workbench.models.graph.svg.SVGUtil.createSVGDocument;
import static net.sf.taverna.t2.workbench.models.graph.svg.SVGUtil.getDot;
import static net.sf.taverna.t2.workbench.models.graph.svg.SVGUtil.getHexValue;
import static net.sf.taverna.t2.workbench.models.graph.svg.SVGUtil.svgNS;
import static org.apache.batik.util.SVGConstants.SVG_ANIMATE_TAG;
import static org.apache.batik.util.SVGConstants.SVG_ELLIPSE_TAG;
import static org.apache.batik.util.SVGConstants.SVG_FILL_ATTRIBUTE;
import static org.apache.batik.util.SVGConstants.SVG_FONT_FAMILY_ATTRIBUTE;
import static org.apache.batik.util.SVGConstants.SVG_FONT_SIZE_ATTRIBUTE;
import static org.apache.batik.util.SVGConstants.SVG_G_TAG;
import static org.apache.batik.util.SVGConstants.SVG_LINE_TAG;
import static org.apache.batik.util.SVGConstants.SVG_PATH_TAG;
import static org.apache.batik.util.SVGConstants.SVG_POINTS_ATTRIBUTE;
import static org.apache.batik.util.SVGConstants.SVG_POLYGON_TAG;
import static org.apache.batik.util.SVGConstants.SVG_RECT_TAG;
import static org.apache.batik.util.SVGConstants.SVG_STYLE_ATTRIBUTE;
import static org.apache.batik.util.SVGConstants.SVG_TEXT_TAG;
import static org.apache.batik.util.SVGConstants.SVG_TRANSFORM_ATTRIBUTE;
import static org.apache.batik.util.SVGConstants.SVG_VIEW_BOX_ATTRIBUTE;
import static org.apache.batik.util.SVGConstants.SVG_X1_ATTRIBUTE;
import static org.apache.batik.util.SVGConstants.SVG_X2_ATTRIBUTE;
import static org.apache.batik.util.SVGConstants.SVG_Y1_ATTRIBUTE;
import static org.apache.batik.util.SVGConstants.SVG_Y2_ATTRIBUTE;
import static org.apache.batik.util.SVGConstants.SVG_Y_ATTRIBUTE;
import java.awt.Color;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import net.sf.taverna.t2.ui.menu.MenuManager;
import net.sf.taverna.t2.workbench.configuration.colour.ColourManager;
import net.sf.taverna.t2.workbench.configuration.workbench.WorkbenchConfiguration;
import net.sf.taverna.t2.workbench.edits.EditManager;
import net.sf.taverna.t2.workbench.models.graph.DotWriter;
import net.sf.taverna.t2.workbench.models.graph.Graph;
import net.sf.taverna.t2.workbench.models.graph.Graph.Alignment;
import net.sf.taverna.t2.workbench.models.graph.GraphController;
import net.sf.taverna.t2.workbench.models.graph.GraphEdge;
import net.sf.taverna.t2.workbench.models.graph.GraphElement;
import net.sf.taverna.t2.workbench.models.graph.GraphNode;
import net.sf.taverna.t2.workbench.models.graph.dot.GraphLayout;
import net.sf.taverna.t2.workbench.models.graph.dot.ParseException;
import org.apache.batik.bridge.UpdateManager;
import org.apache.batik.dom.svg.SVGOMAnimationElement;
import org.apache.batik.dom.svg.SVGOMEllipseElement;
import org.apache.batik.dom.svg.SVGOMGElement;
import org.apache.batik.dom.svg.SVGOMPathElement;
import org.apache.batik.dom.svg.SVGOMPolygonElement;
import org.apache.batik.dom.svg.SVGOMRectElement;
import org.apache.batik.dom.svg.SVGOMTextElement;
import org.apache.batik.swing.JSVGCanvas;
import org.apache.batik.swing.gvt.GVTTreeRendererAdapter;
import org.apache.batik.swing.gvt.GVTTreeRendererEvent;
import org.apache.log4j.Logger;
import org.w3c.dom.Element;
import org.w3c.dom.Text;
import org.w3c.dom.svg.SVGDocument;
import org.w3c.dom.svg.SVGElement;
import org.w3c.dom.svg.SVGPoint;
import org.w3c.dom.svg.SVGSVGElement;
import uk.org.taverna.scufl2.api.core.Workflow;
import uk.org.taverna.scufl2.api.profiles.Profile;
public class SVGGraphController extends GraphController {
private static final Logger logger = Logger.getLogger(SVGGraphController.class);
@SuppressWarnings("unused")
private static final Timer timer = new Timer("SVG Graph controller timer", true);
private static final String dotErrorMessage = "Cannot draw diagram(s)\n" +
"\n" +
"Install dot as described\n" +
"at http://www.taverna.org.uk\n" +
"and specify its location\n" +
"in the workbench preferences";
private Map<String, List<SVGGraphEdge>> datalinkMap = new HashMap<>();
private final JSVGCanvas svgCanvas;
private SVGDocument svgDocument;
private GraphLayout graphLayout = new GraphLayout();
private EdgeLine edgeLine;
private UpdateManager updateManager;
private ExecutorService executor = Executors.newFixedThreadPool(1);
private boolean drawingDiagram = false;
private int animationSpeed;
private Rectangle bounds, oldBounds;
private SVGOMAnimationElement animateBounds;
private boolean dotMissing = false;
private final WorkbenchConfiguration workbenchConfiguration;
public SVGGraphController(Workflow dataflow, Profile profile,
boolean interactive, JSVGCanvas svgCanvas, EditManager editManager,
MenuManager menuManager, ColourManager colourManager,
WorkbenchConfiguration workbenchConfiguration) {
super(dataflow, profile, interactive, svgCanvas, editManager,
menuManager, colourManager);
this.svgCanvas = svgCanvas;
this.workbenchConfiguration = workbenchConfiguration;
installUpdateManager();
layoutSVGDocument(svgCanvas.getBounds());
svgCanvas.setDocument(getSVGDocument());
}
public SVGGraphController(Workflow dataflow, Profile profile,
boolean interactive, JSVGCanvas svgCanvas, Alignment alignment,
PortStyle portStyle, EditManager editManager,
MenuManager menuManager, ColourManager colourManager,
WorkbenchConfiguration workbenchConfiguration) {
super(dataflow, profile, interactive, svgCanvas, alignment, portStyle,
editManager, menuManager, colourManager);
this.svgCanvas = svgCanvas;
this.workbenchConfiguration = workbenchConfiguration;
installUpdateManager();
layoutSVGDocument(svgCanvas.getBounds());
svgCanvas.setDocument(getSVGDocument());
}
private void installUpdateManager() {
svgCanvas.addGVTTreeRendererListener(new GVTTreeRendererAdapter() {
@Override
public void gvtRenderingCompleted(GVTTreeRendererEvent ev) {
setUpdateManager(svgCanvas.getUpdateManager());
}
});
}
@Override
public GraphEdge createGraphEdge() {
return new SVGGraphEdge(this);
}
@Override
public Graph createGraph() {
return new SVGGraph(this);
}
@Override
public GraphNode createGraphNode() {
return new SVGGraphNode(this);
}
public JSVGCanvas getSVGCanvas() {
return svgCanvas;
}
public synchronized SVGDocument getSVGDocument() {
if (svgDocument == null)
svgDocument = createSVGDocument();
return svgDocument;
}
@Override
public void redraw() {
Graph graph = generateGraph();
Rectangle actualBounds = layoutGraph(graph, svgCanvas.getBounds());
setBounds(actualBounds);
transformGraph(getGraph(), graph);
}
private void layoutSVGDocument(Rectangle bounds) {
animateBounds = createAnimationElement(this, SVG_ANIMATE_TAG,
SVG_VIEW_BOX_ATTRIBUTE, null);
updateManager = null;
datalinkMap.clear();
Graph graph = getGraph();
if (graph instanceof SVGGraph) {
SVGGraph svgGraph = (SVGGraph) graph;
SVGSVGElement svgElement = getSVGDocument().getRootElement();
SVGElement graphElement = svgGraph.getSVGElement();
svgElement.appendChild(graphElement);
setBounds(layoutGraph(graph, bounds));
edgeLine = EdgeLine.createAndAdd(getSVGDocument(), this);
}
drawingDiagram = true;
}
public Rectangle layoutGraph(Graph graph, Rectangle bounds) {
Rectangle actualBounds = null;
bounds = new Rectangle(bounds);
StringWriter stringWriter = new StringWriter();
DotWriter dotWriter = new DotWriter(stringWriter);
try {
dotWriter.writeGraph(graph);
String layout = getDot(stringWriter.toString(), workbenchConfiguration);
if (layout.isEmpty())
logger.warn("Invalid dot returned");
else
actualBounds = graphLayout.layoutGraph(this, graph, layout, bounds);
} catch (IOException e) {
outputMessage(dotErrorMessage);
setDotMissing(true);
logger.warn("Couldn't generate dot");
} catch (ParseException e) {
logger.warn("Couldn't layout graph", e);
}
return actualBounds;
}
private void setDotMissing(boolean b) {
this.dotMissing = b;
}
public boolean isDotMissing() {
return dotMissing;
}
public void setBounds(final Rectangle bounds) {
oldBounds = this.bounds;
this.bounds = bounds;
updateSVGDocument(new Runnable() {
@Override
public void run() {
SVGSVGElement svgElement = getSVGDocument().getRootElement();
if (isAnimatable() && oldBounds != null) {
String from = "0 0 " + oldBounds.width + " "
+ oldBounds.height;
String to = "0 0 " + bounds.width + " " + bounds.height;
animate(animateBounds, svgElement, getAnimationSpeed(),
from, to);
} else if ((svgElement != null) && (bounds != null))
svgElement.setAttribute(SVG_VIEW_BOX_ATTRIBUTE,
"0 0 " + String.valueOf(bounds.width) + " "
+ String.valueOf(bounds.height));
}
});
}
private void outputMessage(final String message) {
SVGSVGElement svgElement = getSVGDocument().getRootElement();
String[] parts = message.split("\n");
int initialPosition = 200;
for (int i = 0; i < parts.length; i++) {
Text errorsText = createText(parts[i]);
SVGOMTextElement error = (SVGOMTextElement) createElement(SVG_TEXT_TAG);
error.setAttribute(SVG_Y_ATTRIBUTE,
Integer.toString(initialPosition + i * 60));
error.setAttribute(SVG_FONT_SIZE_ATTRIBUTE, "20");
error.setAttribute(SVG_FONT_FAMILY_ATTRIBUTE, "sans-serif");
error.setAttribute(SVG_FILL_ATTRIBUTE, "red");
error.appendChild(errorsText);
svgElement.appendChild(error);
}
bounds = new Rectangle(300, parts.length * 60 + 200);
svgCanvas.setDocument(getSVGDocument());
}
public void setUpdateManager(UpdateManager updateManager) {
this.updateManager = updateManager;
drawingDiagram = false;
resetSelection();
}
@Override
public boolean startEdgeCreation(GraphElement graphElement, Point point) {
boolean alreadyStarted = edgeCreationFromSource || edgeCreationFromSink;
boolean started = super.startEdgeCreation(graphElement, point);
if (!alreadyStarted && started) {
if (edgeMoveElement instanceof SVGGraphEdge) {
SVGGraphEdge svgGraphEdge = (SVGGraphEdge) edgeMoveElement;
SVGPoint sourcePoint = svgGraphEdge.getPathElement()
.getPointAtLength(0f);
edgeLine.setSourcePoint(new Point((int) sourcePoint.getX(),
(int) sourcePoint.getY()));
} else
edgeLine.setSourcePoint(point);
edgeLine.setTargetPoint(point);
edgeLine.setColour(Color.BLACK);
// edgeLine.setVisible(true);
}
return started;
}
@Override
public boolean moveEdgeCreationTarget(GraphElement graphElement, Point point) {
boolean linkValid = super.moveEdgeCreationTarget(graphElement, point);
if (edgeMoveElement instanceof SVGGraphEdge)
((SVGGraphEdge) edgeMoveElement).setVisible(false);
if (edgeCreationFromSink) {
edgeLine.setSourcePoint(point);
if (linkValid)
edgeLine.setColour(GREEN);
else
edgeLine.setColour(BLACK);
edgeLine.setVisible(true);
} else if (edgeCreationFromSource) {
edgeLine.setTargetPoint(point);
if (linkValid)
edgeLine.setColour(GREEN);
else
edgeLine.setColour(BLACK);
edgeLine.setVisible(true);
}
return linkValid;
}
@Override
public boolean stopEdgeCreation(GraphElement graphElement, Point point) {
GraphEdge movedEdge = edgeMoveElement;
boolean edgeCreated = super.stopEdgeCreation(graphElement, point);
if (!edgeCreated && movedEdge instanceof SVGGraphEdge)
((SVGGraphEdge) movedEdge).setVisible(true);
edgeLine.setVisible(false);
return edgeCreated;
}
@Override
public void setEdgeActive(String edgeId, boolean active) {
if (datalinkMap.containsKey(edgeId))
for (GraphEdge datalink : datalinkMap.get(edgeId))
datalink.setActive(active);
}
public Element createElement(String tag) {
return getSVGDocument().createElementNS(svgNS, tag);
}
SVGOMGElement createGElem() {
return (SVGOMGElement) createElement(SVG_G_TAG);
}
SVGOMPolygonElement createPolygon() {
return (SVGOMPolygonElement) createElement(SVG_POLYGON_TAG);
}
SVGOMEllipseElement createEllipse() {
return (SVGOMEllipseElement) createElement(SVG_ELLIPSE_TAG);
}
SVGOMPathElement createPath() {
return (SVGOMPathElement) createElement(SVG_PATH_TAG);
}
SVGOMRectElement createRect() {
return (SVGOMRectElement) createElement(SVG_RECT_TAG);
}
public Text createText(String text) {
return getSVGDocument().createTextNode(text);
}
SVGOMTextElement createText(Text text) {
SVGOMTextElement elem = (SVGOMTextElement) createElement(SVG_TEXT_TAG);
elem.appendChild(text);
return elem;
}
public void updateSVGDocument(final Runnable thread) {
if (updateManager == null && !drawingDiagram)
thread.run();
else if (!executor.isShutdown())
executor.execute(new Runnable() {
@Override
public void run() {
waitForUpdateManager();
try {
updateManager.getUpdateRunnableQueue().invokeLater(
thread);
} catch (IllegalStateException e) {
logger.error("Update of SVG failed", e);
}
}
private void waitForUpdateManager() {
try {
while (updateManager == null)
Thread.sleep(200);
} catch (InterruptedException e) {
}
}
});
// if (updateManager == null)
// thread.run();
// else
// updateManager.getUpdateRunnableQueue().invokeLater(thread);
}
public boolean isAnimatable() {
return animationSpeed > 0 && updateManager != null && !drawingDiagram;
}
/**
* Returns the animation speed in milliseconds.
*
* @return the animation speed in milliseconds
*/
public int getAnimationSpeed() {
return animationSpeed;
}
/**
* Sets the animation speed in milliseconds. A value of 0 turns off animation.
*
* @param animationSpeed the animation speed in milliseconds
*/
public void setAnimationSpeed(int animationSpeed) {
this.animationSpeed = animationSpeed;
}
@Override
public void shutdown() {
super.shutdown();
executor.execute(new Runnable() {
@Override
public void run() {
getSVGCanvas().stopProcessing();
executor.shutdown();
}
});
}
}
class EdgeLine {
private static final float arrowLength = 10f;
private static final float arrowWidth = 3f;
private Element line;
private Element pointer;
private SVGGraphController graphController;
private EdgeLine(SVGGraphController graphController) {
this.graphController = graphController;
}
public static EdgeLine createAndAdd(SVGDocument svgDocument, SVGGraphController graphController) {
EdgeLine edgeLine = new EdgeLine(graphController);
edgeLine.line = svgDocument.createElementNS(svgNS, SVG_LINE_TAG);
edgeLine.line.setAttribute(SVG_STYLE_ATTRIBUTE,
"fill:none;stroke:black");
edgeLine.line.setAttribute("pointer-events", "none");
edgeLine.line.setAttribute("visibility", "hidden");
edgeLine.line.setAttribute(SVG_X1_ATTRIBUTE, "0");
edgeLine.line.setAttribute(SVG_Y1_ATTRIBUTE, "0");
edgeLine.line.setAttribute(SVG_X2_ATTRIBUTE, "0");
edgeLine.line.setAttribute(SVG_Y2_ATTRIBUTE, "0");
edgeLine.pointer = svgDocument.createElementNS(svgNS, SVG_POLYGON_TAG);
edgeLine.pointer.setAttribute(SVG_STYLE_ATTRIBUTE,
"fill:black;stroke:black");
edgeLine.pointer.setAttribute(SVG_POINTS_ATTRIBUTE, "0,0 "
+ -arrowLength + "," + arrowWidth + " " + -arrowLength + ","
+ -arrowWidth + " 0,0");
edgeLine.pointer.setAttribute("pointer-events", "none");
edgeLine.pointer.setAttribute("visibility", "hidden");
Element svgRoot = svgDocument.getDocumentElement();
svgRoot.insertBefore(edgeLine.line, null);
svgRoot.insertBefore(edgeLine.pointer, null);
return edgeLine;
}
public void setSourcePoint(final Point point) {
graphController.updateSVGDocument(new Runnable() {
@Override
public void run() {
line.setAttribute(SVG_X1_ATTRIBUTE,
String.valueOf(point.getX()));
line.setAttribute(SVG_Y1_ATTRIBUTE,
String.valueOf(point.getY()));
float x = parseFloat(line.getAttribute(SVG_X2_ATTRIBUTE));
float y = parseFloat(line.getAttribute(SVG_Y2_ATTRIBUTE));
double angle = calculateAngle(line);
pointer.setAttribute(SVG_TRANSFORM_ATTRIBUTE, "translate(" + x
+ " " + y + ") rotate(" + angle + " 0 0) ");
}
});
}
public void setTargetPoint(final Point point) {
graphController.updateSVGDocument(new Runnable() {
@Override
public void run() {
line.setAttribute(SVG_X2_ATTRIBUTE,
String.valueOf(point.getX()));
line.setAttribute(SVG_Y2_ATTRIBUTE,
String.valueOf(point.getY()));
double angle = calculateAngle(line);
pointer.setAttribute(SVG_TRANSFORM_ATTRIBUTE, "translate("
+ point.x + " " + point.y + ") rotate(" + angle
+ " 0 0) ");
}
});
}
public void setColour(final Color colour) {
graphController.updateSVGDocument(new Runnable() {
@Override
public void run() {
String hexColour = getHexValue(colour);
line.setAttribute(SVG_STYLE_ATTRIBUTE, "fill:none;stroke:"
+ hexColour + ";");
pointer.setAttribute(SVG_STYLE_ATTRIBUTE, "fill:" + hexColour
+ ";stroke:" + hexColour + ";");
}
});
}
public void setVisible(final boolean visible) {
graphController.updateSVGDocument(new Runnable() {
@Override
public void run() {
if (visible) {
line.setAttribute("visibility", "visible");
pointer.setAttribute("visibility", "visible");
} else {
line.setAttribute("visibility", "hidden");
pointer.setAttribute("visibility", "hidden");
}
}
});
}
}