weka.gui.graphvisualizer.GraphVisualizer Maven / Gradle / Ivy
Show all versions of weka-dev Show documentation
/*
* 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 3 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, see .
*/
/*
* GraphVisualizer.java
* Copyright (C) 2003-2012 University of Waikato, Hamilton, New Zealand
*
*/
package weka.gui.graphvisualizer;
import weka.gui.ExtensionFileFilter;
import weka.gui.WekaFileChooser;
import weka.gui.visualize.PrintablePanel;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.table.AbstractTableModel;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dialog.ModalityType;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
/**
* This class displays the graph we want to visualize. It should be sufficient
* to use only this class in weka.gui.graphvisulizer package to visualize a
* graph. The description of a graph should be provided as a string argument
* using readBIF or readDOT method in either XMLBIF03 or DOT format.
* Alternatively, an InputStream in XMLBIF03 can also be provided to another
* variation of readBIF. It would be necessary in case input is in DOT format to
* call the layoutGraph() method to display the graph correctly after the call
* to readDOT. It is also necessary to do so if readBIF is called and the graph
* description doesn't have x y positions for nodes.
*
* The graph's data is held in two FastVectors, nodes are stored as objects of
* GraphNode class and edges as objects of GraphEdge class.
*
* The graph is displayed by positioning and drawing each node according to its
* x y position and then drawing all the edges coming out of it give by its
* edges[][] array, the arrow heads are ofcourse marked in the opposite(ie
* original direction) or both directions if the edge is reversed or is in both
* directions. The graph is centered if it is smaller than it's display area.
* The edges are drawn from the bottom of the current node to the top of the
* node given by edges[][] array in GraphNode class, to avoid edges crossing
* over other nodes. This might need to be changed if another layout engine is
* added or the current Hierarchical engine is updated to avoid such crossings
* over nodes.
*
* @author Ashraf M. Kibriya ([email protected])
* @version $Revision: 15104 $
*/
public class GraphVisualizer extends JPanel implements GraphConstants,
LayoutCompleteEventListener {
/** for serialization */
private static final long serialVersionUID = -2038911085935515624L;
/** Vector containing nodes */
protected ArrayList m_nodes = new ArrayList();
/** Vector containing edges */
protected ArrayList m_edges = new ArrayList();
/** The current LayoutEngine */
protected LayoutEngine m_le;
/** Panel actually displaying the graph */
protected GraphPanel m_gp;
/** String containing graph's name */
protected String graphID;
/**
* Save button to save the current graph in DOT or XMLBIF format. The graph
* should be layed out again to get the original form if reloaded from command
* line, as the formats do not allow saving specific information for a
* properly layed out graph.
*/
protected JButton m_jBtSave;
/** path for icons */
private final String ICONPATH = "weka/gui/graphvisualizer/icons/";
private final FontMetrics fm = this.getFontMetrics(this.getFont());
private double scale = 1; // current zoom
private int nodeHeight = 2 * fm.getHeight(), nodeWidth = 24;
private int paddedNodeWidth = 24 + 8;
/** TextField for node's width */
private final JTextField jTfNodeWidth = new JTextField(3);
/** TextField for nodes height */
private final JTextField jTfNodeHeight = new JTextField(3);
/**
* Button for laying out the graph again, necessary after changing node's size
* or some other property of the layout engine
*/
private final JButton jBtLayout;
/** used for setting appropriate node size */
private int maxStringWidth = 0;
/** used when using zoomIn and zoomOut buttons */
private final int[] zoomPercents = { 10, 25, 50, 75, 100, 125, 150, 175, 200,
225, 250, 275, 300, 350, 400, 450, 500, 550, 600, 650, 700, 800, 900, 999 };
/** this contains the m_gp GraphPanel */
JScrollPane m_js;
/**
* Constructor
* Sets up the gui and initializes all the other previously uninitialized
* variables.
*/
public GraphVisualizer() {
m_gp = new GraphPanel();
m_js = new JScrollPane(m_gp);
// creating a new layout engine and adding this class as its listener
// to receive layoutComplete events
m_le = new HierarchicalBCEngine(m_nodes, m_edges, paddedNodeWidth,
nodeHeight);
m_le.addLayoutCompleteEventListener(this);
m_jBtSave = new JButton();
java.net.URL tempURL = ClassLoader.getSystemResource(ICONPATH + "save.gif");
if (tempURL != null) {
m_jBtSave.setIcon(new ImageIcon(tempURL));
} else {
System.err.println(ICONPATH
+ "save.gif not found for weka.gui.graphvisualizer.Graph");
}
m_jBtSave.setToolTipText("Save Graph");
m_jBtSave.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ae) {
WekaFileChooser fc = new WekaFileChooser(System.getProperty("user.dir"));
ExtensionFileFilter ef1 = new ExtensionFileFilter(".dot", "DOT files");
ExtensionFileFilter ef2 = new ExtensionFileFilter(".xml",
"XML BIF files");
fc.addChoosableFileFilter(ef1);
fc.addChoosableFileFilter(ef2);
fc.setDialogTitle("Save Graph As");
int rval = fc.showSaveDialog(GraphVisualizer.this);
if (rval == JFileChooser.APPROVE_OPTION) {
// System.out.println("Saving to file \""+
// f.getAbsoluteFile().toString()+"\"");
if (fc.getFileFilter() == ef2) {
String filename = fc.getSelectedFile().toString();
if (!filename.endsWith(".xml")) {
filename = filename.concat(".xml");
}
BIFParser.writeXMLBIF03(filename, graphID, m_nodes, m_edges);
} else {
String filename = fc.getSelectedFile().toString();
if (!filename.endsWith(".dot")) {
filename = filename.concat(".dot");
}
DotParser.writeDOT(filename, graphID, m_nodes, m_edges);
}
}
}
});
final JButton jBtZoomIn = new JButton();
tempURL = ClassLoader.getSystemResource(ICONPATH + "zoomin.gif");
if (tempURL != null) {
jBtZoomIn.setIcon(new ImageIcon(tempURL));
} else {
System.err.println(ICONPATH
+ "zoomin.gif not found for weka.gui.graphvisualizer.Graph");
}
jBtZoomIn.setToolTipText("Zoom In");
final JButton jBtZoomOut = new JButton();
tempURL = ClassLoader.getSystemResource(ICONPATH + "zoomout.gif");
if (tempURL != null) {
jBtZoomOut.setIcon(new ImageIcon(tempURL));
} else {
System.err.println(ICONPATH
+ "zoomout.gif not found for weka.gui.graphvisualizer.Graph");
}
jBtZoomOut.setToolTipText("Zoom Out");
final JTextField jTfZoom = new JTextField("100%");
jTfZoom.setMinimumSize(jTfZoom.getPreferredSize());
jTfZoom.setHorizontalAlignment(JTextField.CENTER);
jTfZoom.setToolTipText("Zoom");
jTfZoom.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ae) {
JTextField jt = (JTextField) ae.getSource();
try {
int i = -1;
i = jt.getText().indexOf('%');
if (i == -1) {
i = Integer.parseInt(jt.getText());
} else {
i = Integer.parseInt(jt.getText().substring(0, i));
}
if (i <= 999) {
scale = i / 100D;
}
jt.setText((int) (scale * 100) + "%");
if (scale > 0.1) {
if (!jBtZoomOut.isEnabled()) {
jBtZoomOut.setEnabled(true);
}
} else {
jBtZoomOut.setEnabled(false);
}
if (scale < 9.99) {
if (!jBtZoomIn.isEnabled()) {
jBtZoomIn.setEnabled(true);
}
} else {
jBtZoomIn.setEnabled(false);
}
setAppropriateSize();
// m_gp.clearBuffer();
m_gp.repaint();
m_gp.invalidate();
m_js.revalidate();
} catch (NumberFormatException ne) {
JOptionPane.showMessageDialog(GraphVisualizer.this.getParent(),
"Invalid integer entered for zoom.", "Error",
JOptionPane.ERROR_MESSAGE);
jt.setText((scale * 100) + "%");
}
}
});
jBtZoomIn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ae) {
int i = 0, s = (int) (scale * 100);
if (s < 300) {
i = s / 25;
} else if (s < 700) {
i = 6 + s / 50;
} else {
i = 13 + s / 100;
}
if (s >= 999) {
JButton b = (JButton) ae.getSource();
b.setEnabled(false);
return;
} else if (s >= 10) {
if (i >= 22) {
JButton b = (JButton) ae.getSource();
b.setEnabled(false);
}
if (s == 10 && !jBtZoomOut.isEnabled()) {
jBtZoomOut.setEnabled(true);
}
// System.out.println("i: "+i+"Zoom is: "+zoomPercents[i+1]);
jTfZoom.setText(zoomPercents[i + 1] + "%");
scale = zoomPercents[i + 1] / 100D;
} else {
if (!jBtZoomOut.isEnabled()) {
jBtZoomOut.setEnabled(true);
}
// System.out.println("i: "+i+"Zoom is: "+zoomPercents[0]);
jTfZoom.setText(zoomPercents[0] + "%");
scale = zoomPercents[0] / 100D;
}
setAppropriateSize();
m_gp.repaint();
m_gp.invalidate();
m_js.revalidate();
}
});
jBtZoomOut.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ae) {
int i = 0, s = (int) (scale * 100);
if (s < 300) {
i = (int) Math.ceil(s / 25D);
} else if (s < 700) {
i = 6 + (int) Math.ceil(s / 50D);
} else {
i = 13 + (int) Math.ceil(s / 100D);
}
if (s <= 10) {
JButton b = (JButton) ae.getSource();
b.setEnabled(false);
} else if (s < 999) {
if (i <= 1) {
JButton b = (JButton) ae.getSource();
b.setEnabled(false);
}
// System.out.println("i: "+i+"Zoom is: "+zoomPercents[i-1]);
jTfZoom.setText(zoomPercents[i - 1] + "%");
scale = zoomPercents[i - 1] / 100D;
} else {
if (!jBtZoomIn.isEnabled()) {
jBtZoomIn.setEnabled(true);
}
// System.out.println("i: "+i+"Zoom is: "+zoomPercents[22]);
jTfZoom.setText(zoomPercents[22] + "%");
scale = zoomPercents[22] / 100D;
}
setAppropriateSize();
m_gp.repaint();
m_gp.invalidate();
m_js.revalidate();
}
});
// This button pops out the extra controls
JButton jBtExtraControls = new JButton();
tempURL = ClassLoader.getSystemResource(ICONPATH + "extra.gif");
if (tempURL != null) {
jBtExtraControls.setIcon(new ImageIcon(tempURL));
} else {
System.err.println(ICONPATH
+ "extra.gif not found for weka.gui.graphvisualizer.Graph");
}
jBtExtraControls.setToolTipText("Show/Hide extra controls");
final JCheckBox jCbCustomNodeSize = new JCheckBox("Custom Node Size");
final JLabel jLbNodeWidth = new JLabel("Width");
final JLabel jLbNodeHeight = new JLabel("Height");
jTfNodeWidth.setHorizontalAlignment(JTextField.CENTER);
jTfNodeWidth.setText("" + nodeWidth);
jTfNodeHeight.setHorizontalAlignment(JTextField.CENTER);
jTfNodeHeight.setText("" + nodeHeight);
jLbNodeWidth.setEnabled(false);
jTfNodeWidth.setEnabled(false);
jLbNodeHeight.setEnabled(false);
jTfNodeHeight.setEnabled(false);
jCbCustomNodeSize.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ae) {
if (((JCheckBox) ae.getSource()).isSelected()) {
jLbNodeWidth.setEnabled(true);
jTfNodeWidth.setEnabled(true);
jLbNodeHeight.setEnabled(true);
jTfNodeHeight.setEnabled(true);
} else {
jLbNodeWidth.setEnabled(false);
jTfNodeWidth.setEnabled(false);
jLbNodeHeight.setEnabled(false);
jTfNodeHeight.setEnabled(false);
setAppropriateNodeSize();
}
}
});
jBtLayout = new JButton("Layout Graph");
jBtLayout.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ae) {
int tmpW, tmpH;
if (jCbCustomNodeSize.isSelected()) {
try {
tmpW = Integer.parseInt(jTfNodeWidth.getText());
} catch (NumberFormatException ne) {
JOptionPane.showMessageDialog(GraphVisualizer.this.getParent(),
"Invalid integer entered for node width.", "Error",
JOptionPane.ERROR_MESSAGE);
tmpW = nodeWidth;
jTfNodeWidth.setText("" + nodeWidth);
}
try {
tmpH = Integer.parseInt(jTfNodeHeight.getText());
} catch (NumberFormatException ne) {
JOptionPane.showMessageDialog(GraphVisualizer.this.getParent(),
"Invalid integer entered for node height.", "Error",
JOptionPane.ERROR_MESSAGE);
tmpH = nodeHeight;
jTfNodeWidth.setText("" + nodeHeight);
}
if (tmpW != nodeWidth || tmpH != nodeHeight) {
nodeWidth = tmpW;
paddedNodeWidth = nodeWidth + 8;
nodeHeight = tmpH;
}
}
JButton bt = (JButton) ae.getSource();
bt.setEnabled(false);
m_le.setNodeSize(paddedNodeWidth, nodeHeight);
m_le.layoutGraph();
}
});
GridBagConstraints gbc = new GridBagConstraints();
final JPanel p = new JPanel(new GridBagLayout());
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.anchor = GridBagConstraints.NORTHWEST;
gbc.fill = GridBagConstraints.NONE;
p.add(m_le.getControlPanel(), gbc);
gbc.gridwidth = 1;
gbc.insets = new Insets(8, 0, 0, 0);
gbc.anchor = GridBagConstraints.NORTHWEST;
gbc.gridwidth = GridBagConstraints.REMAINDER;
p.add(jCbCustomNodeSize, gbc);
gbc.insets = new Insets(0, 0, 0, 0);
gbc.gridwidth = GridBagConstraints.REMAINDER;
Container c = new Container();
c.setLayout(new GridBagLayout());
gbc.gridwidth = GridBagConstraints.RELATIVE;
c.add(jLbNodeWidth, gbc);
gbc.gridwidth = GridBagConstraints.REMAINDER;
c.add(jTfNodeWidth, gbc);
gbc.gridwidth = GridBagConstraints.RELATIVE;
c.add(jLbNodeHeight, gbc);
gbc.gridwidth = GridBagConstraints.REMAINDER;
c.add(jTfNodeHeight, gbc);
gbc.fill = GridBagConstraints.HORIZONTAL;
p.add(c, gbc);
gbc.anchor = GridBagConstraints.NORTHWEST;
gbc.insets = new Insets(8, 0, 0, 0);
gbc.fill = GridBagConstraints.HORIZONTAL;
p.add(jBtLayout, gbc);
gbc.fill = GridBagConstraints.NONE;
p.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createTitledBorder("ExtraControls"),
BorderFactory.createEmptyBorder(4, 4, 4, 4)));
p.setPreferredSize(new Dimension(0, 0));
final JToolBar jTbTools = new JToolBar();
jTbTools.setFloatable(false);
jTbTools.setLayout(new GridBagLayout());
gbc.anchor = GridBagConstraints.NORTHWEST;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.insets = new Insets(0, 0, 0, 0);
jTbTools.add(p, gbc);
gbc.gridwidth = 1;
jTbTools.add(m_jBtSave, gbc);
jTbTools.addSeparator(new Dimension(2, 2));
jTbTools.add(jBtZoomIn, gbc);
gbc.fill = GridBagConstraints.VERTICAL;
gbc.weighty = 1;
JPanel p2 = new JPanel(new BorderLayout());
p2.setPreferredSize(jTfZoom.getPreferredSize());
p2.setMinimumSize(jTfZoom.getPreferredSize());
p2.add(jTfZoom, BorderLayout.CENTER);
jTbTools.add(p2, gbc);
gbc.weighty = 0;
gbc.fill = GridBagConstraints.NONE;
jTbTools.add(jBtZoomOut, gbc);
jTbTools.addSeparator(new Dimension(2, 2));
jTbTools.add(jBtExtraControls, gbc);
jTbTools.addSeparator(new Dimension(4, 2));
gbc.weightx = 1;
gbc.fill = GridBagConstraints.BOTH;
jTbTools.add(m_le.getProgressBar(), gbc);
jBtExtraControls.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ae) {
Dimension d = p.getPreferredSize();
if (d.width == 0 || d.height == 0) {
LayoutManager lm = p.getLayout();
Dimension d2 = lm.preferredLayoutSize(p);
p.setPreferredSize(d2);
jTbTools.revalidate();
/*
* // this piece of code adds in an animation // for popping out the
* extra controls panel Thread th = new Thread() { int h = 0, w = 0;
* LayoutManager lm = p.getLayout(); Dimension d2 =
* lm.preferredLayoutSize(p);
*
* int tow = (int)d2.getWidth(), toh = (int)d2.getHeight(); //toh =
* (int)d2.getHeight(); //tow = (int)d2.getWidth();
*
* public void run() { while(htoh || w>tow) { if((h-10)>toh) h -= 10;
* else if(h>toh) h = toh; if((w-10)>tow) w -= 10; else if(w>tow) w =
* tow;
*
* p.setPreferredSize(new Dimension(w, h)); //p.invalidate();
* jTbTools.revalidate(); //paint(Temp4.this.getGraphics()); try
* {this.sleep(30);} catch(InterruptedException ie)
* {ie.printStackTrace(); break;} } p.setPreferredSize(new
* Dimension(tow,toh)); jTbTools.revalidate(); } }; th.start();
*/
}
}
});
this.setLayout(new BorderLayout());
this.add(jTbTools, BorderLayout.NORTH);
this.add(m_js, BorderLayout.CENTER);
}
/**
* This method sets the node size that is appropriate considering the maximum
* label size that is present. It is used internally when custom node size
* checkbox is unchecked.
*/
protected void setAppropriateNodeSize() {
int strWidth;
if (maxStringWidth == 0) {
for (int i = 0; i < m_nodes.size(); i++) {
strWidth = fm.stringWidth(m_nodes.get(i).lbl);
if (strWidth > maxStringWidth) {
maxStringWidth = strWidth;
}
}
}
nodeWidth = maxStringWidth + 4;
paddedNodeWidth = nodeWidth + 8;
jTfNodeWidth.setText("" + nodeWidth);
nodeHeight = 2 * fm.getHeight();
jTfNodeHeight.setText("" + nodeHeight);
}
/**
* Sets the preferred size for m_gp GraphPanel to the minimum size that is
* neccessary to display the graph.
*/
protected void setAppropriateSize() {
int maxX = 0, maxY = 0;
m_gp.setScale(scale, scale);
for (int i = 0; i < m_nodes.size(); i++) {
GraphNode n = m_nodes.get(i);
if (maxX < n.x) {
maxX = n.x;
}
if (maxY < n.y) {
maxY = n.y;
}
}
// System.out.println("Scale: "+scale+" paddedWidth: "+paddedNodeWidth+
// " nodeHeight: "+nodeHeight+"\nmaxX: "+maxX+" maxY: "+
// maxY+" final: "+(int)((maxX+paddedNodeWidth+2)*scale)+
// ","+(int)((maxY+nodeHeight+2)*scale) );
m_gp.setPreferredSize(new Dimension(
(int) ((maxX + paddedNodeWidth + 2) * scale),
(int) ((maxY + nodeHeight + 2) * scale)));
// System.out.println("Size set to "+this.getPreferredSize());
}
/**
* This method is an implementation for LayoutCompleteEventListener class. It
* sets the size appropriate for m_gp GraphPanel and and revalidates it's
* container JScrollPane once a LayoutCompleteEvent is received from the
* LayoutEngine.
*/
@Override
public void layoutCompleted(LayoutCompleteEvent le) {
setAppropriateSize();
// m_gp.clearBuffer();
m_gp.invalidate();
m_js.revalidate();
m_gp.repaint();
jBtLayout.setEnabled(true);
}
/**
* This method lays out the graph by calling the LayoutEngine's layoutGraph()
* method. This method should be called to display the graph nicely, unless
* the input XMLBIF03 already contains some layout information (ie the x,y
* positions of nodes.
*/
public void layoutGraph() {
if (m_le != null) {
m_le.layoutGraph();
}
}
/*********************************************************
*
* BIF reader
* Reads a graph description in XMLBIF03 from a string
*
*********************************************************
*/
public void readBIF(String instring) throws BIFFormatException {
BIFParser bp = new BIFParser(instring, m_nodes, m_edges);
try {
graphID = bp.parse();
} catch (BIFFormatException bf) {
System.out.println("BIF format error");
bf.printStackTrace();
} catch (Exception ex) {
ex.printStackTrace();
return;
}
setAppropriateNodeSize();
if (m_le != null) {
m_le.setNodeSize(paddedNodeWidth, nodeHeight);
}
} // end readBIF1
/**
*
* BIF reader
* Reads a graph description in XMLBIF03 from an InputStrem
*
*
*/
public void readBIF(InputStream instream) throws BIFFormatException {
BIFParser bp = new BIFParser(instream, m_nodes, m_edges);
try {
graphID = bp.parse();
} catch (BIFFormatException bf) {
System.out.println("BIF format error");
bf.printStackTrace();
} catch (Exception ex) {
ex.printStackTrace();
return;
}
setAppropriateNodeSize();
if (m_le != null) {
m_le.setNodeSize(paddedNodeWidth, nodeHeight);
}
setAppropriateSize();
} // end readBIF2
/*********************************************************
*
* Dot reader
* Reads a graph description in DOT format from a string
*
*********************************************************
*/
public void readDOT(Reader input) {
DotParser dp = new DotParser(input, m_nodes, m_edges);
graphID = dp.parse();
setAppropriateNodeSize();
if (m_le != null) {
m_le.setNodeSize(paddedNodeWidth, nodeHeight);
jBtLayout.setEnabled(false);
layoutGraph();
}
}
/**
* The panel which contains the actual graph.
*/
private class GraphPanel extends PrintablePanel {
/** for serialization */
private static final long serialVersionUID = -3562813603236753173L;
public GraphPanel() {
super();
this.addMouseListener(new GraphVisualizerMouseListener());
this.addMouseMotionListener(new GraphVisualizerMouseMotionListener());
this.setToolTipText("");
}
@Override
public String getToolTipText(MouseEvent me) {
int x, y, nx, ny;
Rectangle r;
GraphNode n;
Dimension d = m_gp.getPreferredSize();
// System.out.println("Preferred Size: "+this.getPreferredSize()+
// " Actual Size: "+this.getSize());
x = y = nx = ny = 0;
if (d.width < m_gp.getWidth()) {
nx = (int) ((nx + m_gp.getWidth() / 2 - d.width / 2) / scale);
}
if (d.height < m_gp.getHeight()) {
ny = (int) ((ny + m_gp.getHeight() / 2 - d.height / 2) / scale);
}
r = new Rectangle(0, 0, (int) (paddedNodeWidth * scale),
(int) (nodeHeight * scale));
x += me.getX();
y += me.getY();
int i;
for (i = 0; i < m_nodes.size(); i++) {
n = m_nodes.get(i);
if (n.nodeType != NORMAL) {
return null;
}
r.x = (int) ((nx + n.x) * scale);
r.y = (int) ((ny + n.y) * scale);
if (r.contains(x, y)) {
if (n.probs == null) {
return n.lbl;
} else {
return n.lbl + " (click to view the probability dist. table)";
}
}
}
return null;
}
@Override
public void paintComponent(Graphics gr) {
Graphics2D g = (Graphics2D) gr;
RenderingHints rh = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
rh.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
g.setRenderingHints(rh);
g.scale(scale, scale);
Rectangle r = g.getClipBounds();
g.clearRect(r.x, r.y, r.width, r.height);
// g.setColor(this.getBackground());
// g.fillRect(0, 0, width+5, height+5);
int x = 0, y = 0;
Dimension d = this.getPreferredSize();
// System.out.println("Preferred Size: "+this.getPreferredSize()+
// " Actual Size: "+this.getSize());
// initializing x & y to display the graph in the middle
// if the display area is larger than the graph
if (d.width < this.getWidth()) {
x = (int) ((x + this.getWidth() / 2 - d.width / 2) / scale);
}
if (d.height < this.getHeight()) {
y = (int) ((y + this.getHeight() / 2 - d.height / 2) / scale);
}
for (int index = 0; index < m_nodes.size(); index++) {
GraphNode n = m_nodes.get(index);
if (n.nodeType == NORMAL) {
g.setColor(this.getBackground().darker().darker());
g.fillOval(x + n.x + paddedNodeWidth - nodeWidth
- (paddedNodeWidth - nodeWidth) / 2, y + n.y, nodeWidth, nodeHeight);
g.setColor(Color.white);
// g.setColor(Color.black);
// System.out.println("drawing "+
// ((GraphNode)m_nodes.get(index)).ID+
// " at "+" x: "+ (x+n.x+paddedNodeWidth/2-
// fm.stringWidth( ((GraphNode)m_nodes.get(index)).ID )/2)+
// " y: "+(y+n.y+nodeHeight/2+fm.getHeight()/2-2) );
// Draw the node's label if it can fit inside the node's current
// width otherwise display its ID or otherwise just display its
// idx in the FastVector (to distinguish it from others)
// if any can fit in node's current width
if (fm.stringWidth(n.lbl) <= nodeWidth) {
g.drawString(n.lbl,
x + n.x + paddedNodeWidth / 2 - fm.stringWidth(n.lbl) / 2, y
+ n.y + nodeHeight / 2 + fm.getHeight() / 2 - 2);
} else if (fm.stringWidth(n.ID) <= nodeWidth) {
g.drawString(n.ID,
x + n.x + paddedNodeWidth / 2 - fm.stringWidth(n.ID) / 2, y + n.y
+ nodeHeight / 2 + fm.getHeight() / 2 - 2);
} else if (fm.stringWidth(Integer.toString(index)) <= nodeWidth) {
g.drawString(Integer.toString(index), x + n.x + paddedNodeWidth / 2
- fm.stringWidth(Integer.toString(index)) / 2, y + n.y
+ nodeHeight / 2 + fm.getHeight() / 2 - 2);
}
g.setColor(Color.black);
} else {
// g.draw( new java.awt.geom.QuadCurve2D.Double(n.x+paddedNodeWidth/2,
// n.y,
// n.x+paddedNodeWidth-nodeSize
// -(paddedNodeWidth-nodeSize)/2,
// n.y+nodeHeight/2,
// n.x+paddedNodeWidth/2, n.y+nodeHeight) );
g.drawLine(x + n.x + paddedNodeWidth / 2, y + n.y, x + n.x
+ paddedNodeWidth / 2, y + n.y + nodeHeight);
}
GraphNode n2;
int x1, y1, x2, y2;
// System.out.println("Drawing edges of "+n.lbl);
// Drawing all the edges coming out from the node,
// including reversed and double ones
if (n.edges != null) {
for (int[] edge : n.edges) {
if (edge[1] > 0) {
n2 = m_nodes.get(edge[0]); // m_nodes.get(k);
// System.out.println(" -->to "+n2.lbl);
x1 = n.x + paddedNodeWidth / 2;
y1 = n.y + nodeHeight;
x2 = n2.x + paddedNodeWidth / 2;
y2 = n2.y;
g.drawLine(x + x1, y + y1, x + x2, y + y2);
if (edge[1] == DIRECTED) {
if (n2.nodeType == GraphConstants.NORMAL) {
drawArrow(g, x + x1, y + y1, x + x2, y + y2);
}
} else if (edge[1] == REVERSED) {
if (n.nodeType == NORMAL) {
drawArrow(g, x + x2, y + y2, x + x1, y + y1);
}
} else if (edge[1] == DOUBLE) {
if (n.nodeType == NORMAL) {
drawArrow(g, x + x2, y + y2, x + x1, y + y1);
}
if (n2.nodeType == NORMAL) {
drawArrow(g, x + x1, y + y1, x + x2, y + y2);
}
}
}
}
}
}
}
/**
* This method draws an arrow on a line from (x1,y1) to (x2,y2). The arrow
* head is seated on (x2,y2) and is in the direction of the line. If the
* arrow is needed to be drawn in the opposite direction then simply swap
* the order of (x1, y1) and (x2, y2) when calling this function.
*/
protected void drawArrow(Graphics g, int x1, int y1, int x2, int y2) {
if (x1 == x2) {
if (y1 < y2) {
g.drawLine(x2, y2, x2 + 4, y2 - 8);
g.drawLine(x2, y2, x2 - 4, y2 - 8);
} else {
g.drawLine(x2, y2, x2 + 4, y2 + 8);
g.drawLine(x2, y2, x2 - 4, y2 + 8);
}
} else {
// theta=line's angle from base, beta=angle of arrow's side from line
double hyp = 0, base = 0, perp = 0, theta, beta;
int x3 = 0, y3 = 0;
if (x2 < x1) {
base = x1 - x2;
hyp = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
theta = Math.acos(base / hyp);
} else { // x1>x2 as we already checked x1==x2 before
base = x1 - x2;
hyp = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
theta = Math.acos(base / hyp);
}
beta = 30 * Math.PI / 180;
// System.out.println("Original base "+base+" perp "+perp+" hyp "+hyp+
// "\ntheta "+theta+" beta "+beta);
hyp = 8;
base = Math.cos(theta - beta) * hyp;
perp = Math.sin(theta - beta) * hyp;
x3 = (int) (x2 + base);
if (y1 < y2) {
y3 = (int) (y2 - perp);
} else {
y3 = (int) (y2 + perp);
}
// System.out.println("Drawing 1 from "+x2+","+y2+" to "+x3+","+y3+
// " x1,y1 is "+x1+","+y1+" base "+base+
// " perp "+perp+" cos(theta-beta) "+
// Math.cos(theta-beta));
g.drawLine(x2, y2, x3, y3);
base = Math.cos(theta + beta) * hyp;
perp = Math.sin(theta + beta) * hyp;
x3 = (int) (x2 + base);
if (y1 < y2) {
y3 = (int) (y2 - perp);
} else {
y3 = (int) (y2 + perp);
}
// System.out.println("Drawing 2 from "+x2+","+y2+" to "+x3+","+y3+
// " x1,y1 is "+x1+","+y1+" base "+base+
// " perp "+perp);
g.drawLine(x2, y2, x3, y3);
}
}
/**
* This method highlights a given node and all its children and the edges
* coming out of it.
*/
public void highLight(GraphNode n) {
Graphics2D g = (Graphics2D) this.getGraphics();
RenderingHints rh = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
rh.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
g.setRenderingHints(rh);
g.setPaintMode();
g.scale(scale, scale);
int x = 0, y = 0;
Dimension d = this.getPreferredSize();
// System.out.println("Preferred Size: "+this.getPreferredSize()+
// " Actual Size: "+this.getSize());
// initializing x & y to display the graph in the middle
// if the display area is larger than the graph
if (d.width < this.getWidth()) {
x = (int) ((x + this.getWidth() / 2 - d.width / 2) / scale);
}
if (d.height < this.getHeight()) {
y = (int) ((y + this.getHeight() / 2 - d.height / 2) / scale);
}
// if the node is of type NORMAL only then highlight
if (n.nodeType == NORMAL) {
g.setXORMode(Color.green); // g.setColor(Color.green);
g.fillOval(x + n.x + paddedNodeWidth - nodeWidth
- (paddedNodeWidth - nodeWidth) / 2, y + n.y, nodeWidth, nodeHeight);
g.setXORMode(Color.red);
// Draw the node's label if it can fit inside the node's current
// width otherwise display its ID or otherwise just display its
// idx in the FastVector (to distinguish it from others)
// if any can fit in node's current width
if (fm.stringWidth(n.lbl) <= nodeWidth) {
g.drawString(n.lbl,
x + n.x + paddedNodeWidth / 2 - fm.stringWidth(n.lbl) / 2, y + n.y
+ nodeHeight / 2 + fm.getHeight() / 2 - 2);
} else if (fm.stringWidth(n.ID) <= nodeWidth) {
g.drawString(n.ID,
x + n.x + paddedNodeWidth / 2 - fm.stringWidth(n.ID) / 2, y + n.y
+ nodeHeight / 2 + fm.getHeight() / 2 - 2);
} else if (fm.stringWidth(Integer.toString(m_nodes.indexOf(n))) <= nodeWidth) {
g.drawString(
Integer.toString(m_nodes.indexOf(n)),
x + n.x + paddedNodeWidth / 2
- fm.stringWidth(Integer.toString(m_nodes.indexOf(n))) / 2, y
+ n.y + nodeHeight / 2 + fm.getHeight() / 2 - 2);
}
g.setXORMode(Color.green);
GraphNode n2;
int x1, y1, x2, y2;
// System.out.println("Drawing edges of "+n.lbl);
if (n.edges != null) {
// Drawing all the edges from and upward ones coming to the node
for (int[] edge2 : n.edges) {
if (edge2[1] == DIRECTED || edge2[1] == DOUBLE) {
n2 = m_nodes.get(edge2[0]); // m_nodes.get(k);
// System.out.println(" -->to "+n2.lbl);
x1 = n.x + paddedNodeWidth / 2;
y1 = n.y + nodeHeight;
x2 = n2.x + paddedNodeWidth / 2;
y2 = n2.y;
g.drawLine(x + x1, y + y1, x + x2, y + y2);
if (edge2[1] == DIRECTED) {
if (n2.nodeType == GraphConstants.NORMAL) {
drawArrow(g, x + x1, y + y1, x + x2, y + y2);
}
} else if (edge2[1] == DOUBLE) {
if (n.nodeType == NORMAL) {
drawArrow(g, x + x2, y + y2, x + x1, y + y1);
}
if (n2.nodeType == NORMAL) {
drawArrow(g, x + x1, y + y1, x + x2, y + y2);
}
}
if (n2.nodeType == NORMAL) {
g.fillOval(x + n2.x + paddedNodeWidth - nodeWidth
- (paddedNodeWidth - nodeWidth) / 2, y + n2.y, nodeWidth,
nodeHeight);
}
// If n2 is not of NORMAL type
// then carry on drawing all the edges and add all the
// dummy nodes encountered in a Vector until no
// more dummy nodes are found and all the child nodes(node n2)
// are of type normal
java.util.Vector t = new java.util.Vector();
while (n2.nodeType != NORMAL || t.size() > 0) { // n2.dummy==true)
// {
// System.out.println("in while processing "+n2.ID);
if (t.size() > 0) {
n2 = t.get(0);
t.removeElementAt(0);
}
if (n2.nodeType != NORMAL) {
g.drawLine(x + n2.x + paddedNodeWidth / 2, y + n2.y, x + n2.x
+ paddedNodeWidth / 2, y + n2.y + nodeHeight);
x1 = n2.x + paddedNodeWidth / 2;
y1 = n2.y + nodeHeight;
// System.out.println("Drawing from "+n2.lbl);
for (int[] edge : n2.edges) {
// System.out.println(" to "+n2.lbl+", "+
// graphMatrix[tmpIndex][m]);
if (edge[1] > 0) {
GraphNode n3 = m_nodes.get(edge[0]); // m_nodes.get(m);
g.drawLine(x + x1, y + y1,
x + n3.x + paddedNodeWidth / 2, y + n3.y);
if (n3.nodeType == NORMAL) { // !n2.dummy)
g.fillOval(x + n3.x + paddedNodeWidth - nodeWidth
- (paddedNodeWidth - nodeWidth) / 2, y + n3.y,
nodeWidth, nodeHeight);
drawArrow(g, x + x1, y + y1, x + n3.x + paddedNodeWidth
/ 2, y + n3.y);
}
// if(n3.nodeType!=n3.NORMAL)
t.addElement(n3);
// break;
}
}
}
}
} else if (edge2[1] == -REVERSED || edge2[1] == -DOUBLE) {
// Drawing all the reversed and double edges which are going
// upwards in the drawing.
n2 = m_nodes.get(edge2[0]); // m_nodes.get(k);
// System.out.println(" -->to "+n2.lbl);
x1 = n.x + paddedNodeWidth / 2;
y1 = n.y;
x2 = n2.x + paddedNodeWidth / 2;
y2 = n2.y + nodeHeight;
g.drawLine(x + x1, y + y1, x + x2, y + y2);
if (edge2[1] == -DOUBLE) {
drawArrow(g, x + x2, y + y2, x + x1, y + y1);
if (n2.nodeType != SINGULAR_DUMMY) {
drawArrow(g, x + x1, y + y1, x + x2, y + y2);
}
}
while (n2.nodeType != NORMAL) { // n2.dummy==true) {
g.drawLine(x + n2.x + paddedNodeWidth / 2, y + n2.y
+ nodeHeight, x + n2.x + paddedNodeWidth / 2, y + n2.y);
x1 = n2.x + paddedNodeWidth / 2;
y1 = n2.y;
for (int[] edge : n2.edges) {
if (edge[1] < 0) {
n2 = m_nodes.get(edge[0]); // m_nodes.get(m);
g.drawLine(x + x1, y + y1, x + n2.x + paddedNodeWidth / 2,
y + n2.y + nodeHeight);
if (n2.nodeType != SINGULAR_DUMMY) {
drawArrow(g, x + x1, y + y1, x + n2.x + paddedNodeWidth
/ 2, y + n2.y + nodeHeight);
}
break;
}
}
}
}
}
}
}
}
}
/**
* Table Model for the Table that shows the probability distribution for a
* node
*/
private class GraphVisualizerTableModel extends AbstractTableModel {
/** for serialization */
private static final long serialVersionUID = -4789813491347366596L;
final String[] columnNames;
final double[][] data;
public GraphVisualizerTableModel(double[][] d, String[] c) {
data = d;
columnNames = c;
}
@Override
public int getColumnCount() {
return columnNames.length;
}
@Override
public int getRowCount() {
return data.length;
}
@Override
public String getColumnName(int col) {
return columnNames[col];
}
@Override
public Object getValueAt(int row, int col) {
return new Double(data[row][col]);
}
/*
* JTable uses this method to determine the default renderer/ editor for
* each cell.
*/
@Override
public Class> getColumnClass(int c) {
return getValueAt(0, c).getClass();
}
/*
* Implemented this to make sure the table is uneditable.
*/
@Override
public boolean isCellEditable(int row, int col) {
return false;
}
}
/**
* Listener class for processing mouseClicked
*/
private class GraphVisualizerMouseListener extends MouseAdapter {
int x, y, nx, ny;
Rectangle r;
/**
* If the mouse is clicked on a node then this method displays a dialog box
* with the probability distribution table for that node IF it exists
*/
@Override
public void mouseClicked(MouseEvent me) {
GraphNode n;
Dimension d = m_gp.getPreferredSize();
// System.out.println("Preferred Size: "+this.getPreferredSize()+
// " Actual Size: "+this.getSize());
x = y = nx = ny = 0;
if (d.width < m_gp.getWidth()) {
nx = (int) ((nx + m_gp.getWidth() / 2 - d.width / 2) / scale);
}
if (d.height < m_gp.getHeight()) {
ny = (int) ((ny + m_gp.getHeight() / 2 - d.height / 2) / scale);
}
r = new Rectangle(0, 0, (int) (paddedNodeWidth * scale),
(int) (nodeHeight * scale));
x += me.getX();
y += me.getY();
int i;
for (i = 0; i < m_nodes.size(); i++) {
n = m_nodes.get(i);
r.x = (int) ((nx + n.x) * scale);
r.y = (int) ((ny + n.y) * scale);
if (r.contains(x, y)) {
if (n.probs == null) {
return;
}
int noOfPrntsOutcomes = 1;
if (n.prnts != null) {
for (int prnt : n.prnts) {
GraphNode n2 = m_nodes.get(prnt);
noOfPrntsOutcomes *= n2.outcomes.length;
}
if (noOfPrntsOutcomes > 511) {
System.err.println("Too many outcomes of parents ("
+ noOfPrntsOutcomes + ") can't display probabilities");
return;
}
}
GraphVisualizerTableModel tm = new GraphVisualizerTableModel(n.probs,
n.outcomes);
JTable jTblProbs = new JTable(tm); // JTable(probabilities,
// (Object[])n.outcomes);
JScrollPane js = new JScrollPane(jTblProbs);
if (n.prnts != null) {
GridBagConstraints gbc = new GridBagConstraints();
JPanel jPlRowHeader = new JPanel(new GridBagLayout());
// indices of the parent nodes in the Vector
int[] idx = new int[n.prnts.length];
// max length of values of each parent
int[] lengths = new int[n.prnts.length];
// System.out.println("n.probs.length "+n.probs.length+
// " should be "+noOfPrntsOutcomes);
// System.out.println("n.probs[0].length "+n.probs[0].length+
// " should be "+n.outcomes.length);
// System.out.println("probabilities are: ");
// for(int j=0; j= 0; k--) {
n2 = m_nodes.get(n.prnts[k]);
if (idx[k] == n2.outcomes.length - 1 && k != 0) {
idx[k] = 0;
continue;
} else {
idx[k]++;
break;
}
}
n2 = m_nodes.get(n.prnts[0]);
if (idx[0] == n2.outcomes.length) {
JLabel lb = (JLabel) jPlRowHeader.getComponent(addNum - 1);
jPlRowHeader.remove(addNum - 1);
lb.setPreferredSize(new Dimension(lb.getPreferredSize().width,
jTblProbs.getRowHeight()));
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.weighty = 1;
jPlRowHeader.add(lb, gbc);
gbc.weighty = 0;
break;
}
}
gbc.gridwidth = 1;
// The following panel contains the names of the parents
// and is displayed above the row names to identify
// which value belongs to which parent
JPanel jPlRowNames = new JPanel(new GridBagLayout());
for (int j = 0; j < n.prnts.length; j++) {
JLabel lb2;
JLabel lb1 = new JLabel(m_nodes.get(n.prnts[j]).lbl);
lb1.setBorder(BorderFactory.createEmptyBorder(1, 2, 1, 1));
Dimension tempd = lb1.getPreferredSize();
// System.out.println("lengths[j]: "+lengths[j]+
// " tempd.width: "+tempd.width);
if (tempd.width < lengths[j]) {
lb1.setPreferredSize(new Dimension(lengths[j], tempd.height));
lb1.setHorizontalAlignment(JLabel.CENTER);
lb1.setMinimumSize(new Dimension(lengths[j], tempd.height));
} else if (tempd.width > lengths[j]) {
lb2 = (JLabel) jPlRowHeader.getComponent(j);
lb2.setPreferredSize(new Dimension(tempd.width, lb2
.getPreferredSize().height));
}
jPlRowNames.add(lb1, gbc);
// System.out.println("After adding "+lb1.getPreferredSize());
}
js.setRowHeaderView(jPlRowHeader);
js.setCorner(JScrollPane.UPPER_LEFT_CORNER, jPlRowNames);
}
JDialog jd = new JDialog(
(Frame) GraphVisualizer.this.getTopLevelAncestor(),
"Probability Distribution Table For " + n.lbl,
ModalityType.DOCUMENT_MODAL);
/*jd.setLocation(
GraphVisualizer.this.getLocation().x
+ GraphVisualizer.this.getWidth() / 2 - 250,
GraphVisualizer.this.getLocation().y
+ GraphVisualizer.this.getHeight() / 2 - 200);*/
jd.getContentPane().setLayout(new BorderLayout());
jd.getContentPane().add(js, BorderLayout.CENTER);
jd.pack();
jd.setSize(450, 350);
jd.setLocationRelativeTo(SwingUtilities.getWindowAncestor(GraphVisualizer.this));
jd.setVisible(true);
return;
}
}
}
}
/**
* private class for handling mouseMoved events to highlight nodes if the the
* mouse is moved on one
*/
private class GraphVisualizerMouseMotionListener extends MouseMotionAdapter {
int x, y, nx, ny;
Rectangle r;
GraphNode lastNode;
@Override
public void mouseMoved(MouseEvent me) {
GraphNode n;
Dimension d = m_gp.getPreferredSize();
// System.out.println("Preferred Size: "+this.getPreferredSize()+
// " Actual Size: "+this.getSize());
x = y = nx = ny = 0;
if (d.width < m_gp.getWidth()) {
nx = (int) ((nx + m_gp.getWidth() / 2 - d.width / 2) / scale);
}
if (d.height < m_gp.getHeight()) {
ny = (int) ((ny + m_gp.getHeight() / 2 - d.height / 2) / scale);
}
r = new Rectangle(0, 0, (int) (paddedNodeWidth * scale),
(int) (nodeHeight * scale));
x += me.getX();
y += me.getY();
int i;
for (i = 0; i < m_nodes.size(); i++) {
n = m_nodes.get(i);
r.x = (int) ((nx + n.x) * scale);
r.y = (int) ((ny + n.y) * scale);
if (r.contains(x, y)) {
if (n != lastNode) {
m_gp.highLight(n);
if (lastNode != null) {
m_gp.highLight(lastNode);
}
lastNode = n; // lastIndex = i;
}
break;
}
}
if (i == m_nodes.size() && lastNode != null) {
m_gp.repaint();
// m_gp.highLight(lastNode);
lastNode = null;
}
}
}
/**
* Main method to load a text file with the description of a graph from the
* command line
*/
public static void main(String[] args) {
weka.core.logging.Logger.log(weka.core.logging.Logger.Level.INFO,
"Logging started");
JFrame jf = new JFrame("Graph Visualizer");
GraphVisualizer g = new GraphVisualizer();
try {
if (args[0].endsWith(".xml")) {
// StringBuffer sb = new StringBuffer();
// FileReader infile = new FileReader(args[0]);
// int i;
// while( (i=infile.read())!=-1) {
// sb.append((char)i);
// }
// System.out.println(sb.toString());
// g.readBIF(sb.toString() );
g.readBIF(new FileInputStream(args[0]));
} else {
// BufferedReader infile=new BufferedReader();
g.readDOT(new FileReader(args[0])); // infile);
}
} catch (IOException ex) {
ex.printStackTrace();
} catch (BIFFormatException bf) {
bf.printStackTrace();
System.exit(-1);
}
jf.getContentPane().add(g);
// RepaintManager.currentManager(jf.getRootPane()).setDoubleBufferingEnabled(false);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setSize(800, 600);
// jf.pack();
jf.setVisible(true);
}
}