package prefuse.action.filter;
import java.util.Iterator;
import prefuse.Constants;
import prefuse.Visualization;
import prefuse.action.GroupAction;
import prefuse.data.Graph;
import prefuse.data.Tree;
import prefuse.data.expression.Predicate;
import prefuse.util.PrefuseLib;
import prefuse.visual.EdgeItem;
import prefuse.visual.NodeItem;
import prefuse.visual.VisualItem;
import prefuse.visual.expression.InGroupPredicate;
/**
* <p>Filter Action that computes a fisheye degree-of-interest function over
* a tree structure (or the spanning tree of a graph structure). Visibility
* and DOI (degree-of-interest) values are set for the nodes in the
* structure. This function includes current focus nodes, and includes
* neighbors only in a limited window around these foci. The size of this
* window is determined by the distance value set for this action. All
* ancestors of a focus up to the root of the tree are considered foci as well.
* By convention, DOI values start at zero for focus nodes, with decreasing
* negative numbers for each hop away from a focus.</p>
*
* <p>This form of filtering was described by George Furnas as early as 1981.
* For more information about Furnas' fisheye view calculation and DOI values,
* take a look at G.W. Furnas, "The FISHEYE View: A New Look at Structured
* Files," Bell Laboratories Tech. Report, Murray Hill, New Jersey, 1981.
* Available online at <a href="http://citeseer.nj.nec.com/furnas81fisheye.html">
* http://citeseer.nj.nec.com/furnas81fisheye.html</a>.</p>
*
* @author <a href="http://jheer.org">jeffrey heer</a>
*/
public class FisheyeTreeFilter extends GroupAction {
private String m_sources;
private Predicate m_groupP;
private int m_threshold;
private NodeItem m_root;
private double m_divisor;
/**
* Create a new FisheyeTreeFilter that processes the given group.
* @param group the data group to process. This should resolve to
* a Graph instance, otherwise exceptions will result when this
* Action is run.
*/
public FisheyeTreeFilter(String group) {
this(group, 1);
}
/**
* Create a new FisheyeTreeFilter that processes the given group.
* @param group the data group to process. This should resolve to
* a Graph instance, otherwise exceptions will result when this
* Action is run.
* @param distance the graph distance threshold from high-interest
* nodes past which nodes will not be visible nor expanded.
*/
public FisheyeTreeFilter(String group, int distance) {
this(group, Visualization.FOCUS_ITEMS, distance);
}
/**
* Create a new FisheyeTreeFilter that processes the given group.
* @param group the data group to process. This should resolve to
* a Graph instance, otherwise exceptions will result when this
* Action is run.
* @param sources the group to use as source nodes, representing the
* nodes of highest degree-of-interest.
* @param distance the graph distance threshold from high-interest
* nodes past which nodes will not be visible nor expanded.
*/
public FisheyeTreeFilter(String group, String sources, int distance)
{
super(group);
m_sources = sources;
m_threshold = -distance;
m_groupP = new InGroupPredicate(
PrefuseLib.getGroupName(group, Graph.NODES));
}
/**
* Get the graph distance threshold used by this filter. This
* is the threshold for high-interest nodes, past which nodes will
* not be visible nor expanded.
* @return the graph distance threshold
*/
public int getDistance() {
return -m_threshold;
}
/**
* Set the graph distance threshold used by this filter. This
* is the threshold for high-interest nodes, past which nodes will
* not be visible nor expanded.
* @param distance the graph distance threshold to use
*/
public void setDistance(int distance) {
m_threshold = -distance;
}
/**
* Get the name of the group to use as source nodes for measuring
* graph distance. These form the roots from which the graph distance
* is measured.
* @return the source data group
*/
public String getSources() {
return m_sources;
}
/**
* Set the name of the group to use as source nodes for measuring
* graph distance. These form the roots from which the graph distance
* is measured.
* @param sources the source data group
*/
public void setSources(String sources) {
m_sources = sources;
}
/**
* @see prefuse.action.GroupAction#run(double)
*/
public void run(double frac) {
Tree tree = ((Graph)m_vis.getGroup(m_group)).getSpanningTree();
m_divisor = tree.getNodeCount();
m_root = (NodeItem)tree.getRoot();
// mark the items
Iterator items = m_vis.visibleItems(m_group);
while ( items.hasNext() ) {
VisualItem item = (VisualItem)items.next();
item.setDOI(Constants.MINIMUM_DOI);
item.setExpanded(false);
}
// compute the fisheye over nodes
Iterator iter = m_vis.items(m_sources, m_groupP);
while ( iter.hasNext() )
visitFocus((NodeItem)iter.next(), null);
visitFocus(m_root, null);
// mark unreached items
items = m_vis.visibleItems(m_group);
while ( items.hasNext() ) {
VisualItem item = (VisualItem)items.next();
if ( item.getDOI() == Constants.MINIMUM_DOI )
PrefuseLib.updateVisible(item, false);
}
}
/**
* Visit a focus node.
*/
private void visitFocus(NodeItem n, NodeItem c) {
if ( n.getDOI() <= -1 ) {
visit(n, c, 0, 0);
if ( m_threshold < 0 )
visitDescendants(n, c);
visitAncestors(n);
}
}
/**
* Visit a specific node and update its degree-of-interest.
*/
private void visit(NodeItem n, NodeItem c, int doi, int ldist) {
PrefuseLib.updateVisible(n, true);
double localDOI = -ldist / Math.min(1000.0, m_divisor);
n.setDOI(doi+localDOI);
if ( c != null ) {
EdgeItem e = (EdgeItem)c.getParentEdge();
e.setDOI(c.getDOI());
PrefuseLib.updateVisible(e, true);
}
}
/**
* Visit tree ancestors and their other descendants.
*/
private void visitAncestors(NodeItem n) {
if ( n == m_root ) return;
visitFocus((NodeItem)n.getParent(), n);
}
/**
* Traverse tree descendents.
*/
private void visitDescendants(NodeItem p, NodeItem skip) {
int lidx = ( skip == null ? 0 : p.getChildIndex(skip) );
Iterator children = p.children();
p.setExpanded(children.hasNext());
for ( int i=0; children.hasNext(); ++i ) {
NodeItem c = (NodeItem)children.next();
if ( c == skip ) { continue; }
int doi = (int)(p.getDOI()-1);
visit(c, c, doi, Math.abs(lidx-i));
if ( doi > m_threshold )
visitDescendants(c, null);
}
}
} // end of class FisheyeTreeFilter