org.fhcrc.cpl.viewer.quant.gui.QuantitationVisualizer.java Source code

Java tutorial

Introduction

Here is the source code for org.fhcrc.cpl.viewer.quant.gui.QuantitationVisualizer.java

Source

/*
 * 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.fhcrc.cpl.toolbox.gui.chart.*;
import org.fhcrc.cpl.toolbox.commandline.CommandLineModuleExecutionException;
import org.fhcrc.cpl.toolbox.commandline.CommandLineModuleUtilities;
import org.fhcrc.cpl.toolbox.ApplicationContext;
import org.fhcrc.cpl.toolbox.datastructure.Pair;
import org.fhcrc.cpl.toolbox.filehandler.TempFileManager;
import org.fhcrc.cpl.toolbox.proteomics.ModifiedAminoAcid;
import org.fhcrc.cpl.toolbox.proteomics.MSRun;
import org.fhcrc.cpl.toolbox.proteomics.feature.Spectrum;
import org.fhcrc.cpl.toolbox.proteomics.feature.Feature;
import org.fhcrc.cpl.toolbox.proteomics.feature.FeatureSet;
import org.fhcrc.cpl.toolbox.proteomics.feature.AnalyzeICAT;
import org.fhcrc.cpl.toolbox.proteomics.feature.extraInfo.MS2ExtraInfoDef;
import org.fhcrc.cpl.toolbox.proteomics.feature.extraInfo.IsotopicLabelExtraInfoDef;
import org.fhcrc.cpl.viewer.quant.QuantEvent;
import org.fhcrc.cpl.viewer.quant.QuantEventAssessor;
import org.fhcrc.cpl.viewer.quant.turk.TurkUtilities;
import org.apache.log4j.Logger;
import org.jfree.chart.plot.XYPlot;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.FileOutputStream;
import java.util.*;
import java.util.List;
import java.awt.*;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;

/**
 * Visualize quantitation events.  This class figures out what events to visualize and gathers all
 * the data from each event, but the actual creation of charts is done in PanelWithSpectrumChart.
 *
 * Summaries of the quantitation event information are written in TSV and HTML format.  Qurate
 * can load the TSV version
 */
public class QuantitationVisualizer {
    protected static Logger _log = Logger.getLogger(QuantitationVisualizer.class);

    protected int resolution = PanelWithSpectrumChart.DEFAULT_RESOLUTION;
    public static final int DEFAULT_IMAGE_HEIGHT_3D = 900;
    public static final int DEFAULT_IMAGE_WIDTH_3D = 900;

    public static final int DEFAULT_SINGLE_SCAN_IMAGE_HEIGHT = 100;
    public static final int DEFAULT_MAX_SINGLE_SCANS_TOTAL_IMAGE_HEIGHT = 4000;
    public static final int DEFAULT_SPECTRUM_IMAGE_HEIGHT = 700;
    public static final int DEFAULT_SPECTRUM_IMAGE_WIDTH = 900;

    public static final float AMINOACID_MODIFICATION_EQUALITY_MASS_TOLERANCE = 0.25f;
    public static final float AMINOACID_MODIFICATION_EQUALITY_MZ_TOLERANCE = 0.25f;

    protected Iterator<FeatureSet> featureSetIterator;

    //a non-displayed button that's used to add listeners who care when we complete each event.
    //TODO: do this in some less silly way
    protected JButton dummyProgressButton = new JButton();

    //Have to stop somewhere.  Chart will extend this number of peaks beyond the heavy monoisotope, plus slop
    int numHeavyPeaksToPlot = 4;

    protected File outDir = null;
    protected File outTsvFile = null;
    //for Mechanical Turk HITs
    protected File outTurkFile = null;
    protected File outHtmlFile = null;
    //Should we append TSV output to the existing file, if one exists?
    protected boolean appendTsvOutput = false;
    protected boolean tsvFileAlreadyExists = false;

    //Do we actually create the charts, or just create the output files?
    protected boolean shouldCreateCharts = true;
    //Should all events that are written be marked Bad?  If not, mark Unknown
    protected boolean markAllEventsBad = false;
    //should we do an automated assessment of the event?
    protected boolean shouldAssessEvents = true;

    //The assumed difference in mass between isotopic peaks
    protected float peakSeparationMass = PanelWithSpectrumChart.DEFAULT_PEAK_SEPARATION_MASS;
    //Mass tolerance around each peak, for summary chart
    protected float peakTolerancePPM = PanelWithSpectrumChart.DEFAULT_PEAK_TOLERANCE_PPM;

    //height and width of images
    protected int scanImageHeight = DEFAULT_SINGLE_SCAN_IMAGE_HEIGHT;
    protected int maxScansImageHeight = DEFAULT_MAX_SINGLE_SCANS_TOTAL_IMAGE_HEIGHT;
    protected int spectrumImageHeight = DEFAULT_SPECTRUM_IMAGE_HEIGHT;
    protected int imageWidth = DEFAULT_SPECTRUM_IMAGE_WIDTH;

    //Directory of mzXML files
    protected File mzXmlDir;
    //Single mzXML file
    protected File mzXmlFile = null;

    //The amount by which two features can differ and still be combined into the same single representation.
    //TODO: convert this to log space, parameterize
    protected float maxCombineFeatureRatioDiff = 0.2f;

    //Should we write out HTML and tsv text for our events as we build them?
    protected boolean writeHTMLAndText = true;

    //PeptideProphet minimum
    protected float minPeptideProphet = 0;

    //Scans to display around event
    protected int numPaddingScans = 3;
    //m/z padding to display around event
    protected float mzPadding = 1.5f;

    //keeps track of the unique peptides of the events plotted
    protected Set<String> peptidesFound = new HashSet<String>();

    //A ghastly structure to hold references to all of the tsv and html files created
    //In order: protein, peptide, fraction, charge, pairs of <tsv,html> files
    protected Map<String, Map<String, Map<String, Map<Integer, List<Pair<File, File>>>>>> proteinPeptideFractionChargeFilesMap = new HashMap<String, Map<String, Map<String, Map<Integer, List<Pair<File, File>>>>>>();

    //For QuantEvent objects with no associated protein
    protected static final String DUMMY_PROTEIN_NAME = "DUMMY_PROTEIN";

    //should we show 3D plots?
    protected boolean show3DPlots = true;
    //Full control of 3D plot parameters
    protected int rotationAngle3D = PanelWithSpectrumChart.DEFAULT_CONTOUR_PLOT_ROTATION;
    protected int tiltAngle3D = PanelWithSpectrumChart.DEFAULT_CONTOUR_PLOT_TILT;
    protected int imageHeight3D = DEFAULT_IMAGE_HEIGHT_3D;
    protected int imageWidth3D = DEFAULT_IMAGE_WIDTH_3D;
    protected boolean show3DAxes = true;
    //Controls whether we write event info directly to the chart images
    protected boolean writeInfoOnCharts = false;
    protected boolean writeTheoreticalPeaksOnCharts = false;
    //If we're adding sidebar information to the images, width of the sidebar
    protected int sidebarWidth = 180;

    //a single scan that we want to visualize.  This should probably be controlled somewhere else
    int scan = 0;

    //Sets of things we want to visualize.  This should probably be controlled somewhere else
    protected Set<String> peptidesToExamine;
    protected Set<String> proteinsToExamine;
    protected Set<String> fractionsToExamine;

    //PrintWriters for the output files
    protected PrintWriter outHtmlPW;
    protected PrintWriter outTsvPW;
    protected PrintWriter outTurkPW;

    //Unique identifier (per file) for Turk HITs
    protected int currentTurkID = 0;
    //URL prefix to add before all image names in Turk output file.  Should end with "/"
    protected String turkImageURLPrefix = "";

    protected int turkChartWidth = 690;
    protected int turkChartHeight = 460;

    //Should we include the Protein column in the output files?
    //TODO: get rid of this, always show protein column
    protected boolean showProteinColumn = true;

    public QuantitationVisualizer() {
    }

