/*
* Copyright 2011-2012 University of Toronto
*
* 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 medsavant.enrichment.app;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.rmi.RemoteException;
import java.sql.SQLException;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import com.jidesoft.grid.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.ut.biolab.medsavant.MedSavantClient;
import medsavant.enrichment.app.AggregatePanel;
import org.ut.biolab.medsavant.client.filter.FilterController;
import org.ut.biolab.medsavant.client.geneset.GeneSetController;
import org.ut.biolab.medsavant.client.login.LoginController;
import org.ut.biolab.medsavant.client.ontology.OntologyFilter;
import org.ut.biolab.medsavant.client.ontology.OntologyFilterView;
import org.ut.biolab.medsavant.client.ontology.OntologyListItem;
import org.ut.biolab.medsavant.shared.model.Gene;
import org.ut.biolab.medsavant.shared.model.OntologyTerm;
import org.ut.biolab.medsavant.shared.model.OntologyType;
import org.ut.biolab.medsavant.shared.model.ProgressStatus;
import org.ut.biolab.medsavant.client.project.ProjectController;
import org.ut.biolab.medsavant.client.reference.ReferenceController;
import org.ut.biolab.medsavant.client.util.MedSavantExceptionHandler;
import org.ut.biolab.medsavant.client.util.MedSavantWorker;
import org.ut.biolab.medsavant.client.util.ThreadController;
import org.ut.biolab.medsavant.client.view.genetics.GeneticsFilterPage;
import org.ut.biolab.medsavant.shared.model.SessionExpiredException;
/**
*
* @author mfiume, tarkvara
*/
public class OntologyAggregatePanel extends AggregatePanel {
private static final Log LOG = LogFactory.getLog(OntologyAggregatePanel.class);
/** Percentage of total fetch operation which is devoted to retrieving terms. */
private static final double TERMS_PERCENT = 10.0;
private JComboBox chooser;
private JProgressBar progress;
private TreeTable tree;
private MedSavantWorker termFetcher;
private VariantFetcher variantFetcher;
public OntologyAggregatePanel(String page) {
super(page);
setLayout(new GridBagLayout());
chooser = new JComboBox(OntologyListItem.DEFAULT_ITEMS);
chooser.setMaximumSize(new Dimension(400, chooser.getMaximumSize().height));
progress = new JProgressBar();
progress.setPreferredSize(new Dimension(600, progress.getMaximumSize().height));
progress.setStringPainted(true);
JPanel banner = new JPanel();
banner.setLayout(new GridBagLayout());
banner.setBackground(new Color(245,245,245));
banner.setBorder(BorderFactory.createTitledBorder("Ontology"));
tree = new TreeTable();
GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 1.0;
gbc.anchor = GridBagConstraints.WEST;
banner.add(chooser, gbc);
gbc.anchor = GridBagConstraints.EAST;
banner.add(progress, gbc);
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.weightx = 1.0;
gbc.weighty = 0.0;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.anchor = GridBagConstraints.NORTH;
add(banner, gbc);
gbc.weighty = 1.0;
gbc.fill = GridBagConstraints.BOTH;
add(new JScrollPane(tree), gbc);
chooser.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (termFetcher != null) {
termFetcher.cancel(true);
termFetcher = null;
}
recalculate();
}
});
tree.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (SwingUtilities.isRightMouseButton(e)) {
createPopup().show(e.getComponent(), e.getX(), e.getY());
}
}
});
}
@Override
public void recalculate() {
if (chooser != null && chooser.getSelectedItem() != null) {
// We only want to cancel the VariantFetcher.
if (variantFetcher != null) {
variantFetcher.cancel(true);
variantFetcher = null;
}
// It's quite possible that we've successfully fetched all the terms, in which case the dead worker tells us that
// we don't need to do it again.
if (termFetcher == null) {
tree.setModel(new OntologyTreeModel(null));
termFetcher = new MedSavantWorker<OntologyTerm[]>(pageName) {
@Override
protected OntologyTerm[] doInBackground() throws Exception {
showProgress(0.0);
return MedSavantClient.OntologyManager.getAllTerms(LoginController.getInstance().getSessionID(), ((OntologyListItem)chooser.getSelectedItem()).getType());
}
@Override
protected void showProgress(double fract) {
if (fract == 1.0) {
// Next stage will be indeterminate in duration.
progress.setIndeterminate(true);
} else {
if (fract == 0.0) {
progress.setIndeterminate(false);
startProgressTimer();
}
double prog = fract * TERMS_PERCENT;
progress.setValue((int)prog);
progress.setString(String.format("%.1f%%", prog));
}
}
@Override
protected void showSuccess(OntologyTerm[] result) {
tree.setModel(new OntologyTreeModel(result));
TableColumn col = tree.getColumnModel().getColumn(3);
col.setCellRenderer(new NodeProgressRenderer());
}
@Override
protected ProgressStatus checkProgress() throws RemoteException {
ProgressStatus stat;
try {
stat = MedSavantClient.OntologyManager.checkProgress(LoginController.getInstance().getSessionID(), false);
} catch (SessionExpiredException ex) {
MedSavantExceptionHandler.handleSessionExpiredException(ex);
return null;
}
LOG.info("OntologyManager returned status " + stat);
return stat;
}
};
termFetcher.execute();
} else {
// Already have our terms. Just reset and start a new variant fetcher.
if (tree.getModel() != null) {
for (int i = 0; i < tree.getRowCount(); i++) {
OntologyNode rowNode = (OntologyNode)tree.getRowAt(i);
rowNode.resetCount();
}
OntologyTreeModel actualModel = (OntologyTreeModel)TableModelWrapperUtils.getActualTableModel(tree.getModel());
actualModel.geneCounts.clear();
actualModel.fireTableDataChanged();
variantFetcher = new VariantFetcher(actualModel);
variantFetcher.execute();
}
}
}
}
private JPopupMenu createPopup() {
JPopupMenu menu = new JPopupMenu();
SortableTreeTableModel model = (SortableTreeTableModel)tree.getModel();
final int[] selRows = tree.getSelectedRows();
JMenuItem posItem = new JMenuItem(String.format("<html>Filter by %s</html>", selRows.length == 1 ? "Ontology Term <i>" + model.getValueAt(selRows[0], 0) + "</i>" : "Selected Ontology Terms"));
posItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ae) {
ThreadController.getInstance().cancelWorkers(pageName);
List<OntologyTerm> terms = new ArrayList<OntologyTerm>();
SortableTreeTableModel model = (SortableTreeTableModel)tree.getModel();
for (int r: selRows) {
OntologyNode rowNode = (OntologyNode)model.getRowAt(r);
terms.add(rowNode.term);
}
OntologyType ont = terms.get(0).getOntology();
GeneticsFilterPage.getSearchBar().loadFilters(OntologyFilterView.wrapState(OntologyFilter.ontologyToTitle(ont), ont, terms, false));
}
});
menu.add(posItem);
return menu;
}
/**
* Class which provides a tree-like data-structure for all terms within a given ontology.
*/
private class OntologyTreeModel extends TreeTableModel {
private final OntologyTerm[] allTerms;
private final Map<OntologyTerm, OntologyTerm[]> allChildren = new HashMap<OntologyTerm, OntologyTerm[]>();
private final Map<String, Integer> geneCounts = new HashMap<String, Integer>();
public OntologyTreeModel(OntologyTerm[] terms) {
allTerms = terms;
if (terms != null) {
for (OntologyTerm t: terms) {
if (t.getParentIDs() == null) {
OntologyNode node = new OntologyNode(t, this);
addRow(node);
}
}
variantFetcher = new VariantFetcher(this);
variantFetcher.execute();
}
}
@Override
public int getColumnCount() {
return 4;
}
@Override
public String getColumnName(int col) {
switch (col) {
case 0:
return "ID";
case 1:
return "Name";
case 2:
return "Definition";
case 3:
return "Variant Count";
}
return null;
}
private OntologyTerm[] getChildTerms(OntologyTerm term) {
if (!allChildren.containsKey(term)) {
OntologyTerm[] children = term.getChildren(allTerms);
allChildren.put(term, children);
return children;
}
return allChildren.get(term);
}
}
/**
* Class which represents a single term within the tree-model.
*/
private class OntologyNode extends DefaultExpandableRow {
private static final int COUNT_COLUMN = 3;
private final OntologyTerm term;
private final OntologyTreeModel model;
private Set<String> uncountedGenes = new HashSet<String>();
private int totalGenes;
private int count;
private OntologyNode(OntologyTerm t, OntologyTreeModel m) {
term = t;
model = m;
addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals(DefaultExpandableRow.PROPERTY_EXPANDED)) {
if (isExpanded()) {
// If we add the children in the actual propertyChange method, they get added twice. A JIDE bug?
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
OntologyTerm[] childTerms = model.getChildTerms(term);
if (childTerms.length > 0 && getChildrenCount() == 0) {
synchronized (model) {
for (OntologyTerm t: childTerms) {
model.addRow(OntologyNode.this, new OntologyNode(t, model));
}
}
}
// We use a new thread to push the new nodes onto the VariantFetcher in order to avoid
// blocking the AWT thread.
new Thread() {
@Override
public void run() {
for (Object child: getChildren()) {
variantFetcher.push((OntologyNode)child);
}
}
}.start();
}
});
}
}
}
});
}
@Override
public Object getValueAt(int col) {
switch (col) {
case 0:
return term.getID();
case 1:
return term.getName();
case 2:
return term.getDef();
case COUNT_COLUMN:
return count;
}
return null;
}
@Override
public boolean hasChildren() {
return model.getChildTerms(term).length > 0;
}
private void increment(String gene, Integer n) {
if (uncountedGenes != null && uncountedGenes.contains(gene)) {
if (n != null) {
count += n;
}
uncountedGenes.remove(gene);
if (uncountedGenes.isEmpty()) {
uncountedGenes = null;
}
model.fireTableCellUpdated(model.getRowIndex(this), COUNT_COLUMN);
}
}
private void resetCount() {
LOG.info("Reset count for " + term);
uncountedGenes = new HashSet<String>();
count = 0;
}
}
private class NodeProgressRenderer extends JPanel implements TableCellRenderer {
private JLabel label;
private JProgressBar bar;
NodeProgressRenderer() {
setLayout(new GridBagLayout());
label = new JLabel() {
@Override
public Dimension getMinimumSize() {
return new Dimension(90, super.getMinimumSize().height);
}
};
bar = new JProgressBar();
bar.putClientProperty("JComponent.sizeVariant", "mini");
GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 0.5;
gbc.anchor = GridBagConstraints.WEST;
gbc.insets = new Insets(3, 3, 3, 3);
add(label, gbc);
gbc.weightx = 1.0;
gbc.fill = GridBagConstraints.HORIZONTAL;
add(bar, gbc);
}
@Override
public Component getTableCellRendererComponent(JTable table, Object val, boolean selected, boolean focus, int row, int col) {
OntologyNode node = (OntologyNode)((TreeTable)table).getRowAt(row);
if (node.uncountedGenes == null) {
// Fully loaded.
label.setText(val.toString());
bar.setVisible(false);
} else {
if (node.uncountedGenes.isEmpty()) {
// Still waiting to get our list of genes.
label.setText("Loading...");
bar.setIndeterminate(true);
bar.setStringPainted(false);
bar.setVisible(true);
} else {
double prog = (1.0 - (double)node.uncountedGenes.size() / node.totalGenes) * 100.0;
label.setText("≥ " + val);
bar.setIndeterminate(false);
bar.setValue((int)prog);
bar.setString(String.format("%.1f%%", prog));
bar.setStringPainted(true);
bar.setVisible(true);
}
}
return this;
}
}
private class VariantFetcher extends MedSavantWorker<Void> {
private final Stack<OntologyNode> nodeStack = new Stack<OntologyNode>();
private final Stack<String> geneStack = new Stack<String>();
private final OntologyTreeModel model;
private VariantFetcher(OntologyTreeModel m) {
super(pageName);
model = m;
for (int i = 0; i < model.getRowCount(); i++) {
push((OntologyNode)model.getRowAt(i));
}
}
/**
* It's hard to get a valid progress percentage for the process as a whole,
*/
@Override
protected void showProgress(double fraction) {
}
@Override
protected void showSuccess(Void result) {
}
@Override
protected Void doInBackground() throws Exception {
while (!isCancelled()) {
do {
progress.setVisible(true);
while (!nodeStack.empty() && !isCancelled()) {
synchronized (nodeStack) {
fetchGenesForNodes();
}
}
if (!geneStack.empty()) {
String geneName = geneStack.pop();
Integer result = model.geneCounts.get(geneName);
if (result == null) {
Gene gene = GeneSetController.getInstance().getGene(geneName);
if (gene != null) {
result = MedSavantClient.VariantManager.getVariantCountInRange(
LoginController.getInstance().getSessionID(),
ProjectController.getInstance().getCurrentProjectID(),
ReferenceController.getInstance().getCurrentReferenceID(),
FilterController.getInstance().getAllFilterConditions(),
gene.getChrom(),
gene.getStart(),
gene.getEnd());
} else {
LOG.info(geneName + " referenced in " + model.allTerms[0].getOntology() + " not found in current gene set.");
result = 0;
}
model.geneCounts.put(geneName, result);
}
synchronized (model) {
for (Object node: model.getRows()) {
((OntologyNode)node).increment(geneName, result);
}
}
}
} while (!geneStack.empty() && !isCancelled());
progress.setVisible(false);
synchronized (nodeStack) {
nodeStack.wait();
}
}
throw new InterruptedException();
}
private void fetchGenesForNodes() throws InterruptedException, SQLException, RemoteException {
OntologyTerm[] terms = new OntologyTerm[nodeStack.size()];
for (int i = 0; i < terms.length; i++) {
terms[i] = nodeStack.get(i).term;
}
Map<OntologyTerm, String[]> genes;
try {
genes = MedSavantClient.OntologyManager.getGenesForTerms(LoginController.getInstance().getSessionID(), terms, ReferenceController.getInstance().getCurrentReferenceName());
} catch (SessionExpiredException ex) {
MedSavantExceptionHandler.handleSessionExpiredException(ex);
return;
}
for (OntologyNode node: nodeStack) {
String[] nodeGenes = genes.get(node.term);
if (nodeGenes != null) {
node.uncountedGenes.addAll(Arrays.asList(nodeGenes));
node.totalGenes = nodeGenes.length;
for (String g: nodeGenes) {
Integer geneCount = model.geneCounts.get(g);
if (geneCount != null) {
// Already counted.
node.increment(g, geneCount);
} else {
push(g);
}
}
} else {
// No genes for this term. Mark it null so we know we're done.
node.uncountedGenes = null;
model.fireTableCellUpdated(model.getRowIndex(node), OntologyNode.COUNT_COLUMN);
}
}
nodeStack.clear();
}
private void push(OntologyNode node) {
synchronized (nodeStack) {
nodeStack.push(node);
nodeStack.notify();
}
}
private void push(String g) {
geneStack.push(g);
}
}
}