/*
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;
/**
* This layout manager lays out components in rows, grouped by the main dataset
* table they are associated with. The main table itself is always first on each
* row.
*
* @author Richard Holland <holland@ebi.ac.uk>
* @version $Revision: 1.15 $, $Date: 2007-10-26 12:24:08 $, modified by
* $Author: rh4 $
* @since 0.6
*/
public class DataSetLayoutManager 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 mainTables;
private final List dimensionTables;
private final List relations;
private final List rowHeights;
private final List rowWidths;
private final Collection fixedComps;
/**
* Sets up some defaults for the layout, ready for use.
*/
public DataSetLayoutManager() {
this.sizeKnown = true;
this.size = new Dimension(0, 0);
this.mainTables = new ArrayList();
this.rowHeights = new ArrayList();
this.rowWidths = new ArrayList();
this.dimensionTables = new ArrayList();
this.relations = new ArrayList();
this.fixedComps = new HashSet();
}
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();
// We have the same number of rows as main/subclass tables.
for (int rowNum = 0; rowNum < this.mainTables.size(); rowNum++) {
int rowHeight = 0;
int rowWidth = 0;
Component comp = (Component) this.mainTables.get(rowNum);
// Start each row by working out space required for
// the main/subclass table that begins it.
if (comp != null) {
final Dimension prefSize = comp.getPreferredSize();
this.prefSizes.put(comp, prefSize);
rowHeight = prefSize.height;
rowWidth = prefSize.width;
}
// Update the row width to accommodate the dimensions too.
for (final Iterator i = ((List) this.dimensionTables
.get(rowNum)).iterator(); i.hasNext();) {
comp = (Component) i.next();
if (!comp.isVisible())
continue;
final Dimension prefSize = comp.getPreferredSize();
this.prefSizes.put(comp, prefSize);
rowHeight = Math.max(rowHeight, prefSize.height);
rowWidth += prefSize.width;
}
// Pad the row with space for relations.
rowHeight += DataSetLayoutManager.TABLE_PADDING * 2;
rowHeight += ((List) this.dimensionTables.get(rowNum)).size()
* DataSetLayoutManager.RELATION_SPACING;
this.rowHeights.set(rowNum, new Integer(rowHeight));
this.size.height += rowHeight;
// Allow horizontal space for relations between each dimension.
rowWidth += DataSetLayoutManager.TABLE_PADDING * 2;
rowWidth += (((List) this.dimensionTables.get(rowNum)).size() + 1)
* DataSetLayoutManager.TABLE_PADDING * 2;
rowWidth += ((List) this.dimensionTables.get(rowNum)).size()
* DataSetLayoutManager.RELATION_SPACING * 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);
else if (comp instanceof DiagramComponent
&& constraints instanceof DataSetLayoutConstraint) {
this.constraints.put(comp, constraints);
final Dimension prefSize = comp.getPreferredSize();
this.prefSizes.put(comp, prefSize);
final int rowNum = ((DataSetLayoutConstraint) constraints)
.getRow();
// Ensure arrays are large enough.
while (this.mainTables.size() - 1 < rowNum) {
this.mainTables.add(null);
this.rowHeights.add(new Integer(0));
this.rowWidths.add(new Integer(
DataSetLayoutManager.TABLE_PADDING * 2));
this.dimensionTables.add(new ArrayList());
}
// Work out where to put it.
if (((DataSetLayoutConstraint) constraints).getType() == DataSetLayoutConstraint.MAIN)
this.mainTables.set(rowNum, comp);
else
((List) this.dimensionTables.get(rowNum)).add(comp);
// Update the row width to accommodate it.
final int oldRowWidth = ((Integer) this.rowWidths.get(rowNum))
.intValue();
int newRowWidth = oldRowWidth;
newRowWidth += prefSize.width
+ DataSetLayoutManager.TABLE_PADDING * 2
+ DataSetLayoutManager.RELATION_SPACING * 2;
this.rowWidths.set(rowNum, new Integer(newRowWidth));
// Increase the row height if necessary.
final int oldRowHeight = ((Integer) this.rowHeights.get(rowNum))
.intValue();
final int newRowHeight = Math.max(oldRowHeight, prefSize.height
+ DataSetLayoutManager.TABLE_PADDING * 2)
+ DataSetLayoutManager.RELATION_SPACING;
this.rowHeights.set(rowNum, new Integer(newRowHeight));
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);
else {
final DataSetLayoutConstraint constraints = (DataSetLayoutConstraint) this.constraints
.remove(comp);
final Dimension prefSize = comp.getPreferredSize();
this.prefSizes.remove(comp);
final int rowNum = constraints.getRow();
// Work out where to remove it from.
if (constraints.getType() == DataSetLayoutConstraint.MAIN)
this.mainTables.set(rowNum, null);
else
((List) this.dimensionTables.get(rowNum)).remove(comp);
// Reduce the row width 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
+ DataSetLayoutManager.TABLE_PADDING * 2
+ DataSetLayoutManager.RELATION_SPACING * 2;
this.rowWidths.set(rowNum, new Integer(newRowWidth));
// If the row maximum height is now too big, reduce it.
int newRowHeight = this.mainTables.get(rowNum) != null ? ((Component) this.mainTables
.get(rowNum)).getPreferredSize().height
: 0;
for (final Iterator i = ((List) this.dimensionTables
.get(rowNum)).iterator(); i.hasNext();)
newRowHeight = Math.max(newRowHeight,
((Component) i.next()).getPreferredSize().height);
newRowHeight += DataSetLayoutManager.TABLE_PADDING * 2
+ ((List) this.dimensionTables.get(rowNum)).size()
* DataSetLayoutManager.RELATION_SPACING;
this.rowHeights.set(rowNum, new Integer(newRowHeight));
this.size.height -= oldRowHeight - newRowHeight;
// While last row is empty, remove last row.
int lastRow = this.mainTables.size() - 1;
while (lastRow >= 0 && this.mainTables.get(lastRow) == null
&& ((List) this.dimensionTables.get(lastRow)).isEmpty()) {
// Remove all references to empty row.
this.mainTables.remove(lastRow);
this.rowHeights.remove(lastRow);
this.rowWidths.remove(lastRow);
this.dimensionTables.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();
// Lay out each row at a time.
int nextY = DataSetLayoutManager.TABLE_PADDING
+ maskedButton.height;
for (int rowNum = 0; rowNum < this.mainTables.size(); rowNum++) {
int x = DataSetLayoutManager.TABLE_PADDING * 3;
final int y = nextY
+ ((Integer) this.rowHeights.get(rowNum)).intValue()
- ((List) this.dimensionTables.get(rowNum)).size()
* DataSetLayoutManager.RELATION_SPACING
- DataSetLayoutManager.TABLE_PADDING;
// First of all print the main/subclass table.
if (this.mainTables.get(rowNum) != null) {
final Component comp = (Component) this.mainTables
.get(rowNum);
final Dimension prefSize = (Dimension) this.prefSizes
.get(comp);
comp.setBounds(x, y - prefSize.height, prefSize.width,
prefSize.height);
comp.validate();
x += prefSize.width + DataSetLayoutManager.TABLE_PADDING
* 2
+ ((List) this.dimensionTables.get(rowNum)).size()
* DataSetLayoutManager.RELATION_SPACING;
}
// Then all the dimensions for that table.
for (final Iterator i = ((List) this.dimensionTables
.get(rowNum)).iterator(); i.hasNext();) {
final Component comp = (Component) i.next();
if (!comp.isVisible())
continue;
final Dimension prefSize = (Dimension) this.prefSizes
.get(comp);
comp.setBounds(x, y - prefSize.height, prefSize.width,
prefSize.height);
comp.validate();
x += prefSize.width + DataSetLayoutManager.TABLE_PADDING
* 2 + DataSetLayoutManager.RELATION_SPACING;
}
nextY += ((Integer) this.rowHeights.get(rowNum)).intValue();
}
// Finally print all relations.
for (final Iterator i = this.relations.iterator(); i.hasNext();) {
final RelationComponent comp = (RelationComponent) i.next();
// Obtain keys and work out position relative to
// diagram.
int rowNum = 0;
int rowBottom = maskedButton.height
+ ((Integer) this.rowHeights.get(rowNum)).intValue();
final KeyComponent firstKey = comp.getFirstKeyComponent();
final KeyComponent secondKey = comp.getSecondKeyComponent();
if (firstKey == null || firstKey.getParent() == null
|| !firstKey.isVisible() || !firstKey.getParent().isValid())
continue;
if (secondKey == null || secondKey.getParent() == null
|| !secondKey.isVisible()
|| !secondKey.getParent().isValid())
continue;
// Update key locations.
Rectangle firstKeyRectangle = firstKey.getBounds();
final int firstKeyInsetX = firstKeyRectangle.x;
Rectangle secondKeyRectangle = secondKey.getBounds();
final int secondKeyInsetX = secondKeyRectangle.x;
firstKeyRectangle = SwingUtilities.convertRectangle(firstKey
.getParent(), firstKeyRectangle, parent);
secondKeyRectangle = SwingUtilities.convertRectangle(secondKey
.getParent(), secondKeyRectangle, parent);
// Work out true row bottom.
while ((firstKeyRectangle.y >= rowBottom || secondKeyRectangle.y >= rowBottom)
&& rowNum < this.rowHeights.size() - 1)
rowBottom += ((Integer) this.rowHeights.get(++rowNum))
.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 (firstKeyRectangle.x == secondKeyRectangle.x) {
// Main/Subclass -> Subclass
relBottomY = (int) Math.max(leftKeyRectangle.getCenterY(),
rightKeyRectangle.getCenterY());
relLeftX = leftKeyRectangle.x
- DataSetLayoutManager.TABLE_PADDING;
relRightX = rightKeyRectangle.x;
leftX = leftKeyRectangle.x - leftKeyInsetX;
leftTagX = leftX - DataSetLayoutManager.RELATION_SPACING;
leftY = (int) leftKeyRectangle.getCenterY();
rightX = rightKeyRectangle.x - rightKeyInsetX;
rightTagX = rightX - DataSetLayoutManager.RELATION_SPACING;
rightY = (int) rightKeyRectangle.getCenterY();
viaX = leftX - DataSetLayoutManager.TABLE_PADDING * 2;
viaY = (leftY + rightY) / 2;
} else {
// Main/Subclass -> Dimension
relRightX = rightKeyRectangle.x;
relLeftX = (int) leftKeyRectangle.getMaxX();
relBottomY = rowBottom;
leftX = (int) leftKeyRectangle.getMaxX() + leftKeyInsetX;
leftTagX = leftX + DataSetLayoutManager.RELATION_SPACING;
leftY = (int) leftKeyRectangle.getCenterY();
rightX = rightKeyRectangle.x - rightKeyInsetX;
rightTagX = rightX - DataSetLayoutManager.RELATION_SPACING;
rightY = (int) rightKeyRectangle.getCenterY();
viaX = leftX
+ ((List) this.dimensionTables.get(rowNum)).size()
* DataSetLayoutManager.RELATION_SPACING / 2;
viaY = relTopY + (int) ((relBottomY - relTopY) * 1.8);
}
// Set overall bounds.
final Rectangle bounds = new Rectangle(
(Math.min(relLeftX, viaX) - DataSetLayoutManager.RELATION_SPACING * 4),
(Math.min(relTopY, viaY) - DataSetLayoutManager.RELATION_SPACING * 4),
(Math.abs(Math.max(relRightX, viaX)
- Math.min(relLeftX, viaX)) + DataSetLayoutManager.RELATION_SPACING * 8),
(Math.abs(Math.max(relBottomY, viaY)
- Math.min(relTopY, viaY)) + DataSetLayoutManager.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 class to specify which row and what type each table should be.
*/
public static class DataSetLayoutConstraint {
/**
* This component is a main/subclass table.
*/
public static final int MAIN = 1;
/**
* This component is a dimension table.
*/
public static final int DIMENSION = 2;
private final int type;
private final int row;
/**
* Construct a constraint indicating which row the component should go
* on, and what type it is.
*
* @param type
* the type of component (see {@link #MAIN} and
* {@link #DIMENSION}).
* @param row
* the row to put it on (zero-indexed).
*/
public DataSetLayoutConstraint(final int type, final int row) {
this.type = type;
this.row = row;
}
private int getType() {
return this.type;
}
private int getRow() {
return this.row;
}
}
}