    /**
     * Visualize just the events specified
     * TODO: fold parts of this in with the no-arg visualizeQuantEvents
     * @param quantEvents
     * @throws IOException
     * @return the list of QuantEvents in the order in which they were written to the file(s)
     */
    public List<QuantEvent> visualizeQuantEvents(List<QuantEvent> quantEvents, boolean saveInProteinDirs)
            throws IOException {
        if (outHtmlFile == null)
            outHtmlFile = new File(outDir, "quantitation.html");
        if (outTsvFile == null)
            outTsvFile = new File(outDir, "quantitation.tsv");
        tsvFileAlreadyExists = outTsvFile.exists();
        if (writeHTMLAndText) {
            outHtmlPW = new PrintWriter(outHtmlFile);
            outTsvPW = new PrintWriter(new FileOutputStream(outTsvFile, appendTsvOutput));

            //if we're appending, don't write header.  Cheating by writing it to a fake file
            PrintWriter conditionalTsvPW = outTsvPW;
            if (appendTsvOutput && tsvFileAlreadyExists)
                conditionalTsvPW = new PrintWriter(
                        TempFileManager.createTempFile("fake_file", "fake_file_for_quantvisualizer"));

            QuantEvent.writeHeader(outHtmlPW, conditionalTsvPW, showProteinColumn, show3DPlots);
            _log.debug("Wrote header to HTML file " + outHtmlFile.getAbsolutePath() + " and tsv file "
                    + outTsvFile.getAbsolutePath());
            TempFileManager.deleteTempFiles("fake_file_for_quantvisualizer");
        }
        if (outTurkFile != null) {
            outTurkPW = new PrintWriter(outTurkFile);
            outTurkPW.println(TurkUtilities.createTurkHITFileHeaderLine());
            outTurkPW.flush();
        }

        //map from fraction name to list of events in that fraction
        Map<String, List<QuantEvent>> fractionEventMap = new HashMap<String, List<QuantEvent>>();

        for (QuantEvent quantEvent : quantEvents) {
            List<QuantEvent> eventList = fractionEventMap.get(quantEvent.getFraction());
            if (eventList == null) {
                eventList = new ArrayList<QuantEvent>();
                fractionEventMap.put(quantEvent.getFraction(), eventList);
            }
            eventList.add(quantEvent);

        }
        //sort events by scan within fractions, to keep recently-used scans in cache
        Comparator<QuantEvent> scanAscComp = new QuantEvent.ScanAscComparator();
        for (List<QuantEvent> eventList : fractionEventMap.values())
            Collections.sort(eventList, scanAscComp);
        int numEventsProcessed = 0;

        //listeners that want to be updated when we finish an event
        ActionListener[] progressListeners = dummyProgressButton.getActionListeners();

        List<QuantEvent> resortedEvents = new ArrayList<QuantEvent>();
        for (String fraction : fractionEventMap.keySet()) {
            File mzXmlFile = CommandLineModuleUtilities.findFileWithPrefix(fraction + ".", mzXmlDir, "mzXML");
            MSRun run = MSRun.load(mzXmlFile.getAbsolutePath());

            for (QuantEvent quantEvent : fractionEventMap.get(fraction)) {
                resortedEvents.add(quantEvent);
                File outDirThisEvent = outDir;
                if (saveInProteinDirs && shouldCreateCharts) {
                    String protein = quantEvent.getProtein();
                    outDirThisEvent = new File(outDir, protein);
                    outDirThisEvent.mkdir();
                }
                handleEvent(run, outDirThisEvent, quantEvent.getProtein(), fraction, quantEvent);
                numEventsProcessed++;

                if (progressListeners != null) {
                    ActionEvent event = new ActionEvent(dummyProgressButton, 0, "" + numEventsProcessed);
                    for (ActionListener listener : progressListeners)
                        listener.actionPerformed(event);
                }
            }
        }

        if (writeHTMLAndText) {
            QuantEvent.writeFooterAndClose(outHtmlPW, outTsvPW);
            ApplicationContext.infoMessage("Saved HTML file " + outHtmlFile.getAbsolutePath());
            ApplicationContext.infoMessage("Saved TSV file " + outTsvFile.getAbsolutePath());
        }
        if (outTurkFile != null) {
            try {
                outTurkPW.close();
            } catch (Exception e) {
            }
        }

        return resortedEvents;
    }

    /**
     * Iterate through all fractions, finding and visualizing the selected events
     */
    public void visualizeQuantEvents() throws IOException {
        //trying to prevent memory leaks
        ImageIO.setUseCache(false);

        if (outHtmlFile == null)
            outHtmlFile = new File(outDir, "quantitation.html");
        if (outTsvFile == null)
            outTsvFile = new File(outDir, "quantitation.tsv");

        _log.debug("visualizeQuantEvents begin, write HTML and text? " + writeHTMLAndText);
        if (writeHTMLAndText) {
            outHtmlPW = new PrintWriter(outHtmlFile);
            outTsvPW = new PrintWriter(new FileOutputStream(outTsvFile, appendTsvOutput));
            _log.debug("opened HTML file " + outHtmlFile.getAbsolutePath() + " and tsv file "
                    + outTsvFile.getAbsolutePath() + " for writing.");

            //if we're appending, don't write header.  Cheating by writing it to a fake file
            PrintWriter conditionalTsvPW = outTsvPW;
            if (appendTsvOutput && tsvFileAlreadyExists)
                conditionalTsvPW = new PrintWriter(
                        TempFileManager.createTempFile("fake_file", "fake_file_for_quantvisualizer"));

            QuantEvent.writeHeader(outHtmlPW, conditionalTsvPW, showProteinColumn, show3DPlots);
            _log.debug("Wrote HTML and TSV header");
            TempFileManager.deleteTempFiles("fake_file_for_quantvisualizer");
        }

        boolean processedAFraction = false;
        while (featureSetIterator.hasNext()) {
            FeatureSet fraction = featureSetIterator.next();
            ApplicationContext
                    .infoMessage("Evaluating fraction " + MS2ExtraInfoDef.getFeatureSetBaseName(fraction));
            if (fractionsToExamine == null
                    || fractionsToExamine.contains(MS2ExtraInfoDef.getFeatureSetBaseName(fraction))) {
                ApplicationContext
                        .infoMessage("Handling fraction " + MS2ExtraInfoDef.getFeatureSetBaseName(fraction));
                handleFraction(fraction);
                processedAFraction = true;
            }
            //trying to prevent memory overflow
            System.gc();
        }
        if (!processedAFraction)
            ApplicationContext.infoMessage("WARNING: no fractions processed");
        if (writeHTMLAndText) {
            QuantEvent.writeFooterAndClose(outHtmlPW, outTsvPW);
            ApplicationContext.infoMessage("Saved HTML file " + outHtmlFile.getAbsolutePath());
            ApplicationContext.infoMessage("Saved TSV file " + outTsvFile.getAbsolutePath());
        }

    }

    /**
     * Visualize selected events from a fraction
     * @param featureSet
     * @throws CommandLineModuleExecutionException
     */
    protected void handleFraction(FeatureSet featureSet) throws IOException {
        String errorMessage = "";

        FeatureSet.FeatureSelector sel = new FeatureSet.FeatureSelector();
        sel.setMinPProphet(minPeptideProphet);
        featureSet = featureSet.filter(sel);

        Arrays.sort(featureSet.getFeatures(), new Feature.ScanAscComparator());

        MSRun run = null;
        File fileToLoad = mzXmlFile;
        if (fileToLoad == null) {
            String fractionName = MS2ExtraInfoDef.getFeatureSetBaseName(featureSet);
            errorMessage = "Error finding mzXML files";
            fileToLoad = CommandLineModuleUtilities.findFileWithPrefix(fractionName, mzXmlDir, "mzXML");
        }

        run = MSRun.load(fileToLoad.getAbsolutePath());
        _log.debug("\tLoaded mzXML file " + fileToLoad.getAbsolutePath());
        if (proteinsToExamine != null)
            handleProteinsInRun(featureSet, run, proteinsToExamine);
        else if (peptidesToExamine != null)
            handlePeptidesInRun(featureSet, run, peptidesToExamine, DUMMY_PROTEIN_NAME, outDir);
        else if (scan != 0) {
            String fractionName = MS2ExtraInfoDef.getFeatureSetBaseName(featureSet);
            if (fractionsToExamine == null || fractionsToExamine.contains(fractionName))
                handleScanInRun(featureSet, fractionName, run, scan);
        } else {
            _log.debug("\t processing all scans");
            //handle all scans
            String fractionName = MS2ExtraInfoDef.getFeatureSetBaseName(featureSet);
            List<QuantEvent> allQuantEvents = new ArrayList<QuantEvent>();
            for (Feature feature : featureSet.getFeatures()) {
                if (IsotopicLabelExtraInfoDef.hasRatio(feature))
                    allQuantEvents.add(new QuantEvent(feature, fractionName));
            }
            if (allQuantEvents.isEmpty())
                ApplicationContext.infoMessage("\tSkipping empty fraction " + fractionName);
            else {
                List<QuantEvent> nonOverlappingQuantEvents = findNonOverlappingEvents(allQuantEvents);
                for (QuantEvent quantEvent : nonOverlappingQuantEvents) {
                    handleEvent(run, outDir, DUMMY_PROTEIN_NAME, fractionName, quantEvent);
                }
                ApplicationContext.infoMessage("\tProcessed " + nonOverlappingQuantEvents.size()
                        + " non-overlapping events for " + featureSet.getFeatures().length + " features");
            }
        }
    }

