/* $Id: ClassdiagramLayouter.java 17863 2010-01-12 20:07:22Z linus $ ***************************************************************************** * Copyright (c) 2009 Contributors - see below * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * tfmorris ***************************************************************************** * * Some portions of this file was previously release using the BSD License: */ // Copyright (c) 1996,2009 The Regents of the University of California. All // Rights Reserved. Permission to use, copy, modify, and distribute this // software and its documentation without fee, and without a written // agreement is hereby granted, provided that the above copyright notice // and this paragraph appear in all copies. This software program and // documentation are copyrighted by The Regents of the University of // California. The software program and documentation are supplied "AS // IS", without any accompanying services from The Regents. The Regents // does not warrant that the operation of the program will be // uninterrupted or error-free. The end-user understands that the program // was developed for research purposes and is advised not to rely // exclusively on the program for any reason. IN NO EVENT SHALL THE // UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, // SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, // ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF // THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE // PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF // CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, // UPDATES, ENHANCEMENTS, OR MODIFICATIONS. package org.argouml.uml.diagram.static_structure.layout; import java.awt.Dimension; import java.awt.Point; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.TreeSet; import org.apache.log4j.Logger; import org.argouml.uml.diagram.ArgoDiagram; import org.argouml.uml.diagram.layout.LayoutedObject; import org.argouml.uml.diagram.layout.Layouter; import org.tigris.gef.presentation.Fig; /** * This class implements a layout algorithm for class diagrams.<p> * * The layout process is performed in a row by row way. The position of the * nodes in a row are set using the sequence given by the <em>natural order * </em> of the nodes.<p> * * The resulting layout sequence: * <ol> * <li>Standalone (i.e. without links) nodes first, followed by linked nodes * <li>Ordered by node-typ: package, interface, class, <em>other</em> * <li>Increasing level in link-hierarchy - root elements first * <li>Decreasing amount of weighted links * <li>Ascending name of model object * </ol> * * @see ClassdiagramNode#compareTo(Object) * */ public class ClassdiagramLayouter implements Layouter { // TODO: make the "magic numbers" configurable /** * This class keeps all the nodes in one row together and provides basic * functionality for them. * * @author David Gunkel */ private class NodeRow implements Iterable<ClassdiagramNode> { /** * Keeps all nodes of this row. */ private List<ClassdiagramNode> nodes = new ArrayList<ClassdiagramNode>(); /** * The row number of this row. */ private int rowNumber; /** * Construct an empty NodeRow with the given row number. * * @param aRowNumber The row number of this row. */ public NodeRow(int aRowNumber) { rowNumber = aRowNumber; } /** * Add a node to this NodeRow. * * @param node The node to be added */ public void addNode(ClassdiagramNode node) { node.setRank(rowNumber); node.setColumn(nodes.size()); nodes.add(node); } /** * Splittable are packages and standalone-nodes. A split is performed, * if the maximum width is reached or when a type change occurs (from * package to not-package, from standalone to not-standalone). * * <ul> * <li>packages * <li>After standalone * </ul> * * Split this row into two, if * <ul> * <li>at least one standalone node is available * <li>and the given maximum row width is exceeded * <li>or a non-standalone element is detected. * </ul> * * Return the new NodeRow or null if this row is not split. * * @param maxWidth * The maximum allowed row width * @param gap * The horizontal gab between two nodes * @return NodeRow */ public NodeRow doSplit(int maxWidth, int gap) { TreeSet<ClassdiagramNode> ts = new TreeSet<ClassdiagramNode>(nodes); if (ts.size() < 2) { return null; } ClassdiagramNode firstNode = ts.first(); if (!firstNode.isStandalone()) { return null; } ClassdiagramNode lastNode = ts.last(); if (firstNode.isStandalone() && lastNode.isStandalone() && (firstNode.isPackage() == lastNode.isPackage()) && getWidth(gap) <= maxWidth) { return null; } boolean hasPackage = firstNode.isPackage(); NodeRow newRow = new NodeRow(rowNumber + 1); ClassdiagramNode split = null; int width = 0; int count = 0; for (Iterator<ClassdiagramNode> iter = ts.iterator(); iter.hasNext() && (width < maxWidth || count < 2);) { ClassdiagramNode node = iter.next(); // split = // (split == null || split.isStandalone()) ? node : split; split = (split == null || (hasPackage && split.isPackage() == hasPackage) || split.isStandalone()) ? node : split; width += node.getSize().width + gap; count++; } nodes = new ArrayList<ClassdiagramNode>(ts.headSet(split)); for (ClassdiagramNode n : ts.tailSet(split)) { newRow.addNode(n); } if (LOG.isDebugEnabled()) { LOG.debug("Row split. This row width: " + getWidth(gap) + " next row(s) width: " + newRow.getWidth(gap)); } return newRow; } /** * @return Returns the nodes. */ public List<ClassdiagramNode> getNodeList() { return nodes; } /** * @return Returns the rowNumber. */ public int getRowNumber() { return rowNumber; } /** * Get the width for this row using the given horizontal gap between * nodes. * * @param gap The horizontal gap between nodes. * @return The width of this row */ public int getWidth(int gap) { int result = 0; for (ClassdiagramNode node : nodes) { result += node.getSize().width + gap; } if (LOG.isDebugEnabled()) { LOG.debug("Width of row " + rowNumber + ": " + result); } return result; } /** * Set the row number of this row. * * @param rowNum The rowNumber to set. */ public void setRowNumber(int rowNum) { this.rowNumber = rowNum; adjustRowNodes(); } /** * Adjust the properties for all nodes in this row: rank, * column, offset for edges. */ private void adjustRowNodes() { int col = 0; int numNodesWithDownlinks = 0; List<ClassdiagramNode> list = new ArrayList<ClassdiagramNode>(); for (ClassdiagramNode node : this ) { node.setRank(rowNumber); node.setColumn(col++); if (!node.getDownNodes().isEmpty()) { numNodesWithDownlinks++; list.add(node); } } int offset = -numNodesWithDownlinks * E_GAP / 2; for (ClassdiagramNode node : list ) { node.setEdgeOffset(offset); offset += E_GAP; } } /** * @return an Iterator for the nodes of this row, sorted by their * natural order. * @see java.lang.Iterable#iterator() */ public Iterator<ClassdiagramNode> iterator() { return (new TreeSet<ClassdiagramNode>(nodes)).iterator(); } } /** * Gap to be left between edges. */ private static final int E_GAP = 5; /** * Horizontal gap between nodes. */ private static final int H_GAP = 80; private static final Logger LOG = Logger.getLogger(ClassdiagramLayouter.class); /** * The maximum row width. */ // TODO: this should be a configurable property private static final int MAX_ROW_WIDTH = 1200; /** * Vertical gap between nodes. */ private static final int V_GAP = 80; /** * The diagram that is being laid out. */ private ArgoDiagram diagram; /** * HashMap with figures as key and Nodes as elements. */ private HashMap<Fig, ClassdiagramNode> figNodes = new HashMap<Fig, ClassdiagramNode>(); /** * layoutedClassNodes is a convenience which holds a subset of * layoutedObjects (only ClassNodes). */ private List<ClassdiagramNode> layoutedClassNodes = new ArrayList<ClassdiagramNode>(); /** * Holds all edges - subset of layoutedObjects. */ private List<ClassdiagramEdge> layoutedEdges = new ArrayList<ClassdiagramEdge>(); /** * List of objects to lay out. */ private List<LayoutedObject> layoutedObjects = new ArrayList<LayoutedObject>(); /** * List of NodeRows in the diagram. */ private List<NodeRow> nodeRows = new ArrayList<NodeRow>(); /** * Base X position to use a starting point for next node. */ private int xPos; /** * Base Y position for the row currently being laid out. */ private int yPos; /** * Constructor for the layouter. Takes a diagram as input to extract all * LayoutedObjects, which will be layouted. * * @param theDiagram The diagram to layout. */ public ClassdiagramLayouter(ArgoDiagram theDiagram) { diagram = theDiagram; for (Fig fig : diagram.getLayer().getContents()) { if (fig.getEnclosingFig() == null) { add(ClassdiagramModelElementFactory.SINGLETON.getInstance(fig)); } } } /** * Add an object to layout. * * @param obj represents the object to layout. */ public void add(LayoutedObject obj) { // TODO: check for duplicates (is this possible???) layoutedObjects.add(obj); if (obj instanceof ClassdiagramNode) { layoutedClassNodes.add((ClassdiagramNode) obj); } else if (obj instanceof ClassdiagramEdge) { layoutedEdges.add((ClassdiagramEdge) obj); } } /** * Get the horizontal gap between nodes. * * @return The horizontal gap between nodes. */ private int getHGap() { return H_GAP; } /** * Return the minimum diagram size after the layout process. * * @return The minimum diagram size after the layout process. */ public Dimension getMinimumDiagramSize() { int width = 0, height = 0; int hGap2 = getHGap() / 2; int vGap2 = getVGap() / 2; for (ClassdiagramNode node : layoutedClassNodes) { width = Math.max(width, node.getLocation().x + (int) node.getSize().getWidth() + hGap2); height = Math.max(height, node.getLocation().y + (int) node.getSize().getHeight() + vGap2); } return new Dimension(width, height); } /** * Return the object with a given index from the layouter. * * @param index * represents the index of this object in the layouter. * @return The LayoutedObject for the given index. */ public LayoutedObject getObject(int index) { return layoutedObjects.get(index); } /** * Return all the objects currently participating in * the layout process. * * @return An array holding all the object in the layouter. */ public LayoutedObject[] getObjects() { return (LayoutedObject[]) layoutedObjects.toArray(); } /** * Get the vertical gap between nodes. * * @return The vertical gap between nodes. */ private int getVGap() { return V_GAP; } /** * Lay out the current diagram. */ public void layout() { long s = System.currentTimeMillis(); setupLinks(); rankAndWeightNodes(); placeNodes(); placeEdges(); LOG.debug("layout duration: " + (System.currentTimeMillis() - s)); } /** * All layoutedObjects of type "Edge" are placed using an * edge-type specific layout algorithm. The offset from a * <em>centered</em> edge is taken from the parent node to avoid * overlaps. * * @see ClassdiagramEdge */ private void placeEdges() { ClassdiagramEdge.setVGap(getVGap()); ClassdiagramEdge.setHGap(getHGap()); for (ClassdiagramEdge edge : layoutedEdges) { if (edge instanceof ClassdiagramInheritanceEdge) { ClassdiagramNode parent = figNodes.get(edge.getDestFigNode()); ((ClassdiagramInheritanceEdge) edge).setOffset(parent .getEdgeOffset()); } edge.layout(); } } /** * Set the placement coordinate for a given node. * * @param node To be placed. */ private void placeNode(ClassdiagramNode node) { List<ClassdiagramNode> uplinks = node.getUpNodes(); List<ClassdiagramNode> downlinks = node.getDownNodes(); int width = node.getSize().width; double xOffset = width + getHGap(); int bumpX = getHGap() / 2; // (xOffset - curW) / 2; int xPosNew = Math.max(xPos + bumpX, uplinks.size() == 1 ? node.getPlacementHint() : -1); node.setLocation(new Point(xPosNew, yPos)); if (LOG.isDebugEnabled()) { LOG.debug("placeNode - Row: " + node.getRank() + " Col: " + node.getColumn() + " Weight: " + node.getWeight() + " Position: (" + xPosNew + "," + yPos + ") xPos: " + xPos + " hint: " + node.getPlacementHint()); } // If there's only a single child (and we're it's only parent), // set a hint for where to place it when we get to its row if (downlinks.size() == 1) { ClassdiagramNode downNode = downlinks.get(0); if (downNode.getUpNodes().get(0).equals(node)) { downNode.setPlacementHint(xPosNew); } } xPos = (int) Math.max(node.getPlacementHint() + width, xPos + xOffset); } /** * Place the NodeRows in the diagram. */ private void placeNodes() { // TODO: place comments near connected classes // TODO: place from middle towards outer edges? (or place largest // groups first) int xInit = 0; yPos = getVGap() / 2; for (NodeRow row : nodeRows) { xPos = xInit; int rowHeight = 0; for (ClassdiagramNode node : row) { placeNode(node); rowHeight = Math.max(rowHeight, node.getSize().height); } yPos += rowHeight + getVGap(); } centerParents(); } /** * Center parents over their children, working from bottom to top. */ private void centerParents() { for (int i = nodeRows.size() - 1; i >= 0; i--) { for (ClassdiagramNode node : nodeRows.get(i)) { List<ClassdiagramNode> children = node.getDownNodes(); if (children.size() > 0) { node.setLocation(new Point(xCenter(children) - node.getSize().width / 2, node.getLocation().y)); } } // TODO: Make another pass to deal with overlaps? } } /** * Compute the horizontal center of a list of nodes. * @param nodes the list of nodes * @return the computed X coordinate */ private int xCenter(List<ClassdiagramNode> nodes) { int left = 9999999; int right = 0; for (ClassdiagramNode node : nodes) { int x = node.getLocation().x; left = Math.min(left, x); right = Math.max(right, x + node.getSize().width); } return (right + left) / 2; } /** * Rank the nodes depending on their level (position in hierarchy) and set * their weight to achieve a proper node-sequence for the layout. Rows * exceeding the maximum row width are split, if standalone nodes are * available. * <p> * Weight the other nodes to determine their columns. * <p> * TODO: Weighting doesn't appear to be working as intended because multiple * groups of children/specializations get intermixed in name order rather * than being grouped by their parent/generalization. - tfm - 20070314 */ private void rankAndWeightNodes() { List<ClassdiagramNode> comments = new ArrayList<ClassdiagramNode>(); nodeRows.clear(); TreeSet<ClassdiagramNode> nodeTree = new TreeSet<ClassdiagramNode>(layoutedClassNodes); // boolean hasPackages = false; // TODO: move "package in row" to NodeRow for (ClassdiagramNode node : nodeTree) { // if (node.isPackage()) { // hasPackages = true; // } else if (hasPackages) { // hasPackages = false; // currentRank = -1; // } if (node.isComment()) { comments.add(node); } else { int rowNum = node.getRank(); for (int i = nodeRows.size(); i <= rowNum; i++) { nodeRows.add(new NodeRow(rowNum)); } nodeRows.get(rowNum).addNode(node); } } for (ClassdiagramNode node : comments) { int rowInd = node.getUpNodes().isEmpty() ? 0 : ((node.getUpNodes().get(0)).getRank()); nodeRows.get(rowInd).addNode(node); } for (int row = 0; row < nodeRows.size();) { NodeRow diaRow = nodeRows.get(row); diaRow.setRowNumber(row++); diaRow = diaRow.doSplit(MAX_ROW_WIDTH, H_GAP); if (diaRow != null) { nodeRows.add(row, diaRow); } } } /** * Remove an object from the layout process. * * @param obj represents the object to remove. */ public void remove(LayoutedObject obj) { layoutedObjects.remove(obj); } /** * Set the up- and downlinks for each node based on the edges which are * shown in the diagram. */ private void setupLinks() { figNodes.clear(); HashMap<Fig, List<ClassdiagramInheritanceEdge>> figParentEdges = new HashMap<Fig, List<ClassdiagramInheritanceEdge>>(); for (ClassdiagramNode node : layoutedClassNodes) { node.getUpNodes().clear(); node.getDownNodes().clear(); figNodes.put(node.getFigure(), node); } for (ClassdiagramEdge edge : layoutedEdges) { Fig parentFig = edge.getDestFigNode(); ClassdiagramNode child = figNodes.get(edge.getSourceFigNode()); ClassdiagramNode parent = figNodes.get(parentFig); if (edge instanceof ClassdiagramInheritanceEdge) { if (parent != null && child != null) { parent.addDownlink(child); child.addUplink(parent); List<ClassdiagramInheritanceEdge> edgeList = figParentEdges.get(parentFig); if (edgeList == null) { edgeList = new ArrayList<ClassdiagramInheritanceEdge>(); figParentEdges.put(parentFig, edgeList); } edgeList.add((ClassdiagramInheritanceEdge) edge); } else { LOG.error("Edge with missing end(s): " + edge); } } else if (edge instanceof ClassdiagramNoteEdge) { if (parent.isComment()) { parent.addUplink(child); } else if (child.isComment()) { child.addUplink(parent); } else { LOG.error("Unexpected parent/child constellation for edge: " + edge); } } else if (edge instanceof ClassdiagramAssociationEdge) { // Associations not supported, yet // TODO: Create appropriate ClassdiagramEdge } else { LOG.error("Unsupported edge type"); } } } }