/*
* (C) Copyright IBM Corp. 2009
*
* LICENSE: Eclipse Public License v1.0
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.ibm.gaiandb.apps.dashboard;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Map.Entry;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import com.ibm.gaiandb.tools.NetworkLinksAnalyser;
import prefuse.Constants;
import prefuse.Display;
import prefuse.Visualization;
import prefuse.action.Action;
import prefuse.action.ActionList;
import prefuse.action.RepaintAction;
import prefuse.action.assignment.ColorAction;
import prefuse.action.assignment.ShapeAction;
import prefuse.action.assignment.SizeAction;
import prefuse.action.layout.graph.ForceDirectedLayout;
import prefuse.activity.Activity;
import prefuse.controls.Control;
import prefuse.controls.ControlAdapter;
import prefuse.controls.DragControl;
import prefuse.controls.FocusControl;
import prefuse.controls.PanControl;
import prefuse.controls.WheelZoomControl;
import prefuse.controls.ZoomControl;
import prefuse.controls.ZoomToFitControl;
import prefuse.data.Graph;
import prefuse.data.Schema;
import prefuse.data.Table;
import prefuse.data.tuple.TupleSet;
import prefuse.render.DefaultRendererFactory;
import prefuse.render.EdgeRenderer;
import prefuse.render.LabelRenderer;
import prefuse.render.RendererFactory;
import prefuse.render.ShapeRenderer;
import prefuse.util.GraphicsLib;
import prefuse.util.display.DisplayLib;
import prefuse.util.force.DragForce;
import prefuse.util.force.ForceSimulator;
import prefuse.util.force.NBodyForce;
import prefuse.util.force.SpringForce;
import prefuse.visual.EdgeItem;
import prefuse.visual.VisualItem;
public class TopologyGraph extends Display {
// Use PROPRIETARY notice if class contains a main() method, otherwise use COPYRIGHT notice.
public static final String COPYRIGHT_NOTICE = "(c) Copyright IBM Corp. 2009";
private static final long serialVersionUID = 4659721334875164242L;
private static final String GROUP = "data";
private static final String NODE_GROUP = GROUP + ".nodes";
private static final String EDGE_GROUP = GROUP + ".edges";
private static final String NODE_ID_FIELD = "node";
private static final String SOURCE_ID_FIELD = "source";
private static final String TARGET_ID_FIELD = "target";
private static final String NODE_NAME_FIELD = "name";
private static final Color TEXT_COLOR = Color.WHITE;
private static final Color DEFAULT_NODE_COLOR = Color.BLACK;
private static final Color EDGE_COLOR = Color.LIGHT_GRAY;
private static final Color SELECTED_EDGE_COLOR = Color.BLACK;
private static final int EDGE_THICKNESS = 1;
private static final int SPRING_LENGTH_MULTIPLIER = 100;
private NetworkLinksAnalyser nla = new NetworkLinksAnalyser();
// Visualisation structures for paths leading to the source data of a logical table
private boolean isQueryPathSet = false;
private ArrayList<ArrayList<String>> queryPaths = new ArrayList<ArrayList<String>>();
private int animationTick =0;
public static class Node implements Comparable<Node> {
private static final Map<String, Node> INSTANCES = new HashMap<String, Node>();
private final String name;
private Color color = null;
private Node(String name) {
this.name = name;
}
public static synchronized Node getInstance(String name) {
Node instance = INSTANCES.get(name);
if (null == instance) {
instance = new Node(name);
INSTANCES.put(name, instance);
}
return instance;
}
public static Set<String> getAllNodeNames() {
return INSTANCES.keySet();
}
public static void removeInstance(String name) {
INSTANCES.remove(name);
}
public String getName() {
return name;
}
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Node)) {
return false;
}
return name.equals(((Node)o).name);
}
public int hashCode() {
return name.hashCode();
}
public int compareTo(Node o) {
return name.compareToIgnoreCase(o.name);
}
public String toString() {
return name;
}
}
public static class Edge implements Comparable<Edge> {
public final Node source;
public final Node target;
public Edge(String sourceName, String targetName) {
Node source = Node.getInstance(sourceName);
Node target = Node.getInstance(targetName);
int comparison = source.compareTo(target);
if (comparison <= 0) {
this.source = source;
this.target = target;
}
else {
this.source = target;
this.target = source;
}
}
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Edge)) {
return false;
}
Edge other = (Edge)o;
return (
source.equals(other.source) &&
target.equals(other.target)
);
}
public int hashCode() {
return source.hashCode() ^ target.hashCode();
}
public int compareTo(Edge o) {
int ret;
ret = source.compareTo(o.source);
if (0 != ret) {
return ret;
}
ret = target.compareTo(o.target);
if (0 != ret) {
return ret;
}
return 0;
}
public String toString() {
return source.name + " <-> " + target.name;
}
}
public Graph data;
public Table nodeTable;
public Table edgeTable;
private Map<Node, Integer> nodes = new TreeMap<Node, Integer>();
private Map<Edge, Integer> edges = new TreeMap<Edge, Integer>();
private Map<Node, Color> newNodeColors = new TreeMap<Node, Color>();
private Set<Edge> newEdges = new TreeSet<Edge>();
private MonitorInfo monitor = null;
// private JPopupMenu popup = new JPopupMenu("POPUP!");
private String localNode = null;
private String currentHoverItemName = null;
private String lastClickedNode = null;
private RendererFactory rendererWithoutLabels;
private RendererFactory rendererWithLabels;
private boolean isBackgroundSelected = false;
public synchronized void recenter() {
if ( null == m_vis || isTranformInProgress() || null != currentHoverItemName || isBackgroundSelected ) return;
try {
long m_duration = 500;
int m_margin = 50;
Rectangle2D bounds = m_vis.getBounds( Visualization.ALL_ITEMS );
// System.out.println(
// "Display size: width: " + this.getWidth() + ", height: " + this.getHeight()
// "\nVis bounds: center: " + bounds.getCenterX() + ", " + bounds.getCenterY() +
// ", bounds X: " + bounds.getMinX() + ", " + bounds.getMaxX() +
// ", bounds Y: " + bounds.getMinY() + ", " + bounds.getMaxY()
// );
if ( bounds.getMinX() == bounds.getMaxX() || bounds.getMinY() == bounds.getMaxY())
return;
GraphicsLib.expand(bounds, m_margin + (int)(1/getScale()));
DisplayLib.fitViewToBounds(this, bounds, m_duration);
} catch( Exception e ) { System.out.println("Exception in recenter(): " + e); }
}
private SpringForce springForce = new SpringForce(
1e-5f, SpringForce.DEFAULT_SPRING_LENGTH);
public MonitorInfo getMonitor() {
return monitor;
}
public void setMonitor(MonitorInfo monitor) {
this.monitor = monitor;
}
public int getNodeCount() {
return null != data ? data.getNodeCount() : 0;
}
public String getLocalNode() {
return localNode;
}
public void setLocalNode(String localNode) {
this.localNode = localNode;
}
public String getCurrentItemName() {
return currentHoverItemName;
}
public String getAndUnsetLastClickedNode() {
String lcn = lastClickedNode;
lastClickedNode = null;
return lcn;
}
public void setNodeRenderer(boolean withLabels) {
if (withLabels) {
m_vis.setRendererFactory(rendererWithLabels);
}
else {
m_vis.setRendererFactory(rendererWithoutLabels);
}
}
private static final Object lock = new Object();
private static TopologyGraph graph = null;
public static TopologyGraph getSingleton() {
if ( null == graph ) synchronized (lock) { if ( null == graph ) graph = new TopologyGraph(); }
return graph;
}
private TopologyGraph() {
super(new Visualization());
String[] nodeColumns = { NODE_ID_FIELD, NODE_NAME_FIELD };
Class<?>[] nodeColumnTypes = { int.class, String.class };
nodeTable = new Schema(nodeColumns, nodeColumnTypes).instantiate();
data = new Graph(nodeTable, false, NODE_ID_FIELD, SOURCE_ID_FIELD, TARGET_ID_FIELD);
edgeTable = data.getEdgeTable();
m_vis.addGraph(GROUP, data);
ShapeRenderer nodeRendererWithoutLabels = new ShapeRenderer();
LabelRenderer nodeRendererWithLabels = new LabelRenderer(NODE_NAME_FIELD);
nodeRendererWithLabels.setRoundedCorner(10, 10);
EdgeRenderer edgeRenderer = new EdgeRenderer() {
public void render(Graphics2D g, VisualItem item) {
// System.out.println("Call stack: " + Util.getStackTraceDigest() + "\nTimeNow: " + System.currentTimeMillis());
EdgeItem edgeItem = (EdgeItem) item;
String src = edgeItem.getSourceItem().getString(NODE_NAME_FIELD);
String tgt = edgeItem.getTargetItem().getString(NODE_NAME_FIELD);
// String edgeString1 = src + " " + tgt, edgeString2 = tgt + " " + src;
//
// if ( !edgesRenderedAfterUpdate.contains(edgeString1) && !edgesRenderedAfterUpdate.contains(edgeString2) ) {
// edgesRenderedAfterUpdate.add(edgeString1);
// System.out.println("Rendering edge: " + edgeString1);
// }
if ( null != currentHoverItemName && (currentHoverItemName.equals( src ) || currentHoverItemName.equals( tgt )) ) {
item.setSize(EDGE_THICKNESS * 2);
item.setStrokeColor(SELECTED_EDGE_COLOR.getRGB());
} else if ( isQueryPathSet && edgeHighlighted( src, tgt ) ) {
item.setSize(EDGE_THICKNESS * 2);
item.setStrokeColor(Color.RED.getRGB());
} else if ( isQueryPathSet && edgeOnPath( src, tgt ) ) {
item.setSize(EDGE_THICKNESS * 2);
item.setStrokeColor( Color.BLUE.getRGB() );
} else {
item.setSize(EDGE_THICKNESS);
item.setStrokeColor(EDGE_COLOR.getRGB());
}
super.render(g, item);
}
private boolean edgeHighlighted(String src, String tgt) {
if ( 0 > animationTick ) return false; // disabled for now if = -1
synchronized ( queryPaths ) {
for ( ArrayList<String> path : queryPaths ) {
if ( path != null && ! path.isEmpty() ) {
int pathSize = path.size();
String tickSrc = path.get(animationTick % pathSize);
String tickTgt = path.get((animationTick + 1) % pathSize);
if ( src.equals(tickSrc) && tgt.equals(tickTgt) || tgt.equals(tickSrc) && src.equals(tickTgt))
return true;
}
}
}
return false;
}
private boolean edgeOnPath(String n1, String n2) {
synchronized ( queryPaths ) {
Iterator<ArrayList<String>> iter = queryPaths.iterator();
while ( iter.hasNext() ) {
ArrayList<String> path = (ArrayList<String>) iter.next();
// Check whether the egde(n1, n2) is one of the segments of this path (note that the path cannot loop)
for ( int i=0; i < path.size()-1 ; i++ ) {
String src = path.get(i), tgt = path.get(i+1);
if ( src.equals(n1) || src.equals(n2) )
if ( tgt.equals(n1) || tgt.equals(n2) ) return true; else break;
}
}
}
return false;
}
};
rendererWithoutLabels = new DefaultRendererFactory( nodeRendererWithoutLabels, edgeRenderer );
rendererWithLabels = new DefaultRendererFactory( nodeRendererWithLabels, edgeRenderer );
m_vis.setRendererFactory( rendererWithoutLabels );
// Set up the drawing actions.
ActionList draw = new ActionList();
draw.add(new ShapeAction(NODE_GROUP, Constants.SHAPE_ELLIPSE) {
public int getShape(VisualItem item) {
if (null != localNode && localNode.equals(item.getString(NODE_NAME_FIELD))) {
return Constants.SHAPE_STAR;
}
else {
return super.getShape(item);
}
}
});
draw.add(new SizeAction(NODE_GROUP) {
public double getSize(VisualItem item) {
if (null != localNode && localNode.equals(item.getString(NODE_NAME_FIELD))) {
return super.getSize(item) * 2;
}
else {
return super.getSize(item);
}
}
});
// draw.add(new ColorAction(NODE_GROUP, VisualItem.TEXTCOLOR, TEXT_COLOR.getRGB()));
draw.add(new ColorAction(EDGE_GROUP, VisualItem.STROKECOLOR, EDGE_COLOR.getRGB()));
draw.add(new RepaintAction());
m_vis.putAction("draw", draw);
Action selectNode = new ColorAction( NODE_GROUP, VisualItem.STROKECOLOR, DEFAULT_NODE_COLOR.getRGB()) {
public int getColor(VisualItem item) {
if ( !item.isInGroup(Visualization.FOCUS_ITEMS) ) return item.getFillColor();
return Color.CYAN.getRGB();
}
};
m_vis.putAction("selectNode", selectNode);
ActionList nodeColor = new ActionList();
nodeColor.add( new ColorAction(NODE_GROUP, VisualItem.FILLCOLOR, DEFAULT_NODE_COLOR.getRGB()) {
public int getColor(VisualItem item) {
if (null == monitor) return super.getColor(item);
String nodeName = item.getString(NODE_NAME_FIELD);
Color color = Node.getInstance(nodeName).getColor();
if (null == color) return super.getColor(item);
return color.getRGB();
}
});
nodeColor.add( new ColorAction(NODE_GROUP, VisualItem.TEXTCOLOR, TEXT_COLOR.getRGB()) {
public int getColor(VisualItem item) {
String nodeName = item.getString(NODE_NAME_FIELD);
Color color = Node.getInstance(nodeName).getColor();
if (null == color || !color.equals(Color.YELLOW) ) return Color.WHITE.getRGB();
return Color.BLACK.getRGB();
}
});
nodeColor.add( selectNode );
m_vis.putAction("nodeColor", nodeColor);
// Set up the positioning actions.
ForceSimulator forces = new ForceSimulator();
forces.addForce(new NBodyForce());
forces.addForce(springForce);
forces.addForce(new DragForce());
ActionList position = new ActionList(Activity.INFINITY);
position.add(new ForceDirectedLayout(GROUP, forces, false));
position.add(new RepaintAction());
m_vis.putAction("position", position);
// for ( String s : new String[] { "Kill", "Execute Last Query" } ) {
// JMenuItem jm = new JMenuItem(s);
//// jm.addActionListener(this);
// popup.add(jm);
// }
// Set up movement and zooming for windows and nodes.
addControlListener(new DragControl()); // { public void mouseClicked(MouseEvent e) { graph.requestFocus(); super.mouseClicked(e); } } );
addControlListener(new PanControl());
addControlListener(new ZoomControl());
addControlListener(new WheelZoomControl());
addControlListener(new FocusControl(1, "selectNode") {
public void mouseClicked(MouseEvent e) {
// System.out.println("button clicked: " + e.getButton() + ", LEFT=" + Control.LEFT_MOUSE_BUTTON +
// ", fo: " + graph.isFocusOwner() + ", fr: " + graph.isRequestFocusEnabled() + ", froot: " + graph.isFocusCycleRoot());
if ( !graph.isFocusOwner() ) graph.requestFocus();
else if ( MouseEvent.BUTTON1 == e.getButton() && !e.isControlDown() ) {
TupleSet ts = m_vis.getFocusGroup(Visualization.FOCUS_ITEMS);
ts.clear(); curFocus = null; m_vis.run(activity);
}
super.mouseClicked(e);
}
public void itemClicked(VisualItem item, MouseEvent e) {
super.itemClicked(item, e);
if ( !e.isControlDown() ) {
TupleSet ts = m_vis.getFocusGroup(Visualization.FOCUS_ITEMS);
ts.clear(); curFocus = null; m_vis.run(activity);
}
}
});
addControlListener(new ZoomToFitControl(Control.MIDDLE_MOUSE_BUTTON));
addControlListener(new ControlAdapter() {
public void itemEntered(VisualItem item, MouseEvent event) {
if (item.isInGroup(NODE_GROUP)) {
currentHoverItemName = item.getString(NODE_NAME_FIELD);
}
}
public void itemExited(VisualItem item, MouseEvent event) {
currentHoverItemName = null;
}
public void mousePressed( MouseEvent event) {
isBackgroundSelected = true;
}
public void mouseReleased( MouseEvent event) {
isBackgroundSelected = false;
}
public void itemClicked(VisualItem item, MouseEvent event) {
if (event.isShiftDown() && item.isInGroup(NODE_GROUP)) { //2 == event.getClickCount() ) {
// DRV - 22/10/2011 - commented out node clicking action. Security credentials are not to be entered this way anymore.
// this should be customer/policy-plugin specific now
// lastClickedNode = item.getString(NODE_NAME_FIELD);
return;
}
// if ( MouseEvent.BUTTON3 == event.getButton() ) {
//// System.out.println("comp: " + event.getComponent() + ", X: " + event.getX() + ", Y: " + event.getY() );
//// System.out.println("comp: " + event.getComponent() + ", X: " + event.getXOnScreen() + ", Y: " + event.getYOnScreen());
// popup.show(event.getComponent(), event.getX(), event.getY());
// }
}
public void keyTyped(KeyEvent event) {
if (event.isControlDown() && event.getKeyChar() == 1) { // == Ctrl-A => toggle select/unselect all nodes
TupleSet ts = m_vis.getFocusGroup(Visualization.FOCUS_ITEMS);
int tupleCount = ts.getTupleCount();
ts.clear();
if ( nodeTable.getRowCount() != tupleCount ) {
@SuppressWarnings("unchecked") // prefuse code is not generic
Iterator<VisualItem> it = m_vis.getGroup(NODE_GROUP).tuples();
while (it.hasNext()) ts.addTuple(it.next());
}
m_vis.run("selectNode");
}
}
});
}
public Set<String> getSelectedNodes() {
TupleSet ts = (TupleSet) m_vis.getFocusGroup(Visualization.FOCUS_ITEMS);
Set<String> nodes = new HashSet<String>();
@SuppressWarnings("unchecked") // prefuse code is not generic
Iterator<VisualItem> it = ts.tuples();
try {
while ( it.hasNext() )
nodes.add(it.next().getString(NODE_NAME_FIELD));
} catch (Exception e) {
return null;
}
return nodes;
}
public synchronized void update() {
synchronized (newEdges) {
m_vis.cancel("position");
processNewEdges();
if (0 == data.getNodeCount()) {
return;
}
springForce.setMaxValue(SpringForce.SPRING_LENGTH,
(float)(Math.log10(data.getNodeCount()) * SPRING_LENGTH_MULTIPLIER));
m_vis.run("position");
m_vis.run("draw");
m_vis.run("nodeColor");
}
}
public synchronized void updateValues() {
processNewNodeColors();
}
public void setNodeColor(String nodeName, Color color) {
if (null != color) {
synchronized (newNodeColors) {
newNodeColors.put(Node.getInstance(nodeName), color);
}
}
}
public void setEdge(String sourceName, String targetName) {
// System.out.println("Setting edge: " + sourceName + "->" + targetName);
synchronized (newEdges) {
if ( null != sourceName && null != targetName )
newEdges.add(new Edge(sourceName, targetName));
}
}
private void processNewNodeColors() {
for (Node node : nodes.keySet()) {
node.setColor(null);
}
for (Entry<Node, Color> nodeValue : newNodeColors.entrySet()) {
nodeValue.getKey().setColor(nodeValue.getValue());
}
newNodeColors.clear();
m_vis.run("nodeColor");
}
private void processNewEdges() {
boolean isGraphChanged = false;
Set<Node> newNodes = new HashSet<Node>();
for (Edge edge : newEdges) {
newNodes.add(edge.source);
newNodes.add(edge.target);
}
Iterator<Edge> edgeIterator = edges.keySet().iterator();
while (edgeIterator.hasNext()) {
Edge edge = edgeIterator.next();
if (!newEdges.contains(edge)) {
Integer row = edges.get(edge);
if (null != row) {
edgeTable.removeRow(row);
}
edgeIterator.remove();
}
}
Iterator<Node> nodeIterator = nodes.keySet().iterator();
while (nodeIterator.hasNext()) {
Node node = nodeIterator.next();
if (!newNodes.contains(node)) {
isGraphChanged = true;
nodeTable.removeRow(nodes.get(node));
nodeIterator.remove();
Node.removeInstance(node.name);
}
}
for (Edge edge : newEdges) {
if (!edges.containsKey(edge)) {
isGraphChanged = true;
Node source = Node.getInstance(edge.source.name);
Integer sourceId = nodes.get(edge.source);
if (null == sourceId) {
sourceId = nodeTable.addRow();
nodeTable.setInt(sourceId, NODE_ID_FIELD, sourceId);
nodeTable.setString(sourceId, NODE_NAME_FIELD, edge.source.name);
nodes.put(source, sourceId);
}
Node target = Node.getInstance(edge.target.name);
Integer targetId = nodes.get(edge.target);
if (null == targetId) {
targetId = nodeTable.addRow();
nodeTable.setInt(targetId, NODE_ID_FIELD, targetId);
nodeTable.setString(targetId, NODE_NAME_FIELD, edge.target.name);
nodes.put(target, targetId);
}
if (sourceId != targetId) {
Integer edgeId = edgeTable.addRow();
edgeTable.setInt(edgeId, SOURCE_ID_FIELD, sourceId);
edgeTable.setInt(edgeId, TARGET_ID_FIELD, targetId);
edges.put(edge, edgeId);
}
else {
edges.put(edge, null);
}
}
}
newEdges.clear();
if ( isGraphChanged ) {
nla.computeStats(edges.keySet());
hasChanged = true;
}
}
private boolean hasChanged = false;
boolean hasChanged() {
boolean hasChanged = this.hasChanged;
this.hasChanged = false;
return hasChanged;
}
Set<String> getAllNodes() {
// Set<String> nodeNames = new HashSet<String>();
// for ( Node node : nodes.keySet() ) nodeNames.add(node.getName());
// return nodeNames;
return Node.getAllNodeNames();
}
int getConnectivity( String node ) { return nla.getNumConnections(node); }
int getEccentricity( String node ) { return nla.getEccentricity(node); }
Set<String> getFurthestNodes( String node ) { return nla.getFurthestNodes(node); }
ArrayList<Integer> getStepCardinalities( String node ) { return nla.getStepCardinalities(node); }
int getDiameter() { return nla.getDiameter(); }
int getRadius() { return nla.getRadius(); }
String getNodesPerEccentricity() { return nla.getNodesPerEccentricity(); }
String getNodesPerConnectivity() { return nla.getNodesPerConnectivity(); }
public void highLightLinks(ArrayList<ArrayList<String>> qPaths, int animationTick) {
synchronized ( queryPaths ) { queryPaths.clear(); isQueryPathSet = !qPaths.isEmpty(); if ( isQueryPathSet ) queryPaths.addAll( qPaths ); }
// System.out.println("The updated Query Paths : " + queryPaths);
this.animationTick = animationTick;
// edgesRenderedAfterUpdate.clear();
}
// private Set<String> edgesRenderedAfterUpdate = new HashSet<String>();
}