    /**
     * Visualize all charts for a particular scan number.  Awfully inefficient, but
     * it doesn't matter because this is only invoked for single-scan runs
     * @param featureSet
     * @param fraction
     * @param run
     * @throws CommandLineModuleExecutionException
     */
    protected void handleScanInRun(FeatureSet featureSet, String fraction, MSRun run, int scanToHandle) {
        for (Feature feature : featureSet.getFeatures()) {
            if (scanToHandle == feature.getScan())
                handleFeature(run, outDir, DUMMY_PROTEIN_NAME, fraction, feature);
        }
    }

    /**
     * Display all passing events for a particular set of proteins
     * @param featureSet
     * @param run
     * @param proteinsToHandle
     * @throws CommandLineModuleExecutionException
     */
    protected void handleProteinsInRun(FeatureSet featureSet, MSRun run, Set<String> proteinsToHandle) {
        for (String protein : proteinsToHandle) {
            Set<String> peptidesThisProtein = new HashSet<String>();
            for (Feature feature : featureSet.getFeatures()) {
                List<String> proteinsThisFeature = MS2ExtraInfoDef.getProteinList(feature);
                if (proteinsThisFeature != null && proteinsThisFeature.contains(protein)) {
                    String peptide = MS2ExtraInfoDef.getFirstPeptide(feature);
                    {

                        ApplicationContext.infoMessage("Found peptide " + peptide + ", protein " + protein);
                        peptidesThisProtein.add(peptide);
                    }
                }

            }

            if (!peptidesThisProtein.isEmpty()) {
                ApplicationContext.infoMessage("Protein " + protein + ": Finding " + peptidesThisProtein.size()
                        + " peptides in run " + MS2ExtraInfoDef.getFeatureSetBaseName(featureSet));
                File proteinOutDir = new File(outDir, protein);
                proteinOutDir.mkdir();
                handlePeptidesInRun(featureSet, run, peptidesThisProtein, protein, proteinOutDir);
            }
        }
    }

    /**
     * Display all passing events for a particular set of peptides
     * @param featureSet
     * @param run
     * @param peptidesToHandle
     * @param proteinName
     * @param outputDir
     * @throws CommandLineModuleExecutionException
     */
    protected void handlePeptidesInRun(FeatureSet featureSet, MSRun run, Set<String> peptidesToHandle,
            String proteinName, File outputDir) {
        String fraction = MS2ExtraInfoDef.getFeatureSetBaseName(featureSet);

        List<QuantEvent> allQuantEventsAllPeptides = new ArrayList<QuantEvent>();
        //        String labeledResidue = null;
        //        float labelMassDiff = 0;
        for (Feature feature : featureSet.getFeatures()) {
            if (peptidesToHandle.contains(MS2ExtraInfoDef.getFirstPeptide(feature))
                    && MS2ExtraInfoDef.getPeptideProphet(feature) >= this.minPeptideProphet
                    && IsotopicLabelExtraInfoDef.hasRatio(feature)) {
                //                if (labeledResidue == null)
                //                {
                //                    AnalyzeICAT.IsotopicLabel label = IsotopicLabelExtraInfoDef.getLabel(feature);
                //                    if (label != null)
                //                    {
                //                        labeledResidue = "" + label.getResidue();
                //                        labelMassDiff = label.getHeavy() - label.getLight();
                //                        _log.debug("Found label: " + labeledResidue + ", " + labelMassDiff);
                //                    }
                //                }
                allQuantEventsAllPeptides.add(new QuantEvent(feature, fraction));
            }
        }
        //        if (labeledResidue == null)
        //            ApplicationContext.infoMessage("WARNING: unable to determine modification used for quantitation.  " +
        //                    "Cannot collapse light and heavy states.");

        List<QuantEvent> nonOverlappingEventsAllPeptides = findNonOverlappingQuantEventsAllPeptides(
                allQuantEventsAllPeptides);

        for (QuantEvent quantEvent : nonOverlappingEventsAllPeptides) {
            int numTotalEvents = 1;
            if (quantEvent.getOtherEvents() != null)
                numTotalEvents += quantEvent.getOtherEvents().size();
            ApplicationContext.infoMessage("\tHandling peptide " + quantEvent.getPeptide() + ", charge "
                    + quantEvent.getCharge() + " with " + numTotalEvents + " events");
            handleEvent(run, outputDir, proteinName, fraction, quantEvent);
        }
    }

    /**
     * Find overlapping events in a list of events that may come from different peptides, fractions, etc.
     * Return a list with one member of each of those overlapping event lists
     * @param quantEvents
     * @return
     */
    public List<QuantEvent> findNonOverlappingQuantEventsAllPeptides(List<QuantEvent> quantEvents) {
        List<QuantEvent> result = new ArrayList<QuantEvent>();
        Map<String, Map<String, Map<Integer, List<QuantEvent>>>> peptideFractionChargeQuantEventsMap = new HashMap<String, Map<String, Map<Integer, List<QuantEvent>>>>();
        _log.debug("findNonOverlappingQuantEventsAllPeptides 1");
        for (QuantEvent quantEvent : quantEvents) {
            String peptide = quantEvent.getPeptide();

            Map<String, Map<Integer, List<QuantEvent>>> fractionChargesMap = peptideFractionChargeQuantEventsMap
                    .get(peptide);
            if (fractionChargesMap == null) {
                fractionChargesMap = new HashMap<String, Map<Integer, List<QuantEvent>>>();
                peptideFractionChargeQuantEventsMap.put(peptide, fractionChargesMap);
            }

            String fraction = quantEvent.getFraction();

            Map<Integer, List<QuantEvent>> chargeEventsMap = fractionChargesMap.get(fraction);
            if (chargeEventsMap == null) {
                chargeEventsMap = new HashMap<Integer, List<QuantEvent>>();
                fractionChargesMap.put(fraction, chargeEventsMap);
            }

            List<QuantEvent> eventsToEvaluate = chargeEventsMap.get(quantEvent.getCharge());

            if (eventsToEvaluate == null) {
                eventsToEvaluate = new ArrayList<QuantEvent>();
                chargeEventsMap.put(quantEvent.getCharge(), eventsToEvaluate);
            }
            eventsToEvaluate.add(quantEvent);
        }
        _log.debug("Built map with " + peptideFractionChargeQuantEventsMap.size() + " peptides");

        //combine modification states that represent light and heavy versions of same species.
        //To do this, first break up everything by peptide, fraction and charge.  Then associate everything
        //within a charge by light mz and find overlapping events within that list.  Don't bother looking at
        //heavy -- we're not dealing with multiple-label scenarios.
        for (String peptide : peptideFractionChargeQuantEventsMap.keySet()) {
            _log.debug("Map peptide " + peptide);
            Map<String, Map<Integer, List<QuantEvent>>> fractionChargesMap = peptideFractionChargeQuantEventsMap
                    .get(peptide);
            for (String fraction : fractionChargesMap.keySet()) {
                _log.debug("  Map fraction " + fraction);
                Map<Integer, List<QuantEvent>> chargeEventsMap = fractionChargesMap.get(fraction);
                for (int charge : chargeEventsMap.keySet()) {
                    _log.debug("  Map charge " + charge + ", " + chargeEventsMap.get(charge) + " events");
                    Map<Float, List<QuantEvent>> lightMzEventsMap = new HashMap<Float, List<QuantEvent>>();
                    for (QuantEvent event : chargeEventsMap.get(charge)) {
                        boolean foundIt = false;
                        for (Float lightMz : lightMzEventsMap.keySet()) {
                            if (Math.abs(
                                    event.getLightMz() - lightMz) < AMINOACID_MODIFICATION_EQUALITY_MZ_TOLERANCE) {
                                lightMzEventsMap.get(lightMz).add(event);
                                foundIt = true;
                                break;
                            }
                        }
                        if (!foundIt) {
                            List<QuantEvent> eventList = new ArrayList<QuantEvent>();
                            eventList.add(event);
                            lightMzEventsMap.put(event.getLightMz(), eventList);
                        }
                    }
                    _log.debug("Collapsing events from " + lightMzEventsMap.size() + " distinct mzs");
                    for (List<QuantEvent> eventList : lightMzEventsMap.values()) {
                        List<QuantEvent> nonOverlappingList = findNonOverlappingEvents(eventList);
                        if (nonOverlappingList.size() < eventList.size())
                            _log.debug("    Reduced " + eventList.size() + " events to " + nonOverlappingList.size()
                                    + " non-overlapping events.");
                        result.addAll(nonOverlappingList);
                    }
                }
            }
        }

        //        for (String peptide : peptideFractionChargeModsQuantEventsMap.keySet())
        //        {
        //            Map<String, Map<Integer, Map<String, List<QuantEvent>>>> fractionChargesMap = peptideFractionChargeModsQuantEventsMap.get(peptide);
        //            _log.debug("processing peptide " + peptide + " with " + fractionChargesMap.size() + " fractions");
        //
        //            for (String fraction : fractionChargesMap.keySet())
        //            {
        //                Map<Integer, Map<String, List<QuantEvent>>> chargeModificationsMap =
        //                        fractionChargesMap.get(fraction);
        //                for (int charge : chargeModificationsMap.keySet())
        //                {
        //                    Map<String, List<QuantEvent>> modificationsEventsMap = chargeModificationsMap.get(charge);
        //                    for (String modifications : modificationsEventsMap.keySet())
        //                    {
        //                        List<QuantEvent> eventList = modificationsEventsMap.get(modifications);
        //                        _log.debug("\tCharge " + charge + ", mods " + modifications + ": " + eventList.size() + " events");
        //                        result.addAll(findNonOverlappingEvents(eventList));
        //                    }
        //                }
        //            }
        //        }
        return result;
    }

