/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/*
* PrefuseGraph.java
* Copyright (C) 2009 University of Waikato, Hamilton, New Zealand
* Copyright (C) Jeffrey Heer (original prefuse demo)
*/
package wekaexamples.gui.visualize.plugins;
import prefuse.Display;
import prefuse.Visualization;
import prefuse.action.ActionList;
import prefuse.action.RepaintAction;
import prefuse.action.assignment.ColorAction;
import prefuse.action.filter.GraphDistanceFilter;
import prefuse.action.layout.graph.ForceDirectedLayout;
import prefuse.activity.Activity;
import prefuse.controls.DragControl;
import prefuse.controls.FocusControl;
import prefuse.controls.NeighborHighlightControl;
import prefuse.controls.PanControl;
import prefuse.controls.WheelZoomControl;
import prefuse.controls.ZoomControl;
import prefuse.controls.ZoomToFitControl;
import prefuse.data.Graph;
import prefuse.data.Tuple;
import prefuse.data.event.TupleSetListener;
import prefuse.data.io.GraphMLReader;
import prefuse.data.tuple.TupleSet;
import prefuse.render.DefaultRendererFactory;
import prefuse.render.LabelRenderer;
import prefuse.util.ColorLib;
import prefuse.util.force.ForceSimulator;
import prefuse.util.ui.JForcePanel;
import prefuse.util.ui.JValueSlider;
import prefuse.visual.VisualGraph;
import prefuse.visual.VisualItem;
import weka.core.FastVector;
import weka.gui.graphvisualizer.BIFParser;
import weka.gui.graphvisualizer.GraphEdge;
import weka.gui.graphvisualizer.GraphNode;
import weka.gui.visualize.plugins.GraphVisualizePlugin;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.Serializable;
import java.io.StringWriter;
import java.util.Date;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
/**
* Displays a graph in <a href="http://www.cs.cmu.edu/~fgcozman/Research/InterchangeFormat/" target="_blank">XML BIF</a>
* format as <a href="http://prefuse.org/" target="_blank">Prefuse</a> graph.
* <p/>
* Based on the <code>prefuse.demos.GraphView</code> demo.
*
* @author <a href="http://jheer.org">jeffrey heer</a> (original prefuse demo)
* @author fracpete (fracpete at waikato dot ac dot nz)
* @version $Revision$
* @see prefuse.demos.GraphView
*/
public class PrefuseGraph
implements Serializable, GraphVisualizePlugin {
/** for serialization. */
private static final long serialVersionUID = 5541844748101135174L;
/**
* Turns the <a href="http://www.cs.cmu.edu/~fgcozman/Research/InterchangeFormat/" target="_blank">XML BIF</a>
* format into <a href="http://graphml.graphdrawing.org/specification/" target="_blank">GraphML XML</a>
* format.
*
* @author fracpete (fracpete at waikato dot ac dot nz)
* @version $Revision$
*/
public static class BIFToGraphML {
/**
* Replaces certain characters with their character entities.
*
* @param s the string to process
* @return the processed string
*/
protected String sanitize(String s) {
String result;
result = s;
result = result.replaceAll("&", "&")
.replaceAll("\"", """)
.replaceAll("'", "'")
.replaceAll("<", "<")
.replaceAll(">", ">");
// in addition, replace some other entities as well
result = result.replaceAll("\n", "
")
.replaceAll("\r", "
")
.replaceAll("\t", " ");
return result;
}
/**
* Writes the header of the GraphML file.
*
* @param writer the writer to use
* @throws Exception if an error occurs
*/
protected void writeHeader(BufferedWriter writer) throws Exception {
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); writer.newLine();
writer.newLine();
writer.write("<!-- This file was generated by Weka (http://www.cs.waikato.ac.nz/ml/weka/). -->"); writer.newLine();
writer.newLine();
writer.write("<graphml xmlns=\"http://graphml.graphdrawing.org/xmlns\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd\">"); writer.newLine();
writer.write("<key id=\"node-label\" for=\"node\" attr.name=\"label\" attr.type=\"string\"/>"); writer.newLine();
writer.write("<graph id=\"" + new Date() + "\" edgedefault=\"directed\">"); writer.newLine();
}
/**
* Writes the node as GraphML.
*
* @param writer the writer to use
* @param node the node to write as GraphML
* @throws Exception if an error occurs
*/
protected void writeNodes(BufferedWriter writer, GraphNode node) throws Exception {
writer.write("<node id=\"" + node.ID + "\">"); writer.newLine();
writer.write("<data key=\"node-label\">" + sanitize(node.lbl) + "</data>"); writer.newLine();
writer.write("</node>"); writer.newLine();
}
/**
* Writes the edge as GraphML.
*
* @param writer the writer to use
* @param nodes the nodes
* @param edge the edge to writge
* @throws Exception if an error occurs
*/
protected void writeEdges(BufferedWriter writer, FastVector nodes, GraphEdge edge) throws Exception {
writer.write("<edge id=\"" + edge.hashCode() + "\" source=\"" + ((GraphNode) nodes.elementAt(edge.src)).ID + "\" target=\"" + ((GraphNode) nodes.elementAt(edge.dest)).ID + "\"/>");
writer.newLine();
}
/**
* Writes the footer of the GraphML file.
*
* @param writer the writer to use
* @throws Exception if an error occurs
*/
protected void writeFooter(BufferedWriter writer) throws Exception {
writer.write("</graph>"); writer.newLine();
writer.write("</graphml>"); writer.newLine();
}
/**
* Parses the incoming data and writes the generated output.
*
* @param input the string read the BIF data from
* @return the generated GraphML
* @throws Exception if parsing or writing fails
*/
public String convert(String input) throws Exception {
BIFParser parser;
FastVector nodes;
FastVector edges;
StringWriter output;
BufferedWriter writer;
int i;
// parse bif format
nodes = new FastVector();
edges = new FastVector();
parser = new BIFParser(input, nodes, edges);
parser.parse();
// generate GraphML output
output = new StringWriter();
writer = new BufferedWriter(output);
writeHeader(writer);
for (i = 0; i < nodes.size(); i++)
writeNodes(writer, (GraphNode) nodes.elementAt(i));
for (i = 0; i < edges.size(); i++)
writeEdges(writer, nodes, (GraphEdge) edges.elementAt(i));
writeFooter(writer);
writer.flush();
return output.toString();
}
}
/**
* A panel for displaying a prefuse graph.
* <p/>
* Based on the <code>prefuse.demos.GraphView</code> demo.
*
* @author fracpete (fracpete at waikato dot ac dot nz)
* @version $Revision$
* @see prefuse.demos.GraphView
*/
public final static class GraphPanel
extends JPanel {
/** for serialization. */
private static final long serialVersionUID = 5943939093143764654L;
/** the constant for "graph". */
public final static String GRAPH = "graph";
/** the constant for "graph.nodes". */
public final static String GRAPH_NODES = "graph.nodes";
/** the constant for "graph.edges". */
public final static String GRAPH_EDGES = "graph.edges";
/** the constant for "label". */
public final static String LABEL = "label";
/** the constant for "draw". */
public final static String DRAW = "draw";
/** the constant for "layout". */
public final static String LAYOUT = "layout";
/** for visualizing the graph. */
protected Visualization m_vis;
/**
* Initializes the panel.
*
* @param graph the graph to display
*/
public GraphPanel(Graph graph) {
super(new BorderLayout());
m_vis = new Visualization();
// --------------------------------------------------------------------
// set up the renderers
LabelRenderer tr = new LabelRenderer();
tr.setRoundedCorner(8, 8);
m_vis.setRendererFactory(new DefaultRendererFactory(tr));
// --------------------------------------------------------------------
// register the data with a visualization
// adds graph to visualization and sets renderer label field
DefaultRendererFactory drf = (DefaultRendererFactory)
m_vis.getRendererFactory();
((LabelRenderer)drf.getDefaultRenderer()).setTextField(LABEL);
// --------------------------------------------------------------------
// create actions to process the visual data
int hops = 30;
final GraphDistanceFilter filter = new GraphDistanceFilter(GRAPH, hops);
ColorAction fill = new ColorAction(GRAPH_NODES,
VisualItem.FILLCOLOR, ColorLib.rgb(200,200,255));
fill.add(VisualItem.FIXED, ColorLib.rgb(255,100,100));
fill.add(VisualItem.HIGHLIGHT, ColorLib.rgb(255,200,125));
ActionList draw = new ActionList();
draw.add(filter);
draw.add(fill);
draw.add(new ColorAction(GRAPH_NODES, VisualItem.STROKECOLOR, 0));
draw.add(new ColorAction(GRAPH_NODES, VisualItem.TEXTCOLOR, ColorLib.rgb(0,0,0)));
draw.add(new ColorAction(GRAPH_EDGES, VisualItem.FILLCOLOR, ColorLib.gray(200)));
draw.add(new ColorAction(GRAPH_EDGES, VisualItem.STROKECOLOR, ColorLib.gray(200)));
ActionList animate = new ActionList(Activity.INFINITY);
animate.add(new ForceDirectedLayout(GRAPH));
animate.add(fill);
animate.add(new RepaintAction());
// finally, we register our ActionList with the Visualization.
// we can later execute our Actions by invoking a method on our
// Visualization, using the name we've chosen below.
m_vis.putAction(DRAW, draw);
m_vis.putAction(LAYOUT, animate);
m_vis.runAfter(DRAW, LAYOUT);
// --------------------------------------------------------------------
// set up a display to show the visualization
Display display = new Display(m_vis);
display.setSize(700,700);
display.pan(350, 350);
display.setForeground(Color.GRAY);
display.setBackground(Color.WHITE);
// main display controls
display.addControlListener(new FocusControl(1));
display.addControlListener(new DragControl());
display.addControlListener(new PanControl());
display.addControlListener(new ZoomControl());
display.addControlListener(new WheelZoomControl());
display.addControlListener(new ZoomToFitControl());
display.addControlListener(new NeighborHighlightControl());
display.setForeground(Color.GRAY);
display.setBackground(Color.WHITE);
// --------------------------------------------------------------------
// launch the visualization
// create a panel for editing force values
ForceSimulator fsim = ((ForceDirectedLayout)animate.get(0)).getForceSimulator();
JForcePanel fpanel = new JForcePanel(fsim);
final JValueSlider slider = new JValueSlider("Distance", 0, hops, hops);
slider.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
filter.setDistance(slider.getValue().intValue());
m_vis.run(DRAW);
}
});
slider.setBackground(Color.WHITE);
slider.setPreferredSize(new Dimension(300,30));
slider.setMaximumSize(new Dimension(300,30));
Box cf = new Box(BoxLayout.Y_AXIS);
cf.add(slider);
cf.setBorder(BorderFactory.createTitledBorder("Connectivity Filter"));
fpanel.add(cf);
//fpanel.add(opanel);
fpanel.add(Box.createVerticalGlue());
// create a new JSplitPane to present the interface
JSplitPane split = new JSplitPane();
split.setLeftComponent(display);
split.setRightComponent(fpanel);
split.setOneTouchExpandable(true);
split.setContinuousLayout(false);
split.setDividerLocation(700);
add(split, BorderLayout.CENTER);
// update graph
m_vis.removeGroup(GRAPH);
VisualGraph vg = m_vis.addGraph(GRAPH, graph);
m_vis.setValue(GRAPH_EDGES, null, VisualItem.INTERACTIVE, Boolean.FALSE);
VisualItem f = (VisualItem)vg.getNode(0);
m_vis.getGroup(Visualization.FOCUS_ITEMS).setTuple(f);
f.setFixed(false);
// fix selected focus nodes
TupleSet focusGroup = m_vis.getGroup(Visualization.FOCUS_ITEMS);
focusGroup.addTupleSetListener(new TupleSetListener() {
public void tupleSetChanged(TupleSet ts, Tuple[] add, Tuple[] rem)
{
for ( int i=0; i<rem.length; ++i )
((VisualItem)rem[i]).setFixed(false);
for ( int i=0; i<add.length; ++i ) {
((VisualItem)add[i]).setFixed(false);
((VisualItem)add[i]).setFixed(true);
}
if ( ts.getTupleCount() == 0 ) {
ts.addTuple(rem[0]);
((VisualItem)rem[0]).setFixed(false);
}
m_vis.run(DRAW);
}
});
m_vis.run(DRAW);
}
}
/**
* Get the minimum version of Weka, inclusive, the class
* is designed to work with. eg: <code>3.5.0</code>
*
* @return the minimum version
*/
public String getMinVersion() {
return "3.5.9";
}
/**
* Get the maximum version of Weka, exclusive, the class
* is designed to work with. eg: <code>3.6.0</code>
*
* @return the maximum version
*/
public String getMaxVersion() {
return "3.7.0";
}
/**
* Get the specific version of Weka the class is designed for.
* eg: <code>3.5.1</code>
*
* @return the version the plugin was designed for
*/
public String getDesignVersion() {
return "3.5.9";
}
/**
* Get a JMenu or JMenuItem which contain action listeners
* that perform the visualization of the graph in XML BIF format.
* Exceptions thrown because of changes in Weka since compilation need to
* be caught by the implementer.
*
* @see NoClassDefFoundError
* @see IncompatibleClassChangeError
*
* @param bif the graph in XML BIF format
* @param name the name of the item (in the Explorer's history list)
* @return menuitem for opening visualization(s), or null
* to indicate no visualization is applicable for the input
*/
public JMenuItem getVisualizeMenuItem(String bif, String name) {
JMenuItem result;
final String bifF = bif;
final String nameF = name;
result = new JMenuItem("Prefuse graph");
result.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
display(bifF, nameF);
}
});
return result;
}
/**
* Displays the error.
*
* @param msg the error to display
*/
protected void displayError(String msg) {
JOptionPane.showMessageDialog(null, msg, "Error displaying graph", JOptionPane.ERROR_MESSAGE);
}
/**
* Converts the XML BIF format to GraphML.
*
* @param bif the graph in XML BIF format
* @return the graph in GraphML or null in case of an error
*/
protected String convert(String bif) {
String result;
BIFToGraphML d2gml;
d2gml = new BIFToGraphML();
try {
result = d2gml.convert(bif);
}
catch (Exception e) {
result = null;
e.printStackTrace();
displayError(e.toString());
}
return result;
}
/**
* Parses the graph in GraphML and returns the built graph.
*
* @param graphml the graph in GraphML
* @return the graph or null in case of an error
*/
protected Graph parse(String graphml) {
ByteArrayInputStream inStream;
Graph result;
try {
inStream = new ByteArrayInputStream(graphml.getBytes());
result = new GraphMLReader().readGraph(inStream);
}
catch ( Exception e ) {
result = null;
e.printStackTrace();
displayError(e.toString());
}
return result;
}
/**
* Displays the graph.
*
* @param bif the graph in XML BIF format
* @param name the name of the graph
*/
protected void display(String bif, String name) {
String graphml;
Graph graph;
JPanel panel;
JFrame frame;
// convert bif graph
graphml = convert(bif);
if (graphml == null)
return;
// parse graph
graph = parse(graphml);
if (graph == null)
return;
// display graph
panel = new GraphPanel(graph);
frame = new JFrame("Prefuse graph [" + name + "]");
frame.setSize(1000, 600);
frame.setContentPane(panel);
frame.setVisible(true);
}
}