/*
* Copyright (c) 2010 The Jackson Laboratory
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jax.bham.test;
import java.awt.BorderLayout;
import java.awt.Point;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import org.jax.bham.BhamApplication;
import org.jax.bham.infer.PlotPhylogeneticTreeAction;
import org.jax.bham.util.JFreeChartUtil;
import org.jax.geneticutil.data.BasePairInterval;
import org.jax.geneticutil.data.CompositeRealValuedBasePairInterval;
import org.jax.geneticutil.data.RealValuedBasePairInterval;
import org.jax.geneticutil.gui.GoToMouseIntervalInCGDGBrowseAction;
import org.jax.geneticutil.gui.GoToMouseIntervalInCGDSnpDatabaseAction;
import org.jax.geneticutil.gui.GoToMouseIntervalInUCSCBrowserAction;
import org.jax.haplotype.analysis.PhylogenyAssociationTest;
import org.jax.haplotype.analysis.visualization.ChromosomeHistogramValues;
import org.jax.haplotype.analysis.visualization.GenomicGraphFactory;
import org.jax.haplotype.analysis.visualization.HighlightedSnpInterval;
import org.jax.haplotype.analysis.visualization.OcclusionFilter;
import org.jax.haplotype.phylogeny.data.PhylogenyTestResult;
import org.jax.haplotype.phylogeny.data.PhylogenyTreeEdge;
import org.jax.haplotype.phylogeny.data.PhylogenyTreeNode;
import org.jax.util.concurrent.MultiTaskProgressPanel;
import org.jax.util.datastructure.SequenceUtilities;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
/**
* A panel for graphing phylogeney association results
* @author <A HREF="mailto:keith.sheppard@jax.org">Keith Sheppard</A>
*/
public class PhylogenyAssociationTestGraphPanel extends JPanel
{
/**
* every {@link java.io.Serializable} is supposed to have one of these
*/
private static final long serialVersionUID = 618496235032301159L;
private final PhylogenyAssociationTest testToPlot;
private final GenomicGraphFactory graphFactory = new GenomicGraphFactory();
private final JComboBox chromosomeComboBox;
private final ChartPanel chartPanel = new ChartPanel(null, true)
{
/**
* every {@link java.io.Serializable} is supposed to have one of these
*/
private static final long serialVersionUID = 2353239091616674857L;
/**
* {@inheritDoc}
*/
@Override
protected void displayPopupMenu(int x, int y)
{
PhylogenyAssociationTestGraphPanel.this.updateClickPosition(x, y);
super.displayPopupMenu(x, y);
}
};
private volatile int lastClickedIntervalIndex;
private volatile List<JComponent> lastMenuItems = Collections.emptyList();
private final Map<Integer, List<PhylogenyTestResult>> chromosomeResultsCache =
Collections.synchronizedMap(new HashMap<Integer, List<PhylogenyTestResult>>());
/**
* Constructor
* @param testToPlot
* the test that we're plotting
*/
public PhylogenyAssociationTestGraphPanel(
PhylogenyAssociationTest testToPlot)
{
super(new BorderLayout());
this.testToPlot = testToPlot;
this.chromosomeComboBox = new JComboBox();
this.initialize();
}
private void updateClickPosition(int x, int y)
{
this.lastClickedIntervalIndex = this.getMaximalIntervalIndex(x, y);
// remove the old menu items
JPopupMenu popupMenu = this.chartPanel.getPopupMenu();
for(int i = 0; i < this.lastMenuItems.size(); i++)
{
popupMenu.remove(0);
}
// now replace the old menus with some new ones
this.lastMenuItems = this.createContextMenuItems();
for(int i = 0; i < this.lastMenuItems.size(); i++)
{
popupMenu.insert(this.lastMenuItems.get(i), i);
}
}
/**
* Gets the index of the interval with the highest value that falls
* under the given Java2D coordinates
* @param x
* the Java2D X coordinate
* @param y
* the Java2D Y coordinate
* @return
* the interval index
*/
private int getMaximalIntervalIndex(int x, int y)
{
int clickIndex = -1;
int[] chromosomes = this.getSelectedChromosomes();
if(chromosomes.length == 1)
{
List<PhylogenyTestResult> selectedPhyloAssociationTests =
this.chromosomeResultsCache.get(chromosomes[0]);
if(selectedPhyloAssociationTests != null)
{
RealValuedBasePairInterval[] selectedValuesList =
selectedPhyloAssociationTests.toArray(
new RealValuedBasePairInterval[selectedPhyloAssociationTests.size()]);
Point2D clickedGraphPoint = JFreeChartUtil.java2DPointToGraphPoint(
new Point(x, y),
this.chartPanel);
// exhaustive search for the maximum clicked index (this could
// be a binary search, but this seems to perform OK for now)
double graphX = clickedGraphPoint.getX();
int valueCount = selectedValuesList.length;
double biggestClickedValue = Double.NEGATIVE_INFINITY;
for(int i = 0; i < valueCount; i++)
{
RealValuedBasePairInterval currValue = selectedValuesList[i];
if(currValue.getStartInBasePairs() < graphX &&
currValue.getEndInBasePairs() > graphX &&
currValue.getRealValue() > biggestClickedValue)
{
biggestClickedValue = currValue.getRealValue();
clickIndex = i;
}
}
// if we didn't click on anything grab the nearest item
// (again this could and should be faster)
if(clickIndex == -1 && valueCount >= 1)
{
clickIndex = 0;
double nearestDistance = Math.min(
Math.abs(selectedValuesList[0].getStartInBasePairs() - graphX),
Math.abs(selectedValuesList[0].getEndInBasePairs() - graphX));
for(int i = 1; i < valueCount; i++)
{
BasePairInterval currValue = selectedValuesList[i];
double currDistance = Math.min(
Math.abs(currValue.getStartInBasePairs() - graphX),
Math.abs(currValue.getEndInBasePairs() - graphX));
if(currDistance < nearestDistance)
{
nearestDistance = currDistance;
clickIndex = i;
}
}
}
}
}
return clickIndex;
}
/**
* a function to initialize the components for this panel
*/
private void initialize()
{
this.chromosomeComboBox.addItem("All Chromosomes");
List<Integer> chromoList = SequenceUtilities.toIntegerList(
this.testToPlot.getPhylogenyDataSource().getAvailableChromosomes());
Collections.sort(chromoList);
for(Integer chromoNum: chromoList)
{
this.chromosomeComboBox.addItem(chromoNum);
}
if(!chromoList.isEmpty())
{
this.chromosomeComboBox.setSelectedIndex(1);
}
this.chromosomeComboBox.addItemListener(new ItemListener()
{
/**
* {@inheritDoc}
*/
public void itemStateChanged(ItemEvent e)
{
if(e.getStateChange() == ItemEvent.SELECTED)
{
PhylogenyAssociationTestGraphPanel.this.chromosomeSelectionChanged();
}
}
});
JToolBar toolBar = new JToolBar();
toolBar.add(new JLabel("Chromosome:"));
// limit the size or the toolbar will try to make the drop-down huge
this.chromosomeComboBox.setMaximumSize(
this.chromosomeComboBox.getPreferredSize());
toolBar.add(this.chromosomeComboBox);
this.add(toolBar, BorderLayout.PAGE_START);
this.add(this.chartPanel, BorderLayout.CENTER);
this.chromosomeSelectionChanged();
}
private int[] getSelectedChromosomes()
{
// implementation assumes that the first item indicates that all
// chromosomes should be displayed and that any other selection is a
// specific chromosome
if(this.chromosomeComboBox.getSelectedIndex() == 0)
{
// all are selected
int[] allChromosomes = new int[this.chromosomeComboBox.getItemCount() - 1];
for(int i = 0; i < allChromosomes.length; i++)
{
allChromosomes[i] = (Integer)this.chromosomeComboBox.getItemAt(i + 1);
}
return allChromosomes;
}
else
{
// a single chromosome is selected
return new int[] {(Integer)this.chromosomeComboBox.getSelectedItem()};
}
}
private void chromosomeSelectionChanged()
{
final int[] selectedChromosomes = this.getSelectedChromosomes();
Thread runTestsThread = new Thread(new Runnable()
{
/**
* {@inheritDoc}
*/
public void run()
{
PhylogenyAssociationTestGraphPanel.this.cacheChromosomeTests(
selectedChromosomes);
SwingUtilities.invokeLater(new Runnable()
{
/**
* {@inheritDoc}
*/
public void run()
{
PhylogenyAssociationTestGraphPanel.this.repaintGraphNow();
}
});
}
});
runTestsThread.start();
}
private void cacheChromosomeTests(
final int[] chromosomes)
{
List<Integer> chromosomesToCalculate =
SequenceUtilities.toIntegerList(chromosomes);
chromosomesToCalculate.removeAll(this.chromosomeResultsCache.keySet());
PerformPhylogenyAssociationTestTask testTask =
new PerformPhylogenyAssociationTestTask(
this.testToPlot,
chromosomesToCalculate);
MultiTaskProgressPanel progressTracker =
BhamApplication.getInstance().getBhamFrame().getMultiTaskProgress();
progressTracker.addTaskToTrack(testTask, true);
while(testTask.hasMoreElements())
{
int nextChromosome = testTask.getNextChromosome();
List<PhylogenyTestResult> results = testTask.nextElement();
List<PhylogenyTestResult> filteredResults = OcclusionFilter.filterOutOccludedIntervals(
results,
true);
PhylogenyAssociationTestGraphPanel.this.chromosomeResultsCache.put(
nextChromosome,
filteredResults);
}
}
private RealValuedBasePairInterval[] toNegLogResults(List<PhylogenyTestResult> filteredResults)
{
RealValuedBasePairInterval[] negLogResults =
new RealValuedBasePairInterval[filteredResults.size()];
for(int i = 0; i < negLogResults.length; i++)
{
negLogResults[i] = new CompositeRealValuedBasePairInterval(
filteredResults.get(i).getPhylogenyInterval().getInterval(),
-Math.log10(filteredResults.get(i).getPValue()));
}
return negLogResults;
}
private void repaintGraphNow()
{
int[] chromosomes = this.getSelectedChromosomes();
if(chromosomes.length == 1)
{
final List<PhylogenyTestResult> intervals =
this.chromosomeResultsCache.get(chromosomes[0]);
if(intervals != null)
{
RealValuedBasePairInterval[] negLog10Intervals =
this.toNegLogResults(intervals);
HighlightedSnpInterval highlightedSnpInterval =
new HighlightedSnpInterval(
0,
negLog10Intervals.length,
new int[0]);
long startPosition = negLog10Intervals[0].getStartInBasePairs();
long endPosition = startPosition;
for(RealValuedBasePairInterval interval: negLog10Intervals)
{
long currEndPosition = interval.getEndInBasePairs();
if(currEndPosition > endPosition)
{
endPosition = currEndPosition;
}
}
JFreeChart jFreeChart = this.graphFactory.createSnpIntervalHistogram(
Arrays.asList(negLog10Intervals),
startPosition,
1 + endPosition - startPosition,
highlightedSnpInterval,
"Base Pair Position",
"-log10(p-value)");
jFreeChart.setTitle(
this.testToPlot.getName() + " - Chromosome " +
this.chromosomeComboBox.getSelectedItem());
this.chartPanel.setChart(jFreeChart);
}
}
else
{
List<ChromosomeHistogramValues> chromoHistos =
new ArrayList<ChromosomeHistogramValues>();
for(int chromosome: chromosomes)
{
final RealValuedBasePairInterval[] intervals =
this.toNegLogResults(this.chromosomeResultsCache.get(chromosome));
if(intervals != null)
{
HighlightedSnpInterval highlightedSnpInterval =
new HighlightedSnpInterval(
0,
intervals.length,
new int[0]);
long startPosition = intervals[0].getStartInBasePairs();
long endPosition = startPosition;
for(RealValuedBasePairInterval interval: intervals)
{
long currEndPosition = interval.getEndInBasePairs();
if(currEndPosition > endPosition)
{
endPosition = currEndPosition;
}
}
ChromosomeHistogramValues chromosomeHistogramValues = new ChromosomeHistogramValues(
chromosome,
Arrays.asList(intervals),
startPosition,
1 + endPosition - startPosition,
highlightedSnpInterval);
chromoHistos.add(chromosomeHistogramValues);
}
}
JFreeChart jFreeChart = this.graphFactory.createMultiChromosomeHistogram(
chromoHistos,
"Chromosome Base Pair Position",
"-log10(p-value)");
jFreeChart.setTitle(
this.testToPlot.getName() + " - All Chromosomes");
this.chartPanel.setChart(jFreeChart);
}
}
private List<JComponent> createContextMenuItems()
{
List<JComponent> menuItems = new ArrayList<JComponent>();
int[] chromosomes = this.getSelectedChromosomes();
if(this.lastClickedIntervalIndex >= 0 && chromosomes.length == 1)
{
PhylogenyTestResult selectedInterval =
this.chromosomeResultsCache.get(chromosomes[0]).get(
this.lastClickedIntervalIndex);
menuItems.add(new JMenuItem(new GoToMouseIntervalInCGDSnpDatabaseAction(
selectedInterval,
BhamApplication.getInstance().getBhamFrame())));
menuItems.add(new JMenuItem(new GoToMouseIntervalInCGDGBrowseAction(
selectedInterval,
BhamApplication.getInstance().getBhamFrame())));
menuItems.add(new JMenuItem(new GoToMouseIntervalInUCSCBrowserAction(
selectedInterval,
BhamApplication.getInstance().getBhamFrame())));
menuItems.add(new JSeparator());
PhylogenyTreeNode phylogenyTree =
selectedInterval.getPhylogenyInterval().getPhylogeny();
menuItems.add(new JMenuItem(new PlotPhylogeneticTreeAction(
phylogenyTree,
this.testToPlot)));
Map<String, List<String>> strainGroups = this.extractStrainGroups(
phylogenyTree);
menuItems.add(new JMenuItem(new ShowPhenotypeEffectPlotAction(
this.testToPlot.getPhenotypeDataSource(),
strainGroups)));
}
return menuItems;
}
/**
* Get all of the strain groups from the given tree
* @param phylogenyTree
* the tree
* @return
* the strains for each node
*/
private Map<String, List<String>> extractStrainGroups(
PhylogenyTreeNode phylogenyTree)
{
Map<String, List<String>> strainGroups =
new HashMap<String, List<String>>();
List<String> strains = phylogenyTree.getStrains();
if(!strains.isEmpty())
{
assert(!strainGroups.containsKey(strains.get(0)));
strainGroups.put(
strains.get(0) + " Group",
strains);
}
List<PhylogenyTreeEdge> children = phylogenyTree.getChildEdges();
for(PhylogenyTreeEdge childEdge: children)
{
strainGroups.putAll(this.extractStrainGroups(
childEdge.getNode()));
}
return strainGroups;
}
}