    /**
     * Find modification states that are equivalent except for modification masses representing the difference
     * between light and heavy labels.
     */
    //    protected List<Pair<String,String>> pairLightAndHeavyModificationStates(Collection<String> modificationStates,
    //                                                          String labeledResidue, float labelMassDiff)
    //    {
    //        ModResidueMassAscComparator modResidueMassAscComparator = new ModResidueMassAscComparator();
    //
    //        List<Pair<String,String>> lightHeavyModStatePairs = new ArrayList<Pair<String,String>>();
    //        Set<String> alreadyAssignedCompareModStates = new HashSet<String>();
    //        for (String baseModificationState : modificationStates)
    //        {
    //            _log.debug("      pairLightAndHeavy, checking " + baseModificationState);
    //            Map<Integer, List<ModifiedAminoAcid>> baseModStateMap =
    //                    MS2ExtraInfoDef.parsePositionModifiedAminoAcidListMapString(baseModificationState);
    //            for (String compareModificationState : modificationStates)
    //            {
    //                if (baseModificationState.equals(compareModificationState) ||
    //                        alreadyAssignedCompareModStates.contains(compareModificationState))
    //                    continue;
    //                boolean foundDifference = false;
    //                Map<Integer, List<ModifiedAminoAcid>> compareModStateMap =
    //                        MS2ExtraInfoDef.parsePositionModifiedAminoAcidListMapString(
    //                                compareModificationState);
    //                if (baseModStateMap.size() != compareModStateMap.size())
    //                {
    //                    continue;
    //                }
    //                for (int position : baseModStateMap.keySet())
    //                {
    //                    if (!compareModStateMap.containsKey(position))
    //                    {
    //                        foundDifference = true;
    //                        break;
    //                    }
    //                    List<ModifiedAminoAcid> baseModsThisPosition = baseModStateMap.get(position);
    //                    List<ModifiedAminoAcid> compareModsThisPosition = compareModStateMap.get(position);
    //
    //                    if (baseModsThisPosition.size() != compareModsThisPosition.size())
    //                    {
    //                        foundDifference = true;
    //                        break;
    //                    }
    //                    Collections.sort(baseModsThisPosition, modResidueMassAscComparator);
    //                    Collections.sort(compareModsThisPosition, modResidueMassAscComparator);
    //
    //                    for (int i=0; i<baseModsThisPosition.size(); i++)
    //                    {
    //                        ModifiedAminoAcid baseMod = baseModsThisPosition.get(i);
    //                        ModifiedAminoAcid compareMod = compareModsThisPosition.get(i);
    //                        if (!baseMod.getAminoAcidAsString().equals(compareMod.getAminoAcidAsString()))
    //                        {
    //                            foundDifference = true;
    //                            break;
    //                        }
    //                        float absMassDiff = (float) Math.abs(baseMod.getMass() - compareMod.getMass());
    //                        if (absMassDiff > AMINOACID_MODIFICATION_EQUALITY_MASS_TOLERANCE)
    //                        {
    //                            if ((!baseMod.getAminoAcidAsString().equals(labeledResidue)) ||
    //                                    (absMassDiff - labelMassDiff > AMINOACID_MODIFICATION_EQUALITY_MASS_TOLERANCE))
    //                            {
    //                                foundDifference = true;
    //                                break;
    //                            }
    //                        }
    //                    }
    //                }
    //                if (foundDifference)
    //                    continue;
    //                alreadyAssignedCompareModStates.add(baseModificationState);
    //                alreadyAssignedCompareModStates.add(compareModificationState);
    //                lightHeavyModStatePairs.add(
    //                        new Pair<String,String>(baseModificationState, compareModificationState));
    //                break;
    //            }
    //        }
    //        return lightHeavyModStatePairs;
    //    }

    protected class ModResidueMassAscComparator implements Comparator<ModifiedAminoAcid> {
        public int compare(ModifiedAminoAcid o1, ModifiedAminoAcid o2) {
            int result = (o1.getAminoAcidAsString().compareTo(o2.getAminoAcidAsString()));
            if (result == 0) {
                if (o1.getMass() > o2.getMass())
                    return 1;
                if (o1.getMass() < o2.getMass())
                    return -1;
                return 0;
            }
            return result;
        }
    }

    /**
     * cover method.  Turn the features into events, generate the nonoverlapping events, and
     * return the feature results
     * @param features
     * @return
     */
    public List<Feature> findNonOverlappingFeatures(List<Feature> features) {
        List<QuantEvent> quantEvents = new ArrayList<QuantEvent>();
        for (Feature feature : features)
            quantEvents.add(new QuantEvent(feature, ""));
        List<QuantEvent> nonOverlappingEvents = findNonOverlappingEvents(quantEvents);
        List<Feature> result = new ArrayList<Feature>();
        for (QuantEvent quantEvent : nonOverlappingEvents) {
            Feature representativeFeature = quantEvent.getSourceFeature();
            if (quantEvent.getOtherEvents() != null && !quantEvent.getOtherEvents().isEmpty()) {
                List<Spectrum.Peak> otherFeaturesAsPeaks = new ArrayList<Spectrum.Peak>();
                for (QuantEvent otherEvent : quantEvent.getOtherEvents())
                    otherFeaturesAsPeaks.add(otherEvent.getSourceFeature());
                representativeFeature.comprised = otherFeaturesAsPeaks
                        .toArray(new Spectrum.Peak[otherFeaturesAsPeaks.size()]);
            }
            result.add(representativeFeature);
        }
        return result;
    }

    /**
     * Given a list of features (presumably with the same peptide ID, in the same charge and
     * modification state), collapse them down to a single feature representing each set of
     * overlapping events
     * @param quantEvents
     * @return
     */
    public List<QuantEvent> findNonOverlappingEvents(List<QuantEvent> quantEvents) {
        List<QuantEvent> result = new ArrayList<QuantEvent>();

        while (!quantEvents.isEmpty()) {
            List<QuantEvent> eventsOverlappingFirst = findEventsOverlappingFirst(quantEvents);
            QuantEvent firstRepresentative = eventsOverlappingFirst.get(0);

            firstRepresentative.setOtherEvents(new ArrayList<QuantEvent>());
            for (int i = 1; i < eventsOverlappingFirst.size(); i++)
                firstRepresentative.getOtherEvents().add(eventsOverlappingFirst.get(i));
            result.add(firstRepresentative);
        }
        return result;
    }

