gephi.spade.panel.fcsFile.java Source code

Java tutorial

Introduction

Here is the source code for gephi.spade.panel.fcsFile.java

Source

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package gephi.spade.panel;
/**
 * fcsFile.java
 * ---
 * <p>Contains the code to read fcs files.</p>
 *
 * <p>Based on the code by Jonathan Irish.</p>
 *
 *
 * Cytobank (TM) is server and client software for web-based management, analysis,
 * and sharing of flow cytometry data.
 *
 * Copyright (C) 2009 Cytobank, Inc.  All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Cytobank, Inc.
 * 659 Oak Grove Avenue #205
 * Menlo Park, CA 94025
 *
 * http://www.cytobank.org
 */

import java.io.*;
import java.util.*;

// Use the new I/O for speed
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import org.apache.commons.math.linear.Array2DRowRealMatrix;
import org.apache.commons.math.linear.BlockRealMatrix;
import org.apache.commons.math.linear.LUDecompositionImpl;
import org.apache.commons.math.linear.RealMatrix;

/**
 * fcsFile ---
 * <p>
 * A class to read FCS files based on fastFacsClasses.cs.
 * </p>
 *
 * <p>
 * The class is final and does not implement the
 * <code>java.io.Serializable</code> interface. This is because it should not be
 * subclassed and is highly file-dependent, respectively.
 * </p>
 */
public final class fcsFile {
    // The ENCODING to use for decoding the text data in the file
    // ISO-8859-1 is the standard extension of ASCII

    private static final String ENCODING = "ISO-8859-1";
    private static final Charset charset = Charset.forName(ENCODING);
    // Size of the version string in bytes
    private static final int VERSION_SIZE = 6;
    // Default prefix for FCS files to check whether the file is a FCS file.
    private static final String FCS_PREFIX = "FCS";
    // Default behavior for whether to extract events
    private static final boolean EXTRACTP = false;
    /**
     * Decoder for parsing the text portions
     */
    private CharsetDecoder decoder;
    /**
     * The underlying file
     */
    private File file;
    /**
     * Boolean flag of whether the file is an FCS file.
     */
    private boolean isFCSP;
    /**
     * File Information
     */
    public String version = null; // Version string
    public int textStart = 0;
    public int textEnd = 0;
    public int dataStart = 0;
    public int dataEnd = 0;
    public int analysisStart = 0;
    public int analysisEnd = 0;
    public int supplementalStart = 0;
    public int supplementalEnd = 0;
    public char delimiter = '\\'; // TEXT segment delimiter character
    public String text = null; // The entire TEXT segment
    /**
     * settings ---
     * <p>
     * <code>java.util.Properties</code> object settings contains all the
     * key/value pairs in the TEXT segment.
     * </p>
     *
     * <p>
     * This is a good way to handle all the pairs for hardcore Java people.
     * </p>
     */
    private Properties settings = null;
    /**
     * All the public fields ---
     * <p>
     * These should be self-explanatory.
     * </p>
     *
     * <p>
     * They are initialized since they may never be set if the file is not an
     * FCS file.
     * </p>
     *
     * <p>
     * At some point, we probably want to wean people away from these and use
     * the Properties settings object instead.
     * </p>
     */
    public boolean littleEndianP;
    public int parameters = 0;
    public String sampleName = null;
    public String dataType = null;
    public String cytometer = null;
    public String mode = null;
    public String instrument = null;
    public String expTime = null;
    public String expFile = null;
    public String operatorName = null;
    public String operatingSystem = null;
    public String creatorSoftware = null;
    public String cytometerNumber = null;
    public String experimentDate = null;
    public String experimentName = null;
    public String exportTime = null;
    public String exportUser = null;
    public String GUID = null;
    public String windowExtension = null;
    public String threshold = null;
    public String tubeName = null;
    public String wellId = null;
    public String plateId = null;
    public String plateName = null;
    public String comment = null;
    public String spillString = null;
    public boolean applyCompensation = false;
    public String source = null;
    public String nextData = null;
    public String endsText = null;
    public String bTime = null;
    public double timeStep = 0;
    public boolean[] isLog = null;
    public int lasers = 0;
    public String[] laserASF = null;
    public String[] laserName = null;
    public String[] laserDelay = null;
    public String[] channelName = null;
    public String[] channelShortname = null;
    public String[] channelGain = null;
    public int[] channelBits = null;
    public String[] channelAmp = null;
    public double[] channelRange = null;
    public String[] channelVoltage = null;
    public boolean[] displayLog = null;
    public double[] ampValue = null;
    public int totalEvents = 0;
    protected double[][] eventList = null;

    /**
     * Constructor ---
     * <p>
     * Given the path to a file, the class grabs all the information about the
     * file. Whether events are extracted is determined by the default value in
     * EXTRACTP.
     * </p>
     *
     * <p>
     * Throws <code>FileNotFoundException</code> and <code>IOException</code>.
     * This way, whatever code is calling the class can handle the exception
     * properly.
     * </p>
     *
     * @param path
     *            path to the underlying file.
     * @throws <code>java.io.FileNotFoundException</code> if the file is not
     *         found.
     * @throws <code>java.io.IOException</code> if an IO exception occurred.
     */
    public fcsFile(String path) throws FileNotFoundException, IOException {
        this(path, EXTRACTP);
    }

    /**
     * Constructor ---
     * <p>
     * Given the path to a file, the class grabs all the information about the
     * file. The flag extractEventsP controls whether to extract the data from
     * the file.
     * </p>
     *
     * <p>
     * Set extractEventsP to false to improve speed.
     * </p>
     *
     * <p>
     * Throws <code>FileNotFoundException</code> and <code>IOException</code>.
     * This way, whatever code is calling the class can handle the exception
     * properly.
     * </p>
     *
     * @param path
     *            path to the underlying file.
     * @param extractEventsP
     *            boolean flag for whether to extract events in the underlying
     *            file.
     * @throws <code>java.io.FileNotFoundException</code> if the file is not
     *         found.
     * @throws <code>java.io.IOException</code> if an IO exception occurred.
     */
    public fcsFile(String path, boolean extractEventsP) throws FileNotFoundException, IOException {
        // Create a file using the path and use the other constructor that takes
        // a File.
        this(new File(path), extractEventsP);
    }

    /**
     * Constructor ---
     * <p>
     * Given a File f, the class grabs all the information about the file.
     * Whether events are extracted is determined by the default value in
     * EXTRACTP.
     * </p>
     *
     * <p>
     * Throws <code>FileNotFoundException</code> and <code>IOException</code>.
     * This way, whatever code is calling the class can handle the exception
     * properly.
     * </p>
     *
     * @param file
     *            <code>File</code> object pointing to the underlying file.
     * @throws <code>java.io.FileNotFoundException</code> if the file is not
     *         found.
     * @throws <code>java.io.IOException</code> if an IO exception occurred.
     */
    public fcsFile(File file) throws FileNotFoundException, IOException {
        this(file, EXTRACTP);
    }

    /**
     * Constructor ---
     * <p>
     * Given a File f, the class grabs all the information about the file. The
     * flag extractEventsP controls whether to extract the data from the file.
     * </p>
     *
     * <p>
     * Set extractEventsP to false to improve speed.
     * </p>
     *
     * <p>
     * Throws <code>FileNotFoundException</code> and <code>IOException</code>.
     * This way, whatever code is calling the class can handle the exception
     * properly.
     * </p>
     *
     * @param file
     *            <code>File</code> object pointing to the underlying file.
     * @param extractEventsP
     *            boolean flag for whether to extract events in the underlying
     *            file.
     * @throws <code>java.io.FileNotFoundException</code> if the file is not
     *         found.
     * @throws <code>java.io.IOException</code> if an IO exception occurred.
     */
    public fcsFile(File file, boolean extractEventsP) throws FileNotFoundException, IOException {
        this.file = file;

        // Set isFCSP to false - start by assuming the file is not an FCS file
        isFCSP = false;

        // Read the file and initialize all the fields
        readFile(extractEventsP);
    }

