//ShowTree Tree Visualization System //Copyright (C) 2009 Yuvi Masory // //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, version 3 only. // //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, write to the Free Software //Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. package display; import java.awt.Color; import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import logic.Node; import logic.treeIterators.PreorderIterator; import display.components.TreeMenu; import display.components.TreePane; /* * This class actually _draws_ the tree to the TreePane. * This means it calls one of the tree _positioning_ algorithms and then performs other tasks * needed for drawing, including translating the coordinates so the tree aligns to the left of the TreePane, * adding the padding so the trees aren't at the exact edge of the TreePane, drawing labels, drawing edges, * computing the area of the tree for the sake of the scroll bars, etc. * * * minYSeparation is required to be at least 25 to ensure nice display. Any smaller and the Nodes will * overlap (since they are always at least BOTTOM_PADDING) tall. This can cause edges to go up instead * of down. This is strictly a display limitation, the positioning algorithms work for any separation. */ public class DrawTree { /* * For the sake of computeTreeGraphicalRectangle * Not updated to null if someone directly access repaint() in TreePane, but then again it doesn't need to be, * since you can't save an image of an empty screen, and only save image calls computeTreeGraphicalRectangle at this point */ private static boolean drawLabels; /* * Tree drawing works by assigning each Node a rectangle of its ultimate size and position, up to translation. * The tree is then translated so it is left-justified on Pane. * Nodes are then recursively drawing, follows by edges. * The size of the TreePane is then recomputed based on the size of the tree, for the sake of scroll-bar * behavior. * @param g - the TreePane's graphical context */ public static void paintTree(Graphics g, boolean drawLabs) { Node root = Start.getRoot(); if(root == null){ TreePane.getInstance().setPreferredSize(new Dimension(0, 0)); TreePane.getInstance().revalidate(); } else { // System.out.println(TreeMenu.getInstance().getPositioningAlgorithm().toString() + " width: " + Start.getRoot().getEmbeddedWidth()); // System.out.println(TreeMenu.getInstance().getPositioningAlgorithm().toString() + " sibling spacing: " + Start.getRoot().getEmbeddedSiblingSpacing()); // System.out.println(); drawLabels = drawLabs; computeNodeRectangles(root, g); leftJustifyTree(root, g, drawLabs); drawNodes(root, g, drawLabs); // DrawEdges.drawOrthogonalEdges(root, g); DrawEdges.drawStraightNodeToNodeEdges(root, g); if(Start.isShowNodeField()) { showNodeFieldHelper(root, g); } Rectangle treeRect = computeTreeGraphicalRectangle(root, g); TreePane.getInstance().setPreferredSize( new Dimension((int) (treeRect.getWidth() + treeRect.getX() + Start.PADDING), (int) (treeRect.getHeight() + treeRect.getY()) + Start.PADDING)); TreePane.getInstance().revalidate(); } } /* * Translates the tree rectangles so it is left-justified on TreePanel. * Does this by changing the Nodes' stored graphicalRectangle values. * @param root - the root of the tree to be translated * @param g - the TreePane's graphics context */ private static void leftJustifyTree(Node root, Graphics g, boolean drawLabels) { Rectangle treeRect = computeTreeGraphicalRectangle(root, g); int rightShift = 0; if(drawLabels) { rightShift = (0 - (int) treeRect.getX()) + Start.PADDING; } else { Rectangle leftMost = findLeftmostNode(root).getGraphicalRectangle(); int midpoint = (int) (leftMost.getX() + leftMost.getWidth()/2); rightShift = (0 - midpoint + Start.PADDING); } int downShift = (0 - (int) treeRect.getY()) + Start.PADDING; PreorderIterator iter = new PreorderIterator(root); Node cur; int curX; int curY; int curWidth; int curHeight; while(iter.hasNext()) { cur = iter.next(); Rectangle curRect = cur.getGraphicalRectangle(); curX = (int) curRect.getX(); curY = (int) curRect.getY(); curWidth = (int) curRect.getWidth(); curHeight = (int) curRect.getHeight(); cur.setGraphicalRectangle(new Rectangle(curX + rightShift, curY + downShift, curWidth, curHeight)); cur.setX(cur.getX() + rightShift); cur.setY(cur.getY() + downShift); } } /* * Returns the most Node positioned farthest to the left in the tree, counting a Node's position * as the CENTER of its bounding rectangle, not the origin of the rectangle. * @param root - the root of the tree */ private static Node findLeftmostNode(Node root) { PreorderIterator iter = new PreorderIterator(root); Node leftMost = root; double minX = Integer.MAX_VALUE; Node cur; while(iter.hasNext()) { cur = iter.next(); Rectangle labelRect = cur.getGraphicalRectangle(); double curMinX = labelRect.getX() + labelRect.getWidth()/2; if(curMinX < minX) { minX = curMinX; leftMost = cur; } } return leftMost; } /* * WARNING: Behavior is dependent upon static boolean drawLabels. If you are calling this outside of the GUI * there is no way for you to tell whether the rectangle will account for labels being drawn or not! * @param root - the root of the tree whose bounding rectangle is sought * @param g - the TreePane's graphics context * @returns Rectangle representing the currently displayed tree's position and dimensions */ public static Rectangle computeTreeGraphicalRectangle(Node root, Graphics g) { PreorderIterator iter = new PreorderIterator(root); int minX = Integer.MAX_VALUE; int maxX = Integer.MIN_VALUE; int minY = Integer.MAX_VALUE; int maxY = Integer.MIN_VALUE; Node cur; while(iter.hasNext()) { cur = iter.next(); Rectangle labelRect = cur.getGraphicalRectangle(); int curMinX = 0; int curMaxX = 0; int curMinY = 0; int curMaxY = 0; if(drawLabels) { curMinX = (int) labelRect.getX(); curMaxX = (int) (labelRect.getX() + labelRect.getWidth()); curMinY = (int) labelRect.getY(); curMaxY = (int) (labelRect.getY() + labelRect.getHeight()); } else { curMinX = (int) cur.getX(); curMaxX = (int) cur.getX(); curMinY = (int) cur.getY(); curMaxY = (int) cur.getY(); } if(curMinX < minX) { minX = curMinX; } if(curMaxX > maxX) { maxX = curMaxX; } if(curMinY < minY) { minY = curMinY; } if(curMaxY > maxY) { maxY = curMaxY; } } return new Rectangle(minX, minY, maxX - minX, maxY - minY); } /* * Recursively draws Nodes, which involves drawing their labels at the right place as to appear centered. * @param n - the current Node whose label is being drawn * @param g - the TreePane's graphics context */ private static void drawNodes(Node n, Graphics g, boolean drawLabels) { drawNode(n, g, drawLabels); ArrayList<Node> children = n.getChildren(); for (Node child : children) { drawNodes(child, g, drawLabels); } } private static void drawNode(Node n, Graphics g, boolean drawLabel) { Rectangle nodeRect = n.getGraphicalRectangle(); Point labelDrawPoint = new Point((int) nodeRect.getX(), (int) nodeRect.getY() + (int) nodeRect.getHeight() - Start.BOTTOM_NODE_PADDING); if(drawLabel) { g.setColor(Color.WHITE); g.fillRect((int) nodeRect.getX(), (int) nodeRect.getY(), (int) nodeRect.getWidth(), (int) nodeRect.getHeight()); } if(Start.isShowNodeBounds()) { g.setColor(Color.RED); g.drawRect((int) nodeRect.getX(), (int) nodeRect.getY(), (int) nodeRect.getWidth(), (int) nodeRect.getHeight()); } if(n.getLabel() == "" || drawLabel == false) { g.setColor(Color.BLACK); double diameter = nodeRect.getHeight(); g.drawOval((int) (nodeRect.getX() + nodeRect.getWidth()/2 - diameter/2), (int) nodeRect.getY(), (int) diameter, (int) diameter); } else { g.setColor(Color.BLACK); g.drawString(n.getLabel(), (int) labelDrawPoint.getX(), (int) labelDrawPoint.getY()); } } /* * Recursively assigns each node its bounding Rectangle where it will be drawn (after translation) on the TreePane * NOTE: the FontMetrics.getStringBounds() function does a poor job finding the exact bounds of a displayed * String. It always gives some padding on the top. That is what BOTTOM_NODE_PADDING is for, to compensate by adding * some space to the bottom as well. This bottom padding however will also appear for empty string labels. * @param n - the root of the tree * @param g = the TreePane's graphics context */ public static void computeNodeRectangles(Node n, Graphics g) { PreorderIterator iter = new PreorderIterator(n); Node cur; while(iter.hasNext()) { cur = iter.next(); cur.setGraphicalRectangle(computeNodeRectangle(cur, g)); } } private static Rectangle computeNodeRectangle(Node n, Graphics g) { int x = (int) n.getX(); int y = (int) n.getY(); String label = n.getLabel(); TreePane tp = TreePane.getInstance(); FontMetrics fm = tp.getFontMetrics(tp.getFont()); Rectangle2D labelRect = fm.getStringBounds(label, g); int xShift = (int) labelRect.getWidth() / 2; int yShift = (int) labelRect.getHeight() / 2; int rectX = x - xShift; int rectY = y - yShift; int rectWidth = ((int) labelRect.getWidth()); int rectHeight = ((int) labelRect.getHeight() + Start.BOTTOM_NODE_PADDING); return new Rectangle(rectX, rectY, rectWidth, rectHeight); } private static void showNodeFieldHelper(Node n, Graphics g) { if(n.getModifier() != 0) { g.drawString( Double.toString(n.getModifier()), (int) (n.getGraphicalRectangle().getX()), (int) (n.getGraphicalRectangle().getY()) ); } ArrayList<Node> children = n.getChildren(); for (Node child : children) { showNodeFieldHelper(child, g); } } }