Cytoscape 2.6 has a headless mode but does not really support it. Cytoscape 3.0 is promised to support this way but the exporter is not implemented yet (from the subversion revision 18418) , I just can't wait. (And 3.0 is somehow complicated). After hours of deeping into the source of cytoscape, I found when exporter get the InternalFrameComponent for printing, it invokes the GUI by calling Cytoscape.getDesktop() . That's to say if I create the InternalFrameComponent manually, maybe it could be a way to void startup the GUI.
I create a class named GraphComponentFactory to create the InternalFrameComponent. Here comes the hacking:
//GraphComponentFactory.java
package cytoscape.util.export;
import giny.view.NodeView;
import java.awt.Dimension;
import java.util.Iterator;
import cytoscape.Cytoscape;
import cytoscape.logger.CyLogger;
import cytoscape.view.CyNetworkView;
import cytoscape.view.InternalFrameComponent;
import ding.view.DGraphView;
import javax.swing.*;
/**
* A class to create the InternalFrameComponent as network view container which used
* by a graphics exporter for printing .
*/
public class GraphComponentFactory {
private static CyLogger logger = CyLogger.getLogger(GraphComponentFactory.class);
public static int maxImageWidth = 1600;
public static InternalFrameComponent createInternalComponent(CyNetworkView view){
Dimension d = getDimension(view);
double scale = Math.min(maxImageWidth/d.getWidth(), maxImageWidth/d.getHeight());
int w,h;
if(scale < 1){
logger.debug("zooming:"+scale);
view.setZoom(scale);
w =(int) (d.getWidth()*scale);
h = (int) (d.getHeight()*scale);
}else{
w = (int) d.getWidth();
h = (int) d.getHeight();
}
return createInternalComponent(view, w, h,false);
}
public static InternalFrameComponent createInternalComponent(CyNetworkView view,int width, int height){
return createInternalComponent(view,width,height,true);
}
public static InternalFrameComponent createInternalComponent(CyNetworkView view,int width, int height, boolean autoZoom){
JLayeredPane pane = new JLayeredPane();
pane.setSize(width,height);
InternalFrameComponent ifc = new InternalFrameComponent(pane, (DGraphView) view);//Cytoscape.getDesktop().getNetworkViewManager().getInternalFrameComponent(view);
ifc.setBounds(0,0,width,height);
if(autoZoom) zoom(view,width,height);
return ifc;
}
/**
* Automatically zoom, so the whole view could be paint within the specified width and height.
*/
private static void zoom(CyNetworkView view, int width, int height) {
Dimension d = getDimension(view);
double h = d.getHeight();
double w = d.getWidth();
double scale = Math.min(width/w,height/h);
logger.debug("view size:("+w+","+h+"), image size: ("+width+","+height+")zoom :"+scale);
view.setZoom(scale);
}
/**
* calculating view's width and height by finding the maximum x and y position of nodes
*/
public static Dimension getDimension(CyNetworkView view){
Iterator<NodeView> nodeviews = view.getNodeViewsIterator();
NodeView nv;
double w = 0;
double h = 0;
while(nodeviews.hasNext()){
nv = nodeviews.next();
w = Math.max(w, nv.getXPosition()+nv.getWidth());
h = Math.max(h, nv.getYPosition()+nv.getHeight());
}
return new Dimension((int)w,(int)h);
}
public static InternalFrameComponent getInternalComponent(CyNetworkView view){
//I modified CyMain code so when it startup in GUI mode, the system property cytoscape.mode will be set to 'gui', just get the InternalFrameCoponent the normal way, otherwise create it manually.
if("gui".equals(System.getProperty("cytoscape.mode")))
return Cytoscape.getDesktop().getNetworkViewManager().getInternalFrameComponent(view);
return createInternalComponent(view);
}
}
Then , replace the get InternalFrameComponent code in exporters. Like in BitmapExporter.java about line 43 in method export, change this line:
InternalFrameComponent ifc = Cytoscape.getDesktop().getNetworkViewManager().getInternalFrameComponent(view);
to:
InternalFrameComponent ifc = GraphComponentFactory.getInternalComponent(view);
Hack other exporters the same way.
By the way, I changed the Exporter interface and all its implementations export method signature, specifically the second parameter from FileOutputStream to OutputStream. It is not necessary to limit the ImageIO's OutputStream. After the modification, the export could take an output stream other than a file (such as servlet response's output stream) as the destination.
Here's comes TestCase:
//ExporterTest.java
package cytoscape.util.export;
import csplugins.layout.LayoutPlugin;
import cytoscape.CyEdge;
import cytoscape.CyNetwork;
import cytoscape.CyNode;
import cytoscape.Cytoscape;
import cytoscape.data.readers.XGMMLReader;
import cytoscape.data.writers.XGMMLWriter;
import cytoscape.layout.CyLayoutAlgorithm;
import cytoscape.layout.CyLayouts;
import cytoscape.view.CyNetworkView;
import cytoscape.view.InternalFrameComponent;
import ding.view.DGraphView;
import giny.model.RootGraph;
import giny.view.EdgeView;
import giny.view.NodeView;
import junit.framework.TestCase;
import javax.swing.*;
import java.awt.Color;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
/**
* Created by IntelliJ IDEA.
* User: Administrator
* Date: 2009-10-27
* Time: 15:36:44
* To change this template use File | Settings | File Templates.
*/
public class ExporterTest extends TestCase {
CyNetwork network;
CyNode node1;
CyNode node2;
CyEdge edge1;
CyEdge edge2;
CyNetworkView view;
NodeView nodeView1;
NodeView nodeView2;
EdgeView edgeView1;
EdgeView edgeView2;
static{
new LayoutPlugin();
}
@Override
protected void setUp() throws Exception {
super.setUp();
node1 = Cytoscape.getCyNode("node1", true);
node2 = Cytoscape.getCyNode("node2", true);
edge1 = Cytoscape.getCyEdge("node1", "node1 (pp) node2", "node2", "pp");
edge2 = Cytoscape.getCyEdge("node2", "node2 (pp) node1", "node1", "pp");
int[] nodeArray = { node1.getRootGraphIndex(), node2.getRootGraphIndex() };
int[] edgeArray = { edge1.getRootGraphIndex(), edge2.getRootGraphIndex() };
network = Cytoscape.createNetwork(nodeArray, edgeArray, null);
view = Cytoscape.createNetworkView(network);
nodeView1 = view.getNodeView(node1);
nodeView1.setShape(NodeView.ROUNDED_RECTANGLE);
nodeView1.setWidth(70);
nodeView1.setHeight(30);
nodeView1.getLabel().setText("GO:2001201\nBiological Process");
nodeView1.setUnselectedPaint(Color.green);
nodeView2 = view.getNodeView(node2);
edgeView1 = view.getEdgeView(edge1);
edgeView2 = view.getEdgeView(edge2);
}
@Override
protected void tearDown() throws Exception {
super.tearDown(); //To change body of overridden methods use File | Settings | File Templates.
}
public ExporterTest(String name) {
super(name);
}
public void testPDFExporter() throws Exception {
//
PDFExporter export = new PDFExporter();
FileOutputStream output = new FileOutputStream("out/exporter_test.pdf");
export.export(view, output);
}
public void testBitmapExporter() throws Exception {
BitmapExporter export = new BitmapExporter("png",2.0);
FileOutputStream output = new FileOutputStream("out/exporter_test.png");
export.export(view, output);
export = new BitmapExporter("jpg",1.0);
for(CyLayoutAlgorithm layout: CyLayouts.getAllLayouts()){
output = new FileOutputStream("out/exporter_test_"+layout.getName()+".jpg");
layout.doLayout(view);
export.export(view,output);
}
}
public void testSVGExporter() throws Exception {
SVGExporter export = new SVGExporter();
FileOutputStream output = new FileOutputStream("out/exporter_test.svg");
export.export(view, output);
}
public void testXGMMLWriter() throws Exception {
XGMMLWriter writer = new XGMMLWriter(network, view);
writer.write(new FileWriter("out/export_test.xgmml"));
}
}
Notice the highlighted static code block, It registers layouts algorithms from the core plugin AutomaticLayout.jar, so include this jar in your classpath if you want using those layouts.
Do NOT ask cytoscape to load plugins, because it will invoke GUI.