    /**
     * readFile ---
     * <p>
     * A helper function to read all the fields in the TEXT segment of the FCS
     * file.
     * </p>
     *
     * <p>
     * This helper function should only be called once by the constructor as it
     * is quite expensive.
     * </p>
     *
     * @param extractEventsP
     *            boolean flag indicating whether to extract events in the
     *            underlying file.
     * @throws <code>java.io.FileNotFoundException</code> if the file is not
     *         found.
     * @throws <code>java.io.IOException</code> if an IO exception occurred.
     */
    private void readFile(boolean extractEventsP) throws FileNotFoundException, IOException {
        // Open a file input stream to the file
        FileInputStream fis = new FileInputStream(file);

        // Create a byte array to hold the version
        byte[] versionArray = new byte[VERSION_SIZE];

        // Read the version into the byte array
        int numRead = fis.read(versionArray);

        if (numRead < VERSION_SIZE) {
            // If the number of bytes read is less than the number of bytes in
            // the version string, then the file is too small to be an FCS file.
            isFCSP = false;

            // Close the file input stream
            fis.close();

            // Quit
            return;
        }

        // Decode the version using the default encoding
        version = new String(versionArray);

        // Determine whether the file is an FCS file by whether the version
        // string starts with the FCS_PREFIX
        isFCSP = version.startsWith(FCS_PREFIX);

        if (!isFCSP) {
            // If the file is not an FCS file, then close the file and quit.
            // Close the file input stream
            fis.close();

            // Quit
            return;
        }

        /**
         * At this point, we are pretty sure that the file is an FCS file. So,
         * we parse it.
         */
        /**
         * Get the standard HEADER stuff
         */
        // Skip 4 bytes to get to byte 10
        fis.skip(4);

        // Create a byte array to hold the HEADER
        byte[] headerArray = new byte[48];

        // Read the header into the byte array
        numRead = fis.read(headerArray);

        if (numRead < 48) {
            // If the number of bytes read is less than 48, then the file is too
            // small to be an FCS file.
            isFCSP = false;

            // Close the file input stream
            fis.close();

            // Quit
            return;
        }

        try {
            // Try to parse the TEXT segment start and end and DATA segment
            // start and end
            textStart = Integer.parseInt((new String(headerArray, 0, 8)).trim());
            textEnd = Integer.parseInt((new String(headerArray, 8, 8)).trim());
            dataStart = Integer.parseInt((new String(headerArray, 16, 8)).trim());
            dataEnd = Integer.parseInt((new String(headerArray, 24, 8)).trim());
        } catch (NumberFormatException nfe) {
            // If a NumberFormatException occured, then quit because there's
            // nothing we can do without the TEXT or DATA segment.
            // Close the file input stream
            fis.close();

            return;
        }

        /**
         * Get the ANALYSIS segment limits
         */
        try {
            // Try to parse the analysisStart and analysisEnd
            analysisStart = Integer.parseInt((new String(headerArray, 32, 8)).trim());
            analysisEnd = Integer.parseInt((new String(headerArray, 40, 8)).trim());
        } catch (NumberFormatException nfe) {
            // If a NumberFormatException occured, then set the ANALYSIS start
            // and end to 0 since this segment is optional.
            analysisStart = 0;
            analysisEnd = 0;
        }

        /**
         * Use NIO to read the OTHER and TEXT segments
         */
        // Get the channel for the input file
        FileChannel fc = fis.getChannel();

        // Move the channel's position back to 0
        fc.position(0);

        // Map the TEXT segment to memory
        MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, textEnd + 1);

        /**
         * Create the character decoder for parsing characters
         */
        decoder = charset.newDecoder();

        /**
         * Get the OTHER segment
         */
        mbb.limit(textStart);
        mbb.position(58);
        CharBuffer other = decoder.decode(mbb.slice());

        /**
         * Get the TEXT segment
         */
        mbb.limit(textEnd + 1);
        mbb.position(textStart);
        text = decoder.decode(mbb.slice()).toString();

        /**
         * Close the file since we have the string version of the TEXT segment
         */
        // Close the file channel
        fc.close();

        // Close the file input stream
        fis.close();

        /**
         * Decode the TEXT segment
         */
        // The first character of the primary TEXT segment contains the
        // delimiter character
        delimiter = text.charAt(0);

        /**
         * Key/Value Pairs
         */
        // Generate all the pairs
        String[] pairs;

        if (delimiter == '\\') {
            // If the delimiter character is a backslash, then we have to escape
            // it in the regular expression.
            pairs = text.split("[\\\\]");
        } else {
            // Otherwise, we can just split it normally by using the character
            // in the regular expression.
            pairs = text.split("[" + Character.toString(delimiter) + "]");
        }

        /**
         * Calculate the number of pairs --- The number of pairs is the length
         * of the pairs array minus 1 divided by 2. The one is due to the empty
         * first element from the Java split above.
         */
        int numPairs = (pairs.length - 1) / 2;

        // Create a mapping for each key and its value
        settings = new Properties();

        // Loop through the TEXT segment we just split to get the keys and
        // values
        // The key is in (i * 2) + 1 to account for the empty first element.
        // The value is in (i * 2) + 2 to account for the empty first element.
        for (int i = 0; i < numPairs; i++) {
            settings.setProperty(pairs[(i * 2) + 1].trim(), pairs[(i * 2) + 2].trim());
        }

        // Go through all the key/value pairs and parse them
        parseSettings();

