Hiding Nodes in a Graph Hierarchy that is Managed by Class HierarchyManager, Part 2

Questions & Answers

Summary

Extending GraphHider to work with hierarchically organized graphs
For a better user experience, please go to the integrated documentation viewer to read this article.

Description

This article is a follow-up to How to Hide and Unhide Nodes Correctly and Hiding Nodes in a Graph Hierarchy that is Managed by Class HierarchyManager, which explain how to use utility class GraphHider with graph hierarchies managed by class HierarchyManager.

Although GraphHider conveniently stores all elements that it hides, it does not keep track of any hierarchical information associated with these elements. As a consequence, unhiding will not restore the graph hierarchy for folder and group nodes. The following sample code demonstrates one way of extending GraphHider to remedy that shortcoming. Of course, this implementation assumes that no structural changes are applied to the graph in question from another source.

package demo.view.hierarchy;


import y.base.Graph;
import y.base.Edge;
import y.base.EdgeCursor;
import y.base.EdgeList;
import y.base.Node;
import y.base.NodeCursor;
import y.base.NodeList;
import y.util.GraphHider;
import y.view.hierarchy.HierarchyManager;

import java.awt.event.ActionEvent;
import java.util.HashMap;
import java.util.Map;

import javax.swing.AbstractAction;
import javax.swing.JToolBar;


/**
 * Demonstrates hiding and unhiding in hierarchically organized graph
 * structures.
 */
public class HierarchyGraphHiderDemo extends HierarchyDemo {
  private final GraphHider gh;

  public HierarchyGraphHiderDemo() {
    // a HierachyManager has been registered in the super class' ctor,
    // so it's ok to create a HierarchyGraphHider for the view's graph
    gh = new HierarchyGraphHider(view.getGraph2D());
    gh.setFireGraphEventsEnabled(true);
  }

  /**
   * Overwritten to add controls for hiding/unhiding.
   */
  protected JToolBar createToolBar() {
    final JToolBar jtb = super.createToolBar();
    jtb.addSeparator();
    jtb.add(new AbstractAction("Hide All") {
      public void actionPerformed( final ActionEvent e ) {
        gh.hideAll();
        view.getGraph2D().updateViews();
      }
    });
    jtb.add(new AbstractAction("Unhide All") {
      public void actionPerformed( final ActionEvent e ) {
        gh.unhideAll();
        view.getGraph2D().updateViews();
      }
    });
    return jtb;
  }

