org.ut.biolab.medsavant.client.view.genetics.variantinfo.VariantFrequencyAggregatePane.java Source code

Java tutorial

Introduction

Here is the source code for org.ut.biolab.medsavant.client.view.genetics.variantinfo.VariantFrequencyAggregatePane.java

Source

/**
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This 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 software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.ut.biolab.medsavant.client.view.genetics.variantinfo;

import com.jidesoft.converter.ConverterContext;
import com.jidesoft.grid.TableHeaderPopupMenuInstaller;
import com.jidesoft.pivot.AggregateTable;
import com.jidesoft.pivot.AggregateTableHeader;
import com.jidesoft.pivot.AggregateTablePopupMenuCustomizer;
import static com.jidesoft.pivot.AggregateTablePopupMenuCustomizer.CONTEXT_MENU_COLLAPSE;
import static com.jidesoft.pivot.AggregateTablePopupMenuCustomizer.CONTEXT_MENU_COLLAPSE_ALL;
import static com.jidesoft.pivot.AggregateTablePopupMenuCustomizer.CONTEXT_MENU_EXPAND;
import static com.jidesoft.pivot.AggregateTablePopupMenuCustomizer.CONTEXT_MENU_EXPAND_ALL;
import static com.jidesoft.pivot.AggregateTablePopupMenuCustomizer.CONTEXT_MENU_GROUP;
import static com.jidesoft.pivot.AggregateTablePopupMenuCustomizer.CONTEXT_MENU_UNGROUP;
import com.jidesoft.pivot.IPivotDataModel;
import com.jidesoft.pivot.PivotConstants;
import com.jidesoft.pivot.PivotField;
import com.jidesoft.pivot.PivotValueProvider;
import com.jidesoft.pivot.SummaryCalculator;
import com.jidesoft.pivot.Value;
import com.jidesoft.pivot.Values;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.rmi.RemoteException;
import java.sql.SQLException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.swing.AbstractCellEditor;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.event.TableColumnModelEvent;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.ut.biolab.medsavant.client.api.Listener;
import org.ut.biolab.medsavant.client.view.login.LoginController;
import org.ut.biolab.medsavant.client.project.ProjectController;
import org.ut.biolab.medsavant.client.util.MedSavantExceptionHandler;
import org.ut.biolab.medsavant.shared.model.Cohort;
import org.ut.biolab.medsavant.shared.model.SessionExpiredException;
import org.ut.biolab.medsavant.shared.vcf.VariantRecord;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumnModel;
import org.apache.commons.lang3.ArrayUtils;
import org.ut.biolab.medsavant.MedSavantClient;
import org.ut.biolab.medsavant.client.util.CacheController;
import org.ut.biolab.medsavant.client.view.component.SplitScreenPanel;
import org.ut.biolab.medsavant.client.view.component.WaitPanel;
import org.ut.biolab.medsavant.client.view.genetics.inspector.ComprehensiveInspector;
import org.ut.biolab.medsavant.client.view.images.IconFactory;
import org.ut.biolab.medsavant.client.view.util.DialogUtils;
import org.ut.biolab.medsavant.client.view.util.ViewUtil;
import org.ut.biolab.medsavant.shared.format.BasicPatientColumns;
import org.ut.biolab.medsavant.shared.format.BasicVariantColumns;
import org.ut.biolab.medsavant.shared.util.ModificationType;
import static org.ut.biolab.medsavant.shared.util.ModificationType.*;

public abstract class VariantFrequencyAggregatePane extends JPanel {

    private static final int MAXIMIUM_VARIANTS_TO_FETCH = 1000;
    //Icon to show on 'link' buttons, which can be clicked to load up other variants in sub inspectors.
    private static final ImageIcon LINK_BUTTON_ICON = IconFactory.getInstance()
            .getIcon(IconFactory.StandardIcon.INSPECTOR);
    private static final int BUTTON_COLUMN_PREFERRED_WIDTH = LINK_BUTTON_ICON.getIconWidth() + 20;
    //The cohort and family entries that correspond to all individuals. (i.e. all distinct dnaIds in the database).
    private static final Cohort ALL_INDIVIDUALS_COHORT = new Cohort(-1, "All Individuals");
    private static final String ALL_INDIVIDUALS_FAMILY = "All Individuals";
    //Various settings that control appearance.
    //private static final Font TABLE_FONT = KeyValuePairPanel.KEY_FONT; //No Longer used.
    private static final Font TABLE_FONT_LARGE = ViewUtil.detailFontPlain;
    private static final Dimension PREFERRED_SIZE = new Dimension(ComprehensiveInspector.INSPECTOR_INNER_WIDTH,
            300);
    //Column names used in the displayed table. 
    private static final String COHORT_COLUMN_NAME = "Cohort";
    private static final String FAMILY_COLUMN_NAME = BasicPatientColumns.FAMILY_ID.getAlias();
    private static final String DNAID_COLUMN_NAME = BasicVariantColumns.DNA_ID.getAlias();
    //Set of all DNAIds that have a variant at the currently selected chromosome and position.
    private Set<String> individualsWithVariantAtThisPosition;
    private String[] aggregateColumns; //The current grouping of columns.
    private AggregateTableHeader header;
    private AggregateTable aggregateTable;
    private SubInspectorTableModel tableModel = new SubInspectorTableModel();
    private JPanel innerPanel;
    //private JPanel splitScreenButtonPanel;
    private boolean isSplitScreen = false;
    private static final Log LOG = LogFactory.getLog(VariantFrequencyAggregatePane.class);
    //Anything >= this column index cannot be grouped, and cannot be moved.
    private int dnaIDIndex;
    //column index of the 'link' buttons in the table.
    private int buttonIndex;
    //Used to prevent rearrangement of the first column, and all columns 
    //with index >= dnaIDIndex.  
    private int fromColumnIndex = -1;
    private int toColumnIndex = -1;
    private MapRefresher mapRefresher; //Thread that refreshes the maps below by invoking server methods.    
    private final Map<String, Set<Cohort>> dnaIDCohortMap = new HashMap<String, Set<Cohort>>();
    private final Map<Cohort, Set<String>> cohortDNAIDMap = new HashMap<Cohort, Set<String>>();
    private final Map<String, Set<String>> dnaIDFamilyIDMap = new HashMap<String, Set<String>>();
    private final Map<String, Set<String>> familyIDdnaIDMap = new HashMap<String, Set<String>>();
    private Listener<Object> variantSelectionListener;
    private JLabel title;
    private SplitScreenPanel splitScreenPanel;
    private String[] columnNames;

    ///////////////////////PUBLIC////////////////////////          
    public abstract Object[] getRow(Cohort cohort, String familyId, VariantRecord variantRecord);

    public abstract String getTitle(String currentFirstColumn);

    public abstract void selectVariant(VariantRecord variantRecord);

    //Constructor
    public VariantFrequencyAggregatePane(String[] columnNames) {
        columnNames = ArrayUtils.removeElement(columnNames, DNAID_COLUMN_NAME);
        columnNames = ArrayUtils.removeElement(columnNames, COHORT_COLUMN_NAME);
        columnNames = ArrayUtils.removeElement(columnNames, FAMILY_COLUMN_NAME);
        columnNames = ArrayUtils.add(ArrayUtils.add(columnNames, DNAID_COLUMN_NAME), "");
        columnNames = ArrayUtils.addAll(new String[] { COHORT_COLUMN_NAME, FAMILY_COLUMN_NAME }, columnNames);
        dnaIDIndex = columnNames.length - 2;
        buttonIndex = columnNames.length - 1;
        this.columnNames = columnNames;

        refreshMaps();
        innerPanel = new JPanel();
        innerPanel.setLayout(new BoxLayout(innerPanel, BoxLayout.Y_AXIS));
        innerPanel.add(new WaitPanel("Loading DNA IDs..."));
        this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
        //outerPanel.add(splitScreenButtonPanel);
        title = new JLabel();
        title.setFont(title.getFont().deriveFont(Font.BOLD));
        this.add(innerPanel);
        setupAggregateColumns(columnNames[0]);

        final VariantFrequencyAggregatePane instance = this;
        final String dbg = columnNames[0];
        CacheController.getInstance().addListener(new Listener<ModificationType>() {
            @Override
            public void handleEvent(ModificationType modificationType) {
                if (modificationType == COHORT || modificationType == PATIENT) {
                    refreshMaps();
                }
            }
        });

    }

    private void setupAggregateColumns(String column) {
        String[] ag = new String[columnNames.length - 2];
        ag[0] = column;
        int j = 1;
        for (int i = 0; i < (columnNames.length - 2); ++i) {
            if (!columnNames[i].equals(ag[0])) {
                ag[j++] = columnNames[i];
            }
        }
        this.aggregateColumns = ag;
    }

    public void reaggregate() {
        aggregateTable.aggregate(this.aggregateColumns);
        setupButtonColumn();
        expandAllButLast();
        this.title.setText(getTitle(this.aggregateColumns[0]));
        aggregateTable.revalidate();
        aggregateTable.repaint();
    }

    public String getFirstColumn() {
        if (this.aggregateColumns != null) {
            return this.aggregateColumns[0];
        } else {
            return "Cohort";
        }
    }

    public void groupBy(String column) {
        setupAggregateColumns(column);
        reaggregate();
    }

    /**
     * Adds a listener for whenever a different variant is selected via this
     * SubInspector. The listener's handleEvent method will be invoked with the
     * VariantRecord corresponding to the selection.
     *
     * @see VariantRecord
     */
    public void setVariantSelectionListener(Listener<Object> l) {
        this.variantSelectionListener = l;
    }

    public VariantFrequencyAggregatePane setSplitScreenPanel(SplitScreenPanel s) {
        this.splitScreenPanel = s;
        return this;
    }

    public boolean isSplit() {
        return isSplitScreen;
    }

    /**
     * Removes most of this SubInspector (except the title and the split-screen
     * toggle button) and makes a split screen view in the main spreadsheet
     * view. top half: spreadsheet; bottom half: this SubInspector.
     */
    public void splitScreen() {
        if (splitScreenPanel != null) {
            if (splitScreenPanel.isSplit()) {
                splitScreenPanel.unsplitScreen();
            }
            splitScreenPanel.splitScreen(innerPanel);
            isSplitScreen = true;
            aggregateTable.setFont(TABLE_FONT_LARGE);
            header.setFont(TABLE_FONT_LARGE);
            this.revalidate();
            this.repaint();
        }
    }

    /**
     * Reverses the effects of splitScreen - i.e. it unsplits the screen.
     *
     * @see splitScreen
     */
    public void unsplitScreen() {
        if (splitScreenPanel != null && splitScreenPanel.isSplit()) {
            splitScreenPanel.unsplitScreen();
            isSplitScreen = false;
        }

    }

    void showWaitPanel(String msg) {
        innerPanel.removeAll();
        innerPanel.add(new WaitPanel(msg));
        innerPanel.revalidate();
        innerPanel.repaint();
    }

    public void setVariantRecords(Set<VariantRecord> records) {
        innerPanel.removeAll();
        //This function executes quickly, no need for waitPanel.
        //innerPanel.add(new WaitPanel("Loading DNA Ids..."));
        tableModel.setValues(records);
        aggregateTable = new AggregateTable(tableModel) {
            @Override
            //Necessary to enable buttons within the table.
            public boolean isCellEditable(int rowIndex, int colIndex) {
                return colIndex == buttonIndex;
            }

            //"Disables" selection within the aggregateTable.
            @Override
            public TableCellRenderer getCellRenderer(final int rowIndex, final int columnIndex) {
                final TableCellRenderer renderer = super.getCellRenderer(rowIndex, columnIndex);
                return new TableCellRenderer() {
                    @Override
                    public Component getTableCellRendererComponent(JTable jtable, final Object o, boolean bln,
                            boolean bln1, int i, int i1) {
                        return renderer.getTableCellRendererComponent(jtable, o, false, false, i, i1);
                    }
                };
            }

            //This overridden method, together with the custom mouse listener on the 
            //AggregateTable header, disallows moving the first column, and columns
            //>= dnaIDIndex                    
            @Override
            protected AggregateTable.DraggingHandler createDraggingColumnPropertyChangeListener() {
                return new AggregateTable.DraggingHandler() {
                    @Override
                    public void columnMoved(TableColumnModelEvent e) {
                        if (fromColumnIndex == -1) {
                            fromColumnIndex = e.getFromIndex();
                        }
                        toColumnIndex = e.getToIndex();
                    }

                };
            }
        };

        header = new AggregateTableHeader(aggregateTable);
        header.addMouseListener(new MouseAdapter() {
            //Disable moving the first column, or columns with index
            //>=dnaIDIndex
            @Override
            public void mouseReleased(MouseEvent e) {
                if (toColumnIndex != -1 && ((toColumnIndex == 0 || fromColumnIndex == 0)
                        || (toColumnIndex >= dnaIDIndex || fromColumnIndex >= dnaIDIndex))) {
                    aggregateTable.moveColumn(toColumnIndex, fromColumnIndex);
                    String msg = "This column cannot be moved.";
                    DialogUtils.displayMessage(msg);
                } else {
                    for (int columnHeaderIndex = 0; columnHeaderIndex < header.getColumnModel().getColumnCount()
                            - 2; columnHeaderIndex++) {
                        String columnTitle = (String) header.getColumnModel().getColumn(columnHeaderIndex)
                                .getHeaderValue();
                        aggregateColumns[columnHeaderIndex] = columnTitle;
                    }
                    reaggregate();
                }
                //aggregateTable.aggregate(this.aggregateColumns);
                fromColumnIndex = -1;
                toColumnIndex = -1;
                //expandAllButLast();
            }
        });

        header.setAutoFilterEnabled(false);
        header.setReorderingAllowed(true);
        header.setFont(TABLE_FONT_LARGE);
        aggregateTable.setTableHeader(header);
        aggregateTable.setAutoResizeMode(AggregateTable.AUTO_RESIZE_ALL_COLUMNS);
        aggregateTable.setFont(TABLE_FONT_LARGE);

        //Setup a custom "summary".  This is what calculates frequencies when cells are
        //collapsed
        PivotField f = aggregateTable.getAggregateTableModel().getField(BasicVariantColumns.DNA_ID.getAlias());
        f.setSummaryType(PivotConstants.SUMMARY_RESERVED_MAX + 1);
        aggregateTable.getAggregateTableModel().setSummaryCalculator(new SummaryCalculator() {
            private Set<String> collapsedDNAIDs = new HashSet<String>();
            private Values lastRowValues;
            private int valueCount = 0;
            private final int SUMMARY_FREQ = PivotConstants.SUMMARY_RESERVED_MAX + 1;

            @Override
            public void addValue(PivotValueProvider dataModel, PivotField field, Values rowValues,
                    Values columnValues, Object object) {
                //this gets called multiple times for all the cells that disappear when 
                //something is collapsed.  
                // row[0] is the value of the corresponding first column: e.g. a Cohort or Family. 
                // columnValues can be ignored (blank)
                // Object is the value of the item in the cell that disappeared. (i.e. those cells which are being summarized)
                // field.getName() is the column name corresponding to Object

                // Useful Debugging code:                      
                //if(field.getName().equals(BasicVariantColumns.DNA_ID.getAlias())){

                /*
                 System.out.println("==========");
                 System.out.println("Field : " + field.getName());
                 System.out.println("Row values: ");
                    
                 for (int i = 0; i < rowValues.getCount(); ++i) {
                 System.out.println("\trow[" + i + "] = " + rowValues.getValueAt(i).getValue());
                    
                 }
                    
                 System.out.println("Column values: ");
                 for (int i = 0; i < columnValues.getCount(); ++i) {
                 System.out.println("\tcol[" + i + "] = " + columnValues.getValueAt(i).getValue());
                 }
                    
                 System.out.println("Object: ");
                 System.out.println("\t" + object);
                 System.out.println("==========");
                 */

                // }
                if (field.getName().equals(BasicVariantColumns.DNA_ID.getAlias())) {
                    collapsedDNAIDs.add((String) object);
                    lastRowValues = rowValues;
                } else {
                    lastRowValues = null;
                }
                valueCount++;
            }

            //Should never be called
            @Override
            public void addValue(Object o) {
                LOG.error("Unexpected method invocation in OtherIndividualsSubInspector (1)");
            }

            //Should never be called
            @Override
            public void addValue(IPivotDataModel ipdm, PivotField pf, int i, int i1, Object o) {
                LOG.error("Unexpected method invocation in OtherIndividualsSubInspector (2)");
            }

            //Should never be called
            @Override
            public void addValue(PivotValueProvider pvp, PivotField pf, Object o) {
                LOG.error("Unexpected method invocation in OtherIndividualsSubInspector (3)");
            }

            @Override
            public void clear() {
                collapsedDNAIDs.clear();
                valueCount = 0;
                lastRowValues = null;
            }

            @Override
            public Object getSummaryResult(int type) {
                //if null, then we're not in the DNAId column.  Return null
                //to show a blank in this cell
                if (lastRowValues == null) {
                    return null;
                }

                int numIndividuals = getNumberOfIndividualsInGroup(lastRowValues.getValueAt(0));

                return new Frequency(collapsedDNAIDs.size(), numIndividuals);
            }

            private int getNumberOfIndividualsInGroup(Value v) {
                if (aggregateColumns[0].equals("Cohort")) {
                    //LOG.debug("Getting number of individuals in group " + v.getValue());
                    Set<String> dnaIds = cohortDNAIDMap.get((Cohort) v.getValue());
                    //for (String id : dnaIds) {
                    //LOG.debug("\tGot id " + id);
                    //}
                    return cohortDNAIDMap.get((Cohort) v.getValue()).size();
                } else if (aggregateColumns[0].equals(BasicPatientColumns.FAMILY_ID.getAlias())) {
                    return familyIDdnaIDMap.get((String) v.getValue()).size();
                } else {
                    LOG.error("Invalid first column");
                    return -1;
                }
            }

            @Override
            public long getCount() {
                return valueCount;
            }

            @Override
            public int getNumberOfSummaries() {
                return 1;
            }

            @Override
            public String getSummaryName(Locale locale, int i) {
                return "Frequency";
            }

            @Override
            public int[] getAllowedSummaries(Class<?> type) {
                return new int[] { SUMMARY_FREQ };
            }

            @Override
            public int[] getAllowedSummaries(Class<?> type, ConverterContext cc) {
                return new int[] { SUMMARY_FREQ };
            }
        });

        //Sets up the context menu for clicking column headers.  This will probably not be used
        //frequently.  Limit the available operations to collapsing, expanding, and grouping.
        TableHeaderPopupMenuInstaller installer = new TableHeaderPopupMenuInstaller(aggregateTable);
        installer.addTableHeaderPopupMenuCustomizer(new AggregateTablePopupMenuCustomizer() {
            @Override
            public void customizePopupMenu(JTableHeader header, JPopupMenu popup, int clickingColumn) {
                super.customizePopupMenu(header, popup, clickingColumn);
                for (int i = 0; i < popup.getComponentCount(); i++) {
                    String menuItemName = popup.getComponent(i).getName();
                    if (!(CONTEXT_MENU_COLLAPSE.equals(menuItemName)
                            || CONTEXT_MENU_COLLAPSE_ALL.equals(menuItemName)
                            || CONTEXT_MENU_EXPAND.equals(menuItemName)
                            || CONTEXT_MENU_EXPAND_ALL.equals(menuItemName)
                            || CONTEXT_MENU_GROUP.equals(menuItemName)
                            || CONTEXT_MENU_UNGROUP.equals(menuItemName))) {

                        popup.remove(popup.getComponent(i));

                    }
                }
            }
        });

        aggregateTable.getAggregateTableModel().setSummaryMode(true);

        aggregateTable.aggregate(aggregateColumns);
        aggregateTable.setShowContextMenu(false);

        expandAllButLast();
        setupButtonColumn();

        JScrollPane jsp = new JScrollPane(aggregateTable);
        jsp.setPreferredSize(PREFERRED_SIZE);

        JPanel bp = new JPanel();

        bp.setLayout(new BoxLayout(bp, BoxLayout.X_AXIS));
        JButton closeButton = new JButton(IconFactory.getInstance().getIcon(IconFactory.StandardIcon.CLOSE));
        closeButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                unsplitScreen();
            }
        });
        bp.add(Box.createHorizontalGlue());
        bp.add(title);
        bp.add(Box.createHorizontalGlue());
        bp.add(closeButton);
        innerPanel.add(bp);
        innerPanel.add(jsp);
        reaggregate();
        innerPanel.revalidate();
        innerPanel.repaint();
    }

    ///////////////////////PRIVATE METHODS
    private void setAggregateColumns(String[] ac) {
        this.aggregateColumns = ac;
    }

    //fully expands all cells in the table except for the cell immediately preceding the
    //DNAId, which is collapsed.  Thus each row of the table corresponds to a unique
    //(Cohort, Family, Ref, Alt, Zygosity) tuple.
    private void expandAllButLast() {
        aggregateTable.expandAll();
        for (int rowIndex = 0; rowIndex <= aggregateTable.getAggregateTableModel().getRowCount(); ++rowIndex) {
            aggregateTable.collapse(rowIndex, dnaIDIndex - 1);
        }
    }

    //associates a family with a dnaId, and vice versa
    private void addFamilyToDnaId(String family, String dnaID) {
        Set<String> familyIds = dnaIDFamilyIDMap.get(dnaID);
        if (familyIds == null) {
            familyIds = new HashSet<String>();
        }
        familyIds.add(family);
        dnaIDFamilyIDMap.put(dnaID, familyIds);

        Set<String> dnaIDs = familyIDdnaIDMap.get(family);
        if (dnaIDs == null) {
            dnaIDs = new HashSet<String>();
        }
        dnaIDs.add(dnaID);

        familyIDdnaIDMap.put(family, dnaIDs);
    }

    //associates a cohort with a dnaId, and vice versa
    private void addCohortToDnaId(Cohort cohort, String dnaId) {
        Set<Cohort> cohorts = dnaIDCohortMap.get(dnaId);
        if (cohorts == null) {
            cohorts = new HashSet<Cohort>();
        }
        cohorts.add(cohort);
        dnaIDCohortMap.put(dnaId, cohorts);

        Set<String> dnaIDs = cohortDNAIDMap.get(cohort);
        if (dnaIDs == null) {
            dnaIDs = new HashSet<String>();
        }
        dnaIDs.add(dnaId);
        cohortDNAIDMap.put(cohort, dnaIDs);
    }

    private void refreshMaps() {
        if (mapRefresher != null && mapRefresher.isAlive()) {
            LOG.info("Interrupting map refresher");
            mapRefresher.interrupt();
        }
        mapRefresher = new MapRefresher();
        mapRefresher.start();
    }

    // Sets up the column with index 'buttonIndex' so that it 
    // can accomadate buttons.
    private void setupButtonColumn() {
        new ButtonColumn(aggregateTable, buttonIndex);
        aggregateTable.getColumnModel().getColumn(buttonIndex).setPreferredWidth(BUTTON_COLUMN_PREFERRED_WIDTH + 5);
        aggregateTable.getColumnModel().getColumn(buttonIndex).setMaxWidth(BUTTON_COLUMN_PREFERRED_WIDTH + 5);
        aggregateTable.getColumnModel().getColumn(buttonIndex).setMinWidth(BUTTON_COLUMN_PREFERRED_WIDTH + 5);
    }

    /////////////////////// INNER CLASSES
    //An item in the popup menu that controls what fraction is displayed.
    //(e.g. fraction of cohort vs fraction of family)
    private class GroupByMenuItem extends JMenuItem implements ActionListener {

        private String[] aggregateColumns;

        public GroupByMenuItem(String[] aggregateColumns, String title) {
            setText(title);
            this.aggregateColumns = aggregateColumns;
            addActionListener(this);
        }

        public GroupByMenuItem(String[] aggregateColumns) {
            this(aggregateColumns, aggregateColumns[0]);
        }

        @Override
        public void actionPerformed(ActionEvent ae) {
            aggregateTable.aggregate(aggregateColumns);
            setAggregateColumns(aggregateColumns);
            setupButtonColumn();
            expandAllButLast();
            aggregateTable.revalidate();
            aggregateTable.repaint();
        }
    };

    //Model for the displayed table.
    private class SubInspectorTableModel extends AbstractTableModel {

        private List<Object[]> tableData = new ArrayList<Object[]>();

        @Override
        public String getColumnName(int col) {
            return columnNames[col].toString();
        }

        @Override
        public int getRowCount() {
            return tableData.size();
        }

        @Override
        public int getColumnCount() {
            return columnNames.length;
        }

        @Override
        public Object getValueAt(int row, int col) {
            Object r = tableData.get(row)[col];
            //sanity check: r should not be null
            if (r == null) {
                LOG.debug("Null table entry for variant");
                return "-";
            }
            return r;
        }

        @Override
        public boolean isCellEditable(int row, int col) {
            return false;
        }

        @Override
        public void setValueAt(Object value, int row, int col) {
            tableData.get(row)[col] = value;
            fireTableCellUpdated(row, col);
        }

        public void setValues(Set<VariantRecord> variantRecords) {
            individualsWithVariantAtThisPosition = new HashSet<String>();
            tableData = new ArrayList<Object[]>(variantRecords.size());
            for (VariantRecord r : variantRecords) {
                String dnaID = r.getDnaID();
                String ref = r.getRef();
                String alt = r.getAlt();
                String zygosity = r.getZygosity().toString();
                individualsWithVariantAtThisPosition.add(dnaID);

                Set<Cohort> cohorts = dnaIDCohortMap.get(dnaID);
                Set<String> familyIds = dnaIDFamilyIDMap.get(dnaID);

                //sanity checks: it should not be possible for either cohorts or 
                //familyIds to be null, as all dnaIds should map to 
                //ALL_INDIVIDUALS_COHORT or ALL_INDIVIDUALS_FAMILY, at the very
                //least.
                if (cohorts == null) {
                    LOG.error("Cohorts is null for dnaID " + dnaID);
                    cohorts = new HashSet<Cohort>();
                    cohorts.add(ALL_INDIVIDUALS_COHORT);
                }

                if (familyIds == null) {
                    LOG.error("Family ID is null for dnaID " + dnaID);
                    familyIds = new HashSet<String>();
                    familyIds.add(ALL_INDIVIDUALS_FAMILY);
                }
                for (Cohort cohort : cohorts) {
                    for (String familyId : familyIds) {
                        Object[] partialRow = getRow(cohort, familyId, r);
                        partialRow = ArrayUtils.addAll(new Object[] { cohort, familyId }, partialRow);
                        Object[] row = ArrayUtils.addAll(partialRow, new Object[] { dnaID, r });

                        tableData.add(row);
                    }
                }
            }
            this.fireTableDataChanged();
        }

    };

    //Thread that refreshes the four one-to-many mappings
    //(dnaId->cohort, dnaId->Family, family->dnaId, cohort->dnaId)
    private class MapRefresher extends Thread {

        @Override
        public void run() {
            synchronized (dnaIDCohortMap) {
                dnaIDCohortMap.clear();
                dnaIDFamilyIDMap.clear();
                try {
                    String sessionID = LoginController.getSessionID();
                    Cohort[] cohorts = MedSavantClient.CohortManager.getCohorts(sessionID,
                            ProjectController.getInstance().getCurrentProjectID());
                    for (Cohort cohort : cohorts) {
                        List<String> dnaIds = MedSavantClient.CohortManager.getDNAIDsForCohort(sessionID,
                                cohort.getId());
                        for (String dnaId : dnaIds) {
                            if (Thread.interrupted()) {
                                return;
                            }
                            addCohortToDnaId(cohort, dnaId);
                        }
                    }

                    Map<Object, List<String>> m = MedSavantClient.PatientManager.getDNAIDsForValues(sessionID,
                            ProjectController.getInstance().getCurrentProjectID(),
                            BasicPatientColumns.FAMILY_ID.getColumnName());
                    for (Map.Entry<Object, List<String>> e : m.entrySet()) {
                        for (String dnaId : e.getValue()) {
                            if (((String) e.getKey()).trim().length() > 0) {
                                addFamilyToDnaId((String) e.getKey(), dnaId);
                            }
                            if (Thread.interrupted()) {
                                return;
                            }
                        }
                    }

                    //FOR DEBUGGING ONLY, with the crm9 database                    
                    //addFamilyToDnaId("110", "330-AD-01");
                    //addFamilyToDnaId("110", "330-DD-03");
                    //addFamilyToDnaId("111", "330-ED-05");
                    //addFamilyToDnaId("111", "330-GD-02");

                    LOG.debug("Counting unique " + BasicVariantColumns.DNA_ID.getColumnName() + " from table "
                            + ProjectController.getInstance().getCurrentVariantTableName());
                    List<String> allDNAIds = MedSavantClient.DBUtils.getDistinctValuesForColumn(sessionID,
                            ProjectController.getInstance().getCurrentVariantTableName(),
                            BasicVariantColumns.DNA_ID.getColumnName(), true); //caching=true

                    LOG.debug("# Distinct DNAIds in Variant Table: " + allDNAIds.size());
                    for (String dnaId : allDNAIds) {
                        addFamilyToDnaId(ALL_INDIVIDUALS_FAMILY, dnaId);
                        //LOG.debug("Mapping " + ALL_INDIVIDUALS_COHORT + " to " + dnaId);
                        addCohortToDnaId(ALL_INDIVIDUALS_COHORT, dnaId);
                    }
                } catch (SQLException se) {
                    LOG.error(se);
                } catch (RemoteException re) {
                    LOG.error(re);
                } catch (SessionExpiredException se) {
                    MedSavantExceptionHandler.handleSessionExpiredException(se);
                } catch (InterruptedException ie) {
                    LOG.error(ie);
                }
            }
        }
    };

    //Handles the calculation and formatting of frequencies from counts.
    private class Frequency {

        private int numerator, denominator;

        public Frequency(int numerator, int denominator) {
            this.numerator = numerator;
            this.denominator = denominator;
        }

        @Override
        public String toString() {
            double frac = numerator / (double) denominator;
            DecimalFormat df = new DecimalFormat("######.##");
            String ret = numerator + " / " + denominator + " (" + df.format(frac) + ")";
            return ret;
        }
    }

    /**
     * Modified from
     * http://tips4java.wordpress.com/2009/07/12/table-button-column/
     *
     * The ButtonColumn class provides a renderer and an editor that looks like
     * a JButton. The renderer and editor will then be used for a specified
     * column in the table. The TableModel will contain the String to be
     * displayed on the button.
     *
     */
    public class ButtonColumn extends AbstractCellEditor
            implements TableCellRenderer, TableCellEditor, MouseListener {

        private JTable table;
        private JButton renderButton;
        private Object editorValue;
        private boolean isButtonColumnEditor;

        public ButtonColumn(JTable table, int column) {
            this.table = table;
            renderButton = new JButton();

            TableColumnModel columnModel = table.getColumnModel();
            columnModel.getColumn(column).setCellRenderer(this);
            columnModel.getColumn(column).setCellEditor(this);
            table.removeMouseListener(this);
            table.addMouseListener(this);
        }

        @Override
        public Component getTableCellEditorComponent(JTable table, final Object value, boolean isSelected, int row,
                int column) {

            if (value instanceof VariantRecord) {
                JButton b = new JButton(LINK_BUTTON_ICON);
                b.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent ae) {
                        fireEditingStopped();
                        VariantRecord vr = (VariantRecord) value;
                        System.out.println("Got VariantRecord " + vr);
                        selectVariant(vr);
                    }
                });
                b.setToolTipText("View information for this variant and individual in Inspector");
                JPanel p = new JPanel();
                p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
                p.setBackground(aggregateTable.getBackground());
                p.add(Box.createHorizontalGlue());
                p.add(b);
                p.add(Box.createHorizontalGlue());
                return p;
            } else {
                return new JLabel("");
            }
        }

        @Override
        public Object getCellEditorValue() {
            return editorValue;
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
                boolean hasFocus, int row, int column) {
            renderButton.setBackground(table.getBackground());
            if (value instanceof VariantRecord) {
                renderButton.setIcon(LINK_BUTTON_ICON);
                renderButton.setToolTipText("View information for this variant and individual in Inspector");
                JPanel p = new JPanel();
                p.setBackground(aggregateTable.getBackground());
                p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
                p.add(Box.createHorizontalGlue());
                p.add(renderButton);
                p.add(Box.createHorizontalGlue());
                return p;
                //return renderButton;
            } else {
                return new JLabel("");
            }
        }

        /*
         *  When the mouse is pressed the editor is invoked. If you then then drag
         *  the mouse to another cell before releasing it, the editor is still
         *  active. Make sure editing is stopped when the mouse is released.
         */
        @Override
        public void mousePressed(MouseEvent e) {
            if (table.isEditing() && table.getCellEditor() == this) {
                isButtonColumnEditor = true;
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            if (isButtonColumnEditor && table.isEditing()) {
                table.getCellEditor().stopCellEditing();
            }

            isButtonColumnEditor = false;
        }

        public void mouseClicked(MouseEvent e) {
        }

        public void mouseEntered(MouseEvent e) {
        }

        public void mouseExited(MouseEvent e) {
        }
    }
}