    /**
     * Find all events overlapping in scans with the first event
     * SIDE EFFECT: Remove them all from eventsWithinCharge
     * @param eventsWithinCharge
     * @return
     */
    public List<QuantEvent> findEventsOverlappingFirst(List<QuantEvent> eventsWithinCharge) {
        if (eventsWithinCharge.size() == 1) {
            List<QuantEvent> result = new ArrayList<QuantEvent>(eventsWithinCharge);
            eventsWithinCharge.remove(0);
            return result;
        }

        QuantEvent firstEvent = eventsWithinCharge.get(0);
        eventsWithinCharge.remove(firstEvent);
        List<QuantEvent> eventsOverlappingFirst = new ArrayList<QuantEvent>();
        eventsOverlappingFirst.add(firstEvent);
        eventsOverlappingFirst.addAll(findEventsOverlappingEvent(firstEvent, eventsWithinCharge));

        //find the "median" feature by scan (round down if even number)
        Collections.sort(eventsOverlappingFirst, new QuantEvent.ScanAscComparator());
        int numOverlapping = eventsOverlappingFirst.size();
        int medianEventIndex = (numOverlapping) / 2;
        if (numOverlapping % 2 == 1)
            medianEventIndex = (numOverlapping - 1) / 2;

        List<QuantEvent> sortedForResult = new ArrayList<QuantEvent>();
        sortedForResult.add(eventsOverlappingFirst.get(medianEventIndex));
        for (int i = 0; i < eventsOverlappingFirst.size(); i++)
            if (i != medianEventIndex)
                sortedForResult.add(eventsOverlappingFirst.get(i));

        QuantEvent representativeEvent = sortedForResult.get(0);
        _log.debug("\t" + representativeEvent.getPeptide() + ", fraction " + representativeEvent.getFraction()
                + ", charge " + representativeEvent.getCharge() + ", ratio " + representativeEvent.getRatio()
                + ", scans " + representativeEvent.getFirstLightQuantScan() + "-"
                + representativeEvent.getLastLightQuantScan() + ", represents " + sortedForResult.size()
                + " event(s)");
        if (sortedForResult.size() > 1)
            for (int i = 1; i < sortedForResult.size(); i++)
                _log.debug("\t\tother event " + i + ", ratio=" + sortedForResult.get(i).getRatio());
        return sortedForResult;
    }

    /**
     * Find all events overlapping in scans with the base event
     * SIDE EFFECT: Remove them all from otherEvents
     * @param baseEvent
     * @param otherEvents
     * @return
     */
    public List<QuantEvent> findEventsOverlappingEvent(QuantEvent baseEvent, List<QuantEvent> otherEvents) {
        List<QuantEvent> eventsOverlappingBaseEvent = new ArrayList<QuantEvent>();
        if (otherEvents.size() > 0) {
            //take a broad interpretation of overlap -- overlapping either light or heavy
            int firstEventStart = Math.min(baseEvent.getFirstLightQuantScan(), baseEvent.getFirstHeavyQuantScan());
            int firstEventEnd = Math.max(baseEvent.getLastLightQuantScan(), baseEvent.getLastHeavyQuantScan());
            if (firstEventStart <= 0)
                firstEventStart = baseEvent.getScan();
            if (firstEventEnd <= 0)
                firstEventEnd = baseEvent.getScan();
            double firstFeatureRatio = baseEvent.getRatio();
            _log.debug("Looking for events overlapping " + firstEventStart + "-" + firstEventEnd + ", ratio "
                    + firstFeatureRatio + ", out of " + otherEvents.size());

            for (QuantEvent compareEvent : otherEvents) {
                int compareFeatureStart = Math.min(compareEvent.getFirstLightQuantScan(),
                        compareEvent.getFirstHeavyQuantScan());
                int compareFeatureEnd = Math.max(compareEvent.getLastLightQuantScan(),
                        compareEvent.getLastHeavyQuantScan());
                if (compareFeatureStart <= 0)
                    compareFeatureStart = compareEvent.getScan();
                if (compareFeatureEnd <= 0)
                    compareFeatureEnd = compareEvent.getScan();
                _log.debug("\tChecking " + compareFeatureStart + "-" + compareFeatureEnd + ", ratio "
                        + compareEvent.getRatio());

                //first check ratio similarity, because that's simplest
                if (Math.abs(compareEvent.getRatio() - firstFeatureRatio) > maxCombineFeatureRatioDiff)
                    continue;

                if (Math.max(firstEventStart, compareFeatureStart) < Math.min(firstEventEnd, compareFeatureEnd)) {
                    _log.debug("\t\tMatch!");
                    eventsOverlappingBaseEvent.add(compareEvent);
                }
            }
            otherEvents.removeAll(eventsOverlappingBaseEvent);
        }

        //        result.comprised = otherFeaturesAsPeaks.toArray(new Spectrum.Peak[otherFeaturesAsPeaks.size()]);
        return eventsOverlappingBaseEvent;
    }

    /**
     * Call PanelWithSpectrumChart to create all the charts for a quantitative event. Save
     * all the charts.
     * Writes a row to the output files
     * @param run
     * @param outputDir
     * @param protein
     * @param fraction
     * @param feature
     * @throws CommandLineModuleExecutionException
     */
    protected void handleFeature(MSRun run, File outputDir, String protein, String fraction, Feature feature) {
        QuantEvent quantEvent = new QuantEvent(feature, fraction);
        handleEvent(run, outputDir, protein, fraction, quantEvent);
    }

    /**
     * Given a QuantEvent and a run, create the PanelWithSpectrumChart object that will create all the charts,
     * and generate them
     * @param run
     * @param quantEvent
     * @return
     */
    public PanelWithSpectrumChart createPanelWithSpectrumChart(MSRun run, QuantEvent quantEvent) {
        int firstLightQuantScan = quantEvent.getFirstLightQuantScan();
        int lastLightQuantScan = quantEvent.getLastLightQuantScan();
        int firstHeavyQuantScan = quantEvent.getFirstLightQuantScan();
        int lastHeavyQuantScan = quantEvent.getLastHeavyQuantScan();

        //Add padding around quant event limits
        int minScanIndex = Math
                .max(Math.abs(run.getIndexForScanNum(Math.min(firstLightQuantScan, firstHeavyQuantScan)))
                        - numPaddingScans, 0);
        int maxScanIndex = Math
                .min(Math.abs(run.getIndexForScanNum(Math.min(lastLightQuantScan, lastHeavyQuantScan)))
                        + numPaddingScans, run.getScanCount() - 1);

        int minScan = run.getScanNumForIndex(minScanIndex);
        int maxScan = run.getScanNumForIndex(maxScanIndex);

        float minMz = quantEvent.getLightMz() - mzPadding;
        float maxMz = quantEvent.getHeavyMz() + numHeavyPeaksToPlot / quantEvent.getCharge() + mzPadding;
        _log.debug("Building chart for event:\n\t" + quantEvent);
        _log.debug("Scan=" + quantEvent.getScan() + ", ratio=" + quantEvent.getRatio() + ", lightInt="
                + quantEvent.getLightIntensity() + ", heavyInt=" + quantEvent.getHeavyIntensity()
                + ", minScanIndex=" + minScanIndex + ", maxScanIndex=" + maxScanIndex + ", minMz=" + minMz
                + ", maxMz=" + maxMz);

        //create the PanelWithSpectrumChart and make it generate everything
        PanelWithSpectrumChart spectrumPanel = new PanelWithSpectrumChart(run, minScan, maxScan, minMz, maxMz,
                firstLightQuantScan, lastLightQuantScan, firstHeavyQuantScan, lastHeavyQuantScan,
                quantEvent.getLightMz(), quantEvent.getHeavyMz(), quantEvent.getCharge());
        spectrumPanel.setResolution(resolution);
        spectrumPanel.setGenerateLineCharts(true);
        spectrumPanel.setGenerate3DChart(show3DPlots);
        spectrumPanel.setIdEventScan(quantEvent.getScan());
        spectrumPanel.setIdEventMz(quantEvent.getMz());
        List<Integer> otherEventScans = new ArrayList<Integer>();
        List<Float> otherEventMzs = new ArrayList<Float>();
        if (quantEvent.getOtherEvents() != null)
            for (QuantEvent otherEvent : quantEvent.getOtherEvents()) {
                otherEventScans.add(otherEvent.getScan());
                otherEventMzs.add(otherEvent.getMz());
            }
        spectrumPanel.setOtherEventScans(otherEventScans);
        spectrumPanel.setOtherEventMZs(otherEventMzs);
        spectrumPanel.setName("Spectrum");
        spectrumPanel.setContourPlotRotationAngle(rotationAngle3D);
        spectrumPanel.setContourPlotTiltAngle(tiltAngle3D);
        spectrumPanel.setContourPlotWidth(imageWidth3D);
        spectrumPanel.setContourPlotHeight(imageHeight3D);
        spectrumPanel.setContourPlotShowAxes(show3DAxes);
        spectrumPanel.setSize(new Dimension(imageWidth, spectrumImageHeight));

        spectrumPanel.setPeakSeparationMass(peakSeparationMass);
        spectrumPanel.setPeakTolerancePPM(peakTolerancePPM);

        spectrumPanel.generateCharts();
        spectrumPanel.setVisible(true);
        spectrumPanel.setMinimumSize(new Dimension(imageWidth, spectrumImageHeight));

        return spectrumPanel;
    }