  public static void main(String[] args) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        initLnF();
        (new HierarchyGraphHiderDemo()).start();
      }
    });
  }


  /**
   * <code>GraphHider</code> extension that keeps track of hierarchy
   * information.
   */
  static class HierarchyGraphHider extends GraphHider
  {
    private final HierarchyManager hierarchy;
    private final Map node2Info;

    private boolean recursiveGroupHidingEnabled;

    /**
     * Creates a new instance of <code>HierarchyGraphHider</code> for the
     * specified hierarchically organized graph. In this context
     * <em>hierarchically orgnized</em> means that a
     * {@link y.view.hierarchy.HierarchyManager} has been registered for the
     * graph such that <code>HierarchyManager.getInstance(g)</code> will not
     * return <code>null</code>.
     *
     * @param g   a <code>Graph</code> for which a
     *            {@link y.view.hierarchy.HierarchyManager} has been registered
     */
    public HierarchyGraphHider( final Graph g ) {
      super(g);

      recursiveGroupHidingEnabled = true;
      node2Info = new HashMap();
      hierarchy = HierarchyManager.getInstance(g);

      super.setFireGraphEventsEnabled(true);
    }

    /**
     * Returns <code>true</code> if this <code>HierarchyGraphHider</code> will
     * recursively hide the contents of group nodes.
     * @return <code>true</code> if this <code>HierarchyGraphHider</code> will
     * recursively hide the contents of group nodes.
     */
    public boolean isRecursiveGroupHidingEnabled() {
      return recursiveGroupHidingEnabled;
    }

    /**
     * Specifies whether this <code>HierarchyGraphHider</code> should
     * recursively hide the contents of group nodes.
     * Defaults to <code>true</code>.
     * @param b   if <code>true</code>, this <code>HierarchyGraphHider</code>
     *            will recursively hide the contents of group nodes.
     */
    public void setRecursiveGroupHidingEnabled( final boolean b ) {
      this.recursiveGroupHidingEnabled = b;
    }

    /**
     * Overwritten to do nothing.
     */
    public void setFireGraphEventsEnabled( final boolean fireEvents ) {
      // no op: we *need* graph events for hierarchy management
    }

    public void hide( final Node v ) {
      if (v.getGraph() == null) {
        // node already hidden
        return;
      }

      final NodeInfo info = new NodeInfo();

      if (hierarchy.isFolderNode(v)) {
        info.type = NodeInfo.FOLDER;
        info.parent = hierarchy.getParentNode(v);

        final EdgeList el = new EdgeList();
        for (EdgeCursor ec = v.edges(); ec.ok(); ec.next()) {
          final Edge edge = ec.edge();
          el.add(edge);
          // intentional use of hide, since we do not want the hierarchy
          // manager to receive these event
          getGraph().hide(edge);
        }

        // we convert the folder node to a group node to gain easy access to
        // its internal graph
        hierarchy.convertToGroupNode(v);
        info.nestedNodes = new NodeList(hierarchy.getChildren(v));
        info.nestedEdges = new EdgeList();

        // now we hide the folder contents recursively
        // since we inserted these elements into the current graph by
        // converting the folder to a group, we have to ensure that the
        // GraphHider caching mechanism does not store these
        final EdgeList he = hiddenEdges;
        hiddenEdges = info.nestedEdges;
        final NodeList hn = hiddenNodes;
        hiddenNodes = new NodeList();
        for (NodeCursor nc = info.nestedNodes.nodes(); nc.ok(); nc.next()) {
          hide(nc.node());
        }
        hiddenNodes = hn;
        hiddenEdges = he;

        hierarchy.convertToFolderNode(v);
        while (!el.isEmpty()) {
          // intentional use of unhide, since we do not want the hierarchy
          // manager to receive these event
          getGraph().unhide(el.popEdge());
        }
      } else if (hierarchy.isGroupNode(v)) {
        info.type = NodeInfo.GROUP;
        info.parent = hierarchy.getParentNode(v);
        info.nestedNodes = new NodeList(hierarchy.getChildren(v));
        if (recursiveGroupHidingEnabled) {
          hide(info.nestedNodes);
        }
      } else { // NORMAL
        info.parent = hierarchy.getParentNode(v);
      }
      node2Info.put(v, info);
      super.hide(v);
    }

    protected void unhide( final Node v ) {
      super.unhide(v);

      final NodeInfo info = (NodeInfo)node2Info.get(v);
      if (info == null) {
        return;
      }

      if (info.parent != null) {
        hierarchy.setParentNode(v, info.parent);
      }
      switch (info.type) {
        case NodeInfo.FOLDER:
          hierarchy.convertToGroupNode(v);
          for (NodeCursor nc = info.nestedNodes.nodes(); nc.ok(); nc.next()) {
            unhide(nc.node());
          }
          for (EdgeCursor ec = info.nestedEdges.edges(); ec.ok(); ec.next()) {
            unhide(ec.edge());
          }
          hierarchy.closeGroup(v);
          break;
        case NodeInfo.GROUP:
          hierarchy.convertToGroupNode(v);
          if (recursiveGroupHidingEnabled) {
            for (NodeCursor nc = info.nestedNodes.nodes(); nc.ok(); nc.next()) {
              final Node node = nc.node();
              unhide(node);
              hiddenNodes.remove(node);
            }
          }
          for (NodeCursor nc = info.nestedNodes.nodes(); nc.ok(); nc.next()) {
            hierarchy.setParentNode(nc.node(), v);
          }
          break;
        default:
          break;
      }

      node2Info.remove(v);
    }

    protected void unhide( final Edge e ) {
      final boolean wasInterEdge = hierarchy.isInterEdge(e);

      final Node src = e.source();
      final Node tgt = e.target();

      final Graph ancestor = hierarchy.getNearestCommonAncestor(src.getGraph(), tgt.getGraph());
      final Node srcRep = hierarchy.getRepresentative(wasInterEdge ? hierarchy.getRealSource(e) : src, ancestor);
      final Node tgtRep = hierarchy.getRepresentative(wasInterEdge ? hierarchy.getRealTarget(e) : tgt, ancestor);

      if (srcRep != src || tgtRep != tgt) {
        ancestor.changeEdge(e, srcRep, tgtRep);
      }

      final boolean willBeNormalEdge =
              wasInterEdge &&
              e.source() == hierarchy.getRealSource(e) &&
              e.target() == hierarchy.getRealTarget(e);

      ancestor.reInsertEdge(e);

      if (!wasInterEdge && (srcRep != src || tgtRep != tgt)) {
        hierarchy.convertToInterEdge(e, src, tgt);
      }

      if (willBeNormalEdge) {
        hierarchy.convertToNormalEdge(e);
      }
    }

    /**
     * Stores hierarchical information associated to a node.
     */
    private static final class NodeInfo
    {
      static final byte NORMAL = 0;
      static final byte GROUP  = 1;
      static final byte FOLDER = 2;

      byte type;
      Node parent;
      NodeList nestedNodes;
      EdgeList nestedEdges;

      private NodeInfo() {
        type = NORMAL;
        parent = null;
        nestedNodes = null;
        nestedEdges = null;
      }
    }
  }
}

Although the above sample code is based on yFiles 2.5, it can be easily used with previous versions, too.

Resources

Categories this article belongs to:
yFiles for Java > yFiles Viewer > Graph Hierarchies > Managing Graph Hierarchies
Applies to:
yFiles for Java 2: 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10, 2.11, 2.12, 2.13, 2.14, 2.15, 2.16, 2.17, 2.18
Keywords:
GraphHider - HierarchyManager - hide - hiding - node - nodes