/* * 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.gui; import org.apache.log4j.Logger; import org.fhcrc.cpl.toolbox.proteomics.feature.FeatureSet; import org.fhcrc.cpl.toolbox.proteomics.feature.Feature; import org.fhcrc.cpl.toolbox.proteomics.feature.Spectrum; import org.fhcrc.cpl.toolbox.proteomics.MSRun; import org.fhcrc.cpl.toolbox.proteomics.gui.IntensityPlot; import org.fhcrc.cpl.viewer.util.SharedProperties; import org.fhcrc.cpl.toolbox.ApplicationContext; import org.fhcrc.cpl.toolbox.datastructure.FloatArray; import org.fhcrc.cpl.toolbox.datastructure.FloatRange; import javax.swing.*; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeEvent; import java.awt.event.*; import java.awt.*; import java.awt.image.BufferedImage; import java.util.Comparator; import java.util.Arrays; /** * HeatMapPanel - Main JPanel, contains and controls images for * each of the individual charges */ public class HeatMapPanel extends JPanel implements PropertyChangeListener, ComponentListener { private static Logger _log = Logger.getLogger(HeatMapPanel.class); private static final int MAX_CHARGE = 6; private static final int RESAMPLE_FREQ = 36; private final JFrame frame; private FeatureSet featureSet = null; private Feature[] featureClone = null; private int currentFeatureIndex = -1; // image panels register themselves here; add one so we can index directly by charge private ChargeImagePanel[] chargeImagePanels = new ChargeImagePanel[MAX_CHARGE + 1]; private int[] chargeOffset = new int[MAX_CHARGE + 2]; // Add an extra 1 so that we can store charge 6 end offset private Comparator sortOrder = featureChargeMassAscComparator; private int panelWidth = 600; private int panelHeight = 600; /** * */ public HeatMapPanel(JFrame frame) { this.frame = frame; setPreferredSize(new Dimension(panelWidth, panelHeight)); // Listen for changes to the run, feature ranges, or selected point ApplicationContext.addPropertyChangeListener(SharedProperties.MS_RUN, this); ApplicationContext.addPropertyChangeListener(SharedProperties.FEATURE_RANGES, this); ApplicationContext.addPropertyChangeListener(SharedProperties.SELECTED_POINT, this); ApplicationContext.addPropertyChangeListener(SharedProperties.SELECTED, this); setLayout(new GridLayout(0,2,5,5)); for (int charge = 1; charge <= MAX_CHARGE; charge++) add(new ChargePanel(charge)); getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "previousFeature"); getInputMap().put(KeyStroke.getKeyStroke('-'), "previousFeature"); getActionMap().put("previousFeature", previousFeatureAction); getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "nextFeature"); getInputMap().put(KeyStroke.getKeyStroke('+'), "nextFeature"); getInputMap().put(KeyStroke.getKeyStroke('='), "nextFeature"); // no need to hit shift getActionMap().put("nextFeature", nextFeatureAction); addComponentListener(this); } /** * Check that the currently loaded feature set matches the one currently displayed */ boolean updateFeatureSet(boolean displayWhenDone) { FeatureSet fs = HeatMapFrame.getDisplayedFeatureSet(); if ( fs == featureSet && null != featureSet ) { if (displayWhenDone) frame.setVisible(true); return true; } IntensityWindowScanner.load(this, fs, displayWhenDone); return false; } /** * Update the heat map panel and the individual charge images */ void updatePanel(FeatureSet fs, boolean forceDisplay) { featureSet = fs; if ( null == fs ) clearChargeImages(); else updateChargeImages(); // ????? Set highlight if ( forceDisplay && null != fs ) frame.setVisible(true); } /** * */ boolean setHighlight(int index) { int charge = -1; int newFeatureIndex = -1; if (index >= 0) charge = featureClone[index].charge; for (int i = 1; i <= MAX_CHARGE; i++) if ( i == charge ) newFeatureIndex = chargeImagePanels[i].setHighlight(index); else chargeImagePanels[i].clearHighlight(); // Highlighted feature did not change if (newFeatureIndex == currentFeatureIndex) return false; currentFeatureIndex = newFeatureIndex; return true; } private boolean isHitCloseEnough(Feature feature, int index) { if (index < 0 || index >= featureClone.length) return false; Feature f = featureClone[index]; return (feature.charge == f.charge && Math.abs(feature.mz - f.mz) <= 0.01 && Math.abs(feature.scan - f.scan) <= 1); } boolean setHighlight(Feature feature) { int index = Arrays.binarySearch(featureClone, feature, sortOrder); if ( index < 0 ) { // Be a little flexible on mz comparisons int i = - index - 1; if (isHitCloseEnough(feature, i)) index = i; else if (isHitCloseEnough(feature, i - 1)) index = i - 1; else if (isHitCloseEnough(feature, i + 1)) index = i + 1; } return setHighlight(index); } void broadcastHighlight() { MSRun run = (MSRun)ApplicationContext.getProperty(SharedProperties.MS_RUN); Feature f = currentFeatureIndex < 0 ? null : featureClone[currentFeatureIndex]; ApplicationContext.setProperty(SharedProperties.SELECTED_POINT, f); ApplicationContext.setProperty(SharedProperties.SELECTED, f); if (null != run) { int i = getIndexForScanNum(run, f.scan); ApplicationContext.setProperty(SharedProperties.MS_SCAN, run.getScan(i)); } } void setHighlightAndBroadcast(int index) { if (setHighlight(index)) broadcastHighlight(); } /** * */ void clearHighlights() { for (int charge = 1; charge <= MAX_CHARGE; charge++) chargeImagePanels[charge].clearHighlight(); currentFeatureIndex = -1; } /** * */ void clearChargeImages() { clearHighlights(); frame.setTitle(getTitle(null)); for(int charge = 0; charge <= MAX_CHARGE + 1; charge++) chargeOffset[charge] = 0; for (int charge = 1; charge <= MAX_CHARGE; charge++) chargeImagePanels[charge].clearImage(); repaint(); } /** * */ void updateChargeImages() { clearHighlights(); frame.setTitle(getTitle(featureSet)); Feature[] f = featureSet.getFeatures(); featureClone = new Feature[f.length]; System.arraycopy(f, 0, featureClone, 0, f.length); Arrays.sort(featureClone, sortOrder); int charge = 0; for (int i = 0; i < f.length; i++) while (featureClone[i].charge > charge) chargeOffset[charge++] = i; for( ; charge <= MAX_CHARGE + 1; charge++) chargeOffset[charge] = f.length; for (charge = 1; charge <= MAX_CHARGE; charge++) chargeImagePanels[charge].update(); repaint(); } /** * Build a frame title from the given feature set */ static String getTitle(FeatureSet fs) { StringBuffer sb = new StringBuffer(WorkbenchFrame.getAppName()); sb.append(" - FeatureHeatMap"); if ( null != fs ) { sb.append(" -- "); sb.append(fs.getSourceFile().getName()); } return sb.toString(); } /** * Get the index corresponding to the given scan number. Since we may be handed * ms2 or ms3 scans, pick the previous scan if not found */ static int getIndexForScanNum(MSRun run, int n) { int i = run.getIndexForScanNum(n); if ( i == -1 ) return 0; if ( i < 0 ) return -2 - i; return i; } /** * */ public void propertyChange(PropertyChangeEvent event) { _log.debug("Heat map panel saw an event " + event.getPropertyName() + " " + event); // Loading a new run or selecting a new set of features invalidates current // images even if we are not displayed. if ( SharedProperties.MS_RUN.equals(event.getPropertyName()) ) { frame.setVisible(false); // Disable the heat map; no longer valid return; } // Need to know about new feature sets if we are displayed *or* // processing another in the background. if (!frame.isVisible() && !IntensityWindowScanner.isRunning()) return; // New Feature Set loaded, created, or selected if ( SharedProperties.FEATURE_RANGES.equals(event.getPropertyName()) ) { SwingUtilities.invokeLater(new Runnable() { public void run() { updateFeatureSet(false); } }); } // Don't mark the current feature unless we are visible if (!frame.isVisible()) return; // New point or Feature. If Feature, highlight it if ( SharedProperties.SELECTED.equals(event.getPropertyName()) || SharedProperties.SELECTED_POINT.equals(event.getPropertyName()) ) { if (null != event.getNewValue() && event.getNewValue() instanceof Feature) { Feature f = (Feature)event.getNewValue(); // _log.debug("Heat map saw feature selection (" + f.charge + ", " + f.mz + ", " + f.scan + ")"); setHighlight(f); } else { clearHighlights(); } } } Action previousFeatureAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { if (currentFeatureIndex <= 0 || currentFeatureIndex >= featureClone.length ) return; int charge = featureClone[currentFeatureIndex].charge; int index = currentFeatureIndex - 1; if ( charge != featureClone[index].charge ) return; setHighlightAndBroadcast(index); } }; Action nextFeatureAction = new AbstractAction() { public void actionPerformed(ActionEvent e) { if (currentFeatureIndex < 0 || currentFeatureIndex >= featureClone.length - 1 ) return; int charge = featureClone[currentFeatureIndex].charge; int index = currentFeatureIndex + 1; if ( charge != featureClone[index].charge ) return; setHighlightAndBroadcast(index); } }; /** * */ public Comparator getSortOrder() { return sortOrder; } /** * */ public void setSortOrder(Comparator sortOrder) { this.sortOrder = sortOrder; updateChargeImages(); } /** * */ public void setSortOrderMass() { setSortOrder(featureChargeMassAscComparator); } /** * */ public void setSortOrderKl() { setSortOrder(featureChargeKlDscComparator); } /** * */ public void setSortOrderScan() { setSortOrder(featureChargeScanAscComparator); } /** * */ public void setSortOrderIntensity() { setSortOrder(featureChargeIntensityAscComparator); } /** * */ public void setSortOrderTotalIntensity() { setSortOrder(featureChargeTotalIntensityAscComparator); } /*----------------------------------------------------------------------* * Comparators *----------------------------------------------------------------------*/ /** * */ static Comparator featureScanAscComparator = new Comparator() { public int compare(Object o1, Object o2) { Feature f1 = (Feature)o1; Feature f2 = (Feature)o2; return (f1.scan < f2.scan ? -1 : (f1.scan == f2.scan ? 0 : 1)); } }; /** * */ static Comparator featureChargeMassAscComparator = new Comparator() { public int compare(Object o1, Object o2) { Feature f1 = (Feature)o1; Feature f2 = (Feature)o2; if ( f1.charge != f2.charge ) return f1.charge < f2.charge ? -1 : 1; if ( f1.mz != f2.mz ) return f1.mz < f2.mz ? -1 : 1; return f1.scan < f2.scan ? -1 : (f1.scan > f2.scan ? 1 : 0); } }; /** * */ static Comparator featureChargeScanAscComparator = new Comparator() { public int compare(Object o1, Object o2) { Feature f1 = (Feature)o1; Feature f2 = (Feature)o2; if ( f1.charge != f2.charge ) return f1.charge < f2.charge ? -1 : 1; if ( f1.scan != f2.scan ) return f1.scan < f2.scan ? -1 : 1; return f1.mz < f2.mz ? -1 : (f1.mz > f2.mz ? 1 : 0); } }; /** * */ static Comparator featureChargeKlDscComparator = new Comparator() { public int compare(Object o1, Object o2) { Feature f1 = (Feature)o1; Feature f2 = (Feature)o2; if ( f1.charge != f2.charge ) return f1.charge < f2.charge ? -1 : 1; if ( f1.kl != f2.kl ) return f1.kl < f2.kl ? 1 : -1; if ( f1.scan != f2.scan ) return f1.scan < f2.scan ? -1 : 1; return f1.mz < f2.mz ? -1 : (f1.mz > f2.mz ? 1 : 0); } }; /** * */ static Comparator featureChargeIntensityAscComparator = new Comparator() { public int compare(Object o1, Object o2) { Feature f1 = (Feature)o1; Feature f2 = (Feature)o2; if ( f1.charge != f2.charge ) return f1.charge < f2.charge ? -1 : 1; if ( f1.intensity != f2.intensity ) return f1.intensity < f2.intensity ? -1 : 1; if ( f1.scan != f2.scan ) return f1.scan < f2.scan ? -1 : 1; return f1.mz < f2.mz ? -1 : (f1.mz > f2.mz ? 1 : 0); } }; /** * */ static Comparator featureChargeTotalIntensityAscComparator = new Comparator() { public int compare(Object o1, Object o2) { Feature f1 = (Feature)o1; Feature f2 = (Feature)o2; if ( f1.charge != f2.charge ) return f1.charge < f2.charge ? -1 : 1; if ( f1.totalIntensity != f2.totalIntensity ) return f1.totalIntensity < f2.totalIntensity ? -1 : 1; if ( f1.scan != f2.scan ) return f1.scan < f2.scan ? -1 : 1; return f1.mz < f2.mz ? -1 : (f1.mz > f2.mz ? 1 : 0); } }; /*----------------------------------------------------------------------* * Component actions, currently unused *----------------------------------------------------------------------*/ public void componentResized(ComponentEvent e) { panelWidth = getSize().width; panelHeight = getSize().height; for (int i = 1; i <= MAX_CHARGE; i++) chargeImagePanels[i].setSize(); revalidate(); } public void componentHidden(ComponentEvent e) { } public void componentMoved(ComponentEvent e) { } public void componentShown(ComponentEvent e) { } /** * Panel displaying the heat map for a single charge */ class ChargePanel extends JPanel { /** * */ public ChargePanel(int charge) { JLabel chargeLabel = new JLabel("Charge " + charge); if (false) { setLayout(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); c.anchor = GridBagConstraints.CENTER; c.weightx = 0.0; c.weighty = 0.0; c.gridx = 0; c.gridy = 0; c.insets = new Insets(4, 4, 4, 4); add(new ChargeImagePanel(charge), c); c.gridx = 0; c.gridy = 1; c.anchor = GridBagConstraints.PAGE_END; c.insets = new Insets(0, 0, 0, 0); add(chargeLabel, c); } else { setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); ChargeImagePanel cip = new ChargeImagePanel(charge); cip.setAlignmentX(Component.CENTER_ALIGNMENT); add(cip); add(chargeLabel); } } } /** * Image for a single charge */ class ChargeImagePanel extends JPanel implements MouseListener { private int _charge = 0; BufferedImage img; int imgWidth, imgHeight; int highlightX = -1; float xscale; /** * */ public ChargeImagePanel(int charge) { _charge = charge; chargeImagePanels[_charge] = this; setBorder(BorderFactory.createLineBorder(Color.BLACK)); setSize(); addMouseListener(this); } public void setSize() { int w = panelWidth/2 - 10; int h = panelHeight/3 - 20; setPreferredSize(new Dimension(imgWidth, imgHeight)); // setMinimumSize(new Dimension(imgWidth, imgHeight)); revalidate(); update(); repaint(); } public void update() { imgWidth = getWidth(); imgHeight = getHeight(); // Can't do anything yet... if (imgWidth == 0 || imgHeight == 0) return; img = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_3BYTE_BGR); Graphics2D g = (Graphics2D)img.getGraphics(); g.setBackground(Color.GRAY); g.clearRect(0, 0, imgWidth, imgHeight); float maxColor = (1 << 8) - 1; int start = chargeOffset[_charge - 1]; int end = chargeOffset[_charge]; if ( end - start <= 0 ) return; xscale = (float)imgWidth / (end - start); if (xscale > 20.f) xscale = 20.f; FloatArray x = new FloatArray(); FloatArray y = new FloatArray(); FloatArray z = new FloatArray(); // TODO: Minor speedup: if i is the same on consecutive iterations, just copy image column for (int xi = 0; xi < imgWidth; xi++) { int i = (int)(xi / xscale) + start; if ( i >= end ) break; // Can happen if feature set is out of range for this run (user error)... if ( null == featureClone[i].intensityWindow ) continue; float yscale = imgHeight / (featureClone[i].intensityWindow.length - 1.f); // Scale each window so that monoisotopic peak is maximum intensity, // then clamp so IntensityPlot does not rescale float intensityScale = 0.f; int center = featureClone[i].intensityLeadingPeaks * RESAMPLE_FREQ; for (int j = center - 10; j < center + 10; j++) if (featureClone[i].intensityWindow[j] > intensityScale) intensityScale = featureClone[i].intensityWindow[j]; if (intensityScale == 0.f) { // no signal near center peak; consider entire window for (int j = 0; j < featureClone[i].intensityWindow.length; j++) if (featureClone[i].intensityWindow[j] > intensityScale) intensityScale = featureClone[i].intensityWindow[j]; } if ( intensityScale != 0.f ) intensityScale = maxColor / intensityScale; else intensityScale = 1.f; // punt int maxy = featureClone[i].intensityWindow.length; for (int yi = 0; yi < imgHeight; yi++) { x.add((float)xi); y.add((float)yi); int j = (int) (yi / yscale); float c = featureClone[i].intensityWindow[j] * intensityScale; z.add( c > maxColor ? maxColor : (c < 0 ? 0 : c)); } } IntensityPlot plot = new IntensityPlot(); String scheme = "Heat"; plot.setData(x, y, z); plot.plotSqrt(img, -1.f, 1, scheme); } public int setHighlight(int featureIndex) { // _log.debug(" Highlight charge " +_charge + ": " + chargeOffset[_charge - 1] + " <= " + featureIndex + " < " + chargeOffset[_charge]); if ( featureIndex < chargeOffset[_charge - 1] || featureIndex >= chargeOffset[_charge] ) { clearHighlight(); return -1; } highlightX = (int)((featureIndex - chargeOffset[_charge - 1] + .5) * xscale); repaint(); return featureIndex; } public void clearImage() { int width = img.getWidth(); int height = img.getHeight(); Graphics2D g = (Graphics2D)img.getGraphics(); g.setBackground(Color.GRAY); g.clearRect(0, 0, width, height); } public void clearHighlight() { if ( highlightX >= 0 ) { highlightX = -1; repaint(); } } /** * */ public int getCharge() { return _charge; } /** * */ public void paintComponent(Graphics g) { if ( img != null ) { g.drawImage(img, 0, 0, this); if ( highlightX >= 0 && highlightX < imgWidth) { g.setColor(Color.BLUE); // line g.drawLine(highlightX, 0, highlightX, imgHeight - 1); // top mark g.fillPolygon(new int[]{highlightX-3, highlightX+3, highlightX}, new int[]{0, 0, 7}, 3); // bottom mark g.fillPolygon(new int[]{highlightX-3, highlightX+3, highlightX}, new int[]{imgHeight, imgHeight, imgHeight-7}, 3); } } } /*----------------------------------------------------------------------* * Mouse events *----------------------------------------------------------------------*/ /** * */ public void mouseClicked(MouseEvent e) { ChargeImagePanel chargeImagePanel = (ChargeImagePanel)e.getComponent(); clearHighlights(); int featureIndex = (int)(e.getX() / xscale) + chargeOffset[_charge - 1]; if ( featureIndex < chargeOffset[_charge] && featureIndex < featureClone.length ) HeatMapPanel.this.setHighlightAndBroadcast(featureIndex); } public void mousePressed(MouseEvent e) { } public void mouseReleased(MouseEvent e) { } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } } /** * Runnable to gather intensity windows for features that do not yet * have them */ static class IntensityWindowScanner implements Runnable { static IntensityWindowScanner instance = null; static Object lock = new Object(); Thread worker = null; HeatMapPanel panel = null; FeatureSet featureSet = null; boolean displayWhenDone; private IntensityWindowScanner(HeatMapPanel panel, FeatureSet featureSet, boolean displayWhenDone) { this.panel = panel; this.featureSet = featureSet; this.displayWhenDone = displayWhenDone; } static void load(HeatMapPanel panel, FeatureSet featureSet, boolean displayWhenDone) { synchronized(lock) { // Abort current load, if any if ( null != instance && null != instance.worker ) { instance.worker.interrupt(); instance.worker = null; instance = null; } instance = new IntensityWindowScanner(panel, featureSet, displayWhenDone); Thread w = new Thread(instance); w.setPriority(Thread.MIN_PRIORITY); w.start(); instance.worker = w; } } public void run() { Thread currentThread = Thread.currentThread(); try { ApplicationContext.setMessage("Started extracting intensity windows..."); if (null == featureSet) { ApplicationContext.setMessage("HeatMap: Could not find a displayed feature set."); update(); return; } MSRun run = (MSRun)ApplicationContext.getProperty(SharedProperties.MS_RUN); Feature[] features = featureSet.getFeatures(); if (null == run || null == features || 0 >= features.length) { ApplicationContext.setMessage("HeatMap: Feature set contained nothing to display."); featureSet = null; update(); return; } Feature[] f = new Feature[features.length]; System.arraycopy(features, 0, f, 0, f.length); Arrays.sort(f, featureScanAscComparator); MSRun.MSScan scan = null; float[][] spectrum = null; for (int i = 0; i < f.length; i++) { if ( 0 == (i % 100) ) { float p = Math.round(1000.f * i / f.length) / 10.f; ApplicationContext.setMessage("Extracting intensity windows, " + String.valueOf(p) + "% complete..."); } // If feature intensity window was already extracted, just skip. if ( f[i].intensityLeadingPeaks == 3 && f[i].intensityTrailingPeaks == 3 ) continue; int n = getIndexForScanNum(run, f[i].scan); // This can happen if user, in error, applies feature set from one run to another. if ( n >= run.getScanCount() ) continue; scan = run.getScan(n); spectrum = scan.getSpectrum(); if (null == spectrum) { _log.error("Failed to get spectrum for scan " + f[i].scan); ApplicationContext.setMessage("Failed to get spectrum for scan " + f[i].scan); featureSet = null; update(); return; } if (currentThread.isInterrupted()) throw new InterruptedException(); f[i].intensityWindow = Spectrum.Resample(spectrum, new FloatRange(f[i].mz - 3, f[i].mz + 3), RESAMPLE_FREQ); f[i].intensityLeadingPeaks = 3; f[i].intensityTrailingPeaks = 3; } ApplicationContext.setMessage(""); update(); } catch (InterruptedException e) { _log.debug("loader interrupted"); // just ignore; we're aborting the load if we get here. } catch (Exception e) { ApplicationContext.errorMessage("Error extracting intensity windows", e); ApplicationContext.setMessage("Error extracting intensity windows"); } finally { synchronized(lock) { worker = null; } } } static synchronized boolean isRunning() { synchronized(lock) { return null != instance && null != instance.worker; } } /** * */ private void update() { final HeatMapPanel hmp = panel; final FeatureSet fs = featureSet; final boolean dwd = displayWhenDone; SwingUtilities.invokeLater(new Runnable() { public void run() { hmp.updatePanel(fs, dwd); } }); } } }