    /**
     * Side effect!  Sets QuantEvent.ratioOnePeak, but only if charts are created
     * @param run
     * @param outputDir
     * @param protein
     * @param fraction
     * @param quantEvent
     */
    protected void handleEvent(MSRun run, File outputDir, String protein, String fraction, QuantEvent quantEvent) {
        if (markAllEventsBad) {
            quantEvent.setQuantCurationStatus(QuantEvent.CURATION_STATUS_BAD);
        }
        if (shouldAssessEvents) {
            //only assess if not already assessed
            if (quantEvent.getAlgorithmicAssessment() == null) {
                QuantEventAssessor eventAssessor = new QuantEventAssessor();
                //                eventAssessor.setLabelType(labelType);
                eventAssessor.assessQuantEvent(quantEvent, run);
                //System.err.println("*******ASSESSED!!!!! " + quantEvent.getAlgorithmicAssessment());
            }
            //else System.err.println("NOT ASSESSING!");            
        }
        if (shouldCreateCharts || outTurkPW != null) {
            String filePrefix = quantEvent.getPeptide() + "_" + fraction + "_" + quantEvent.getCharge() + "_"
                    + quantEvent.getScan();

            //chart output files
            if (quantEvent.getSpectrumFile() == null)
                quantEvent.setSpectrumFile(new File(outputDir, filePrefix + "_spectrum.png"));
            if (quantEvent.getScansFile() == null)
                quantEvent.setScansFile(new File(outputDir, filePrefix + "_scans.png"));
            if (quantEvent.getIntensitySumFile() == null)
                quantEvent.setIntensitySumFile(new File(outputDir, filePrefix + "_intensitysum.png"));
            if (quantEvent.getFile3D() == null)
                quantEvent.setFile3D(new File(outputDir, filePrefix + "_3D.png"));

            PanelWithSpectrumChart spectrumPanel = createPanelWithSpectrumChart(run, quantEvent);
            //The single-peak ratio calculated by QuantEventAssessor is better than the slapdash one I calculate
            //in spectrumPanel, so if we've got that, use it.
            if (quantEvent.getAlgorithmicAssessment() != null)
                quantEvent.setRatioOnePeak(quantEvent.getAlgorithmicAssessment().getSinglePeakRatio());
            else
                quantEvent.setRatioOnePeak(spectrumPanel.getRatioOnePeak());

            Map<Integer, PanelWithLineChart> scanChartMap = spectrumPanel.getScanLineChartMap();
            List<Integer> allScans = new ArrayList<Integer>(scanChartMap.keySet());
            Collections.sort(allScans);

            if (outTurkPW != null) {
                try {

                    outTurkPW.println(saveTurkImage(quantEvent, spectrumPanel, outDir, currentTurkID));
                    outTurkPW.flush();
                    currentTurkID++;
                } catch (Exception e) {
                    throw new RuntimeException("Failed to save image file", e);
                }
            }

            if (shouldCreateCharts) {
                //Save the chart images, with sidebar data in case we need it.  Clunky.
                File currentImageFile = new File("");
                try {
                    currentImageFile = quantEvent.getSpectrumFile();
                    saveChartToImageFile(quantEvent, spectrumPanel, currentImageFile, writeInfoOnCharts, 0, 0,
                            false);
                    ApplicationContext.infoMessage(
                            "Wrote spectrum to image " + quantEvent.getSpectrumFile().getAbsolutePath());
                    currentImageFile = quantEvent.getIntensitySumFile();
                    saveChartToImageFile(quantEvent, spectrumPanel.getIntensitySumChart(), currentImageFile,
                            writeInfoOnCharts, 0, 0, false);
                    ApplicationContext.infoMessage(
                            "Wrote intensity sum to image " + quantEvent.getIntensitySumFile().getAbsolutePath());
                    currentImageFile = quantEvent.getScansFile();
                    spectrumPanel.savePerScanSpectraImage(imageWidth, scanImageHeight, maxScansImageHeight,
                            currentImageFile);
                    ApplicationContext
                            .infoMessage("Wrote scans to image " + quantEvent.getScansFile().getAbsolutePath());
                } catch (Exception e) {
                    throw new RuntimeException("Failed to save image file " + currentImageFile.getAbsolutePath(),
                            e);
                }
            }

            if (show3DPlots) {
                try {
                    saveChartToImageFile(quantEvent, spectrumPanel.getContourPlot(), quantEvent.getFile3D(),
                            writeInfoOnCharts, 0, 0, false);
                    ApplicationContext
                            .infoMessage("Wrote 3D plot to image " + quantEvent.getFile3D().getAbsolutePath());
                } catch (Exception e) {
                    throw new RuntimeException("Failed to save 3D image file", e);
                }
            }

            //record event
            Map<String, Map<String, Map<Integer, List<Pair<File, File>>>>> peptideFractionChargeFilesMap = proteinPeptideFractionChargeFilesMap
                    .get(protein);
            if (peptideFractionChargeFilesMap == null) {
                peptideFractionChargeFilesMap = new HashMap<String, Map<String, Map<Integer, List<Pair<File, File>>>>>();
                proteinPeptideFractionChargeFilesMap.put(protein, peptideFractionChargeFilesMap);
            }
            Map<String, Map<Integer, List<Pair<File, File>>>> fractionChargeFilesMap = peptideFractionChargeFilesMap
                    .get(quantEvent.getPeptide());
            if (fractionChargeFilesMap == null) {
                fractionChargeFilesMap = new HashMap<String, Map<Integer, List<Pair<File, File>>>>();
                peptideFractionChargeFilesMap.put(quantEvent.getPeptide(), fractionChargeFilesMap);
            }
            Map<Integer, List<Pair<File, File>>> chargeFilesMap = fractionChargeFilesMap.get(fraction);
            if (chargeFilesMap == null) {
                chargeFilesMap = new HashMap<Integer, List<Pair<File, File>>>();
                fractionChargeFilesMap.put(fraction, chargeFilesMap);
            }
            List<Pair<File, File>> filesList = chargeFilesMap.get(quantEvent.getCharge());
            if (filesList == null) {
                filesList = new ArrayList<Pair<File, File>>();
                chargeFilesMap.put(quantEvent.getCharge(), filesList);
            }
            filesList.add(new Pair<File, File>(quantEvent.getSpectrumFile(), quantEvent.getScansFile()));

            //Now is a VERY good time to do GC
            System.gc();
        } //end if shouldCreateCharts

        String outChartsRelativeDirName = "";
        if (proteinsToExamine != null)
            outChartsRelativeDirName = protein + File.separatorChar;
        if (writeHTMLAndText) {
            outHtmlPW.println(
                    quantEvent.createOutputRowHtml(outChartsRelativeDirName, showProteinColumn, show3DPlots));
            outHtmlPW.flush();
            outTsvPW.println(quantEvent.createOutputRowTsv(showProteinColumn, show3DPlots));
            outTsvPW.flush();
        }

    }

    /**
     * Save the intensity-sum image to a file, appropriately sized for the Mechanical Turk.
     * Return a String that's appropriate to be one line of a turk HIT file
     * @param quantEvent
     * @param spectrumPanel
     * @param outDir
     * @param turkId
     * @return
     * @throws IOException
     */
    public String saveTurkImage(QuantEvent quantEvent, PanelWithSpectrumChart spectrumPanel, File outDir,
            int turkId) throws IOException {
        String imageFileName = TurkUtilities.createTurkImageFileName(turkId);
        saveChartToImageFile(quantEvent, spectrumPanel.getIntensitySumChart(), new File(outDir, imageFileName),
                true, turkChartWidth, turkChartHeight, true);
        return TurkUtilities.createTurkHITFileLine(quantEvent, turkId, turkImageURLPrefix);
    }

