/* 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.dialogs; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.swing.ButtonGroup; import javax.swing.JCheckBox; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JScrollPane; import javax.swing.WindowConstants; import org.biomart.builder.model.DataSet; import org.biomart.builder.model.Table; import org.biomart.builder.model.TransformationUnit; import org.biomart.builder.model.DataSet.DataSetTable; import org.biomart.builder.model.TransformationUnit.Expression; import org.biomart.builder.model.TransformationUnit.JoinTable; import org.biomart.builder.model.TransformationUnit.SelectFromTable; import org.biomart.builder.model.TransformationUnit.SkipTable; import org.biomart.builder.model.TransformationUnit.UnrollTable; import org.biomart.builder.view.gui.SchemaTabSet; import org.biomart.builder.view.gui.MartTabSet.MartTab; import org.biomart.builder.view.gui.diagrams.ExplainTransformationDiagram; import org.biomart.builder.view.gui.diagrams.components.TableComponent; import org.biomart.builder.view.gui.diagrams.contexts.ExplainContext; import org.biomart.builder.view.gui.diagrams.contexts.TransformationContext; import org.biomart.common.exceptions.BioMartError; import org.biomart.common.resources.Resources; import org.biomart.common.resources.Settings; import org.biomart.common.utils.Transaction; import org.biomart.common.utils.Transaction.TransactionEvent; import org.biomart.common.utils.Transaction.TransactionListener; import org.biomart.common.view.gui.LongProcess; /** * This simple dialog explains a table by drawing a series of diagrams of the * underlying tables and relations involved in it. * <p> * It has two tabs. In the first tab goes an overview diagram. In the second tab * goes a series of smaller diagrams, each one an instance of * {@link ExplainTransformationDiagram} which represents a single step in the * transformation process required to produce the table being explained. * * @author Richard Holland <holland@ebi.ac.uk> * @version $Revision: 1.47 $, $Date: 2008-02-29 11:26:11 $, modified by * $Author: rh4 $ * @since 0.5 */ public class ExplainTableDialog extends JDialog implements TransactionListener { private static final long serialVersionUID = 1; private static final int MAX_UNITS = Settings.getProperty("maxunits") == null ? 50 : Integer.parseInt(Settings.getProperty("maxunits")); private JCheckBox maskedHidden; private boolean needsRebuild; /** * Opens an explanation showing the underlying relations and tables behind a * specific dataset table. * * @param martTab * the mart tab which will handle menu events. * @param table * the table to explain. */ public static void showTableExplanation(final MartTab martTab, final DataSetTable table) { new ExplainTableDialog(martTab, table).setVisible(true); } private final SchemaTabSet schemaTabSet; private final DataSet ds; private final DataSetTable dsTable; private GridBagConstraints fieldConstraints; private GridBagConstraints fieldLastRowConstraints; private GridBagConstraints labelConstraints; private GridBagConstraints labelLastRowConstraints; private MartTab martTab; private JPanel transformation; private final List transformationTableDiagrams = new ArrayList(); private TransformationContext transformationContext; private final ExplainContext explainContext; private final PropertyChangeListener listener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { ExplainTableDialog.this.needsRebuild = true; } }; private final PropertyChangeListener recalcListener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { ExplainTableDialog.this.maskedHidden .setSelected(ExplainTableDialog.this.dsTable .isExplainHideMasked()); ExplainTableDialog.this.recalculateTransformation(); } }; /** * The background for the masked checkbox. */ public static final Color MASK_BG_COLOR = Color.WHITE; private ExplainTableDialog(final MartTab martTab, final DataSetTable dsTable) { // Create the blank dialog, and give it an appropriate title. super(); this.setTitle(Resources.get("explainTableDialogTitle", dsTable .getModifiedName())); this.setModal(true); this.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); this.ds = dsTable.getDataSet(); this.dsTable = dsTable; this.martTab = martTab; this.schemaTabSet = martTab.getSchemaTabSet(); // Create a context. this.explainContext = new ExplainContext(this.martTab, this.ds, dsTable); // Create the hide masked box. this.maskedHidden = new JCheckBox(Resources.get("hideMaskedTitle")); this.maskedHidden.setSelected(dsTable.isExplainHideMasked()); this.maskedHidden.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { dsTable .setExplainHideMasked(ExplainTableDialog.this.maskedHidden .isSelected()); } }); // It has a semi-transparent background with no border. this.maskedHidden.setOpaque(true); this.maskedHidden.setBackground(ExplainTableDialog.MASK_BG_COLOR); // Weak-listen to DataSetTable.explainHideMasked and recalc on change. dsTable.addPropertyChangeListener("explainHideMasked", this.recalcListener); // Make the content pane. final JPanel displayArea = new JPanel(new CardLayout()); // Attach the context to the schema tabset. this.schemaTabSet.setDiagramContext(this.explainContext); // Must be set visible as previous display location is invisible. this.schemaTabSet.setVisible(true); displayArea.add(this.schemaTabSet, "WINDOW_CARD"); // Create constraints for labels that are not in the last row. this.labelConstraints = new GridBagConstraints(); this.labelConstraints.gridwidth = GridBagConstraints.RELATIVE; this.labelConstraints.fill = GridBagConstraints.HORIZONTAL; this.labelConstraints.anchor = GridBagConstraints.LINE_END; this.labelConstraints.insets = new Insets(0, 2, 0, 0); // Create constraints for fields that are not in the last row. this.fieldConstraints = new GridBagConstraints(); this.fieldConstraints.gridwidth = GridBagConstraints.REMAINDER; this.fieldConstraints.fill = GridBagConstraints.NONE; this.fieldConstraints.anchor = GridBagConstraints.LINE_START; this.fieldConstraints.insets = new Insets(0, 1, 0, 2); // Create constraints for labels that are in the last row. this.labelLastRowConstraints = (GridBagConstraints) this.labelConstraints .clone(); this.labelLastRowConstraints.gridheight = GridBagConstraints.REMAINDER; // Create constraints for fields that are in the last row. this.fieldLastRowConstraints = (GridBagConstraints) this.fieldConstraints .clone(); this.fieldLastRowConstraints.gridheight = GridBagConstraints.REMAINDER; // Compute the transformation diagram. this.transformation = new JPanel(new GridBagLayout()); displayArea.add(new JScrollPane(this.transformation), "TRANSFORMATION_CARD"); // Create panel which contains the buttons. final JPanel buttonsPanel = new JPanel(); // Create the button that selects the window card. final JRadioButton windowButton = new JRadioButton(Resources .get("windowButtonName")); windowButton.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { if (e.getSource() == windowButton) { final CardLayout cards = (CardLayout) displayArea .getLayout(); cards.show(displayArea, "WINDOW_CARD"); } } }); // Create the button that selects the transformation card. final JRadioButton transformationButton = new JRadioButton(Resources .get("transformationButtonName")); transformationButton.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { if (e.getSource() == transformationButton) { final CardLayout cards = (CardLayout) displayArea .getLayout(); cards.show(displayArea, "TRANSFORMATION_CARD"); } } }); // Add the card buttons to the panel. buttonsPanel.add(windowButton); buttonsPanel.add(transformationButton); // Make buttons mutually exclusive. final ButtonGroup buttons = new ButtonGroup(); buttons.add(windowButton); buttons.add(transformationButton); // Set up our content pane. final JPanel content = new JPanel(new BorderLayout()); this.setContentPane(content); // Add the display area to the pane. content.add(buttonsPanel, BorderLayout.NORTH); content.add(displayArea, BorderLayout.CENTER); // Work out what size we want the diagram to be. final Dimension size = this.schemaTabSet.getPreferredSize(); final Dimension maxSize = this.martTab.getSize(); // The +20s in the following are to cater for scrollbar widths // and window borders. size.width = Math.max(100, Math .min(size.width + 20, maxSize.width - 20)); size.height = Math.max(100, Math.min(size.height + 20, maxSize.height - 20)); content.setPreferredSize(size); // Make a context for our sub-diagrams. this.transformationContext = new TransformationContext(this.martTab, this.ds); // Pack the window. this.pack(); // Move ourselves. this.setLocationRelativeTo(null); // Add a listener to the dataset such that if any part of the dataset // changes, we recalculate ourselves entirely. this.needsRebuild = false; Transaction.addTransactionListener(this); this.ds.addPropertyChangeListener("directModified", this.listener); // Select the default button (which shows the transformation card). // We must physically click on it to make the card show. this.recalculateTransformation(); transformationButton.doClick(); } public boolean isDirectModified() { return false; } public void setDirectModified(final boolean modified) { // Ignore, for now. } public boolean isVisibleModified() { return false; } public void setVisibleModified(final boolean modified) { // Ignore, for now. } public void transactionResetDirectModified() { // Ignore, for now. } public void transactionResetVisibleModified() { // Ignore, for now. } public void transactionStarted(final TransactionEvent evt) { // Ignore, for now. } public void transactionEnded(final TransactionEvent evt) { if (this.needsRebuild) this.recalculateTransformation(); } private void recalculateTransformation() { this.needsRebuild = false; new LongProcess() { public void run() throws Exception { // Keep a note of shown tables. final Map shownTables = new HashMap(); for (int i = 1; i <= ExplainTableDialog.this.transformationTableDiagrams .size(); i++) { final TableComponent[] comps = ((ExplainTransformationDiagram) ExplainTableDialog.this.transformationTableDiagrams .get(i - 1)).getTableComponents(); final Map map = new HashMap(); shownTables.put("" + i, map); for (int j = 0; j < comps.length; j++) map.put(((Table) comps[j].getObject()).getName(), comps[j].getState()); } // Clear the transformation box. ExplainTableDialog.this.transformation.removeAll(); ExplainTableDialog.this.transformationTableDiagrams.clear(); // Keep track of columns counted so far. final List columnsSoFar = new ArrayList(); // Count our steps. int stepNumber = 1; // If more than a set limit of units, we hit memory // and performance issues. Refuse to do the display and // instead put up a helpful message. Limit should be // configurable from a properties file. final Collection units = new ArrayList( ExplainTableDialog.this.dsTable .getTransformationUnits()); // To prevent // concmod. if (units.size() > ExplainTableDialog.MAX_UNITS) ExplainTableDialog.this.transformation.add(new JLabel( Resources.get("tooManyUnits", "" + ExplainTableDialog.MAX_UNITS)), ExplainTableDialog.this.fieldLastRowConstraints); else { // Insert show/hide hidden steps button. ExplainTableDialog.this.transformation.add(new JLabel(), ExplainTableDialog.this.labelConstraints); JPanel field = new JPanel(); field.add(ExplainTableDialog.this.maskedHidden); ExplainTableDialog.this.transformation.add(field, ExplainTableDialog.this.fieldConstraints); // Iterate over transformation units. for (final Iterator i = units.iterator(); i.hasNext();) { final TransformationUnit tu = (TransformationUnit) i .next(); // Holders for our stuff. final JLabel label; final ExplainTransformationDiagram diagram; Map map = (Map) shownTables.get("" + stepNumber); if (map==null) map = Collections.EMPTY_MAP; // Draw the unit. if (tu instanceof Expression) { // Do an expression column list. label = new JLabel( Resources .get( "stepTableLabel", new String[] { "" + stepNumber, Resources .get("explainExpressionLabel") })); diagram = new ExplainTransformationDiagram.AdditionalColumns( ExplainTableDialog.this.martTab, tu, stepNumber, ExplainTableDialog.this.explainContext, map); } else if (tu instanceof SkipTable) { // Don't show these if we're hiding masked things. if (ExplainTableDialog.this.dsTable .isExplainHideMasked()) continue; // Temp table to schema table join. label = new JLabel( Resources .get( "stepTableLabel", new String[] { "" + stepNumber, Resources .get("explainSkipLabel") })); diagram = new ExplainTransformationDiagram.SkipTempReal( ExplainTableDialog.this.martTab, (SkipTable) tu, columnsSoFar, stepNumber, ExplainTableDialog.this.explainContext, map); } else if (tu instanceof UnrollTable) { // Temp table to schema table join. label = new JLabel( Resources .get( "stepTableLabel", new String[] { "" + stepNumber, Resources .get("explainUnrollLabel") })); diagram = new ExplainTransformationDiagram.TempUnrollReal( ExplainTableDialog.this.martTab, (UnrollTable) tu, columnsSoFar, stepNumber, ExplainTableDialog.this.explainContext, map); } else if (tu instanceof JoinTable) { // Temp table to schema table join. label = new JLabel( Resources .get( "stepTableLabel", new String[] { "" + stepNumber, Resources .get("explainMergeLabel") })); diagram = new ExplainTransformationDiagram.TempReal( ExplainTableDialog.this.martTab, (JoinTable) tu, columnsSoFar, stepNumber, ExplainTableDialog.this.explainContext, map); } else if (tu instanceof SelectFromTable) { // Do a single-step select. label = new JLabel( Resources .get( "stepTableLabel", new String[] { "" + stepNumber, Resources .get("explainSelectLabel") })); diagram = new ExplainTransformationDiagram.SingleTable( ExplainTableDialog.this.martTab, (SelectFromTable) tu, stepNumber, ExplainTableDialog.this.explainContext, map); } else throw new BioMartError(); // Display the diagram. ExplainTableDialog.this.transformation .add( label, i.hasNext() ? ExplainTableDialog.this.labelConstraints : ExplainTableDialog.this.labelLastRowConstraints); diagram .setDiagramContext(ExplainTableDialog.this.transformationContext); field = new JPanel(); field.add(diagram); ExplainTableDialog.this.transformation .add( field, i.hasNext() ? ExplainTableDialog.this.fieldConstraints : ExplainTableDialog.this.fieldLastRowConstraints); // Add columns from this unit to the transformed table. columnsSoFar.addAll(tu.getNewColumnNameMap().values()); stepNumber++; // Remember what tables we just added. ExplainTableDialog.this.transformationTableDiagrams .add(diagram); } } // Resize the diagram to fit the components. ExplainTableDialog.this.transformation.revalidate(); ExplainTableDialog.this.transformation.repaint(); } }.start(); } }