/*
Copyright 2008-2010 Gephi
Authors : Jeremy Subtil <jeremy.subtil@gephi.org>
Website : http://www.gephi.org
This file is part of Gephi.
Gephi is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
Gephi 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Gephi. If not, see <http://www.gnu.org/licenses/>.
*/
package org.gephi.io.exporter.preview;
import java.io.Writer;
import java.util.HashMap;
import java.util.Locale;
import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.DocumentLoader;
import org.apache.batik.bridge.GVTBuilder;
import org.apache.batik.bridge.UserAgent;
import org.apache.batik.bridge.UserAgentAdapter;
import org.apache.batik.dom.svg.SVGDOMImplementation;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.svg2svg.SVGTranscoder;
import org.gephi.io.exporter.preview.util.LengthUnit;
import org.gephi.io.exporter.preview.util.SupportSize;
import org.gephi.io.exporter.spi.CharacterExporter;
import org.gephi.io.exporter.spi.VectorExporter;
import org.gephi.preview.api.BidirectionalEdge;
import org.gephi.preview.api.CubicBezierCurve;
import org.gephi.preview.api.DirectedEdge;
import org.gephi.preview.api.Edge;
import org.gephi.preview.api.EdgeArrow;
import org.gephi.preview.api.EdgeLabel;
import org.gephi.preview.api.EdgeMiniLabel;
import org.gephi.preview.api.Graph;
import org.gephi.preview.api.GraphRenderer;
import org.gephi.preview.api.GraphSheet;
import org.gephi.preview.api.Node;
import org.gephi.preview.api.NodeLabel;
import org.gephi.preview.api.NodeLabelBorder;
import org.gephi.preview.api.Point;
import org.gephi.preview.api.PreviewController;
import org.gephi.preview.api.SelfLoop;
import org.gephi.preview.api.UndirectedEdge;
import org.gephi.preview.api.UnidirectionalEdge;
import org.gephi.utils.longtask.spi.LongTask;
import org.gephi.utils.progress.Progress;
import org.gephi.utils.progress.ProgressTicket;
import org.gephi.project.api.Workspace;
import org.openide.util.Lookup;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.Text;
import org.w3c.dom.svg.SVGLocatable;
import org.w3c.dom.svg.SVGRect;
/**
* Class exporting the preview graph as an SVG image.
*
* @author Jérémy Subtil <jeremy.subtil@gephi.org>
*/
public class SVGExporter implements GraphRenderer, CharacterExporter, VectorExporter, LongTask {
//Architecture
private Document doc;
private ProgressTicket progress;
private boolean cancel = false;
private Workspace workspace;
private Writer writer;
//Settings
private final static float MARGIN = 25f;
private final String namespaceURI = SVGDOMImplementation.SVG_NAMESPACE_URI;
private boolean scaleStrokes = false;
//Helper
private final HashMap<NodeLabel, SVGLocatable> nodeLabelMap = new HashMap<NodeLabel, SVGLocatable>();
private Element svgRoot;
private Element nodeGroupElem;
private Element edgeGroupElem;
private Element labelGroupElem;
private Element labelBorderGroupElem;
private float scaleRatio = 1f;
public boolean execute() {
PreviewController controller = Lookup.getDefault().lookup(PreviewController.class);
GraphSheet graphSheet = controller.getGraphSheet();
try {
exportData(graphSheet);
} catch (Exception e) {
throw new RuntimeException(e);
}
return !cancel;
}
/**
* Does export the preview graph as an SVG image.
*
* @param file the output SVG file
* @param supportSize the support size of the exported image
* @throws Exception
*/
private void exportData(GraphSheet graphSheet) throws Exception {
SupportSize supportSize = new SupportSize(595, 841, LengthUnit.PIXELS);
Progress.start(progress);
Graph graph = graphSheet.getGraph();
// calculates progress units count
int max = 0;
if (graph.showNodes()) {
max += graph.countNodes();
}
if (graph.showEdges()) {
max += graph.countUnidirectionalEdges() + graph.countBidirectionalEdges();
if (graph.showSelfLoops()) {
max += graph.countSelfLoops();
}
}
Progress.switchToDeterminate(progress, max);
// export tasks
buildDOM(graphSheet, supportSize);
// creates SVG-to-SVG transcoder
SVGTranscoder t = new SVGTranscoder();
t.addTranscodingHint(SVGTranscoder.KEY_XML_DECLARATION, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
// sets transcoder input and output
TranscoderInput input = new TranscoderInput(doc);
// performs transcoding
try {
TranscoderOutput output = new TranscoderOutput(writer);
t.transcode(input, output);
} finally {
writer.close();
}
Progress.finish(progress);
}
public void renderGraph(Graph graph) {
if (graph.showEdges()) {
renderGraphEdges(graph);
}
if (graph.showNodes()) {
renderGraphNodes(graph);
}
renderGraphLabels(graph);
renderGraphLabelBorders(graph);
}
public void renderGraphEdges(Graph graph) {
edgeGroupElem = createGroupElem("edges");
svgRoot.appendChild(edgeGroupElem);
renderGraphUnidirectionalEdges(graph);
renderGraphBidirectionalEdges(graph);
renderGraphUndirectedEdges(graph);
if (graph.showSelfLoops()) {
renderGraphSelfLoops(graph);
}
}
public void renderGraphSelfLoops(Graph graph) {
for (SelfLoop sl : graph.getSelfLoops()) {
renderSelfLoop(sl);
}
}
public void renderGraphUnidirectionalEdges(Graph graph) {
for (UnidirectionalEdge edge : graph.getUnidirectionalEdges()) {
renderDirectedEdge(edge);
}
}
public void renderGraphBidirectionalEdges(Graph graph) {
for (BidirectionalEdge edge : graph.getBidirectionalEdges()) {
renderDirectedEdge(edge);
}
}
public void renderGraphUndirectedEdges(Graph graph) {
for (UndirectedEdge e : graph.getUndirectedEdges()) {
renderEdge(e);
}
}
public void renderGraphNodes(Graph graph) {
nodeGroupElem = createGroupElem("nodes");
svgRoot.appendChild(nodeGroupElem);
for (Node n : graph.getNodes()) {
renderNode(n);
}
}
public void renderGraphLabels(Graph graph) {
labelGroupElem = createGroupElem("labels");
svgRoot.appendChild(labelGroupElem);
for (UnidirectionalEdge e : graph.getUnidirectionalEdges()) {
if (!e.isCurved()) {
if (e.showLabel() && e.hasLabel() && e.getLabel().getFont() != null) {
renderEdgeLabel(e.getLabel());
}
if (e.showMiniLabels()) {
renderEdgeMiniLabels(e);
}
}
}
for (BidirectionalEdge e : graph.getBidirectionalEdges()) {
if (!e.isCurved()) {
if (e.showLabel() && e.hasLabel() && e.getLabel().getFont() != null) {
renderEdgeLabel(e.getLabel());
}
if (e.showMiniLabels()) {
renderEdgeMiniLabels(e);
}
}
}
for (UndirectedEdge e : graph.getUndirectedEdges()) {
if (e.showLabel() && !e.isCurved() && e.hasLabel() && e.getLabel().getFont() != null) {
renderEdgeLabel(e.getLabel());
}
}
for (Node n : graph.getNodes()) {
if (n.showLabel() && n.hasLabel() && n.getLabel().getFont() != null) {
renderNodeLabel(n.getLabel());
}
}
}
public void renderGraphLabelBorders(Graph graph) {
labelBorderGroupElem = createGroupElem("label borders");
svgRoot.insertBefore(labelBorderGroupElem, labelGroupElem);
for (Node n : graph.getNodes()) {
if (n.showLabel() && n.hasLabel() && n.showLabelBorders() && n.getLabel().getFont() != null) {
renderNodeLabelBorder(n.getLabelBorder());
}
}
}
public void renderNode(Node node) {
Element nodeElem = createElement("circle");
nodeElem.setAttribute("cx", node.getPosition().getX().toString());
nodeElem.setAttribute("cy", node.getPosition().getY().toString());
nodeElem.setAttribute("r", node.getRadius().toString());
nodeElem.setAttribute("fill", node.getColor().toHexString());
nodeElem.setAttribute("stroke", node.getBorderColor().toHexString());
nodeElem.setAttribute("stroke-width", new Float(node.getBorderWidth() * scaleRatio).toString());
nodeGroupElem.appendChild(nodeElem);
Progress.progress(progress);
}
public void renderNodeLabel(NodeLabel label) {
Text labelText = createTextNode(label.getValue());
Element labelElem = createElement("text");
labelElem.setAttribute("x", label.getPosition().getX().toString());
labelElem.setAttribute("y", label.getPosition().getY().toString());
labelElem.setAttribute("style", "text-anchor: middle");
labelElem.setAttribute("fill", label.getColor().toHexString());
labelElem.setAttribute("font-family", label.getFont().getFamily());
labelElem.setAttribute("font-size", Integer.toString(label.getFont().getSize()));
labelElem.appendChild(labelText);
labelGroupElem.appendChild(labelElem);
// need to save the label element in order to eventually draw its border
nodeLabelMap.put(label, (SVGLocatable) labelElem);
}
public void renderNodeLabelBorder(NodeLabelBorder border) {
// retrieve label's bounding box
SVGRect rect = nodeLabelMap.get(border.getLabel()).getBBox();
Element borderElem = createElement("rect");
borderElem.setAttribute("x", Float.toString(rect.getX()));
borderElem.setAttribute("y", Float.toString(rect.getY()));
borderElem.setAttribute("width", Float.toString(rect.getWidth()));
borderElem.setAttribute("height", Float.toString(rect.getHeight()));
borderElem.setAttribute("fill", border.getColor().toHexString());
labelBorderGroupElem.appendChild(borderElem);
}
public void renderSelfLoop(SelfLoop selfLoop) {
CubicBezierCurve curve = selfLoop.getCurve();
Element selfLoopElem = createElement("path");
selfLoopElem.setAttribute("d", String.format(Locale.ENGLISH, "M %f,%f C %f,%f %f,%f %f,%f",
curve.getPt1().getX(), curve.getPt1().getY(),
curve.getPt2().getX(), curve.getPt2().getY(),
curve.getPt3().getX(), curve.getPt3().getY(),
curve.getPt4().getX(), curve.getPt4().getY()));
selfLoopElem.setAttribute("stroke", selfLoop.getColor().toHexString());
selfLoopElem.setAttribute("stroke-width", Float.toString(selfLoop.getThickness() * selfLoop.getScale() * scaleRatio));
selfLoopElem.setAttribute("fill", "none");
edgeGroupElem.appendChild(selfLoopElem);
}
public void renderDirectedEdge(DirectedEdge edge) {
renderEdge(edge);
if (!edge.isCurved() && edge.showArrows()) {
renderEdgeArrows(edge);
}
}
public void renderEdge(Edge edge) {
if (edge.isCurved()) {
renderCurvedEdge(edge);
} else {
renderStraightEdge(edge);
}
Progress.progress(progress);
}
public void renderStraightEdge(Edge edge) {
Point boundary1 = edge.getNode1().getPosition();
Point boundary2 = edge.getNode2().getPosition();
Element edgeElem = createElement("path");
edgeElem.setAttribute("d", String.format(Locale.ENGLISH, "M %f,%f L %f,%f",
boundary1.getX(), boundary1.getY(),
boundary2.getX(), boundary2.getY()));
edgeElem.setAttribute("stroke", edge.getColor().toHexString());
edgeElem.setAttribute("stroke-width", Float.toString(edge.getThickness() * edge.getScale() * scaleRatio));
edgeGroupElem.appendChild(edgeElem);
}
public void renderCurvedEdge(Edge edge) {
for (CubicBezierCurve curve : edge.getCurves()) {
Element curveElem = createElement("path");
curveElem.setAttribute("d", String.format(Locale.ENGLISH, "M %f,%f C %f,%f %f,%f %f,%f",
curve.getPt1().getX(), curve.getPt1().getY(),
curve.getPt2().getX(), curve.getPt2().getY(),
curve.getPt3().getX(), curve.getPt3().getY(),
curve.getPt4().getX(), curve.getPt4().getY()));
curveElem.setAttribute("stroke", edge.getColor().toHexString());
curveElem.setAttribute("stroke-width", Float.toString(edge.getThickness() * edge.getScale() * scaleRatio));
curveElem.setAttribute("fill", "none");
edgeGroupElem.appendChild(curveElem);
}
}
public void renderEdgeArrows(DirectedEdge edge) {
for (EdgeArrow a : edge.getArrows()) {
renderEdgeArrow(a);
}
}
public void renderEdgeMiniLabels(DirectedEdge edge) {
for (EdgeMiniLabel ml : edge.getMiniLabels()) {
renderEdgeMiniLabel(ml);
}
}
public void renderEdgeArrow(EdgeArrow arrow) {
Element arrowElem = createElement("polyline");
arrowElem.setAttribute("points", String.format(Locale.ENGLISH, "%f,%f %f,%f %f,%f",
arrow.getPt1().getX(), arrow.getPt1().getY(),
arrow.getPt2().getX(), arrow.getPt2().getY(),
arrow.getPt3().getX(), arrow.getPt3().getY()));
arrowElem.setAttribute("fill", arrow.getColor().toHexString());
arrowElem.setAttribute("stroke", "none");
edgeGroupElem.appendChild(arrowElem);
}
public void renderEdgeLabel(EdgeLabel label) {
Text text = createTextNode(label.getValue());
Element labelElem = createElement("text");
labelElem.setAttribute("x", "0");
labelElem.setAttribute("y", "0");
labelElem.setAttribute("style", "text-anchor: middle");
labelElem.setAttribute("fill", label.getColor().toHexString());
labelElem.setAttribute("font-family", label.getFont().getFamily());
labelElem.setAttribute("font-size", Integer.toString(label.getFont().getSize()));
labelElem.setAttribute("transform", String.format(Locale.ENGLISH, "translate(%f,%f) rotate(%f)",
label.getPosition().getX(), label.getPosition().getY(),
Math.toDegrees(label.getAngle())));
labelElem.appendChild(text);
labelGroupElem.appendChild(labelElem);
}
public void renderEdgeMiniLabel(EdgeMiniLabel miniLabel) {
Text text = createTextNode(miniLabel.getValue());
Element miniLabelElem = createElement("text");
miniLabelElem.setAttribute("x", "0");
miniLabelElem.setAttribute("y", "0");
miniLabelElem.setAttribute("style", miniLabel.getHAlign().toCSS());
miniLabelElem.setAttribute("fill", miniLabel.getColor().toHexString());
miniLabelElem.setAttribute("font-family", miniLabel.getFont().getFamily());
miniLabelElem.setAttribute("font-size", Integer.toString(miniLabel.getFont().getSize()));
miniLabelElem.setAttribute("transform", String.format(Locale.ENGLISH, "translate(%f,%f) rotate(%f)",
miniLabel.getPosition().getX(), miniLabel.getPosition().getY(),
Math.toDegrees(miniLabel.getAngle())));
miniLabelElem.appendChild(text);
labelGroupElem.appendChild(miniLabelElem);
}
/**
* Builds the DOM from the preview graph.
*
* @param graphSheet the preview graph sheet
* @param supportSize the support size of the exported image
*/
protected Document buildDOM(GraphSheet graphSheet, SupportSize supportSize) {
// creates SVG document
DOMImplementation impl = SVGDOMImplementation.getDOMImplementation();
DocumentType doctype = impl.createDocumentType(
"-//W3C//DTD SVG 1.1//EN",
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd",
"");
doc = impl.createDocument(namespaceURI, "svg", doctype);
// initializes CSS and SVG specific DOM interfaces
UserAgent userAgent = new UserAgentAdapter();
DocumentLoader loader = new DocumentLoader(userAgent);
BridgeContext ctx = new BridgeContext(userAgent, loader);
ctx.setDynamicState(BridgeContext.DYNAMIC);
GVTBuilder builder = new GVTBuilder();
builder.build(ctx, doc);
// image margin
graphSheet.setMargin(MARGIN);
//Dimension
int width = graphSheet.getWidth().intValue();
int height = graphSheet.getHeight().intValue();
if (width > height) {
supportSize = new SupportSize(width * supportSize.getHeightInt() / height, supportSize.getHeightInt(), LengthUnit.PIXELS);
} else if (height > width) {
supportSize = new SupportSize(supportSize.getWidthInt(), height * supportSize.getWidthInt() / width, LengthUnit.PIXELS);
}
// root element
svgRoot = doc.getDocumentElement();
svgRoot.setAttributeNS(null, "width", supportSize.getWidth());
svgRoot.setAttributeNS(null, "height", supportSize.getHeight());
svgRoot.setAttributeNS(null, "version", "1.1");
svgRoot.setAttributeNS(null, "viewBox", String.format(Locale.ENGLISH, "%d %d %d %d",
graphSheet.getTopLeftPosition().getX().intValue(),
graphSheet.getTopLeftPosition().getY().intValue(),
width,
height));
//Scale & ratio
if(scaleStrokes) {
scaleRatio = supportSize.getWidthInt() / (float)width;
}
// draws the graph exporting it into the DOM
renderGraph(graphSheet.getGraph());
return doc;
}
/**
* Creates a new element from the current document.
*
* @param qualifiedName the qualified name of the element type to
* instantiate
* @return a new <code>Element</code> object
*/
private Element createElement(String qualifiedName) {
return doc.createElementNS(namespaceURI, qualifiedName);
}
/**
* Creates a text node from the current document.
*
* @param data the data for the node
* @return a new <code>Text</code> object
*/
private Text createTextNode(String data) {
return doc.createTextNode(data);
}
/**
* Creates the group element corresponding to the given type.
*
* @param type the name of the group element to create
* @return the created group element
*/
private Element createGroupElem(String name) {
Element group = createElement("g");
group.setAttribute("id", name);
return group;
}
public boolean cancel() {
cancel = true;
return true;
}
public void setProgressTicket(ProgressTicket progressTicket) {
this.progress = progressTicket;
}
public void setWriter(Writer writer) {
this.writer = writer;
}
public Workspace getWorkspace() {
return workspace;
}
public void setWorkspace(Workspace workspace) {
this.workspace = workspace;
}
public void setScaleStrokes(boolean scaleStrokes) {
this.scaleStrokes = scaleStrokes;
}
public boolean isScaleStrokes() {
return scaleStrokes;
}
}