    public static PanelWithPeakChart buildTheoreticalPeakChart(QuantEvent quantEvent, int chartWidth,
            int chartHeight) {
        return buildTheoreticalPeakChart(quantEvent.getLightMz(), quantEvent.getHeavyMz(), quantEvent.getCharge(),
                quantEvent.getRatio(), chartWidth, chartHeight);
    }

    /**
     * Build a peaks chart showing the theoretical distribution, assuming the ratio is correct
     * @param lightMz
     * @param heavyMz
     * @param charge
     * @param ratio
     * @param chartWidth
     * @param chartHeight
     * @return
     */
    public static PanelWithPeakChart buildTheoreticalPeakChart(float lightMz, float heavyMz, int charge,
            float ratio, int chartWidth, int chartHeight) {
        float lightNeutralMass = (lightMz - Spectrum.HYDROGEN_ION_MASS) * charge;
        //Hardcoded 6 is number of peakd returned by Poisson()
        //Can't just use the result of Poisson(), as that's a static array and we're gonna mess with it
        float[] lightTheoreticalPeaks = new float[6];
        System.arraycopy(Spectrum.Poisson(lightNeutralMass), 0, lightTheoreticalPeaks, 0,
                lightTheoreticalPeaks.length);
        //System.err.println("***LIGHT, mz=" + lightMz + ", mass=" + lightNeutralMass + ", charge=" + charge);
        //for (float peak : lightTheoreticalPeaks)
        //    System.err.println("\t" + peak);
        float[] lightPeakMzs = new float[6];
        for (int i = 0; i < 6; i++)
            lightPeakMzs[i] = lightMz + (Spectrum.HYDROGEN_ION_MASS * i / charge);
        PanelWithPeakChart theoreticalPeaksChart = new PanelWithPeakChart(lightPeakMzs, lightTheoreticalPeaks,
                "Theoretical Peaks");

        float heavyNeutralMass = (heavyMz - Spectrum.HYDROGEN_ION_MASS) * charge;

        float[] heavyTheoreticalPeaks = new float[6];
        System.arraycopy(Spectrum.Poisson(heavyNeutralMass), 0, heavyTheoreticalPeaks, 0,
                heavyTheoreticalPeaks.length);
        //fix 0 ratios at 0.001 to avoid divide by 0
        for (int i = 0; i < heavyTheoreticalPeaks.length; i++)
            heavyTheoreticalPeaks[i] *= 1 / Math.max(ratio, 0.001f);

        float[] heavyPeakMzs = new float[6];
        for (int i = 0; i < 6; i++)
            heavyPeakMzs[i] = heavyMz + (Spectrum.HYDROGEN_ION_MASS * i / charge);
        //Adjust heavy peaks if light peaks intrude.  Light appears in front of heavy
        for (int i = 0; i < heavyPeakMzs.length; i++) {
            for (int j = 0; j < lightPeakMzs.length; j++)
                if (heavyPeakMzs[i] - lightPeakMzs[j] < 0.1)
                    heavyTheoreticalPeaks[i] += lightTheoreticalPeaks[j];
        }
        //System.err.println("***HEAVY, mz=" + heavyMz + ", mass=" + heavyNeutralMass);
        //for (float peak : heavyTheoreticalPeaks)
        //    System.err.println("\t" + peak);
        theoreticalPeaksChart.addData(heavyPeakMzs, heavyTheoreticalPeaks, "heavy");

        theoreticalPeaksChart.setPreferredSize(new Dimension(chartWidth, chartHeight));
        theoreticalPeaksChart.setSize(new Dimension(chartWidth, chartHeight));
        theoreticalPeaksChart.getChart().removeLegend();

        //remove axes from chart
        ((XYPlot) theoreticalPeaksChart.getPlot()).getDomainAxis().setVisible(false);
        ((XYPlot) theoreticalPeaksChart.getPlot()).getRangeAxis().setVisible(false);

        return theoreticalPeaksChart;
    }

    /**
     * Cover method, harvests everything from quantEvent.  Still silly, because quantEvent is also passed
     * @param quantEvent
     * @param chartPanel
     * @throws IOException
     */
    public void saveChartToImageFile(QuantEvent quantEvent, PanelWithChart chartPanel, File outputFile,
            boolean writeInfo, int width, int height, boolean overrideSize) throws IOException {
        saveChartToImageFile(chartPanel, outputFile, sidebarWidth, quantEvent.getCharge(), quantEvent.getLightMz(),
                quantEvent.getHeavyMz(), quantEvent.getRatio(), writeInfo, width, height, overrideSize);
    }

    /**
     * Save chart to an image file, with or without the sidebar information and/or theoretical peaks
     * @param chartPanel
     * @param outFile
     * @param sidebarWidth
     * @param charge
     * @param lightMz
     * @param heavyMz
     * @param ratio
     * @throws IOException
     */
    public void saveChartToImageFile(PanelWithChart chartPanel, File outFile, int sidebarWidth, int charge,
            float lightMz, float heavyMz, float ratio, boolean writeChartInfo, int width, int height,
            boolean overrideSize) throws IOException {
        BufferedImage spectrumImage = null;
        if (overrideSize)
            spectrumImage = chartPanel.createImage(width, height);
        else
            spectrumImage = chartPanel.createImage();
        BufferedImage imageToWrite = spectrumImage;

        if (writeChartInfo) {
            int fullImageWidth = spectrumImage.getWidth() + sidebarWidth;

            imageToWrite = new BufferedImage(fullImageWidth, spectrumImage.getHeight(), BufferedImage.TYPE_INT_RGB);

            Graphics2D g = imageToWrite.createGraphics();
            g.drawImage(spectrumImage, sidebarWidth, 0, null);

            //write in sidebar
            int lineHeight = 20;
            int lineNum = 1;
            int indent = 5;

            if (writeChartInfo) {
                g.setPaint(Color.WHITE);
                //                g.drawString(peptide, indent, lineNum++ * lineHeight);
                //                g.drawString("Charge=" + charge, indent, lineNum++ * lineHeight);
                //                g.drawString("Light mass=" + lightMass, indent, lineNum++ * lineHeight);
                //                g.drawString("Light m/z=" + lightMz, indent, lineNum++ * lineHeight);
                //                g.drawString("Heavy m/z=" + heavyMz, indent, lineNum++ * lineHeight);
                //                g.drawString("Light int=" + lightIntensity, indent, lineNum++ * lineHeight);
                //                g.drawString("Heavy int=" + heavyIntensity, indent, lineNum++ * lineHeight);
                g.drawString("Ratio=" + ratio, indent, lineNum++ * lineHeight);

                //                g.drawString("MinscanLt=" + lightMinQuantScan, indent, lineNum++ * lineHeight);
                //                g.drawString("MaxscanLt=" + lightMaxQuantScan, indent, lineNum++ * lineHeight);
                //                g.drawString("MinScanHv=" + heavyMinQuantScan, indent, lineNum++ * lineHeight);
                //                g.drawString("MaxScanHv=" + heavyMaxQuantScan, indent, lineNum++ * lineHeight);
                //                g.drawString("ID scan=" + idScan, indent, lineNum++ * lineHeight);
                //                g.drawString("IDscan level=" + idScanLevel, indent, lineNum++ * lineHeight);

                //theoretical peaks in bottom left
                int theoreticalPeaksHeight = (int) (sidebarWidth * 2.0 / 3.0);
                int theoreticalPeaksTop = spectrumImage.getHeight() - theoreticalPeaksHeight - 10;
                g.drawString("Ideal Peaks", indent, theoreticalPeaksTop - 20);
                //combined theoretical peak distribution chart
                PanelWithPeakChart theoreticalPeakChart = buildTheoreticalPeakChart(lightMz, heavyMz, charge, ratio,
                        sidebarWidth, (int) (sidebarWidth * 2.0 / 3.0));
                g.drawImage(theoreticalPeakChart.createImage(sidebarWidth, (int) (sidebarWidth * 2.0 / 3.0)), 0,
                        theoreticalPeaksTop, null);
            }
            g.dispose();
        }
        ImageIO.write(imageToWrite, "png", outFile);
    }

    public int getResolution() {
        return resolution;
    }

    public void setResolution(int resolution) {
        this.resolution = resolution;
    }

    public int getNumHeavyPeaksToPlot() {
        return numHeavyPeaksToPlot;
    }

