/* Copyright (C) 2006 EBI This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the itmplied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.biomart.builder.view.gui.diagrams; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Insets; import java.awt.LayoutManager2; import java.awt.Rectangle; import java.awt.geom.GeneralPath; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.swing.SwingUtilities; import org.biomart.builder.view.gui.diagrams.components.DiagramComponent; import org.biomart.builder.view.gui.diagrams.components.KeyComponent; import org.biomart.builder.view.gui.diagrams.components.RelationComponent; import org.biomart.builder.view.gui.diagrams.components.SchemaComponent; /** * This layout manager lays out components in rows of a square block. * * @author Richard Holland <holland@ebi.ac.uk> * @version $Revision: 1.16 $, $Date: 2007-11-02 16:22:37 $, modified by * $Author: rh4 $ * @since 0.6 */ public class SchemaLayoutManager implements LayoutManager2 { private static final int RELATION_SPACING = 5; // 72 = 1 inch private static final int TABLE_PADDING = 10; // 72 = 1 inch private Dimension size; private boolean sizeKnown; private final Map prefSizes = new HashMap(); private final Map constraints = new HashMap(); private final List rows = new ArrayList(); private final List relations = new ArrayList(); private final List rowHeights = new ArrayList(); private final List rowWidths = new ArrayList(); private final List rowSpacings = new ArrayList(); private int tableCount; private final Collection fixedComps = new HashSet(); /** * Sets up some defaults for the layout, ready for use. */ public SchemaLayoutManager() { this.sizeKnown = true; this.size = new Dimension(0, 0); this.tableCount = 0; } public float getLayoutAlignmentX(final Container target) { return 0.5f; } public float getLayoutAlignmentY(final Container target) { return 0.5f; } public void invalidateLayout(final Container target) { this.sizeKnown = false; } public void addLayoutComponent(final String name, final Component comp) { this.addLayoutComponent(comp, null); } public Dimension maximumLayoutSize(final Container target) { return this.minimumLayoutSize(target); } public Dimension preferredLayoutSize(final Container parent) { return this.minimumLayoutSize(parent); } public Dimension minimumLayoutSize(final Container parent) { // Work out how big we are. this.calculateSize(parent); synchronized (parent.getTreeLock()) { // Work out our parent's insets. final Insets insets = parent.getInsets(); // The minimum size is our size plus our // parent's insets size. final Dimension dim = new Dimension(0, 0); dim.width = this.size.width + insets.left + insets.right; dim.height = this.size.height + insets.top + insets.bottom; // That's it! return dim; } } private void calculateSize(final Container parent) { synchronized (parent.getTreeLock()) { if (this.sizeKnown) return; // Assumption that we are laying out a diagram. final Dimension maskedButton = ((Diagram) parent) .getHideMaskedArea(); this.size.height = maskedButton.height; this.size.width = maskedButton.width; this.prefSizes.clear(); for (int rowNum = 0; rowNum < this.rows.size(); rowNum++) { final List row = (List) this.rows.get(rowNum); int rowHeight = 0; int rowWidth = 0; int rowSpacing = 0; // For each component, allow space plus padding either // side equivalent to the number of relations from that // component. for (final Iterator i = row.iterator(); i.hasNext();) { final Component comp = (Component) i.next(); final Dimension prefSize = comp.getPreferredSize(); this.prefSizes.put(comp, prefSize); final int compSpacing = ((SchemaLayoutConstraint) this.constraints .get(comp)).getRelCount() * SchemaLayoutManager.RELATION_SPACING; rowHeight = Math.max(rowHeight, prefSize.height + compSpacing * 2); rowWidth += prefSize.width + compSpacing * 2; rowSpacing = Math.max(rowSpacing, compSpacing); } this.rowSpacings.set(rowNum, new Integer(rowSpacing)); // Add a bit of padding above and below each row. rowHeight += SchemaLayoutManager.TABLE_PADDING * 2; this.rowHeights.set(rowNum, new Integer(rowHeight)); this.size.height += rowHeight; // Add a bit of padding at each end of each row. rowWidth += (row.size() + 1) * SchemaLayoutManager.TABLE_PADDING * 2; this.rowWidths.set(rowNum, new Integer(rowWidth)); this.size.width = Math.max(rowWidth, this.size.width); } this.sizeKnown = true; } } public void addLayoutComponent(final Component comp, final Object constraints) { synchronized (comp.getTreeLock()) { if (comp instanceof RelationComponent) { this.relations.add(comp); this.constraints.put(comp, constraints); } else if (comp instanceof DiagramComponent && constraints instanceof SchemaLayoutConstraint) { this.constraints.put(comp, constraints); final Dimension prefSize = comp.getPreferredSize(); this.prefSizes.put(comp, prefSize); // Work out how many components per row we need to make // a square, and therefore which row has space on to // put this component. final int rowLength = (int) Math.ceil(Math .sqrt(++this.tableCount)); int rowNum = 0; while (rowNum < this.rows.size() && ((List) this.rows.get(rowNum)).size() >= rowLength) rowNum++; ((SchemaLayoutConstraint) constraints).setRow(rowNum); // Ensure arrays are large enough. while (rowNum >= this.rows.size()) { this.rowSpacings.add(new Integer(0)); this.rowHeights.add(new Integer(0)); this.rowWidths.add(new Integer(0)); this.rows.add(new ArrayList()); } ((List) this.rows.get(rowNum)).add(comp); // The component needs space for its relations. final int compSpacing = SchemaLayoutManager.RELATION_SPACING * ((SchemaLayoutConstraint) constraints).getRelCount(); // Update the row to accommodate the new component. final int oldRowWidth = ((Integer) this.rowWidths.get(rowNum)) .intValue(); int newRowWidth = oldRowWidth; newRowWidth += prefSize.width + SchemaLayoutManager.TABLE_PADDING * 2 + compSpacing * 2; this.rowWidths.set(rowNum, new Integer(newRowWidth)); // Update the row height if the new component plus spacing // is higher than the old row. final int oldRowHeight = ((Integer) this.rowHeights.get(rowNum)) .intValue(); final int newRowHeight = Math.max(oldRowHeight, prefSize.height + SchemaLayoutManager.TABLE_PADDING * 2) + compSpacing * 2; this.rowHeights.set(rowNum, new Integer(newRowHeight)); // Increase the space to the next row to accommodate the // relations from this new component, if necessary. this.rowSpacings.set(rowNum, new Integer(Math.max( ((Integer) this.rowSpacings.get(rowNum)).intValue(), compSpacing))); this.size.height += newRowHeight - oldRowHeight; this.size.width = Math.max(this.size.width, newRowWidth); } else this.fixedComps.add(comp); } } public void removeLayoutComponent(final Component comp) { synchronized (comp.getTreeLock()) { if (this.fixedComps.contains(comp)) this.fixedComps.remove(comp); else if (comp instanceof RelationComponent) { this.relations.remove(comp); this.constraints.remove(comp); } else { final SchemaLayoutConstraint constraints = (SchemaLayoutConstraint) this.constraints .remove(comp); final Dimension prefSize = comp.getPreferredSize(); this.prefSizes.remove(comp); // How much padding was this component given? final int compSpacing = SchemaLayoutManager.RELATION_SPACING * constraints.getRelCount(); this.tableCount--; final int rowNum = constraints.getRow(); ((List) this.rows.get(rowNum)).remove(comp); // Reduce the row width and height accordingly. final int oldRowWidth = ((Integer) this.rowWidths.get(rowNum)) .intValue(); final int oldRowHeight = ((Integer) this.rowHeights.get(rowNum)) .intValue(); int newRowWidth = oldRowWidth; newRowWidth -= prefSize.width + SchemaLayoutManager.TABLE_PADDING * 2 + compSpacing * 2; this.rowWidths.set(rowNum, new Integer(newRowWidth)); int newRowHeight = 0; for (final Iterator i = ((List) this.rows.get(rowNum)) .iterator(); i.hasNext();) newRowHeight = Math.max(newRowHeight, ((Component) i.next()).getPreferredSize().height + compSpacing * 2); newRowHeight += SchemaLayoutManager.TABLE_PADDING * 2; this.rowHeights.set(rowNum, new Integer(newRowHeight)); this.size.height -= oldRowHeight - newRowHeight; // While last row is empty, remove last row. int lastRow = this.rows.size() - 1; while (lastRow >= 0 && this.rows.get(lastRow) == null && ((List) this.rows.get(lastRow)).isEmpty()) { // Remove all references to empty row. this.rows.remove(lastRow); this.rowHeights.remove(lastRow); this.rowSpacings.remove(lastRow); this.rowWidths.remove(lastRow); // Update last row pointer. lastRow--; } // New width needs re-calculating from all rows. this.size.width = 0; for (final Iterator i = this.rowWidths.iterator(); i.hasNext();) this.size.width = Math.max(((Integer) i.next()).intValue(), this.size.width); } } } public void layoutContainer(final Container parent) { // Work out how big we are. this.calculateSize(parent); synchronized (parent.getTreeLock()) { // Fixed components are ignored. The parent should lay // them out. // Assumption that we are laying out a diagram. final Dimension maskedButton = ((Diagram) parent) .getHideMaskedArea(); int nextY = SchemaLayoutManager.TABLE_PADDING + maskedButton.height; // Layout each row in turn. for (int rowNum = 0; rowNum < this.rows.size(); rowNum++) { int x = SchemaLayoutManager.TABLE_PADDING; final int y = nextY + ((Integer) this.rowHeights.get(rowNum)).intValue() - SchemaLayoutManager.TABLE_PADDING * 2 - ((Integer) this.rowSpacings.get(rowNum)).intValue(); // Layout each component in the row. for (final Iterator i = ((List) this.rows.get(rowNum)) .iterator(); i.hasNext();) { final Component comp = (Component) i.next(); final Dimension prefSize = (Dimension) this.prefSizes .get(comp); final int compSpacing = ((SchemaLayoutConstraint) this.constraints .get(comp)).getRelCount() * SchemaLayoutManager.RELATION_SPACING; x += compSpacing; comp.setBounds(x, y - prefSize.height, prefSize.width, prefSize.height); comp.validate(); x += prefSize.width + SchemaLayoutManager.TABLE_PADDING * 2 + compSpacing; } nextY += ((Integer) this.rowHeights.get(rowNum)).intValue(); } // Work out how the relations are going to join things up. for (final Iterator i = this.relations.iterator(); i.hasNext();) { final RelationComponent comp = (RelationComponent) i.next(); // Obtain first key and work out position relative to // diagram. int firstRowNum = 0; if (this.rowHeights.size()==0) continue; int firstRowBottom = ((Integer) this.rowHeights .get(firstRowNum)).intValue(); final KeyComponent firstKey = comp.getFirstKeyComponent(); if (firstKey == null) continue; Rectangle firstKeyRectangle = firstKey.getBounds(); int firstKeyInsetX = firstKeyRectangle.x; firstKeyRectangle = SwingUtilities.convertRectangle(firstKey .getParent(), firstKeyRectangle, parent); if (firstKey.getParent() == null || !firstKey.getParent().isValid()) continue; if (firstKey.getParent().getParent() instanceof SchemaComponent) firstKeyInsetX += firstKey.getParent().getBounds().x; while (firstKeyRectangle.y >= firstRowBottom && firstRowNum < this.rows.size() - 1) firstRowBottom += ((Integer) this.rowHeights .get(++firstRowNum)).intValue(); // Do the same for the second key. int secondRowNum = 0; if (this.rowHeights.size()==0) continue; int secondRowBottom = ((Integer) this.rowHeights .get(secondRowNum)).intValue(); final KeyComponent secondKey = comp.getSecondKeyComponent(); if (secondKey == null) continue; Rectangle secondKeyRectangle = secondKey.getBounds(); int secondKeyInsetX = secondKeyRectangle.x; secondKeyRectangle = SwingUtilities.convertRectangle(secondKey .getParent(), secondKeyRectangle, parent); if (secondKey.getParent() == null || !secondKey.getParent().isValid()) continue; if (secondKey.getParent().getParent() instanceof SchemaComponent) secondKeyInsetX += secondKey.getParent().getBounds().x; while (secondKeyRectangle.y >= secondRowBottom && secondRowNum < this.rows.size() - 1) secondRowBottom += ((Integer) this.rowHeights .get(++secondRowNum)).intValue(); // Work out left/right most. final Rectangle leftKeyRectangle = firstKeyRectangle.x <= secondKeyRectangle.x ? firstKeyRectangle : secondKeyRectangle; final Rectangle rightKeyRectangle = firstKeyRectangle.x > secondKeyRectangle.x ? firstKeyRectangle : secondKeyRectangle; final int leftKeyInsetX = leftKeyRectangle == firstKeyRectangle ? firstKeyInsetX : secondKeyInsetX; final int rightKeyInsetX = rightKeyRectangle == firstKeyRectangle ? firstKeyInsetX : secondKeyInsetX; // Work out Y coord for top of relation. final int relTopY = (int) Math.min(leftKeyRectangle .getCenterY(), rightKeyRectangle.getCenterY()); int relBottomY, relLeftX, relRightX; int leftX, rightX, leftY, rightY, viaX, viaY; int leftTagX, rightTagX; // Both at same X location? if (Math.abs(firstKeyRectangle.x - secondKeyRectangle.x) < 100) { relBottomY = (int) Math.max(leftKeyRectangle.getCenterY(), rightKeyRectangle.getCenterY()); relLeftX = leftKeyRectangle.x - SchemaLayoutManager.TABLE_PADDING; relRightX = rightKeyRectangle.x; leftX = leftKeyRectangle.x - leftKeyInsetX; leftTagX = leftX - SchemaLayoutManager.RELATION_SPACING; leftY = (int) leftKeyRectangle.getCenterY(); rightX = rightKeyRectangle.x - rightKeyInsetX; rightTagX = rightX - SchemaLayoutManager.RELATION_SPACING; rightY = (int) rightKeyRectangle.getCenterY(); viaX = leftX - SchemaLayoutManager.TABLE_PADDING * 2; viaY = (leftY + rightY) / 2; } else { relRightX = (int) Math.max(leftKeyRectangle.getMaxX(), rightKeyRectangle.x); relLeftX = (int) Math.min(leftKeyRectangle.getMaxX(), rightKeyRectangle.x); relBottomY = Math.max(firstRowBottom, secondRowBottom); leftX = (int) leftKeyRectangle.getMaxX() + leftKeyInsetX; leftTagX = leftX + SchemaLayoutManager.RELATION_SPACING; leftY = (int) leftKeyRectangle.getCenterY(); rightX = rightKeyRectangle.x - rightKeyInsetX; rightTagX = rightX - SchemaLayoutManager.RELATION_SPACING; rightY = (int) rightKeyRectangle.getCenterY(); viaX = (leftX + rightX) / 2; if (Math.abs(rightX - leftX) < 100) viaY = (leftY + rightY) / 2; else if (Math.abs(rightY - leftY) > 100) viaY = (relBottomY + relTopY) / 2; else viaY = relTopY + (int) ((relBottomY - relTopY) * 1.8); } // Set overall bounds. final Rectangle bounds = new Rectangle( (Math.min(relLeftX, viaX) - SchemaLayoutManager.RELATION_SPACING * 4), (Math.min(relTopY, viaY) - SchemaLayoutManager.RELATION_SPACING * 4), (Math.abs(Math.max(relRightX, viaX) - Math.min(relLeftX, viaX)) + SchemaLayoutManager.RELATION_SPACING * 8), (Math.abs(Math.max(relBottomY, viaY) - Math.min(relTopY, viaY)) + SchemaLayoutManager.RELATION_SPACING * 8)); comp.setBounds(bounds); // Create a path to describe the relation shape. It // will have 2 components to it - move, curve. final GeneralPath path = new GeneralPath( GeneralPath.WIND_EVEN_ODD, 4); // Move to starting point at primary key. path.moveTo(leftX - bounds.x, leftY - bounds.y); // Left tag. path.lineTo(leftTagX - bounds.x, leftY - bounds.y); // Draw from the first key midpoint across to the vertical // track. path.quadTo(viaX - bounds.x, viaY - bounds.y, rightTagX - bounds.x, rightY - bounds.y); // Right tag. path.lineTo(rightX - bounds.x, rightY - bounds.y); // Set the shape. comp.setLineShape(path); } } } /** * Use this constraint to indicate to the layout manager how much spacing to * give each component. */ public static class SchemaLayoutConstraint { private final int relCount; private int row; /** * Construct a new constraint indicating that the given number of * relations lead off this component, so that space is left for them. * Or, if the component is a relation, this indicates the index off the * table that this relation is so that it bends out of the way of other * relations accordingly. * * @param relCount * the number of relations from this component. */ public SchemaLayoutConstraint(final int relCount) { this.relCount = relCount; this.row = 0; } private int getRelCount() { return this.relCount; } private void setRow(final int row) { this.row = row; } private int getRow() { return this.row; } } }