/* * Copyright (c) 2003- michael lawley and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public License version 2.1 as published by the Free Software Foundation * which accompanies this distribution, and is available by writing to * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * Contributors: * michael lawley * * * */ package tefkat.engine.view; import java.awt.Rectangle; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.HashMap; import java.util.Map; import java.util.Properties; import org.jgraph.JGraph; import org.jgraph.graph.CellView; import org.jgraph.graph.DefaultEdge; import org.jgraph.graph.GraphConstants; import org.jgraph.graph.GraphModel; import org.jgraph.graph.VertexView; public class RadialTreeLayoutAlgorithm { /** * Based on the paper "Radial Tree Graph Drawing Algorithm for Representing * Large Hierarchies" by Greg Book and Neeta Keshary. * * Algorithm is modified from that in the above paper since it contains bugs * and the sample code contains major inefficiencies. * * Since this algorithm needs to be applied to a tree but we have a directed * graph, a spanning tree is first constructed then the algorithm is applied * to it. */ /** * If WIDTH is specified, then CENTRE_X and RADIUS_X are calculated from it * based on the maximum depth of the spanning tree. */ public static final String KEY_WIDTH = "Width"; /** * If HEIGHT is specified, then CENTRE_Y and RADIUS_Y are calculated from it * based on the maximum depth of the spanning tree. */ public static final String KEY_HEIGHT = "Height"; public static final String KEY_CENTRE_X = "CentreX"; public static final String KEY_CENTRE_Y = "CentreY"; public static final String KEY_RADIUS_X = "RadiusX"; public static final String KEY_RADIUS_Y = "RadiusY"; public static final String CONTAINMENT_EDGE = "ContainmentEdge"; private static final String RADIAL_TREE_VISITED = "RadialTreeVisited"; private static final double TWO_PI = Math.PI * 2.0; private double RADIUSX; private double RADIUSY; private double ROOTX; private double ROOTY; private List leaves = null; JGraph jgraph = null; private double depth; public RadialTreeLayoutAlgorithm() { } public void perform(JGraph jgraph, boolean applyToAll, Properties configuration) { this.jgraph = jgraph; Object[] selectedCells = (applyToAll ? jgraph.getRoots() : jgraph.getSelectionCells()); if (selectedCells.length == 0) { return; // nothing to do } System.err.println("Layout " + selectedCells.length); // TODO delete CellView[] selectedCellViews = jgraph.getGraphLayoutCache().getMapping(selectedCells, true); // search all roots List roots = getRoots(jgraph, selectedCellViews); TreeNode tree = getSpanningTree(selectedCellViews, roots); depth = tree.getDepth() + 0.25; if (configuration.containsKey(KEY_WIDTH)) { double WIDTH = Double.parseDouble(configuration .getProperty(KEY_WIDTH)); ROOTX = WIDTH / 2.0; RADIUSX = ROOTX / depth; } else { ROOTX = Double.parseDouble(configuration.getProperty(KEY_CENTRE_X)); RADIUSX = Double.parseDouble(configuration .getProperty(KEY_RADIUS_X)); } if (configuration.containsKey(KEY_HEIGHT)) { double HEIGHT = Double.parseDouble(configuration .getProperty(KEY_HEIGHT)); ROOTY = HEIGHT / 2.0; RADIUSY = ROOTY / depth; } else { ROOTY = Double.parseDouble(configuration.getProperty(KEY_CENTRE_Y)); RADIUSY = Double.parseDouble(configuration .getProperty(KEY_RADIUS_Y)); } Map viewMap = new HashMap(); // layoutTree0(viewMap, tree); layoutTree(viewMap, tree); jgraph.getGraphLayoutCache().edit(viewMap, null, null, null); } private void layoutTree(Map viewMap, TreeNode root) { double arc = TWO_PI / leaves.size(); double angle = 0; // System.err.println(arc); // Set the angle of each leaf node for (int i = 0; i < leaves.size(); i++) { TreeNode leaf = (TreeNode) leaves.get(i); leaf.angle = angle; angle += arc; leaf.x = computeX(leaf.rank, leaf.angle); leaf.y = computeY(leaf.rank, leaf.angle); VertexView view = (VertexView) leaf.getView(); if (null != view) { placeView(viewMap, view, leaf.x, leaf.y); } } // //System.err.println("------------------------------------------------"); List nodes = new ArrayList(leaves); while (nodes.size() > 0) { TreeNode node = (TreeNode) nodes.remove(0); TreeNode parent = node.getParent(); if (null != parent) { TreeNode firstChild = (TreeNode) parent.children.get(0); TreeNode lastChild = (TreeNode) parent.children.get(parent.children.size()-1); parent.angle = (firstChild.angle + lastChild.angle) / 2; parent.x = computeX(parent.rank, parent.angle); parent.y = computeY(parent.rank, parent.angle); VertexView view = (VertexView) parent.getView(); if (null != view) { placeView(viewMap, view, parent.x, parent.y); } nodes.removeAll(parent.children); nodes.add(parent); } } } /** * @param rank * @param angle * @return */ private double computeY(int rank, double angle) { return ROOTY + ((rank * RADIUSY) * Math.sin(angle)); } /** * @param rank * @param angle * @return */ private double computeX(int rank, double angle) { return ROOTX + ((rank * RADIUSX) * Math.cos(angle)); } private void placeView(Map viewMap, VertexView view, double x, double y) { Rectangle bounds = (Rectangle) view.getBounds().clone(); bounds.x = (int) Math.round(x); bounds.y = (int) Math.round(y); Object cell = view.getCell(); Map map = GraphConstants.createMap(); GraphConstants.setBounds(map, bounds); viewMap.put(cell, map); } private List getChildren(VertexView view, List level) { ArrayList children = new ArrayList(); Object vertex = view.getCell(); GraphModel model = jgraph.getModel(); int portCount = model.getChildCount(vertex); // iterate any NodePort for (int i = 0; i < portCount; i++) { Object port = model.getChild(vertex, i); // iterate any Edge in the port Iterator itrEdges = model.edges(port); while (itrEdges.hasNext()) { Object edge = itrEdges.next(); // only include containment edges if (edge instanceof DefaultEdge && ((DefaultEdge) edge).getAttributes().containsKey(CONTAINMENT_EDGE)) { // if the Edge is a forward edge we should follow this edge if (port == model.getSource(edge)) { Object targetPort = model.getTarget(edge); Object targetVertex = model.getParent(targetPort); VertexView targetVertexView = (VertexView) jgraph .getGraphLayoutCache().getMapping(targetVertex, false); if (level.contains(targetVertexView)) { children.add(targetVertexView); } } } } } return children; } private List getRoots(JGraph jgraph, CellView[] cellViews) { List roots = new ArrayList(); GraphModel model = jgraph.getModel(); for (int i = 0; i < cellViews.length; i++) { if (cellViews[i] instanceof VertexView) { VertexView vertexView = (VertexView) cellViews[i]; boolean isRoot = true; Object vertex = vertexView.getCell(); int portCount = model.getChildCount(vertex); for (int j = 0; isRoot && j < portCount; j++) { Object port = model.getChild(vertex, j); Iterator itrEdges = model.edges(port); while (isRoot && itrEdges.hasNext()) { Object edge = itrEdges.next(); if (edge instanceof DefaultEdge && ((DefaultEdge) edge).getAttributes().containsKey(CONTAINMENT_EDGE)) { if (model.getTarget(edge) == port) { isRoot = false; } } } } if (isRoot) { roots.add(vertexView); } } } return roots; } /** * Algorithm assumes a single root node so if there are multiple roots * (nodes with no incoming edges), then we construct the spanning tree with * an invisible root node that is the parent of the real roots. */ private TreeNode getSpanningTree(CellView[] cellViews, List roots) { List vertexViews = new ArrayList(cellViews.length); leaves = new ArrayList(); // first: mark all as not visited for (int i = 0; i < cellViews.length; i++) { if (cellViews[i] instanceof VertexView) { VertexView vertexView = (VertexView) cellViews[i]; vertexView.getAttributes().remove(RADIAL_TREE_VISITED); vertexViews.add(vertexView); } } TreeNode node; if (roots.size() == 0) { // pick an arbitrary node roots.add(vertexViews.get(0)); } if (roots.size() > 1) { node = new TreeNode(null); // Initialize the list of leaf nodes leaves.add(node); buildSpanningTree(vertexViews, node, roots); } else { VertexView vertexView = (VertexView) roots.get(0); node = new TreeNode(vertexView); vertexView.getAttributes().put(RADIAL_TREE_VISITED, Boolean.TRUE); leaves.add(node); buildSpanningTree(vertexViews, node, getChildren(vertexView, vertexViews)); } return node; } /** * Breadth-first traversal of the graph. */ private void buildSpanningTree(List vertexViews, TreeNode node, List children) { List childNodes = new ArrayList(children.size()); for( Iterator itr = children.iterator(); itr.hasNext(); ) { VertexView vertexView = (VertexView) itr.next(); if (null == vertexView.getAttributes().get(RADIAL_TREE_VISITED)) { vertexView.getAttributes().put(RADIAL_TREE_VISITED, Boolean.TRUE); TreeNode childNode = new TreeNode(vertexView); node.addChild(childNode); childNodes.add(childNode); } } if (childNodes.size() > 0) { int idx = leaves.indexOf(node); // int n = leaves.size(); leaves.remove(idx); leaves.addAll(idx, childNodes); // System.out.println(n + " " + childNodes.size() + " " + leaves.size()); } for (Iterator itr = childNodes.iterator(); itr.hasNext(); ) { TreeNode childNode = (TreeNode) itr.next(); VertexView vertexView = childNode.getView(); buildSpanningTree(vertexViews, childNode, getChildren(vertexView, vertexViews)); } } private static class TreeNode { public int rank = 0; private VertexView view; private TreeNode parent = null; private List children = new ArrayList(); public double angle, x, y, rightBisector, leftBisector, rightTangent, leftTangent; TreeNode(VertexView view) { this.view = view; } public int getDepth() { int depth = 0; Iterator itr = children.iterator(); while (itr.hasNext()) { TreeNode node = (TreeNode) itr.next(); int childDepth = node.getDepth(); if (childDepth >= depth) { depth = childDepth + 1; } } return depth; } public VertexView getView() { return view; } public void addChild(TreeNode node) { children.add(node); node.parent = this; node.updateRank(rank + 1); } public List getChildren() { return children; } public boolean hasChildren() { return children.size() > 0; } public TreeNode getParent() { return parent; } public double leftLimit() { return Math.min(normalize(leftBisector), (leftTangent)); } public double rightLimit() { return Math.max(normalize(rightBisector), (rightTangent)); } private double normalize(double angle) { /* while (angle > TWO_PI) { angle -= TWO_PI; } while (angle < -TWO_PI) { angle += TWO_PI; } */ return angle; } private void updateRank(int newRank) { rank = newRank; for (int i = 0; i < children.size(); i++) { ((TreeNode) children.get(i)).updateRank(rank + 1); } } } }