/*
* Copyright (c) 2003-2012 Fred Hutchinson Cancer Research Center
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.fhcrc.cpl.viewer.quant.gui;
import org.apache.log4j.Logger;
import org.fhcrc.cpl.toolbox.proteomics.filehandler.ProtXmlReader;
import org.fhcrc.cpl.toolbox.proteomics.filehandler.ProteinGroup;
import org.fhcrc.cpl.toolbox.ApplicationContext;
import org.fhcrc.cpl.toolbox.gui.ListenerHelper;
import org.fhcrc.cpl.toolbox.gui.SwingUtils;
import org.fhcrc.cpl.viewer.Localizer;
import org.fhcrc.cpl.viewer.gui.WorkbenchFileChooser;
import javax.swing.*;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
import javax.xml.stream.XMLStreamException;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.util.*;
import java.util.List;
/**
* A window displaying a table with info on all the quantitated proteins in a protXML file. A single
* row, or multiple may be selected and the quantitative events pulled up in a ProteinQuantSummaryFrame.
*
* Gene information for proteins can optionally be pulled from a protein-gene mapping file, in which case the
* Gene column of the table will be populated.
*
* The table is sortable on all columns
*/
public class ProteinSummarySelectorFrame extends JFrame
{
protected static Logger _log = Logger.getLogger(ProteinSummarySelectorFrame.class);
//dimensions
protected int width = 750;
protected int height = 800;
//This is hacky. It's for sizing the window appropriately when we know the number of table rows. There
//is probably a much more reasonable way to do this in AWT.
protected final int TITLEBAR_HEIGHT = 55;
protected final int STATUSPANEL_HEIGHT = 25;
protected final int SUMMARYPANEL_HEIGHT = 81;
protected int LOGRATIO_HISTOGRAM_PANEL_HEIGHT = 150;
protected final int TABLEROW_HEIGHT = 17;
//the main table
protected ProteinSummaryTable proteinSummaryTable;
protected ListSelectionModel tableSelectionModel;
//Track the selected protein
protected List<ProtXmlReader.Protein> selectedProteins = null;
//All the proteins in the table
protected java.util.List<ProtXmlReader.Protein> proteins;
//map from proteins to protein group numbers, so that you san sort on group number
protected Map<ProtXmlReader.Protein, Integer> proteinGroupNumberMap;
//ProteinProphet cutoff for display in the table
protected float minProteinProphet = 0.75f;
//min and max ratio for display in table. Protein must be > min OR < max, or both
protected float minHighRatio = 0f;
protected float maxLowRatio = 999f;
protected boolean allowMultipleSelection = true;
//Containers
public JPanel contentPanel;
public JPanel summaryPanel;
public JPanel mainPanel;
public PanelWithLogRatioHistAndFields logRatioHistogramPanel;
//buttons
protected JButton buttonShowEvents = new JButton("Show Events");
protected JButton buttonSaveTSV = new JButton("Save Table");
protected JButton buttonSelectedProtein = new JButton("DUMMY");
protected JButton buttonSelectAllVisible = new JButton("Select All Visible");
protected JButton buttonDeselectAll = new JButton("Clear All");
// protected PanelWithHistogram logRatioHistogram;
//Status message
public JPanel statusPanel;
public JLabel messageLabel;
//protein ratio filter control
public JLabel maxLowRatioLabel;
public JLabel minHighRatioLabel;
public JLabel numPassingProteinsLabel;
//Map from proteins to genes, for displaying a Gene column
protected Map<String, List<String>> proteinGeneMap;
public ProteinSummarySelectorFrame()
{
super();
}
public ProteinSummarySelectorFrame(boolean allowMultipleSelection)
{
this.allowMultipleSelection = allowMultipleSelection;
}
public ProteinSummarySelectorFrame(File protXmlFile)
throws XMLStreamException, FileNotFoundException
{
this();
displayProteins(protXmlFile);
}
/**
* Initialize GUI components
*/
protected void initGUI()
{
setTitle("Protein Summary");
setSize(width, height);
try
{
Localizer.renderSwixml("org/fhcrc/cpl/viewer/quant/gui/ProteinSummarySelectorFrame.xml",this);
assert null != contentPanel;
setContentPane(contentPanel);
}
catch (Exception x)
{
ApplicationContext.errorMessage("error creating dialog", x);
throw new RuntimeException(x);
}
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.BOTH;
gbc.anchor = GridBagConstraints.PAGE_START;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.insets = new Insets(0,0,0,0);
gbc.weighty = 1;
gbc.weightx = 1;
ListenerHelper helper = new ListenerHelper(this);
summaryPanel.setMaximumSize(new Dimension(1200, 80));
gbc.fill = GridBagConstraints.NONE;
gbc.insets = new Insets(5,5,5,5);
buttonShowEvents.setEnabled(false);
helper.addListener(buttonShowEvents, "buttonShowEvents_actionPerformed");
gbc.gridwidth = 1;
summaryPanel.add(buttonShowEvents, gbc);
buttonSelectAllVisible.setEnabled(false);
helper.addListener(buttonSelectAllVisible, "buttonSelectAllVisible_actionPerformed");
summaryPanel.add(buttonSelectAllVisible, gbc);
buttonDeselectAll.setEnabled(false);
helper.addListener(buttonDeselectAll, "buttonDeselectAll_actionPerformed");
summaryPanel.add(buttonDeselectAll, gbc);
buttonSaveTSV.setEnabled(false);
buttonSaveTSV.setToolTipText("Save the contents of this table to a tab-separated-value file");
helper.addListener(buttonSaveTSV, "buttonSaveTSV_actionPerformed");
gbc.gridwidth = GridBagConstraints.RELATIVE;
summaryPanel.add(buttonSaveTSV, gbc);
JButton buttonCancel = new JButton("Cancel");
helper.addListener(buttonCancel, "buttonCancel_actionPerformed");
gbc.gridwidth = GridBagConstraints.REMAINDER;
summaryPanel.add(buttonCancel, gbc);
gbc.fill = GridBagConstraints.BOTH;
gbc.insets = new Insets(0,0,0,0);
proteinSummaryTable = new ProteinSummaryTable();
if (proteinGeneMap != null)
proteinSummaryTable.proteinGeneMap = proteinGeneMap;
tableSelectionModel = proteinSummaryTable.getSelectionModel();
if (allowMultipleSelection)
tableSelectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
//when a protein is selected, enable the "show events" button
tableSelectionModel.addListSelectionListener(new ListSelectionListener()
{
public void valueChanged(ListSelectionEvent e)
{
if (!e.getValueIsAdjusting())
{
ListSelectionModel lsm = (ListSelectionModel) e.getSource();
if (!lsm.isSelectionEmpty())
{
buttonShowEvents.setEnabled(true);
}
}
}
});
JScrollPane summaryTableScrollPane = new JScrollPane();
summaryTableScrollPane.setViewportView(proteinSummaryTable);
mainPanel.add(summaryTableScrollPane, gbc);
logRatioHistogramPanel = new PanelWithLogRatioHistAndFields();
logRatioHistogramPanel.setBorder(BorderFactory.createTitledBorder("Log Ratios"));
logRatioHistogramPanel.setPreferredSize(new Dimension(width-10, 300));
gbc.fill = GridBagConstraints.BOTH;
gbc.weighty=100;
gbc.gridwidth = GridBagConstraints.REMAINDER;
add(logRatioHistogramPanel, gbc);
statusPanel = new JPanel();
gbc.weighty=1;
statusPanel.setPreferredSize(new Dimension(width-10, 50));
messageLabel = new JLabel();
statusPanel.add(messageLabel, gbc);
add(statusPanel, gbc);
contentPanel.updateUI();
}
/**
* Define the mapping from protein names to gene symbols
* @param proteinGeneMap
*/
public void setProteinGeneMap(Map<String, List<String>> proteinGeneMap)
{
this.proteinGeneMap = proteinGeneMap;
if (proteinSummaryTable != null)
proteinSummaryTable.proteinGeneMap = proteinGeneMap;
}
/**
* Display all the proteins in the protXML file that pass the ProteinProphet threshold and have ratios
* @param protXmlFile
* @throws XMLStreamException
* @throws FileNotFoundException
*/
public void displayProteins(File protXmlFile)
throws XMLStreamException, FileNotFoundException, IllegalArgumentException
{
initGUI();
proteins = new ArrayList<ProtXmlReader.Protein>();
ProtXmlReader protXmlReader = new ProtXmlReader(protXmlFile);
ProtXmlReader.ProteinGroupIterator groupIterator = protXmlReader.iterator();
proteinGroupNumberMap = new HashMap<ProtXmlReader.Protein, Integer>();
List<Float> proteinLogRatios = new ArrayList<Float>();
while (groupIterator.hasNext())
{
ProteinGroup proteinGroup = groupIterator.next();
for (ProtXmlReader.Protein protein : proteinGroup.getProteins())
{
ProtXmlReader.QuantitationRatio quantRatio = protein.getQuantitationRatio();
if (protein.getProbability() > minProteinProphet && quantRatio != null)
{
proteinLogRatios.add((float) Math.log(quantRatio.getRatioMean()));
proteins.add(protein);
proteinGroupNumberMap.put(protein, proteinGroup.getGroupNumber());
}
}
}
if (proteins.isEmpty())
throw new IllegalArgumentException("No quantified proteins to display");
Collections.sort(proteins, new ProteinRatioAscComparator());
for (ProtXmlReader.Protein protein : proteins)
{
proteinSummaryTable.addProtein(protein, proteinGroupNumberMap.get(protein));
}
proteinSummaryTable.updateUI();
updateExtremeRatioGUI();
buttonSaveTSV.setEnabled(true);
buttonSelectAllVisible.setEnabled(true);
buttonDeselectAll.setEnabled(true);
logRatioHistogramPanel.setMaxLowRatio(maxLowRatio);
logRatioHistogramPanel.setMinHighRatio(minHighRatio);
logRatioHistogramPanel.setLogRatios(proteinLogRatios);
logRatioHistogramPanel.setSize(width-5, LOGRATIO_HISTOGRAM_PANEL_HEIGHT-20);
logRatioHistogramPanel.addRangeUpdateListener(new LogRatioHistogramListener(this));
logRatioHistogramPanel.updateUI();
height = Math.min(700, Math.max((proteins.size() + 1) * TABLEROW_HEIGHT,500) + SUMMARYPANEL_HEIGHT +
STATUSPANEL_HEIGHT + TITLEBAR_HEIGHT + LOGRATIO_HISTOGRAM_PANEL_HEIGHT);
setSize(width, height);
}
/**
* A chart listener that picks up events indicating changes to the selected area
*/
protected class LogRatioHistogramListener implements ActionListener
{
protected ProteinSummarySelectorFrame proteinSummaryFrame;
public LogRatioHistogramListener(ProteinSummarySelectorFrame proteinSummaryFrame)
{
this.proteinSummaryFrame = proteinSummaryFrame;
}
public void actionPerformed(ActionEvent e)
{
PanelWithLogRatioHistAndFields logRatioHistAndFields = (PanelWithLogRatioHistAndFields) e.getSource();
proteinSummaryFrame.minHighRatio = logRatioHistAndFields.getMinHighRatio();
proteinSummaryFrame.maxLowRatio = logRatioHistAndFields.getMaxLowRatio();
proteinSummaryFrame.updateExtremeRatioGUI();
}
}
/**
* In response to a user action restricting ratios, modify what's shown on the table and reflected in the labels.
* Called from two places
*/
protected void updateExtremeRatioGUI()
{
proteinSummaryTable.showOnlyExtremeRatios(maxLowRatio, minHighRatio);
}
/**
* Add a listener for selecting a row in the table. This is used for populating the selected protein
* @param listener
*/
public void addSelectionListener(ActionListener listener)
{
buttonSelectedProtein.addActionListener(listener);
}
/**
* Show the detail screen with a table of the actual events for this/these protein(s)
* @param event
*/
public void buttonShowEvents_actionPerformed(ActionEvent event)
{
int[] selectedRows = proteinSummaryTable.getSelectedRows();
if (selectedRows.length == 0)
return;
selectedProteins = new ArrayList<ProtXmlReader.Protein>(selectedRows.length);
for (int selectedIndex : selectedRows)
selectedProteins.add(proteins.get(selectedIndex));
ActionListener[] buttonListeners = buttonSelectedProtein.getActionListeners();
if (buttonListeners != null)
{
for (ActionListener listener : buttonListeners)
listener.actionPerformed(event);
}
}
/**
* Select all currently-visible (i.e., passes filter) rows
* @param event
*/
public void buttonSelectAllVisible_actionPerformed(ActionEvent event)
{
for (int i=0; i<proteinSummaryTable.getRowCount(); i++)
tableSelectionModel.addSelectionInterval(i, i);
}
/**
* Deselect all currently-selected rows, whether or not they're currently passing the filter
* @param event
*/
public void buttonDeselectAll_actionPerformed(ActionEvent event)
{
tableSelectionModel.clearSelection();
}
/**
* Save the table contents as a TSV file
* @param event
*/
public void buttonSaveTSV_actionPerformed(ActionEvent event)
{
WorkbenchFileChooser wfc = new WorkbenchFileChooser();
int chooserStatus = wfc.showSaveDialog(this);
//if user didn't hit OK, ignore
if (chooserStatus != JFileChooser.APPROVE_OPTION)
return;
File outTsvFile = wfc.getSelectedFile();
try
{
SwingUtils.SaveTableAsTSV(proteinSummaryTable, outTsvFile);
setMessage("Saved table to file " + outTsvFile.getAbsolutePath());
}
catch (IOException e)
{
errorMessage("Failed to save file " + outTsvFile.getAbsolutePath(), e);
}
}
/**
* Quit
* @param event
*/
public void buttonCancel_actionPerformed(ActionEvent event)
{
selectedProteins = null;
setVisible(false);
}
/**
* Sort proteins by ratio, ascending
*/
public static class ProteinRatioAscComparator implements Comparator<ProtXmlReader.Protein>
{
public int compare(ProtXmlReader.Protein o1, ProtXmlReader.Protein o2)
{
if (o1.getQuantitationRatio().getRatioMean() > o2.getQuantitationRatio().getRatioMean())
return 1;
if (o1.getQuantitationRatio().getRatioMean() < o2.getQuantitationRatio().getRatioMean())
return -1;
return 0;
}
}
public void displayProteins()
{
proteinSummaryTable.clearRows();
for (ProtXmlReader.Protein protein : proteins)
proteinSummaryTable.addProtein(protein, proteinGroupNumberMap.get(protein));
proteinSummaryTable.updateUI();
buttonSaveTSV.setEnabled(true);
}
/**
* Sortable on all columns
*/
public static final class ProteinSummaryTable extends JTable
{
protected Map<String, List<String>> proteinGeneMap;
protected int ratioColumnIndex = 0;
protected TableRowSorter<TableModel> sorter;
protected static final String[] columnTitles = new String[]
{
"Protein", "Group", "Genes", "Prob", "Ratio", "IDPeptides", "QuantEvents"
};
protected static final int[] columnWidths = new int[]
{
100, 70, 170, 70, 70, 90, 90
};
DefaultTableModel model = new DefaultTableModel(0, columnTitles.length)
{
//all cells uneditable
public boolean isCellEditable(int row, int column)
{
return false;
}
public Class getColumnClass(int columnIndex)
{
String columnTitle = columnTitles[columnIndex];
if ("Group".equals(columnTitle) || "IDPeptides".equals(columnTitle) ||
"QuantEvents".equals(columnTitle))
return Integer.class;
if ("Probability".equals(columnTitle) || "Ratio".equals(columnTitle))
return Float.class;
return String.class;
}
};
public ProteinSummaryTable()
{
setModel(model);
for (int i=0; i<columnTitles.length; i++)
{
getColumnModel().getColumn(i).setHeaderValue(columnTitles[i]);
getColumnModel().getColumn(i).setPreferredWidth(columnWidths[i]);
}
getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
sorter = new TableRowSorter<TableModel>(model);
setRowSorter(sorter);
}
/**
* Filters rows based on having extreme ratios
*/
protected class RatioRowFilter extends RowFilter<TableModel, Object>
{
protected float maxLowRatioValue;
protected float minHighRatioValue;
public RatioRowFilter(float maxLowRatioValue, float minHighRatioValue)
{
this.maxLowRatioValue = maxLowRatioValue;
this.minHighRatioValue = minHighRatioValue;
}
public boolean include(RowFilter.Entry entry)
{
float ratio = (Float) entry.getValue(ratioColumnIndex);
return isRatioIncluded(ratio);
}
public boolean isRatioIncluded(float ratio)
{
return (ratio <= maxLowRatioValue || ratio >= minHighRatioValue);
}
}
public void showOnlyExtremeRatios(float maxLowRatioValue, float minHighRatioValue)
{
RowFilter<TableModel, Object> rf = new RatioRowFilter(maxLowRatioValue, minHighRatioValue);
sorter.setRowFilter(rf);
}
/**
* Override parent behavior, which doesn't take sorting into account
* @return
*/
public int[] getSelectedRows()
{
int[] rawRows = super.getSelectedRows();
for (int i=0; i<rawRows.length; i++)
{
rawRows[i] = convertRowIndexToModel(rawRows[i]);
}
return rawRows;
}
/**
* Remove all properties from table
*/
public void clearRows()
{
while (model.getRowCount() > 0)
{
model.removeRow(0);
}
}
public void addProtein(ProtXmlReader.Protein protein, int proteinGroupNumber)
{
int numRows = model.getRowCount();
model.setRowCount(numRows + 1);
int currentColIndex = 0;
model.setValueAt(protein.getProteinName(), numRows, currentColIndex++);
model.setValueAt(proteinGroupNumber, numRows, currentColIndex++);
if (proteinGeneMap != null && proteinGeneMap.containsKey(protein.getProteinName()))
{
List<String> genes = proteinGeneMap.get(protein.getProteinName());
if (genes != null && !genes.isEmpty())
{
StringBuffer genesStringBuf = new StringBuffer(genes.get(0));
for (int i=1; i<genes.size(); i++)
genesStringBuf.append("," + genes.get(i));
model.setValueAt(genesStringBuf.toString(), numRows, currentColIndex++);
}
}
else model.setValueAt("", numRows, currentColIndex++);
model.setValueAt(protein.getProbability(), numRows, currentColIndex++);
ratioColumnIndex = currentColIndex;
model.setValueAt(protein.getQuantitationRatio().getRatioMean(), numRows, currentColIndex++);
model.setValueAt(protein.getUniquePeptidesCount(), numRows, currentColIndex++);
model.setValueAt(protein.getQuantitationRatio().getRatioNumberPeptides(), numRows, currentColIndex++);
}
}
public List<ProtXmlReader.Protein> getSelectedProteins()
{
return selectedProteins;
}
public void setSelectedProteins(List<ProtXmlReader.Protein> selectedProteins)
{
this.selectedProteins = selectedProteins;
}
public float getMinProteinProphet()
{
return minProteinProphet;
}
public void setMinProteinProphet(float minProteinProphet)
{
this.minProteinProphet = minProteinProphet;
}
/**
* Set status message. Separate thread necessary or UI hangs
* @param message
*/
public void setMessage(String message)
{
if (EventQueue.isDispatchThread())
{
if (null == message || 0 == message.length())
message = " ";
messageLabel.setText(message);
}
else
{
final String msg = message;
EventQueue.invokeLater(new Runnable()
{
public void run()
{
setMessage(msg);
}
});
}
}
/**
* Display a dialog box with info message and stack trace
* @param message
* @param t
*/
protected void errorMessage(String message, Throwable t)
{
if (null != t)
{
message = message + "\n" + t.getMessage() + "\n";
StringWriter sw = new StringWriter();
PrintWriter w = new PrintWriter(sw);
t.printStackTrace(w);
w.flush();
message += "\n";
message += sw.toString();
}
ApplicationContext.errorMessage(message, t);
JOptionPane.showMessageDialog(ApplicationContext.getFrame(), message, "Information",
JOptionPane.INFORMATION_MESSAGE);
}
public float getMinHighRatio()
{
return minHighRatio;
}
public void setMinHighRatio(float minHighRatio)
{
this.minHighRatio = minHighRatio;
}
public float getMaxLowRatio()
{
return maxLowRatio;
}
public void setMaxLowRatio(float maxLowRatio)
{
this.maxLowRatio = maxLowRatio;
}
}