    public void setNumHeavyPeaksToPlot(int numHeavyPeaksToPlot) {
        this.numHeavyPeaksToPlot = numHeavyPeaksToPlot;
    }

    public File getOutDir() {
        return outDir;
    }

    public void setOutDir(File outDir) {
        this.outDir = outDir;
    }

    public int getScanImageHeight() {
        return scanImageHeight;
    }

    public void setScanImageHeight(int scanImageHeight) {
        this.scanImageHeight = scanImageHeight;
    }

    public int getMaxScansImageHeight() {
        return maxScansImageHeight;
    }

    public void setMaxScansImageHeight(int maxScansImageHeight) {
        this.maxScansImageHeight = maxScansImageHeight;
    }

    public int getSpectrumImageHeight() {
        return spectrumImageHeight;
    }

    public void setSpectrumImageHeight(int spectrumImageHeight) {
        this.spectrumImageHeight = spectrumImageHeight;
    }

    public int getImageWidth() {
        return imageWidth;
    }

    public void setImageWidth(int imageWidth) {
        this.imageWidth = imageWidth;
    }

    public File getMzXmlDir() {
        return mzXmlDir;
    }

    public void setMzXmlDir(File mzXmlDir) {
        this.mzXmlDir = mzXmlDir;
    }

    public float getMaxCombineFeatureRatioDiff() {
        return maxCombineFeatureRatioDiff;
    }

    public void setMaxCombineFeatureRatioDiff(float maxCombineFeatureRatioDiff) {
        this.maxCombineFeatureRatioDiff = maxCombineFeatureRatioDiff;
    }

    public boolean isWriteHTMLAndText() {
        return writeHTMLAndText;
    }

    public void setWriteHTMLAndText(boolean writeHTMLAndText) {
        this.writeHTMLAndText = writeHTMLAndText;
    }

    public float getMinPeptideProphet() {
        return minPeptideProphet;
    }

    public void setMinPeptideProphet(float minPeptideProphet) {
        this.minPeptideProphet = minPeptideProphet;
    }

    public File getMzXmlFile() {
        return mzXmlFile;
    }

    public void setMzXmlFile(File mzXmlFile) {
        this.mzXmlFile = mzXmlFile;
    }

    public int getNumPaddingScans() {
        return numPaddingScans;
    }

    public void setNumPaddingScans(int numPaddingScans) {
        this.numPaddingScans = numPaddingScans;
    }

    public float getMzPadding() {
        return mzPadding;
    }

    public void setMzPadding(float mzPadding) {
        this.mzPadding = mzPadding;
    }

    public Set<String> getPeptidesFound() {
        return peptidesFound;
    }

    public void setPeptidesFound(Set<String> peptidesFound) {
        this.peptidesFound = peptidesFound;
    }

    public int getSidebarWidth() {
        return sidebarWidth;
    }

    public void setSidebarWidth(int sidebarWidth) {
        this.sidebarWidth = sidebarWidth;
    }

    public Map<String, Map<String, Map<String, Map<Integer, List<Pair<File, File>>>>>> getProteinPeptideFractionChargeFilesMap() {
        return proteinPeptideFractionChargeFilesMap;
    }

    public void setProteinPeptideFractionChargeFilesMap(
            Map<String, Map<String, Map<String, Map<Integer, List<Pair<File, File>>>>>> proteinPeptideFractionChargeFilesMap) {
        this.proteinPeptideFractionChargeFilesMap = proteinPeptideFractionChargeFilesMap;
    }

    public PrintWriter getOutHtmlPW() {
        return outHtmlPW;
    }

    public void setOutHtmlPW(PrintWriter outHtmlPW) {
        this.outHtmlPW = outHtmlPW;
    }

    public PrintWriter getOutTsvPW() {
        return outTsvPW;
    }

    public void setOutTsvPW(PrintWriter outTsvPW) {
        this.outTsvPW = outTsvPW;
    }

    public boolean isShow3DPlots() {
        return show3DPlots;
    }

    public void setShow3DPlots(boolean show3DPlots) {
        this.show3DPlots = show3DPlots;
    }

    public int getRotationAngle3D() {
        return rotationAngle3D;
    }

    public void setRotationAngle3D(int rotationAngle3D) {
        this.rotationAngle3D = rotationAngle3D;
    }

    public int getTiltAngle3D() {
        return tiltAngle3D;
    }

    public void setTiltAngle3D(int tiltAngle3D) {
        this.tiltAngle3D = tiltAngle3D;
    }

    public int getImageHeight3D() {
        return imageHeight3D;
    }

    public void setImageHeight3D(int imageHeight3D) {
        this.imageHeight3D = imageHeight3D;
    }

    public int getImageWidth3D() {
        return imageWidth3D;
    }

    public void setImageWidth3D(int imageWidth3D) {
        this.imageWidth3D = imageWidth3D;
    }

    public boolean isShow3DAxes() {
        return show3DAxes;
    }

    public void setShow3DAxes(boolean show3DAxes) {
        this.show3DAxes = show3DAxes;
    }

    public int getScan() {
        return scan;
    }

    public void setScan(int scan) {
        this.scan = scan;
    }

    public Set<String> getPeptidesToExamine() {
        return peptidesToExamine;
    }

    public void setPeptidesToExamine(Set<String> peptidesToExamine) {
        this.peptidesToExamine = peptidesToExamine;
    }

    public Set<String> getProteinsToExamine() {
        return proteinsToExamine;
    }

    public void setProteinsToExamine(Set<String> proteinsToExamine) {
        this.proteinsToExamine = proteinsToExamine;
    }

    public Set<String> getFractionsToExamine() {
        return fractionsToExamine;
    }

    public void setFractionsToExamine(Set<String> fractionsToExamine) {
        this.fractionsToExamine = fractionsToExamine;
    }

    public Iterator<FeatureSet> getFeatureSetIterator() {
        return featureSetIterator;
    }

    public void setFeatureSetIterator(Iterator<FeatureSet> featureSetIterator) {
        this.featureSetIterator = featureSetIterator;
    }

    public boolean isWriteInfoOnCharts() {
        return writeInfoOnCharts;
    }

    public void setWriteInfoOnCharts(boolean writeInfoOnCharts) {
        this.writeInfoOnCharts = writeInfoOnCharts;
    }

    public boolean isWriteTheoreticalPeaksOnCharts() {
        return writeTheoreticalPeaksOnCharts;
    }

    public void setWriteTheoreticalPeaksOnCharts(boolean writeTheoreticalPeaksOnCharts) {
        this.writeTheoreticalPeaksOnCharts = writeTheoreticalPeaksOnCharts;
    }

    public File getOutHtmlFile() {
        return outHtmlFile;
    }

    public void setOutHtmlFile(File outHtmlFile) {
        this.outHtmlFile = outHtmlFile;
    }

    public File getOutTsvFile() {
        return outTsvFile;
    }

    public File getOutTurkFile() {
        return outTurkFile;
    }

    public void setOutTurkFile(File outTurkFile) {
        this.outTurkFile = outTurkFile;
    }

    public void setOutTsvFile(File outTsvFile) {
        this.outTsvFile = outTsvFile;
    }

    public float getPeakSeparationMass() {
        return peakSeparationMass;
    }

    public void setPeakSeparationMass(float peakSeparationMass) {
        this.peakSeparationMass = peakSeparationMass;
    }

    public float getPeakTolerancePPM() {
        return peakTolerancePPM;
    }

    public void setPeakTolerancePPM(float peakTolerancePPM) {
        this.peakTolerancePPM = peakTolerancePPM;
    }

    public boolean isAppendTsvOutput() {
        return appendTsvOutput;
    }

    public void setAppendTsvOutput(boolean appendTsvOutput) {
        this.appendTsvOutput = appendTsvOutput;
    }

    public void addProgressListener(ActionListener listener) {
        dummyProgressButton.addActionListener(listener);
    }

    public boolean isShouldCreateCharts() {
        return shouldCreateCharts;
    }

    public void setShouldCreateCharts(boolean shouldCreateCharts) {
        this.shouldCreateCharts = shouldCreateCharts;
    }

    public boolean isMarkAllEventsBad() {
        return markAllEventsBad;
    }

    public void setMarkAllEventsBad(boolean markAllEventsBad) {
        this.markAllEventsBad = markAllEventsBad;
    }

    public String getTurkImageURLPrefix() {
        return turkImageURLPrefix;
    }

    public void setTurkImageURLPrefix(String turkImageURLPrefix) {
        this.turkImageURLPrefix = turkImageURLPrefix;
    }
}