        /**
         * Extract Events
         */
        if (extractEventsP) {
            // If we are extracting data, then do so.
            extractEvents();
        }
    }

    /**
     * parseSettings ---
     * <p>
     * Uses all the properties in Properties settings to initialize the fields
     * of fcsFile.
     * </p>
     *
     * <p>
     * I pulled it out since it involves a lot of constants and I don't want to
     * mix it with the file reading and parsing code.
     * </p>
     */
    private void parseSettings() {
        if (settings == null) {
            // If settings is null, then quit since there is nothing that can be
            // done here.
            return;
        }

        /**
         * At this point, we know settings is not null.
         */
        if (settings.isEmpty()) {
            // If settings is empty, then quit.
            return;
        }

        /**
         * At this point, we know settings has some mappings, so try to load
         * them.
         */
        if ((settings.getProperty("$BEGINSTEXT") != null) && (settings.getProperty("$ENDSTEXT") != null)) {
            // If the byte offset keywords for the supplemental TEXT segment are
            // not null, then parse the byte offsets.
            try {
                // Try to parse the supplemental start
                supplementalStart = Integer.parseInt(settings.getProperty("$BEGINSTEXT"));
            } catch (NumberFormatException nfe) {
                // If a NumberFormatException occurs, then set the supplemental
                // start to 0.
                supplementalStart = 0;
            }

            try {
                // Try to parse the supplemental end
                supplementalEnd = Integer.parseInt(settings.getProperty("$ENDSTEXT"));
            } catch (NumberFormatException nfe) {
                // If a NumberFormatException occurs, then set the supplemental
                // end to 0.
                supplementalEnd = 0;
            }
        }

        if ((dataStart == 0) && (dataEnd == 0)) {
            // If the begin and the end byte offsets for the DATA segment is 0,
            // then parse the byte offsets from the TEXT segment.
            try {
                // Try to parse the data start
                dataStart = Integer.parseInt(settings.getProperty("$BEGINDATA"));
            } catch (NumberFormatException nfe) {
                // If a NumberFormatException occurs, then set the data start to
                // 0.
                dataStart = 0;
            }

            try {
                // Try to parse the data end
                dataEnd = Integer.parseInt(settings.getProperty("$ENDDATA"));
            } catch (NumberFormatException nfe) {
                // If a NumberFormatException occurs, then set the data end to
                // 0.
                dataEnd = 0;
            }
        }

        if ((analysisStart == 0) && (analysisEnd == 0)) {
            // If the begin and the end byte offsets for the ANALYSIS segment is
            // 0, then parse the byte offsets from the TEXT segment.
            try {
                // Try to parse the analysis start
                analysisStart = Integer.parseInt(settings.getProperty("$BEGINANALYSIS"));
            } catch (NumberFormatException nfe) {
                // If a NumberFormatException occurs, then set the analysis
                // start to 0.
                analysisStart = 0;
            }

            try {
                // Try to parse the analysis end
                analysisEnd = Integer.parseInt(settings.getProperty("$ENDANALYSIS"));
            } catch (NumberFormatException nfe) {
                // If a NumberFormatException occurs, then set the analysis end
                // to 0.
                analysisEnd = 0;
            }
        }

        if (settings.getProperty("$PAR") != null) {
            // If "$PAR" has a mapped value, then try to parse it.
            try {
                // Try to parse the number of parameters
                parameters = Integer.parseInt(settings.getProperty("$PAR"));
            } catch (NumberFormatException nfe) {
                // If a NumberFormatException occurs, then set the number of
                // parameters to 0.
                parameters = 0;
            }
        }

        if (settings.getProperty("$TOT") != null) {
            // If "$TOT" has a mapped value, then try to parse it.
            try {
                // Try to parse the number of events
                totalEvents = Integer.parseInt(settings.getProperty("$TOT"));
            } catch (NumberFormatException nfe2) {
                // If a NumberFormatException occurs, then set the number of
                // events to 0.
                totalEvents = 0;
            }
        }

        // Get the data type
        dataType = settings.getProperty("$DATATYPE");

        // Get the sample name
        sampleName = settings.getProperty("SAMPLE ID");

        if (sampleName == null) {
            // If the sample name is null, then use the filename as the sample
            // name.
            sampleName = file.getName();
        }

        // Initialize whether the byte order is little endian to false
        littleEndianP = false;

        // Get the byte order
        String byteOrder = settings.getProperty("$BYTEORD");

        if ((byteOrder != null) && (byteOrder.length() > 0)) {
            // If the byte order is not null and not empty, then set whether the
            // byte order is little endian.
            littleEndianP = (byteOrder.equals("1,2,3,4") || byteOrder.equals("1,2"));
        }

        // Initialize all the parameter-based arrays
        channelName = new String[parameters];
        channelShortname = new String[parameters];
        channelGain = new String[parameters];
        channelBits = new int[parameters];
        channelAmp = new String[parameters];
        channelRange = new double[parameters];
        channelVoltage = new String[parameters];

        isLog = new boolean[parameters];
        ampValue = new double[parameters];
        displayLog = new boolean[parameters];

        cytometer = settings.getProperty("$CYT");
        mode = settings.getProperty("$MODE");
        instrument = settings.getProperty("$INST");
        expTime = settings.getProperty("$ETIM");
        expFile = settings.getProperty("$FIL");
        operatorName = settings.getProperty("$OP");
        operatingSystem = settings.getProperty("$SYS");
        experimentDate = settings.getProperty("$DATE");
        comment = settings.getProperty("$COM");

        creatorSoftware = settings.getProperty("CREATOR");
        cytometerNumber = settings.getProperty("CYTNUM");
        experimentName = settings.getProperty("EXPERIMENT NAME");
        exportTime = settings.getProperty("EXPORT TIME");
        exportUser = settings.getProperty("EXPORT USER NAME");
        GUID = settings.getProperty("GUID");
        windowExtension = settings.getProperty("WINDOW EXTENSION");
        threshold = settings.getProperty("THRESHOLD");
        spillString = settings.getProperty("SPILL");
        tubeName = settings.getProperty("TUBE NAME");
        wellId = settings.getProperty("WELL ID");
        plateId = settings.getProperty("PLATE ID");
        plateName = settings.getProperty("PLATE NAME");

        // Try to read the $TIMESTEP key, knowing it may be null, so default to
        // 0
        timeStep = 0;

        if (settings.getProperty("$TIMESTEP") != null) {
            // If "$TIMESTEP" has a mapped value, then try to parse it.
            try {
                // Try to parse the time step
                timeStep = Double.parseDouble(settings.getProperty("$TIMESTEP"));
            } catch (NumberFormatException nfe3) {
                // If a NumberFormatException occurs, then set the time step to
                // 0.
                timeStep = 0;
            }
        }

        source = settings.getProperty("$SRC");
        endsText = settings.getProperty("$ENDSTEXT");
        nextData = settings.getProperty("$NEXTDATA");
        bTime = settings.getProperty("$BTIM");

        // Try to read the APPLY COMPENSATION key, knowing it may be null, so
        // default to false
        applyCompensation = false;

        String applyString = settings.getProperty("APPLY COMPENSATION");
        if ((applyString != null) && applyString.equalsIgnoreCase("true")) {
            // If the value of "APPLY COMPENSATION" is "true", then set
            // applyCompensation to true.
            applyCompensation = true;
        }

        lasers = 0;

        // Count the number of lasers
        for (int i = 1; i <= settings.size(); i++) {
            if (settings.getProperty("LASER" + i + "NAME") != null) {
                // If "LASER#NAME" exists, then increment the number of lasers.
                lasers++;
            }
        }

        laserASF = new String[lasers];
        laserDelay = new String[lasers];
        laserName = new String[lasers];

        for (int i = 1; i <= lasers; i++) {
            laserASF[i - 1] = settings.getProperty("LASER" + i + "ASF");
            laserDelay[i - 1] = settings.getProperty("LASER" + i + "DELAY");
            laserName[i - 1] = settings.getProperty("LASER" + i + "NAME");
        }

        for (int i = 1; i <= parameters; i++) {
            channelShortname[i - 1] = settings.getProperty("$P" + i + "N");
            channelName[i - 1] = settings.getProperty("$P" + i + "S");

            if (channelName[i - 1] == null) {
                channelName[i - 1] = channelShortname[i - 1];
            }

            channelGain[i - 1] = settings.getProperty("$P" + i + "G");

            try {
                channelBits[i - 1] = Integer.parseInt(settings.getProperty("$P" + i + "B"));
            } catch (NumberFormatException nfe) {
                if (dataType != null) {
                    if (dataType.equalsIgnoreCase("I")) {
                        // If the data type is "I", then it is binary integer.
                        channelBits[i - 1] = 16;
                    } else if (dataType.equalsIgnoreCase("F")) {
                        // If the data type is "F", then it is floating point.
                        channelBits[i - 1] = 32;
                    } else if (dataType.equalsIgnoreCase("D")) {
                        // If the data type is "D", then it is double precision
                        // floating point
                        channelBits[i - 1] = 64;
                    } else if (dataType.equalsIgnoreCase("A")) {
                        // If the data type is "A", then it is ASCII.
                        channelBits[i - 1] = 8;
                    }
                } else {
                    // Otherwise, set the number of channel bits to 0.
                    channelBits[i - 1] = 0;
                }
            }

            channelAmp[i - 1] = settings.getProperty("$P" + i + "E");

            displayLog[i - 1] = false;

            String displayString = settings.getProperty("P" + i + "DISPLAY");
            if ((displayString != null) && displayString.equalsIgnoreCase("log")) {
                displayLog[i - 1] = true;
            }

            if (channelAmp[i - 1] != null) {
                String[] ampArray = channelAmp[i - 1].split(",");

                try {
                    // Try to parse the amp value
                    ampValue[i - 1] = Double.parseDouble(ampArray[0]);
                } catch (NumberFormatException nfe4) {
                    // If a NumberFormatException occurs, then set the amp value
                    // to 0.
                    ampValue[i - 1] = 0;
                }
            } else {
                // Otherwise, set the amp value to 0 in the event that the
                // channel amp is missing.
                ampValue[i - 1] = 0d;
            }

            if (ampValue[i - 1] > 0) {
                isLog[i - 1] = true;
            } else {
                // Otherwise, set is log to false.
                isLog[i - 1] = false;
            }

            try {
                channelRange[i - 1] = Double.parseDouble(settings.getProperty("$P" + i + "R"));
            } catch (NumberFormatException nfe5) {
                channelRange[i - 1] = 0;
            }
            channelVoltage[i - 1] = settings.getProperty("$P" + i + "V");

            if ((channelVoltage[i - 1] == null) && (cytometer != null) && cytometer.equals("FACSCalibur")) {
                switch (i - 1) {
                case 2:
                    channelVoltage[i - 1] = settings.getProperty("BD$WORD3");
                    break;
                case 3:
                    channelVoltage[i - 1] = settings.getProperty("BD$WORD5");
                    break;
                case 4:
                    channelVoltage[i - 1] = settings.getProperty("BD$WORD7");
                    break;
                case 5:
                    channelVoltage[i - 1] = settings.getProperty("BD$WORD9");
                    break;
                case 6:
                    channelVoltage[i - 1] = settings.getProperty("BD$WORD11");
                    break;
                case 7:
                    channelVoltage[i - 1] = settings.getProperty("BD$WORD55");
                    break;
                case 8:
                    channelVoltage[i - 1] = settings.getProperty("BD$WORD24");
                    break;
                default:
                    channelVoltage[i - 1] = "";
                    break;
                }
            }
        }

        /**
         * Calculate the number of events in the flow file based on the number
         * of bits in each event
         */
        // Initialize the number of bits in each event to 0
        int numBitsPerEvent = 0;

        // Loop through all the parameters adding the number of bits in each
        // parameter
        for (int i = 0; i < parameters; i++) {
            numBitsPerEvent += channelBits[i];
        }

        if (numBitsPerEvent > 0) {
            // If the number of bits in each event is greater than 0, then
            // calculate the number of events based on the size of the DATA
            // segment.
            int calculatedNumEvents = (dataEnd - dataStart + 1) * 8 / numBitsPerEvent;

            if (totalEvents > calculatedNumEvents) {
                // If the number of events is greater than the calculated number
                // of events, then update the number of events.
                totalEvents = calculatedNumEvents;
            }
        }
    }

    /**
     * extractEvents ---
     * <p>
     * Extracts the events from the FCS file using NIO.
     * </p>
     *
     * @throws <code>java.io.FileNotFoundException</code> if the file is not
     *         found.
     * @throws <code>java.io.IOException</code> if an IO exception occurred.
     */
    private void extractEvents() throws FileNotFoundException, IOException {
        if ((dataStart >= dataEnd) || (totalEvents <= 0)) {
            // If the byte offset of the start of the DATA segment is greater
            // than or equal to the end of the DATA segment or the number of
            // events is equal to 0, then create an empty array of events.
            eventList = new double[0][parameters];

            return;
        }

        // Open a file input stream to the file
        FileInputStream fis = new FileInputStream(file);

        // Get the channel for the file
        FileChannel fc = fis.getChannel();

        // Map the DATA segment to memory
        MappedByteBuffer data;

        try {
            data = fc.map(FileChannel.MapMode.READ_ONLY, dataStart, dataEnd - dataStart + 1);
        } catch (Throwable t) {
            // Try again with a workaround to see if we can compensate for off-by-one errors that
            // some FCS files have been known to incorporate in the ENDDATA property.
            data = fc.map(FileChannel.MapMode.READ_ONLY, dataStart, dataEnd - dataStart);
        }

        /**
         * We don't need to worry about endian-ness here since ASCII is one
         * byte, and float and double are IEEE standards.
         */
        if (dataType != null) {
            if (dataType.equalsIgnoreCase("I")) {
                // If the data type is "I", then it is binary integer.
                readBinIntData(data);
            } else if (dataType.equalsIgnoreCase("F")) {
                // If the data type is "F", then it is floating point.
                readFloatData(data);
            } else if (dataType.equalsIgnoreCase("D")) {
                // If the data type is "D", then it is double precision floating
                // point
                readDoubleData(data);
            } else if (dataType.equalsIgnoreCase("A")) {
                // If the data type is "A", then it is ASCII.
                readASCIIData(data);
            }
        }

        // Close the file channel
        fc.close();

        // Close the file input stream
        fis.close();
    }

    /**
     * readBinIntData ---
     * <p>
     * Reads binary integers in list mode in the DATA segment and updates
     * eventList.
     * </p>
     *
     * <p>
     * Assumes that the bits for the values are byte-aligned. It needs to be
     * fixed if that is not the case.
     * </p>
     *
     * @param data
     *            <code>ByteBuffer</code> containing the DATA segment of the
     *            underlying file.
     */
    private void readBinIntData(ByteBuffer data) {
        // Allocate the eventList
        eventList = new double[parameters][totalEvents];

        int numBytes, value, range, currByte;

        final int totalEvents = this.totalEvents;
        final int parameters = this.parameters;

        for (int i = 0; i < totalEvents; i++) {
            for (int j = 0; j < parameters; j++) {
                // Calculate the number of bytes used from the number of bits
                // used
                // Round up to the next full byte
                numBytes = (int) Math.ceil(((double) channelBits[j]) / Byte.SIZE);

                // Get the range of the current parameter - will let us build
                // the mask
                range = (int) channelRange[j];

                // Initialize the current value to 0
                value = 0;

                if (littleEndianP) {
                    // If the byte order is little endian, then build the value
                    // to the left.
                    for (int k = 0; k < numBytes; k++) {
                        // Get the next 8 bits masking to make sure Java doesn't
                        // prepend 1's
                        currByte = (data.get() & 0xFF);

                        // Shift the 8 bits into position
                        currByte <<= (8 * k);

                        // Or the 8 bits with the current value
                        value |= currByte;
                    }
                } else {
                    // Otherwise, the byte order is big endian, so build the
                    // value to the right.
                    for (int k = 0; k < numBytes; k++) {
                        // Left shift the previous bits in value to make room
                        value <<= 8;

                        // Grab the next 8 bits masking to make sure Java
                        // doesn't prepend 1's
                        value |= (data.get() & 0xFF);
                    }
                }

                /**
                 * From the FCS specification: --- The remaining bits are
                 * usually unused and set to "0"; however, some file writers
                 * store non-data information in that bit-space. Implementers
                 * must use a bit mask when reading these list mode parameter
                 * values to insure that erroneous values are not read from the
                 * unused bits.
                 */
                // Mask the value based on the range
                value &= (range - 1);

                // Store the value into the array
                eventList[j][i] = value;
            }
        }
    }

    /**
     * readFloatData ---
     * <p>
     * Reads floating point values in list mode in the DATA segment and updates
     * eventList with the integer values of the values.
     * </p>
     *
     * @param data
     *            <code>ByteBuffer</code> containing the DATA segment of the
     *            underlying file.
     */
    private void readFloatData(ByteBuffer data) {
        // Allocate the eventList
        eventList = new double[parameters][totalEvents];

        if (littleEndianP) {
            data.order(ByteOrder.LITTLE_ENDIAN);
        }

        // Convert the byte buffer into a float buffer - doesn't get any easier
        FloatBuffer fb = data.asFloatBuffer();

        final int totalEvents = this.totalEvents;
        final int parameters = this.parameters;

        for (int i = 0; i < totalEvents; i++) {
            for (int j = 0; j < parameters; j++) {
                // Store the value into the array
                eventList[j][i] = fb.get();
            }
        }
    }

    /**
     * readDoubleData ---
     * <p>
     * Reads double precision floating point values in list mode in the DATA
     * segment and updates eventList with the integer values of the values.
     * </p>
     *
     * @param data
     *            <code>ByteBuffer</code> containing the DATA segment of the
     *            underlying file.
     */
    private void readDoubleData(ByteBuffer data) {
        // Allocate the eventList
        eventList = new double[parameters][totalEvents];

        if (littleEndianP) {
            data.order(ByteOrder.LITTLE_ENDIAN);
        }

        // Convert the byte buffer into a double buffer - doesn't get any easier
        DoubleBuffer db = data.asDoubleBuffer();

        final int totalEvents = this.totalEvents;
        final int parameters = this.parameters;

        for (int i = 0; i < totalEvents; i++) {
            for (int j = 0; j < parameters; j++) {
                // Store the value into the array
                eventList[j][i] = db.get();
            }
        }
    }

    /**
     * readASCIIData ---
     * <p>
     * Reads ASCII values in list mode in the DATA segment and updates eventList
     * with the integer values of the values.
     * </p>
     *
     * @param data
     *            <code>ByteBuffer</code> containing the DATA segment of the
     *            underlying file.
     */
    private void readASCIIData(ByteBuffer data) {
        /**
         * Calculate the number of characters in each event of the flow file
         */
        // Initialize the number of characters in each event to 0
        int numCharsPerEvent = 0;

        // Loop through all the parameters adding the number of characters in
        // each parameter
        for (int i = 0; i < parameters; i++) {
            numCharsPerEvent += channelBits[i];
        }

        // Allocate the eventList
        eventList = new double[parameters][totalEvents];

        // Convert the byte buffer into a char buffer - doesn't get any easier
        CharBuffer cb = data.asCharBuffer();

        // Initialize the current character to 0
        int currChar = 0;

        for (int i = 0; i < totalEvents; i++) {
            for (int j = 0; j < parameters; j++) {
                try {
                    // Store the value into the array
                    eventList[j][i] = Integer
                            .parseInt(cb.subSequence(currChar, currChar + channelBits[i]).toString());
                } catch (NumberFormatException nfe) {
                    eventList[j][i] = 0;
                }

                // Increment the current character
                currChar += channelBits[i];
            }
        }
    }

    /**
     * getEventList ---
     * <p>
     * Returns the event list.
     * <p>
     *
     * @return array of double arrays containing the events.
     */
    public double[][] getEventList() {
        if (eventList == null) {
            // If the array of events is null, then try to extract the events
            // from the FCS file.
            try {
                // Try to extract the events
                extractEvents();
            } catch (FileNotFoundException fnfe) {
                // If a FileNotFoundException occurred, then return an empty
                // array of events.
                return new double[0][0];
            } catch (IOException ioe) {
                // If a IOException occurred, then return an empty array of
                // events.
                return new double[0][0];
            }
        }

        // Return the array of events
        return eventList;
    }

    /**
     * getCompensatedEventList ---
     * <p>
     * Returns the event list compensated by the SPILL matrix.
     * <p>
     *
     * @return array of double arrays containing the events.
     */
    public double[][] getCompensatedEventList() {
        double[][] events = this.getEventList();
        if (events.length != this.getNumChannels())
            return events; // Unable to extract the underlying events

        // Convert the SPILL string to a compensation matrix
        String compString = this.getSpillString();
        if (compString == null)
            return events; // No compensation, just return the events

        // Split the compensation string into its values
        //
        // The basic structure for SPILL* is:
        // $SPILLOVER/n,string1,string2,...,f1,f2,f3,f4,.../

        String[] compValues = compString.split(",");
        String[] compNames = null;
        String[] compData = null;
        int compDataStart = 0;

        int n = 0;
        try {
            // Try to parse the number of acquisition parameters
            n = Integer.parseInt(compValues[0]);
            if (n <= 0 || n > this.parameters)
                throw new NumberFormatException();
        } catch (NumberFormatException nfe) {
            //CyLogger.getLogger().error("Failed to parse parameter count in spill string",nfe);
            return events;
        }

        compNames = Arrays.copyOfRange(compValues, 1, n + 1);

        // Match names in spill string to columns in parameter lists
        compDataStart = Arrays.asList(this.channelShortname).indexOf(compNames[0]);
        if (compDataStart < 0) {
            //CyLogger.getLogger().error("Failed to match channel "+compNames[0]+" to parameter in file");
            return events; // Failure match spill string names to channels
        }
        for (int i = 0; i < n; i++) {
            if (!compNames[i].equals(this.channelShortname[compDataStart + i])) {
                //CyLogger.getLogger().error("Spill channel are not continguous parameters in file");
                return events; // Spill string columns not in order
            }
        }

        // Extract actual compensation data
        compData = Arrays.copyOfRange(compValues, n + 1, compValues.length);
        if (compData.length != (n * n))
            return events;

        /**
         * Populate the compensation matrix --- The values are stored in
         * row-major order, i.e., the elements in the first row appear
         * first.
         */
        double[][] matrix = new double[n][n];

        // Loop through the array of compensation values
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                try {
                    matrix[i][j] = Double.parseDouble(compData[i * n + j]);
                } catch (NumberFormatException nfe) {
                    // Set default value If a NumberFormatException occurred
                    matrix[i][j] = 0.0d;
                }
            }
        }

        // Compute the inverse of the compensation data and then apply
        // to data matrix (which is column major). Specifically compute
        // transpose(inverse(<SPILL MATRIX>)) * data
        RealMatrix comp = (new LUDecompositionImpl(new Array2DRowRealMatrix(matrix))).getSolver().getInverse();
        RealMatrix data = new BlockRealMatrix(events);
        data.setSubMatrix( // Update compensated portion of data matrix
                comp.transpose()
                        .multiply(data.getSubMatrix(compDataStart, compDataStart + n - 1, 0,
                                this.getEventCount() - 1))
                        .getData(),

                compDataStart, 0);
        return data.getData();
    }

    /**
     * Accessors
     */
    /**
     * isFCS ---
     * <p>
     * Returns whether the file is an FCS file.
     * </p>
     *
     * <p>
     * Note: All the public fields are valid if and only if the file is an FCS
     * file.
     * </p>
     *
     * @return boolean flag indicating whether the file is an FCS file.
     */
    public boolean isFCS() {
        return isFCSP;
    }

    /**
     * getVersion ---
     * <p>
     * Returns the FCS version string of the underlying FCS file.
     * </p>
     *
     * @return <code>String</code> FCS version string of the underlying FCS
     *         file.
     */
    public String getVersion() {
        return version;
    }

    public int getTextStart() {
        return textStart;
    }

    public int getTextEnd() {
        return textEnd;
    }

    public int getDataStart() {
        return dataStart;
    }

    public int getDataEnd() {
        return dataEnd;
    }

    public int getAnalysisStart() {
        return analysisStart;
    }

    public int getAnalysisEnd() {
        return analysisEnd;
    }

    public char getDelimiter() {
        return delimiter;
    }

    public String getText() {
        return text;
    }

    public String getCytometer() {
        return cytometer;
    }

    /**
     * getChannelHelper ---
     * <p>
     * Returns the channel number of the forward scatter channel if sideScatterP
     * is false or the side scatter channel if sideScatterP is true or -1 if it
     * is not found.
     * </p>
     *
     * <p>
     * For forward scatter, it looks in all the channel names for the channel
     * whose name is "Forward Scatter" or whose channel name or shortname starts
     * with "FS".
     * </p>
     *
     * <p>
     * For side scatter, it looks in all the channel names for the channel whose
     * name is "Side Scatter" or whose channel name or shortname starts with
     * "SS".
     * </p>
     *
     * @return int channel number of the forward scatter channel if sideScatterP
     *         is false or the side scatter channel if sideScatterP is true or
     *         -1 if it is not found.
     */
    private int getChannelHelper(boolean sideScatterP) {
        if ((channelName == null) || (channelShortname == null)) {
            // If the channel name array or the channel shortname array is null,
            // then quit, since they have not been initialized suggesting
            // something went wrong in the TEXT parsing.
            return -1;
        }

        // Loop through all the channels
        for (int i = 0; i < parameters; i++) {
            if (channelName[i] == null) {
                // If either the channel name is null, then there is nothing we
                // can do with the channel, so skip to the next channel.
                continue;
            } else if (sideScatterP) {
                // If sideScatterP, then try to find the side scatter channel.
                if (channelName[i].indexOf("Side Scatter") >= 0) {
                    // If the channel name contains "Side Scatter", then we are
                    // pretty sure the channel is the side scatter channel, so
                    // return the channel number.
                    return i;
                } else if (channelName[i].startsWith("SS")
                        || ((channelShortname[i] != null) && channelName[i].startsWith("SS"))) {
                    // If the channel name starts with "SS" or the channel
                    // shortname starts with "SS", then we less certain the
                    // channel is side scatter, but it probably is, so return
                    // the channel number.
                    // This part can be changed.
                    return i;
                }
            } else {
                // Otherwise, try to find the forward scatter channel.
                if (channelName[i].indexOf("Forward Scatter") >= 0) {
                    // If the channel name contains "Forward Scatter", then we
                    // are pretty sure the channel is the forward scatter
                    // channel, so return the channel number.
                    return i;
                } else if (channelName[i].startsWith("FS")
                        || ((channelShortname[i] != null) && channelName[i].startsWith("FS"))) {
                    // If the channel name starts with "FS" or the channel
                    // shortname starts with "FS", then we less certain the
                    // channel is forward scatter, but it probably is, so return
                    // the channel number.
                    // This part can be changed.
                    return i;
                }
            }
        }

        // At this point, we couldn't determine which channel was Side Scatter,
        // so return -1.
        return -1;
    }

    /**
     * getForwardScatterChannel ---
     * <p>
     * Returns the channel number of the forward scatter channel or -1 if it is
     * not found.
     * </p>
     *
     * @return int channel number of the forward scatter channel or -1 if it is
     *         not found.
     */
    public int getForwardScatterChannel() {
        return getChannelHelper(false);
    }

    /**
     * getSideScatterChannel ---
     * <p>
     * Returns the channel number of the side scatter channel or -1 if it is not
     * found.
     * </p>
     *
     * @return int channel number of the side scatter channel or -1 if it is not
     *         found.
     */
    public int getSideScatterChannel() {
        return getChannelHelper(true);
    }

    /**
     * getChannelShortName ---
     * <p>
     * Returns the $PN of the channel with number channelNumber.
     * </p>
     *
     * @param channelNumber
     *            int number of the channel whose name to return.
     * @return <code>String</code> short name of the channel with number
     *         channelNumber.
     */
    public String getChannelShortName(int channelNumber) {
        if ((channelShortname == null) || (channelNumber < 0) || (channelNumber >= getNumChannels())) {
            // If the channel name array or the channel shortname array is null
            // or the channel number is invalid, then quit, since they have not
            // been initialized suggesting something went wrong in the TEXT
            // parsing.
            return "Error reading this channel!";
        }

        if (channelShortname[channelNumber] == null) {
            // If the channel short name for the channel is also null, then
            // return "Channel #" as the channel name.
            return ("Channel " + channelNumber);
        } else {
            // Otherwise, return the channel short name as the channel name.
            return channelShortname[channelNumber];
        }
    }

    /**
     * getChannelIdFromShortName
     *
     */
    public int getChannelIdFromShortName(String name) {
        for (int i = 0; i < channelShortname.length; i++)
            if (channelShortname[i].equals(name))
                return i;
        return -1;
    }

    /**
     * getChannelName ---
     * <p>
     * Returns the name of the channel with number channelNumber.
     * </p>
     *
     * <p>
     * I do this way too often in the code. I don't know why I didn't realize
     * that a method is needed sooner.
     * </p>
     *
     * @param channelNumber
     *            int number of the channel whose name to return.
     * @return <code>String</code> name of the channel with number
     *         channelNumber.
     */
    public String getChannelName(int channelNumber) {
        if ((channelName == null) || (channelShortname == null) || (channelNumber < 0)
                || (channelNumber >= getNumChannels())) {
            // If the channel name array or the channel shortname array is null
            // or the channel number is invalid, then quit, since they have not
            // been initialized suggesting something went wrong in the TEXT
            // parsing.
            return "Error reading this channel!";
        }

        if (channelName[channelNumber] == null) {
            // If the channel name for the channel is null, then check the
            // channel short name.
            if (channelShortname[channelNumber] == null) {
                // If the channel short name for the channel is also null, then
                // return "Channel #" as the channel name.
                return ("Channel " + channelNumber);
            } else {
                // Otherwise, return the channel short name as the channel name.
                return channelShortname[channelNumber];
            }
        } else {
            // Otherwise, return the channel name as the channel name.
            return channelName[channelNumber];
        }
    }

    /**
     * getChannelCount ---
     * <p>
     * Returns the number of channels in the flow file.
     * </p>
     *
     * @return int number of channels in the flow file.
     */
    public int getChannelCount() {
        return parameters;
    }

    /**
     * getNumChannels ---
     * <p>
     * Returns the number of channels in the flow file.
     * </p>
     *
     * @return int number of channels in the flow file.
     */
    public int getNumChannels() {
        return getChannelCount();
    }

    /**
     * isLog ---
     * <p>
     * Returns a boolean flag indicating whether the channel with channel number
     * channelNumber is stored in log format.
     * </p>
     *
     * @param channelNumber
     *            int number of the channel.
     * @return boolean flag indicating whether the channel with channel number
     *         channelNumber is stored in log format.
     */
    public boolean isLog(int channelNumber) {
        if ((isLog != null) && (channelNumber >= 0) && (channelNumber < isLog.length)) {
            // If the isLog array is not null and the channel number is in the
            // range of the array,
            // then return the value at channelNumber in the isLog array.
            return isLog[channelNumber];
        }

        // Otherwise, return false.
        return false;
    }

    /**
     * getIsLog ---
     * <p>
     * Returns the <code>boolean</code> status of whether the given channel is
     * stored in log format.
     * </p>
     *
     * @param channelNumber
     *            int number of the channel.
     * @return boolean flag indicating whether the channel with channel number
     *         channelNumber is stored in log format.
     */
    public boolean getIsLog(int channelNumber) {
        return isLog(channelNumber);
    }

    /**
     * isDisplayLog ---
     * <p>
     * Returns a boolean flag indicating whether the channel with channel number
     * channelNumber should be displayed in log format.
     * </p>
     *
     * @param channelNumber
     *            int number of the channel.
     * @return boolean flag indicating whether the channel with channel number
     *         channelNumber should be displayed in log format.
     */
    public boolean isDisplayLog(int channelNumber) {
        if ((displayLog != null) && (channelNumber >= 0) && (channelNumber < displayLog.length)) {
            // If the displayLog array is not null and the channel number is in
            // the range of the array,
            // then return the value at channelNumber in the displayLog array.
            return displayLog[channelNumber];
        }

        // Otherwise, return false.
        return false;
    }

    /**
     * getDisplayLog ---
     * <p>
     * Returns the <code>boolean</code> status of whether the given channel
     * should be displayed in log format.
     * </p>
     *
     * @param channelNumber
     *            int number of the channel.
     * @return boolean flag indicating whether the channel with channel number
     *         channelNumber should be displayed in log format.
     */
    public boolean getDisplayLog(int channelNumber) {
        return isDisplayLog(channelNumber);
    }

    /**
     * getAmpValue ---
     * <p>
     * Returns the <code>int</code> log amplifier value.
     * </p>
     *
     * @return <code>double</code> log amplifier value.
     * @param channelNumber
     *            <code>int</code> the number index of the channel in the array
     */
    public double getAmpValue(int channelNumber) {
        if (ampValue != null && channelNumber >= 0 && channelNumber < ampValue.length) {
            return ampValue[channelNumber];
        }
        return 0;
    }

    /**
     * getChannelRange ---
     * <p>
     * Returns the <code>int</code> range of the given channel (before any log
     * amp, as stored in the fcs file).
     * </p>
     *
     * @return <code>int</code> range of the given channel.
     * @param channelNumber
     *            <code>int</code> the number index of the channel in the array
     */
    public double getChannelRange(int channelNumber) {
        if (channelRange != null && channelNumber >= 0 && channelNumber < channelRange.length) {
            return channelRange[channelNumber];
        }
        return 0;
    }

    /**
     * getScaleRange ---
     * <p>
     * Returns the <code>int</code> scale range of the given channel (after any
     * log amp).
     * </p>
     *
     * @return <code>int</code> range of the given channel.
     * @param channelNumber
     *            <code>int</code> the number index of the channel in the array
     */
    public double getScaleRange(int channelNumber) {
        if (getIsLog(channelNumber)) {
            return Math.pow(10, getAmpValue(channelNumber));
        } else {
            return getChannelRange(channelNumber);
        }
    }

    /**
     * getAmpDisplay ---
     * <p>
     * Returns <code>boolean</code> need to log amplify the channel display or
     * not.
     * </p>
     *
     * @return <code>boolean</code> of whether to log amplify or not.
     * @param channelNumber
     *            <code>int</code> the number index of the channel in the array
     */
    public boolean getAmpDisplay(int channelNumber) {
        // If the channel is not log but is being displayed as log (i.e. LSR II
        // data)
        if (!getIsLog(channelNumber) && getDisplayLog(channelNumber)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * getSettings ---
     * <p>
     * Returns the <code>Properties</code> object containing all the key/value
     * pairs containing all the settings of the FCS file.
     * </p>
     *
     * @return <code>Properties</code> object containing all the key/value pairs
     *         containing all the settings of the FCS file.
     */
    public Properties getSettings() {
        return settings;
    }

    /**
     * getEventCount ---
     * <p>
     * Returns the number of events in the flow file.
     * </p>
     *
     * @return int number of events in the flow file.
     */
    public int getEventCount() {
        return totalEvents;
    }

    /**
     * getSpillString ---
     * <p>
     * Returns the spill string of the flow file.
     * </p>
     *
     * @return <code>String</code> spill string of the flow file.
     */
    public String getSpillString() {
        return spillString;
    }

    /**
     * getFile ---
     * <p>
     * Returns a <code>File</code> object corresponding to the underlying file.
     * </p>
     *
     * @return <code>File</code> object corresponding to the underlying file.
     */
    public File getFile() {
        return file;
    }

    /**
     * getPath ---
     * <p>
     * Returns the <code>String</code> path to the underlying file.
     * </p>
     *
     * @return <code>String</code> path to the underlying file.
     */
    public String getPath() {
        return file.getPath();
    }

    /**
     * getName ---
     * <p>
     * Returns the <code>String</code> name of the underlying file.
     * </p>
     *
     * @return <code>String</code> name of the underlying file.
     */
    public String getName() {
        return file.getName();
    }

    /**
     * length ---
     * <p>
     * Returns the length of the underlying file.
     * </p>
     *
     * @return long length of the underlying file.
     */
    public long length() {
        return file.length();
    }

    /**
     * isFCSFile ---
     * <p>
     * A static helper method to determine whether the file in the
     * <code>java.io.File</code> object file is a FCS file.
     * </p>
     *
     * @param file
     *            <code>java.io.File</code> object to the file to test.
     * @return boolean flag indicating whether the file in the
     *         <code>java.io.File</code> object file is a FCS file.
     */
    public static boolean isFCSFile(File file) throws FileNotFoundException, IOException {
        if ((file == null) || (!file.exists()) || (!file.isFile())) {
            // If the file is null, does not exist, or is not a file, then
            // return false.
            return false;
        }

        // Open a file input stream to the file
        FileInputStream fis = new FileInputStream(file);

        // Create a byte array to hold the version
        byte[] versionArray = new byte[VERSION_SIZE];

        // Read the version into the byte array
        int numRead = fis.read(versionArray);

        // Close the file input stream
        fis.close();

        if (numRead < VERSION_SIZE) {
            // If the number of bytes read is less than the number of bytes in
            // the version string, then the file is too small to be an FCS file.
            return false;
        } else {
            // Otherwise, at least 6 bytes were read, so decode it and determine
            // whether the file is an FCS file.
            // Decode the version using the default encoding
            String version = new String(versionArray);

            // Determine whether the file is an FCS file by whether the version
            // string starts with the FCS_PREFIX
            return version.startsWith(FCS_PREFIX);
        }
    }

    /**
     * Testing Code ---
     * <p>
     * The functions below are static functions used to test the fcsFile class.
     * </p>
     *
     * <p>
     * Different testing functions to exercise different parts of the class.
     * </p>
     */
    /**
     * printFCSFile ---
     * <p>
     * Prints all the fields of <code>fcsFile</code> f to standard output - for
     * testing.
     * <p>
     *
     * @param f
     *            <code>fcsFile</code> object to print to standard output.
     */
    public static void printFCSFile(fcsFile f) {
        System.out.println("Version ID -=" + f.version + "=-");
        System.out.println("Text starts at -=" + f.textStart + "=-");
        System.out.println("Text ends at -=" + f.textEnd + "=-");
        System.out.println("Data starts at -=" + f.dataStart + "=-");
        System.out.println("Data ends at -=" + f.dataEnd + "=-");
        System.out.println("Analysis starts at -=" + f.analysisStart + "=-");
        System.out.println("Analysis ends at -=" + f.analysisEnd + "=-");
        System.out.println("Text delimiter -=" + Character.toString(f.delimiter) + "=-");
        System.out.println("Data type -=" + f.dataType + "=-");
        System.out.println("Cytometer -=" + f.cytometer + "=-");
        System.out.println("Lasers -=" + f.lasers + "=-");
        /*
         * System.out.println("LaserName (Count: " + f.laserASF.length + "):");
         * for(int i = 0; i < f.laserASF.length; i++) {
         * System.out.println("\t::" + f.laserASF[i]); }
         *
         * System.out.println("LaserASF (Count: " + f.laserName.length + "):");
         * for(int i = 0; i < f.laserName.length; i++) {
         * System.out.println("\t::" + f.laserName[i]); }
         *
         * System.out.println("LaserDelay (Count: " + f.laserDelay.length +
         * "):"); for(int i = 0; i < f.laserDelay.length; i++) {
         * System.out.println("\t::" + f.laserDelay[i]); }
         *
         * System.out.println("MODE -=" + f.mode + "=-");
         * System.out.println("Instrument -=" + f.instrument + "=-");
         * System.out.println("Experiment Time -=" + f.expTime + "=-");
         * System.out.println("Experiment File Name -=" + f.expFile + "=-");
         * System.out.println("Operator -=" + f.operatorName + "=-");
         * System.out.println("OS -=" + f.operatingSystem + "=-");
         * System.out.println("Date -=" + f.experimentDate + "=-");
         * System.out.println("Creator Software -=" + f.creatorSoftware + "=-");
         * System.out.println("Cytometer Number -=" + f.cytometerNumber + "=-");
         * System.out.println("Experiment Name -=" + f.experimentName + "=-");
         * System.out.println("Export Time -=" + f.exportTime + "=-");
         * System.out.println("Export User -=" + f.exportUser + "=-");
         * System.out.println("GUID -=" + f.GUID + "=-");
         * System.out.println("Window Extension -=" + f.windowExtension + "=-");
         * System.out.println("Threshold -=" + f.threshold + "=-");
         * System.out.println("Spill String -=" + f.spillString + "=-");
         * System.out.println("Tube Name -=" + f.tubeName + "=-");
         * System.out.println("Time Step -=" + f.timeStep + "=-");
         * System.out.println("Source -=" + f.source + "=-");
         * System.out.println("Text Ends -=" + f.endsText + "=-");
         * System.out.println("Next Data -=" + f.nextData + "=-");
         * System.out.println("B Time -=" + f.bTime + "=-");
         * System.out.println("Apply Compensation -=" + f.applyCompensation +
         * "=-"); System.out.println("Sample name -=" + f.sampleName + "=-");
         * System.out.println("Parameters -=" + f.parameters + "=-");
         * System.out.println("Total events -=" + f.totalEvents + "=-");
         *
         * System.out.println("Channel names (Count: " + f.channelName.length +
         * "):"); for(int i = 0; i < f.channelName.length; i++) {
         * System.out.println("\t::" + f.channelName[i]); }
         *
         * System.out.println("Channel short names (Count: " +
         * f.channelShortname.length + "):"); for(int i = 0; i <
         * f.channelShortname.length; i++) { System.out.println("\t::" +
         * f.channelShortname[i]); }
         *
         * System.out.println("Channel gains (Count: " + f.channelGain.length +
         * "):"); for(int i = 0; i < f.channelGain.length; i++) {
         * System.out.println("\t::" + f.channelGain[i]); }
         *
         * System.out.println(
         * "Channel amps (4=log, 0=linear -- aka the number of decades on the plot) (Count: "
         * + f.channelAmp.length + "):"); for(int i = 0; i <
         * f.channelAmp.length; i++) { System.out.println("\t::" +
         * f.channelAmp[i]); }
         *
         * System.out.println("Therefore, are channels log? (Count: " +
         * f.isLog.length + "):"); for(int i = 0; i < f.isLog.length; i++) {
         * System.out.println("\t::" + f.isLog[i]); }
         *
         * System.out.println("Display as log? (Count: " + f.displayLog.length +
         * "):"); for(int i = 0; i < f.displayLog.length; i++) {
         * System.out.println("\t::" + f.displayLog[i]); }
         *
         * System.out.println("Amp values (Count: " + f.ampValue.length + "):");
         * for(int i = 0; i < f.ampValue.length; i++) {
         * System.out.println("\t::" + f.ampValue[i]); }
         *
         * System.out.println("Channel bits (Count: " + f.channelBits.length +
         * "):"); for(int i = 0; i < f.channelBits.length; i++) {
         * System.out.println("\t::" + f.channelBits[i]); }
         *
         * System.out.println(
         * "Channel ranges (maximum number of bins; 1024 on Calibur; 262144 on LSRII) (Count: "
         * + f.channelRange.length + "):"); for(int i = 0; i <
         * f.channelRange.length; i++) { System.out.println("\t::" +
         * f.channelRange[i]); }
         *
         * System.out.println(
         * "Channel voltages (not working for Calibur yet) (Count: " +
         * f.channelVoltage.length + "):"); for(int i = 0; i <
         * f.channelVoltage.length; i++) { System.out.println("\t::" +
         * f.channelVoltage[i]); }
         *
         * System.out.println("All settings key / value pairs:");
         * //f.getSettings().list(System.out);
         *
         * // Get the settings Properties settings = f.getSettings();
         *
         * // Get all the keys Enumeration keys = settings.propertyNames();
         *
         * // Create a list to hold all the keys ArrayList keysList = new
         * ArrayList();
         *
         * String key;
         *
         * // Loop through all the keys getting their values
         * while(keys.hasMoreElements()) { // Get the current key key =
         * (String)keys.nextElement();
         *
         * // Add the current key to the list of keys keysList.add(key); }
         *
         * if(keysList.size() > 1) { // If there are more than one key in the
         * list of keys, then sort it. Collections.sort(keysList); }
         *
         * // Loop through the list of keys for(int i = 0; i < keysList.size();
         * i++) { // Get the current key key = (String)keysList.get(i);
         *
         * // Print the key/value pair System.out.print(key);
         * System.out.print("="); System.out.println(settings.getProperty(key));
         * }
         */

        // System.out.println("The entire text section (unformatted) -=" +
        // f.text + "=-");
        // Print out the first 50 events, if there are any
        f.printEvents(50);
    }

    /**
     * printEvents ---
     * <p>
     * Prints the first numEvents events to standard output for testing.
     * </p>
     *
     * @param numEvents
     *            int number of events to print.
     */
    private void printEvents(int numEvents) {
        if (eventList == null) {
            // If the eventList was not populated, then simply quit.
            return;
        }

        System.out.println("The events (Count: " + numEvents + "):");

        // Print out all the channel short names
        for (int i = 0; i < parameters; i++) {
            System.out.print("\t" + channelShortname[i]);
        }
        System.out.println();

        // Loop through the first numEvents
        for (int i = 0; i < numEvents; i++) {
            System.out.print("Event " + (i + 1) + ":");

            // Loop through all the parameters for that event
            for (int j = 0; j < parameters; j++) {
                // Print out all the parameters for that event
                System.out.print("\t" + eventList[i][j]);
            }

            System.out.println();
        }
    }

    /**
     * main ---
     * <p>
     * A main method to test the class.
     * </p>
     *
     * @param args
     *            <code>String</code> array of arguments at the command prompt.
     */
    public static void main(String[] args) {
        if (args.length <= 0) {
            // If there are no arguments, then exit.
            System.out.println("Usage:");
            System.out.println("---");
            System.out.println(">java fcsFile <path to FCS file>");

            System.exit(0);
        }

        try {
            // Echo the file we are checking
            System.out.println("Checking \"" + args[0] + "\" ...");

            fcsFile f;

            // Read in the file
            if (args.length > 1) {
                // If there are more than one argument, then extract the events.
                f = new fcsFile(args[0], true);
            } else {
                // Otherwise, don't extract the events.
                f = new fcsFile(args[0], false);
            }

            // Test whether the file is an FCS file
            if (f.isFCS()) {
                // If the file is an FCS file, then print out the file is an FCS
                // file.
                System.out.println("\"" + args[0] + "\" is an FCS file.");

                // Print out the information if the file is an FCS file
                printFCSFile(f);
            } else {
                // Otherwise, print out the file is not an FCS file.
                System.out.println("\"" + args[0] + "\" is not an FCS file.");
            }

            /*
             * // Print out whether the file is an FCS file using the static
             * method if(isFCSFile(f.getFile())) { // If the file is an FCS
             * file, then print out the file is an FCS file.
             * System.out.println("\"" + args[0] + "\" is an FCS file."); } else
             * { // Otherwise, print out the file is not an FCS file.
             * System.out.println("\"" + args[0] + "\" is not an FCS file."); }
             */
        } catch (Exception e) {
            // Print out any exceptions
            System.err.println(e.toString());
        }
    }
}